Compare commits

..

No commits in common. "a4ad23c7fab3877eeb34c010783efef66f0c1559" and "e677cd5d8cd7f8652e6552ca5389e960e8bda6dd" have entirely different histories.

23 changed files with 753 additions and 3311 deletions

View file

@ -5,46 +5,6 @@ Glossary
.. glossary:: .. glossary::
:sorted: :sorted:
external customer
A customer account from an external system. Sideshow can be
configured to lookup customer data from external system(s) when
creating an :term:`order`.
See also :term:`local customer` and :term:`pending customer`.
external product
A product record from an external system. Sideshow can be
configured to lookup customer data from external system(s) when
creating an :term:`order`.
See also :term:`local product` and :term:`pending product`.
local customer
A customer account in the :term:`app database`. By default,
Sideshow will use its native "Local Customers" table for lookup
when creating an :term:`order`.
The data model for this is
:class:`~sideshow.db.model.customers.LocalCustomer`.
See also :term:`external customer` and :term:`pending customer`.
local product
A product record in the :term:`app database`. By default,
Sideshow will use its native "Local Products" table for lookup
when creating an :term:`order`.
The data model for this is
:class:`~sideshow.db.model.products.LocalProduct`.
See also :term:`external product` and :term:`pending product`.
new order batch
When user is creating a new order, under the hood a :term:`batch`
is employed to keep track of user input. When user ultimately
"submits" the order, the batch is executed which creates a true
:term:`order`.
order order
This is the central focus of the app; it refers to a customer This is the central focus of the app; it refers to a customer
case/special order which is tracked over time, from placement to case/special order which is tracked over time, from placement to
@ -60,19 +20,17 @@ Glossary
sibling items. sibling items.
pending customer pending customer
A "temporary" customer record used when creating an :term:`order` Generally refers to a "new / unknown" customer, e.g. for whom a
for new/unknown customer. new order is being created. This allows the order lifecycle to
get going before the customer has a proper account in the system.
The data model for this is See :class:`~sideshow.db.model.customers.PendingCustomer` for the
:class:`~sideshow.db.model.customers.PendingCustomer`. data model.
See also :term:`external customer` and :term:`pending customer`.
pending product pending product
A "temporary" product record used when creating an :term:`order` Generally refers to a "new / unknown" product, e.g. for which a
for new/unknown product. new order is being created. This allows the order lifecycle to
get going before the product has a true record in the system.
The data model for this is See :class:`~sideshow.db.model.products.PendingProduct` for the
:class:`~sideshow.db.model.products.PendingProduct`. data model.
See also :term:`external product` and :term:`pending product`.

View file

@ -27,8 +27,6 @@ New Order Batch Handler
import datetime import datetime
import decimal import decimal
import sqlalchemy as sa
from wuttjamaican.batch import BatchHandler from wuttjamaican.batch import BatchHandler
from sideshow.db.model import NewOrderBatch from sideshow.db.model import NewOrderBatch
@ -36,8 +34,7 @@ from sideshow.db.model import NewOrderBatch
class NewOrderBatchHandler(BatchHandler): class NewOrderBatchHandler(BatchHandler):
""" """
The :term:`batch handler` for :term:`new order batches <new order The :term:`batch handler` for New Order Batches.
batch>`.
This is responsible for business logic around the creation of new This is responsible for business logic around the creation of new
:term:`orders <order>`. A :term:`orders <order>`. A
@ -47,333 +44,188 @@ class NewOrderBatchHandler(BatchHandler):
""" """
model_class = NewOrderBatch model_class = NewOrderBatch
def use_local_customers(self): def set_pending_customer(self, batch, data):
""" """
Returns boolean indicating whether :term:`local customer` Set (add or update) pending customer info for the batch.
accounts should be used. This is true by default, but may be
false for :term:`external customer` lookups.
"""
return self.config.get_bool('sideshow.orders.use_local_customers',
default=True)
def use_local_products(self): This will clear the
""" :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.customer_id`
Returns boolean indicating whether :term:`local product` and set the
records should be used. This is true by default, but may be :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`,
false for :term:`external product` lookups. creating a new record if needed. It then updates the pending
""" customer record per the given ``data``.
return self.config.get_bool('sideshow.orders.use_local_products',
default=True)
def allow_unknown_products(self):
"""
Returns boolean indicating whether :term:`pending products
<pending product>` are allowed when creating an order.
This is true by default, so user can enter new/unknown product
when creating an order. This can be disabled, to force user
to choose existing local/external product.
"""
return self.config.get_bool('sideshow.orders.allow_unknown_products',
default=True)
def set_customer(self, batch, customer_info, user=None):
"""
Set/update customer info for the batch.
This will first set one of the following:
* :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.customer_id`
* :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.local_customer`
* :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`
Note that a new
:class:`~sideshow.db.model.customers.PendingCustomer` record
is created if necessary.
And then it will update these accordingly:
* :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.customer_name`
* :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.phone_number`
* :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.email_address`
Note that ``customer_info`` may be ``None``, which will cause
all the above to be set to ``None`` also.
:param batch: :param batch:
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch` to :class:`~sideshow.db.model.batch.neworder.NewOrderBatch`
update. to be updated.
:param customer_info: Customer ID string, or dict of :param data: Dict of field data for the
:class:`~sideshow.db.model.customers.PendingCustomer` data, :class:`~sideshow.db.model.customers.PendingCustomer`
or ``None`` to clear the customer info. record.
:param user:
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
is performing the action. This is used to set
:attr:`~sideshow.db.model.customers.PendingCustomer.created_by`
on the pending customer, if applicable. If not specified,
the batch creator is assumed.
""" """
model = self.app.model model = self.app.model
enum = self.app.enum enum = self.app.enum
session = self.app.get_session(batch)
use_local = self.use_local_customers()
# set customer info # remove customer account if set
if isinstance(customer_info, str): batch.customer_id = None
if use_local:
# local_customer # create pending customer if needed
customer = session.get(model.LocalCustomer, customer_info) pending = batch.pending_customer
if not customer: if not pending:
raise ValueError("local customer not found") kw = dict(data)
batch.local_customer = customer kw.setdefault('status', enum.PendingCustomerStatus.PENDING)
batch.customer_name = customer.full_name pending = model.PendingCustomer(**kw)
batch.phone_number = customer.phone_number batch.pending_customer = pending
batch.email_address = customer.email_address
else: # external customer_id # update pending customer
#batch.customer_id = customer_info if 'first_name' in data:
raise NotImplementedError pending.first_name = data['first_name']
if 'last_name' in data:
pending.last_name = data['last_name']
if 'full_name' in data:
pending.full_name = data['full_name']
elif 'first_name' in data or 'last_name' in data:
pending.full_name = self.app.make_full_name(data.get('first_name'),
data.get('last_name'))
if 'phone_number' in data:
pending.phone_number = data['phone_number']
if 'email_address' in data:
pending.email_address = data['email_address']
elif customer_info: # update batch per pending customer
batch.customer_name = pending.full_name
batch.phone_number = pending.phone_number
batch.email_address = pending.email_address
# pending_customer def add_pending_product(self, batch, pending_info,
batch.customer_id = None order_qty, order_uom):
batch.local_customer = None
customer = batch.pending_customer
if not customer:
customer = model.PendingCustomer(status=enum.PendingCustomerStatus.PENDING,
created_by=user or batch.created_by)
session.add(customer)
batch.pending_customer = customer
fields = [
'full_name',
'first_name',
'last_name',
'phone_number',
'email_address',
]
for key in fields:
setattr(customer, key, customer_info.get(key))
if 'full_name' not in customer_info:
customer.full_name = self.app.make_full_name(customer.first_name,
customer.last_name)
batch.customer_name = customer.full_name
batch.phone_number = customer.phone_number
batch.email_address = customer.email_address
else:
# null
batch.customer_id = None
batch.local_customer = None
batch.customer_name = None
batch.phone_number = None
batch.email_address = None
session.flush()
def add_item(self, batch, product_info, order_qty, order_uom, user=None):
""" """
Add a new item/row to the batch, for given product and quantity. Add a new row to the batch, for the given "pending" product
and order quantity.
See also :meth:`update_item()`. See also :meth:`set_pending_product()` to update an existing row.
:param batch: :param batch:
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch` to :class:`~sideshow.db.model.batch.neworder.NewOrderBatch` to
update. which the row should be added.
:param product_info: Product ID string, or dict of :param pending_info: Dict of kwargs to use when constructing a
:class:`~sideshow.db.model.products.PendingProduct` data. new :class:`~sideshow.db.model.products.PendingProduct`.
:param order_qty: :param order_qty: Quantity of the product to be added to the
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_qty` order.
value for the new row.
:param order_uom: :param order_uom: UOM for the order quantity; must be a code
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_uom` from :data:`~sideshow.enum.ORDER_UOM`.
value for the new row.
:param user:
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
is performing the action. This is used to set
:attr:`~sideshow.db.model.products.PendingProduct.created_by`
on the pending product, if applicable. If not specified,
the batch creator is assumed.
:returns: :returns:
:class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow` :class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
instance. which was added to the batch.
""" """
model = self.app.model model = self.app.model
enum = self.app.enum enum = self.app.enum
session = self.app.get_session(batch) session = self.app.get_session(batch)
use_local = self.use_local_products()
row = self.make_row()
# set product info # make new pending product
if isinstance(product_info, str): kw = dict(pending_info)
if use_local: kw.setdefault('status', enum.PendingProductStatus.PENDING)
product = model.PendingProduct(**kw)
session.add(product)
session.flush()
# nb. this may convert float to decimal etc.
session.refresh(product)
# local_product # make/add new row, w/ pending product
local = session.get(model.LocalProduct, product_info) row = self.make_row(pending_product=product,
if not local: order_qty=order_qty, order_uom=order_uom)
raise ValueError("local product not found")
row.local_product = local
else: # external product_id
#row.product_id = product_info
raise NotImplementedError
else:
# pending_product
if not self.allow_unknown_products():
raise TypeError("unknown/pending product not allowed for new orders")
row.product_id = None
row.local_product = None
pending = model.PendingProduct(status=enum.PendingProductStatus.PENDING,
created_by=user or batch.created_by)
fields = [
'scancode',
'brand_name',
'description',
'size',
'weighed',
'department_id',
'department_name',
'special_order',
'vendor_name',
'vendor_item_code',
'case_size',
'unit_cost',
'unit_price_reg',
'notes',
]
for key in fields:
setattr(pending, key, product_info.get(key))
# nb. this may convert float to decimal etc.
session.add(pending)
session.flush()
session.refresh(pending)
row.pending_product = pending
# set order info
row.order_qty = order_qty
row.order_uom = order_uom
# add row to batch
self.add_row(batch, row) self.add_row(batch, row)
session.add(row)
session.flush() session.flush()
return row return row
def update_item(self, row, product_info, order_qty, order_uom, user=None): def set_pending_product(self, row, data):
""" """
Update an item/row, per given product and quantity. Set (add or update) pending product info for the given batch row.
See also :meth:`add_item()`. This will clear the
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.product_id`
and set the
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.pending_product`,
creating a new record if needed. It then updates the pending
product record per the given ``data``, and finally calls
:meth:`refresh_row()`.
Note that this does not update order quantity for the item.
See also :meth:`add_pending_product()` to add a new row
instead of updating.
:param row: :param row:
:class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow` :class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
to update. to be updated.
:param product_info: Product ID string, or dict of :param data: Dict of field data for the
:class:`~sideshow.db.model.products.PendingProduct` data. :class:`~sideshow.db.model.products.PendingProduct` record.
:param order_qty: New
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_qty`
value for the row.
:param order_uom: New
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_uom`
value for the row.
:param user:
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
is performing the action. This is used to set
:attr:`~sideshow.db.model.products.PendingProduct.created_by`
on the pending product, if applicable. If not specified,
the batch creator is assumed.
""" """
model = self.app.model model = self.app.model
enum = self.app.enum enum = self.app.enum
session = self.app.get_session(row) session = self.app.get_session(row)
use_local = self.use_local_products()
# set product info # values for these fields can be used as-is
if isinstance(product_info, str): simple_fields = [
if use_local: 'scancode',
'brand_name',
'description',
'size',
'weighed',
'department_id',
'department_name',
'special_order',
'vendor_name',
'vendor_item_code',
'notes',
'unit_cost',
'case_size',
'case_cost',
'unit_price_reg',
]
# local_product # clear true product id
local = session.get(model.LocalProduct, product_info) row.product_id = None
if not local:
raise ValueError("local product not found")
row.local_product = local
else: # external product_id # make pending product if needed
#row.product_id = product_info product = row.pending_product
raise NotImplementedError if not product:
kw = dict(data)
else: kw.setdefault('status', enum.PendingProductStatus.PENDING)
# pending_product product = model.PendingProduct(**kw)
if not self.allow_unknown_products(): session.add(product)
raise TypeError("unknown/pending product not allowed for new orders") row.pending_product = product
row.product_id = None
row.local_product = None
pending = row.pending_product
if not pending:
pending = model.PendingProduct(status=enum.PendingProductStatus.PENDING,
created_by=user or row.batch.created_by)
session.add(pending)
row.pending_product = pending
fields = [
'scancode',
'brand_name',
'description',
'size',
'weighed',
'department_id',
'department_name',
'special_order',
'vendor_name',
'vendor_item_code',
'case_size',
'unit_cost',
'unit_price_reg',
'notes',
]
for key in fields:
setattr(pending, key, product_info.get(key))
# nb. this may convert float to decimal etc.
session.flush() session.flush()
session.refresh(pending)
# set order info # update pending product
row.order_qty = order_qty for field in simple_fields:
row.order_uom = order_uom if field in data:
setattr(product, field, data[field])
# nb. this may convert float to decimal etc. # nb. this may convert float to decimal etc.
session.flush() session.flush()
session.refresh(row) session.refresh(product)
# refresh per new info # refresh per new info
self.refresh_row(row) self.refresh_row(row)
def refresh_row(self, row): def refresh_row(self, row, now=None):
""" """
Refresh data for the row. This is called when adding a new Refresh all data for the row. This is called when adding a
row to the batch, or anytime the row is updated (e.g. when new row to the batch, or anytime the row is updated (e.g. when
changing order quantity). changing order quantity).
This calls one of the following to update product-related This calls one of the following to update product-related
attributes: attributes for the row:
* :meth:`refresh_row_from_external_product()`
* :meth:`refresh_row_from_local_product()`
* :meth:`refresh_row_from_pending_product()` * :meth:`refresh_row_from_pending_product()`
* :meth:`refresh_row_from_true_product()`
It then re-calculates the row's It then re-calculates the row's
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.total_price` :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.total_price`
@ -387,7 +239,7 @@ class NewOrderBatchHandler(BatchHandler):
row.status_text = None row.status_text = None
# ensure product # ensure product
if not row.product_id and not row.local_product and not row.pending_product: if not row.product_id and not row.pending_product:
row.status_code = row.STATUS_MISSING_PRODUCT row.status_code = row.STATUS_MISSING_PRODUCT
return return
@ -398,9 +250,7 @@ class NewOrderBatchHandler(BatchHandler):
# update product attrs on row # update product attrs on row
if row.product_id: if row.product_id:
self.refresh_row_from_external_product(row) self.refresh_row_from_true_product(row)
elif row.local_product:
self.refresh_row_from_local_product(row)
else: else:
self.refresh_row_from_pending_product(row) self.refresh_row_from_pending_product(row)
@ -412,7 +262,7 @@ class NewOrderBatchHandler(BatchHandler):
row.case_price_quoted = None row.case_price_quoted = None
if row.unit_price_sale is not None and ( if row.unit_price_sale is not None and (
not row.sale_ends not row.sale_ends
or row.sale_ends > datetime.datetime.now()): or row.sale_ends > (now or datetime.datetime.now())):
row.unit_price_quoted = row.unit_price_sale row.unit_price_quoted = row.unit_price_sale
else: else:
row.unit_price_quoted = row.unit_price_reg row.unit_price_quoted = row.unit_price_reg
@ -440,27 +290,6 @@ class NewOrderBatchHandler(BatchHandler):
# all ok # all ok
row.status_code = row.STATUS_OK row.status_code = row.STATUS_OK
def refresh_row_from_local_product(self, row):
"""
Update product-related attributes on the row, from its
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.local_product`
record.
This is called automatically from :meth:`refresh_row()`.
"""
product = row.local_product
row.product_scancode = product.scancode
row.product_brand = product.brand_name
row.product_description = product.description
row.product_size = product.size
row.product_weighed = product.weighed
row.department_id = product.department_id
row.department_name = product.department_name
row.special_order = product.special_order
row.case_size = product.case_size
row.unit_cost = product.unit_cost
row.unit_price_reg = product.unit_price_reg
def refresh_row_from_pending_product(self, row): def refresh_row_from_pending_product(self, row):
""" """
Update product-related attributes on the row, from its Update product-related attributes on the row, from its
@ -470,6 +299,7 @@ class NewOrderBatchHandler(BatchHandler):
This is called automatically from :meth:`refresh_row()`. This is called automatically from :meth:`refresh_row()`.
""" """
product = row.pending_product product = row.pending_product
row.product_scancode = product.scancode row.product_scancode = product.scancode
row.product_brand = product.brand_name row.product_brand = product.brand_name
row.product_description = product.description row.product_description = product.description
@ -482,10 +312,10 @@ class NewOrderBatchHandler(BatchHandler):
row.unit_cost = product.unit_cost row.unit_cost = product.unit_cost
row.unit_price_reg = product.unit_price_reg row.unit_price_reg = product.unit_price_reg
def refresh_row_from_external_product(self, row): def refresh_row_from_true_product(self, row):
""" """
Update product-related attributes on the row, from its Update product-related attributes on the row, from its "true"
:term:`external product` record indicated by product record indicated by
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.product_id`. :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.product_id`.
This is called automatically from :meth:`refresh_row()`. This is called automatically from :meth:`refresh_row()`.
@ -510,39 +340,31 @@ class NewOrderBatchHandler(BatchHandler):
def do_delete(self, batch, user, **kwargs): def do_delete(self, batch, user, **kwargs):
""" """
Delete a batch completely. Delete the given batch entirely.
If the batch has :term:`pending customer` or :term:`pending If the batch has a
product` records, they are also deleted - unless still :attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`
referenced by some order(s). record, that is deleted also.
""" """
session = self.app.get_session(batch) # maybe delete pending customer record, if it only exists for
# sake of this batch
# maybe delete pending customer if batch.pending_customer:
customer = batch.pending_customer if len(batch.pending_customer.new_order_batches) == 1:
if customer and not customer.orders: # TODO: check for past orders too
session.delete(customer) session = self.app.get_session(batch)
session.delete(batch.pending_customer)
# maybe delete pending products
for row in batch.rows:
product = row.pending_product
if product and not product.order_items:
session.delete(product)
# continue with normal deletion # continue with normal deletion
super().do_delete(batch, user, **kwargs) super().do_delete(batch, user, **kwargs)
def why_not_execute(self, batch, **kwargs): def why_not_execute(self, batch, **kwargs):
""" """
By default this checks to ensure the batch has a customer with By default this checks to ensure the batch has a customer and
phone number, and at least one item. at least one item.
""" """
if not batch.customer_name: if not batch.customer_id and not batch.pending_customer:
return "Must assign the customer" return "Must assign the customer"
if not batch.phone_number:
return "Customer phone number is required"
rows = self.get_effective_rows(batch) rows = self.get_effective_rows(batch)
if not rows: if not rows:
return "Must add at least one valid item" return "Must add at least one valid item"
@ -559,113 +381,17 @@ class NewOrderBatchHandler(BatchHandler):
def execute(self, batch, user=None, progress=None, **kwargs): def execute(self, batch, user=None, progress=None, **kwargs):
""" """
Execute the batch; this should make a proper :term:`order`. By default, this will call :meth:`make_new_order()` and return
the new :class:`~sideshow.db.model.orders.Order` instance.
By default, this will call:
* :meth:`make_local_customer()`
* :meth:`make_local_products()`
* :meth:`make_new_order()`
And will return the new
:class:`~sideshow.db.model.orders.Order` instance.
Note that callers should use Note that callers should use
:meth:`~wuttjamaican:wuttjamaican.batch.BatchHandler.do_execute()` :meth:`~wuttjamaican:wuttjamaican.batch.BatchHandler.do_execute()`
instead, which calls this method automatically. instead, which calls this method automatically.
""" """
rows = self.get_effective_rows(batch) rows = self.get_effective_rows(batch)
self.make_local_customer(batch)
self.make_local_products(batch, rows)
order = self.make_new_order(batch, rows, user=user, progress=progress, **kwargs) order = self.make_new_order(batch, rows, user=user, progress=progress, **kwargs)
return order return order
def make_local_customer(self, batch):
"""
If applicable, this converts the batch :term:`pending
customer` into a :term:`local customer`.
This is called automatically from :meth:`execute()`.
This logic will happen only if :meth:`use_local_customers()`
returns true, and the batch has pending instead of local
customer (so far).
It will create a new
:class:`~sideshow.db.model.customers.LocalCustomer` record and
populate it from the batch
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`.
The latter is then deleted.
"""
if not self.use_local_customers():
return
# nothing to do if no pending customer
pending = batch.pending_customer
if not pending:
return
session = self.app.get_session(batch)
# maybe convert pending to local customer
if not batch.local_customer:
model = self.app.model
inspector = sa.inspect(model.LocalCustomer)
local = model.LocalCustomer()
for prop in inspector.column_attrs:
if hasattr(pending, prop.key):
setattr(local, prop.key, getattr(pending, prop.key))
session.add(local)
batch.local_customer = local
# remove pending customer
batch.pending_customer = None
session.delete(pending)
session.flush()
def make_local_products(self, batch, rows):
"""
If applicable, this converts all :term:`pending products
<pending product>` into :term:`local products <local
product>`.
This is called automatically from :meth:`execute()`.
This logic will happen only if :meth:`use_local_products()`
returns true, and the batch has pending instead of local items
(so far).
For each affected row, it will create a new
:class:`~sideshow.db.model.products.LocalProduct` record and
populate it from the row
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.pending_product`.
The latter is then deleted.
"""
if not self.use_local_products():
return
model = self.app.model
session = self.app.get_session(batch)
inspector = sa.inspect(model.LocalProduct)
for row in rows:
if row.local_product or not row.pending_product:
continue
pending = row.pending_product
local = model.LocalProduct()
for prop in inspector.column_attrs:
if hasattr(pending, prop.key):
setattr(local, prop.key, getattr(pending, prop.key))
session.add(local)
row.local_product = local
row.pending_product = None
session.delete(pending)
session.flush()
def make_new_order(self, batch, rows, user=None, progress=None, **kwargs): def make_new_order(self, batch, rows, user=None, progress=None, **kwargs):
""" """
Create a new :term:`order` from the batch data. Create a new :term:`order` from the batch data.
@ -689,7 +415,6 @@ class NewOrderBatchHandler(BatchHandler):
batch_fields = [ batch_fields = [
'store_id', 'store_id',
'customer_id', 'customer_id',
'local_customer',
'pending_customer', 'pending_customer',
'customer_name', 'customer_name',
'phone_number', 'phone_number',
@ -698,9 +423,7 @@ class NewOrderBatchHandler(BatchHandler):
] ]
row_fields = [ row_fields = [
'product_id', 'pending_product_uuid',
'local_product',
'pending_product',
'product_scancode', 'product_scancode',
'product_brand', 'product_brand',
'product_description', 'product_description',

View file

@ -26,8 +26,8 @@ def upgrade() -> None:
sa.Enum('PENDING', 'READY', 'RESOLVED', name='pendingcustomerstatus').create(op.get_bind()) sa.Enum('PENDING', 'READY', 'RESOLVED', name='pendingcustomerstatus').create(op.get_bind())
sa.Enum('PENDING', 'READY', 'RESOLVED', name='pendingproductstatus').create(op.get_bind()) sa.Enum('PENDING', 'READY', 'RESOLVED', name='pendingproductstatus').create(op.get_bind())
# sideshow_customer_pending # sideshow_pending_customer
op.create_table('sideshow_customer_pending', op.create_table('sideshow_pending_customer',
sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False), sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False),
sa.Column('customer_id', sa.String(length=20), nullable=True), sa.Column('customer_id', sa.String(length=20), nullable=True),
sa.Column('full_name', sa.String(length=100), nullable=True), sa.Column('full_name', sa.String(length=100), nullable=True),
@ -38,24 +38,12 @@ def upgrade() -> None:
sa.Column('status', postgresql.ENUM('PENDING', 'READY', 'RESOLVED', name='pendingcustomerstatus', create_type=False), nullable=False), sa.Column('status', postgresql.ENUM('PENDING', 'READY', 'RESOLVED', name='pendingcustomerstatus', create_type=False), nullable=False),
sa.Column('created', sa.DateTime(timezone=True), nullable=False), sa.Column('created', sa.DateTime(timezone=True), nullable=False),
sa.Column('created_by_uuid', wuttjamaican.db.util.UUID(), nullable=False), sa.Column('created_by_uuid', wuttjamaican.db.util.UUID(), nullable=False),
sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_customer_pending_created_by_uuid_user')), sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_pending_customer_created_by_uuid_user')),
sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_customer_pending')) sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_pending_customer'))
) )
# sideshow_customer_local # sideshow_pending_product
op.create_table('sideshow_customer_local', op.create_table('sideshow_pending_product',
sa.Column('full_name', sa.String(length=100), nullable=True),
sa.Column('first_name', sa.String(length=50), nullable=True),
sa.Column('last_name', sa.String(length=50), nullable=True),
sa.Column('phone_number', sa.String(length=20), nullable=True),
sa.Column('email_address', sa.String(length=255), nullable=True),
sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False),
sa.Column('external_id', sa.String(length=20), nullable=True),
sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_customer_local'))
)
# sideshow_product_pending
op.create_table('sideshow_product_pending',
sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False), sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False),
sa.Column('product_id', sa.String(length=20), nullable=True), sa.Column('product_id', sa.String(length=20), nullable=True),
sa.Column('scancode', sa.String(length=14), nullable=True), sa.Column('scancode', sa.String(length=14), nullable=True),
@ -75,29 +63,8 @@ def upgrade() -> None:
sa.Column('status', postgresql.ENUM('PENDING', 'READY', 'RESOLVED', name='pendingproductstatus', create_type=False), nullable=False), sa.Column('status', postgresql.ENUM('PENDING', 'READY', 'RESOLVED', name='pendingproductstatus', create_type=False), nullable=False),
sa.Column('created', sa.DateTime(timezone=True), nullable=False), sa.Column('created', sa.DateTime(timezone=True), nullable=False),
sa.Column('created_by_uuid', wuttjamaican.db.util.UUID(), nullable=False), sa.Column('created_by_uuid', wuttjamaican.db.util.UUID(), nullable=False),
sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_product_pending_created_by_uuid_user')), sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_pending_product_created_by_uuid_user')),
sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_product_pending')) sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_pending_product'))
)
# sideshow_product_local
op.create_table('sideshow_product_local',
sa.Column('scancode', sa.String(length=14), nullable=True),
sa.Column('brand_name', sa.String(length=100), nullable=True),
sa.Column('description', sa.String(length=255), nullable=True),
sa.Column('size', sa.String(length=30), nullable=True),
sa.Column('weighed', sa.Boolean(), nullable=True),
sa.Column('department_id', sa.String(length=10), nullable=True),
sa.Column('department_name', sa.String(length=30), nullable=True),
sa.Column('special_order', sa.Boolean(), nullable=True),
sa.Column('vendor_name', sa.String(length=50), nullable=True),
sa.Column('vendor_item_code', sa.String(length=20), nullable=True),
sa.Column('case_size', sa.Numeric(precision=9, scale=4), nullable=True),
sa.Column('unit_cost', sa.Numeric(precision=9, scale=5), nullable=True),
sa.Column('unit_price_reg', sa.Numeric(precision=8, scale=3), nullable=True),
sa.Column('notes', sa.Text(), nullable=True),
sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False),
sa.Column('external_id', sa.String(length=20), nullable=True),
sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_product_local'))
) )
# sideshow_order # sideshow_order
@ -106,7 +73,6 @@ def upgrade() -> None:
sa.Column('order_id', sa.Integer(), nullable=False), sa.Column('order_id', sa.Integer(), nullable=False),
sa.Column('store_id', sa.String(length=10), nullable=True), sa.Column('store_id', sa.String(length=10), nullable=True),
sa.Column('customer_id', sa.String(length=20), nullable=True), sa.Column('customer_id', sa.String(length=20), nullable=True),
sa.Column('local_customer_uuid', wuttjamaican.db.util.UUID(), nullable=True),
sa.Column('pending_customer_uuid', wuttjamaican.db.util.UUID(), nullable=True), sa.Column('pending_customer_uuid', wuttjamaican.db.util.UUID(), nullable=True),
sa.Column('customer_name', sa.String(length=100), nullable=True), sa.Column('customer_name', sa.String(length=100), nullable=True),
sa.Column('phone_number', sa.String(length=20), nullable=True), sa.Column('phone_number', sa.String(length=20), nullable=True),
@ -114,8 +80,7 @@ def upgrade() -> None:
sa.Column('total_price', sa.Numeric(precision=10, scale=3), nullable=True), sa.Column('total_price', sa.Numeric(precision=10, scale=3), nullable=True),
sa.Column('created', sa.DateTime(timezone=True), nullable=False), sa.Column('created', sa.DateTime(timezone=True), nullable=False),
sa.Column('created_by_uuid', wuttjamaican.db.util.UUID(), nullable=False), sa.Column('created_by_uuid', wuttjamaican.db.util.UUID(), nullable=False),
sa.ForeignKeyConstraint(['local_customer_uuid'], ['sideshow_customer_local.uuid'], name=op.f('fk_order_local_customer_uuid_local_customer')), sa.ForeignKeyConstraint(['pending_customer_uuid'], ['sideshow_pending_customer.uuid'], name=op.f('fk_order_pending_customer_uuid_pending_customer')),
sa.ForeignKeyConstraint(['pending_customer_uuid'], ['sideshow_customer_pending.uuid'], name=op.f('fk_order_pending_customer_uuid_pending_customer')),
sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_order_created_by_uuid_user')), sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_order_created_by_uuid_user')),
sa.PrimaryKeyConstraint('uuid', name=op.f('pk_order')) sa.PrimaryKeyConstraint('uuid', name=op.f('pk_order'))
) )
@ -126,7 +91,6 @@ def upgrade() -> None:
sa.Column('order_uuid', wuttjamaican.db.util.UUID(), nullable=False), sa.Column('order_uuid', wuttjamaican.db.util.UUID(), nullable=False),
sa.Column('sequence', sa.Integer(), nullable=False), sa.Column('sequence', sa.Integer(), nullable=False),
sa.Column('product_id', sa.String(length=20), nullable=True), sa.Column('product_id', sa.String(length=20), nullable=True),
sa.Column('local_product_uuid', wuttjamaican.db.util.UUID(), nullable=True),
sa.Column('pending_product_uuid', wuttjamaican.db.util.UUID(), nullable=True), sa.Column('pending_product_uuid', wuttjamaican.db.util.UUID(), nullable=True),
sa.Column('product_scancode', sa.String(length=14), nullable=True), sa.Column('product_scancode', sa.String(length=14), nullable=True),
sa.Column('product_brand', sa.String(length=100), nullable=True), sa.Column('product_brand', sa.String(length=100), nullable=True),
@ -151,8 +115,7 @@ def upgrade() -> None:
sa.Column('paid_amount', sa.Numeric(precision=8, scale=3), nullable=False), sa.Column('paid_amount', sa.Numeric(precision=8, scale=3), nullable=False),
sa.Column('payment_transaction_number', sa.String(length=20), nullable=True), sa.Column('payment_transaction_number', sa.String(length=20), nullable=True),
sa.ForeignKeyConstraint(['order_uuid'], ['sideshow_order.uuid'], name=op.f('fk_sideshow_order_item_order_uuid_order')), sa.ForeignKeyConstraint(['order_uuid'], ['sideshow_order.uuid'], name=op.f('fk_sideshow_order_item_order_uuid_order')),
sa.ForeignKeyConstraint(['local_product_uuid'], ['sideshow_product_local.uuid'], name=op.f('fk_sideshow_order_item_local_product_uuid_local_product')), sa.ForeignKeyConstraint(['pending_product_uuid'], ['sideshow_pending_product.uuid'], name=op.f('fk_sideshow_order_item_pending_product_uuid_pending_product')),
sa.ForeignKeyConstraint(['pending_product_uuid'], ['sideshow_product_pending.uuid'], name=op.f('fk_sideshow_order_item_pending_product_uuid_pending_product')),
sa.PrimaryKeyConstraint('uuid', name=op.f('pk_order_item')) sa.PrimaryKeyConstraint('uuid', name=op.f('pk_order_item'))
) )
@ -171,7 +134,6 @@ def upgrade() -> None:
sa.Column('executed_by_uuid', wuttjamaican.db.util.UUID(), nullable=True), sa.Column('executed_by_uuid', wuttjamaican.db.util.UUID(), nullable=True),
sa.Column('store_id', sa.String(length=10), nullable=True), sa.Column('store_id', sa.String(length=10), nullable=True),
sa.Column('customer_id', sa.String(length=20), nullable=True), sa.Column('customer_id', sa.String(length=20), nullable=True),
sa.Column('local_customer_uuid', wuttjamaican.db.util.UUID(), nullable=True),
sa.Column('pending_customer_uuid', wuttjamaican.db.util.UUID(), nullable=True), sa.Column('pending_customer_uuid', wuttjamaican.db.util.UUID(), nullable=True),
sa.Column('customer_name', sa.String(length=100), nullable=True), sa.Column('customer_name', sa.String(length=100), nullable=True),
sa.Column('phone_number', sa.String(length=20), nullable=True), sa.Column('phone_number', sa.String(length=20), nullable=True),
@ -179,8 +141,7 @@ def upgrade() -> None:
sa.Column('total_price', sa.Numeric(precision=10, scale=3), nullable=True), sa.Column('total_price', sa.Numeric(precision=10, scale=3), nullable=True),
sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_batch_neworder_created_by_uuid_user')), sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_batch_neworder_created_by_uuid_user')),
sa.ForeignKeyConstraint(['executed_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_batch_neworder_executed_by_uuid_user')), sa.ForeignKeyConstraint(['executed_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_batch_neworder_executed_by_uuid_user')),
sa.ForeignKeyConstraint(['local_customer_uuid'], ['sideshow_customer_local.uuid'], name=op.f('fk_sideshow_batch_neworder_local_customer_uuid_local_customer')), sa.ForeignKeyConstraint(['pending_customer_uuid'], ['sideshow_pending_customer.uuid'], name=op.f('fk_sideshow_batch_neworder_pending_customer_uuid_pending_customer')),
sa.ForeignKeyConstraint(['pending_customer_uuid'], ['sideshow_customer_pending.uuid'], name=op.f('fk_sideshow_batch_neworder_pending_customer_uuid_pending_customer')),
sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_batch_neworder')) sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_batch_neworder'))
) )
@ -191,9 +152,8 @@ def upgrade() -> None:
sa.Column('sequence', sa.Integer(), nullable=False), sa.Column('sequence', sa.Integer(), nullable=False),
sa.Column('status_text', sa.String(length=255), nullable=True), sa.Column('status_text', sa.String(length=255), nullable=True),
sa.Column('modified', sa.DateTime(timezone=True), nullable=True), sa.Column('modified', sa.DateTime(timezone=True), nullable=True),
sa.Column('product_id', sa.String(length=20), nullable=True),
sa.Column('local_product_uuid', wuttjamaican.db.util.UUID(), nullable=True),
sa.Column('pending_product_uuid', wuttjamaican.db.util.UUID(), nullable=True), sa.Column('pending_product_uuid', wuttjamaican.db.util.UUID(), nullable=True),
sa.Column('product_id', sa.String(length=20), nullable=True),
sa.Column('product_scancode', sa.String(length=14), nullable=True), sa.Column('product_scancode', sa.String(length=14), nullable=True),
sa.Column('product_brand', sa.String(length=100), nullable=True), sa.Column('product_brand', sa.String(length=100), nullable=True),
sa.Column('product_description', sa.String(length=255), nullable=True), sa.Column('product_description', sa.String(length=255), nullable=True),
@ -213,10 +173,9 @@ def upgrade() -> None:
sa.Column('case_price_quoted', sa.Numeric(precision=8, scale=3), nullable=True), sa.Column('case_price_quoted', sa.Numeric(precision=8, scale=3), nullable=True),
sa.Column('discount_percent', sa.Numeric(precision=5, scale=3), nullable=True), sa.Column('discount_percent', sa.Numeric(precision=5, scale=3), nullable=True),
sa.Column('total_price', sa.Numeric(precision=8, scale=3), nullable=True), sa.Column('total_price', sa.Numeric(precision=8, scale=3), nullable=True),
sa.Column('status_code', sa.Integer(), nullable=True), sa.Column('status_code', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['batch_uuid'], ['sideshow_batch_neworder.uuid'], name=op.f('fk_sideshow_batch_neworder_row_batch_uuid_batch_neworder')), sa.ForeignKeyConstraint(['batch_uuid'], ['sideshow_batch_neworder.uuid'], name=op.f('fk_sideshow_batch_neworder_row_batch_uuid_batch_neworder')),
sa.ForeignKeyConstraint(['local_product_uuid'], ['sideshow_product_local.uuid'], name=op.f('fk_sideshow_batch_neworder_row_local_product_uuid_local_product')), sa.ForeignKeyConstraint(['pending_product_uuid'], ['sideshow_pending_product.uuid'], name=op.f('fk_sideshow_batch_neworder_row_pending_product_uuid_pending_product')),
sa.ForeignKeyConstraint(['pending_product_uuid'], ['sideshow_product_pending.uuid'], name=op.f('fk_sideshow_batch_neworder_row_pending_product_uuid_pending_product')),
sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_batch_neworder_row')) sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_batch_neworder_row'))
) )
@ -233,17 +192,11 @@ def downgrade() -> None:
# sideshow_order # sideshow_order
op.drop_table('sideshow_order') op.drop_table('sideshow_order')
# sideshow_product_local # sideshow_pending_product
op.drop_table('sideshow_product_local') op.drop_table('sideshow_pending_product')
# sideshow_product_pending # sideshow_pending_customer
op.drop_table('sideshow_product_pending') op.drop_table('sideshow_pending_customer')
# sideshow_customer_local
op.drop_table('sideshow_customer_local')
# sideshow_customer_pending
op.drop_table('sideshow_customer_pending')
# enums # enums
sa.Enum('PENDING', 'READY', 'RESOLVED', name='pendingproductstatus').drop(op.get_bind()) sa.Enum('PENDING', 'READY', 'RESOLVED', name='pendingproductstatus').drop(op.get_bind())

View file

@ -32,8 +32,6 @@ Primary :term:`data models <data model>`:
* :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.customers.LocalCustomer`
* :class:`~sideshow.db.model.products.LocalProduct`
* :class:`~sideshow.db.model.customers.PendingCustomer` * :class:`~sideshow.db.model.customers.PendingCustomer`
* :class:`~sideshow.db.model.products.PendingProduct` * :class:`~sideshow.db.model.products.PendingProduct`
@ -47,8 +45,8 @@ And the :term:`batch` models:
from wuttjamaican.db.model import * from wuttjamaican.db.model import *
# sideshow models # sideshow models
from .customers import LocalCustomer, PendingCustomer from .customers import PendingCustomer
from .products import LocalProduct, PendingProduct from .products import PendingProduct
from .orders import Order, OrderItem from .orders import Order, OrderItem
# batch models # batch models

View file

@ -58,8 +58,7 @@ class NewOrderBatch(model.BatchMixin, model.Base):
@declared_attr @declared_attr
def __table_args__(cls): def __table_args__(cls):
return cls.__default_table_args__() + ( return cls.__default_table_args__() + (
sa.ForeignKeyConstraint(['local_customer_uuid'], ['sideshow_customer_local.uuid']), sa.ForeignKeyConstraint(['pending_customer_uuid'], ['sideshow_pending_customer.uuid']),
sa.ForeignKeyConstraint(['pending_customer_uuid'], ['sideshow_customer_pending.uuid']),
) )
STATUS_OK = 1 STATUS_OK = 1
@ -73,27 +72,13 @@ class NewOrderBatch(model.BatchMixin, model.Base):
""") """)
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 ID of the proper customer account to which the order pertains, if
order pertains, if applicable. applicable.
See also :attr:`local_customer` and :attr:`pending_customer`. This will be set only when an "existing" customer account can be
selected for the order. See also :attr:`pending_customer`.
""") """)
local_customer_uuid = sa.Column(model.UUID(), nullable=True)
@declared_attr
def local_customer(cls):
return orm.relationship(
'LocalCustomer',
back_populates='new_order_batches',
doc="""
Reference to the
:class:`~sideshow.db.model.customers.LocalCustomer` record
for the order, if applicable.
See also :attr:`customer_id` and :attr:`pending_customer`.
""")
pending_customer_uuid = sa.Column(model.UUID(), nullable=True) pending_customer_uuid = sa.Column(model.UUID(), nullable=True)
@declared_attr @declared_attr
@ -106,7 +91,8 @@ class NewOrderBatch(model.BatchMixin, model.Base):
:class:`~sideshow.db.model.customers.PendingCustomer` :class:`~sideshow.db.model.customers.PendingCustomer`
record for the order, if applicable. record for the order, if applicable.
See also :attr:`customer_id` and :attr:`local_customer`. This is set only when making an order for a "new /
unknown" customer. See also :attr:`customer_id`.
""") """)
customer_name = sa.Column(sa.String(length=100), nullable=True, doc=""" customer_name = sa.Column(sa.String(length=100), nullable=True, doc="""
@ -140,8 +126,7 @@ class NewOrderBatchRow(model.BatchRowMixin, model.Base):
@declared_attr @declared_attr
def __table_args__(cls): def __table_args__(cls):
return cls.__default_table_args__() + ( return cls.__default_table_args__() + (
sa.ForeignKeyConstraint(['local_product_uuid'], ['sideshow_product_local.uuid']), sa.ForeignKeyConstraint(['pending_product_uuid'], ['sideshow_pending_product.uuid']),
sa.ForeignKeyConstraint(['pending_product_uuid'], ['sideshow_product_pending.uuid']),
) )
STATUS_OK = 1 STATUS_OK = 1
@ -173,27 +158,13 @@ class NewOrderBatchRow(model.BatchRowMixin, model.Base):
""" """
product_id = sa.Column(sa.String(length=20), nullable=True, doc=""" product_id = sa.Column(sa.String(length=20), nullable=True, doc="""
Proper ID for the :term:`external product` which the order item ID of the true product which the order item represents, if
represents, if applicable. applicable.
See also :attr:`local_product` and :attr:`pending_product`. This will be set only when an "existing" product can be selected
for the order. See also :attr:`pending_product`.
""") """)
local_product_uuid = sa.Column(model.UUID(), nullable=True)
@declared_attr
def local_product(cls):
return orm.relationship(
'LocalProduct',
back_populates='new_order_batch_rows',
doc="""
Reference to the
:class:`~sideshow.db.model.products.LocalProduct` record
for the order item, if applicable.
See also :attr:`product_id` and :attr:`pending_product`.
""")
pending_product_uuid = sa.Column(model.UUID(), nullable=True) pending_product_uuid = sa.Column(model.UUID(), nullable=True)
@declared_attr @declared_attr
@ -206,7 +177,8 @@ class NewOrderBatchRow(model.BatchRowMixin, model.Base):
:class:`~sideshow.db.model.products.PendingProduct` record :class:`~sideshow.db.model.products.PendingProduct` record
for the order item, if applicable. for the order item, if applicable.
See also :attr:`product_id` and :attr:`local_product`. This is set only when making an order for a "new /
unknown" product. See also :attr:`product_id`.
""") """)
product_scancode = sa.Column(sa.String(length=14), nullable=True, doc=""" product_scancode = sa.Column(sa.String(length=14), nullable=True, doc="""

View file

@ -34,13 +34,19 @@ from wuttjamaican.db import model
from sideshow.enum import PendingCustomerStatus from sideshow.enum import PendingCustomerStatus
class CustomerMixin: class PendingCustomer(model.Base):
""" """
Base class for customer tables. This has shared columns, used by e.g.: A "pending" customer record, used when entering an :term:`order`
for new/unknown customer.
"""
__tablename__ = 'sideshow_pending_customer'
* :class:`LocalCustomer` uuid = model.uuid_column()
* :class:`PendingCustomer`
""" customer_id = sa.Column(sa.String(length=20), nullable=True, doc="""
ID of the proper customer account associated with this record, if
applicable.
""")
full_name = sa.Column(sa.String(length=100), nullable=True, doc=""" full_name = sa.Column(sa.String(length=100), nullable=True, doc="""
Full display name for the customer account. Full display name for the customer account.
@ -62,74 +68,6 @@ class CustomerMixin:
Email address for the customer. Email address for the customer.
""") """)
def __str__(self):
return self.full_name or ""
class LocalCustomer(CustomerMixin, model.Base):
"""
This table contains the :term:`local customer` records.
Sideshow will do customer lookups against this table by default,
unless it's configured to use :term:`external customers <external
customer>` instead.
Also by default, when a :term:`new order batch` with a
:term:`pending customer` is executed, a new record is added to
this local customers table, for lookup next time.
"""
__tablename__ = 'sideshow_customer_local'
uuid = model.uuid_column()
external_id = sa.Column(sa.String(length=20), nullable=True, doc="""
ID of the proper customer account associated with this record, if
applicable.
""")
orders = orm.relationship(
'Order',
order_by='Order.order_id.desc()',
back_populates='local_customer',
cascade_backrefs=False,
doc="""
List of :class:`~sideshow.db.model.orders.Order` records
associated with this customer.
""")
new_order_batches = orm.relationship(
'NewOrderBatch',
order_by='NewOrderBatch.id.desc()',
back_populates='local_customer',
cascade_backrefs=False,
doc="""
List of
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch`
records associated with this customer.
""")
class PendingCustomer(CustomerMixin, model.Base):
"""
This table contains the :term:`pending customer` records, used
when creating an :term:`order` for new/unknown customer.
Sideshow will automatically create and (hopefully) delete these
records as needed.
By default, when a :term:`new order batch` with a pending customer
is executed, a new record is added to the :term:`local customers
<local customer>` table, for lookup next time.
"""
__tablename__ = 'sideshow_customer_pending'
uuid = model.uuid_column()
customer_id = sa.Column(sa.String(length=20), nullable=True, doc="""
ID of the proper customer account associated with this record, if
applicable.
""")
status = sa.Column(sa.Enum(PendingCustomerStatus), nullable=False, doc=""" status = sa.Column(sa.Enum(PendingCustomerStatus), nullable=False, doc="""
Status code for the customer record. Status code for the customer record.
""") """)
@ -169,3 +107,6 @@ class PendingCustomer(CustomerMixin, model.Base):
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch` :class:`~sideshow.db.model.batch.neworder.NewOrderBatch`
records associated with this customer. records associated with this customer.
""") """)
def __str__(self):
return self.full_name or ""

View file

@ -63,26 +63,14 @@ class Order(model.Base):
""") """)
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 ID of the proper customer account to which the order pertains, if
order pertains, if applicable. applicable.
See also :attr:`local_customer` and :attr:`pending_customer`. This will be set only when an "existing" customer account can be
assigned for the order. See also :attr:`pending_customer`.
""") """)
local_customer_uuid = model.uuid_fk_column('sideshow_customer_local.uuid', nullable=True) pending_customer_uuid = model.uuid_fk_column('sideshow_pending_customer.uuid', nullable=True)
local_customer = orm.relationship(
'LocalCustomer',
cascade_backrefs=False,
back_populates='orders',
doc="""
Reference to the
:class:`~sideshow.db.model.customers.LocalCustomer` record
for the order, if applicable.
See also :attr:`customer_id` and :attr:`pending_customer`.
""")
pending_customer_uuid = model.uuid_fk_column('sideshow_customer_pending.uuid', nullable=True)
pending_customer = orm.relationship( pending_customer = orm.relationship(
'PendingCustomer', 'PendingCustomer',
cascade_backrefs=False, cascade_backrefs=False,
@ -92,7 +80,8 @@ class Order(model.Base):
:class:`~sideshow.db.model.customers.PendingCustomer` record :class:`~sideshow.db.model.customers.PendingCustomer` record
for the order, if applicable. for the order, if applicable.
See also :attr:`customer_id` and :attr:`local_customer`. This is set only when the order is for a "new / unknown"
customer. See also :attr:`customer_id`.
""") """)
customer_name = sa.Column(sa.String(length=100), nullable=True, doc=""" customer_name = sa.Column(sa.String(length=100), nullable=True, doc="""
@ -169,26 +158,14 @@ class OrderItem(model.Base):
""") """)
product_id = sa.Column(sa.String(length=20), nullable=True, doc=""" product_id = sa.Column(sa.String(length=20), nullable=True, doc="""
Proper ID for the :term:`external product` which the order item ID of the true product which the order item represents, if
represents, if applicable. applicable.
See also :attr:`local_product` and :attr:`pending_product`. This will be set only when an "existing" product can be selected
for the order. See also :attr:`pending_product`.
""") """)
local_product_uuid = model.uuid_fk_column('sideshow_product_local.uuid', nullable=True) pending_product_uuid = model.uuid_fk_column('sideshow_pending_product.uuid', nullable=True)
local_product = orm.relationship(
'LocalProduct',
cascade_backrefs=False,
back_populates='order_items',
doc="""
Reference to the
:class:`~sideshow.db.model.products.LocalProduct` record for
the order item, if applicable.
See also :attr:`product_id` and :attr:`pending_product`.
""")
pending_product_uuid = model.uuid_fk_column('sideshow_product_pending.uuid', nullable=True)
pending_product = orm.relationship( pending_product = orm.relationship(
'PendingProduct', 'PendingProduct',
cascade_backrefs=False, cascade_backrefs=False,
@ -198,7 +175,8 @@ class OrderItem(model.Base):
:class:`~sideshow.db.model.products.PendingProduct` record for :class:`~sideshow.db.model.products.PendingProduct` record for
the order item, if applicable. the order item, if applicable.
See also :attr:`product_id` and :attr:`local_product`. This is set only when the order item is for a "new / unknown"
product. See also :attr:`product_id`.
""") """)
product_scancode = sa.Column(sa.String(length=14), nullable=True, doc=""" product_scancode = sa.Column(sa.String(length=14), nullable=True, doc="""
@ -332,15 +310,5 @@ class OrderItem(model.Base):
applicable/known. applicable/known.
""") """)
@property
def full_description(self):
""" """
fields = [
self.product_brand or '',
self.product_description or '',
self.product_size or '']
fields = [f.strip() for f in fields if f.strip()]
return ' '.join(fields)
def __str__(self): def __str__(self):
return self.full_description return str(self.pending_product or self.product_description or "")

View file

@ -34,13 +34,18 @@ from wuttjamaican.db import model
from sideshow.enum import PendingProductStatus from sideshow.enum import PendingProductStatus
class ProductMixin: class PendingProduct(model.Base):
""" """
Base class for product tables. This has shared columns, used by e.g.: A "pending" product record, used when entering an :term:`order
item` for new/unknown product.
"""
__tablename__ = 'sideshow_pending_product'
* :class:`LocalProduct` uuid = model.uuid_column()
* :class:`PendingProduct`
""" product_id = sa.Column(sa.String(length=20), nullable=True, doc="""
ID of the true product associated with this record, if applicable.
""")
scancode = sa.Column(sa.String(length=14), nullable=True, doc=""" scancode = sa.Column(sa.String(length=14), nullable=True, doc="""
Scancode for the product, as string. Scancode for the product, as string.
@ -112,82 +117,6 @@ class ProductMixin:
Arbitrary notes regarding the product, if applicable. Arbitrary notes regarding the product, if applicable.
""") """)
@property
def full_description(self):
""" """
fields = [
self.brand_name or '',
self.description or '',
self.size or '']
fields = [f.strip() for f in fields if f.strip()]
return ' '.join(fields)
def __str__(self):
return self.full_description
class LocalProduct(ProductMixin, model.Base):
"""
This table contains the :term:`local product` records.
Sideshow will do customer lookups against this table by default,
unless it's configured to use :term:`external products <external
product>` instead.
Also by default, when a :term:`new order batch` with
:term:`pending product(s) <pending product>` is executed, new
record(s) will be added to this local products table, for lookup
next time.
"""
__tablename__ = 'sideshow_product_local'
uuid = model.uuid_column()
external_id = sa.Column(sa.String(length=20), nullable=True, doc="""
ID of the true external product associated with this record, if
applicable.
""")
order_items = orm.relationship(
'OrderItem',
back_populates='local_product',
cascade_backrefs=False,
doc="""
List of :class:`~sideshow.db.model.orders.OrderItem` records
associated with this product.
""")
new_order_batch_rows = orm.relationship(
'NewOrderBatchRow',
back_populates='local_product',
cascade_backrefs=False,
doc="""
List of
:class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
records associated with this product.
""")
class PendingProduct(ProductMixin, model.Base):
"""
This table contains the :term:`pending product` records, used when
creating an :term:`order` for new/unknown product(s).
Sideshow will automatically create and (hopefully) delete these
records as needed.
By default, when a :term:`new order batch` with pending product(s)
is executed, new record(s) will be added to the :term:`local
products <local product>` table, for lookup next time.
"""
__tablename__ = 'sideshow_product_pending'
uuid = model.uuid_column()
product_id = sa.Column(sa.String(length=20), nullable=True, doc="""
ID of the true product associated with this record, if applicable.
""")
status = sa.Column(sa.Enum(PendingProductStatus), nullable=False, doc=""" status = sa.Column(sa.Enum(PendingProductStatus), nullable=False, doc="""
Status code for the product record. Status code for the product record.
""") """)
@ -209,8 +138,10 @@ class PendingProduct(ProductMixin, model.Base):
order_items = orm.relationship( order_items = orm.relationship(
'OrderItem', 'OrderItem',
back_populates='pending_product', # TODO
# order_by='NewOrderBatchRow.id.desc()',
cascade_backrefs=False, cascade_backrefs=False,
back_populates='pending_product',
doc=""" doc="""
List of :class:`~sideshow.db.model.orders.OrderItem` records List of :class:`~sideshow.db.model.orders.OrderItem` records
associated with this product. associated with this product.
@ -218,10 +149,25 @@ class PendingProduct(ProductMixin, model.Base):
new_order_batch_rows = orm.relationship( new_order_batch_rows = orm.relationship(
'NewOrderBatchRow', 'NewOrderBatchRow',
back_populates='pending_product', # TODO
# order_by='NewOrderBatchRow.id.desc()',
cascade_backrefs=False, cascade_backrefs=False,
back_populates='pending_product',
doc=""" doc="""
List of List of
:class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow` :class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
records associated with this product. records associated with this product.
""") """)
@property
def full_description(self):
""" """
fields = [
self.brand_name or '',
self.description or '',
self.size or '']
fields = [f.strip() for f in fields if f.strip()]
return ' '.join(fields)
def __str__(self):
return self.full_description

View file

@ -29,7 +29,7 @@ from wuttaweb.forms.schema import ObjectRef
class OrderRef(ObjectRef): class OrderRef(ObjectRef):
""" """
Schema type for an :class:`~sideshow.db.model.orders.Order` Custom schema type for an :class:`~sideshow.db.model.orders.Order`
reference field. reference field.
This is a subclass of This is a subclass of
@ -51,34 +51,9 @@ class OrderRef(ObjectRef):
return self.request.route_url('orders.view', uuid=order.uuid) return self.request.route_url('orders.view', uuid=order.uuid)
class LocalCustomerRef(ObjectRef):
"""
Schema type for a
:class:`~sideshow.db.model.customers.LocalCustomer` reference
field.
This is a subclass of
:class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`.
"""
@property
def model_class(self):
""" """
model = self.app.model
return model.LocalCustomer
def sort_query(self, query):
""" """
return query.order_by(self.model_class.full_name)
def get_object_url(self, customer):
""" """
return self.request.route_url('local_customers.view', uuid=customer.uuid)
class PendingCustomerRef(ObjectRef): class PendingCustomerRef(ObjectRef):
""" """
Schema type for a Custom schema type for a
:class:`~sideshow.db.model.customers.PendingCustomer` reference :class:`~sideshow.db.model.customers.PendingCustomer` reference
field. field.
@ -101,33 +76,9 @@ class PendingCustomerRef(ObjectRef):
return self.request.route_url('pending_customers.view', uuid=customer.uuid) return self.request.route_url('pending_customers.view', uuid=customer.uuid)
class LocalProductRef(ObjectRef):
"""
Schema type for a
:class:`~sideshow.db.model.products.LocalProduct` reference field.
This is a subclass of
:class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`.
"""
@property
def model_class(self):
""" """
model = self.app.model
return model.LocalProduct
def sort_query(self, query):
""" """
return query.order_by(self.model_class.scancode)
def get_object_url(self, product):
""" """
return self.request.route_url('local_products.view', uuid=product.uuid)
class PendingProductRef(ObjectRef): class PendingProductRef(ObjectRef):
""" """
Schema type for a Custom schema type for a
:class:`~sideshow.db.model.products.PendingProduct` reference :class:`~sideshow.db.model.products.PendingProduct` reference
field. field.

View file

@ -36,15 +36,14 @@ class SideshowMenuHandler(base.MenuHandler):
""" """ """ """
return [ return [
self.make_orders_menu(request), self.make_orders_menu(request),
self.make_customers_menu(request), self.make_pending_menu(request),
self.make_products_menu(request),
self.make_batch_menu(request), self.make_batch_menu(request),
self.make_admin_menu(request), self.make_admin_menu(request),
] ]
def make_orders_menu(self, request, **kwargs): def make_orders_menu(self, request, **kwargs):
""" """
Generate the Orders menu. Generate a typical Orders menu.
""" """
return { return {
'title': "Orders", 'title': "Orders",
@ -56,55 +55,34 @@ class SideshowMenuHandler(base.MenuHandler):
'perm': 'orders.create', 'perm': 'orders.create',
}, },
{'type': 'sep'}, {'type': 'sep'},
{
'title': "All Order Items",
'route': 'order_items',
'perm': 'order_items.list',
},
{ {
'title': "All Orders", 'title': "All Orders",
'route': 'orders', 'route': 'orders',
'perm': 'orders.list', 'perm': 'orders.list',
}, },
{
'title': "All Order Items",
'route': 'order_items',
'perm': 'order_items.list',
},
], ],
} }
def make_customers_menu(self, request, **kwargs): def make_pending_menu(self, request, **kwargs):
""" """
Generate the Customers menu. Generate a typical Pending menu.
""" """
return { return {
'title': "Customers", 'title': "Pending",
'type': 'menu', 'type': 'menu',
'items': [ 'items': [
{ {
'title': "Local Customers", 'title': "Customers",
'route': 'local_customers',
'perm': 'local_customers.list',
},
{
'title': "Pending Customers",
'route': 'pending_customers', 'route': 'pending_customers',
'perm': 'pending_customers.list', 'perm': 'pending_customers.list',
}, },
],
}
def make_products_menu(self, request, **kwargs):
"""
Generate the Products menu.
"""
return {
'title': "Products",
'type': 'menu',
'items': [
{ {
'title': "Local Products", 'title': "Products",
'route': 'local_products',
'perm': 'local_products.list',
},
{
'title': "Pending Products",
'route': 'pending_products', 'route': 'pending_products',
'perm': 'pending_products.list', 'perm': 'pending_products.list',
}, },
@ -113,7 +91,7 @@ class SideshowMenuHandler(base.MenuHandler):
def make_batch_menu(self, request, **kwargs): def make_batch_menu(self, request, **kwargs):
""" """
Generate the Batch menu. Generate a typical Batch menu.
""" """
return { return {
'title': "Batches", 'title': "Batches",

View file

@ -1,41 +0,0 @@
## -*- coding: utf-8; -*-
<%inherit file="/configure.mako" />
<%def name="form_content()">
<h3 class="block is-size-3">Products</h3>
<div class="block" style="padding-left: 2rem;">
<b-field message="If set, user can enter details of an arbitrary new &quot;pending&quot; product.">
<b-checkbox name="sideshow.orders.allow_unknown_products"
v-model="simpleSettings['sideshow.orders.allow_unknown_products']"
native-value="true"
@input="settingsNeedSaved = true">
Allow creating orders for "unknown" products
</b-checkbox>
</b-field>
<div v-show="simpleSettings['sideshow.orders.allow_unknown_products']"
style="padding-left: 2rem;">
<p class="block">
Require these fields for new product:
</p>
<div class="block"
style="margin-left: 2rem;">
% for field in pending_product_fields:
<b-field>
<b-checkbox name="sideshow.orders.unknown_product.fields.${field}.required"
v-model="simpleSettings['sideshow.orders.unknown_product.fields.${field}.required']"
native-value="true"
@input="settingsNeedSaved = true">
${field}
</b-checkbox>
</b-field>
% endfor
</div>
</div>
</div>
</%def>

View file

@ -130,20 +130,28 @@
<b-field label="Customer"> <b-field label="Customer">
<div style="display: flex; gap: 1rem; width: 100%;"> <div style="display: flex; gap: 1rem; width: 100%;">
<wutta-autocomplete ref="customerAutocomplete" <b-autocomplete ref="customerAutocomplete"
v-model="customerID" v-model="customerID"
:display="customerName" :style="{'flex-grow': customerID ? '0' : '1'}"
service-url="${url(f'{route_prefix}.customer_autocomplete')}" expanded
placeholder="Enter name or phone number" placeholder="Enter name or phone number"
## TODO
## serviceUrl="${url(f'{route_prefix}.customer_autocomplete')}"
% if request.use_oruga:
## :assigned-label="customerName"
@update:model-value="customerChanged"
% else:
## :initial-label="customerName"
@input="customerChanged" @input="customerChanged"
:style="{'flex-grow': customerID ? '0' : '1'}" % endif
expanded /> >
</b-autocomplete>
<b-button v-if="customerID" <b-button v-if="customerID"
@click="refreshCustomer" @click="refreshCustomer"
icon-pack="fas" icon-pack="fas"
icon-left="redo" icon-left="redo"
:disabled="refreshingCustomer"> :disabled="refreshingCustomer">
{{ refreshingCustomer ? "Refreshing" : "Refresh" }} {{ refreshingCustomer ? "Refreshig" : "Refresh" }}
</b-button> </b-button>
</div> </div>
</b-field> </b-field>
@ -340,9 +348,9 @@
<${b}-modal <${b}-modal
% if request.use_oruga: % if request.use_oruga:
v-model:active="editItemShowDialog" v-model:active="showingItemDialog"
% else: % else:
:active.sync="editItemShowDialog" :active.sync="showingItemDialog"
% endif % endif
:can-cancel="['escape', 'x']" :can-cancel="['escape', 'x']"
> >
@ -374,19 +382,28 @@
<div style="flex-grow: 1;"> <div style="flex-grow: 1;">
<b-field label="Product"> <b-field label="Product">
<wutta-autocomplete ref="productAutocomplete" <b-autocomplete ref="productLookup"
v-model="productID" v-model="productID"
:display="productDisplay" ## :style="{'flex-grow': customerID ? '0' : '1'}"
service-url="${url(f'{route_prefix}.product_autocomplete')}" ## expanded
placeholder="Enter brand, description etc." ## placeholder="Enter name or phone number"
@input="productChanged" /> ## ## serviceUrl="${url(f'{route_prefix}.customer_autocomplete')}"
## % if request.use_oruga:
## ## :assigned-label="customerName"
## @update:model-value="customerChanged"
## % else:
## ## :initial-label="customerName"
## @input="customerChanged"
## % endif
>
</b-autocomplete>
</b-field> </b-field>
<div v-if="productID"> <div v-if="productID">
<b-field grouped> <b-field grouped>
<b-field label="Scancode"> <b-field :label="productKeyLabel">
<span>{{ productScancode }}</span> <span>{{ productKey }}</span>
</b-field> </b-field>
<b-field label="Unit Size"> <b-field label="Unit Size">
@ -426,16 +443,16 @@
</div> </div>
</div> </div>
## <img v-if="productID" <img v-if="productID"
## :src="productImageURL" :src="productImageURL"
## style="max-height: 150px; max-width: 150px; "/> style="max-height: 150px; max-width: 150px; "/>
</div> </div>
<br /> <br />
<div class="field"> <div class="field">
<b-radio v-model="productIsKnown" <b-radio v-model="productIsKnown"
% if not allow_unknown_products: % if not allow_unknown_product:
disabled disabled
% endif % endif
:native-value="false"> :native-value="false">
@ -476,12 +493,12 @@
<div style="display: flex; gap: 1rem;"> <div style="display: flex; gap: 1rem;">
<b-field label="Scancode" <b-field :label="productKeyLabel"
% if 'scancode' in pending_product_required_fields: % if 'key' in pending_product_required_fields:
:type="pendingProduct.scancode ? null : 'is-danger'" :type="pendingProduct[productKeyField] ? null : 'is-danger'"
% endif % endif
style="width: 100%;"> style="width: 100%;">
<b-input v-model="pendingProduct.scancode" /> <b-input v-model="pendingProduct[productKeyField]" />
</b-field> </b-field>
<b-field label="Department" <b-field label="Department"
@ -688,7 +705,7 @@
</${b}-tabs> </${b}-tabs>
<div class="buttons"> <div class="buttons">
<b-button @click="editItemShowDialog = false"> <b-button @click="showingItemDialog = false">
Cancel Cancel
</b-button> </b-button>
<b-button type="is-primary" <b-button type="is-primary"
@ -696,7 +713,7 @@
:disabled="itemDialogSaveDisabled" :disabled="itemDialogSaveDisabled"
icon-pack="fas" icon-pack="fas"
icon-left="save"> icon-left="save">
{{ itemDialogSaving ? "Working, please wait..." : (this.editItemRow ? "Update Item" : "Add Item") }} {{ itemDialogSaving ? "Working, please wait..." : (this.editingItem ? "Update Item" : "Add Item") }}
</b-button> </b-button>
</div> </div>
@ -708,9 +725,9 @@
:data="items" :data="items"
:row-class="(row, i) => row.product_id ? null : 'has-text-success'"> :row-class="(row, i) => row.product_id ? null : 'has-text-success'">
<${b}-table-column label="Scancode" <${b}-table-column :label="productKeyLabel"
v-slot="props"> v-slot="props">
{{ props.row.product_scancode }} {{ props.row.product_key }}
</${b}-table-column> </${b}-table-column>
<${b}-table-column label="Brand" <${b}-table-column label="Brand"
@ -740,10 +757,8 @@
<${b}-table-column label="Unit Price" <${b}-table-column label="Unit Price"
v-slot="props"> v-slot="props">
<span <span :class="props.row.pricing_reflects_sale ? 'has-background-warning' : null">
##:class="props.row.pricing_reflects_sale ? 'has-background-warning' : null" {{ props.row.unit_price_display }}
>
{{ props.row.unit_price_quoted_display }}
</span> </span>
</${b}-table-column> </${b}-table-column>
@ -756,15 +771,17 @@
<${b}-table-column label="Vendor" <${b}-table-column label="Vendor"
v-slot="props"> v-slot="props">
{{ props.row.vendor_name }} {{ props.row.vendor_display }}
</${b}-table-column> </${b}-table-column>
<${b}-table-column field="actions" <${b}-table-column field="actions"
label="Actions" label="Actions"
v-slot="props"> v-slot="props">
<a href="#" <a href="#"
@click.prevent="editItemInit(props.row)"> % if not request.use_oruga:
class="grid-action"
% endif
@click.prevent="showEditItemDialog(props.row)">
% if request.use_oruga: % if request.use_oruga:
<span class="icon-text"> <span class="icon-text">
<o-icon icon="edit" /> <o-icon icon="edit" />
@ -829,14 +846,13 @@
batchTotalPriceDisplay: ${json.dumps(normalized_batch['total_price_display'])|n}, batchTotalPriceDisplay: ${json.dumps(normalized_batch['total_price_display'])|n},
customerPanelOpen: false, customerPanelOpen: false,
customerIsKnown: ${json.dumps(customer_is_known)|n}, customerIsKnown: ${json.dumps(bool(batch.customer_id))|n},
customerID: ${json.dumps(customer_id)|n}, customerID: ${json.dumps(batch.customer_id)|n},
customerName: ${json.dumps(customer_name)|n}, customerName: ${json.dumps(batch.customer_name)|n},
orderPhoneNumber: ${json.dumps(phone_number)|n}, orderPhoneNumber: ${json.dumps(batch.phone_number)|n},
orderEmailAddress: ${json.dumps(email_address)|n}, orderEmailAddress: ${json.dumps(batch.email_address)|n},
refreshingCustomer: false, refreshingCustomer: false,
newCustomerFullName: ${json.dumps(new_customer_full_name)|n},
newCustomerFirstName: ${json.dumps(new_customer_first_name)|n}, newCustomerFirstName: ${json.dumps(new_customer_first_name)|n},
newCustomerLastName: ${json.dumps(new_customer_last_name)|n}, newCustomerLastName: ${json.dumps(new_customer_last_name)|n},
newCustomerPhone: ${json.dumps(new_customer_phone)|n}, newCustomerPhone: ${json.dumps(new_customer_phone)|n},
@ -851,8 +867,8 @@
items: ${json.dumps(order_items)|n}, items: ${json.dumps(order_items)|n},
editItemRow: null, editingItem: null,
editItemShowDialog: false, showingItemDialog: false,
itemDialogSaving: false, itemDialogSaving: false,
% if request.use_oruga: % if request.use_oruga:
itemDialogTab: 'product', itemDialogTab: 'product',
@ -860,11 +876,16 @@
itemDialogTabIndex: 0, itemDialogTabIndex: 0,
% endif % endif
productIsKnown: true, ## TODO
productIsKnown: false,
selectedProduct: null, selectedProduct: null,
productID: null, productID: null,
productDisplay: null, productDisplay: null,
productScancode: null, ## TODO
productKey: null,
productKeyField: 'scancode',
productKeyLabel: "Scancode",
productSize: null, productSize: null,
productCaseQuantity: null, productCaseQuantity: null,
productUnitPrice: null, productUnitPrice: null,
@ -905,8 +926,20 @@
customerPanelHeader() { customerPanelHeader() {
let text = "Customer" let text = "Customer"
if (this.customerName) { if (this.customerIsKnown) {
text = "Customer: " + this.customerName if (this.customerID) {
## TODO
text = "Customer: TODO"
## if (this.$refs.customerAutocomplete) {
## text = "Customer: " + this.$refs.customerAutocomplete.getDisplayText()
## } else {
## text = "Customer: " + this.customerName
## }
}
} else {
if (this.customerName) {
text = "Customer: " + this.customerName
}
} }
if (!this.customerPanelOpen) { if (!this.customerPanelOpen) {
@ -1049,29 +1082,41 @@
} }
}, },
watch: { ## TODO
## watch: {
customerIsKnown: function(val) { ##
## contactIsKnown: function(val) {
if (val) { ##
// user clicks "customer is in the system" ## // when user clicks "contact is known" then we want to
## // set focus to the autocomplete component
// clear customer ## if (val) {
this.customerChanged(null) ## this.$nextTick(() => {
## this.$refs.customerAutocomplete.focus()
// focus customer autocomplete ## })
this.$nextTick(() => { ##
this.$refs.customerAutocomplete.focus() ## // if user has already specified a proper contact,
}) ## // i.e. `contactUUID` is not null, *and* user has
## // clicked the "contact is not yet in the system"
} else { ## // button, i.e. `val` is false, then we want to *clear
// user clicks "customer is NOT in the system" ## // out* the existing contact selection. this is
## // primarily to avoid any ambiguity.
// remove true customer; set pending (or null) ## } else if (this.contactUUID) {
this.setPendingCustomer() ## this.$refs.customerAutocomplete.clearSelection()
} ## }
}, ## },
}, ##
## productIsKnown(newval, oldval) {
## // TODO: seems like this should be better somehow?
## // e.g. maybe we should not be clearing *everything*
## // in case user accidentally clicks, and then clicks
## // "is known" again? and if we *should* clear all,
## // why does that require 2 steps?
## if (!newval) {
## this.selectedProduct = null
## this.clearProduct()
## }
## },
## },
methods: { methods: {
@ -1117,7 +1162,7 @@
this.submittingOrder = true this.submittingOrder = true
const params = { const params = {
action: 'submit_order', action: 'submit_new_order',
} }
this.submitBatchData(params, response => { this.submitBatchData(params, response => {
@ -1133,28 +1178,29 @@
customerChanged(customerID, callback) { customerChanged(customerID, callback) {
const params = {} let params
if (customerID) { if (!customerID) {
params.action = 'assign_customer' params = {
params.customer_id = customerID action: 'unassign_contact',
}
} else { } else {
params.action = 'unassign_customer' params = {
action: 'assign_contact',
customer_id: customerID,
}
} }
this.submitBatchData(params, response => {
this.submitBatchData(params, ({data}) => { this.customerID = response.data.customer_id
this.customerID = data.customer_id this.customerName = response.data.customer_name
this.customerName = data.customer_name this.orderPhoneNumber = response.data.phone_number
this.orderPhoneNumber = data.phone_number this.orderEmailAddress = response.data.email_address
this.orderEmailAddress = data.email_address this.addOtherPhoneNumber = response.data.add_phone_number
this.addOtherEmailAddress = response.data.add_email_address
this.contactPhones = response.data.contact_phones
this.contactEmails = response.data.contact_emails
if (callback) { if (callback) {
callback() callback()
} }
}, response => {
this.$buefy.toast.open({
message: "Update failed: " + (response.data.error || "(unknown error)"),
type: 'is-danger',
duration: 2000, // 2 seconds
})
}) })
}, },
@ -1193,8 +1239,7 @@
} }
this.submitBatchData(params, response => { this.submitBatchData(params, response => {
this.customerName = response.data.new_customer_full_name this.customerName = response.data.new_customer_name
this.newCustomerFullName = response.data.new_customer_full_name
this.newCustomerFirstName = response.data.new_customer_first_name this.newCustomerFirstName = response.data.new_customer_first_name
this.newCustomerLastName = response.data.new_customer_last_name this.newCustomerLastName = response.data.new_customer_last_name
this.newCustomerPhone = response.data.phone_number this.newCustomerPhone = response.data.phone_number
@ -1214,40 +1259,6 @@
}, },
// remove true customer; set pending customer if present
// (else null). this happens when user clicks "customer is
// NOT in the system"
setPendingCustomer() {
let params
if (this.newCustomerFirstName) {
params = {
action: 'set_pending_customer',
first_name: this.newCustomerFirstName,
last_name: this.newCustomerLastName,
phone_number: this.newCustomerPhone,
email_address: this.newCustomerEmail,
}
} else {
params = {
action: 'unassign_customer',
}
}
this.submitBatchData(params, ({data}) => {
this.customerID = data.customer_id
this.customerName = data.new_customer_full_name
this.orderPhoneNumber = data.phone_number
this.orderEmailAddress = data.email_address
}, response => {
this.$buefy.toast.open({
message: "Update failed: " + (response.data.error || "(unknown error)"),
type: 'is-danger',
duration: 2000, // 2 seconds
})
})
},
getCasePriceDisplay() { getCasePriceDisplay() {
if (this.productIsKnown) { if (this.productIsKnown) {
return this.productCasePriceDisplay return this.productCasePriceDisplay
@ -1302,76 +1313,6 @@
} }
}, },
clearProduct() {
this.productID = null
this.productDisplay = null
this.productScancode = null
this.productSize = null
this.productCaseQuantity = null
this.productUnitPrice = null
this.productUnitPriceDisplay = null
this.productUnitRegularPriceDisplay = null
this.productCasePrice = null
this.productCasePriceDisplay = null
this.productSalePrice = null
this.productSalePriceDisplay = null
this.productSaleEndsDisplay = null
this.productUnitChoices = this.defaultUnitChoices
},
productChanged(productID) {
if (productID) {
const params = {
action: 'get_product_info',
product_id: productID,
}
// nb. it is possible for the handler to "swap"
// the product selection, i.e. user chooses a "per
// LB" item but the handler only allows selling by
// the "case" item. so we do not assume the uuid
// received above is the correct one, but just use
// whatever came back from handler
this.submitBatchData(params, ({data}) => {
this.selectedProduct = data
this.productID = data.product_id
this.productScancode = data.scancode
this.productDisplay = data.full_description
this.productSize = data.size
this.productCaseQuantity = data.case_size
// TODO: what is the difference here
this.productUnitPrice = data.unit_price_reg
this.productUnitPriceDisplay = data.unit_price_reg_display
this.productUnitRegularPriceDisplay = data.unit_price_display
this.productCasePrice = data.case_price_quoted
this.productCasePriceDisplay = data.case_price_quoted_display
this.productSalePrice = data.unit_price_sale
this.productSalePriceDisplay = data.unit_price_sale_display
this.productSaleEndsDisplay = data.sale_ends_display
// this.setProductUnitChoices(data.uom_choices)
% if request.use_oruga:
this.itemDialogTab = 'quantity'
% else:
this.itemDialogTabIndex = 1
% endif
// nb. hack to force refresh for vue3
this.refreshProductDescription += 1
this.refreshTotalPrice += 1
}, response => {
this.clearProduct()
})
} else {
this.clearProduct()
}
},
## TODO ## TODO
## productLookupSelected(selected) { ## productLookupSelected(selected) {
## // TODO: this still is a hack somehow, am sure of it. ## // TODO: this still is a hack somehow, am sure of it.
@ -1399,12 +1340,14 @@
showAddItemDialog() { showAddItemDialog() {
this.customerPanelOpen = false this.customerPanelOpen = false
this.editItemRow = null this.editingItem = null
this.productIsKnown = true // TODO
// this.productIsKnown = true
this.productIsKnown = false
## this.selectedProduct = null ## this.selectedProduct = null
this.productID = null this.productID = null
this.productDisplay = null this.productDisplay = null
this.productScancode = null ## this.productKey = null
this.productSize = null this.productSize = null
this.productCaseQuantity = null this.productCaseQuantity = null
this.productUnitPrice = null this.productUnitPrice = null
@ -1428,15 +1371,14 @@
% else: % else:
this.itemDialogTabIndex = 0 this.itemDialogTabIndex = 0
% endif % endif
this.editItemShowDialog = true this.showingItemDialog = true
this.$nextTick(() => { this.$nextTick(() => {
// this.$refs.productLookup.focus() this.$refs.productLookup.focus()
this.$refs.productAutocomplete.focus()
}) })
}, },
editItemInit(row) { showEditItemDialog(row) {
this.editItemRow = row this.editingItem = row
this.productIsKnown = !!row.product_id this.productIsKnown = !!row.product_id
this.productID = row.product_id this.productID = row.product_id
@ -1460,9 +1402,10 @@
this.pendingProduct = pending this.pendingProduct = pending
this.productDisplay = row.product_full_description this.productDisplay = row.product_full_description
this.productScancode = row.product_scancode this.productKey = row.product_key
this.productSize = row.product_size this.productSize = row.product_size
this.productCaseQuantity = row.case_size this.productCaseQuantity = row.case_quantity
this.productURL = row.product_url
this.productUnitPrice = row.unit_price_quoted this.productUnitPrice = row.unit_price_quoted
this.productUnitPriceDisplay = row.unit_price_quoted_display this.productUnitPriceDisplay = row.unit_price_quoted_display
this.productUnitRegularPriceDisplay = row.unit_price_reg_display this.productUnitRegularPriceDisplay = row.unit_price_reg_display
@ -1486,7 +1429,7 @@
% else: % else:
this.itemDialogTabIndex = 1 this.itemDialogTabIndex = 1
% endif % endif
this.editItemShowDialog = true this.showingItemDialog = true
}, },
deleteItem(index) { deleteItem(index) {
@ -1515,20 +1458,25 @@
itemDialogAttemptSave() { itemDialogAttemptSave() {
this.itemDialogSaving = true this.itemDialogSaving = true
const params = { let params = {
product_is_known: this.productIsKnown,
order_qty: this.productQuantity, order_qty: this.productQuantity,
order_uom: this.productUOM, order_uom: this.productUOM,
} }
% if allow_item_discounts:
params.discount_percent = this.productDiscountPercent
% endif
if (this.productIsKnown) { if (this.productIsKnown) {
params.product_info = this.productID params.product_uuid = this.productUUID
} else { } else {
params.product_info = this.pendingProduct params.pending_product = this.pendingProduct
} }
if (this.editItemRow) { if (this.editingItem) {
params.action = 'update_item' params.action = 'update_item'
params.uuid = this.editItemRow.uuid params.uuid = this.editingItem.uuid
} else { } else {
params.action = 'add_item' params.action = 'add_item'
} }
@ -1543,7 +1491,7 @@
// overwriting the item record, or else display will // overwriting the item record, or else display will
// not update properly // not update properly
for (let [key, value] of Object.entries(response.data.row)) { for (let [key, value] of Object.entries(response.data.row)) {
this.editItemRow[key] = value this.editingItem[key] = value
} }
} }
@ -1551,7 +1499,7 @@
this.batchTotalPriceDisplay = response.data.batch.total_price_display this.batchTotalPriceDisplay = response.data.batch.total_price_display
this.itemDialogSaving = false this.itemDialogSaving = false
this.editItemShowDialog = false this.showingItemDialog = false
}, response => { }, response => {
this.itemDialogSaving = false this.itemDialogSaving = false
}) })

View file

@ -29,7 +29,7 @@ from wuttaweb.forms.schema import WuttaMoney
from sideshow.db.model import NewOrderBatch from sideshow.db.model import NewOrderBatch
from sideshow.batch.neworder import NewOrderBatchHandler from sideshow.batch.neworder import NewOrderBatchHandler
from sideshow.web.forms.schema import LocalCustomerRef, PendingCustomerRef from sideshow.web.forms.schema import PendingCustomerRef
class NewOrderBatchView(BatchMasterView): class NewOrderBatchView(BatchMasterView):
@ -87,7 +87,6 @@ class NewOrderBatchView(BatchMasterView):
'id', 'id',
'store_id', 'store_id',
'customer_id', 'customer_id',
'local_customer',
'pending_customer', 'pending_customer',
'customer_name', 'customer_name',
'phone_number', 'phone_number',
@ -116,11 +115,9 @@ class NewOrderBatchView(BatchMasterView):
'product_description', 'product_description',
'product_size', 'product_size',
'special_order', 'special_order',
'unit_price_quoted',
'case_size',
'case_price_quoted',
'order_qty', 'order_qty',
'order_uom', 'order_uom',
'case_size',
'total_price', 'total_price',
'status_code', 'status_code',
] ]
@ -141,9 +138,6 @@ class NewOrderBatchView(BatchMasterView):
""" """ """ """
super().configure_form(f) super().configure_form(f)
# local_customer
f.set_node('local_customer', LocalCustomerRef(self.request))
# pending_customer # pending_customer
f.set_node('pending_customer', PendingCustomerRef(self.request)) f.set_node('pending_customer', PendingCustomerRef(self.request))
@ -159,14 +153,6 @@ class NewOrderBatchView(BatchMasterView):
# order_uom # order_uom
#g.set_renderer('order_uom', self.grid_render_enum, enum=enum.ORDER_UOM) #g.set_renderer('order_uom', self.grid_render_enum, enum=enum.ORDER_UOM)
# unit_price_quoted
g.set_label('unit_price_quoted', "Unit Price", column_only=True)
g.set_renderer('unit_price_quoted', 'currency')
# case_price_quoted
g.set_label('case_price_quoted', "Case Price", column_only=True)
g.set_renderer('case_price_quoted', 'currency')
# total_price # total_price
g.set_renderer('total_price', 'currency') g.set_renderer('total_price', 'currency')

View file

@ -27,161 +27,7 @@ Views for Customers
from wuttaweb.views import MasterView from wuttaweb.views import MasterView
from wuttaweb.forms.schema import UserRef, WuttaEnum from wuttaweb.forms.schema import UserRef, WuttaEnum
from sideshow.db.model import LocalCustomer, PendingCustomer from sideshow.db.model import PendingCustomer
class LocalCustomerView(MasterView):
"""
Master view for
:class:`~sideshow.db.model.customers.LocalCustomer`; route prefix
is ``local_customers``.
Notable URLs provided by this class:
* ``/local/customers/``
* ``/local/customers/new``
* ``/local/customers/XXX``
* ``/local/customers/XXX/edit``
* ``/local/customers/XXX/delete``
"""
model_class = LocalCustomer
model_title = "Local Customer"
route_prefix = 'local_customers'
url_prefix = '/local/customers'
labels = {
'external_id': "External ID",
}
grid_columns = [
'external_id',
'full_name',
'first_name',
'last_name',
'phone_number',
'email_address',
]
sort_defaults = 'full_name'
form_fields = [
'external_id',
'full_name',
'first_name',
'last_name',
'phone_number',
'email_address',
'orders',
'new_order_batches',
]
def configure_grid(self, g):
""" """
super().configure_grid(g)
# links
g.set_link('full_name')
g.set_link('first_name')
g.set_link('last_name')
g.set_link('phone_number')
g.set_link('email_address')
def configure_form(self, f):
""" """
super().configure_form(f)
customer = f.model_instance
# external_id
if self.creating:
f.remove('external_id')
else:
f.set_readonly('external_id')
# full_name
if self.creating or self.editing:
f.remove('full_name')
# orders
if self.creating or self.editing:
f.remove('orders')
else:
f.set_grid('orders', self.make_orders_grid(customer))
# new_order_batches
if self.creating or self.editing:
f.remove('new_order_batches')
else:
f.set_grid('new_order_batches', self.make_new_order_batches_grid(customer))
def make_orders_grid(self, customer):
"""
Make and return the grid for the Orders field.
"""
model = self.app.model
route_prefix = self.get_route_prefix()
grid = self.make_grid(key=f'{route_prefix}.view.orders',
model_class=model.Order,
data=customer.orders,
columns=[
'order_id',
'total_price',
'created',
'created_by',
],
labels={
'order_id': "Order ID",
})
grid.set_renderer('total_price', grid.render_currency)
if self.request.has_perm('orders.view'):
url = lambda order, i: self.request.route_url('orders.view', uuid=order.uuid)
grid.add_action('view', icon='eye', url=url)
grid.set_link('order_id')
return grid
def make_new_order_batches_grid(self, customer):
"""
Make and return the grid for the New Order Batches field.
"""
model = self.app.model
route_prefix = self.get_route_prefix()
grid = self.make_grid(key=f'{route_prefix}.view.new_order_batches',
model_class=model.NewOrderBatch,
data=customer.new_order_batches,
columns=[
'id',
'total_price',
'created',
'created_by',
'executed',
],
labels={
'id': "Batch ID",
},
renderers={
'id': 'batch_id',
'total_price': 'currency',
})
if self.request.has_perm('neworder_batches.view'):
url = lambda batch, i: self.request.route_url('neworder_batches.view', uuid=batch.uuid)
grid.add_action('view', icon='eye', url=url)
grid.set_link('id')
return grid
def objectify(self, form):
""" """
enum = self.app.enum
customer = super().objectify(form)
customer.full_name = self.app.make_full_name(customer.first_name,
customer.last_name)
return customer
class PendingCustomerView(MasterView): class PendingCustomerView(MasterView):
@ -225,9 +71,12 @@ class PendingCustomerView(MasterView):
'customer_id', 'customer_id',
'full_name', 'full_name',
'first_name', 'first_name',
'middle_name',
'last_name', 'last_name',
'phone_number', 'phone_number',
'phone_type',
'email_address', 'email_address',
'email_type',
'status', 'status',
'created', 'created',
'created_by', 'created_by',
@ -389,9 +238,6 @@ class PendingCustomerView(MasterView):
def defaults(config, **kwargs): def defaults(config, **kwargs):
base = globals() base = globals()
LocalCustomerView = kwargs.get('LocalCustomerView', base['LocalCustomerView'])
LocalCustomerView.defaults(config)
PendingCustomerView = kwargs.get('PendingCustomerView', base['PendingCustomerView']) PendingCustomerView = kwargs.get('PendingCustomerView', base['PendingCustomerView'])
PendingCustomerView.defaults(config) PendingCustomerView.defaults(config)

View file

@ -31,13 +31,11 @@ import colander
from sqlalchemy import orm from sqlalchemy import orm
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
from sideshow.db.model import Order, OrderItem from sideshow.db.model import Order, OrderItem
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, PendingCustomerRef, PendingProductRef
LocalCustomerRef, LocalProductRef,
PendingCustomerRef, PendingProductRef)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -60,7 +58,6 @@ class OrderView(MasterView):
""" """
model_class = Order model_class = Order
editable = False editable = False
configurable = True
labels = { labels = {
'order_id': "Order ID", 'order_id': "Order ID",
@ -84,7 +81,6 @@ class OrderView(MasterView):
'order_id', 'order_id',
'store_id', 'store_id',
'customer_id', 'customer_id',
'local_customer',
'pending_customer', 'pending_customer',
'customer_name', 'customer_name',
'phone_number', 'phone_number',
@ -126,14 +122,15 @@ class OrderView(MasterView):
PENDING_PRODUCT_ENTRY_FIELDS = [ PENDING_PRODUCT_ENTRY_FIELDS = [
'scancode', 'scancode',
'department_id',
'department_name',
'brand_name', 'brand_name',
'description', 'description',
'size', 'size',
'department_name',
'vendor_name', 'vendor_name',
'vendor_item_code', 'vendor_item_code',
'case_size',
'unit_cost', 'unit_cost',
'case_size',
'unit_price_reg', 'unit_price_reg',
] ]
@ -168,20 +165,6 @@ class OrderView(MasterView):
batch, which in turn creates a true batch, which in turn creates a true
:class:`~sideshow.db.model.orders.Order`, and user is :class:`~sideshow.db.model.orders.Order`, and user is
redirected to the "view order" page. redirected to the "view order" page.
See also these methods which may be called from this one,
based on user actions:
* :meth:`start_over()`
* :meth:`cancel_order()`
* :meth:`assign_customer()`
* :meth:`unassign_customer()`
* :meth:`set_pending_customer()`
* :meth:`get_product_info()`
* :meth:`add_item()`
* :meth:`update_item()`
* :meth:`delete_item()`
* :meth:`submit_order()`
""" """
enum = self.app.enum enum = self.app.enum
self.creating = True self.creating = True
@ -205,25 +188,22 @@ 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 = [
'assign_customer', # 'assign_contact',
'unassign_customer', # 'unassign_contact',
# 'update_phone_number', # 'update_phone_number',
# 'update_email_address', # 'update_email_address',
'set_pending_customer', 'set_pending_customer',
# 'get_customer_info', # 'get_customer_info',
# # 'set_customer_data', # # 'set_customer_data',
'get_product_info', # 'get_product_info',
# 'get_past_items', # 'get_past_items',
'add_item', 'add_item',
'update_item', 'update_item',
'delete_item', 'delete_item',
'submit_order', 'submit_new_order',
] ]
if action in json_actions: if action in json_actions:
try: result = getattr(self, action)(batch, data)
result = getattr(self, action)(batch, data)
except Exception as error:
result = {'error': self.app.render_error(error)}
return self.json_response(result) return self.json_response(result)
return self.json_response({'error': "unknown form action"}) return self.json_response({'error': "unknown form action"})
@ -233,10 +213,10 @@ 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],
'allow_unknown_product': True, # TODO
'default_uom_choices': self.get_default_uom_choices(), 'default_uom_choices': self.get_default_uom_choices(),
'default_uom': None, # TODO? 'default_uom': None, # TODO?
'allow_unknown_products': (self.batch_handler.allow_unknown_products()
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(),
}) })
return self.render_to_response('create', context) return self.render_to_response('create', context)
@ -275,96 +255,6 @@ class OrderView(MasterView):
return batch return batch
def customer_autocomplete(self):
"""
AJAX view for customer autocomplete, when entering new order.
This should invoke a configured handler for the autocomplete
behavior, but that is not yet implemented. For now it uses
built-in logic only, which queries the
:class:`~sideshow.db.model.customers.LocalCustomer` table.
"""
session = self.Session()
term = self.request.GET.get('term', '').strip()
if not term:
return []
return self.mock_autocomplete_customers(session, term, user=self.request.user)
# TODO: move this to some handler
def mock_autocomplete_customers(self, session, term, user=None):
""" """
import sqlalchemy as sa
model = self.app.model
# base query
query = session.query(model.LocalCustomer)
# filter query
criteria = [model.LocalCustomer.full_name.ilike(f'%{word}%')
for word in term.split()]
query = query.filter(sa.and_(*criteria))
# sort query
query = query.order_by(model.LocalCustomer.full_name)
# get data
# TODO: need max_results option
customers = query.all()
# get results
def result(customer):
return {'value': customer.uuid.hex,
'label': customer.full_name}
return [result(c) for c in customers]
def product_autocomplete(self):
"""
AJAX view for product autocomplete, when entering new order.
This should invoke a configured handler for the autocomplete
behavior, but that is not yet implemented. For now it uses
built-in logic only, which queries the
:class:`~sideshow.db.model.products.LocalProduct` table.
"""
session = self.Session()
term = self.request.GET.get('term', '').strip()
if not term:
return []
return self.mock_autocomplete_products(session, term, user=self.request.user)
# TODO: move this to some handler
def mock_autocomplete_products(self, session, term, user=None):
""" """
import sqlalchemy as sa
model = self.app.model
# base query
query = session.query(model.LocalProduct)
# filter query
criteria = []
for word in term.split():
criteria.append(sa.or_(
model.LocalProduct.brand_name.ilike(f'%{word}%'),
model.LocalProduct.description.ilike(f'%{word}%')))
query = query.filter(sa.and_(*criteria))
# sort query
query = query.order_by(model.LocalProduct.brand_name,
model.LocalProduct.description)
# get data
# TODO: need max_results option
products = query.all()
# get results
def result(product):
return {'value': product.uuid.hex,
'label': product.full_description}
return [result(c) for c in products]
def get_pending_product_required_fields(self): def get_pending_product_required_fields(self):
""" """ """ """
required = [] required = []
@ -384,10 +274,7 @@ class OrderView(MasterView):
new batch for them. new batch for them.
This is a "batch action" method which may be called from This is a "batch action" method which may be called from
:meth:`create()`. See also: :meth:`create()`.
* :meth:`cancel_order()`
* :meth:`submit_order()`
""" """
# drop current batch # drop current batch
self.batch_handler.do_delete(batch, self.request.user) self.batch_handler.do_delete(batch, self.request.user)
@ -404,10 +291,7 @@ class OrderView(MasterView):
back to "List Orders" page. back to "List Orders" page.
This is a "batch action" method which may be called from This is a "batch action" method which may be called from
:meth:`create()`. See also: :meth:`create()`.
* :meth:`start_over()`
* :meth:`submit_order()`
""" """
self.batch_handler.do_delete(batch, self.request.user) self.batch_handler.do_delete(batch, self.request.user)
self.Session.flush() self.Session.flush()
@ -422,193 +306,86 @@ class OrderView(MasterView):
def get_context_customer(self, batch): def get_context_customer(self, batch):
""" """ """ """
context = { context = {
'customer_is_known': True, 'customer_id': batch.customer_id,
'customer_id': None,
'customer_name': batch.customer_name, 'customer_name': batch.customer_name,
'phone_number': batch.phone_number, 'phone_number': batch.phone_number,
'email_address': batch.email_address, 'email_address': batch.email_address,
'new_customer_name': None,
'new_customer_first_name': None,
'new_customer_last_name': None,
'new_customer_phone': None,
'new_customer_email': None,
} }
# customer_id
use_local = self.batch_handler.use_local_customers()
if use_local:
local = batch.local_customer
if local:
context['customer_id'] = local.uuid.hex
else: # use external
context['customer_id'] = batch.customer_id
# pending customer
pending = batch.pending_customer pending = batch.pending_customer
if pending: if pending:
context.update({ context.update({
'new_customer_first_name': pending.first_name, 'new_customer_first_name': pending.first_name,
'new_customer_last_name': pending.last_name, 'new_customer_last_name': pending.last_name,
'new_customer_full_name': pending.full_name, 'new_customer_name': pending.full_name,
'new_customer_phone': pending.phone_number, 'new_customer_phone': pending.phone_number,
'new_customer_email': pending.email_address, 'new_customer_email': pending.email_address,
}) })
# declare customer "not known" only if pending is in use # figure out if customer is "known" from user's perspective.
if (pending # if we have an ID then it's definitely known, otherwise if we
and not batch.customer_id and not batch.local_customer # have a pending customer then it's definitely *not* known,
and batch.customer_name): # but if no pending customer yet then we can still "assume" it
context['customer_is_known'] = False # is known, by default, until user specifies otherwise.
if batch.customer_id:
context['customer_is_known'] = True
else:
context['customer_is_known'] = not pending
return context return context
def assign_customer(self, batch, data):
"""
Assign the true customer account for a batch.
This calls
:meth:`~sideshow.batch.neworder.NewOrderBatchHandler.set_customer()`
for the heavy lifting.
This is a "batch action" method which may be called from
:meth:`create()`. See also:
* :meth:`unassign_customer()`
* :meth:`set_pending_customer()`
"""
customer_id = data.get('customer_id')
if not customer_id:
return {'error': "Must provide customer_id"}
self.batch_handler.set_customer(batch, customer_id)
return self.get_context_customer(batch)
def unassign_customer(self, batch, data):
"""
Clear the customer info for a batch.
This calls
:meth:`~sideshow.batch.neworder.NewOrderBatchHandler.set_customer()`
for the heavy lifting.
This is a "batch action" method which may be called from
:meth:`create()`. See also:
* :meth:`assign_customer()`
* :meth:`set_pending_customer()`
"""
self.batch_handler.set_customer(batch, None)
return self.get_context_customer(batch)
def set_pending_customer(self, batch, data): def set_pending_customer(self, batch, data):
""" """
This will set/update the batch pending customer info. This will set/update the batch pending customer info.
This calls This calls
:meth:`~sideshow.batch.neworder.NewOrderBatchHandler.set_customer()` :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.set_pending_customer()`
for the heavy lifting. for the heavy lifting.
This is a "batch action" method which may be called from
:meth:`create()`. See also:
* :meth:`assign_customer()`
* :meth:`unassign_customer()`
"""
self.batch_handler.set_customer(batch, data, user=self.request.user)
return self.get_context_customer(batch)
def get_product_info(self, batch, data):
"""
Fetch data for a specific product. (Nothing is modified.)
Depending on config, this will fetch a :term:`local product`
or :term:`external product` to get the data.
This should invoke a configured handler for the query
behavior, but that is not yet implemented. For now it uses
built-in logic only, which queries the
: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()`.
""" """
product_id = data.get('product_id') data['created_by'] = self.request.user
if not product_id: try:
return {'error': "Must specify a product ID"} self.batch_handler.set_pending_customer(batch, data)
except Exception as error:
return {'error': self.app.render_error(error)}
use_local = self.batch_handler.use_local_products() self.Session.flush()
if use_local: context = self.get_context_customer(batch)
data = self.get_local_product_info(product_id) return context
else:
raise NotImplementedError("TODO: add integration handler")
if 'error' in data:
return data
if 'unit_price_reg' in data and 'unit_price_reg_display' not in data:
data['unit_price_reg_display'] = self.app.render_currency(data['unit_price_reg'])
if 'unit_price_reg' in data and 'unit_price_quoted' not in data:
data['unit_price_quoted'] = data['unit_price_reg']
if 'unit_price_quoted' in data and 'unit_price_quoted_display' not in data:
data['unit_price_quoted_display'] = self.app.render_currency(data['unit_price_quoted'])
if 'case_price_quoted' not in data:
if data.get('unit_price_quoted') is not None and data.get('case_size') is not None:
data['case_price_quoted'] = data['unit_price_quoted'] * data['case_size']
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'])
decimal_fields = [
'case_size',
'unit_price_reg',
'unit_price_quoted',
'case_price_quoted',
]
for field in decimal_fields:
if field in list(data):
value = data[field]
if isinstance(value, decimal.Decimal):
data[field] = float(value)
return data
# TODO: move this to some handler
def get_local_product_info(self, product_id):
""" """
model = self.app.model
session = self.Session()
product = session.get(model.LocalProduct, product_id)
if not product:
return {'error': "Product not found"}
return {
'product_id': product.uuid.hex,
'scancode': product.scancode,
'brand_name': product.brand_name,
'description': product.description,
'size': product.size,
'full_description': product.full_description,
'weighed': product.weighed,
'special_order': product.special_order,
'department_id': product.department_id,
'department_name': product.department_name,
'case_size': product.case_size,
'unit_price_reg': product.unit_price_reg,
'vendor_name': product.vendor_name,
'vendor_item_code': product.vendor_item_code,
}
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.
This is a "batch action" method which may be called from This is a "batch action" method which may be called from
:meth:`create()`. See also: :meth:`create()`.
* :meth:`update_item()`
* :meth:`delete_item()`
""" """
row = self.batch_handler.add_item(batch, data['product_info'], order_qty = decimal.Decimal(data.get('order_qty') or '0')
data['order_qty'], data['order_uom']) order_uom = data['order_uom']
if data.get('product_is_known'):
raise NotImplementedError
else: # unknown product; add pending
pending = data['pending_product']
for field in ('unit_cost', 'unit_price_reg', 'case_size'):
if field in pending:
try:
pending[field] = decimal.Decimal(pending[field])
except decimal.InvalidOperation:
return {'error': f"Invalid entry for field: {field}"}
pending['created_by'] = self.request.user
row = self.batch_handler.add_pending_product(batch, pending,
order_qty, order_uom)
return {'batch': self.normalize_batch(batch), return {'batch': self.normalize_batch(batch),
'row': self.normalize_row(row)} 'row': self.normalize_row(row)}
@ -618,17 +395,15 @@ class OrderView(MasterView):
This updates a row in the user's current new order batch. This updates a row in the user's current new order batch.
This is a "batch action" method which may be called from This is a "batch action" method which may be called from
:meth:`create()`. See also: :meth:`create()`.
* :meth:`add_item()`
* :meth:`delete_item()`
""" """
model = self.app.model model = self.app.model
enum = self.app.enum
session = self.Session() session = self.Session()
uuid = data.get('uuid') uuid = data.get('uuid')
if not uuid: if not uuid:
return {'error': "Must specify row UUID"} return {'error': "Must specify a row UUID"}
row = session.get(model.NewOrderBatchRow, uuid) row = session.get(model.NewOrderBatchRow, uuid)
if not row: if not row:
@ -637,8 +412,20 @@ class OrderView(MasterView):
if row.batch is not batch: if row.batch is not batch:
return {'error': "Row is for wrong batch"} return {'error': "Row is for wrong batch"}
self.batch_handler.update_item(row, data['product_info'], order_qty = decimal.Decimal(data.get('order_qty') or '0')
data['order_qty'], data['order_uom']) order_uom = data['order_uom']
if data.get('product_is_known'):
raise NotImplementedError
else: # pending product
# set these first, since row will be refreshed below
row.order_qty = order_qty
row.order_uom = order_uom
# nb. this will refresh the row
self.batch_handler.set_pending_product(row, data['pending_product'])
return {'batch': self.normalize_batch(batch), return {'batch': self.normalize_batch(batch),
'row': self.normalize_row(row)} 'row': self.normalize_row(row)}
@ -648,10 +435,7 @@ class OrderView(MasterView):
This deletes a row from the user's current new order batch. This deletes a row from the user's current new order batch.
This is a "batch action" method which may be called from This is a "batch action" method which may be called from
:meth:`create()`. See also: :meth:`create()`.
* :meth:`add_item()`
* :meth:`update_item()`
""" """
model = self.app.model model = self.app.model
session = self.app.get_session(batch) session = self.app.get_session(batch)
@ -668,18 +452,16 @@ class OrderView(MasterView):
return {'error': "Row is for wrong batch"} return {'error': "Row is for wrong batch"}
self.batch_handler.do_remove_row(row) self.batch_handler.do_remove_row(row)
session.flush()
return {'batch': self.normalize_batch(batch)} return {'batch': self.normalize_batch(batch)}
def submit_order(self, batch, data): def submit_new_order(self, batch, data):
""" """
This submits the user's current new order batch, hence This submits the user's current new order batch, hence
executing the batch and creating the true order. executing the batch and creating the true order.
This is a "batch action" method which may be called from This is a "batch action" method which may be called from
:meth:`create()`. See also: :meth:`create()`.
* :meth:`start_over()`
* :meth:`cancel_order()`
""" """
user = self.request.user user = self.request.user
reason = self.batch_handler.why_not_execute(batch, user=user) reason = self.batch_handler.why_not_execute(batch, user=user)
@ -720,7 +502,6 @@ class OrderView(MasterView):
data = { data = {
'uuid': row.uuid.hex, 'uuid': row.uuid.hex,
'sequence': row.sequence, 'sequence': row.sequence,
'product_id': None,
'product_scancode': row.product_scancode, 'product_scancode': row.product_scancode,
'product_brand': row.product_brand, 'product_brand': row.product_brand,
'product_description': row.product_description, 'product_description': row.product_description,
@ -728,8 +509,8 @@ class OrderView(MasterView):
'product_weighed': row.product_weighed, 'product_weighed': row.product_weighed,
'department_display': row.department_name, 'department_display': 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': self.app.render_quantity(row.case_size),
'order_qty': float(row.order_qty), 'order_qty': self.app.render_quantity(row.order_qty),
'order_uom': row.order_uom, 'order_uom': row.order_uom,
'order_uom_choices': self.get_default_uom_choices(), 'order_uom_choices': self.get_default_uom_choices(),
'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,
@ -742,33 +523,6 @@ class OrderView(MasterView):
'status_text': row.status_text, 'status_text': row.status_text,
} }
use_local = self.batch_handler.use_local_products()
# product_id
if use_local:
if row.local_product:
data['product_id'] = row.local_product.uuid.hex
else:
data['product_id'] = row.product_id
# product_full_description
if use_local:
if row.local_product:
data['product_full_description'] = row.local_product.full_description
else: # use external
pass # TODO
if not data.get('product_id') and row.pending_product:
data['product_full_description'] = row.pending_product.full_description
# vendor_name
if use_local:
if row.local_product:
data['vendor_name'] = row.local_product.vendor_name
else: # use external
pass # TODO
if not data.get('product_id') and row.pending_product:
data['vendor_name'] = row.pending_product.vendor_name
if row.unit_price_reg: if row.unit_price_reg:
data['unit_price_reg'] = float(row.unit_price_reg) data['unit_price_reg'] = float(row.unit_price_reg)
data['unit_price_reg_display'] = self.app.render_currency(row.unit_price_reg) data['unit_price_reg_display'] = self.app.render_currency(row.unit_price_reg)
@ -781,8 +535,21 @@ class OrderView(MasterView):
data['sale_ends'] = str(row.sale_ends) data['sale_ends'] = str(row.sale_ends)
data['sale_ends_display'] = self.app.render_date(row.sale_ends) data['sale_ends_display'] = self.app.render_date(row.sale_ends)
# if row.unit_price_sale and row.unit_price_quoted == row.unit_price_sale:
# data['pricing_reflects_sale'] = True
# TODO
if row.pending_product:
data['product_full_description'] = row.pending_product.full_description
# else:
# data['product_full_description'] = row.product_description
# if row.pending_product:
# data['vendor_display'] = row.pending_product.vendor_name
if row.pending_product: if row.pending_product:
pending = row.pending_product pending = row.pending_product
# data['vendor_display'] = pending.vendor_name
data['pending_product'] = { data['pending_product'] = {
'uuid': pending.uuid.hex, 'uuid': pending.uuid.hex,
'scancode': pending.scancode, 'scancode': pending.scancode,
@ -800,17 +567,19 @@ class OrderView(MasterView):
'special_order': pending.special_order, 'special_order': pending.special_order,
} }
# TODO: remove this
data['product_key'] = row.product_scancode
# display text for order qty/uom # display text for order qty/uom
if row.order_uom == enum.ORDER_UOM_CASE: if row.order_uom == enum.ORDER_UOM_CASE:
order_qty = self.app.render_quantity(row.order_qty)
if row.case_size is None: if row.case_size is None:
case_qty = unit_qty = '??' case_qty = unit_qty = '??'
else: else:
case_qty = self.app.render_quantity(row.case_size) case_qty = data['case_size']
unit_qty = self.app.render_quantity(row.order_qty * row.case_size) unit_qty = self.app.render_quantity(row.order_qty * row.case_size)
CS = enum.ORDER_UOM[enum.ORDER_UOM_CASE] CS = enum.ORDER_UOM[enum.ORDER_UOM_CASE]
EA = enum.ORDER_UOM[enum.ORDER_UOM_UNIT] EA = enum.ORDER_UOM[enum.ORDER_UOM_UNIT]
data['order_qty_display'] = (f"{order_qty} {CS} " data['order_qty_display'] = (f"{data['order_qty']} {CS} "
f"(&times; {case_qty} = {unit_qty} {EA})") f"(&times; {case_qty} = {unit_qty} {EA})")
else: else:
unit_qty = self.app.render_quantity(row.order_qty) unit_qty = self.app.render_quantity(row.order_qty)
@ -826,16 +595,9 @@ class OrderView(MasterView):
def configure_form(self, f): def configure_form(self, f):
""" """ """ """
super().configure_form(f) super().configure_form(f)
order = f.model_instance
# local_customer
f.set_node('local_customer', LocalCustomerRef(self.request))
# pending_customer # pending_customer
if order.customer_id or order.local_customer: f.set_node('pending_customer', PendingCustomerRef(self.request))
f.remove('pending_customer')
else:
f.set_node('pending_customer', PendingCustomerRef(self.request))
# total_price # total_price
f.set_node('total_price', WuttaMoney(self.request)) f.set_node('total_price', WuttaMoney(self.request))
@ -908,75 +670,6 @@ class OrderView(MasterView):
""" """ """ """
return self.request.route_url('order_items.view', uuid=item.uuid) return self.request.route_url('order_items.view', uuid=item.uuid)
def configure_get_simple_settings(self):
""" """
settings = [
# products
{'name': 'sideshow.orders.allow_unknown_products',
'type': bool,
'default': True},
]
# required fields for new product entry
for field in self.PENDING_PRODUCT_ENTRY_FIELDS:
setting = {'name': f'sideshow.orders.unknown_product.fields.{field}.required',
'type': bool}
if field == 'description':
setting['default'] = True
settings.append(setting)
return settings
def configure_get_context(self, **kwargs):
""" """
context = super().configure_get_context(**kwargs)
context['pending_product_fields'] = self.PENDING_PRODUCT_ENTRY_FIELDS
return context
@classmethod
def defaults(cls, config):
cls._order_defaults(config)
cls._defaults(config)
@classmethod
def _order_defaults(cls, config):
route_prefix = cls.get_route_prefix()
permission_prefix = cls.get_permission_prefix()
url_prefix = cls.get_url_prefix()
model_title = cls.get_model_title()
model_title_plural = cls.get_model_title_plural()
# fix perm group
config.add_wutta_permission_group(permission_prefix,
model_title_plural,
overwrite=False)
# extra perm required to create order with unknown/pending product
config.add_wutta_permission(permission_prefix,
f'{permission_prefix}.create_unknown_product',
f"Create new {model_title} for unknown/pending product")
# customer autocomplete
config.add_route(f'{route_prefix}.customer_autocomplete',
f'{url_prefix}/customer-autocomplete',
request_method='GET')
config.add_view(cls, attr='customer_autocomplete',
route_name=f'{route_prefix}.customer_autocomplete',
renderer='json',
permission=f'{permission_prefix}.list')
# product autocomplete
config.add_route(f'{route_prefix}.product_autocomplete',
f'{url_prefix}/product-autocomplete',
request_method='GET')
config.add_view(cls, attr='product_autocomplete',
route_name=f'{route_prefix}.product_autocomplete',
renderer='json',
permission=f'{permission_prefix}.list')
class OrderItemView(MasterView): class OrderItemView(MasterView):
""" """
@ -1006,8 +699,7 @@ class OrderItemView(MasterView):
'product_brand': "Brand", 'product_brand': "Brand",
'product_description': "Description", 'product_description': "Description",
'product_size': "Size", 'product_size': "Size",
'product_weighed': "Sold by Weight", 'department_name': "Department",
'department_id': "Department ID",
'order_uom': "Order UOM", 'order_uom': "Order UOM",
'status_code': "Status", 'status_code': "Status",
} }
@ -1035,7 +727,6 @@ class OrderItemView(MasterView):
# 'customer_name', # 'customer_name',
'sequence', 'sequence',
'product_id', 'product_id',
'local_product',
'pending_product', 'pending_product',
'product_scancode', 'product_scancode',
'product_brand', 'product_brand',
@ -1115,46 +806,27 @@ class OrderItemView(MasterView):
enum = self.app.enum enum = self.app.enum
return enum.ORDER_ITEM_STATUS[value] return enum.ORDER_ITEM_STATUS[value]
def get_instance_title(self, item):
""" """
enum = self.app.enum
title = str(item)
status = enum.ORDER_ITEM_STATUS[item.status_code]
return f"({status}) {title}"
def configure_form(self, f): def configure_form(self, f):
""" """ """ """
super().configure_form(f) super().configure_form(f)
enum = self.app.enum enum = self.app.enum
item = f.model_instance
# order # order
f.set_node('order', OrderRef(self.request)) f.set_node('order', OrderRef(self.request))
# local_product
f.set_node('local_product', LocalProductRef(self.request))
# pending_product # pending_product
if item.product_id or item.local_product: f.set_node('pending_product', PendingProductRef(self.request))
f.remove('pending_product')
else:
f.set_node('pending_product', PendingProductRef(self.request))
# order_qty # order_qty
f.set_node('order_qty', WuttaQuantity(self.request)) f.set_node('order_qty', WuttaQuantity(self.request))
# order_uom # order_uom
f.set_node('order_uom', WuttaDictEnum(self.request, enum.ORDER_UOM)) # TODO
#f.set_node('order_uom', WuttaEnum(self.request, enum.OrderUOM))
# case_size # case_size
f.set_node('case_size', WuttaQuantity(self.request)) f.set_node('case_size', WuttaQuantity(self.request))
# unit_cost
f.set_node('unit_cost', WuttaMoney(self.request, scale=4))
# unit_price_reg
f.set_node('unit_price_reg', WuttaMoney(self.request))
# unit_price_quoted # unit_price_quoted
f.set_node('unit_price_quoted', WuttaMoney(self.request)) f.set_node('unit_price_quoted', WuttaMoney(self.request))
@ -1164,21 +836,18 @@ class OrderItemView(MasterView):
# total_price # total_price
f.set_node('total_price', WuttaMoney(self.request)) f.set_node('total_price', WuttaMoney(self.request))
# status
f.set_node('status_code', WuttaDictEnum(self.request, enum.ORDER_ITEM_STATUS))
# paid_amount # paid_amount
f.set_node('paid_amount', WuttaMoney(self.request)) f.set_node('paid_amount', WuttaMoney(self.request))
def get_xref_buttons(self, item): def get_xref_buttons(self, item):
""" """ """ """
buttons = super().get_xref_buttons(item) buttons = super().get_xref_buttons(item)
model = self.app.model
if self.request.has_perm('orders.view'): if self.request.has_perm('orders.view'):
url = self.request.route_url('orders.view', uuid=item.order_uuid) url = self.request.route_url('orders.view', uuid=item.order_uuid)
buttons.append( buttons.append(
self.make_button("View the Order", url=url, self.make_button("View the Order", primary=True, icon_left='eye', url=url))
primary=True, icon_left='eye'))
return buttons return buttons

View file

@ -25,194 +25,9 @@ Views for Products
""" """
from wuttaweb.views import MasterView from wuttaweb.views import MasterView
from wuttaweb.forms.schema import UserRef, WuttaEnum, WuttaMoney, WuttaQuantity from wuttaweb.forms.schema import UserRef, WuttaEnum, WuttaMoney
from sideshow.db.model import LocalProduct, PendingProduct from sideshow.db.model import PendingProduct
class LocalProductView(MasterView):
"""
Master view for :class:`~sideshow.db.model.products.LocalProduct`;
route prefix is ``local_products``.
Notable URLs provided by this class:
* ``/local/products/``
* ``/local/products/new``
* ``/local/products/XXX``
* ``/local/products/XXX/edit``
* ``/local/products/XXX/delete``
"""
model_class = LocalProduct
model_title = "Local Product"
route_prefix = 'local_products'
url_prefix = '/local/products'
labels = {
'external_id': "External ID",
'department_id': "Department ID",
}
grid_columns = [
'scancode',
'brand_name',
'description',
'size',
'department_name',
'special_order',
'case_size',
'unit_cost',
'unit_price_reg',
]
sort_defaults = 'scancode'
form_fields = [
'external_id',
'scancode',
'brand_name',
'description',
'size',
'department_id',
'department_name',
'special_order',
'vendor_name',
'vendor_item_code',
'case_size',
'unit_cost',
'unit_price_reg',
'notes',
'orders',
'new_order_batches',
]
def configure_grid(self, g):
""" """
super().configure_grid(g)
# unit_cost
g.set_renderer('unit_cost', 'currency', scale=4)
# unit_price_reg
g.set_label('unit_price_reg', "Reg. Price", column_only=True)
g.set_renderer('unit_price_reg', 'currency')
# links
g.set_link('scancode')
g.set_link('brand_name')
g.set_link('description')
g.set_link('size')
def configure_form(self, f):
""" """
super().configure_form(f)
enum = self.app.enum
product = f.model_instance
# external_id
if self.creating:
f.remove('external_id')
else:
f.set_readonly('external_id')
# TODO: should not have to explicitly mark these nodes
# as required=False.. i guess i do for now b/c i am
# totally overriding the node from colanderlachemy
# case_size
f.set_node('case_size', WuttaQuantity(self.request))
f.set_required('case_size', False)
# unit_cost
f.set_node('unit_cost', WuttaMoney(self.request, scale=4))
f.set_required('unit_cost', False)
# unit_price_reg
f.set_node('unit_price_reg', WuttaMoney(self.request))
f.set_required('unit_price_reg', False)
# notes
f.set_widget('notes', 'notes')
# orders
if self.creating or self.editing:
f.remove('orders')
else:
f.set_grid('orders', self.make_orders_grid(product))
# new_order_batches
if self.creating or self.editing:
f.remove('new_order_batches')
else:
f.set_grid('new_order_batches', self.make_new_order_batches_grid(product))
def make_orders_grid(self, product):
"""
Make and return the grid for the Orders field.
"""
model = self.app.model
route_prefix = self.get_route_prefix()
orders = set([item.order for item in product.order_items])
orders = sorted(orders, key=lambda order: order.order_id)
grid = self.make_grid(key=f'{route_prefix}.view.orders',
model_class=model.Order,
data=orders,
columns=[
'order_id',
'total_price',
'created',
'created_by',
],
labels={
'order_id': "Order ID",
},
renderers={
'total_price': 'currency',
})
if self.request.has_perm('orders.view'):
url = lambda order, i: self.request.route_url('orders.view', uuid=order.uuid)
grid.add_action('view', icon='eye', url=url)
grid.set_link('order_id')
return grid
def make_new_order_batches_grid(self, product):
"""
Make and return the grid for the New Order Batches field.
"""
model = self.app.model
route_prefix = self.get_route_prefix()
batches = set([row.batch for row in product.new_order_batch_rows])
batches = sorted(batches, key=lambda batch: batch.id)
grid = self.make_grid(key=f'{route_prefix}.view.new_order_batches',
model_class=model.NewOrderBatch,
data=batches,
columns=[
'id',
'total_price',
'created',
'created_by',
'executed',
],
labels={
'id': "Batch ID",
'status_code': "Status",
},
renderers={
'id': 'batch_id',
})
if self.request.has_perm('neworder_batches.view'):
url = lambda batch, i: self.request.route_url('neworder_batches.view', uuid=batch.uuid)
grid.add_action('view', icon='eye', url=url)
grid.set_link('id')
return grid
class PendingProductView(MasterView): class PendingProductView(MasterView):
@ -434,9 +249,6 @@ class PendingProductView(MasterView):
def defaults(config, **kwargs): def defaults(config, **kwargs):
base = globals() base = globals()
LocalProductView = kwargs.get('LocalProductView', base['LocalProductView'])
LocalProductView.defaults(config)
PendingProductView = kwargs.get('PendingProductView', base['PendingProductView']) PendingProductView = kwargs.get('PendingProductView', base['PendingProductView'])
PendingProductView.defaults(config) PendingProductView.defaults(config)

View file

@ -1,8 +1,6 @@
# -*- coding: utf-8; -*- # -*- coding: utf-8; -*-
import datetime
import decimal import decimal
from unittest.mock import patch
from wuttjamaican.testing import DataTestCase from wuttjamaican.testing import DataTestCase
@ -20,133 +18,61 @@ class TestNewOrderBatchHandler(DataTestCase):
def make_handler(self): def make_handler(self):
return mod.NewOrderBatchHandler(self.config) return mod.NewOrderBatchHandler(self.config)
def tets_use_local_customers(self): def test_set_pending_customer(self):
handler = self.make_handler()
# true by default
self.assertTrue(handler.use_local_customers())
# config can disable
config.setdefault('sideshow.orders.use_local_customers', 'false')
self.assertFalse(handler.use_local_customers())
def tets_use_local_products(self):
handler = self.make_handler()
# true by default
self.assertTrue(handler.use_local_products())
# config can disable
config.setdefault('sideshow.orders.use_local_products', 'false')
self.assertFalse(handler.use_local_products())
def tets_allow_unknown_products(self):
handler = self.make_handler()
# true by default
self.assertTrue(handler.allow_unknown_products())
# config can disable
config.setdefault('sideshow.orders.allow_unknown_products', 'false')
self.assertFalse(handler.allow_unknown_products())
def test_set_customer(self):
model = self.app.model model = self.app.model
handler = self.make_handler() handler = self.make_handler()
user = model.User(username='barney') user = model.User(username='barney')
self.session.add(user) self.session.add(user)
# customer starts blank batch = handler.make_batch(self.session, created_by=user, customer_id=42)
batch = handler.make_batch(self.session, created_by=user) self.assertEqual(batch.customer_id, 42)
self.session.add(batch)
self.session.flush()
self.assertIsNone(batch.customer_id)
self.assertIsNone(batch.local_customer)
self.assertIsNone(batch.pending_customer) self.assertIsNone(batch.pending_customer)
self.assertIsNone(batch.customer_name) self.assertIsNone(batch.customer_name)
self.assertIsNone(batch.phone_number) self.assertIsNone(batch.phone_number)
self.assertIsNone(batch.email_address) self.assertIsNone(batch.email_address)
# pending, typical (nb. full name is automatic) # auto full_name
handler.set_customer(batch, { handler.set_pending_customer(batch, {
'first_name': "Fred", 'first_name': "Fred",
'last_name': "Flintstone", 'last_name': "Flintstone",
'phone_number': '555-1234', 'phone_number': '555-1234',
'email_address': 'fred@mailinator.com', 'email_address': 'fred@mailinator.com',
}) })
self.assertIsNone(batch.customer_id) self.assertIsNone(batch.customer_id)
self.assertIsNone(batch.local_customer)
self.assertIsInstance(batch.pending_customer, model.PendingCustomer) self.assertIsInstance(batch.pending_customer, model.PendingCustomer)
customer = batch.pending_customer customer = batch.pending_customer
self.assertEqual(customer.full_name, "Fred Flintstone")
self.assertEqual(customer.first_name, "Fred") self.assertEqual(customer.first_name, "Fred")
self.assertEqual(customer.last_name, "Flintstone") self.assertEqual(customer.last_name, "Flintstone")
self.assertEqual(customer.full_name, "Fred Flintstone")
self.assertEqual(customer.phone_number, '555-1234') self.assertEqual(customer.phone_number, '555-1234')
self.assertEqual(customer.email_address, 'fred@mailinator.com') self.assertEqual(customer.email_address, 'fred@mailinator.com')
self.assertEqual(batch.customer_name, "Fred Flintstone") self.assertEqual(batch.customer_name, "Fred Flintstone")
self.assertEqual(batch.phone_number, '555-1234') self.assertEqual(batch.phone_number, '555-1234')
self.assertEqual(batch.email_address, 'fred@mailinator.com') self.assertEqual(batch.email_address, 'fred@mailinator.com')
# pending, minimal # explicit full_name
last_customer = customer # save ref to prev record batch = handler.make_batch(self.session, created_by=user, customer_id=42)
handler.set_customer(batch, {'full_name': "Wilma Flintstone"}) handler.set_pending_customer(batch, {
'full_name': "Freddy Flintstone",
'first_name': "Fred",
'last_name': "Flintstone",
'phone_number': '555-1234',
'email_address': 'fred@mailinator.com',
})
self.assertIsNone(batch.customer_id) self.assertIsNone(batch.customer_id)
self.assertIsNone(batch.local_customer)
self.assertIsInstance(batch.pending_customer, model.PendingCustomer) self.assertIsInstance(batch.pending_customer, model.PendingCustomer)
customer = batch.pending_customer customer = batch.pending_customer
self.assertIs(customer, last_customer) self.assertEqual(customer.full_name, "Freddy Flintstone")
self.assertEqual(customer.full_name, "Wilma Flintstone") self.assertEqual(customer.first_name, "Fred")
self.assertIsNone(customer.first_name) self.assertEqual(customer.last_name, "Flintstone")
self.assertIsNone(customer.last_name) self.assertEqual(customer.phone_number, '555-1234')
self.assertIsNone(customer.phone_number) self.assertEqual(customer.email_address, 'fred@mailinator.com')
self.assertIsNone(customer.email_address) self.assertEqual(batch.customer_name, "Freddy Flintstone")
self.assertEqual(batch.customer_name, "Wilma Flintstone") self.assertEqual(batch.phone_number, '555-1234')
self.assertIsNone(batch.phone_number) self.assertEqual(batch.email_address, 'fred@mailinator.com')
self.assertIsNone(batch.email_address)
# local customer def test_add_pending_product(self):
local = model.LocalCustomer(full_name="Bam Bam",
first_name="Bam", last_name="Bam",
phone_number='555-4321')
self.session.add(local)
self.session.flush()
handler.set_customer(batch, local.uuid.hex)
self.session.flush()
self.assertIsNone(batch.customer_id)
# nb. pending customer does not get removed
self.assertIsInstance(batch.pending_customer, model.PendingCustomer)
self.assertIsInstance(batch.local_customer, model.LocalCustomer)
customer = batch.local_customer
self.assertEqual(customer.full_name, "Bam Bam")
self.assertEqual(customer.first_name, "Bam")
self.assertEqual(customer.last_name, "Bam")
self.assertEqual(customer.phone_number, '555-4321')
self.assertIsNone(customer.email_address)
self.assertEqual(batch.customer_name, "Bam Bam")
self.assertEqual(batch.phone_number, '555-4321')
self.assertIsNone(batch.email_address)
# local customer, not found
mock_uuid = self.app.make_true_uuid()
self.assertRaises(ValueError, handler.set_customer, batch, mock_uuid.hex)
# external lookup not implemented
self.config.setdefault('sideshow.orders.use_local_customers', 'false')
self.assertRaises(NotImplementedError, handler.set_customer, batch, '42')
# null
handler.set_customer(batch, None)
self.session.flush()
self.assertIsNone(batch.customer_id)
# nb. pending customer does not get removed
self.assertIsInstance(batch.pending_customer, model.PendingCustomer)
self.assertIsNone(batch.local_customer)
self.assertIsNone(batch.customer_name)
self.assertIsNone(batch.phone_number)
self.assertIsNone(batch.email_address)
def test_add_item(self):
model = self.app.model model = self.app.model
enum = self.app.enum enum = self.app.enum
handler = self.make_handler() handler = self.make_handler()
@ -158,115 +84,42 @@ class TestNewOrderBatchHandler(DataTestCase):
self.session.add(batch) self.session.add(batch)
self.assertEqual(len(batch.rows), 0) self.assertEqual(len(batch.rows), 0)
# pending, typical
kw = dict( kw = dict(
scancode='07430500001', scancode='07430500132',
brand_name='Bragg', brand_name='Bragg',
description='Vinegar', description='Vinegar',
size='1oz', size='32oz',
case_size=12, case_size=12,
unit_cost=decimal.Decimal('1.99'), unit_cost=decimal.Decimal('3.99'),
unit_price_reg=decimal.Decimal('2.99'), unit_price_reg=decimal.Decimal('5.99'),
created_by=user,
) )
row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_UNIT) row = handler.add_pending_product(batch, kw, 1, enum.ORDER_UOM_UNIT)
# nb. this is the first row in batch
self.assertEqual(len(batch.rows), 1) self.assertEqual(len(batch.rows), 1)
self.assertIs(batch.rows[0], row) self.assertIs(batch.rows[0], row)
self.assertIsNone(row.product_id)
self.assertIsNone(row.local_product) self.assertEqual(row.product_scancode, '07430500132')
product = row.pending_product
self.assertIsInstance(product, model.PendingProduct)
self.assertEqual(product.scancode, '07430500001')
self.assertEqual(product.brand_name, 'Bragg')
self.assertEqual(product.description, 'Vinegar')
self.assertEqual(product.size, '1oz')
self.assertEqual(product.case_size, 12)
self.assertEqual(product.unit_cost, decimal.Decimal('1.99'))
self.assertEqual(product.unit_price_reg, decimal.Decimal('2.99'))
self.assertEqual(row.product_scancode, '07430500001')
self.assertEqual(row.product_brand, 'Bragg') self.assertEqual(row.product_brand, 'Bragg')
self.assertEqual(row.product_description, 'Vinegar') self.assertEqual(row.product_description, 'Vinegar')
self.assertEqual(row.product_size, '1oz') self.assertEqual(row.product_size, '32oz')
self.assertEqual(row.case_size, 12) self.assertEqual(row.case_size, 12)
self.assertEqual(row.unit_cost, decimal.Decimal('1.99')) self.assertEqual(row.unit_cost, decimal.Decimal('3.99'))
self.assertEqual(row.unit_price_reg, decimal.Decimal('2.99')) self.assertEqual(row.unit_price_reg, decimal.Decimal('5.99'))
self.assertEqual(row.unit_price_quoted, decimal.Decimal('2.99')) self.assertEqual(row.unit_price_quoted, decimal.Decimal('5.99'))
self.assertEqual(row.case_price_quoted, decimal.Decimal('35.88')) self.assertEqual(row.case_price_quoted, decimal.Decimal('71.88'))
self.assertEqual(row.total_price, decimal.Decimal('2.99'))
# pending, minimal
row = handler.add_item(batch, {'description': "Tangerines"}, 1, enum.ORDER_UOM_UNIT)
self.assertIsNone(row.product_id)
self.assertIsNone(row.local_product)
product = row.pending_product product = row.pending_product
self.assertIsInstance(product, model.PendingProduct) self.assertIsInstance(product, model.PendingProduct)
self.assertIsNone(product.scancode) self.assertEqual(product.scancode, '07430500132')
self.assertIsNone(product.brand_name) self.assertEqual(product.brand_name, 'Bragg')
self.assertEqual(product.description, 'Tangerines')
self.assertIsNone(product.size)
self.assertIsNone(product.case_size)
self.assertIsNone(product.unit_cost)
self.assertIsNone(product.unit_price_reg)
self.assertIsNone(row.product_scancode)
self.assertIsNone(row.product_brand)
self.assertEqual(row.product_description, 'Tangerines')
self.assertIsNone(row.product_size)
self.assertIsNone(row.case_size)
self.assertIsNone(row.unit_cost)
self.assertIsNone(row.unit_price_reg)
self.assertIsNone(row.unit_price_quoted)
self.assertIsNone(row.case_price_quoted)
self.assertIsNone(row.total_price)
# error if unknown products not allowed
self.config.setdefault('sideshow.orders.allow_unknown_products', 'false')
self.assertRaises(TypeError, handler.add_item, batch, kw, 1, enum.ORDER_UOM_UNIT)
# local product
local = model.LocalProduct(scancode='07430500002',
description='Vinegar',
size='2oz',
unit_price_reg=2.99,
case_size=12)
self.session.add(local)
self.session.flush()
row = handler.add_item(batch, local.uuid.hex, 1, enum.ORDER_UOM_CASE)
self.session.flush()
self.session.refresh(row)
self.session.refresh(local)
self.assertIsNone(row.product_id)
self.assertIsNone(row.pending_product)
product = row.local_product
self.assertIsInstance(product, model.LocalProduct)
self.assertEqual(product.scancode, '07430500002')
self.assertIsNone(product.brand_name)
self.assertEqual(product.description, 'Vinegar') self.assertEqual(product.description, 'Vinegar')
self.assertEqual(product.size, '2oz') self.assertEqual(product.size, '32oz')
self.assertEqual(product.case_size, 12) self.assertEqual(product.case_size, 12)
self.assertIsNone(product.unit_cost) self.assertEqual(product.unit_cost, decimal.Decimal('3.99'))
self.assertEqual(product.unit_price_reg, decimal.Decimal('2.99')) self.assertEqual(product.unit_price_reg, decimal.Decimal('5.99'))
self.assertEqual(row.product_scancode, '07430500002') self.assertIs(product.created_by, user)
self.assertIsNone(row.product_brand)
self.assertEqual(row.product_description, 'Vinegar')
self.assertEqual(row.product_size, '2oz')
self.assertEqual(row.case_size, 12)
self.assertIsNone(row.unit_cost)
self.assertEqual(row.unit_price_reg, decimal.Decimal('2.99'))
self.assertEqual(row.unit_price_quoted, decimal.Decimal('2.99'))
self.assertEqual(row.case_price_quoted, decimal.Decimal('35.88'))
self.assertEqual(row.total_price, decimal.Decimal('35.88'))
# local product, not found def test_set_pending_product(self):
mock_uuid = self.app.make_true_uuid()
self.assertRaises(ValueError, handler.add_item,
batch, mock_uuid.hex, 1, enum.ORDER_UOM_CASE)
# external lookup not implemented
self.config.setdefault('sideshow.orders.use_local_products', 'false')
self.assertRaises(NotImplementedError, handler.add_item,
batch, '42', 1, enum.ORDER_UOM_CASE)
def test_update_item(self):
model = self.app.model model = self.app.model
enum = self.app.enum enum = self.app.enum
handler = self.make_handler() handler = self.make_handler()
@ -278,170 +131,84 @@ class TestNewOrderBatchHandler(DataTestCase):
self.session.add(batch) self.session.add(batch)
self.assertEqual(len(batch.rows), 0) self.assertEqual(len(batch.rows), 0)
# start with typical pending product # start with mock product_id
kw = dict( row = handler.make_row(product_id=42, order_qty=1, order_uom=enum.ORDER_UOM_UNIT)
scancode='07430500001', handler.add_row(batch, row)
brand_name='Bragg', self.session.flush()
description='Vinegar', self.assertEqual(row.product_id, 42)
size='1oz', self.assertIsNone(row.pending_product)
case_size=12,
unit_cost=decimal.Decimal('1.99'),
unit_price_reg=decimal.Decimal('2.99'),
)
row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_CASE)
self.assertIsNone(row.product_id)
self.assertIsNone(row.local_product)
product = row.pending_product
self.assertIsInstance(product, model.PendingProduct)
self.assertEqual(product.scancode, '07430500001')
self.assertEqual(product.brand_name, 'Bragg')
self.assertEqual(product.description, 'Vinegar')
self.assertEqual(product.size, '1oz')
self.assertEqual(product.case_size, 12)
self.assertEqual(product.unit_cost, decimal.Decimal('1.99'))
self.assertEqual(product.unit_price_reg, decimal.Decimal('2.99'))
self.assertEqual(row.product_scancode, '07430500001')
self.assertEqual(row.product_brand, 'Bragg')
self.assertEqual(row.product_description, 'Vinegar')
self.assertEqual(row.product_size, '1oz')
self.assertEqual(row.case_size, 12)
self.assertEqual(row.unit_cost, decimal.Decimal('1.99'))
self.assertEqual(row.unit_price_reg, decimal.Decimal('2.99'))
self.assertEqual(row.unit_price_quoted, decimal.Decimal('2.99'))
self.assertEqual(row.case_price_quoted, decimal.Decimal('35.88'))
self.assertEqual(row.order_qty, 1)
self.assertEqual(row.order_uom, enum.ORDER_UOM_CASE)
self.assertEqual(row.total_price, decimal.Decimal('35.88'))
# set pending, minimal
handler.update_item(row, {'description': 'Vinegar'}, 1, enum.ORDER_UOM_UNIT)
# self.session.flush()
self.assertIsNone(row.product_id)
self.assertIsNone(row.local_product)
product = row.pending_product
self.assertIsInstance(product, model.PendingProduct)
self.assertIsNone(product.scancode)
self.assertIsNone(product.brand_name)
self.assertEqual(product.description, 'Vinegar')
self.assertIsNone(product.size)
self.assertIsNone(product.case_size)
self.assertIsNone(product.unit_cost)
self.assertIsNone(product.unit_price_reg)
self.assertIsNone(row.product_scancode) self.assertIsNone(row.product_scancode)
self.assertIsNone(row.product_brand) self.assertIsNone(row.product_brand)
self.assertEqual(row.product_description, 'Vinegar') self.assertIsNone(row.product_description)
self.assertIsNone(row.product_size) self.assertIsNone(row.product_size)
self.assertIsNone(row.case_size) self.assertIsNone(row.case_size)
self.assertIsNone(row.unit_cost) self.assertIsNone(row.unit_cost)
self.assertIsNone(row.unit_price_reg) self.assertIsNone(row.unit_price_reg)
self.assertIsNone(row.unit_price_quoted) self.assertIsNone(row.unit_price_quoted)
self.assertIsNone(row.case_price_quoted)
self.assertEqual(row.order_qty, 1)
self.assertEqual(row.order_uom, enum.ORDER_UOM_UNIT)
self.assertIsNone(row.total_price)
# start over, new row w/ local product # set pending, which clears product_id
local = model.LocalProduct(scancode='07430500002', handler.set_pending_product(row, dict(
description='Vinegar', scancode='07430500132',
size='2oz', brand_name='Bragg',
unit_price_reg=3.99, description='Vinegar',
case_size=12) size='32oz',
self.session.add(local) case_size=12,
unit_cost=decimal.Decimal('3.99'),
unit_price_reg=decimal.Decimal('5.99'),
created_by=user,
))
self.session.flush() self.session.flush()
row = handler.add_item(batch, local.uuid.hex, 1, enum.ORDER_UOM_CASE)
self.session.flush()
self.session.refresh(row)
self.session.refresh(local)
self.assertIsNone(row.product_id) self.assertIsNone(row.product_id)
self.assertIsNone(row.pending_product) self.assertIsInstance(row.pending_product, model.PendingProduct)
product = row.local_product self.assertEqual(row.product_scancode, '07430500132')
self.assertIsInstance(product, model.LocalProduct)
self.assertEqual(product.scancode, '07430500002')
self.assertIsNone(product.brand_name)
self.assertEqual(product.description, 'Vinegar')
self.assertEqual(product.size, '2oz')
self.assertEqual(product.case_size, 12)
self.assertIsNone(product.unit_cost)
self.assertEqual(product.unit_price_reg, decimal.Decimal('3.99'))
self.assertEqual(row.product_scancode, '07430500002')
self.assertIsNone(row.product_brand)
self.assertEqual(row.product_description, 'Vinegar')
self.assertEqual(row.product_size, '2oz')
self.assertEqual(row.case_size, 12)
self.assertIsNone(row.unit_cost)
self.assertEqual(row.unit_price_reg, decimal.Decimal('3.99'))
self.assertEqual(row.unit_price_quoted, decimal.Decimal('3.99'))
self.assertEqual(row.case_price_quoted, decimal.Decimal('47.88'))
self.assertEqual(row.order_qty, 1)
self.assertEqual(row.order_uom, enum.ORDER_UOM_CASE)
self.assertEqual(row.total_price, decimal.Decimal('47.88'))
# update w/ pending product
handler.update_item(row, kw, 2, enum.ORDER_UOM_CASE)
self.assertIsNone(row.product_id)
self.assertIsNone(row.local_product)
product = row.pending_product
self.assertIsInstance(product, model.PendingProduct)
self.assertEqual(product.scancode, '07430500001')
self.assertEqual(product.brand_name, 'Bragg')
self.assertEqual(product.description, 'Vinegar')
self.assertEqual(product.size, '1oz')
self.assertEqual(product.case_size, 12)
self.assertEqual(product.unit_cost, decimal.Decimal('1.99'))
self.assertEqual(product.unit_price_reg, decimal.Decimal('2.99'))
self.assertEqual(row.product_scancode, '07430500001')
self.assertEqual(row.product_brand, 'Bragg') self.assertEqual(row.product_brand, 'Bragg')
self.assertEqual(row.product_description, 'Vinegar') self.assertEqual(row.product_description, 'Vinegar')
self.assertEqual(row.product_size, '1oz') self.assertEqual(row.product_size, '32oz')
self.assertEqual(row.case_size, 12) self.assertEqual(row.case_size, 12)
self.assertEqual(row.unit_cost, decimal.Decimal('1.99')) self.assertEqual(row.unit_cost, decimal.Decimal('3.99'))
self.assertEqual(row.unit_price_reg, decimal.Decimal('2.99')) self.assertEqual(row.unit_price_reg, decimal.Decimal('5.99'))
self.assertEqual(row.unit_price_quoted, decimal.Decimal('2.99')) self.assertEqual(row.unit_price_quoted, decimal.Decimal('5.99'))
self.assertEqual(row.case_price_quoted, decimal.Decimal('35.88')) self.assertEqual(row.case_price_quoted, decimal.Decimal('71.88'))
self.assertEqual(row.order_qty, 2) product = row.pending_product
self.assertEqual(row.order_uom, enum.ORDER_UOM_CASE) self.assertIsInstance(product, model.PendingProduct)
self.assertEqual(row.total_price, decimal.Decimal('71.76')) self.assertEqual(product.scancode, '07430500132')
self.assertEqual(product.brand_name, 'Bragg')
# update w/ pending, error if not allowed
self.config.setdefault('sideshow.orders.allow_unknown_products', 'false')
self.assertRaises(TypeError, handler.update_item, row, kw, 1, enum.ORDER_UOM_UNIT)
# update w/ local product
handler.update_item(row, local.uuid.hex, 1, enum.ORDER_UOM_CASE)
self.assertIsNone(row.product_id)
# nb. pending remains intact here
self.assertIsNotNone(row.pending_product)
product = row.local_product
self.assertIsInstance(product, model.LocalProduct)
self.assertEqual(product.scancode, '07430500002')
self.assertIsNone(product.brand_name)
self.assertEqual(product.description, 'Vinegar') self.assertEqual(product.description, 'Vinegar')
self.assertEqual(product.size, '2oz') self.assertEqual(product.size, '32oz')
self.assertEqual(product.case_size, 12) self.assertEqual(product.case_size, 12)
self.assertIsNone(product.unit_cost) self.assertEqual(product.unit_cost, decimal.Decimal('3.99'))
self.assertEqual(product.unit_price_reg, decimal.Decimal('3.99')) self.assertEqual(product.unit_price_reg, decimal.Decimal('5.99'))
self.assertEqual(row.product_scancode, '07430500002') self.assertIs(product.created_by, user)
self.assertIsNone(row.product_brand)
# set again to update pending
handler.set_pending_product(row, dict(
scancode='07430500116',
size='16oz',
unit_cost=decimal.Decimal('2.19'),
unit_price_reg=decimal.Decimal('3.59'),
))
self.session.flush()
self.assertIsNone(row.product_id)
self.assertIsInstance(row.pending_product, model.PendingProduct)
self.assertEqual(row.product_scancode, '07430500116')
self.assertEqual(row.product_brand, 'Bragg')
self.assertEqual(row.product_description, 'Vinegar') self.assertEqual(row.product_description, 'Vinegar')
self.assertEqual(row.product_size, '2oz') self.assertEqual(row.product_size, '16oz')
self.assertEqual(row.case_size, 12) self.assertEqual(row.case_size, 12)
self.assertIsNone(row.unit_cost) self.assertEqual(row.unit_cost, decimal.Decimal('2.19'))
self.assertEqual(row.unit_price_reg, decimal.Decimal('3.99')) self.assertEqual(row.unit_price_reg, decimal.Decimal('3.59'))
self.assertEqual(row.unit_price_quoted, decimal.Decimal('3.99')) self.assertEqual(row.unit_price_quoted, decimal.Decimal('3.59'))
self.assertEqual(row.case_price_quoted, decimal.Decimal('47.88')) self.assertEqual(row.case_price_quoted, decimal.Decimal('43.08'))
self.assertEqual(row.order_qty, 1) product = row.pending_product
self.assertEqual(row.order_uom, enum.ORDER_UOM_CASE) self.assertIsInstance(product, model.PendingProduct)
self.assertEqual(row.total_price, decimal.Decimal('47.88')) self.assertEqual(product.scancode, '07430500116')
self.assertEqual(product.brand_name, 'Bragg')
# update w/ local, not found self.assertEqual(product.description, 'Vinegar')
mock_uuid = self.app.make_true_uuid() self.assertEqual(product.size, '16oz')
self.assertRaises(ValueError, handler.update_item, self.assertEqual(product.case_size, 12)
batch, mock_uuid.hex, 1, enum.ORDER_UOM_CASE) self.assertEqual(product.unit_cost, decimal.Decimal('2.19'))
self.assertEqual(product.unit_price_reg, decimal.Decimal('3.59'))
# external lookup not implemented self.assertIs(product.created_by, user)
self.config.setdefault('sideshow.orders.use_local_products', 'false')
self.assertRaises(NotImplementedError, handler.update_item,
row, '42', 1, enum.ORDER_UOM_CASE)
def test_refresh_row(self): def test_refresh_row(self):
model = self.app.model model = self.app.model
@ -543,37 +310,6 @@ class TestNewOrderBatchHandler(DataTestCase):
self.assertEqual(row.case_price_quoted, decimal.Decimal('71.88')) self.assertEqual(row.case_price_quoted, decimal.Decimal('71.88'))
self.assertEqual(row.total_price, decimal.Decimal('143.76')) self.assertEqual(row.total_price, decimal.Decimal('143.76'))
# refreshed from pending product (sale price)
product = model.PendingProduct(scancode='07430500132',
brand_name='Bragg',
description='Vinegar',
size='32oz',
case_size=12,
unit_cost=decimal.Decimal('3.99'),
unit_price_reg=decimal.Decimal('5.99'),
created_by=user,
status=enum.PendingProductStatus.PENDING)
row = handler.make_row(pending_product=product, order_qty=2, order_uom=enum.ORDER_UOM_CASE,
unit_price_sale=decimal.Decimal('5.19'),
sale_ends=datetime.datetime(2099, 1, 1))
self.assertIsNone(row.status_code)
handler.add_row(batch, row)
self.assertEqual(row.status_code, row.STATUS_OK)
self.assertIsNone(row.product_id)
self.assertIs(row.pending_product, product)
self.assertEqual(row.product_scancode, '07430500132')
self.assertEqual(row.product_brand, 'Bragg')
self.assertEqual(row.product_description, 'Vinegar')
self.assertEqual(row.product_size, '32oz')
self.assertEqual(row.case_size, 12)
self.assertEqual(row.unit_cost, decimal.Decimal('3.99'))
self.assertEqual(row.unit_price_reg, decimal.Decimal('5.99'))
self.assertEqual(row.unit_price_sale, decimal.Decimal('5.19'))
self.assertEqual(row.sale_ends, datetime.datetime(2099, 1, 1))
self.assertEqual(row.unit_price_quoted, decimal.Decimal('5.19'))
self.assertEqual(row.case_price_quoted, decimal.Decimal('62.28'))
self.assertEqual(row.total_price, decimal.Decimal('124.56'))
def test_remove_row(self): def test_remove_row(self):
model = self.app.model model = self.app.model
enum = self.app.enum enum = self.app.enum
@ -596,7 +332,7 @@ class TestNewOrderBatchHandler(DataTestCase):
unit_price_reg=decimal.Decimal('5.99'), unit_price_reg=decimal.Decimal('5.99'),
created_by=user, created_by=user,
) )
row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_CASE) row = handler.add_pending_product(batch, kw, 1, enum.ORDER_UOM_CASE)
self.session.add(row) self.session.add(row)
self.session.flush() self.session.flush()
self.assertEqual(batch.row_count, 1) self.assertEqual(batch.row_count, 1)
@ -632,70 +368,25 @@ class TestNewOrderBatchHandler(DataTestCase):
self.assertNotIn(batch, self.session) self.assertNotIn(batch, self.session)
self.assertEqual(self.session.query(model.PendingCustomer).count(), 0) self.assertEqual(self.session.query(model.PendingCustomer).count(), 0)
# make new pending customer, assigned to batch + order # make new pending customer
customer = model.PendingCustomer(full_name="Wilma Flintstone", customer = model.PendingCustomer(full_name="Fred Flintstone",
status=enum.PendingCustomerStatus.PENDING, status=enum.PendingCustomerStatus.PENDING,
created_by=user) created_by=user)
self.session.add(customer) self.session.add(customer)
batch = handler.make_batch(self.session, created_by=user, pending_customer=customer)
self.session.add(batch)
order = model.Order(order_id=77, created_by=user, pending_customer=customer)
self.session.add(order)
self.session.flush()
# deleting batch will *not* delete pending customer # make 2 batches with same pending customer
self.assertIn(batch, self.session) batch1 = handler.make_batch(self.session, created_by=user, pending_customer=customer)
self.assertEqual(self.session.query(model.PendingCustomer).count(), 1) batch2 = handler.make_batch(self.session, created_by=user, pending_customer=customer)
handler.do_delete(batch, user) self.session.add(batch1)
self.session.add(batch2)
self.session.commit() self.session.commit()
self.assertNotIn(batch, self.session)
# deleting 1 will not delete pending customer
self.assertEqual(self.session.query(model.PendingCustomer).count(), 1) self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
handler.do_delete(batch1, user)
# make new pending product, associate w/ batch + order
batch = handler.make_batch(self.session, created_by=user)
self.session.add(batch)
handler.set_customer(batch, {'full_name': "Jack Black"})
row = handler.add_item(batch, dict(
scancode='07430500132',
brand_name='Bragg',
description='Vinegar',
size='32oz',
unit_price_reg=5.99,
), 1, enum.ORDER_UOM_UNIT)
product = row.pending_product
order = model.Order(order_id=33, created_by=user)
item = model.OrderItem(pending_product=product, order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
status_code=enum.ORDER_ITEM_STATUS_INITIATED)
order.items.append(item)
self.session.add(order)
self.session.flush()
# deleting batch will *not* delete pending product
self.assertIn(batch, self.session)
self.assertEqual(self.session.query(model.PendingProduct).count(), 1)
handler.do_delete(batch, user)
self.session.commit() self.session.commit()
self.assertNotIn(batch, self.session)
self.assertEqual(self.session.query(model.PendingCustomer).count(), 1) self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
self.assertIs(batch2.pending_customer, customer)
# make another batch w/ same pending product
batch = handler.make_batch(self.session, created_by=user)
self.session.add(batch)
row = handler.make_row(pending_product=product,
order_qty=1, order_uom=enum.ORDER_UOM_UNIT)
handler.add_row(batch, row)
# also delete the associated order
self.session.delete(order)
self.session.flush()
# deleting this batch *will* delete pending product
self.assertIn(batch, self.session)
self.assertEqual(self.session.query(model.PendingProduct).count(), 1)
handler.do_delete(batch, user)
self.session.commit()
self.assertNotIn(batch, self.session)
self.assertEqual(self.session.query(model.PendingProduct).count(), 0)
def test_get_effective_rows(self): def test_get_effective_rows(self):
model = self.app.model model = self.app.model
@ -746,12 +437,6 @@ class TestNewOrderBatchHandler(DataTestCase):
self.assertEqual(reason, "Must assign the customer") self.assertEqual(reason, "Must assign the customer")
batch.customer_id = 42 batch.customer_id = 42
batch.customer_name = "Fred Flintstone"
reason = handler.why_not_execute(batch)
self.assertEqual(reason, "Customer phone number is required")
batch.phone_number = '555-1234'
reason = handler.why_not_execute(batch) reason = handler.why_not_execute(batch)
self.assertEqual(reason, "Must add at least one valid item") self.assertEqual(reason, "Must add at least one valid item")
@ -766,206 +451,13 @@ class TestNewOrderBatchHandler(DataTestCase):
unit_price_reg=decimal.Decimal('5.99'), unit_price_reg=decimal.Decimal('5.99'),
created_by=user, created_by=user,
) )
row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_CASE) row = handler.add_pending_product(batch, kw, 1, enum.ORDER_UOM_CASE)
self.session.add(row) self.session.add(row)
self.session.flush() self.session.flush()
reason = handler.why_not_execute(batch) reason = handler.why_not_execute(batch)
self.assertIsNone(reason) self.assertIsNone(reason)
def test_make_local_customer(self):
model = self.app.model
enum = self.app.enum
handler = self.make_handler()
user = model.User(username='barney')
self.session.add(user)
# make a typical batch
batch = handler.make_batch(self.session, created_by=user)
self.session.add(batch)
handler.set_customer(batch, {'first_name': "John", 'last_name': "Doe",
'phone_number': '555-1234'})
row = handler.add_item(batch, dict(
scancode='07430500132',
brand_name='Bragg',
description='Vinegar',
size='32oz',
unit_price_reg=5.99,
), 1, enum.ORDER_UOM_UNIT)
self.session.flush()
# making local customer removes pending customer
self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
self.assertEqual(self.session.query(model.LocalCustomer).count(), 0)
self.assertIsNotNone(batch.pending_customer)
self.assertIsNone(batch.local_customer)
handler.make_local_customer(batch)
self.assertEqual(self.session.query(model.PendingCustomer).count(), 0)
self.assertEqual(self.session.query(model.LocalCustomer).count(), 1)
self.assertIsNone(batch.pending_customer)
local = batch.local_customer
self.assertIsNotNone(local)
self.assertEqual(local.first_name, "John")
self.assertEqual(local.last_name, "Doe")
self.assertEqual(local.full_name, "John Doe")
self.assertEqual(local.phone_number, '555-1234')
# trying again does nothing
handler.make_local_customer(batch)
self.assertEqual(self.session.query(model.PendingCustomer).count(), 0)
self.assertEqual(self.session.query(model.LocalCustomer).count(), 1)
self.assertIsNone(batch.pending_customer)
local = batch.local_customer
self.assertIsNotNone(local)
self.assertEqual(local.first_name, "John")
self.assertEqual(local.last_name, "Doe")
self.assertEqual(local.full_name, "John Doe")
self.assertEqual(local.phone_number, '555-1234')
# make another typical batch
batch = handler.make_batch(self.session, created_by=user)
self.session.add(batch)
handler.set_customer(batch, {'first_name': "Chuck", 'last_name': "Norris",
'phone_number': '555-1234'})
row = handler.add_item(batch, dict(
scancode='07430500132',
brand_name='Bragg',
description='Vinegar',
size='32oz',
unit_price_reg=5.99,
), 1, enum.ORDER_UOM_UNIT)
self.session.flush()
# should do nothing if local customers disabled
with patch.object(handler, 'use_local_customers', return_value=False):
self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
self.assertEqual(self.session.query(model.LocalCustomer).count(), 1)
self.assertIsNotNone(batch.pending_customer)
self.assertIsNone(batch.local_customer)
handler.make_local_customer(batch)
self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
self.assertEqual(self.session.query(model.LocalCustomer).count(), 1)
self.assertIsNotNone(batch.pending_customer)
self.assertIsNone(batch.local_customer)
# but things happen by default, since local customers enabled
self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
self.assertEqual(self.session.query(model.LocalCustomer).count(), 1)
self.assertIsNotNone(batch.pending_customer)
self.assertIsNone(batch.local_customer)
handler.make_local_customer(batch)
self.assertEqual(self.session.query(model.PendingCustomer).count(), 0)
self.assertEqual(self.session.query(model.LocalCustomer).count(), 2)
self.assertIsNone(batch.pending_customer)
local = batch.local_customer
self.assertIsNotNone(local)
self.assertEqual(local.first_name, "Chuck")
self.assertEqual(local.last_name, "Norris")
self.assertEqual(local.full_name, "Chuck Norris")
self.assertEqual(local.phone_number, '555-1234')
def test_make_local_products(self):
model = self.app.model
enum = self.app.enum
handler = self.make_handler()
user = model.User(username='barney')
self.session.add(user)
# make a batch w/ one each local + pending products
batch = handler.make_batch(self.session, created_by=user)
self.session.add(batch)
handler.set_customer(batch, {'full_name': "John Doe"})
local = model.LocalProduct(scancode='07430500116',
brand_name='Bragg',
description='Vinegar',
size='16oz',
unit_price_reg=3.59)
self.session.add(local)
self.session.flush()
row1 = handler.add_item(batch, local.uuid.hex, 1, enum.ORDER_UOM_UNIT)
row2 = handler.add_item(batch, dict(
scancode='07430500132',
brand_name='Bragg',
description='Vinegar',
size='32oz',
unit_price_reg=5.99,
), 1, enum.ORDER_UOM_UNIT)
self.session.flush()
# making local product removes pending product
self.assertEqual(self.session.query(model.PendingProduct).count(), 1)
self.assertEqual(self.session.query(model.LocalProduct).count(), 1)
self.assertIsNotNone(row2.pending_product)
self.assertIsNone(row2.local_product)
handler.make_local_products(batch, batch.rows)
self.assertEqual(self.session.query(model.PendingProduct).count(), 0)
self.assertEqual(self.session.query(model.LocalProduct).count(), 2)
self.assertIsNone(row2.pending_product)
local = row2.local_product
self.assertIsNotNone(local)
self.assertEqual(local.scancode, '07430500132')
self.assertEqual(local.brand_name, 'Bragg')
self.assertEqual(local.description, 'Vinegar')
self.assertEqual(local.size, '32oz')
self.assertEqual(local.unit_price_reg, decimal.Decimal('5.99'))
# trying again does nothing
handler.make_local_products(batch, batch.rows)
self.assertEqual(self.session.query(model.PendingProduct).count(), 0)
self.assertEqual(self.session.query(model.LocalProduct).count(), 2)
self.assertIsNone(row2.pending_product)
local = row2.local_product
self.assertIsNotNone(local)
self.assertEqual(local.scancode, '07430500132')
self.assertEqual(local.brand_name, 'Bragg')
self.assertEqual(local.description, 'Vinegar')
self.assertEqual(local.size, '32oz')
self.assertEqual(local.unit_price_reg, decimal.Decimal('5.99'))
# make another typical batch
batch = handler.make_batch(self.session, created_by=user)
self.session.add(batch)
handler.set_customer(batch, {'full_name': "Chuck Norris"})
row = handler.add_item(batch, dict(
scancode='07430500164',
brand_name='Bragg',
description='Vinegar',
size='64oz',
unit_price_reg=9.99,
), 1, enum.ORDER_UOM_UNIT)
self.session.flush()
# should do nothing if local products disabled
with patch.object(handler, 'use_local_products', return_value=False):
self.assertEqual(self.session.query(model.PendingProduct).count(), 1)
self.assertEqual(self.session.query(model.LocalProduct).count(), 2)
self.assertIsNotNone(row.pending_product)
self.assertIsNone(row.local_product)
handler.make_local_products(batch, batch.rows)
self.assertEqual(self.session.query(model.PendingProduct).count(), 1)
self.assertEqual(self.session.query(model.LocalProduct).count(), 2)
self.assertIsNotNone(row.pending_product)
self.assertIsNone(row.local_product)
# but things happen by default, since local products enabled
self.assertEqual(self.session.query(model.PendingProduct).count(), 1)
self.assertEqual(self.session.query(model.LocalProduct).count(), 2)
self.assertIsNotNone(row.pending_product)
self.assertIsNone(row.local_product)
handler.make_local_products(batch, batch.rows)
self.assertEqual(self.session.query(model.PendingProduct).count(), 0)
self.assertEqual(self.session.query(model.LocalProduct).count(), 3)
self.assertIsNone(row.pending_product)
local = row.local_product
self.assertIsNotNone(local)
self.assertEqual(local.scancode, '07430500164')
self.assertEqual(local.brand_name, 'Bragg')
self.assertEqual(local.description, 'Vinegar')
self.assertEqual(local.size, '64oz')
self.assertEqual(local.unit_price_reg, decimal.Decimal('9.99'))
def test_make_new_order(self): def test_make_new_order(self):
model = self.app.model model = self.app.model
enum = self.app.enum enum = self.app.enum
@ -987,7 +479,7 @@ class TestNewOrderBatchHandler(DataTestCase):
unit_price_reg=decimal.Decimal('5.99'), unit_price_reg=decimal.Decimal('5.99'),
created_by=user, created_by=user,
) )
row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_CASE) row = handler.add_pending_product(batch, kw, 1, enum.ORDER_UOM_CASE)
self.session.add(row) self.session.add(row)
self.session.flush() self.session.flush()
@ -1027,7 +519,7 @@ class TestNewOrderBatchHandler(DataTestCase):
unit_price_reg=decimal.Decimal('5.99'), unit_price_reg=decimal.Decimal('5.99'),
created_by=user, created_by=user,
) )
row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_CASE) row = handler.add_pending_product(batch, kw, 1, enum.ORDER_UOM_CASE)
self.session.add(row) self.session.add(row)
self.session.flush() self.session.flush()

View file

@ -19,19 +19,6 @@ class TestOrder(DataTestCase):
class TestOrderItem(DataTestCase): class TestOrderItem(DataTestCase):
def test_full_description(self):
item = mod.OrderItem()
self.assertEqual(item.full_description, "")
item = mod.OrderItem(product_description="Vinegar")
self.assertEqual(item.full_description, "Vinegar")
item = mod.OrderItem(product_brand='Bragg',
product_description='Vinegar',
product_size='32oz')
self.assertEqual(item.full_description, "Bragg Vinegar 32oz")
def test_str(self): def test_str(self):
item = mod.OrderItem() item = mod.OrderItem()
@ -40,7 +27,8 @@ class TestOrderItem(DataTestCase):
item = mod.OrderItem(product_description="Vinegar") item = mod.OrderItem(product_description="Vinegar")
self.assertEqual(str(item), "Vinegar") self.assertEqual(str(item), "Vinegar")
item = mod.OrderItem(product_brand='Bragg', product = PendingProduct(brand_name="Bragg",
product_description='Vinegar', description="Vinegar",
product_size='32oz') size="32oz")
item = mod.OrderItem(pending_product=product)
self.assertEqual(str(item), "Bragg Vinegar 32oz") self.assertEqual(str(item), "Bragg Vinegar 32oz")

View file

@ -32,31 +32,6 @@ class TestOrderRef(WebTestCase):
self.assertIn(f'/orders/{order.uuid}', url) self.assertIn(f'/orders/{order.uuid}', url)
class TestLocalCustomerRef(WebTestCase):
def test_sort_query(self):
typ = mod.LocalCustomerRef(self.request, session=self.session)
query = typ.get_query()
self.assertIsInstance(query, orm.Query)
sorted_query = typ.sort_query(query)
self.assertIsInstance(sorted_query, orm.Query)
self.assertIsNot(sorted_query, query)
def test_get_object_url(self):
self.pyramid_config.add_route('local_customers.view', '/local/customers/{uuid}')
model = self.app.model
enum = self.app.enum
customer = model.LocalCustomer()
self.session.add(customer)
self.session.commit()
typ = mod.LocalCustomerRef(self.request, session=self.session)
url = typ.get_object_url(customer)
self.assertIsNotNone(url)
self.assertIn(f'/local/customers/{customer.uuid}', url)
class TestPendingCustomerRef(WebTestCase): class TestPendingCustomerRef(WebTestCase):
def test_sort_query(self): def test_sort_query(self):
@ -85,31 +60,6 @@ class TestPendingCustomerRef(WebTestCase):
self.assertIn(f'/pending/customers/{customer.uuid}', url) self.assertIn(f'/pending/customers/{customer.uuid}', url)
class TestLocalProductRef(WebTestCase):
def test_sort_query(self):
typ = mod.LocalProductRef(self.request, session=self.session)
query = typ.get_query()
self.assertIsInstance(query, orm.Query)
sorted_query = typ.sort_query(query)
self.assertIsInstance(sorted_query, orm.Query)
self.assertIsNot(sorted_query, query)
def test_get_object_url(self):
self.pyramid_config.add_route('local_products.view', '/local/products/{uuid}')
model = self.app.model
enum = self.app.enum
product = model.LocalProduct()
self.session.add(product)
self.session.commit()
typ = mod.LocalProductRef(self.request, session=self.session)
url = typ.get_object_url(product)
self.assertIsNotNone(url)
self.assertIn(f'/local/products/{product.uuid}', url)
class TestPendingProductRef(WebTestCase): class TestPendingProductRef(WebTestCase):
def test_sort_query(self): def test_sort_query(self):

View file

@ -9,4 +9,4 @@ class TestSideshowMenuHandler(WebTestCase):
def test_make_menus(self): def test_make_menus(self):
handler = mod.SideshowMenuHandler(self.config) handler = mod.SideshowMenuHandler(self.config)
menus = handler.make_menus(self.request) menus = handler.make_menus(self.request)
self.assertEqual(len(menus), 5) self.assertEqual(len(menus), 4)

View file

@ -16,114 +16,6 @@ class TestIncludeme(WebTestCase):
mod.includeme(self.pyramid_config) mod.includeme(self.pyramid_config)
class TestLocalCustomerView(WebTestCase):
def make_view(self):
return mod.LocalCustomerView(self.request)
def test_configure_grid(self):
model = self.app.model
view = self.make_view()
grid = view.make_grid(model_class=model.LocalCustomer)
self.assertNotIn('full_name', grid.linked_columns)
view.configure_grid(grid)
self.assertIn('full_name', grid.linked_columns)
def test_configure_form(self):
model = self.app.model
view = self.make_view()
# creating
with patch.object(view, 'creating', new=True):
form = view.make_form(model_class=model.LocalCustomer)
view.configure_form(form)
self.assertNotIn('external_id', form)
self.assertNotIn('full_name', form)
self.assertNotIn('orders', form)
self.assertNotIn('new_order_batches', form)
user = model.User(username='barney')
self.session.add(user)
customer = model.LocalCustomer()
self.session.add(customer)
self.session.commit()
# viewing
with patch.object(view, 'viewing', new=True):
form = view.make_form(model_instance=customer)
view.configure_form(form)
self.assertIn('external_id', form)
self.assertIn('full_name', form)
self.assertIn('orders', form)
self.assertIn('new_order_batches', form)
def test_make_orders_grid(self):
model = self.app.model
view = self.make_view()
user = model.User(username='barney')
self.session.add(user)
customer = model.LocalCustomer()
self.session.add(customer)
order = model.Order(order_id=42, local_customer=customer, created_by=user)
self.session.add(order)
self.session.commit()
# no view perm
grid = view.make_orders_grid(customer)
self.assertEqual(len(grid.actions), 0)
# with view perm
with patch.object(self.request, 'is_root', new=True):
grid = view.make_orders_grid(customer)
self.assertEqual(len(grid.actions), 1)
self.assertEqual(grid.actions[0].key, 'view')
def test_make_new_order_batches_grid(self):
model = self.app.model
handler = NewOrderBatchHandler(self.config)
view = self.make_view()
user = model.User(username='barney')
self.session.add(user)
customer = model.LocalCustomer()
self.session.add(customer)
batch = handler.make_batch(self.session, local_customer=customer, created_by=user)
self.session.add(batch)
self.session.commit()
# no view perm
grid = view.make_new_order_batches_grid(customer)
self.assertEqual(len(grid.actions), 0)
# with view perm
with patch.object(self.request, 'is_root', new=True):
grid = view.make_new_order_batches_grid(customer)
self.assertEqual(len(grid.actions), 1)
self.assertEqual(grid.actions[0].key, 'view')
def test_objectify(self):
model = self.app.model
view = self.make_view()
user = model.User(username='barney')
self.session.add(user)
self.session.commit()
with patch.object(view, 'creating', new=True):
with patch.object(self.request, 'user', new=user):
form = view.make_model_form()
with patch.object(form, 'validated', create=True, new={
'first_name': 'Chuck',
'last_name': 'Norris',
}):
customer = view.objectify(form)
self.assertIsInstance(customer, model.LocalCustomer)
self.assertEqual(customer.first_name, 'Chuck')
self.assertEqual(customer.last_name, 'Norris')
self.assertEqual(customer.full_name, 'Chuck Norris')
class TestPendingCustomerView(WebTestCase): class TestPendingCustomerView(WebTestCase):
def make_view(self): def make_view(self):

View file

@ -13,7 +13,7 @@ from wuttaweb.forms.schema import WuttaMoney
from sideshow.batch.neworder import NewOrderBatchHandler from sideshow.batch.neworder import NewOrderBatchHandler
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
class TestIncludeme(WebTestCase): class TestIncludeme(WebTestCase):
@ -27,9 +27,6 @@ class TestOrderView(WebTestCase):
def make_view(self): def make_view(self):
return mod.OrderView(self.request) return mod.OrderView(self.request)
def make_handler(self):
return NewOrderBatchHandler(self.config)
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()
@ -43,7 +40,6 @@ class TestOrderView(WebTestCase):
def test_create(self): def test_create(self):
self.pyramid_config.include('sideshow.web.views') self.pyramid_config.include('sideshow.web.views')
model = self.app.model model = self.app.model
enum = self.app.enum
view = self.make_view() view = self.make_view()
user = model.User(username='barney') user = model.User(username='barney')
@ -95,7 +91,7 @@ class TestOrderView(WebTestCase):
'customer_name': 'Fred Flintstone', 'customer_name': 'Fred Flintstone',
'phone_number': '555-1234', 'phone_number': '555-1234',
'email_address': 'fred@mailinator.com', 'email_address': 'fred@mailinator.com',
'new_customer_full_name': 'Fred Flintstone', 'new_customer_name': 'Fred Flintstone',
'new_customer_first_name': 'Fred', 'new_customer_first_name': 'Fred',
'new_customer_last_name': 'Flintstone', 'new_customer_last_name': 'Flintstone',
'new_customer_phone': '555-1234', 'new_customer_phone': '555-1234',
@ -112,40 +108,6 @@ class TestOrderView(WebTestCase):
self.assertEqual(response.content_type, 'application/json') self.assertEqual(response.content_type, 'application/json')
self.assertEqual(response.json_body, {'error': 'unknown form action'}) self.assertEqual(response.json_body, {'error': 'unknown form action'})
# add item
with patch.multiple(self.request, create=True,
method='POST',
json_body={'action': 'add_item',
'product_info': {
'scancode': '07430500132',
'description': 'Vinegar',
'unit_price_reg': 5.99,
},
'order_qty': 1,
'order_uom': enum.ORDER_UOM_UNIT}):
response = view.create()
self.assertIsInstance(response, Response)
self.assertEqual(response.content_type, 'application/json')
data = response.json_body
self.assertEqual(sorted(data), ['batch', 'row'])
# add item, w/ error
with patch.object(NewOrderBatchHandler, 'add_item', side_effect=RuntimeError):
with patch.multiple(self.request, create=True,
method='POST',
json_body={'action': 'add_item',
'product_info': {
'scancode': '07430500116',
'description': 'Vinegar',
'unit_price_reg': 3.59,
},
'order_qty': 1,
'order_uom': enum.ORDER_UOM_UNIT}):
response = view.create()
self.assertIsInstance(response, Response)
self.assertEqual(response.content_type, 'application/json')
self.assertEqual(response.json_body, {'error': 'RuntimeError'})
def test_get_current_batch(self): def test_get_current_batch(self):
model = self.app.model model = self.app.model
handler = NewOrderBatchHandler(self.config) handler = NewOrderBatchHandler(self.config)
@ -175,75 +137,6 @@ class TestOrderView(WebTestCase):
self.assertEqual(self.session.query(model.NewOrderBatch).count(), 1) self.assertEqual(self.session.query(model.NewOrderBatch).count(), 1)
self.assertIs(batch2, batch) self.assertIs(batch2, batch)
def test_customer_autocomplete(self):
model = self.app.model
view = self.make_view()
with patch.object(view, 'Session', return_value=self.session):
# empty results by default
self.assertEqual(view.customer_autocomplete(), [])
with patch.object(self.request, 'GET', new={'term': 'foo'}, create=True):
self.assertEqual(view.customer_autocomplete(), [])
# add a customer
customer = model.LocalCustomer(full_name="Chuck Norris")
self.session.add(customer)
self.session.flush()
# search for chuck finds chuck
with patch.object(self.request, 'GET', new={'term': 'chuck'}, create=True):
result = view.customer_autocomplete()
self.assertEqual(len(result), 1)
self.assertEqual(result[0], {
'value': customer.uuid.hex,
'label': "Chuck Norris",
})
# search for sally finds nothing
with patch.object(self.request, 'GET', new={'term': 'sally'}, create=True):
result = view.customer_autocomplete()
self.assertEqual(result, [])
def test_product_autocomplete(self):
model = self.app.model
view = self.make_view()
with patch.object(view, 'Session', return_value=self.session):
# empty results by default
self.assertEqual(view.product_autocomplete(), [])
with patch.object(self.request, 'GET', new={'term': 'foo'}, create=True):
self.assertEqual(view.product_autocomplete(), [])
# add a product
product = model.LocalProduct(brand_name="Bragg's", description="Vinegar")
self.session.add(product)
self.session.flush()
# search for vinegar finds product
with patch.object(self.request, 'GET', new={'term': 'vinegar'}, create=True):
result = view.product_autocomplete()
self.assertEqual(len(result), 1)
self.assertEqual(result[0], {
'value': product.uuid.hex,
'label': "Bragg's Vinegar",
})
# search for brag finds product
with patch.object(self.request, 'GET', new={'term': 'brag'}, create=True):
result = view.product_autocomplete()
self.assertEqual(len(result), 1)
self.assertEqual(result[0], {
'value': product.uuid.hex,
'label': "Bragg's Vinegar",
})
# search for juice finds nothing
with patch.object(self.request, 'GET', new={'term': 'juice'}, create=True):
result = view.product_autocomplete()
self.assertEqual(result, [])
def test_get_pending_product_required_fields(self): def test_get_pending_product_required_fields(self):
model = self.app.model model = self.app.model
view = self.make_view() view = self.make_view()
@ -265,51 +158,38 @@ class TestOrderView(WebTestCase):
model = self.app.model model = self.app.model
handler = NewOrderBatchHandler(self.config) handler = NewOrderBatchHandler(self.config)
view = self.make_view() view = self.make_view()
view.batch_handler = handler
user = model.User(username='barney') user = model.User(username='barney')
self.session.add(user) self.session.add(user)
# with external customer # with true customer
with patch.object(handler, 'use_local_customers', return_value=False):
batch = handler.make_batch(self.session, created_by=user,
customer_id=42, customer_name='Fred Flintstone',
phone_number='555-1234', email_address='fred@mailinator.com')
self.session.add(batch)
self.session.flush()
context = view.get_context_customer(batch)
self.assertEqual(context, {
'customer_is_known': True,
'customer_id': 42,
'customer_name': 'Fred Flintstone',
'phone_number': '555-1234',
'email_address': 'fred@mailinator.com',
})
# with local customer
local = model.LocalCustomer(full_name="Betty Boop")
self.session.add(local)
batch = handler.make_batch(self.session, created_by=user, batch = handler.make_batch(self.session, created_by=user,
local_customer=local, customer_name='Betty Boop', customer_id=42, customer_name='Fred Flintstone',
phone_number='555-8888') phone_number='555-1234', email_address='fred@mailinator.com')
self.session.add(batch) self.session.add(batch)
self.session.flush() self.session.flush()
context = view.get_context_customer(batch) context = view.get_context_customer(batch)
self.assertEqual(context, { self.assertEqual(context, {
'customer_is_known': True, 'customer_is_known': True,
'customer_id': local.uuid.hex, 'customer_id': 42,
'customer_name': 'Betty Boop', 'customer_name': 'Fred Flintstone',
'phone_number': '555-8888', 'phone_number': '555-1234',
'email_address': None, 'email_address': 'fred@mailinator.com',
'new_customer_name': None,
'new_customer_first_name': None,
'new_customer_last_name': None,
'new_customer_phone': None,
'new_customer_email': None,
}) })
# with pending customer # with pending customer
batch = handler.make_batch(self.session, created_by=user) batch = handler.make_batch(self.session, created_by=user)
self.session.add(batch) self.session.add(batch)
handler.set_customer(batch, dict( handler.set_pending_customer(batch, dict(
full_name="Fred Flintstone", full_name="Fred Flintstone",
first_name="Fred", last_name="Flintstone", first_name="Fred", last_name="Flintstone",
phone_number='555-1234', email_address='fred@mailinator.com', phone_number='555-1234', email_address='fred@mailinator.com',
created_by=user,
)) ))
self.session.flush() self.session.flush()
context = view.get_context_customer(batch) context = view.get_context_customer(batch)
@ -319,7 +199,7 @@ class TestOrderView(WebTestCase):
'customer_name': 'Fred Flintstone', 'customer_name': 'Fred Flintstone',
'phone_number': '555-1234', 'phone_number': '555-1234',
'email_address': 'fred@mailinator.com', 'email_address': 'fred@mailinator.com',
'new_customer_full_name': 'Fred Flintstone', 'new_customer_name': 'Fred Flintstone',
'new_customer_first_name': 'Fred', 'new_customer_first_name': 'Fred',
'new_customer_last_name': 'Flintstone', 'new_customer_last_name': 'Flintstone',
'new_customer_phone': '555-1234', 'new_customer_phone': '555-1234',
@ -337,6 +217,11 @@ class TestOrderView(WebTestCase):
'customer_name': None, 'customer_name': None,
'phone_number': None, 'phone_number': None,
'email_address': None, 'email_address': None,
'new_customer_name': None,
'new_customer_first_name': None,
'new_customer_last_name': None,
'new_customer_phone': None,
'new_customer_email': None,
}) })
def test_start_over(self): def test_start_over(self):
@ -383,80 +268,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_assign_customer(self):
self.pyramid_config.add_route('orders.create', '/orders/new')
model = self.app.model
handler = NewOrderBatchHandler(self.config)
view = self.make_view()
user = model.User(username='barney')
self.session.add(user)
weirdal = model.LocalCustomer(full_name="Weird Al")
self.session.add(weirdal)
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()
# normal
self.assertIsNone(batch.local_customer)
self.assertIsNone(batch.pending_customer)
context = view.assign_customer(batch, {'customer_id': weirdal.uuid.hex})
self.assertIsNone(batch.pending_customer)
self.assertIs(batch.local_customer, weirdal)
self.assertEqual(context, {
'customer_is_known': True,
'customer_id': weirdal.uuid.hex,
'customer_name': 'Weird Al',
'phone_number': None,
'email_address': None,
})
# missing customer_id
context = view.assign_customer(batch, {})
self.assertEqual(context, {'error': "Must provide customer_id"})
def test_unassign_customer(self):
self.pyramid_config.add_route('orders.create', '/orders/new')
model = self.app.model
handler = NewOrderBatchHandler(self.config)
view = self.make_view()
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()
view.set_pending_customer(batch, {'first_name': 'Jack',
'last_name': 'Black'})
# normal
self.assertIsNone(batch.local_customer)
self.assertIsNotNone(batch.pending_customer)
self.assertEqual(batch.customer_name, 'Jack Black')
context = view.unassign_customer(batch, {})
# nb. pending record remains, but not used
self.assertIsNotNone(batch.pending_customer)
self.assertIsNone(batch.customer_name)
self.assertIsNone(batch.local_customer)
self.assertEqual(context, {
'customer_is_known': True,
'customer_id': None,
'customer_name': None,
'phone_number': None,
'email_address': None,
'new_customer_full_name': 'Jack Black',
'new_customer_first_name': 'Jack',
'new_customer_last_name': 'Black',
'new_customer_phone': None,
'new_customer_email': None,
})
def test_set_pending_customer(self): def test_set_pending_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
@ -490,58 +301,19 @@ class TestOrderView(WebTestCase):
'customer_name': 'Fred Flintstone', 'customer_name': 'Fred Flintstone',
'phone_number': '555-1234', 'phone_number': '555-1234',
'email_address': 'fred@mailinator.com', 'email_address': 'fred@mailinator.com',
'new_customer_full_name': 'Fred Flintstone', 'new_customer_name': 'Fred Flintstone',
'new_customer_first_name': 'Fred', 'new_customer_first_name': 'Fred',
'new_customer_last_name': 'Flintstone', 'new_customer_last_name': 'Flintstone',
'new_customer_phone': '555-1234', 'new_customer_phone': '555-1234',
'new_customer_email': 'fred@mailinator.com', 'new_customer_email': 'fred@mailinator.com',
}) })
def test_get_product_info(self): # error
model = self.app.model with patch.object(handler, 'set_pending_customer', side_effect=RuntimeError):
handler = self.make_handler() context = view.set_pending_customer(batch, data)
view = self.make_view() self.assertEqual(context, {
'error': 'RuntimeError',
user = model.User(username='barney') })
self.session.add(user)
local = model.LocalProduct(scancode='07430500132',
brand_name='Bragg',
description='Vinegar',
size='32oz',
case_size=12,
unit_price_reg=decimal.Decimal('5.99'))
self.session.add(local)
self.session.flush()
with patch.object(view, 'Session', return_value=self.session):
with patch.object(view, 'batch_handler', create=True, new=handler):
with patch.object(self.request, 'user', new=user):
batch = view.get_current_batch()
# typical, for local product
context = view.get_product_info(batch, {'product_id': local.uuid.hex})
self.assertEqual(context['product_id'], local.uuid.hex)
self.assertEqual(context['scancode'], '07430500132')
self.assertEqual(context['brand_name'], 'Bragg')
self.assertEqual(context['description'], 'Vinegar')
self.assertEqual(context['size'], '32oz')
self.assertEqual(context['full_description'], 'Bragg Vinegar 32oz')
self.assertEqual(context['case_size'], 12)
self.assertEqual(context['unit_price_reg'], 5.99)
# error if local product missing
mock_uuid = self.app.make_true_uuid()
context = view.get_product_info(batch, {'product_id': mock_uuid.hex})
self.assertEqual(context, {'error': "Product not found"})
# error if no product_id
context = view.get_product_info(batch, {})
self.assertEqual(context, {'error': "Must specify a product ID"})
# external lookup not implemented (yet)
with patch.object(handler, 'use_local_products', return_value=False):
self.assertRaises(NotImplementedError, view.get_product_info,
batch, {'product_id': '42'})
def test_add_item(self): def test_add_item(self):
model = self.app.model model = self.app.model
@ -554,7 +326,7 @@ class TestOrderView(WebTestCase):
self.session.commit() self.session.commit()
data = { data = {
'product_info': { 'pending_product': {
'scancode': '07430500132', 'scancode': '07430500132',
'brand_name': 'Bragg', 'brand_name': 'Bragg',
'description': 'Vinegar', 'description': 'Vinegar',
@ -581,10 +353,16 @@ class TestOrderView(WebTestCase):
row = batch.rows[0] row = batch.rows[0]
self.assertIsInstance(row.pending_product, model.PendingProduct) self.assertIsInstance(row.pending_product, model.PendingProduct)
# external product not yet supported # pending w/ invalid price
with patch.object(handler, 'use_local_products', return_value=False): with patch.dict(data['pending_product'], unit_price_reg='invalid'):
with patch.dict(data, product_info='42'): result = view.add_item(batch, data)
self.assertRaises(NotImplementedError, view.add_item, batch, data) self.assertEqual(result, {'error': "Invalid entry for field: unit_price_reg"})
self.session.flush()
self.assertEqual(len(batch.rows), 1) # still just the 1st row
# true product not yet supported
with patch.dict(data, product_is_known=True):
self.assertRaises(NotImplementedError, view.add_item, batch, data)
def test_update_item(self): def test_update_item(self):
model = self.app.model model = self.app.model
@ -597,7 +375,7 @@ class TestOrderView(WebTestCase):
self.session.commit() self.session.commit()
data = { data = {
'product_info': { 'pending_product': {
'scancode': '07430500132', 'scancode': '07430500132',
'brand_name': 'Bragg', 'brand_name': 'Bragg',
'description': 'Vinegar', 'description': 'Vinegar',
@ -625,7 +403,7 @@ class TestOrderView(WebTestCase):
# missing row uuid # missing row uuid
result = view.update_item(batch, data) result = view.update_item(batch, data)
self.assertEqual(result, {'error': "Must specify row UUID"}) self.assertEqual(result, {'error': "Must specify a row UUID"})
# row not found # row not found
with patch.dict(data, uuid=self.app.make_true_uuid()): with patch.dict(data, uuid=self.app.make_true_uuid()):
@ -642,18 +420,16 @@ class TestOrderView(WebTestCase):
result = view.update_item(batch, data) result = view.update_item(batch, data)
self.assertEqual(result, {'error': "Row is for wrong batch"}) self.assertEqual(result, {'error': "Row is for wrong batch"})
# set row for remaining tests
data['uuid'] = row.uuid
# true product not yet supported # true product not yet supported
with patch.object(handler, 'use_local_products', return_value=False): with patch.dict(data, product_is_known=True):
self.assertRaises(NotImplementedError, view.update_item, batch, { self.assertRaises(NotImplementedError, view.update_item, batch, data)
'uuid': row.uuid,
'product_info': '42',
'order_qty': 1,
'order_uom': enum.ORDER_UOM_UNIT,
})
# update row, pending product # update row, pending product
with patch.dict(data, uuid=row.uuid, order_qty=2): with patch.dict(data, order_qty=2):
with patch.dict(data['product_info'], scancode='07430500116'): with patch.dict(data['pending_product'], scancode='07430500116'):
self.assertEqual(row.product_scancode, '07430500132') self.assertEqual(row.product_scancode, '07430500132')
self.assertEqual(row.order_qty, 1) self.assertEqual(row.order_qty, 1)
result = view.update_item(batch, data) result = view.update_item(batch, data)
@ -662,7 +438,7 @@ class TestOrderView(WebTestCase):
self.assertEqual(row.order_qty, 2) self.assertEqual(row.order_qty, 2)
self.assertEqual(row.pending_product.scancode, '07430500116') self.assertEqual(row.pending_product.scancode, '07430500116')
self.assertEqual(result['row']['product_scancode'], '07430500116') self.assertEqual(result['row']['product_scancode'], '07430500116')
self.assertEqual(result['row']['order_qty'], 2) self.assertEqual(result['row']['order_qty'], '2')
def test_delete_item(self): def test_delete_item(self):
model = self.app.model model = self.app.model
@ -675,7 +451,7 @@ class TestOrderView(WebTestCase):
self.session.commit() self.session.commit()
data = { data = {
'product_info': { 'pending_product': {
'scancode': '07430500132', 'scancode': '07430500132',
'brand_name': 'Bragg', 'brand_name': 'Bragg',
'description': 'Vinegar', 'description': 'Vinegar',
@ -730,7 +506,7 @@ class TestOrderView(WebTestCase):
self.assertEqual(len(batch.rows), 0) self.assertEqual(len(batch.rows), 0)
self.assertEqual(batch.row_count, 0) self.assertEqual(batch.row_count, 0)
def test_submit_order(self): def test_submit_new_order(self):
self.pyramid_config.add_route('orders.view', '/orders/{uuid}') self.pyramid_config.add_route('orders.view', '/orders/{uuid}')
model = self.app.model model = self.app.model
enum = self.app.enum enum = self.app.enum
@ -742,7 +518,7 @@ class TestOrderView(WebTestCase):
self.session.commit() self.session.commit()
data = { data = {
'product_info': { 'pending_product': {
'scancode': '07430500132', 'scancode': '07430500132',
'brand_name': 'Bragg', 'brand_name': 'Bragg',
'description': 'Vinegar', 'description': 'Vinegar',
@ -758,33 +534,28 @@ class TestOrderView(WebTestCase):
with patch.object(view, 'Session', return_value=self.session): with patch.object(view, 'Session', return_value=self.session):
with patch.object(self.request, 'user', new=user): with patch.object(self.request, 'user', new=user):
batch = view.get_current_batch() batch = view.get_current_batch()
self.session.flush()
self.assertEqual(len(batch.rows), 0) self.assertEqual(len(batch.rows), 0)
# add row w/ pending product # add row w/ pending product
view.add_item(batch, data) view.add_item(batch, data)
self.assertEqual(len(batch.rows), 1) self.session.flush()
row = batch.rows[0] row = batch.rows[0]
self.assertIsInstance(row.pending_product, model.PendingProduct) self.assertIsInstance(row.pending_product, model.PendingProduct)
self.assertEqual(row.unit_price_quoted, decimal.Decimal('5.99')) self.assertEqual(row.unit_price_quoted, decimal.Decimal('5.99'))
# execute not allowed yet (no customer) # execute not allowed yet (no customer)
result = view.submit_order(batch, {}) result = view.submit_new_order(batch, {})
self.assertEqual(result, {'error': "Must assign the customer"}) self.assertEqual(result, {'error': "Must assign the customer"})
# execute not allowed yet (no phone number)
view.set_pending_customer(batch, {'full_name': 'John Doe'})
result = view.submit_order(batch, {})
self.assertEqual(result, {'error': "Customer phone number is required"})
# submit/execute ok # submit/execute ok
view.set_pending_customer(batch, {'full_name': 'John Doe', batch.customer_id = 42
'phone_number': '555-1234'}) result = view.submit_new_order(batch, {})
result = view.submit_order(batch, {})
self.assertEqual(sorted(result), ['next_url']) self.assertEqual(sorted(result), ['next_url'])
self.assertIn('/orders/', result['next_url']) self.assertIn('/orders/', result['next_url'])
# error (already executed) # error (already executed)
result = view.submit_order(batch, {}) result = view.submit_new_order(batch, {})
self.assertEqual(result, { self.assertEqual(result, {
'error': f"ValueError: batch has already been executed: {batch}", 'error': f"ValueError: batch has already been executed: {batch}",
}) })
@ -814,8 +585,9 @@ class TestOrderView(WebTestCase):
'size': '32oz', 'size': '32oz',
'unit_price_reg': 5.99, 'unit_price_reg': 5.99,
'case_size': 12, 'case_size': 12,
'created_by': user,
} }
row = handler.add_item(batch, pending, 1, enum.ORDER_UOM_CASE) row = handler.add_pending_product(batch, pending, 1, enum.ORDER_UOM_CASE)
self.session.commit() self.session.commit()
data = view.normalize_batch(batch) data = view.normalize_batch(batch)
@ -832,15 +604,11 @@ class TestOrderView(WebTestCase):
enum = self.app.enum enum = self.app.enum
handler = NewOrderBatchHandler(self.config) handler = NewOrderBatchHandler(self.config)
view = self.make_view() view = self.make_view()
view.batch_handler = handler
user = model.User(username='barney') user = model.User(username='barney')
self.session.add(user) self.session.add(user)
batch = handler.make_batch(self.session, created_by=user) batch = handler.make_batch(self.session, created_by=user)
self.session.add(batch) self.session.add(batch)
self.session.flush()
# add 1st row w/ pending product
pending = { pending = {
'scancode': '07430500132', 'scancode': '07430500132',
'brand_name': 'Bragg', 'brand_name': 'Bragg',
@ -848,22 +616,19 @@ class TestOrderView(WebTestCase):
'size': '32oz', 'size': '32oz',
'unit_price_reg': 5.99, 'unit_price_reg': 5.99,
'case_size': 12, 'case_size': 12,
'vendor_name': 'Acme Warehouse', 'created_by': user,
'vendor_item_code': '1234',
} }
row1 = handler.add_item(batch, pending, 2, enum.ORDER_UOM_CASE) row = handler.add_pending_product(batch, pending, 2, enum.ORDER_UOM_CASE)
self.session.commit()
# typical, pending product # normal
data = view.normalize_row(row1) data = view.normalize_row(row)
self.assertIsInstance(data, dict) self.assertIsInstance(data, dict)
self.assertEqual(data['uuid'], row1.uuid.hex) self.assertEqual(data['uuid'], row.uuid.hex)
self.assertEqual(data['sequence'], 1) self.assertEqual(data['sequence'], 1)
self.assertIsNone(data['product_id'])
self.assertEqual(data['product_scancode'], '07430500132') self.assertEqual(data['product_scancode'], '07430500132')
self.assertEqual(data['product_full_description'], 'Bragg Vinegar 32oz') self.assertEqual(data['case_size'], '12')
self.assertEqual(data['case_size'], 12) self.assertEqual(data['order_qty'], '2')
self.assertEqual(data['vendor_name'], 'Acme Warehouse')
self.assertEqual(data['order_qty'], 2)
self.assertEqual(data['order_uom'], 'CS') self.assertEqual(data['order_uom'], 'CS')
self.assertEqual(data['order_qty_display'], '2 Cases (&times; 12 = 24 Units)') self.assertEqual(data['order_qty_display'], '2 Cases (&times; 12 = 24 Units)')
self.assertEqual(data['unit_price_reg'], 5.99) self.assertEqual(data['unit_price_reg'], 5.99)
@ -879,9 +644,9 @@ class TestOrderView(WebTestCase):
self.assertEqual(data['total_price'], 143.76) self.assertEqual(data['total_price'], 143.76)
self.assertEqual(data['total_price_display'], '$143.76') self.assertEqual(data['total_price_display'], '$143.76')
self.assertIsNone(data['special_order']) self.assertIsNone(data['special_order'])
self.assertEqual(data['status_code'], row1.STATUS_OK) self.assertEqual(data['status_code'], row.STATUS_OK)
self.assertEqual(data['pending_product'], { self.assertEqual(data['pending_product'], {
'uuid': row1.pending_product_uuid.hex, 'uuid': row.pending_product_uuid.hex,
'scancode': '07430500132', 'scancode': '07430500132',
'brand_name': 'Bragg', 'brand_name': 'Bragg',
'description': 'Vinegar', 'description': 'Vinegar',
@ -889,117 +654,44 @@ class TestOrderView(WebTestCase):
'department_id': None, 'department_id': None,
'department_name': None, 'department_name': None,
'unit_price_reg': 5.99, 'unit_price_reg': 5.99,
'vendor_name': 'Acme Warehouse', 'vendor_name': None,
'vendor_item_code': '1234', 'vendor_item_code': None,
'unit_cost': None, 'unit_cost': None,
'case_size': 12.0, 'case_size': 12.0,
'notes': None, 'notes': None,
'special_order': None, 'special_order': None,
}) })
# the next few tests will morph 1st row..
# unknown case size # unknown case size
row1.pending_product.case_size = None row.pending_product.case_size = None
handler.refresh_row(row1) handler.refresh_row(row)
self.session.flush() self.session.flush()
data = view.normalize_row(row1) data = view.normalize_row(row)
self.assertIsNone(data['case_size'])
self.assertEqual(data['order_qty_display'], '2 Cases (&times; ?? = ?? Units)') self.assertEqual(data['order_qty_display'], '2 Cases (&times; ?? = ?? Units)')
# order by unit # order by unit
row1.order_uom = enum.ORDER_UOM_UNIT row.order_uom = enum.ORDER_UOM_UNIT
handler.refresh_row(row1) handler.refresh_row(row)
self.session.flush() self.session.flush()
data = view.normalize_row(row1) data = view.normalize_row(row)
self.assertEqual(data['order_uom'], enum.ORDER_UOM_UNIT)
self.assertEqual(data['order_qty_display'], '2 Units') self.assertEqual(data['order_qty_display'], '2 Units')
# item on sale # item on sale
row1.pending_product.case_size = 12 row.pending_product.case_size = 12
row1.unit_price_sale = decimal.Decimal('5.19') row.unit_price_sale = decimal.Decimal('5.19')
row1.sale_ends = datetime.datetime(2099, 1, 5, 20, 32) row.sale_ends = datetime.datetime(2025, 1, 5, 20, 32)
handler.refresh_row(row1) handler.refresh_row(row, now=datetime.datetime(2025, 1, 5, 19))
self.session.flush() self.session.flush()
data = view.normalize_row(row1) data = view.normalize_row(row)
self.assertEqual(data['unit_price_sale'], 5.19) self.assertEqual(data['unit_price_sale'], 5.19)
self.assertEqual(data['unit_price_sale_display'], '$5.19') self.assertEqual(data['unit_price_sale_display'], '$5.19')
self.assertEqual(data['sale_ends'], '2099-01-05 20:32:00') self.assertEqual(data['sale_ends'], '2025-01-05 20:32:00')
self.assertEqual(data['sale_ends_display'], '2099-01-05') self.assertEqual(data['sale_ends_display'], '2025-01-05')
self.assertEqual(data['unit_price_quoted'], 5.19) self.assertEqual(data['unit_price_quoted'], 5.19)
self.assertEqual(data['unit_price_quoted_display'], '$5.19') self.assertEqual(data['unit_price_quoted_display'], '$5.19')
self.assertEqual(data['case_price_quoted'], 62.28) self.assertEqual(data['case_price_quoted'], 62.28)
self.assertEqual(data['case_price_quoted_display'], '$62.28') self.assertEqual(data['case_price_quoted_display'], '$62.28')
# add 2nd row w/ local product
local = model.LocalProduct(brand_name="Lay's",
description="Potato Chips",
vendor_name='Acme Distribution',
unit_price_reg=3.29)
self.session.add(local)
self.session.flush()
row2 = handler.add_item(batch, local.uuid.hex, 1, enum.ORDER_UOM_UNIT)
# typical, local product
data = view.normalize_row(row2)
self.assertEqual(data['uuid'], row2.uuid.hex)
self.assertEqual(data['sequence'], 2)
self.assertEqual(data['product_id'], local.uuid.hex)
self.assertIsNone(data['product_scancode'])
self.assertEqual(data['product_full_description'], "Lay's Potato Chips")
self.assertIsNone(data['case_size'])
self.assertEqual(data['vendor_name'], 'Acme Distribution')
self.assertEqual(data['order_qty'], 1)
self.assertEqual(data['order_uom'], 'EA')
self.assertEqual(data['order_qty_display'], '1 Units')
self.assertEqual(data['unit_price_reg'], 3.29)
self.assertEqual(data['unit_price_reg_display'], '$3.29')
self.assertNotIn('unit_price_sale', data)
self.assertNotIn('unit_price_sale_display', data)
self.assertNotIn('sale_ends', data)
self.assertNotIn('sale_ends_display', data)
self.assertEqual(data['unit_price_quoted'], 3.29)
self.assertEqual(data['unit_price_quoted_display'], '$3.29')
self.assertIsNone(data['case_price_quoted'])
self.assertEqual(data['case_price_quoted_display'], '')
self.assertEqual(data['total_price'], 3.29)
self.assertEqual(data['total_price_display'], '$3.29')
self.assertIsNone(data['special_order'])
self.assertEqual(data['status_code'], row2.STATUS_OK)
self.assertNotIn('pending_product', data)
# the next few tests will morph 2nd row..
# typical, external product
row2.product_id = '42'
with patch.object(handler, 'use_local_products', return_value=False):
data = view.normalize_row(row2)
self.assertEqual(data['uuid'], row2.uuid.hex)
self.assertEqual(data['sequence'], 2)
self.assertEqual(data['product_id'], '42')
self.assertIsNone(data['product_scancode'])
self.assertNotIn('product_full_description', data) # TODO
self.assertIsNone(data['case_size'])
self.assertNotIn('vendor_name', data) # TODO
self.assertEqual(data['order_qty'], 1)
self.assertEqual(data['order_uom'], 'EA')
self.assertEqual(data['order_qty_display'], '1 Units')
self.assertEqual(data['unit_price_reg'], 3.29)
self.assertEqual(data['unit_price_reg_display'], '$3.29')
self.assertNotIn('unit_price_sale', data)
self.assertNotIn('unit_price_sale_display', data)
self.assertNotIn('sale_ends', data)
self.assertNotIn('sale_ends_display', data)
self.assertEqual(data['unit_price_quoted'], 3.29)
self.assertEqual(data['unit_price_quoted_display'], '$3.29')
self.assertIsNone(data['case_price_quoted'])
self.assertEqual(data['case_price_quoted_display'], '')
self.assertEqual(data['total_price'], 3.29)
self.assertEqual(data['total_price_display'], '$3.29')
self.assertIsNone(data['special_order'])
self.assertEqual(data['status_code'], row2.STATUS_OK)
self.assertNotIn('pending_product', data)
def test_get_instance_title(self): def test_get_instance_title(self):
model = self.app.model model = self.app.model
view = self.make_view() view = self.make_view()
@ -1023,31 +715,13 @@ class TestOrderView(WebTestCase):
self.session.add(order) self.session.add(order)
self.session.commit() self.session.commit()
# viewing (no customer) # viewing
with patch.object(view, 'viewing', new=True): with patch.object(view, 'viewing', new=True):
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')
view.configure_form(form) view.configure_form(form)
schema = form.get_schema() schema = form.get_schema()
self.assertIn('pending_customer', form)
self.assertIsInstance(schema['total_price'].typ, WuttaMoney)
# assign local customer
local = model.LocalCustomer(first_name='Jack', last_name='Black',
phone_number='555-1234')
self.session.add(local)
order.local_customer = local
self.session.flush()
# viewing (local customer)
with patch.object(view, 'viewing', new=True):
form = view.make_form(model_instance=order)
# nb. this is to avoid include/exclude ambiguity
form.remove('items')
view.configure_form(form)
self.assertNotIn('pending_customer', form)
schema = form.get_schema()
self.assertIsInstance(schema['total_price'].typ, WuttaMoney) self.assertIsInstance(schema['total_price'].typ, WuttaMoney)
def test_get_xref_buttons(self): def test_get_xref_buttons(self):
@ -1157,46 +831,6 @@ class TestOrderView(WebTestCase):
url = view.get_row_action_url_view(item, 0) url = view.get_row_action_url_view(item, 0)
self.assertIn(f'/order-items/{item.uuid}', url) self.assertIn(f'/order-items/{item.uuid}', url)
def test_configure(self):
self.pyramid_config.add_route('home', '/')
self.pyramid_config.add_route('login', '/auth/login')
self.pyramid_config.add_route('orders', '/orders/')
model = self.app.model
view = self.make_view()
with patch.object(view, 'Session', return_value=self.session):
with patch.multiple(self.config, usedb=True, preferdb=True):
# sanity check
allowed = self.config.get_bool('sideshow.orders.allow_unknown_products',
session=self.session)
self.assertIsNone(allowed)
self.assertEqual(self.session.query(model.Setting).count(), 0)
# fetch initial page
response = view.configure()
self.assertIsInstance(response, Response)
self.assertNotIsInstance(response, HTTPFound)
self.session.flush()
allowed = self.config.get_bool('sideshow.orders.allow_unknown_products',
session=self.session)
self.assertIsNone(allowed)
self.assertEqual(self.session.query(model.Setting).count(), 0)
# post new settings
with patch.multiple(self.request, create=True,
method='POST',
POST={
'sideshow.orders.allow_unknown_products': 'true',
}):
response = view.configure()
self.assertIsInstance(response, HTTPFound)
self.session.flush()
allowed = self.config.get_bool('sideshow.orders.allow_unknown_products',
session=self.session)
self.assertTrue(allowed)
self.assertTrue(self.session.query(model.Setting).count() > 1)
class TestOrderItemView(WebTestCase): class TestOrderItemView(WebTestCase):
@ -1230,18 +864,6 @@ class TestOrderItemView(WebTestCase):
self.assertEqual(view.render_status_code(None, None, enum.ORDER_ITEM_STATUS_INITIATED), self.assertEqual(view.render_status_code(None, None, enum.ORDER_ITEM_STATUS_INITIATED),
'initiated') 'initiated')
def test_get_instance_title(self):
model = self.app.model
enum = self.app.enum
view = self.make_view()
item = model.OrderItem(product_brand='Bragg',
product_description='Vinegar',
product_size='32oz',
status_code=enum.ORDER_ITEM_STATUS_INITIATED)
title = view.get_instance_title(item)
self.assertEqual(title, "(initiated) Bragg Vinegar 32oz")
def test_configure_form(self): def test_configure_form(self):
model = self.app.model model = self.app.model
enum = self.app.enum enum = self.app.enum
@ -1249,24 +871,12 @@ class TestOrderItemView(WebTestCase):
item = model.OrderItem(status_code=enum.ORDER_ITEM_STATUS_INITIATED) item = model.OrderItem(status_code=enum.ORDER_ITEM_STATUS_INITIATED)
# viewing, w/ pending product # viewing
with patch.object(view, 'viewing', new=True): with patch.object(view, 'viewing', new=True):
form = view.make_form(model_instance=item) form = view.make_form(model_instance=item)
view.configure_form(form) view.configure_form(form)
schema = form.get_schema() schema = form.get_schema()
self.assertIsInstance(schema['order'].typ, OrderRef) self.assertIsInstance(schema['order'].typ, OrderRef)
self.assertIn('pending_product', form)
self.assertIsInstance(schema['pending_product'].typ, PendingProductRef)
# viewing, w/ local product
local = model.LocalProduct()
item.local_product = local
with patch.object(view, 'viewing', new=True):
form = view.make_form(model_instance=item)
view.configure_form(form)
schema = form.get_schema()
self.assertIsInstance(schema['order'].typ, OrderRef)
self.assertNotIn('pending_product', form)
def test_get_xref_buttons(self): def test_get_xref_buttons(self):
self.pyramid_config.add_route('orders.view', '/orders/{uuid}') self.pyramid_config.add_route('orders.view', '/orders/{uuid}')

View file

@ -16,104 +16,6 @@ class TestIncludeme(WebTestCase):
mod.includeme(self.pyramid_config) mod.includeme(self.pyramid_config)
class TestLocalProductView(WebTestCase):
def make_view(self):
return mod.LocalProductView(self.request)
def test_configure_grid(self):
model = self.app.model
view = self.make_view()
grid = view.make_grid(model_class=model.LocalProduct)
self.assertNotIn('scancode', grid.linked_columns)
self.assertNotIn('brand_name', grid.linked_columns)
self.assertNotIn('description', grid.linked_columns)
view.configure_grid(grid)
self.assertIn('scancode', grid.linked_columns)
self.assertIn('brand_name', grid.linked_columns)
self.assertIn('description', grid.linked_columns)
def test_configure_form(self):
model = self.app.model
view = self.make_view()
# creating
with patch.object(view, 'creating', new=True):
form = view.make_form(model_class=model.LocalProduct)
self.assertIn('external_id', form)
view.configure_form(form)
self.assertNotIn('external_id', form)
user = model.User(username='barney')
self.session.add(user)
product = model.LocalProduct()
self.session.add(product)
self.session.commit()
# viewing
with patch.object(view, 'viewing', new=True):
form = view.make_form(model_instance=product)
self.assertNotIn('external_id', form.readonly_fields)
self.assertNotIn('local_products.view.orders', form.grid_vue_context)
view.configure_form(form)
self.assertIn('external_id', form.readonly_fields)
self.assertIn('local_products.view.orders', form.grid_vue_context)
def test_make_orders_grid(self):
model = self.app.model
enum = self.app.enum
view = self.make_view()
user = model.User(username='barney')
self.session.add(user)
order = model.Order(order_id=42, customer_id=42, created_by=user)
product = model.LocalProduct()
self.session.add(product)
item = model.OrderItem(local_product=product,
order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
status_code=enum.ORDER_ITEM_STATUS_INITIATED)
order.items.append(item)
self.session.add(order)
self.session.commit()
# no view perm
grid = view.make_orders_grid(product)
self.assertEqual(len(grid.actions), 0)
# with view perm
with patch.object(self.request, 'is_root', new=True):
grid = view.make_orders_grid(product)
self.assertEqual(len(grid.actions), 1)
self.assertEqual(grid.actions[0].key, 'view')
def test_make_new_order_batches_grid(self):
model = self.app.model
enum = self.app.enum
handler = NewOrderBatchHandler(self.config)
view = self.make_view()
user = model.User(username='barney')
self.session.add(user)
batch = handler.make_batch(self.session, created_by=user)
self.session.add(batch)
product = model.LocalProduct()
self.session.add(product)
row = handler.make_row(local_product=product,
order_qty=1, order_uom=enum.ORDER_UOM_UNIT)
handler.add_row(batch, row)
self.session.commit()
# no view perm
grid = view.make_new_order_batches_grid(product)
self.assertEqual(len(grid.actions), 0)
# with view perm
with patch.object(self.request, 'is_root', new=True):
grid = view.make_new_order_batches_grid(product)
self.assertEqual(len(grid.actions), 1)
self.assertEqual(grid.actions[0].key, 'view')
class TestPendingProductView(WebTestCase): class TestPendingProductView(WebTestCase):
def make_view(self): def make_view(self):