Compare commits
No commits in common. "3ca89a8479be333303eaf95b2e6f7304f809b2a1" and "76075f146ce251608cfc54fb94ce0be332e2849d" have entirely different histories.
3ca89a8479
...
76075f146c
|
@ -1,6 +0,0 @@
|
||||||
|
|
||||||
``sideshow.app``
|
|
||||||
================
|
|
||||||
|
|
||||||
.. automodule:: sideshow.app
|
|
||||||
:members:
|
|
|
@ -1,6 +0,0 @@
|
||||||
|
|
||||||
``sideshow.db.model.stores``
|
|
||||||
============================
|
|
||||||
|
|
||||||
.. automodule:: sideshow.db.model.stores
|
|
||||||
:members:
|
|
|
@ -1,6 +0,0 @@
|
||||||
|
|
||||||
``sideshow.web.views.stores``
|
|
||||||
=============================
|
|
||||||
|
|
||||||
.. automodule:: sideshow.web.views.stores
|
|
||||||
:members:
|
|
|
@ -30,7 +30,6 @@ For an online demo see https://demo.wuttaproject.org/
|
||||||
:caption: Package API:
|
:caption: Package API:
|
||||||
|
|
||||||
api/sideshow
|
api/sideshow
|
||||||
api/sideshow.app
|
|
||||||
api/sideshow.batch
|
api/sideshow.batch
|
||||||
api/sideshow.batch.neworder
|
api/sideshow.batch.neworder
|
||||||
api/sideshow.cli
|
api/sideshow.cli
|
||||||
|
@ -44,7 +43,6 @@ For an online demo see https://demo.wuttaproject.org/
|
||||||
api/sideshow.db.model.customers
|
api/sideshow.db.model.customers
|
||||||
api/sideshow.db.model.orders
|
api/sideshow.db.model.orders
|
||||||
api/sideshow.db.model.products
|
api/sideshow.db.model.products
|
||||||
api/sideshow.db.model.stores
|
|
||||||
api/sideshow.enum
|
api/sideshow.enum
|
||||||
api/sideshow.orders
|
api/sideshow.orders
|
||||||
api/sideshow.web
|
api/sideshow.web
|
||||||
|
@ -60,4 +58,3 @@ For an online demo see https://demo.wuttaproject.org/
|
||||||
api/sideshow.web.views.customers
|
api/sideshow.web.views.customers
|
||||||
api/sideshow.web.views.orders
|
api/sideshow.web.views.orders
|
||||||
api/sideshow.web.views.products
|
api/sideshow.web.views.products
|
||||||
api/sideshow.web.views.stores
|
|
||||||
|
|
|
@ -51,9 +51,6 @@ sideshow_libcache = "sideshow.web.static:libcache"
|
||||||
[project.entry-points."paste.app_factory"]
|
[project.entry-points."paste.app_factory"]
|
||||||
"main" = "sideshow.web.app:main"
|
"main" = "sideshow.web.app:main"
|
||||||
|
|
||||||
[project.entry-points."wutta.app.providers"]
|
|
||||||
sideshow = "sideshow.app:SideshowAppProvider"
|
|
||||||
|
|
||||||
[project.entry-points."wutta.batch.neworder"]
|
[project.entry-points."wutta.batch.neworder"]
|
||||||
"sideshow" = "sideshow.batch.neworder:NewOrderBatchHandler"
|
"sideshow" = "sideshow.batch.neworder:NewOrderBatchHandler"
|
||||||
|
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# Sideshow -- Case/Special Order Tracker
|
|
||||||
# Copyright © 2024 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of Sideshow.
|
|
||||||
#
|
|
||||||
# Sideshow is free software: you can redistribute it and/or modify it
|
|
||||||
# under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Sideshow is distributed in the hope that it will be useful, but
|
|
||||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Sideshow. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
Sideshow app provider
|
|
||||||
"""
|
|
||||||
|
|
||||||
from wuttjamaican import app as base
|
|
||||||
|
|
||||||
|
|
||||||
class SideshowAppProvider(base.AppProvider):
|
|
||||||
"""
|
|
||||||
The :term:`app provider` for Sideshow.
|
|
||||||
|
|
||||||
This adds the :meth:`get_order_handler()` method to the :term:`app
|
|
||||||
handler`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_order_handler(self, **kwargs):
|
|
||||||
"""
|
|
||||||
Get the configured :term:`order handler` for the app.
|
|
||||||
|
|
||||||
You can specify a custom handler in your :term:`config file`
|
|
||||||
like:
|
|
||||||
|
|
||||||
.. code-block:: ini
|
|
||||||
|
|
||||||
[sideshow]
|
|
||||||
orders.handler_spec = poser.orders:PoserOrderHandler
|
|
||||||
|
|
||||||
:returns: Instance of :class:`~sideshow.orders.OrderHandler`.
|
|
||||||
"""
|
|
||||||
if 'order_handler' not in self.__dict__:
|
|
||||||
spec = self.config.get('sideshow.orders.handler_spec',
|
|
||||||
default='sideshow.orders:OrderHandler')
|
|
||||||
self.order_handler = self.app.load_object(spec)(self.config)
|
|
||||||
return self.order_handler
|
|
|
@ -26,7 +26,6 @@ New Order Batch Handler
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
@ -51,14 +50,6 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
"""
|
"""
|
||||||
model_class = NewOrderBatch
|
model_class = NewOrderBatch
|
||||||
|
|
||||||
def get_default_store_id(self):
|
|
||||||
"""
|
|
||||||
Returns the configured default value for
|
|
||||||
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.store_id`,
|
|
||||||
or ``None``.
|
|
||||||
"""
|
|
||||||
return self.config.get('sideshow.orders.default_store_id')
|
|
||||||
|
|
||||||
def use_local_customers(self):
|
def use_local_customers(self):
|
||||||
"""
|
"""
|
||||||
Returns boolean indicating whether :term:`local customer`
|
Returns boolean indicating whether :term:`local customer`
|
||||||
|
@ -174,18 +165,6 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
'label': customer.full_name}
|
'label': customer.full_name}
|
||||||
return [result(c) for c in customers]
|
return [result(c) for c in customers]
|
||||||
|
|
||||||
def init_batch(self, batch, session=None, progress=None, **kwargs):
|
|
||||||
"""
|
|
||||||
Initialize a new batch.
|
|
||||||
|
|
||||||
This sets the
|
|
||||||
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.store_id`,
|
|
||||||
if the batch does not yet have one and a default is
|
|
||||||
configured.
|
|
||||||
"""
|
|
||||||
if not batch.store_id:
|
|
||||||
batch.store_id = self.get_default_store_id()
|
|
||||||
|
|
||||||
def set_customer(self, batch, customer_info, user=None):
|
def set_customer(self, batch, customer_info, user=None):
|
||||||
"""
|
"""
|
||||||
Set/update customer info for the batch.
|
Set/update customer info for the batch.
|
||||||
|
@ -380,21 +359,6 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
'label': product.full_description}
|
'label': product.full_description}
|
||||||
return [result(c) for c in products]
|
return [result(c) for c in products]
|
||||||
|
|
||||||
def get_default_uom_choices(self):
|
|
||||||
"""
|
|
||||||
Returns a list of ordering UOM choices which should be
|
|
||||||
presented to the user by default.
|
|
||||||
|
|
||||||
The built-in logic here will return everything from
|
|
||||||
:data:`~sideshow.enum.ORDER_UOM`.
|
|
||||||
|
|
||||||
:returns: List of dicts, each with ``key`` and ``value``
|
|
||||||
corresponding to the UOM code and label, respectively.
|
|
||||||
"""
|
|
||||||
enum = self.app.enum
|
|
||||||
return [{'key': key, 'value': val}
|
|
||||||
for key, val in enum.ORDER_UOM.items()]
|
|
||||||
|
|
||||||
def get_product_info_external(self, session, product_id, user=None):
|
def get_product_info_external(self, session, product_id, user=None):
|
||||||
"""
|
"""
|
||||||
Returns basic info for an :term:`external product` as pertains
|
Returns basic info for an :term:`external product` as pertains
|
||||||
|
@ -404,8 +368,7 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
choose order quantity and UOM based on case size, pricing
|
choose order quantity and UOM based on case size, pricing
|
||||||
etc., this method is called to retrieve the product info.
|
etc., this method is called to retrieve the product info.
|
||||||
|
|
||||||
There is no default logic here; subclass must implement. See
|
There is no default logic here; subclass must implement.
|
||||||
also :meth:`get_product_info_local()`.
|
|
||||||
|
|
||||||
:param session: Current app :term:`db session`.
|
:param session: Current app :term:`db session`.
|
||||||
|
|
||||||
|
@ -461,58 +424,21 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
|
|
||||||
def get_product_info_local(self, session, uuid, user=None):
|
def get_product_info_local(self, session, uuid, user=None):
|
||||||
"""
|
"""
|
||||||
Returns basic info for a :term:`local product` as pertains to
|
Returns basic info for a
|
||||||
ordering.
|
:class:`~sideshow.db.model.products.LocalProduct` as pertains
|
||||||
|
to ordering.
|
||||||
|
|
||||||
When user has located a product via search, and must then
|
When user has located a product via search, and must then
|
||||||
choose order quantity and UOM based on case size, pricing
|
choose order quantity and UOM based on case size, pricing
|
||||||
etc., this method is called to retrieve the product info.
|
etc., this method is called to retrieve the product info.
|
||||||
|
|
||||||
See :meth:`get_product_info_external()` for more explanation.
|
See :meth:`get_product_info_external()` for more explanation.
|
||||||
|
|
||||||
This method will locate the
|
|
||||||
:class:`~sideshow.db.model.products.LocalProduct` record, then
|
|
||||||
(if found) it calls :meth:`normalize_local_product()` and
|
|
||||||
returns the result.
|
|
||||||
|
|
||||||
:param session: Current :term:`db session`.
|
|
||||||
|
|
||||||
:param uuid: UUID for the desired
|
|
||||||
:class:`~sideshow.db.model.products.LocalProduct`.
|
|
||||||
|
|
||||||
:param user:
|
|
||||||
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
|
|
||||||
is performing the action, if known.
|
|
||||||
|
|
||||||
:returns: Dict of product info.
|
|
||||||
"""
|
"""
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
product = session.get(model.LocalProduct, uuid)
|
product = session.get(model.LocalProduct, uuid)
|
||||||
if not product:
|
if not product:
|
||||||
raise ValueError(f"Local Product not found: {uuid}")
|
raise ValueError(f"Local Product not found: {uuid}")
|
||||||
|
|
||||||
return self.normalize_local_product(product)
|
|
||||||
|
|
||||||
def normalize_local_product(self, product):
|
|
||||||
"""
|
|
||||||
Returns a normalized dict of info for the given :term:`local
|
|
||||||
product`.
|
|
||||||
|
|
||||||
This is called by:
|
|
||||||
|
|
||||||
* :meth:`get_product_info_local()`
|
|
||||||
* :meth:`get_past_products()`
|
|
||||||
|
|
||||||
:param product:
|
|
||||||
:class:`~sideshow.db.model.products.LocalProduct` instance.
|
|
||||||
|
|
||||||
:returns: Dict of product info.
|
|
||||||
|
|
||||||
The keys for this dict should essentially one-to-one for the
|
|
||||||
product fields, with one exception:
|
|
||||||
|
|
||||||
* ``product_id`` will be set to the product UUID as string
|
|
||||||
"""
|
|
||||||
return {
|
return {
|
||||||
'product_id': product.uuid.hex,
|
'product_id': product.uuid.hex,
|
||||||
'scancode': product.scancode,
|
'scancode': product.scancode,
|
||||||
|
@ -530,109 +456,6 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
'vendor_item_code': product.vendor_item_code,
|
'vendor_item_code': product.vendor_item_code,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_past_orders(self, batch):
|
|
||||||
"""
|
|
||||||
Retrieve a (possibly empty) list of past :term:`orders
|
|
||||||
<order>` for the batch customer.
|
|
||||||
|
|
||||||
This is called by :meth:`get_past_products()`.
|
|
||||||
|
|
||||||
:param batch:
|
|
||||||
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch`
|
|
||||||
instance.
|
|
||||||
|
|
||||||
:returns: List of :class:`~sideshow.db.model.orders.Order`
|
|
||||||
records.
|
|
||||||
"""
|
|
||||||
model = self.app.model
|
|
||||||
session = self.app.get_session(batch)
|
|
||||||
orders = session.query(model.Order)
|
|
||||||
|
|
||||||
if batch.customer_id:
|
|
||||||
orders = orders.filter(model.Order.customer_id == batch.customer_id)
|
|
||||||
elif batch.local_customer:
|
|
||||||
orders = orders.filter(model.Order.local_customer == batch.local_customer)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"batch has no customer: {batch}")
|
|
||||||
|
|
||||||
orders = orders.order_by(model.Order.created.desc())
|
|
||||||
return orders.all()
|
|
||||||
|
|
||||||
def get_past_products(self, batch, user=None):
|
|
||||||
"""
|
|
||||||
Retrieve a (possibly empty) list of products which have been
|
|
||||||
previously ordered by the batch customer.
|
|
||||||
|
|
||||||
Note that this does not return :term:`order items <order
|
|
||||||
item>`, nor does it return true product records, but rather it
|
|
||||||
returns a list of dicts. Each will have product info but will
|
|
||||||
*not* have order quantity etc.
|
|
||||||
|
|
||||||
This method calls :meth:`get_past_orders()` and then iterates
|
|
||||||
through each order item therein. Any duplicated products
|
|
||||||
encountered will be skipped, so the final list contains unique
|
|
||||||
products.
|
|
||||||
|
|
||||||
Each dict in the result is obtained by calling one of:
|
|
||||||
|
|
||||||
* :meth:`normalize_local_product()`
|
|
||||||
* :meth:`get_product_info_external()`
|
|
||||||
|
|
||||||
:param batch:
|
|
||||||
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch`
|
|
||||||
instance.
|
|
||||||
|
|
||||||
:param user:
|
|
||||||
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
|
|
||||||
is performing the action, if known.
|
|
||||||
|
|
||||||
:returns: List of product info dicts.
|
|
||||||
"""
|
|
||||||
model = self.app.model
|
|
||||||
session = self.app.get_session(batch)
|
|
||||||
use_local = self.use_local_products()
|
|
||||||
user = user or batch.created_by
|
|
||||||
products = OrderedDict()
|
|
||||||
|
|
||||||
# track down all order items for batch contact
|
|
||||||
for order in self.get_past_orders(batch):
|
|
||||||
for item in order.items:
|
|
||||||
|
|
||||||
# nb. we only need the first match for each product
|
|
||||||
if use_local:
|
|
||||||
product = item.local_product
|
|
||||||
if product and product.uuid not in products:
|
|
||||||
products[product.uuid] = self.normalize_local_product(product)
|
|
||||||
elif item.product_id and item.product_id not in products:
|
|
||||||
products[item.product_id] = self.get_product_info_external(
|
|
||||||
session, item.product_id, user=user)
|
|
||||||
|
|
||||||
products = list(products.values())
|
|
||||||
for product in products:
|
|
||||||
|
|
||||||
price = product['unit_price_reg']
|
|
||||||
|
|
||||||
if 'unit_price_reg_display' not in product:
|
|
||||||
product['unit_price_reg_display'] = self.app.render_currency(price)
|
|
||||||
|
|
||||||
if 'unit_price_quoted' not in product:
|
|
||||||
product['unit_price_quoted'] = price
|
|
||||||
|
|
||||||
if 'unit_price_quoted_display' not in product:
|
|
||||||
product['unit_price_quoted_display'] = product['unit_price_reg_display']
|
|
||||||
|
|
||||||
if ('case_price_quoted' not in product
|
|
||||||
and product.get('unit_price_quoted') is not None
|
|
||||||
and product.get('case_size') is not None):
|
|
||||||
product['case_price_quoted'] = product['unit_price_quoted'] * product['case_size']
|
|
||||||
|
|
||||||
if ('case_price_quoted_display' not in product
|
|
||||||
and 'case_price_quoted' in product):
|
|
||||||
product['case_price_quoted_display'] = self.app.render_currency(
|
|
||||||
product['case_price_quoted'])
|
|
||||||
|
|
||||||
return products
|
|
||||||
|
|
||||||
def add_item(self, batch, product_info, order_qty, order_uom,
|
def add_item(self, batch, product_info, order_qty, order_uom,
|
||||||
discount_percent=None, user=None):
|
discount_percent=None, user=None):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
"""add stores
|
|
||||||
|
|
||||||
Revision ID: a4273360d379
|
|
||||||
Revises: 7a6df83afbd4
|
|
||||||
Create Date: 2025-01-27 17:48:20.638664
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
import wuttjamaican.db.util
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = 'a4273360d379'
|
|
||||||
down_revision: Union[str, None] = '7a6df83afbd4'
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
|
|
||||||
# sideshow_store
|
|
||||||
op.create_table('sideshow_store',
|
|
||||||
sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False),
|
|
||||||
sa.Column('store_id', sa.String(length=10), nullable=False),
|
|
||||||
sa.Column('name', sa.String(length=100), nullable=False),
|
|
||||||
sa.Column('archived', sa.Boolean(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_store')),
|
|
||||||
sa.UniqueConstraint('store_id', name=op.f('uq_sideshow_store_store_id')),
|
|
||||||
sa.UniqueConstraint('name', name=op.f('uq_sideshow_store_name'))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
|
|
||||||
# sideshow_store
|
|
||||||
op.drop_table('sideshow_store')
|
|
|
@ -30,7 +30,6 @@ This namespace exposes everything from
|
||||||
|
|
||||||
Primary :term:`data models <data model>`:
|
Primary :term:`data models <data model>`:
|
||||||
|
|
||||||
* :class:`~sideshow.db.model.stores.Store`
|
|
||||||
* :class:`~sideshow.db.model.orders.Order`
|
* :class:`~sideshow.db.model.orders.Order`
|
||||||
* :class:`~sideshow.db.model.orders.OrderItem`
|
* :class:`~sideshow.db.model.orders.OrderItem`
|
||||||
* :class:`~sideshow.db.model.orders.OrderItemEvent`
|
* :class:`~sideshow.db.model.orders.OrderItemEvent`
|
||||||
|
@ -49,7 +48,6 @@ And the :term:`batch` models:
|
||||||
from wuttjamaican.db.model import *
|
from wuttjamaican.db.model import *
|
||||||
|
|
||||||
# sideshow models
|
# sideshow models
|
||||||
from .stores import Store
|
|
||||||
from .customers import LocalCustomer, PendingCustomer
|
from .customers import LocalCustomer, PendingCustomer
|
||||||
from .products import LocalProduct, PendingProduct
|
from .products import LocalProduct, PendingProduct
|
||||||
from .orders import Order, OrderItem, OrderItemEvent
|
from .orders import Order, OrderItem, OrderItemEvent
|
||||||
|
|
|
@ -62,15 +62,6 @@ class Order(model.Base):
|
||||||
ID of the store to which the order pertains, if applicable.
|
ID of the store to which the order pertains, if applicable.
|
||||||
""")
|
""")
|
||||||
|
|
||||||
store = orm.relationship(
|
|
||||||
'Store',
|
|
||||||
primaryjoin='Store.store_id == Order.store_id',
|
|
||||||
foreign_keys='Order.store_id',
|
|
||||||
doc="""
|
|
||||||
Reference to the :class:`~sideshow.db.model.stores.Store`
|
|
||||||
record, if applicable.
|
|
||||||
""")
|
|
||||||
|
|
||||||
customer_id = sa.Column(sa.String(length=20), nullable=True, doc="""
|
customer_id = sa.Column(sa.String(length=20), nullable=True, doc="""
|
||||||
Proper account ID for the :term:`external customer` to which the
|
Proper account ID for the :term:`external customer` to which the
|
||||||
order pertains, if applicable.
|
order pertains, if applicable.
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# Sideshow -- Case/Special Order Tracker
|
|
||||||
# Copyright © 2024-2025 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of Sideshow.
|
|
||||||
#
|
|
||||||
# Sideshow is free software: you can redistribute it and/or modify it
|
|
||||||
# under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Sideshow is distributed in the hope that it will be useful, but
|
|
||||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Sideshow. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
Data models for Stores
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
from wuttjamaican.db import model
|
|
||||||
|
|
||||||
|
|
||||||
class Store(model.Base):
|
|
||||||
"""
|
|
||||||
Represents a physical location for the business.
|
|
||||||
"""
|
|
||||||
__tablename__ = 'sideshow_store'
|
|
||||||
|
|
||||||
uuid = model.uuid_column()
|
|
||||||
|
|
||||||
store_id = sa.Column(sa.String(length=10), nullable=False, unique=True, doc="""
|
|
||||||
Unique ID for the store.
|
|
||||||
""")
|
|
||||||
|
|
||||||
name = sa.Column(sa.String(length=100), nullable=False, unique=True, doc="""
|
|
||||||
Display name for the store (must be unique!).
|
|
||||||
""")
|
|
||||||
|
|
||||||
archived = sa.Column(sa.Boolean(), nullable=False, default=False, doc="""
|
|
||||||
Indicates the store has been "retired" essentially, and mostly
|
|
||||||
hidden from view.
|
|
||||||
""")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.get_display()
|
|
||||||
|
|
||||||
def get_display(self):
|
|
||||||
"""
|
|
||||||
Returns the display string for the store, e.g. "001 Acme Goods".
|
|
||||||
"""
|
|
||||||
return ' '.join([(self.store_id or '').strip(),
|
|
||||||
(self.name or '').strip()])\
|
|
||||||
.strip()
|
|
|
@ -37,14 +37,6 @@ class OrderHandler(GenericHandler):
|
||||||
handler is responsible for creation logic.)
|
handler is responsible for creation logic.)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def expose_store_id(self):
|
|
||||||
"""
|
|
||||||
Returns boolean indicating whether the ``store_id`` field
|
|
||||||
should be exposed at all. This is false by default.
|
|
||||||
"""
|
|
||||||
return self.config.get_bool('sideshow.orders.expose_store_id',
|
|
||||||
default=False)
|
|
||||||
|
|
||||||
def get_order_qty_uom_text(self, order_qty, order_uom, case_size=None, html=False):
|
def get_order_qty_uom_text(self, order_qty, order_uom, case_size=None, html=False):
|
||||||
"""
|
"""
|
||||||
Return the display text for a given order quantity.
|
Return the display text for a given order quantity.
|
||||||
|
|
|
@ -162,12 +162,4 @@ class SideshowMenuHandler(base.MenuHandler):
|
||||||
def make_admin_menu(self, request, **kwargs):
|
def make_admin_menu(self, request, **kwargs):
|
||||||
""" """
|
""" """
|
||||||
kwargs['include_people'] = True
|
kwargs['include_people'] = True
|
||||||
menu = super().make_admin_menu(request, **kwargs)
|
return super().make_admin_menu(request, **kwargs)
|
||||||
|
|
||||||
menu['items'].insert(0, {
|
|
||||||
'title': "Stores",
|
|
||||||
'route': 'stores',
|
|
||||||
'perm': 'stores.list',
|
|
||||||
})
|
|
||||||
|
|
||||||
return menu
|
|
||||||
|
|
|
@ -29,17 +29,6 @@
|
||||||
<b-field horizontal label="ID">
|
<b-field horizontal label="ID">
|
||||||
<span>${h.link_to(f"Order ID {order.order_id}", url('orders.view', uuid=order.uuid))} — Item #${item.sequence}</span>
|
<span>${h.link_to(f"Order ID {order.order_id}", url('orders.view', uuid=order.uuid))} — Item #${item.sequence}</span>
|
||||||
</b-field>
|
</b-field>
|
||||||
% if expose_store_id:
|
|
||||||
<b-field horizontal label="Store">
|
|
||||||
<span>
|
|
||||||
% if order.store:
|
|
||||||
${h.link_to(order.store.get_display(), url('stores.view', uuid=order.store.uuid))}
|
|
||||||
% elif order.store_id:
|
|
||||||
${order.store_id}
|
|
||||||
% endif
|
|
||||||
</span>
|
|
||||||
</b-field>
|
|
||||||
% endif
|
|
||||||
<b-field horizontal label="Order Qty">
|
<b-field horizontal label="Order Qty">
|
||||||
<span>${order_qty_uom_text|n}</span>
|
<span>${order_qty_uom_text|n}</span>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
@ -234,10 +223,7 @@
|
||||||
<b-field horizontal label="Sold by Weight">
|
<b-field horizontal label="Sold by Weight">
|
||||||
<span>${app.render_boolean(item.product_weighed)}</span>
|
<span>${app.render_boolean(item.product_weighed)}</span>
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-field horizontal label="Department ID">
|
<b-field horizontal label="Department">
|
||||||
<span>${item.department_id}</span>
|
|
||||||
</b-field>
|
|
||||||
<b-field horizontal label="Department Name">
|
|
||||||
<span>${item.department_name}</span>
|
<span>${item.department_name}</span>
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-field horizontal label="Special Order">
|
<b-field horizontal label="Special Order">
|
||||||
|
|
|
@ -3,28 +3,6 @@
|
||||||
|
|
||||||
<%def name="form_content()">
|
<%def name="form_content()">
|
||||||
|
|
||||||
<h3 class="block is-size-3">Stores</h3>
|
|
||||||
<div class="block" style="padding-left: 2rem;">
|
|
||||||
|
|
||||||
<b-field>
|
|
||||||
<b-checkbox name="sideshow.orders.expose_store_id"
|
|
||||||
v-model="simpleSettings['sideshow.orders.expose_store_id']"
|
|
||||||
native-value="true"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
Show/choose the Store ID for each order
|
|
||||||
</b-checkbox>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field v-show="simpleSettings['sideshow.orders.expose_store_id']"
|
|
||||||
label="Default Store ID">
|
|
||||||
<b-input name="sideshow.orders.default_store_id"
|
|
||||||
v-model="simpleSettings['sideshow.orders.default_store_id']"
|
|
||||||
@input="settingsNeedSaved = true"
|
|
||||||
style="width: 25rem;" />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="block is-size-3">Customers</h3>
|
<h3 class="block is-size-3">Customers</h3>
|
||||||
<div class="block" style="padding-left: 2rem;">
|
<div class="block" style="padding-left: 2rem;">
|
||||||
|
|
||||||
|
@ -36,12 +14,41 @@
|
||||||
<option value="false">External Customers (e.g. in POS)</option>
|
<option value="false">External Customers (e.g. in POS)</option>
|
||||||
</b-select>
|
</b-select>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="block is-size-3">Products</h3>
|
<h3 class="block is-size-3">Products</h3>
|
||||||
<div class="block" style="padding-left: 2rem;">
|
<div class="block" style="padding-left: 2rem;">
|
||||||
|
|
||||||
|
<b-field>
|
||||||
|
<b-checkbox name="sideshow.orders.allow_item_discounts"
|
||||||
|
v-model="simpleSettings['sideshow.orders.allow_item_discounts']"
|
||||||
|
native-value="true"
|
||||||
|
@input="settingsNeedSaved = true">
|
||||||
|
Allow per-item discounts
|
||||||
|
</b-checkbox>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field v-show="simpleSettings['sideshow.orders.allow_item_discounts']">
|
||||||
|
<b-checkbox name="sideshow.orders.allow_item_discounts_if_on_sale"
|
||||||
|
v-model="simpleSettings['sideshow.orders.allow_item_discounts_if_on_sale']"
|
||||||
|
native-value="true"
|
||||||
|
@input="settingsNeedSaved = true">
|
||||||
|
Allow discount even if item is on sale
|
||||||
|
</b-checkbox>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<div v-show="simpleSettings['sideshow.orders.allow_item_discounts']"
|
||||||
|
class="level-left block">
|
||||||
|
<div class="level-item">Default item discount</div>
|
||||||
|
<div class="level-item">
|
||||||
|
<b-input name="sideshow.orders.default_item_discount"
|
||||||
|
v-model="simpleSettings['sideshow.orders.default_item_discount']"
|
||||||
|
@input="settingsNeedSaved = true"
|
||||||
|
style="width: 5rem;" />
|
||||||
|
</div>
|
||||||
|
<div class="level-item">%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<b-field label="Product Source">
|
<b-field label="Product Source">
|
||||||
<b-select name="sideshow.orders.use_local_products"
|
<b-select name="sideshow.orders.use_local_products"
|
||||||
v-model="simpleSettings['sideshow.orders.use_local_products']"
|
v-model="simpleSettings['sideshow.orders.use_local_products']"
|
||||||
|
@ -85,136 +92,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="block is-size-3">Pricing</h3>
|
|
||||||
<div class="block" style="padding-left: 2rem;">
|
|
||||||
|
|
||||||
<b-field>
|
|
||||||
<b-checkbox name="sideshow.orders.allow_item_discounts"
|
|
||||||
v-model="simpleSettings['sideshow.orders.allow_item_discounts']"
|
|
||||||
native-value="true"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
Allow per-item discounts
|
|
||||||
</b-checkbox>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field v-show="simpleSettings['sideshow.orders.allow_item_discounts']">
|
|
||||||
<b-checkbox name="sideshow.orders.allow_item_discounts_if_on_sale"
|
|
||||||
v-model="simpleSettings['sideshow.orders.allow_item_discounts_if_on_sale']"
|
|
||||||
native-value="true"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
Allow discount even if item is on sale
|
|
||||||
</b-checkbox>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<div v-show="simpleSettings['sideshow.orders.allow_item_discounts']"
|
|
||||||
class="block"
|
|
||||||
style="display: flex; gap: 0.5rem; align-items: center;">
|
|
||||||
<span>Global default item discount</span>
|
|
||||||
<b-input name="sideshow.orders.default_item_discount"
|
|
||||||
v-model="simpleSettings['sideshow.orders.default_item_discount']"
|
|
||||||
@input="settingsNeedSaved = true"
|
|
||||||
style="width: 5rem;" />
|
|
||||||
<span>%</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-show="simpleSettings['sideshow.orders.allow_item_discounts']"
|
|
||||||
style="width: 50%;">
|
|
||||||
<div style="display: flex; gap: 1rem; align-items: center;">
|
|
||||||
<p>Per-Department default item discounts</p>
|
|
||||||
<div>
|
|
||||||
<b-button type="is-primary"
|
|
||||||
@click="deptItemDiscountInit()"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="plus">
|
|
||||||
Add
|
|
||||||
</b-button>
|
|
||||||
<input type="hidden" name="dept_item_discounts" :value="JSON.stringify(deptItemDiscounts)" />
|
|
||||||
<${b}-modal has-modal-card
|
|
||||||
% if request.use_oruga:
|
|
||||||
v-model:active="deptItemDiscountShowDialog"
|
|
||||||
% else:
|
|
||||||
:active.sync="deptItemDiscountShowDialog"
|
|
||||||
% endif
|
|
||||||
>
|
|
||||||
<div class="modal-card">
|
|
||||||
|
|
||||||
<header class="modal-card-head">
|
|
||||||
<p class="modal-card-title">Default Discount for Department</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section class="modal-card-body">
|
|
||||||
<div style="display: flex; gap: 1rem;">
|
|
||||||
<b-field label="Dept. ID"
|
|
||||||
:type="deptItemDiscountDeptID ? null : 'is-danger'">
|
|
||||||
<b-input v-model="deptItemDiscountDeptID"
|
|
||||||
ref="deptItemDiscountDeptID"
|
|
||||||
style="width: 6rem;;" />
|
|
||||||
</b-field>
|
|
||||||
<b-field label="Department Name"
|
|
||||||
:type="deptItemDiscountDeptName ? null : 'is-danger'"
|
|
||||||
style="flex-grow: 1;">
|
|
||||||
<b-input v-model="deptItemDiscountDeptName" />
|
|
||||||
</b-field>
|
|
||||||
<b-field label="Discount"
|
|
||||||
:type="deptItemDiscountPercent ? null : 'is-danger'">
|
|
||||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
|
||||||
<b-input v-model="deptItemDiscountPercent"
|
|
||||||
ref="deptItemDiscountPercent"
|
|
||||||
style="width: 6rem;" />
|
|
||||||
<span>%</span>
|
|
||||||
</div>
|
|
||||||
</b-field>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<footer class="modal-card-foot">
|
|
||||||
<b-button type="is-primary"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="save"
|
|
||||||
:disabled="deptItemDiscountSaveDisabled"
|
|
||||||
@click="deptItemDiscountSave()">
|
|
||||||
Save
|
|
||||||
</b-button>
|
|
||||||
<b-button @click="deptItemDiscountShowDialog = false">
|
|
||||||
Cancel
|
|
||||||
</b-button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</${b}-modal>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<${b}-table :data="deptItemDiscounts">
|
|
||||||
<${b}-table-column field="department_id"
|
|
||||||
label="Dept. ID"
|
|
||||||
v-slot="props">
|
|
||||||
{{ props.row.department_id }}
|
|
||||||
</${b}-table-column>
|
|
||||||
<${b}-table-column field="department_name"
|
|
||||||
label="Department Name"
|
|
||||||
v-slot="props">
|
|
||||||
{{ props.row.department_name }}
|
|
||||||
</${b}-table-column>
|
|
||||||
<${b}-table-column field="default_item_discount"
|
|
||||||
label="Discount"
|
|
||||||
v-slot="props">
|
|
||||||
{{ props.row.default_item_discount }} %
|
|
||||||
</${b}-table-column>
|
|
||||||
<${b}-table-column label="Actions"
|
|
||||||
v-slot="props">
|
|
||||||
<a href="#" @click.prevent="deptItemDiscountInit(props.row)">
|
|
||||||
<i class="fas fa-edit" />
|
|
||||||
Edit
|
|
||||||
</a>
|
|
||||||
<a href="#" @click.prevent="deptItemDiscountDelete(props.row)"
|
|
||||||
class="has-text-danger">
|
|
||||||
<i class="fas fa-trash" />
|
|
||||||
Delete
|
|
||||||
</a>
|
|
||||||
</${b}-table-column>
|
|
||||||
</${b}-table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="block is-size-3">Batches</h3>
|
<h3 class="block is-size-3">Batches</h3>
|
||||||
<div class="block" style="padding-left: 2rem;">
|
<div class="block" style="padding-left: 2rem;">
|
||||||
|
|
||||||
|
@ -241,62 +118,5 @@
|
||||||
|
|
||||||
ThisPageData.batchHandlers = ${json.dumps(batch_handlers)|n}
|
ThisPageData.batchHandlers = ${json.dumps(batch_handlers)|n}
|
||||||
|
|
||||||
ThisPageData.deptItemDiscounts = ${json.dumps(dept_item_discounts)|n}
|
|
||||||
ThisPageData.deptItemDiscountShowDialog = false
|
|
||||||
ThisPageData.deptItemDiscountRow = null
|
|
||||||
ThisPageData.deptItemDiscountDeptID = null
|
|
||||||
ThisPageData.deptItemDiscountDeptName = null
|
|
||||||
ThisPageData.deptItemDiscountPercent = null
|
|
||||||
|
|
||||||
ThisPage.computed.deptItemDiscountSaveDisabled = function() {
|
|
||||||
if (!this.deptItemDiscountDeptID) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (!this.deptItemDiscountDeptName) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (!this.deptItemDiscountPercent) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
ThisPage.methods.deptItemDiscountDelete = function(row) {
|
|
||||||
const i = this.deptItemDiscounts.indexOf(row)
|
|
||||||
this.deptItemDiscounts.splice(i, 1)
|
|
||||||
this.settingsNeedSaved = true
|
|
||||||
}
|
|
||||||
|
|
||||||
ThisPage.methods.deptItemDiscountInit = function(row) {
|
|
||||||
this.deptItemDiscountRow = row
|
|
||||||
this.deptItemDiscountDeptID = row?.department_id
|
|
||||||
this.deptItemDiscountDeptName = row?.department_name
|
|
||||||
this.deptItemDiscountPercent = row?.default_item_discount
|
|
||||||
this.deptItemDiscountShowDialog = true
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (row) {
|
|
||||||
this.$refs.deptItemDiscountPercent.focus()
|
|
||||||
} else {
|
|
||||||
this.$refs.deptItemDiscountDeptID.focus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ThisPage.methods.deptItemDiscountSave = function() {
|
|
||||||
if (this.deptItemDiscountRow) {
|
|
||||||
this.deptItemDiscountRow.department_id = this.deptItemDiscountDeptID
|
|
||||||
this.deptItemDiscountRow.department_name = this.deptItemDiscountDeptName
|
|
||||||
this.deptItemDiscountRow.default_item_discount = this.deptItemDiscountPercent
|
|
||||||
} else {
|
|
||||||
this.deptItemDiscounts.push({
|
|
||||||
department_id: this.deptItemDiscountDeptID,
|
|
||||||
department_name: this.deptItemDiscountDeptName,
|
|
||||||
default_item_discount: this.deptItemDiscountPercent,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.deptItemDiscountShowDialog = false
|
|
||||||
this.settingsNeedSaved = true
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
|
@ -42,25 +42,7 @@
|
||||||
<script type="text/x-template" id="order-creator-template">
|
<script type="text/x-template" id="order-creator-template">
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; margin-bottom: 1.5rem;">
|
|
||||||
<div>
|
|
||||||
% if expose_store_id:
|
|
||||||
<b-loading v-model="storeLoading" is-full-page />
|
|
||||||
<b-field label="Store" horizontal
|
|
||||||
:type="storeID ? null : 'is-danger'">
|
|
||||||
<b-select v-model="storeID"
|
|
||||||
@input="storeChanged">
|
|
||||||
<option v-for="store in stores"
|
|
||||||
:key="store.store_id"
|
|
||||||
:value="store.store_id">
|
|
||||||
{{ store.display }}
|
|
||||||
</option>
|
|
||||||
</b-select>
|
|
||||||
</b-field>
|
|
||||||
% endif
|
|
||||||
</div>
|
|
||||||
${self.order_form_buttons()}
|
${self.order_form_buttons()}
|
||||||
</div>
|
|
||||||
|
|
||||||
<${b}-collapse class="panel"
|
<${b}-collapse class="panel"
|
||||||
:class="customerPanelType"
|
:class="customerPanelType"
|
||||||
|
@ -355,135 +337,6 @@
|
||||||
@click="showAddItemDialog()">
|
@click="showAddItemDialog()">
|
||||||
Add Item
|
Add Item
|
||||||
</b-button>
|
</b-button>
|
||||||
% if allow_past_item_reorder:
|
|
||||||
<b-button v-if="customerIsKnown && customerID"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="plus"
|
|
||||||
@click="showAddPastItem()">
|
|
||||||
Add Past Item
|
|
||||||
</b-button>
|
|
||||||
|
|
||||||
<${b}-modal
|
|
||||||
% if request.use_oruga:
|
|
||||||
v-model:active="pastItemsShowDialog"
|
|
||||||
% else:
|
|
||||||
:active.sync="pastItemsShowDialog"
|
|
||||||
% endif
|
|
||||||
>
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-content">
|
|
||||||
|
|
||||||
<${b}-table :data="pastItems"
|
|
||||||
icon-pack="fas"
|
|
||||||
:loading="pastItemsLoading"
|
|
||||||
% if request.use_oruga:
|
|
||||||
v-model:selected="pastItemsSelected"
|
|
||||||
% else:
|
|
||||||
:selected.sync="pastItemsSelected"
|
|
||||||
% endif
|
|
||||||
sortable
|
|
||||||
paginated
|
|
||||||
per-page="5"
|
|
||||||
## :debounce-search="1000"
|
|
||||||
>
|
|
||||||
|
|
||||||
<${b}-table-column label="Scancode"
|
|
||||||
field="key"
|
|
||||||
v-slot="props"
|
|
||||||
sortable>
|
|
||||||
{{ props.row.scancode }}
|
|
||||||
</${b}-table-column>
|
|
||||||
|
|
||||||
<${b}-table-column label="Brand"
|
|
||||||
field="brand_name"
|
|
||||||
v-slot="props"
|
|
||||||
sortable
|
|
||||||
searchable>
|
|
||||||
{{ props.row.brand_name }}
|
|
||||||
</${b}-table-column>
|
|
||||||
|
|
||||||
<${b}-table-column label="Description"
|
|
||||||
field="description"
|
|
||||||
v-slot="props"
|
|
||||||
sortable
|
|
||||||
searchable>
|
|
||||||
{{ props.row.description }}
|
|
||||||
{{ props.row.size }}
|
|
||||||
</${b}-table-column>
|
|
||||||
|
|
||||||
<${b}-table-column label="Unit Price"
|
|
||||||
field="unit_price_reg_display"
|
|
||||||
v-slot="props"
|
|
||||||
sortable>
|
|
||||||
{{ props.row.unit_price_reg_display }}
|
|
||||||
</${b}-table-column>
|
|
||||||
|
|
||||||
<${b}-table-column label="Sale Price"
|
|
||||||
field="sale_price"
|
|
||||||
v-slot="props"
|
|
||||||
sortable>
|
|
||||||
<span class="has-background-warning">
|
|
||||||
{{ props.row.sale_price_display }}
|
|
||||||
</span>
|
|
||||||
</${b}-table-column>
|
|
||||||
|
|
||||||
<${b}-table-column label="Sale Ends"
|
|
||||||
field="sale_ends"
|
|
||||||
v-slot="props"
|
|
||||||
sortable>
|
|
||||||
<span class="has-background-warning">
|
|
||||||
{{ props.row.sale_ends_display }}
|
|
||||||
</span>
|
|
||||||
</${b}-table-column>
|
|
||||||
|
|
||||||
<${b}-table-column label="Department"
|
|
||||||
field="department_name"
|
|
||||||
v-slot="props"
|
|
||||||
sortable
|
|
||||||
searchable>
|
|
||||||
{{ props.row.department_name }}
|
|
||||||
</${b}-table-column>
|
|
||||||
|
|
||||||
<${b}-table-column label="Vendor"
|
|
||||||
field="vendor_name"
|
|
||||||
v-slot="props"
|
|
||||||
sortable
|
|
||||||
searchable>
|
|
||||||
{{ props.row.vendor_name }}
|
|
||||||
</${b}-table-column>
|
|
||||||
|
|
||||||
<template #empty>
|
|
||||||
<div class="content has-text-grey has-text-centered">
|
|
||||||
<p>
|
|
||||||
<b-icon
|
|
||||||
pack="fas"
|
|
||||||
icon="sad-tear"
|
|
||||||
size="is-large">
|
|
||||||
</b-icon>
|
|
||||||
</p>
|
|
||||||
<p>Nothing here.</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</${b}-table>
|
|
||||||
|
|
||||||
<div class="buttons">
|
|
||||||
<b-button @click="pastItemsShowDialog = false">
|
|
||||||
Cancel
|
|
||||||
</b-button>
|
|
||||||
<b-button type="is-primary"
|
|
||||||
icon-pack="fas"
|
|
||||||
icon-left="plus"
|
|
||||||
@click="pastItemsAddSelected()"
|
|
||||||
:disabled="!pastItemsSelected">
|
|
||||||
Add Selected Item
|
|
||||||
</b-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</${b}-modal>
|
|
||||||
|
|
||||||
% endif
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<${b}-modal
|
<${b}-modal
|
||||||
|
@ -633,16 +486,7 @@
|
||||||
<b-input v-model="pendingProduct.scancode" />
|
<b-input v-model="pendingProduct.scancode" />
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field label="Dept. ID"
|
<b-field label="Department"
|
||||||
% if 'department_id' in pending_product_required_fields:
|
|
||||||
:type="pendingProduct.department_id ? null : 'is-danger'"
|
|
||||||
% endif
|
|
||||||
style="width: 15rem;">
|
|
||||||
<b-input v-model="pendingProduct.department_id"
|
|
||||||
@input="updateDiscount" />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field label="Department Name"
|
|
||||||
% if 'department_name' in pending_product_required_fields:
|
% if 'department_name' in pending_product_required_fields:
|
||||||
:type="pendingProduct.department_name ? null : 'is-danger'"
|
:type="pendingProduct.department_name ? null : 'is-danger'"
|
||||||
% endif
|
% endif
|
||||||
|
@ -650,7 +494,8 @@
|
||||||
<b-input v-model="pendingProduct.department_name" />
|
<b-input v-model="pendingProduct.department_name" />
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field label="Special Order">
|
<b-field label="Special Order"
|
||||||
|
style="width: 100%;">
|
||||||
<b-checkbox v-model="pendingProduct.special_order" />
|
<b-checkbox v-model="pendingProduct.special_order" />
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
|
@ -887,7 +732,7 @@
|
||||||
|
|
||||||
<${b}-table-column label="Department"
|
<${b}-table-column label="Department"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.department_name }}
|
{{ props.row.department_display }}
|
||||||
</${b}-table-column>
|
</${b}-table-column>
|
||||||
|
|
||||||
<${b}-table-column label="Quantity"
|
<${b}-table-column label="Quantity"
|
||||||
|
@ -992,12 +837,6 @@
|
||||||
|
|
||||||
batchTotalPriceDisplay: ${json.dumps(normalized_batch['total_price_display'])|n},
|
batchTotalPriceDisplay: ${json.dumps(normalized_batch['total_price_display'])|n},
|
||||||
|
|
||||||
% if expose_store_id:
|
|
||||||
stores: ${json.dumps(stores)|n},
|
|
||||||
storeID: ${json.dumps(batch.store_id)|n},
|
|
||||||
storeLoading: false,
|
|
||||||
% endif
|
|
||||||
|
|
||||||
customerPanelOpen: false,
|
customerPanelOpen: false,
|
||||||
customerLoading: false,
|
customerLoading: false,
|
||||||
customerIsKnown: ${json.dumps(customer_is_known)|n},
|
customerIsKnown: ${json.dumps(customer_is_known)|n},
|
||||||
|
@ -1059,10 +898,8 @@
|
||||||
productCaseSize: null,
|
productCaseSize: null,
|
||||||
|
|
||||||
% if allow_item_discounts:
|
% if allow_item_discounts:
|
||||||
defaultItemDiscount: ${json.dumps(default_item_discount)|n},
|
productDiscountPercent: ${json.dumps(default_item_discount)|n},
|
||||||
deptItemDiscounts: ${json.dumps(dept_item_discounts)|n},
|
|
||||||
allowDiscountsIfOnSale: ${json.dumps(allow_item_discounts_if_on_sale)|n},
|
allowDiscountsIfOnSale: ${json.dumps(allow_item_discounts_if_on_sale)|n},
|
||||||
productDiscountPercent: null,
|
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
pendingProduct: {},
|
pendingProduct: {},
|
||||||
|
@ -1071,13 +908,6 @@
|
||||||
## departmentOptions: ${json.dumps(department_options)|n},
|
## departmentOptions: ${json.dumps(department_options)|n},
|
||||||
departmentOptions: [],
|
departmentOptions: [],
|
||||||
|
|
||||||
% if allow_past_item_reorder:
|
|
||||||
pastItemsShowDialog: false,
|
|
||||||
pastItemsLoading: false,
|
|
||||||
pastItems: [],
|
|
||||||
pastItemsSelected: null,
|
|
||||||
% endif
|
|
||||||
|
|
||||||
// nb. hack to force refresh for vue3
|
// nb. hack to force refresh for vue3
|
||||||
refreshProductDescription: 1,
|
refreshProductDescription: 1,
|
||||||
refreshTotalPrice: 1,
|
refreshTotalPrice: 1,
|
||||||
|
@ -1330,31 +1160,8 @@
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
% if expose_store_id:
|
|
||||||
|
|
||||||
storeChanged(storeID) {
|
|
||||||
this.storeLoading = true
|
|
||||||
const params = {
|
|
||||||
action: 'set_store',
|
|
||||||
store_id: storeID,
|
|
||||||
}
|
|
||||||
this.submitBatchData(params, ({data}) => {
|
|
||||||
this.storeLoading = false
|
|
||||||
}, response => {
|
|
||||||
this.$buefy.toast.open({
|
|
||||||
message: "Update failed: " + (response.data.error || "(unknown error)"),
|
|
||||||
type: 'is-danger',
|
|
||||||
duration: 2000, // 2 seconds
|
|
||||||
})
|
|
||||||
this.storeLoading = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
% endif
|
|
||||||
|
|
||||||
customerChanged(customerID, callback) {
|
customerChanged(customerID, callback) {
|
||||||
this.customerLoading = true
|
this.customerLoading = true
|
||||||
this.pastItems = []
|
|
||||||
|
|
||||||
const params = {}
|
const params = {}
|
||||||
if (customerID) {
|
if (customerID) {
|
||||||
|
@ -1406,23 +1213,6 @@
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
% if allow_item_discounts:
|
|
||||||
|
|
||||||
updateDiscount(deptID) {
|
|
||||||
if (deptID) {
|
|
||||||
// nb. our map requires ID as string
|
|
||||||
deptID = deptID.toString()
|
|
||||||
}
|
|
||||||
const i = Object.keys(this.deptItemDiscounts).indexOf(deptID)
|
|
||||||
if (i == -1) {
|
|
||||||
this.productDiscountPercent = this.defaultItemDiscount
|
|
||||||
} else {
|
|
||||||
this.productDiscountPercent = this.deptItemDiscounts[deptID]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
% endif
|
|
||||||
|
|
||||||
editNewCustomerSave() {
|
editNewCustomerSave() {
|
||||||
this.editNewCustomerSaving = true
|
this.editNewCustomerSaving = true
|
||||||
|
|
||||||
|
@ -1561,7 +1351,7 @@
|
||||||
this.productUnitChoices = this.defaultUnitChoices
|
this.productUnitChoices = this.defaultUnitChoices
|
||||||
|
|
||||||
% if allow_item_discounts:
|
% if allow_item_discounts:
|
||||||
this.productDiscountPercent = this.defaultItemDiscount
|
this.productDiscountPercent = ${json.dumps(default_item_discount)|n}
|
||||||
% endif
|
% endif
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1600,15 +1390,7 @@
|
||||||
this.productSaleEndsDisplay = data.sale_ends_display
|
this.productSaleEndsDisplay = data.sale_ends_display
|
||||||
|
|
||||||
% if allow_item_discounts:
|
% if allow_item_discounts:
|
||||||
if (this.allowItemDiscount) {
|
this.productDiscountPercent = this.allowItemDiscount ? data.default_item_discount : null
|
||||||
if (data?.default_item_discount != null) {
|
|
||||||
this.productDiscountPercent = data.default_item_discount
|
|
||||||
} else {
|
|
||||||
this.updateDiscount(data?.department_id)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.productDiscountPercent = null
|
|
||||||
}
|
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
// this.setProductUnitChoices(data.uom_choices)
|
// this.setProductUnitChoices(data.uom_choices)
|
||||||
|
@ -1686,7 +1468,7 @@
|
||||||
this.productUOM = this.defaultUOM
|
this.productUOM = this.defaultUOM
|
||||||
|
|
||||||
% if allow_item_discounts:
|
% if allow_item_discounts:
|
||||||
this.productDiscountPercent = this.defaultItemDiscount
|
this.productDiscountPercent = ${json.dumps(default_item_discount)|n}
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
% if request.use_oruga:
|
% if request.use_oruga:
|
||||||
|
@ -1740,7 +1522,7 @@
|
||||||
## this.productSpecialOrder = row.special_order
|
## this.productSpecialOrder = row.special_order
|
||||||
|
|
||||||
this.productQuantity = row.order_qty
|
this.productQuantity = row.order_qty
|
||||||
this.productUnitChoices = row?.order_uom_choices || this.defaultUnitChoices
|
this.productUnitChoices = row.order_uom_choices
|
||||||
this.productUOM = row.order_uom
|
this.productUOM = row.order_uom
|
||||||
|
|
||||||
% if allow_item_discounts:
|
% if allow_item_discounts:
|
||||||
|
@ -1782,77 +1564,12 @@
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
% if allow_past_item_reorder:
|
|
||||||
|
|
||||||
showAddPastItem() {
|
|
||||||
this.pastItemsSelected = null
|
|
||||||
if (!this.pastItems.length) {
|
|
||||||
this.pastItemsLoading = true
|
|
||||||
const params = {action: 'get_past_products'}
|
|
||||||
this.submitBatchData(params, ({data}) => {
|
|
||||||
this.pastItems = data
|
|
||||||
this.pastItemsLoading = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.pastItemsShowDialog = true
|
|
||||||
},
|
|
||||||
|
|
||||||
pastItemsAddSelected() {
|
|
||||||
this.pastItemsShowDialog = false
|
|
||||||
const selected = this.pastItemsSelected
|
|
||||||
|
|
||||||
this.editItemRow = null
|
|
||||||
this.productIsKnown = true
|
|
||||||
this.productID = selected.product_id
|
|
||||||
|
|
||||||
this.selectedProduct = {
|
|
||||||
product_id: selected.product_id,
|
|
||||||
full_description: selected.full_description,
|
|
||||||
// url: selected.product_url,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.productDisplay = selected.full_description
|
|
||||||
this.productScancode = selected.scancode
|
|
||||||
this.productSize = selected.size
|
|
||||||
this.productCaseQuantity = selected.case_size
|
|
||||||
this.productUnitPrice = selected.unit_price_quoted
|
|
||||||
this.productUnitPriceDisplay = selected.unit_price_quoted_display
|
|
||||||
this.productUnitRegularPriceDisplay = selected.unit_price_reg_display
|
|
||||||
this.productCasePrice = selected.case_price_quoted
|
|
||||||
this.productCasePriceDisplay = selected.case_price_quoted_display
|
|
||||||
this.productSalePrice = selected.unit_price_sale
|
|
||||||
this.productSalePriceDisplay = selected.unit_price_sale_display
|
|
||||||
this.productSaleEndsDisplay = selected.sale_ends_display
|
|
||||||
this.productSpecialOrder = selected.special_order
|
|
||||||
|
|
||||||
this.productQuantity = 1
|
|
||||||
this.productUnitChoices = selected?.order_uom_choices || this.defaultUnitChoices
|
|
||||||
this.productUOM = selected?.order_uom || this.defaultUOM
|
|
||||||
|
|
||||||
% if allow_item_discounts:
|
|
||||||
this.updateDiscount(selected.department_id)
|
|
||||||
% endif
|
|
||||||
|
|
||||||
// nb. hack to force refresh for vue3
|
|
||||||
this.refreshProductDescription += 1
|
|
||||||
this.refreshTotalPrice += 1
|
|
||||||
|
|
||||||
% if request.use_oruga:
|
|
||||||
this.itemDialogTab = 'quantity'
|
|
||||||
% else:
|
|
||||||
this.itemDialogTabIndex = 1
|
|
||||||
% endif
|
|
||||||
this.editItemShowDialog = true
|
|
||||||
},
|
|
||||||
|
|
||||||
% endif
|
|
||||||
|
|
||||||
itemDialogAttemptSave() {
|
itemDialogAttemptSave() {
|
||||||
this.itemDialogSaving = true
|
this.itemDialogSaving = true
|
||||||
this.editItemLoading = true
|
this.editItemLoading = true
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
order_qty: parseFloat(this.productQuantity),
|
order_qty: this.productQuantity,
|
||||||
order_uom: this.productUOM,
|
order_uom: this.productUOM,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1863,9 +1580,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
% if allow_item_discounts:
|
% if allow_item_discounts:
|
||||||
if (this.productDiscountPercent) {
|
params.discount_percent = this.productDiscountPercent
|
||||||
params.discount_percent = parseFloat(this.productDiscountPercent)
|
|
||||||
}
|
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
if (this.editItemRow) {
|
if (this.editItemRow) {
|
||||||
|
|
|
@ -35,7 +35,6 @@ def includeme(config):
|
||||||
})
|
})
|
||||||
|
|
||||||
# sideshow views
|
# sideshow views
|
||||||
config.include('sideshow.web.views.stores')
|
|
||||||
config.include('sideshow.web.views.customers')
|
config.include('sideshow.web.views.customers')
|
||||||
config.include('sideshow.web.views.products')
|
config.include('sideshow.web.views.products')
|
||||||
config.include('sideshow.web.views.orders')
|
config.include('sideshow.web.views.orders')
|
||||||
|
|
|
@ -126,10 +126,6 @@ class NewOrderBatchView(BatchMasterView):
|
||||||
'status_code',
|
'status_code',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, request, context=None):
|
|
||||||
super().__init__(request, context=context)
|
|
||||||
self.order_handler = self.app.get_order_handler()
|
|
||||||
|
|
||||||
def get_batch_handler(self):
|
def get_batch_handler(self):
|
||||||
""" """
|
""" """
|
||||||
# TODO: call self.app.get_batch_handler()
|
# TODO: call self.app.get_batch_handler()
|
||||||
|
@ -139,10 +135,6 @@ class NewOrderBatchView(BatchMasterView):
|
||||||
""" """
|
""" """
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
||||||
# store_id
|
|
||||||
if not self.order_handler.expose_store_id():
|
|
||||||
g.remove('store_id')
|
|
||||||
|
|
||||||
# total_price
|
# total_price
|
||||||
g.set_renderer('total_price', 'currency')
|
g.set_renderer('total_price', 'currency')
|
||||||
|
|
||||||
|
@ -150,10 +142,6 @@ class NewOrderBatchView(BatchMasterView):
|
||||||
""" """
|
""" """
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
|
|
||||||
# store_id
|
|
||||||
if not self.order_handler.expose_store_id():
|
|
||||||
f.remove('store_id')
|
|
||||||
|
|
||||||
# local_customer
|
# local_customer
|
||||||
f.set_node('local_customer', LocalCustomerRef(self.request))
|
f.set_node('local_customer', LocalCustomerRef(self.request))
|
||||||
|
|
||||||
|
|
|
@ -25,9 +25,7 @@ Views for Orders
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import decimal
|
import decimal
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
@ -37,9 +35,9 @@ from webhelpers2.html import tags, HTML
|
||||||
|
|
||||||
from wuttaweb.views import MasterView
|
from wuttaweb.views import MasterView
|
||||||
from wuttaweb.forms.schema import UserRef, WuttaMoney, WuttaQuantity, WuttaEnum, WuttaDictEnum
|
from wuttaweb.forms.schema import UserRef, WuttaMoney, WuttaQuantity, WuttaEnum, WuttaDictEnum
|
||||||
from wuttaweb.util import make_json_safe
|
|
||||||
|
|
||||||
from sideshow.db.model import Order, OrderItem
|
from sideshow.db.model import Order, OrderItem
|
||||||
|
from sideshow.orders import OrderHandler
|
||||||
from sideshow.batch.neworder import NewOrderBatchHandler
|
from sideshow.batch.neworder import NewOrderBatchHandler
|
||||||
from sideshow.web.forms.schema import (OrderRef,
|
from sideshow.web.forms.schema import (OrderRef,
|
||||||
LocalCustomerRef, LocalProductRef,
|
LocalCustomerRef, LocalProductRef,
|
||||||
|
@ -67,13 +65,13 @@ class OrderView(MasterView):
|
||||||
.. attribute:: order_handler
|
.. attribute:: order_handler
|
||||||
|
|
||||||
Reference to the :term:`order handler` as returned by
|
Reference to the :term:`order handler` as returned by
|
||||||
:meth:`~sideshow.app.SideshowAppProvider.get_order_handler()`.
|
:meth:`get_order_handler()`. This gets set in the constructor.
|
||||||
This gets set in the constructor.
|
|
||||||
|
|
||||||
.. attribute:: batch_handler
|
.. attribute:: batch_handler
|
||||||
|
|
||||||
Reference to the :term:`new order batch` handler. This gets
|
Reference to the :term:`new order batch` handler, as returned
|
||||||
set in the constructor.
|
by :meth:`get_batch_handler()`. This gets set in the
|
||||||
|
constructor.
|
||||||
"""
|
"""
|
||||||
model_class = Order
|
model_class = Order
|
||||||
editable = False
|
editable = False
|
||||||
|
@ -147,7 +145,6 @@ class OrderView(MasterView):
|
||||||
'brand_name',
|
'brand_name',
|
||||||
'description',
|
'description',
|
||||||
'size',
|
'size',
|
||||||
'department_id',
|
|
||||||
'department_name',
|
'department_name',
|
||||||
'vendor_name',
|
'vendor_name',
|
||||||
'vendor_item_code',
|
'vendor_item_code',
|
||||||
|
@ -158,17 +155,41 @@ class OrderView(MasterView):
|
||||||
|
|
||||||
def __init__(self, request, context=None):
|
def __init__(self, request, context=None):
|
||||||
super().__init__(request, context=context)
|
super().__init__(request, context=context)
|
||||||
self.order_handler = self.app.get_order_handler()
|
self.order_handler = self.get_order_handler()
|
||||||
self.batch_handler = self.app.get_batch_handler('neworder')
|
|
||||||
|
def get_order_handler(self):
|
||||||
|
"""
|
||||||
|
Returns the configured :term:`order handler`.
|
||||||
|
|
||||||
|
You normally would not need to call this, and can use
|
||||||
|
:attr:`order_handler` instead.
|
||||||
|
|
||||||
|
:rtype: :class:`~sideshow.orders.OrderHandler`
|
||||||
|
"""
|
||||||
|
if hasattr(self, 'order_handler'):
|
||||||
|
return self.order_handler
|
||||||
|
return OrderHandler(self.config)
|
||||||
|
|
||||||
|
def get_batch_handler(self):
|
||||||
|
"""
|
||||||
|
Returns the configured :term:`handler` for :term:`new order
|
||||||
|
batches <new order batch>`.
|
||||||
|
|
||||||
|
You normally would not need to call this, and can use
|
||||||
|
:attr:`batch_handler` instead.
|
||||||
|
|
||||||
|
:returns:
|
||||||
|
:class:`~sideshow.batch.neworder.NewOrderBatchHandler`
|
||||||
|
instance.
|
||||||
|
"""
|
||||||
|
if hasattr(self, 'batch_handler'):
|
||||||
|
return self.batch_handler
|
||||||
|
return self.app.get_batch_handler('neworder')
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
""" """
|
""" """
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
|
||||||
# store_id
|
|
||||||
if not self.order_handler.expose_store_id():
|
|
||||||
g.remove('store_id')
|
|
||||||
|
|
||||||
# order_id
|
# order_id
|
||||||
g.set_link('order_id')
|
g.set_link('order_id')
|
||||||
|
|
||||||
|
@ -202,7 +223,6 @@ class OrderView(MasterView):
|
||||||
|
|
||||||
* :meth:`start_over()`
|
* :meth:`start_over()`
|
||||||
* :meth:`cancel_order()`
|
* :meth:`cancel_order()`
|
||||||
* :meth:`set_store()`
|
|
||||||
* :meth:`assign_customer()`
|
* :meth:`assign_customer()`
|
||||||
* :meth:`unassign_customer()`
|
* :meth:`unassign_customer()`
|
||||||
* :meth:`set_pending_customer()`
|
* :meth:`set_pending_customer()`
|
||||||
|
@ -212,11 +232,10 @@ class OrderView(MasterView):
|
||||||
* :meth:`delete_item()`
|
* :meth:`delete_item()`
|
||||||
* :meth:`submit_order()`
|
* :meth:`submit_order()`
|
||||||
"""
|
"""
|
||||||
model = self.app.model
|
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
session = self.Session()
|
|
||||||
batch = self.get_current_batch()
|
|
||||||
self.creating = True
|
self.creating = True
|
||||||
|
self.batch_handler = self.get_batch_handler()
|
||||||
|
batch = self.get_current_batch()
|
||||||
|
|
||||||
context = self.get_context_customer(batch)
|
context = self.get_context_customer(batch)
|
||||||
|
|
||||||
|
@ -235,7 +254,6 @@ class OrderView(MasterView):
|
||||||
data = dict(self.request.json_body)
|
data = dict(self.request.json_body)
|
||||||
action = data.pop('action')
|
action = data.pop('action')
|
||||||
json_actions = [
|
json_actions = [
|
||||||
'set_store',
|
|
||||||
'assign_customer',
|
'assign_customer',
|
||||||
'unassign_customer',
|
'unassign_customer',
|
||||||
# 'update_phone_number',
|
# 'update_phone_number',
|
||||||
|
@ -244,7 +262,7 @@ class OrderView(MasterView):
|
||||||
# 'get_customer_info',
|
# 'get_customer_info',
|
||||||
# # 'set_customer_data',
|
# # 'set_customer_data',
|
||||||
'get_product_info',
|
'get_product_info',
|
||||||
'get_past_products',
|
# 'get_past_items',
|
||||||
'add_item',
|
'add_item',
|
||||||
'update_item',
|
'update_item',
|
||||||
'delete_item',
|
'delete_item',
|
||||||
|
@ -265,36 +283,20 @@ class OrderView(MasterView):
|
||||||
'normalized_batch': self.normalize_batch(batch),
|
'normalized_batch': self.normalize_batch(batch),
|
||||||
'order_items': [self.normalize_row(row)
|
'order_items': [self.normalize_row(row)
|
||||||
for row in batch.rows],
|
for row in batch.rows],
|
||||||
'default_uom_choices': self.batch_handler.get_default_uom_choices(),
|
'default_uom_choices': self.get_default_uom_choices(),
|
||||||
'default_uom': None, # TODO?
|
'default_uom': None, # TODO?
|
||||||
'expose_store_id': self.order_handler.expose_store_id(),
|
|
||||||
'allow_item_discounts': self.batch_handler.allow_item_discounts(),
|
'allow_item_discounts': self.batch_handler.allow_item_discounts(),
|
||||||
'allow_unknown_products': (self.batch_handler.allow_unknown_products()
|
'allow_unknown_products': (self.batch_handler.allow_unknown_products()
|
||||||
and self.has_perm('create_unknown_product')),
|
and self.has_perm('create_unknown_product')),
|
||||||
'pending_product_required_fields': self.get_pending_product_required_fields(),
|
'pending_product_required_fields': self.get_pending_product_required_fields(),
|
||||||
'allow_past_item_reorder': True, # TODO: make configurable?
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if context['expose_store_id']:
|
|
||||||
stores = session.query(model.Store)\
|
|
||||||
.filter(model.Store.archived == False)\
|
|
||||||
.order_by(model.Store.store_id)\
|
|
||||||
.all()
|
|
||||||
context['stores'] = [{'store_id': store.store_id, 'display': store.get_display()}
|
|
||||||
for store in stores]
|
|
||||||
|
|
||||||
# set default so things just work
|
|
||||||
if not batch.store_id:
|
|
||||||
batch.store_id = self.batch_handler.get_default_store_id()
|
|
||||||
|
|
||||||
if context['allow_item_discounts']:
|
if context['allow_item_discounts']:
|
||||||
context['allow_item_discounts_if_on_sale'] = self.batch_handler\
|
context['allow_item_discounts_if_on_sale'] = self.batch_handler\
|
||||||
.allow_item_discounts_if_on_sale()
|
.allow_item_discounts_if_on_sale()
|
||||||
# nb. render quantity so that '10.0' => '10'
|
# nb. render quantity so that '10.0' => '10'
|
||||||
context['default_item_discount'] = self.app.render_quantity(
|
context['default_item_discount'] = self.app.render_quantity(
|
||||||
self.batch_handler.get_default_item_discount())
|
self.batch_handler.get_default_item_discount())
|
||||||
context['dept_item_discounts'] = dict([(d['department_id'], d['default_item_discount'])
|
|
||||||
for d in self.get_dept_item_discounts()])
|
|
||||||
|
|
||||||
return self.render_to_response('create', context)
|
return self.render_to_response('create', context)
|
||||||
|
|
||||||
|
@ -350,7 +352,7 @@ class OrderView(MasterView):
|
||||||
if not term:
|
if not term:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
handler = self.batch_handler
|
handler = self.get_batch_handler()
|
||||||
if handler.use_local_customers():
|
if handler.use_local_customers():
|
||||||
return handler.autocomplete_customers_local(session, term, user=self.request.user)
|
return handler.autocomplete_customers_local(session, term, user=self.request.user)
|
||||||
else:
|
else:
|
||||||
|
@ -374,7 +376,7 @@ class OrderView(MasterView):
|
||||||
if not term:
|
if not term:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
handler = self.batch_handler
|
handler = self.get_batch_handler()
|
||||||
if handler.use_local_products():
|
if handler.use_local_products():
|
||||||
return handler.autocomplete_products_local(session, term, user=self.request.user)
|
return handler.autocomplete_products_local(session, term, user=self.request.user)
|
||||||
else:
|
else:
|
||||||
|
@ -392,43 +394,6 @@ class OrderView(MasterView):
|
||||||
required.append(field)
|
required.append(field)
|
||||||
return required
|
return required
|
||||||
|
|
||||||
def get_dept_item_discounts(self):
|
|
||||||
"""
|
|
||||||
Returns the list of per-department default item discount settings.
|
|
||||||
|
|
||||||
Each entry in the list will look like::
|
|
||||||
|
|
||||||
{
|
|
||||||
'department_id': '42',
|
|
||||||
'department_name': 'Grocery',
|
|
||||||
'default_item_discount': 10,
|
|
||||||
}
|
|
||||||
|
|
||||||
:returns: List of department settings as shown above.
|
|
||||||
"""
|
|
||||||
model = self.app.model
|
|
||||||
session = self.Session()
|
|
||||||
pattern = re.compile(r'^sideshow\.orders\.departments\.([^.]+)\.default_item_discount$')
|
|
||||||
|
|
||||||
dept_item_discounts = []
|
|
||||||
settings = session.query(model.Setting)\
|
|
||||||
.filter(model.Setting.name.like('sideshow.orders.departments.%.default_item_discount'))\
|
|
||||||
.all()
|
|
||||||
for setting in settings:
|
|
||||||
match = pattern.match(setting.name)
|
|
||||||
if not match:
|
|
||||||
log.warning("invalid setting name: %s", setting.name)
|
|
||||||
continue
|
|
||||||
deptid = match.group(1)
|
|
||||||
name = self.app.get_setting(session, f'sideshow.orders.departments.{deptid}.name')
|
|
||||||
dept_item_discounts.append({
|
|
||||||
'department_id': deptid,
|
|
||||||
'department_name': name,
|
|
||||||
'default_item_discount': setting.value,
|
|
||||||
})
|
|
||||||
dept_item_discounts.sort(key=lambda d: d['department_name'])
|
|
||||||
return dept_item_discounts
|
|
||||||
|
|
||||||
def start_over(self, batch):
|
def start_over(self, batch):
|
||||||
"""
|
"""
|
||||||
This will delete the user's current batch, then redirect user
|
This will delete the user's current batch, then redirect user
|
||||||
|
@ -471,26 +436,9 @@ class OrderView(MasterView):
|
||||||
url = self.get_index_url()
|
url = self.get_index_url()
|
||||||
return self.redirect(url)
|
return self.redirect(url)
|
||||||
|
|
||||||
def set_store(self, batch, data):
|
|
||||||
"""
|
|
||||||
Assign the
|
|
||||||
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.store_id`
|
|
||||||
for a batch.
|
|
||||||
|
|
||||||
This is a "batch action" method which may be called from
|
|
||||||
:meth:`create()`.
|
|
||||||
"""
|
|
||||||
store_id = data.get('store_id')
|
|
||||||
if not store_id:
|
|
||||||
return {'error': "Must provide store_id"}
|
|
||||||
|
|
||||||
batch.store_id = store_id
|
|
||||||
return self.get_context_customer(batch)
|
|
||||||
|
|
||||||
def get_context_customer(self, batch):
|
def get_context_customer(self, batch):
|
||||||
""" """
|
""" """
|
||||||
context = {
|
context = {
|
||||||
'store_id': batch.store_id,
|
|
||||||
'customer_is_known': True,
|
'customer_is_known': True,
|
||||||
'customer_id': None,
|
'customer_id': None,
|
||||||
'customer_name': batch.customer_name,
|
'customer_name': batch.customer_name,
|
||||||
|
@ -583,20 +531,18 @@ class OrderView(MasterView):
|
||||||
|
|
||||||
def get_product_info(self, batch, data):
|
def get_product_info(self, batch, data):
|
||||||
"""
|
"""
|
||||||
Fetch data for a specific product.
|
Fetch data for a specific product. (Nothing is modified.)
|
||||||
|
|
||||||
Depending on config, this calls one of the following to get
|
Depending on config, this will fetch a :term:`local product`
|
||||||
its primary data:
|
or :term:`external product` to get the data.
|
||||||
|
|
||||||
* :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.get_product_info_local()`
|
This should invoke a configured handler for the query
|
||||||
* :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.get_product_info_external()`
|
behavior, but that is not yet implemented. For now it uses
|
||||||
|
built-in logic only, which queries the
|
||||||
It then may supplement the data with additional fields.
|
:class:`~sideshow.db.model.products.LocalProduct` table.
|
||||||
|
|
||||||
This is a "batch action" method which may be called from
|
This is a "batch action" method which may be called from
|
||||||
:meth:`create()`.
|
:meth:`create()`.
|
||||||
|
|
||||||
:returns: Dict of product info.
|
|
||||||
"""
|
"""
|
||||||
product_id = data.get('product_id')
|
product_id = data.get('product_id')
|
||||||
if not product_id:
|
if not product_id:
|
||||||
|
@ -628,6 +574,9 @@ class OrderView(MasterView):
|
||||||
if 'case_price_quoted' in data and 'case_price_quoted_display' not in data:
|
if 'case_price_quoted' in data and 'case_price_quoted_display' not in data:
|
||||||
data['case_price_quoted_display'] = self.app.render_currency(data['case_price_quoted'])
|
data['case_price_quoted_display'] = self.app.render_currency(data['case_price_quoted'])
|
||||||
|
|
||||||
|
if 'default_item_discount' not in data:
|
||||||
|
data['default_item_discount'] = self.batch_handler.get_default_item_discount()
|
||||||
|
|
||||||
decimal_fields = [
|
decimal_fields = [
|
||||||
'case_size',
|
'case_size',
|
||||||
'unit_price_reg',
|
'unit_price_reg',
|
||||||
|
@ -644,22 +593,6 @@ class OrderView(MasterView):
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def get_past_products(self, batch, data):
|
|
||||||
"""
|
|
||||||
Fetch past products for convenient re-ordering.
|
|
||||||
|
|
||||||
This essentially calls
|
|
||||||
:meth:`~sideshow.batch.neworder.NewOrderBatchHandler.get_past_products()`
|
|
||||||
on the :attr:`batch_handler` and returns the result.
|
|
||||||
|
|
||||||
This is a "batch action" method which may be called from
|
|
||||||
:meth:`create()`.
|
|
||||||
|
|
||||||
:returns: List of product info dicts.
|
|
||||||
"""
|
|
||||||
past_products = self.batch_handler.get_past_products(batch)
|
|
||||||
return make_json_safe(past_products)
|
|
||||||
|
|
||||||
def add_item(self, batch, data):
|
def add_item(self, batch, data):
|
||||||
"""
|
"""
|
||||||
This adds a row to the user's current new order batch.
|
This adds a row to the user's current new order batch.
|
||||||
|
@ -776,6 +709,12 @@ class OrderView(MasterView):
|
||||||
'status_text': batch.status_text,
|
'status_text': batch.status_text,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_default_uom_choices(self):
|
||||||
|
""" """
|
||||||
|
enum = self.app.enum
|
||||||
|
return [{'key': key, 'value': val}
|
||||||
|
for key, val in enum.ORDER_UOM.items()]
|
||||||
|
|
||||||
def normalize_row(self, row):
|
def normalize_row(self, row):
|
||||||
""" """
|
""" """
|
||||||
data = {
|
data = {
|
||||||
|
@ -790,12 +729,12 @@ class OrderView(MasterView):
|
||||||
row.product_description,
|
row.product_description,
|
||||||
row.product_size),
|
row.product_size),
|
||||||
'product_weighed': row.product_weighed,
|
'product_weighed': row.product_weighed,
|
||||||
'department_id': row.department_id,
|
'department_display': row.department_name,
|
||||||
'department_name': row.department_name,
|
|
||||||
'special_order': row.special_order,
|
'special_order': row.special_order,
|
||||||
'case_size': float(row.case_size) if row.case_size is not None else None,
|
'case_size': float(row.case_size) if row.case_size is not None else None,
|
||||||
'order_qty': float(row.order_qty),
|
'order_qty': float(row.order_qty),
|
||||||
'order_uom': row.order_uom,
|
'order_uom': row.order_uom,
|
||||||
|
'order_uom_choices': self.get_default_uom_choices(),
|
||||||
'discount_percent': self.app.render_quantity(row.discount_percent),
|
'discount_percent': self.app.render_quantity(row.discount_percent),
|
||||||
'unit_price_quoted': float(row.unit_price_quoted) if row.unit_price_quoted is not None else None,
|
'unit_price_quoted': float(row.unit_price_quoted) if row.unit_price_quoted is not None else None,
|
||||||
'unit_price_quoted_display': self.app.render_currency(row.unit_price_quoted),
|
'unit_price_quoted_display': self.app.render_currency(row.unit_price_quoted),
|
||||||
|
@ -871,10 +810,6 @@ class OrderView(MasterView):
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
order = f.model_instance
|
order = f.model_instance
|
||||||
|
|
||||||
# store_id
|
|
||||||
if not self.order_handler.expose_store_id():
|
|
||||||
f.remove('store_id')
|
|
||||||
|
|
||||||
# local_customer
|
# local_customer
|
||||||
if order.customer_id and not order.local_customer:
|
if order.customer_id and not order.local_customer:
|
||||||
f.remove('local_customer')
|
f.remove('local_customer')
|
||||||
|
@ -975,10 +910,8 @@ class OrderView(MasterView):
|
||||||
""" """
|
""" """
|
||||||
settings = [
|
settings = [
|
||||||
|
|
||||||
# stores
|
# batches
|
||||||
{'name': 'sideshow.orders.expose_store_id',
|
{'name': 'wutta.batch.neworder.handler.spec'},
|
||||||
'type': bool},
|
|
||||||
{'name': 'sideshow.orders.default_store_id'},
|
|
||||||
|
|
||||||
# customers
|
# customers
|
||||||
{'name': 'sideshow.orders.use_local_customers',
|
{'name': 'sideshow.orders.use_local_customers',
|
||||||
|
@ -987,6 +920,12 @@ class OrderView(MasterView):
|
||||||
'default': 'true'},
|
'default': 'true'},
|
||||||
|
|
||||||
# products
|
# products
|
||||||
|
{'name': 'sideshow.orders.allow_item_discounts',
|
||||||
|
'type': bool},
|
||||||
|
{'name': 'sideshow.orders.allow_item_discounts_if_on_sale',
|
||||||
|
'type': bool},
|
||||||
|
{'name': 'sideshow.orders.default_item_discount',
|
||||||
|
'type': float},
|
||||||
{'name': 'sideshow.orders.use_local_products',
|
{'name': 'sideshow.orders.use_local_products',
|
||||||
# nb. this is really a bool but we present as string in config UI
|
# nb. this is really a bool but we present as string in config UI
|
||||||
#'type': bool,
|
#'type': bool,
|
||||||
|
@ -994,17 +933,6 @@ class OrderView(MasterView):
|
||||||
{'name': 'sideshow.orders.allow_unknown_products',
|
{'name': 'sideshow.orders.allow_unknown_products',
|
||||||
'type': bool,
|
'type': bool,
|
||||||
'default': True},
|
'default': True},
|
||||||
|
|
||||||
# pricing
|
|
||||||
{'name': 'sideshow.orders.allow_item_discounts',
|
|
||||||
'type': bool},
|
|
||||||
{'name': 'sideshow.orders.allow_item_discounts_if_on_sale',
|
|
||||||
'type': bool},
|
|
||||||
{'name': 'sideshow.orders.default_item_discount',
|
|
||||||
'type': float},
|
|
||||||
|
|
||||||
# batches
|
|
||||||
{'name': 'wutta.batch.neworder.handler.spec'},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# required fields for new product entry
|
# required fields for new product entry
|
||||||
|
@ -1027,39 +955,8 @@ class OrderView(MasterView):
|
||||||
handlers = [{'spec': spec} for spec in handlers]
|
handlers = [{'spec': spec} for spec in handlers]
|
||||||
context['batch_handlers'] = handlers
|
context['batch_handlers'] = handlers
|
||||||
|
|
||||||
context['dept_item_discounts'] = self.get_dept_item_discounts()
|
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def configure_gather_settings(self, data, simple_settings=None):
|
|
||||||
""" """
|
|
||||||
settings = super().configure_gather_settings(data, simple_settings=simple_settings)
|
|
||||||
|
|
||||||
for dept in json.loads(data['dept_item_discounts']):
|
|
||||||
deptid = dept['department_id']
|
|
||||||
settings.append({'name': f'sideshow.orders.departments.{deptid}.name',
|
|
||||||
'value': dept['department_name']})
|
|
||||||
settings.append({'name': f'sideshow.orders.departments.{deptid}.default_item_discount',
|
|
||||||
'value': dept['default_item_discount']})
|
|
||||||
|
|
||||||
return settings
|
|
||||||
|
|
||||||
def configure_remove_settings(self, **kwargs):
|
|
||||||
""" """
|
|
||||||
model = self.app.model
|
|
||||||
session = self.Session()
|
|
||||||
|
|
||||||
super().configure_remove_settings(**kwargs)
|
|
||||||
|
|
||||||
to_delete = session.query(model.Setting)\
|
|
||||||
.filter(sa.or_(
|
|
||||||
model.Setting.name.like('sideshow.orders.departments.%.name'),
|
|
||||||
model.Setting.name.like('sideshow.orders.departments.%.default_item_discount')))\
|
|
||||||
.all()
|
|
||||||
for setting in to_delete:
|
|
||||||
self.app.delete_setting(session, setting.name)
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def defaults(cls, config):
|
def defaults(cls, config):
|
||||||
cls._order_defaults(config)
|
cls._order_defaults(config)
|
||||||
|
@ -1140,7 +1037,6 @@ class OrderItemView(MasterView):
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
'order_id': "Order ID",
|
'order_id': "Order ID",
|
||||||
'store_id': "Store ID",
|
|
||||||
'product_id': "Product ID",
|
'product_id': "Product ID",
|
||||||
'product_scancode': "Scancode",
|
'product_scancode': "Scancode",
|
||||||
'product_brand': "Brand",
|
'product_brand': "Brand",
|
||||||
|
@ -1154,7 +1050,6 @@ class OrderItemView(MasterView):
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
'order_id',
|
'order_id',
|
||||||
'store_id',
|
|
||||||
'customer_name',
|
'customer_name',
|
||||||
# 'sequence',
|
# 'sequence',
|
||||||
'product_scancode',
|
'product_scancode',
|
||||||
|
@ -1204,7 +1099,20 @@ class OrderItemView(MasterView):
|
||||||
|
|
||||||
def __init__(self, request, context=None):
|
def __init__(self, request, context=None):
|
||||||
super().__init__(request, context=context)
|
super().__init__(request, context=context)
|
||||||
self.order_handler = self.app.get_order_handler()
|
self.order_handler = self.get_order_handler()
|
||||||
|
|
||||||
|
def get_order_handler(self):
|
||||||
|
"""
|
||||||
|
Returns the configured :term:`order handler`.
|
||||||
|
|
||||||
|
You normally would not need to call this, and can use
|
||||||
|
:attr:`order_handler` instead.
|
||||||
|
|
||||||
|
:rtype: :class:`~sideshow.orders.OrderHandler`
|
||||||
|
"""
|
||||||
|
if hasattr(self, 'order_handler'):
|
||||||
|
return self.order_handler
|
||||||
|
return OrderHandler(self.config)
|
||||||
|
|
||||||
def get_fallback_templates(self, template):
|
def get_fallback_templates(self, template):
|
||||||
""" """
|
""" """
|
||||||
|
@ -1224,19 +1132,11 @@ class OrderItemView(MasterView):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
# enum = self.app.enum
|
# enum = self.app.enum
|
||||||
|
|
||||||
# store_id
|
|
||||||
if not self.order_handler.expose_store_id():
|
|
||||||
g.remove('store_id')
|
|
||||||
|
|
||||||
# order_id
|
# order_id
|
||||||
g.set_sorter('order_id', model.Order.order_id)
|
g.set_sorter('order_id', model.Order.order_id)
|
||||||
g.set_renderer('order_id', self.render_order_attr)
|
g.set_renderer('order_id', self.render_order_id)
|
||||||
g.set_link('order_id')
|
g.set_link('order_id')
|
||||||
|
|
||||||
# store_id
|
|
||||||
g.set_sorter('store_id', model.Order.store_id)
|
|
||||||
g.set_renderer('store_id', self.render_order_attr)
|
|
||||||
|
|
||||||
# customer_name
|
# customer_name
|
||||||
g.set_label('customer_name', "Customer", column_only=True)
|
g.set_label('customer_name', "Customer", column_only=True)
|
||||||
|
|
||||||
|
@ -1265,10 +1165,9 @@ class OrderItemView(MasterView):
|
||||||
# status_code
|
# status_code
|
||||||
g.set_renderer('status_code', self.render_status_code)
|
g.set_renderer('status_code', self.render_status_code)
|
||||||
|
|
||||||
def render_order_attr(self, item, key, value):
|
def render_order_id(self, item, key, value):
|
||||||
""" """
|
""" """
|
||||||
order = item.order
|
return item.order.order_id
|
||||||
return getattr(order, key)
|
|
||||||
|
|
||||||
def render_status_code(self, item, key, value):
|
def render_status_code(self, item, key, value):
|
||||||
""" """
|
""" """
|
||||||
|
@ -1338,8 +1237,6 @@ class OrderItemView(MasterView):
|
||||||
item = context['instance']
|
item = context['instance']
|
||||||
form = context['form']
|
form = context['form']
|
||||||
|
|
||||||
context['expose_store_id'] = self.order_handler.expose_store_id()
|
|
||||||
|
|
||||||
context['item'] = item
|
context['item'] = item
|
||||||
context['order'] = item.order
|
context['order'] = item.order
|
||||||
context['order_qty_uom_text'] = self.order_handler.get_order_qty_uom_text(
|
context['order_qty_uom_text'] = self.order_handler.get_order_qty_uom_text(
|
||||||
|
|
|
@ -235,7 +235,6 @@ class PendingProductView(MasterView):
|
||||||
url_prefix = '/pending/products'
|
url_prefix = '/pending/products'
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
'department_id': "Department ID",
|
|
||||||
'product_id': "Product ID",
|
'product_id': "Product ID",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,120 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
################################################################################
|
|
||||||
#
|
|
||||||
# Sideshow -- Case/Special Order Tracker
|
|
||||||
# Copyright © 2024-2025 Lance Edgar
|
|
||||||
#
|
|
||||||
# This file is part of Sideshow.
|
|
||||||
#
|
|
||||||
# Sideshow is free software: you can redistribute it and/or modify it
|
|
||||||
# under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# Sideshow is distributed in the hope that it will be useful, but
|
|
||||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
||||||
# General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with Sideshow. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
Views for Stores
|
|
||||||
"""
|
|
||||||
|
|
||||||
from wuttaweb.views import MasterView
|
|
||||||
|
|
||||||
from sideshow.db.model import Store
|
|
||||||
|
|
||||||
|
|
||||||
class StoreView(MasterView):
|
|
||||||
"""
|
|
||||||
Master view for
|
|
||||||
:class:`~sideshow.db.model.stores.Store`; route prefix
|
|
||||||
is ``stores``.
|
|
||||||
|
|
||||||
Notable URLs provided by this class:
|
|
||||||
|
|
||||||
* ``/stores/``
|
|
||||||
* ``/stores/new``
|
|
||||||
* ``/stores/XXX``
|
|
||||||
* ``/stores/XXX/edit``
|
|
||||||
* ``/stores/XXX/delete``
|
|
||||||
"""
|
|
||||||
model_class = Store
|
|
||||||
|
|
||||||
labels = {
|
|
||||||
'store_id': "Store ID",
|
|
||||||
}
|
|
||||||
|
|
||||||
filter_defaults = {
|
|
||||||
'archived': {'active': True, 'verb': 'is_false'},
|
|
||||||
}
|
|
||||||
|
|
||||||
sort_defaults = 'store_id'
|
|
||||||
|
|
||||||
def configure_grid(self, g):
|
|
||||||
""" """
|
|
||||||
super().configure_grid(g)
|
|
||||||
|
|
||||||
# links
|
|
||||||
g.set_link('store_id')
|
|
||||||
g.set_link('name')
|
|
||||||
|
|
||||||
def grid_row_class(self, store, data, i):
|
|
||||||
""" """
|
|
||||||
if store.archived:
|
|
||||||
return 'has-background-warning'
|
|
||||||
|
|
||||||
def configure_form(self, f):
|
|
||||||
""" """
|
|
||||||
super().configure_form(f)
|
|
||||||
|
|
||||||
# store_id
|
|
||||||
f.set_validator('store_id', self.unique_store_id)
|
|
||||||
|
|
||||||
# name
|
|
||||||
f.set_validator('name', self.unique_name)
|
|
||||||
|
|
||||||
def unique_store_id(self, node, value):
|
|
||||||
""" """
|
|
||||||
model = self.app.model
|
|
||||||
session = self.Session()
|
|
||||||
|
|
||||||
query = session.query(model.Store)\
|
|
||||||
.filter(model.Store.store_id == value)
|
|
||||||
|
|
||||||
if self.editing:
|
|
||||||
uuid = self.request.matchdict['uuid']
|
|
||||||
query = query.filter(model.Store.uuid != uuid)
|
|
||||||
|
|
||||||
if query.count():
|
|
||||||
node.raise_invalid("Store ID must be unique")
|
|
||||||
|
|
||||||
def unique_name(self, node, value):
|
|
||||||
""" """
|
|
||||||
model = self.app.model
|
|
||||||
session = self.Session()
|
|
||||||
|
|
||||||
query = session.query(model.Store)\
|
|
||||||
.filter(model.Store.name == value)
|
|
||||||
|
|
||||||
if self.editing:
|
|
||||||
uuid = self.request.matchdict['uuid']
|
|
||||||
query = query.filter(model.Store.uuid != uuid)
|
|
||||||
|
|
||||||
if query.count():
|
|
||||||
node.raise_invalid("Name must be unique")
|
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
|
||||||
base = globals()
|
|
||||||
|
|
||||||
StoreView = kwargs.get('StoreView', base['StoreView'])
|
|
||||||
StoreView.defaults(config)
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
|
||||||
defaults(config)
|
|
|
@ -4,8 +4,6 @@ import datetime
|
||||||
import decimal
|
import decimal
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
from wuttjamaican.testing import DataTestCase
|
from wuttjamaican.testing import DataTestCase
|
||||||
|
|
||||||
from sideshow.batch import neworder as mod
|
from sideshow.batch import neworder as mod
|
||||||
|
@ -22,16 +20,6 @@ class TestNewOrderBatchHandler(DataTestCase):
|
||||||
def make_handler(self):
|
def make_handler(self):
|
||||||
return mod.NewOrderBatchHandler(self.config)
|
return mod.NewOrderBatchHandler(self.config)
|
||||||
|
|
||||||
def test_get_default_store_id(self):
|
|
||||||
handler = self.make_handler()
|
|
||||||
|
|
||||||
# null by default
|
|
||||||
self.assertIsNone(handler.get_default_store_id())
|
|
||||||
|
|
||||||
# whatever is configured
|
|
||||||
self.config.setdefault('sideshow.orders.default_store_id', '042')
|
|
||||||
self.assertEqual(handler.get_default_store_id(), '042')
|
|
||||||
|
|
||||||
def test_use_local_customers(self):
|
def test_use_local_customers(self):
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
|
|
||||||
|
@ -120,23 +108,6 @@ class TestNewOrderBatchHandler(DataTestCase):
|
||||||
# search for sally finds nothing
|
# search for sally finds nothing
|
||||||
self.assertEqual(handler.autocomplete_customers_local(self.session, 'sally'), [])
|
self.assertEqual(handler.autocomplete_customers_local(self.session, 'sally'), [])
|
||||||
|
|
||||||
def test_init_batch(self):
|
|
||||||
model = self.app.model
|
|
||||||
handler = self.make_handler()
|
|
||||||
|
|
||||||
# store_id is null by default
|
|
||||||
batch = handler.model_class()
|
|
||||||
self.assertIsNone(batch.store_id)
|
|
||||||
handler.init_batch(batch)
|
|
||||||
self.assertIsNone(batch.store_id)
|
|
||||||
|
|
||||||
# but default can be configured
|
|
||||||
self.config.setdefault('sideshow.orders.default_store_id', '042')
|
|
||||||
batch = handler.model_class()
|
|
||||||
self.assertIsNone(batch.store_id)
|
|
||||||
handler.init_batch(batch)
|
|
||||||
self.assertEqual(batch.store_id, '042')
|
|
||||||
|
|
||||||
def test_set_customer(self):
|
def test_set_customer(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
|
@ -269,14 +240,6 @@ class TestNewOrderBatchHandler(DataTestCase):
|
||||||
# search for juice finds nothing
|
# search for juice finds nothing
|
||||||
self.assertEqual(handler.autocomplete_products_local(self.session, 'juice'), [])
|
self.assertEqual(handler.autocomplete_products_local(self.session, 'juice'), [])
|
||||||
|
|
||||||
def test_get_default_uom_choices(self):
|
|
||||||
enum = self.app.enum
|
|
||||||
handler = self.make_handler()
|
|
||||||
|
|
||||||
uoms = handler.get_default_uom_choices()
|
|
||||||
self.assertEqual(uoms, [{'key': key, 'value': val}
|
|
||||||
for key, val in enum.ORDER_UOM.items()])
|
|
||||||
|
|
||||||
def test_get_product_info_external(self):
|
def test_get_product_info_external(self):
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
self.assertRaises(NotImplementedError, handler.get_product_info_external,
|
self.assertRaises(NotImplementedError, handler.get_product_info_external,
|
||||||
|
@ -318,174 +281,6 @@ class TestNewOrderBatchHandler(DataTestCase):
|
||||||
mock_uuid = self.app.make_true_uuid()
|
mock_uuid = self.app.make_true_uuid()
|
||||||
self.assertRaises(ValueError, handler.get_product_info_local, self.session, mock_uuid.hex)
|
self.assertRaises(ValueError, handler.get_product_info_local, self.session, mock_uuid.hex)
|
||||||
|
|
||||||
def test_normalize_local_product(self):
|
|
||||||
model = self.app.model
|
|
||||||
handler = self.make_handler()
|
|
||||||
|
|
||||||
product = model.LocalProduct(scancode='07430500132',
|
|
||||||
brand_name="Bragg's",
|
|
||||||
description="Apple Cider Vinegar",
|
|
||||||
size="32oz",
|
|
||||||
department_name="Grocery",
|
|
||||||
case_size=12,
|
|
||||||
unit_price_reg=5.99,
|
|
||||||
vendor_name="UNFI",
|
|
||||||
vendor_item_code='1234')
|
|
||||||
self.session.add(product)
|
|
||||||
self.session.flush()
|
|
||||||
|
|
||||||
info = handler.normalize_local_product(product)
|
|
||||||
self.assertIsInstance(info, dict)
|
|
||||||
self.assertEqual(info['product_id'], product.uuid.hex)
|
|
||||||
for prop in sa.inspect(model.LocalProduct).column_attrs:
|
|
||||||
if prop.key == 'uuid':
|
|
||||||
continue
|
|
||||||
if prop.key not in info:
|
|
||||||
continue
|
|
||||||
self.assertEqual(info[prop.key], getattr(product, prop.key))
|
|
||||||
|
|
||||||
def test_get_past_orders(self):
|
|
||||||
model = self.app.model
|
|
||||||
handler = self.make_handler()
|
|
||||||
|
|
||||||
user = model.User(username='barney')
|
|
||||||
self.session.add(user)
|
|
||||||
batch = handler.make_batch(self.session, created_by=user)
|
|
||||||
self.session.add(batch)
|
|
||||||
self.session.flush()
|
|
||||||
|
|
||||||
# ..will test local customers first
|
|
||||||
|
|
||||||
# error if no customer
|
|
||||||
self.assertRaises(ValueError, handler.get_past_orders, batch)
|
|
||||||
|
|
||||||
# empty history for customer
|
|
||||||
customer = model.LocalCustomer(full_name='Fred Flintstone')
|
|
||||||
batch.local_customer = customer
|
|
||||||
self.session.flush()
|
|
||||||
orders = handler.get_past_orders(batch)
|
|
||||||
self.assertEqual(len(orders), 0)
|
|
||||||
|
|
||||||
# mock historical order
|
|
||||||
order = model.Order(order_id=42, local_customer=customer, created_by=user)
|
|
||||||
self.session.add(order)
|
|
||||||
self.session.flush()
|
|
||||||
|
|
||||||
# that should now be returned
|
|
||||||
orders = handler.get_past_orders(batch)
|
|
||||||
self.assertEqual(len(orders), 1)
|
|
||||||
self.assertIs(orders[0], order)
|
|
||||||
|
|
||||||
# ..now we test external customers, w/ new batch
|
|
||||||
with patch.object(handler, 'use_local_customers', return_value=False):
|
|
||||||
batch2 = handler.make_batch(self.session, created_by=user)
|
|
||||||
self.session.add(batch2)
|
|
||||||
self.session.flush()
|
|
||||||
|
|
||||||
# error if no customer
|
|
||||||
self.assertRaises(ValueError, handler.get_past_orders, batch2)
|
|
||||||
|
|
||||||
# empty history for customer
|
|
||||||
batch2.customer_id = '123'
|
|
||||||
self.session.flush()
|
|
||||||
orders = handler.get_past_orders(batch2)
|
|
||||||
self.assertEqual(len(orders), 0)
|
|
||||||
|
|
||||||
# mock historical order
|
|
||||||
order2 = model.Order(order_id=42, customer_id='123', created_by=user)
|
|
||||||
self.session.add(order2)
|
|
||||||
self.session.flush()
|
|
||||||
|
|
||||||
# that should now be returned
|
|
||||||
orders = handler.get_past_orders(batch2)
|
|
||||||
self.assertEqual(len(orders), 1)
|
|
||||||
self.assertIs(orders[0], order2)
|
|
||||||
|
|
||||||
def test_get_past_products(self):
|
|
||||||
model = self.app.model
|
|
||||||
enum = self.app.enum
|
|
||||||
handler = self.make_handler()
|
|
||||||
|
|
||||||
user = model.User(username='barney')
|
|
||||||
self.session.add(user)
|
|
||||||
batch = handler.make_batch(self.session, created_by=user)
|
|
||||||
self.session.add(batch)
|
|
||||||
self.session.flush()
|
|
||||||
|
|
||||||
# (nb. this all assumes local customers)
|
|
||||||
|
|
||||||
# ..will test local products first
|
|
||||||
|
|
||||||
# error if no customer
|
|
||||||
self.assertRaises(ValueError, handler.get_past_products, batch)
|
|
||||||
|
|
||||||
# empty history for customer
|
|
||||||
customer = model.LocalCustomer(full_name='Fred Flintstone')
|
|
||||||
batch.local_customer = customer
|
|
||||||
self.session.flush()
|
|
||||||
products = handler.get_past_products(batch)
|
|
||||||
self.assertEqual(len(products), 0)
|
|
||||||
|
|
||||||
# mock historical order
|
|
||||||
order = model.Order(order_id=42, local_customer=customer, created_by=user)
|
|
||||||
product = model.LocalProduct(scancode='07430500132', description='Vinegar',
|
|
||||||
unit_price_reg=5.99, case_size=12)
|
|
||||||
item = model.OrderItem(local_product=product, order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
|
||||||
status_code=enum.ORDER_ITEM_STATUS_READY)
|
|
||||||
order.items.append(item)
|
|
||||||
self.session.add(order)
|
|
||||||
self.session.flush()
|
|
||||||
self.session.refresh(product)
|
|
||||||
|
|
||||||
# that should now be returned
|
|
||||||
products = handler.get_past_products(batch)
|
|
||||||
self.assertEqual(len(products), 1)
|
|
||||||
self.assertEqual(products[0]['product_id'], product.uuid.hex)
|
|
||||||
self.assertEqual(products[0]['scancode'], '07430500132')
|
|
||||||
self.assertEqual(products[0]['description'], 'Vinegar')
|
|
||||||
self.assertEqual(products[0]['case_price_quoted'], decimal.Decimal('71.88'))
|
|
||||||
self.assertEqual(products[0]['case_price_quoted_display'], '$71.88')
|
|
||||||
|
|
||||||
# ..now we test external products, w/ new batch
|
|
||||||
with patch.object(handler, 'use_local_products', return_value=False):
|
|
||||||
batch2 = handler.make_batch(self.session, created_by=user)
|
|
||||||
self.session.add(batch2)
|
|
||||||
self.session.flush()
|
|
||||||
|
|
||||||
# error if no customer
|
|
||||||
self.assertRaises(ValueError, handler.get_past_products, batch2)
|
|
||||||
|
|
||||||
# empty history for customer
|
|
||||||
batch2.local_customer = customer
|
|
||||||
self.session.flush()
|
|
||||||
products = handler.get_past_products(batch2)
|
|
||||||
self.assertEqual(len(products), 0)
|
|
||||||
|
|
||||||
# mock historical order
|
|
||||||
order2 = model.Order(order_id=44, local_customer=customer, created_by=user)
|
|
||||||
self.session.add(order2)
|
|
||||||
item2 = model.OrderItem(product_id='07430500116',
|
|
||||||
order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
|
||||||
status_code=enum.ORDER_ITEM_STATUS_READY)
|
|
||||||
order2.items.append(item2)
|
|
||||||
self.session.flush()
|
|
||||||
|
|
||||||
# its product should now be returned
|
|
||||||
with patch.object(handler, 'get_product_info_external', return_value={
|
|
||||||
'product_id': '07430500116',
|
|
||||||
'scancode': '07430500116',
|
|
||||||
'description': 'VINEGAR',
|
|
||||||
'unit_price_reg': decimal.Decimal('3.99'),
|
|
||||||
'case_size': 12,
|
|
||||||
}):
|
|
||||||
products = handler.get_past_products(batch2)
|
|
||||||
self.assertEqual(len(products), 1)
|
|
||||||
self.assertEqual(products[0]['product_id'], '07430500116')
|
|
||||||
self.assertEqual(products[0]['scancode'], '07430500116')
|
|
||||||
self.assertEqual(products[0]['description'], 'VINEGAR')
|
|
||||||
self.assertEqual(products[0]['case_price_quoted'], decimal.Decimal('47.88'))
|
|
||||||
self.assertEqual(products[0]['case_price_quoted_display'], '$47.88')
|
|
||||||
|
|
||||||
def test_add_item(self):
|
def test_add_item(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
|
|
||||||
from wuttjamaican.testing import DataTestCase
|
|
||||||
|
|
||||||
from sideshow.db.model import stores as mod
|
|
||||||
|
|
||||||
|
|
||||||
class TestPendingCustomer(DataTestCase):
|
|
||||||
|
|
||||||
def test_str(self):
|
|
||||||
store = mod.Store()
|
|
||||||
self.assertEqual(str(store), "")
|
|
||||||
|
|
||||||
store.name = "Acme Goods"
|
|
||||||
self.assertEqual(str(store), "Acme Goods")
|
|
||||||
|
|
||||||
store.store_id = "001"
|
|
||||||
self.assertEqual(str(store), "001 Acme Goods")
|
|
||||||
|
|
||||||
def test_get_display(self):
|
|
||||||
store = mod.Store()
|
|
||||||
self.assertEqual(store.get_display(), "")
|
|
||||||
|
|
||||||
store.name = "Acme Goods"
|
|
||||||
self.assertEqual(store.get_display(), "Acme Goods")
|
|
||||||
|
|
||||||
store.store_id = "001"
|
|
||||||
self.assertEqual(store.get_display(), "001 Acme Goods")
|
|
|
@ -1,17 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
|
|
||||||
from wuttjamaican.testing import ConfigTestCase
|
|
||||||
|
|
||||||
from sideshow import app as mod
|
|
||||||
from sideshow.orders import OrderHandler
|
|
||||||
|
|
||||||
|
|
||||||
class TestSideshowAppProvider(ConfigTestCase):
|
|
||||||
|
|
||||||
def make_provider(self):
|
|
||||||
return mod.SideshowAppProvider(self.config)
|
|
||||||
|
|
||||||
def test_get_order_handler(self):
|
|
||||||
provider = self.make_provider()
|
|
||||||
handler = provider.get_order_handler()
|
|
||||||
self.assertIsInstance(handler, OrderHandler)
|
|
|
@ -16,16 +16,6 @@ class TestOrderHandler(DataTestCase):
|
||||||
def make_handler(self):
|
def make_handler(self):
|
||||||
return mod.OrderHandler(self.config)
|
return mod.OrderHandler(self.config)
|
||||||
|
|
||||||
def test_expose_store_id(self):
|
|
||||||
handler = self.make_handler()
|
|
||||||
|
|
||||||
# false by default
|
|
||||||
self.assertFalse(handler.expose_store_id())
|
|
||||||
|
|
||||||
# config can enable
|
|
||||||
self.config.setdefault('sideshow.orders.expose_store_id', 'true')
|
|
||||||
self.assertTrue(handler.expose_store_id())
|
|
||||||
|
|
||||||
def test_get_order_qty_uom_text(self):
|
def test_get_order_qty_uom_text(self):
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
|
|
|
@ -30,19 +30,10 @@ class TestNewOrderBatchView(WebTestCase):
|
||||||
def test_configure_grid(self):
|
def test_configure_grid(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
|
||||||
# store_id not exposed by default
|
|
||||||
grid = view.make_grid(model_class=model.NewOrderBatch)
|
grid = view.make_grid(model_class=model.NewOrderBatch)
|
||||||
self.assertIn('store_id', grid.columns)
|
self.assertNotIn('total_price', grid.renderers)
|
||||||
view.configure_grid(grid)
|
view.configure_grid(grid)
|
||||||
self.assertNotIn('store_id', grid.columns)
|
self.assertIn('total_price', grid.renderers)
|
||||||
|
|
||||||
# store_id is exposed if configured
|
|
||||||
self.config.setdefault('sideshow.orders.expose_store_id', 'true')
|
|
||||||
grid = view.make_grid(model_class=model.NewOrderBatch)
|
|
||||||
self.assertIn('store_id', grid.columns)
|
|
||||||
view.configure_grid(grid)
|
|
||||||
self.assertIn('store_id', grid.columns)
|
|
||||||
|
|
||||||
def test_configure_form(self):
|
def test_configure_form(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
@ -67,19 +58,6 @@ class TestNewOrderBatchView(WebTestCase):
|
||||||
self.assertIsInstance(schema['pending_customer'].typ, PendingCustomerRef)
|
self.assertIsInstance(schema['pending_customer'].typ, PendingCustomerRef)
|
||||||
self.assertIsInstance(schema['total_price'].typ, WuttaMoney)
|
self.assertIsInstance(schema['total_price'].typ, WuttaMoney)
|
||||||
|
|
||||||
# store_id not exposed by default
|
|
||||||
form = view.make_form(model_instance=batch)
|
|
||||||
self.assertIn('store_id', form)
|
|
||||||
view.configure_form(form)
|
|
||||||
self.assertNotIn('store_id', form)
|
|
||||||
|
|
||||||
# store_id is exposed if configured
|
|
||||||
self.config.setdefault('sideshow.orders.expose_store_id', 'true')
|
|
||||||
form = view.make_form(model_instance=batch)
|
|
||||||
self.assertIn('store_id', form)
|
|
||||||
view.configure_form(form)
|
|
||||||
self.assertIn('store_id', form)
|
|
||||||
|
|
||||||
def test_configure_row_grid(self):
|
def test_configure_row_grid(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
import json
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
|
@ -16,7 +15,6 @@ from sideshow.orders import OrderHandler
|
||||||
from sideshow.testing import WebTestCase
|
from sideshow.testing import WebTestCase
|
||||||
from sideshow.web.views import orders as mod
|
from sideshow.web.views import orders as mod
|
||||||
from sideshow.web.forms.schema import OrderRef, PendingProductRef
|
from sideshow.web.forms.schema import OrderRef, PendingProductRef
|
||||||
from sideshow.config import SideshowConfig
|
|
||||||
|
|
||||||
|
|
||||||
class TestIncludeme(WebTestCase):
|
class TestIncludeme(WebTestCase):
|
||||||
|
@ -27,39 +25,33 @@ class TestIncludeme(WebTestCase):
|
||||||
|
|
||||||
class TestOrderView(WebTestCase):
|
class TestOrderView(WebTestCase):
|
||||||
|
|
||||||
def make_config(self, **kw):
|
|
||||||
config = super().make_config(**kw)
|
|
||||||
SideshowConfig().configure(config)
|
|
||||||
return config
|
|
||||||
|
|
||||||
def make_view(self):
|
def make_view(self):
|
||||||
return mod.OrderView(self.request)
|
return mod.OrderView(self.request)
|
||||||
|
|
||||||
def make_handler(self):
|
def make_handler(self):
|
||||||
return NewOrderBatchHandler(self.config)
|
return NewOrderBatchHandler(self.config)
|
||||||
|
|
||||||
|
def test_order_handler(self):
|
||||||
|
view = self.make_view()
|
||||||
|
handler = view.order_handler
|
||||||
|
self.assertIsInstance(handler, OrderHandler)
|
||||||
|
handler2 = view.get_order_handler()
|
||||||
|
self.assertIs(handler2, handler)
|
||||||
|
|
||||||
def test_configure_grid(self):
|
def test_configure_grid(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
grid = view.make_grid(model_class=model.PendingProduct)
|
||||||
# store_id hidden by default
|
self.assertNotIn('order_id', grid.linked_columns)
|
||||||
grid = view.make_grid(model_class=model.Order, columns=['store_id', 'order_id'])
|
self.assertNotIn('total_price', grid.renderers)
|
||||||
self.assertIn('store_id', grid.columns)
|
|
||||||
view.configure_grid(grid)
|
view.configure_grid(grid)
|
||||||
self.assertNotIn('store_id', grid.columns)
|
self.assertIn('order_id', grid.linked_columns)
|
||||||
|
self.assertIn('total_price', grid.renderers)
|
||||||
# store_id is shown if configured
|
|
||||||
self.config.setdefault('sideshow.orders.expose_store_id', 'true')
|
|
||||||
grid = view.make_grid(model_class=model.Order, columns=['store_id', 'order_id'])
|
|
||||||
self.assertIn('store_id', grid.columns)
|
|
||||||
view.configure_grid(grid)
|
|
||||||
self.assertIn('store_id', grid.columns)
|
|
||||||
|
|
||||||
def test_create(self):
|
def test_create(self):
|
||||||
self.pyramid_config.include('sideshow.web.views')
|
self.pyramid_config.include('sideshow.web.views')
|
||||||
self.config.setdefault('wutta.batch.neworder.handler.spec',
|
self.config.setdefault('wutta.batch.neworder.handler.spec',
|
||||||
'sideshow.batch.neworder:NewOrderBatchHandler')
|
'sideshow.batch.neworder:NewOrderBatchHandler')
|
||||||
self.config.setdefault('sideshow.orders.expose_store_id', 'true')
|
|
||||||
self.config.setdefault('sideshow.orders.allow_item_discounts', 'true')
|
self.config.setdefault('sideshow.orders.allow_item_discounts', 'true')
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
|
@ -67,10 +59,6 @@ class TestOrderView(WebTestCase):
|
||||||
|
|
||||||
user = model.User(username='barney')
|
user = model.User(username='barney')
|
||||||
self.session.add(user)
|
self.session.add(user)
|
||||||
store = model.Store(store_id='001', name='Acme Goods')
|
|
||||||
self.session.add(store)
|
|
||||||
store = model.Store(store_id='002', name='Acme Services')
|
|
||||||
self.session.add(store)
|
|
||||||
self.session.flush()
|
self.session.flush()
|
||||||
|
|
||||||
with patch.object(view, 'Session', return_value=self.session):
|
with patch.object(view, 'Session', return_value=self.session):
|
||||||
|
@ -113,7 +101,6 @@ class TestOrderView(WebTestCase):
|
||||||
self.assertIsInstance(response, Response)
|
self.assertIsInstance(response, Response)
|
||||||
self.assertEqual(response.content_type, 'application/json')
|
self.assertEqual(response.content_type, 'application/json')
|
||||||
self.assertEqual(response.json_body, {
|
self.assertEqual(response.json_body, {
|
||||||
'store_id': None,
|
|
||||||
'customer_is_known': False,
|
'customer_is_known': False,
|
||||||
'customer_id': None,
|
'customer_id': None,
|
||||||
'customer_name': 'Fred Flintstone',
|
'customer_name': 'Fred Flintstone',
|
||||||
|
@ -298,50 +285,6 @@ class TestOrderView(WebTestCase):
|
||||||
fields = view.get_pending_product_required_fields()
|
fields = view.get_pending_product_required_fields()
|
||||||
self.assertEqual(fields, ['brand_name', 'size', 'unit_price_reg'])
|
self.assertEqual(fields, ['brand_name', 'size', 'unit_price_reg'])
|
||||||
|
|
||||||
def test_get_dept_item_discounts(self):
|
|
||||||
model = self.app.model
|
|
||||||
view = self.make_view()
|
|
||||||
|
|
||||||
with patch.object(view, 'Session', return_value=self.session):
|
|
||||||
|
|
||||||
# empty list by default
|
|
||||||
discounts = view.get_dept_item_discounts()
|
|
||||||
self.assertEqual(discounts, [])
|
|
||||||
|
|
||||||
# mock settings
|
|
||||||
self.app.save_setting(self.session, 'sideshow.orders.departments.5.name', 'Bulk')
|
|
||||||
self.app.save_setting(self.session, 'sideshow.orders.departments.5.default_item_discount', '15')
|
|
||||||
self.app.save_setting(self.session, 'sideshow.orders.departments.6.name', 'Produce')
|
|
||||||
self.app.save_setting(self.session, 'sideshow.orders.departments.6.default_item_discount', '5')
|
|
||||||
discounts = view.get_dept_item_discounts()
|
|
||||||
self.assertEqual(len(discounts), 2)
|
|
||||||
self.assertEqual(discounts[0], {
|
|
||||||
'department_id': '5',
|
|
||||||
'department_name': 'Bulk',
|
|
||||||
'default_item_discount': '15',
|
|
||||||
})
|
|
||||||
self.assertEqual(discounts[1], {
|
|
||||||
'department_id': '6',
|
|
||||||
'department_name': 'Produce',
|
|
||||||
'default_item_discount': '5',
|
|
||||||
})
|
|
||||||
|
|
||||||
# invalid setting
|
|
||||||
self.app.save_setting(self.session, 'sideshow.orders.departments.I.N.V.A.L.I.D.name', 'Bad News')
|
|
||||||
self.app.save_setting(self.session, 'sideshow.orders.departments.I.N.V.A.L.I.D.default_item_discount', '42')
|
|
||||||
discounts = view.get_dept_item_discounts()
|
|
||||||
self.assertEqual(len(discounts), 2)
|
|
||||||
self.assertEqual(discounts[0], {
|
|
||||||
'department_id': '5',
|
|
||||||
'department_name': 'Bulk',
|
|
||||||
'default_item_discount': '15',
|
|
||||||
})
|
|
||||||
self.assertEqual(discounts[1], {
|
|
||||||
'department_id': '6',
|
|
||||||
'department_name': 'Produce',
|
|
||||||
'default_item_discount': '5',
|
|
||||||
})
|
|
||||||
|
|
||||||
def test_get_context_customer(self):
|
def test_get_context_customer(self):
|
||||||
self.pyramid_config.add_route('orders', '/orders/')
|
self.pyramid_config.add_route('orders', '/orders/')
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
@ -361,7 +304,6 @@ class TestOrderView(WebTestCase):
|
||||||
self.session.flush()
|
self.session.flush()
|
||||||
context = view.get_context_customer(batch)
|
context = view.get_context_customer(batch)
|
||||||
self.assertEqual(context, {
|
self.assertEqual(context, {
|
||||||
'store_id': None,
|
|
||||||
'customer_is_known': True,
|
'customer_is_known': True,
|
||||||
'customer_id': 42,
|
'customer_id': 42,
|
||||||
'customer_name': 'Fred Flintstone',
|
'customer_name': 'Fred Flintstone',
|
||||||
|
@ -379,7 +321,6 @@ class TestOrderView(WebTestCase):
|
||||||
self.session.flush()
|
self.session.flush()
|
||||||
context = view.get_context_customer(batch)
|
context = view.get_context_customer(batch)
|
||||||
self.assertEqual(context, {
|
self.assertEqual(context, {
|
||||||
'store_id': None,
|
|
||||||
'customer_is_known': True,
|
'customer_is_known': True,
|
||||||
'customer_id': local.uuid.hex,
|
'customer_id': local.uuid.hex,
|
||||||
'customer_name': 'Betty Boop',
|
'customer_name': 'Betty Boop',
|
||||||
|
@ -398,7 +339,6 @@ class TestOrderView(WebTestCase):
|
||||||
self.session.flush()
|
self.session.flush()
|
||||||
context = view.get_context_customer(batch)
|
context = view.get_context_customer(batch)
|
||||||
self.assertEqual(context, {
|
self.assertEqual(context, {
|
||||||
'store_id': None,
|
|
||||||
'customer_is_known': False,
|
'customer_is_known': False,
|
||||||
'customer_id': None,
|
'customer_id': None,
|
||||||
'customer_name': 'Fred Flintstone',
|
'customer_name': 'Fred Flintstone',
|
||||||
|
@ -417,7 +357,6 @@ class TestOrderView(WebTestCase):
|
||||||
self.session.flush()
|
self.session.flush()
|
||||||
context = view.get_context_customer(batch)
|
context = view.get_context_customer(batch)
|
||||||
self.assertEqual(context, {
|
self.assertEqual(context, {
|
||||||
'store_id': None,
|
|
||||||
'customer_is_known': True, # nb. this is for UI default
|
'customer_is_known': True, # nb. this is for UI default
|
||||||
'customer_id': None,
|
'customer_id': None,
|
||||||
'customer_name': None,
|
'customer_name': None,
|
||||||
|
@ -469,34 +408,6 @@ class TestOrderView(WebTestCase):
|
||||||
self.session.flush()
|
self.session.flush()
|
||||||
self.assertEqual(self.session.query(model.NewOrderBatch).count(), 0)
|
self.assertEqual(self.session.query(model.NewOrderBatch).count(), 0)
|
||||||
|
|
||||||
def test_set_store(self):
|
|
||||||
model = self.app.model
|
|
||||||
view = self.make_view()
|
|
||||||
handler = NewOrderBatchHandler(self.config)
|
|
||||||
|
|
||||||
user = model.User(username='barney')
|
|
||||||
self.session.add(user)
|
|
||||||
self.session.flush()
|
|
||||||
|
|
||||||
with patch.object(view, 'batch_handler', create=True, new=handler):
|
|
||||||
with patch.object(view, 'Session', return_value=self.session):
|
|
||||||
with patch.object(self.request, 'user', new=user):
|
|
||||||
|
|
||||||
batch = view.get_current_batch()
|
|
||||||
self.assertIsNone(batch.store_id)
|
|
||||||
|
|
||||||
# store_id is required
|
|
||||||
result = view.set_store(batch, {})
|
|
||||||
self.assertEqual(result, {'error': "Must provide store_id"})
|
|
||||||
result = view.set_store(batch, {'store_id': ''})
|
|
||||||
self.assertEqual(result, {'error': "Must provide store_id"})
|
|
||||||
|
|
||||||
# store_id is set on batch
|
|
||||||
result = view.set_store(batch, {'store_id': '042'})
|
|
||||||
self.assertEqual(batch.store_id, '042')
|
|
||||||
self.assertIn('store_id', result)
|
|
||||||
self.assertEqual(result['store_id'], '042')
|
|
||||||
|
|
||||||
def test_assign_customer(self):
|
def test_assign_customer(self):
|
||||||
self.pyramid_config.add_route('orders.create', '/orders/new')
|
self.pyramid_config.add_route('orders.create', '/orders/new')
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
@ -521,7 +432,6 @@ class TestOrderView(WebTestCase):
|
||||||
self.assertIsNone(batch.pending_customer)
|
self.assertIsNone(batch.pending_customer)
|
||||||
self.assertIs(batch.local_customer, weirdal)
|
self.assertIs(batch.local_customer, weirdal)
|
||||||
self.assertEqual(context, {
|
self.assertEqual(context, {
|
||||||
'store_id': None,
|
|
||||||
'customer_is_known': True,
|
'customer_is_known': True,
|
||||||
'customer_id': weirdal.uuid.hex,
|
'customer_id': weirdal.uuid.hex,
|
||||||
'customer_name': 'Weird Al',
|
'customer_name': 'Weird Al',
|
||||||
|
@ -560,7 +470,6 @@ class TestOrderView(WebTestCase):
|
||||||
self.assertIsNone(batch.customer_name)
|
self.assertIsNone(batch.customer_name)
|
||||||
self.assertIsNone(batch.local_customer)
|
self.assertIsNone(batch.local_customer)
|
||||||
self.assertEqual(context, {
|
self.assertEqual(context, {
|
||||||
'store_id': None,
|
|
||||||
'customer_is_known': True,
|
'customer_is_known': True,
|
||||||
'customer_id': None,
|
'customer_id': None,
|
||||||
'customer_name': None,
|
'customer_name': None,
|
||||||
|
@ -601,7 +510,6 @@ class TestOrderView(WebTestCase):
|
||||||
context = view.set_pending_customer(batch, data)
|
context = view.set_pending_customer(batch, data)
|
||||||
self.assertIsInstance(batch.pending_customer, model.PendingCustomer)
|
self.assertIsInstance(batch.pending_customer, model.PendingCustomer)
|
||||||
self.assertEqual(context, {
|
self.assertEqual(context, {
|
||||||
'store_id': None,
|
|
||||||
'customer_is_known': False,
|
'customer_is_known': False,
|
||||||
'customer_id': None,
|
'customer_id': None,
|
||||||
'customer_name': 'Fred Flintstone',
|
'customer_name': 'Fred Flintstone',
|
||||||
|
@ -667,51 +575,6 @@ class TestOrderView(WebTestCase):
|
||||||
context = view.get_product_info(batch, {'product_id': '42'})
|
context = view.get_product_info(batch, {'product_id': '42'})
|
||||||
self.assertEqual(context, {'error': "something smells fishy"})
|
self.assertEqual(context, {'error': "something smells fishy"})
|
||||||
|
|
||||||
def test_get_past_products(self):
|
|
||||||
model = self.app.model
|
|
||||||
enum = self.app.enum
|
|
||||||
view = self.make_view()
|
|
||||||
handler = view.batch_handler
|
|
||||||
|
|
||||||
user = model.User(username='barney')
|
|
||||||
self.session.add(user)
|
|
||||||
batch = handler.make_batch(self.session, created_by=user)
|
|
||||||
self.session.add(batch)
|
|
||||||
self.session.flush()
|
|
||||||
|
|
||||||
# (nb. this all assumes local customers and products)
|
|
||||||
|
|
||||||
# error if no customer
|
|
||||||
self.assertRaises(ValueError, view.get_past_products, batch, {})
|
|
||||||
|
|
||||||
# empty history for customer
|
|
||||||
customer = model.LocalCustomer(full_name='Fred Flintstone')
|
|
||||||
batch.local_customer = customer
|
|
||||||
self.session.flush()
|
|
||||||
products = view.get_past_products(batch, {})
|
|
||||||
self.assertEqual(len(products), 0)
|
|
||||||
|
|
||||||
# mock historical order
|
|
||||||
order = model.Order(order_id=42, local_customer=customer, created_by=user)
|
|
||||||
product = model.LocalProduct(scancode='07430500132', description='Vinegar',
|
|
||||||
unit_price_reg=5.99, case_size=12)
|
|
||||||
item = model.OrderItem(local_product=product, order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
|
||||||
status_code=enum.ORDER_ITEM_STATUS_READY)
|
|
||||||
order.items.append(item)
|
|
||||||
self.session.add(order)
|
|
||||||
self.session.flush()
|
|
||||||
self.session.refresh(product)
|
|
||||||
|
|
||||||
# that should now be returned
|
|
||||||
products = view.get_past_products(batch, {})
|
|
||||||
self.assertEqual(len(products), 1)
|
|
||||||
self.assertEqual(products[0]['product_id'], product.uuid.hex)
|
|
||||||
self.assertEqual(products[0]['scancode'], '07430500132')
|
|
||||||
self.assertEqual(products[0]['description'], 'Vinegar')
|
|
||||||
# nb. this is a float, since result is JSON-safe
|
|
||||||
self.assertEqual(products[0]['case_price_quoted'], 71.88)
|
|
||||||
self.assertEqual(products[0]['case_price_quoted_display'], '$71.88')
|
|
||||||
|
|
||||||
def test_add_item(self):
|
def test_add_item(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
|
@ -962,6 +825,14 @@ class TestOrderView(WebTestCase):
|
||||||
'error': f"ValueError: batch has already been executed: {batch}",
|
'error': f"ValueError: batch has already been executed: {batch}",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def test_get_default_uom_choices(self):
|
||||||
|
enum = self.app.enum
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
uoms = view.get_default_uom_choices()
|
||||||
|
self.assertEqual(uoms, [{'key': key, 'value': val}
|
||||||
|
for key, val in enum.ORDER_UOM.items()])
|
||||||
|
|
||||||
def test_normalize_batch(self):
|
def test_normalize_batch(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
|
@ -1207,11 +1078,7 @@ class TestOrderView(WebTestCase):
|
||||||
form = view.make_form(model_instance=order)
|
form = view.make_form(model_instance=order)
|
||||||
# nb. this is to avoid include/exclude ambiguity
|
# nb. this is to avoid include/exclude ambiguity
|
||||||
form.remove('items')
|
form.remove('items')
|
||||||
# nb. store_id gets hidden by default
|
|
||||||
form.append('store_id')
|
|
||||||
self.assertIn('store_id', form)
|
|
||||||
view.configure_form(form)
|
view.configure_form(form)
|
||||||
self.assertNotIn('store_id', form)
|
|
||||||
schema = form.get_schema()
|
schema = form.get_schema()
|
||||||
self.assertIn('pending_customer', form)
|
self.assertIn('pending_customer', form)
|
||||||
self.assertIsInstance(schema['total_price'].typ, WuttaMoney)
|
self.assertIsInstance(schema['total_price'].typ, WuttaMoney)
|
||||||
|
@ -1222,20 +1089,13 @@ class TestOrderView(WebTestCase):
|
||||||
self.session.add(local)
|
self.session.add(local)
|
||||||
self.session.flush()
|
self.session.flush()
|
||||||
|
|
||||||
# nb. from now on we include store_id
|
|
||||||
self.config.setdefault('sideshow.orders.expose_store_id', 'true')
|
|
||||||
|
|
||||||
# viewing (local customer)
|
# viewing (local customer)
|
||||||
with patch.object(view, 'viewing', new=True):
|
with patch.object(view, 'viewing', new=True):
|
||||||
with patch.object(order, 'local_customer', new=local):
|
with patch.object(order, 'local_customer', new=local):
|
||||||
form = view.make_form(model_instance=order)
|
form = view.make_form(model_instance=order)
|
||||||
# nb. this is to avoid include/exclude ambiguity
|
# nb. this is to avoid include/exclude ambiguity
|
||||||
form.remove('items')
|
form.remove('items')
|
||||||
# nb. store_id will now remain
|
|
||||||
form.append('store_id')
|
|
||||||
self.assertIn('store_id', form)
|
|
||||||
view.configure_form(form)
|
view.configure_form(form)
|
||||||
self.assertIn('store_id', form)
|
|
||||||
self.assertNotIn('pending_customer', form)
|
self.assertNotIn('pending_customer', form)
|
||||||
schema = form.get_schema()
|
schema = form.get_schema()
|
||||||
self.assertIsInstance(schema['total_price'].typ, WuttaMoney)
|
self.assertIsInstance(schema['total_price'].typ, WuttaMoney)
|
||||||
|
@ -1376,12 +1236,6 @@ class TestOrderView(WebTestCase):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
|
||||||
self.app.save_setting(self.session, 'sideshow.orders.departments.5.name', 'Bulk')
|
|
||||||
self.app.save_setting(self.session, 'sideshow.orders.departments.5.default_item_discount', '15')
|
|
||||||
self.app.save_setting(self.session, 'sideshow.orders.departments.6.name', 'Produce')
|
|
||||||
self.app.save_setting(self.session, 'sideshow.orders.departments.6.default_item_discount', '5')
|
|
||||||
self.session.commit()
|
|
||||||
|
|
||||||
with patch.object(view, 'Session', return_value=self.session):
|
with patch.object(view, 'Session', return_value=self.session):
|
||||||
with patch.multiple(self.config, usedb=True, preferdb=True):
|
with patch.multiple(self.config, usedb=True, preferdb=True):
|
||||||
|
|
||||||
|
@ -1389,19 +1243,7 @@ class TestOrderView(WebTestCase):
|
||||||
allowed = self.config.get_bool('sideshow.orders.allow_unknown_products',
|
allowed = self.config.get_bool('sideshow.orders.allow_unknown_products',
|
||||||
session=self.session)
|
session=self.session)
|
||||||
self.assertIsNone(allowed)
|
self.assertIsNone(allowed)
|
||||||
self.assertEqual(self.session.query(model.Setting).count(), 4)
|
self.assertEqual(self.session.query(model.Setting).count(), 0)
|
||||||
discounts = view.get_dept_item_discounts()
|
|
||||||
self.assertEqual(len(discounts), 2)
|
|
||||||
self.assertEqual(discounts[0], {
|
|
||||||
'department_id': '5',
|
|
||||||
'department_name': 'Bulk',
|
|
||||||
'default_item_discount': '15',
|
|
||||||
})
|
|
||||||
self.assertEqual(discounts[1], {
|
|
||||||
'department_id': '6',
|
|
||||||
'department_name': 'Produce',
|
|
||||||
'default_item_discount': '5',
|
|
||||||
})
|
|
||||||
|
|
||||||
# fetch initial page
|
# fetch initial page
|
||||||
response = view.configure()
|
response = view.configure()
|
||||||
|
@ -1411,18 +1253,13 @@ class TestOrderView(WebTestCase):
|
||||||
allowed = self.config.get_bool('sideshow.orders.allow_unknown_products',
|
allowed = self.config.get_bool('sideshow.orders.allow_unknown_products',
|
||||||
session=self.session)
|
session=self.session)
|
||||||
self.assertIsNone(allowed)
|
self.assertIsNone(allowed)
|
||||||
self.assertEqual(self.session.query(model.Setting).count(), 4)
|
self.assertEqual(self.session.query(model.Setting).count(), 0)
|
||||||
|
|
||||||
# post new settings
|
# post new settings
|
||||||
with patch.multiple(self.request, create=True,
|
with patch.multiple(self.request, create=True,
|
||||||
method='POST',
|
method='POST',
|
||||||
POST={
|
POST={
|
||||||
'sideshow.orders.allow_unknown_products': 'true',
|
'sideshow.orders.allow_unknown_products': 'true',
|
||||||
'dept_item_discounts': json.dumps([{
|
|
||||||
'department_id': '5',
|
|
||||||
'department_name': 'Grocery',
|
|
||||||
'default_item_discount': 10,
|
|
||||||
}])
|
|
||||||
}):
|
}):
|
||||||
response = view.configure()
|
response = view.configure()
|
||||||
self.assertIsInstance(response, HTTPFound)
|
self.assertIsInstance(response, HTTPFound)
|
||||||
|
@ -1431,17 +1268,17 @@ class TestOrderView(WebTestCase):
|
||||||
session=self.session)
|
session=self.session)
|
||||||
self.assertTrue(allowed)
|
self.assertTrue(allowed)
|
||||||
self.assertTrue(self.session.query(model.Setting).count() > 1)
|
self.assertTrue(self.session.query(model.Setting).count() > 1)
|
||||||
discounts = view.get_dept_item_discounts()
|
|
||||||
self.assertEqual(len(discounts), 1)
|
|
||||||
self.assertEqual(discounts[0], {
|
|
||||||
'department_id': '5',
|
|
||||||
'department_name': 'Grocery',
|
|
||||||
'default_item_discount': '10',
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class OrderItemViewTestMixin:
|
class OrderItemViewTestMixin:
|
||||||
|
|
||||||
|
def test_common_order_handler(self):
|
||||||
|
view = self.make_view()
|
||||||
|
handler = view.order_handler
|
||||||
|
self.assertIsInstance(handler, OrderHandler)
|
||||||
|
handler2 = view.get_order_handler()
|
||||||
|
self.assertIs(handler2, handler)
|
||||||
|
|
||||||
def test_common_get_fallback_templates(self):
|
def test_common_get_fallback_templates(self):
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
|
||||||
|
@ -1457,29 +1294,18 @@ class OrderItemViewTestMixin:
|
||||||
def test_common_configure_grid(self):
|
def test_common_configure_grid(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
|
||||||
# store_id is removed by default
|
|
||||||
grid = view.make_grid(model_class=model.OrderItem)
|
grid = view.make_grid(model_class=model.OrderItem)
|
||||||
grid.append('store_id')
|
self.assertNotIn('order_id', grid.linked_columns)
|
||||||
self.assertIn('store_id', grid.columns)
|
|
||||||
view.configure_grid(grid)
|
view.configure_grid(grid)
|
||||||
self.assertNotIn('store_id', grid.columns)
|
self.assertIn('order_id', grid.linked_columns)
|
||||||
|
|
||||||
# store_id is shown if configured
|
def test_common_render_order_id(self):
|
||||||
self.config.setdefault('sideshow.orders.expose_store_id', 'true')
|
|
||||||
grid = view.make_grid(model_class=model.OrderItem)
|
|
||||||
grid.append('store_id')
|
|
||||||
self.assertIn('store_id', grid.columns)
|
|
||||||
view.configure_grid(grid)
|
|
||||||
self.assertIn('store_id', grid.columns)
|
|
||||||
|
|
||||||
def test_common_render_order_attr(self):
|
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
order = model.Order(order_id=42)
|
order = model.Order(order_id=42)
|
||||||
item = model.OrderItem()
|
item = model.OrderItem()
|
||||||
order.items.append(item)
|
order.items.append(item)
|
||||||
self.assertEqual(view.render_order_attr(item, 'order_id', None), 42)
|
self.assertEqual(view.render_order_id(item, None, None), 42)
|
||||||
|
|
||||||
def test_common_render_status_code(self):
|
def test_common_render_status_code(self):
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
|
|
|
@ -1,94 +0,0 @@
|
||||||
# -*- coding: utf-8; -*-
|
|
||||||
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
import colander
|
|
||||||
|
|
||||||
from sideshow.testing import WebTestCase
|
|
||||||
from sideshow.web.views import stores as mod
|
|
||||||
|
|
||||||
|
|
||||||
class TestIncludeme(WebTestCase):
|
|
||||||
|
|
||||||
def test_coverage(self):
|
|
||||||
mod.includeme(self.pyramid_config)
|
|
||||||
|
|
||||||
|
|
||||||
class TestStoreView(WebTestCase):
|
|
||||||
|
|
||||||
def make_view(self):
|
|
||||||
return mod.StoreView(self.request)
|
|
||||||
|
|
||||||
def test_configure_grid(self):
|
|
||||||
model = self.app.model
|
|
||||||
view = self.make_view()
|
|
||||||
grid = view.make_grid(model_class=model.Store)
|
|
||||||
self.assertNotIn('store_id', grid.linked_columns)
|
|
||||||
self.assertNotIn('name', grid.linked_columns)
|
|
||||||
view.configure_grid(grid)
|
|
||||||
self.assertIn('store_id', grid.linked_columns)
|
|
||||||
self.assertIn('name', grid.linked_columns)
|
|
||||||
|
|
||||||
def test_grid_row_class(self):
|
|
||||||
model = self.app.model
|
|
||||||
view = self.make_view()
|
|
||||||
|
|
||||||
store = model.Store()
|
|
||||||
self.assertFalse(store.archived)
|
|
||||||
self.assertIsNone(view.grid_row_class(store, {}, 0))
|
|
||||||
|
|
||||||
store = model.Store(archived=True)
|
|
||||||
self.assertTrue(store.archived)
|
|
||||||
self.assertEqual(view.grid_row_class(store, {}, 0), 'has-background-warning')
|
|
||||||
|
|
||||||
def test_configure_form(self):
|
|
||||||
model = self.app.model
|
|
||||||
view = self.make_view()
|
|
||||||
|
|
||||||
# unique validators are set
|
|
||||||
form = view.make_form(model_class=model.Store)
|
|
||||||
self.assertNotIn('store_id', form.validators)
|
|
||||||
self.assertNotIn('name', form.validators)
|
|
||||||
view.configure_form(form)
|
|
||||||
self.assertIn('store_id', form.validators)
|
|
||||||
self.assertIn('name', form.validators)
|
|
||||||
|
|
||||||
def test_unique_store_id(self):
|
|
||||||
model = self.app.model
|
|
||||||
view = self.make_view()
|
|
||||||
|
|
||||||
store = model.Store(store_id='001', name='whatever')
|
|
||||||
self.session.add(store)
|
|
||||||
self.session.commit()
|
|
||||||
|
|
||||||
with patch.object(view, 'Session', return_value=self.session):
|
|
||||||
|
|
||||||
# invalid if same store_id in data
|
|
||||||
node = colander.SchemaNode(colander.String(), name='store_id')
|
|
||||||
self.assertRaises(colander.Invalid, view.unique_store_id, node, '001')
|
|
||||||
|
|
||||||
# but not if store_id belongs to current store
|
|
||||||
with patch.object(self.request, 'matchdict', new={'uuid': store.uuid}):
|
|
||||||
with patch.object(view, 'editing', new=True):
|
|
||||||
node = colander.SchemaNode(colander.String(), name='store_id')
|
|
||||||
self.assertIsNone(view.unique_store_id(node, '001'))
|
|
||||||
|
|
||||||
def test_unique_name(self):
|
|
||||||
model = self.app.model
|
|
||||||
view = self.make_view()
|
|
||||||
|
|
||||||
store = model.Store(store_id='001', name='Acme Goods')
|
|
||||||
self.session.add(store)
|
|
||||||
self.session.commit()
|
|
||||||
|
|
||||||
with patch.object(view, 'Session', return_value=self.session):
|
|
||||||
|
|
||||||
# invalid if same name in data
|
|
||||||
node = colander.SchemaNode(colander.String(), name='name')
|
|
||||||
self.assertRaises(colander.Invalid, view.unique_name, node, 'Acme Goods')
|
|
||||||
|
|
||||||
# but not if name belongs to current store
|
|
||||||
with patch.object(self.request, 'matchdict', new={'uuid': store.uuid}):
|
|
||||||
with patch.object(view, 'editing', new=True):
|
|
||||||
node = colander.SchemaNode(colander.String(), name='name')
|
|
||||||
self.assertIsNone(view.unique_name(node, 'Acme Goods'))
|
|
Loading…
Reference in a new issue