feat: add basic support for local customer, product lookups

also convert pending to local (where relevant) when executing batch
This commit is contained in:
Lance Edgar 2025-01-09 12:13:58 -06:00
parent ebd22fe6ee
commit a4ad23c7fa
23 changed files with 3205 additions and 796 deletions

View file

@ -5,6 +5,46 @@ Glossary
.. glossary::
: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
This is the central focus of the app; it refers to a customer
case/special order which is tracked over time, from placement to
@ -20,17 +60,19 @@ Glossary
sibling items.
pending customer
Generally refers to a "new / unknown" customer, e.g. for whom a
new order is being created. This allows the order lifecycle to
get going before the customer has a proper account in the system.
A "temporary" customer record used when creating an :term:`order`
for new/unknown customer.
See :class:`~sideshow.db.model.customers.PendingCustomer` for the
data model.
The data model for this is
:class:`~sideshow.db.model.customers.PendingCustomer`.
See also :term:`external customer` and :term:`pending customer`.
pending product
Generally refers to a "new / unknown" product, e.g. for which a
new order is being created. This allows the order lifecycle to
get going before the product has a true record in the system.
A "temporary" product record used when creating an :term:`order`
for new/unknown product.
See :class:`~sideshow.db.model.products.PendingProduct` for the
data model.
The data model for this is
:class:`~sideshow.db.model.products.PendingProduct`.
See also :term:`external product` and :term:`pending product`.

View file

@ -27,6 +27,8 @@ New Order Batch Handler
import datetime
import decimal
import sqlalchemy as sa
from wuttjamaican.batch import BatchHandler
from sideshow.db.model import NewOrderBatch
@ -34,7 +36,8 @@ from sideshow.db.model import NewOrderBatch
class NewOrderBatchHandler(BatchHandler):
"""
The :term:`batch handler` for New Order Batches.
The :term:`batch handler` for :term:`new order batches <new order
batch>`.
This is responsible for business logic around the creation of new
:term:`orders <order>`. A
@ -44,149 +47,195 @@ class NewOrderBatchHandler(BatchHandler):
"""
model_class = NewOrderBatch
def allow_unknown_product(self):
def use_local_customers(self):
"""
Returns a boolean indicating whether "unknown" (pending)
products are allowed when creating a new order.
Returns boolean indicating whether :term:`local customer`
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.allow_unknown_product',
return self.config.get_bool('sideshow.orders.use_local_customers',
default=True)
def set_pending_customer(self, batch, data):
def use_local_products(self):
"""
Set (add or update) pending customer info for the batch.
This will clear the
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.customer_id`
and set the
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`,
creating a new record if needed. It then updates the pending
customer record per the given ``data``.
:param batch:
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch`
to be updated.
:param data: Dict of field data for the
:class:`~sideshow.db.model.customers.PendingCustomer`
record.
Returns boolean indicating whether :term:`local product`
records should be used. This is true by default, but may be
false for :term:`external product` lookups.
"""
model = self.app.model
enum = self.app.enum
return self.config.get_bool('sideshow.orders.use_local_products',
default=True)
# remove customer account if set
batch.customer_id = None
# create pending customer if needed
pending = batch.pending_customer
if not pending:
kw = dict(data)
kw.setdefault('status', enum.PendingCustomerStatus.PENDING)
pending = model.PendingCustomer(**kw)
batch.pending_customer = pending
# update pending customer
if 'first_name' in data:
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']
# update batch per pending customer
batch.customer_name = pending.full_name
batch.phone_number = pending.phone_number
batch.email_address = pending.email_address
def add_pending_product(self, batch, pending_info,
order_qty, order_uom):
def allow_unknown_products(self):
"""
Add a new row to the batch, for the given "pending" product
and order quantity.
Returns boolean indicating whether :term:`pending products
<pending product>` are allowed when creating an order.
See also :meth:`set_pending_product()` to update an existing row.
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:
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch` to
which the row should be added.
update.
:param pending_info: Dict of kwargs to use when constructing a
new :class:`~sideshow.db.model.products.PendingProduct`.
:param customer_info: Customer ID string, or dict of
:class:`~sideshow.db.model.customers.PendingCustomer` data,
or ``None`` to clear the customer info.
:param order_qty: Quantity of the product to be added to the
order.
:param order_uom: UOM for the order quantity; must be a code
from :data:`~sideshow.enum.ORDER_UOM`.
:returns:
:class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
which was added to the batch.
: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.
"""
if not self.allow_unknown_product():
raise TypeError("unknown/pending product not allowed for new orders")
model = self.app.model
enum = self.app.enum
session = self.app.get_session(batch)
use_local = self.use_local_customers()
# set customer info
if isinstance(customer_info, str):
if use_local:
# local_customer
customer = session.get(model.LocalCustomer, customer_info)
if not customer:
raise ValueError("local customer not found")
batch.local_customer = customer
batch.customer_name = customer.full_name
batch.phone_number = customer.phone_number
batch.email_address = customer.email_address
else: # external customer_id
#batch.customer_id = customer_info
raise NotImplementedError
elif customer_info:
# pending_customer
batch.customer_id = None
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
# make new pending product
kw = dict(pending_info)
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)
# make/add new row, w/ pending product
row = self.make_row(pending_product=product,
order_qty=order_qty, order_uom=order_uom)
self.add_row(batch, row)
session.add(row)
session.flush()
return row
def set_pending_product(self, row, data):
def add_item(self, batch, product_info, order_qty, order_uom, user=None):
"""
Set (add or update) pending product info for the given batch row.
Add a new item/row to the batch, for given product and quantity.
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()`.
See also :meth:`update_item()`.
Note that this does not update order quantity for the item.
:param batch:
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch` to
update.
See also :meth:`add_pending_product()` to add a new row
instead of updating.
:param product_info: Product ID string, or dict of
:class:`~sideshow.db.model.products.PendingProduct` data.
:param row:
:param order_qty:
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_qty`
value for the new row.
:param order_uom:
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.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:
:class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
to be updated.
:param data: Dict of field data for the
:class:`~sideshow.db.model.products.PendingProduct` record.
instance.
"""
if not self.allow_unknown_product():
raise TypeError("unknown/pending product not allowed for new orders")
model = self.app.model
enum = self.app.enum
session = self.app.get_session(row)
session = self.app.get_session(batch)
use_local = self.use_local_products()
row = self.make_row()
# values for these fields can be used as-is
simple_fields = [
# set product info
if isinstance(product_info, str):
if use_local:
# local_product
local = session.get(model.LocalProduct, product_info)
if not local:
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',
@ -197,49 +246,134 @@ class NewOrderBatchHandler(BatchHandler):
'special_order',
'vendor_name',
'vendor_item_code',
'notes',
'unit_cost',
'case_size',
'case_cost',
'unit_cost',
'unit_price_reg',
'notes',
]
for key in fields:
setattr(pending, key, product_info.get(key))
# clear true product id
row.product_id = None
# make pending product if needed
product = row.pending_product
if not product:
kw = dict(data)
kw.setdefault('status', enum.PendingProductStatus.PENDING)
product = model.PendingProduct(**kw)
session.add(product)
row.pending_product = product
# nb. this may convert float to decimal etc.
session.add(pending)
session.flush()
session.refresh(pending)
row.pending_product = pending
# update pending product
for field in simple_fields:
if field in data:
setattr(product, field, data[field])
# set order info
row.order_qty = order_qty
row.order_uom = order_uom
# add row to batch
self.add_row(batch, row)
session.flush()
return row
def update_item(self, row, product_info, order_qty, order_uom, user=None):
"""
Update an item/row, per given product and quantity.
See also :meth:`add_item()`.
:param row:
:class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
to update.
:param product_info: Product ID string, or dict of
:class:`~sideshow.db.model.products.PendingProduct` data.
: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
enum = self.app.enum
session = self.app.get_session(row)
use_local = self.use_local_products()
# set product info
if isinstance(product_info, str):
if use_local:
# local_product
local = session.get(model.LocalProduct, product_info)
if not local:
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 = 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.refresh(product)
session.refresh(pending)
# set order info
row.order_qty = order_qty
row.order_uom = order_uom
# nb. this may convert float to decimal etc.
session.flush()
session.refresh(row)
# refresh per new info
self.refresh_row(row)
def refresh_row(self, row, now=None):
def refresh_row(self, row):
"""
Refresh all data for the row. This is called when adding a
new row to the batch, or anytime the row is updated (e.g. when
Refresh data for the row. This is called when adding a new
row to the batch, or anytime the row is updated (e.g. when
changing order quantity).
This calls one of the following to update product-related
attributes for the row:
attributes:
* :meth:`refresh_row_from_external_product()`
* :meth:`refresh_row_from_local_product()`
* :meth:`refresh_row_from_pending_product()`
* :meth:`refresh_row_from_true_product()`
It then re-calculates the row's
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.total_price`
@ -253,7 +387,7 @@ class NewOrderBatchHandler(BatchHandler):
row.status_text = None
# ensure product
if not row.product_id and not row.pending_product:
if not row.product_id and not row.local_product and not row.pending_product:
row.status_code = row.STATUS_MISSING_PRODUCT
return
@ -264,7 +398,9 @@ class NewOrderBatchHandler(BatchHandler):
# update product attrs on row
if row.product_id:
self.refresh_row_from_true_product(row)
self.refresh_row_from_external_product(row)
elif row.local_product:
self.refresh_row_from_local_product(row)
else:
self.refresh_row_from_pending_product(row)
@ -276,7 +412,7 @@ class NewOrderBatchHandler(BatchHandler):
row.case_price_quoted = None
if row.unit_price_sale is not None and (
not row.sale_ends
or row.sale_ends > (now or datetime.datetime.now())):
or row.sale_ends > datetime.datetime.now()):
row.unit_price_quoted = row.unit_price_sale
else:
row.unit_price_quoted = row.unit_price_reg
@ -304,16 +440,15 @@ class NewOrderBatchHandler(BatchHandler):
# all ok
row.status_code = row.STATUS_OK
def refresh_row_from_pending_product(self, row):
def refresh_row_from_local_product(self, row):
"""
Update product-related attributes on the row, from its
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.pending_product`
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.local_product`
record.
This is called automatically from :meth:`refresh_row()`.
"""
product = row.pending_product
product = row.local_product
row.product_scancode = product.scancode
row.product_brand = product.brand_name
row.product_description = product.description
@ -326,10 +461,31 @@ class NewOrderBatchHandler(BatchHandler):
row.unit_cost = product.unit_cost
row.unit_price_reg = product.unit_price_reg
def refresh_row_from_true_product(self, row):
def refresh_row_from_pending_product(self, row):
"""
Update product-related attributes on the row, from its "true"
product record indicated by
Update product-related attributes on the row, from its
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.pending_product`
record.
This is called automatically from :meth:`refresh_row()`.
"""
product = row.pending_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_external_product(self, row):
"""
Update product-related attributes on the row, from its
:term:`external product` record indicated by
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.product_id`.
This is called automatically from :meth:`refresh_row()`.
@ -354,31 +510,39 @@ class NewOrderBatchHandler(BatchHandler):
def do_delete(self, batch, user, **kwargs):
"""
Delete the given batch entirely.
Delete a batch completely.
If the batch has a
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`
record, that is deleted also.
If the batch has :term:`pending customer` or :term:`pending
product` records, they are also deleted - unless still
referenced by some order(s).
"""
# maybe delete pending customer record, if it only exists for
# sake of this batch
if batch.pending_customer:
if len(batch.pending_customer.new_order_batches) == 1:
# TODO: check for past orders too
session = self.app.get_session(batch)
session.delete(batch.pending_customer)
# maybe delete pending customer
customer = batch.pending_customer
if customer and not customer.orders:
session.delete(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
super().do_delete(batch, user, **kwargs)
def why_not_execute(self, batch, **kwargs):
"""
By default this checks to ensure the batch has a customer and
at least one item.
By default this checks to ensure the batch has a customer with
phone number, and at least one item.
"""
if not batch.customer_id and not batch.pending_customer:
if not batch.customer_name:
return "Must assign the customer"
if not batch.phone_number:
return "Customer phone number is required"
rows = self.get_effective_rows(batch)
if not rows:
return "Must add at least one valid item"
@ -395,17 +559,113 @@ class NewOrderBatchHandler(BatchHandler):
def execute(self, batch, user=None, progress=None, **kwargs):
"""
By default, this will call :meth:`make_new_order()` and return
the new :class:`~sideshow.db.model.orders.Order` instance.
Execute the batch; this should make a proper :term:`order`.
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
:meth:`~wuttjamaican:wuttjamaican.batch.BatchHandler.do_execute()`
instead, which calls this method automatically.
"""
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)
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):
"""
Create a new :term:`order` from the batch data.
@ -429,6 +689,7 @@ class NewOrderBatchHandler(BatchHandler):
batch_fields = [
'store_id',
'customer_id',
'local_customer',
'pending_customer',
'customer_name',
'phone_number',
@ -437,7 +698,9 @@ class NewOrderBatchHandler(BatchHandler):
]
row_fields = [
'pending_product_uuid',
'product_id',
'local_product',
'pending_product',
'product_scancode',
'product_brand',
'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='pendingproductstatus').create(op.get_bind())
# sideshow_pending_customer
op.create_table('sideshow_pending_customer',
# sideshow_customer_pending
op.create_table('sideshow_customer_pending',
sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False),
sa.Column('customer_id', sa.String(length=20), nullable=True),
sa.Column('full_name', sa.String(length=100), nullable=True),
@ -38,12 +38,24 @@ def upgrade() -> None:
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_by_uuid', wuttjamaican.db.util.UUID(), nullable=False),
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_pending_customer'))
sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_customer_pending_created_by_uuid_user')),
sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_customer_pending'))
)
# sideshow_pending_product
op.create_table('sideshow_pending_product',
# sideshow_customer_local
op.create_table('sideshow_customer_local',
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('product_id', sa.String(length=20), nullable=True),
sa.Column('scancode', sa.String(length=14), nullable=True),
@ -63,8 +75,29 @@ def upgrade() -> None:
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_by_uuid', wuttjamaican.db.util.UUID(), nullable=False),
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_pending_product'))
sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_product_pending_created_by_uuid_user')),
sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_product_pending'))
)
# 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
@ -73,6 +106,7 @@ def upgrade() -> None:
sa.Column('order_id', sa.Integer(), nullable=False),
sa.Column('store_id', sa.String(length=10), 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('customer_name', sa.String(length=100), nullable=True),
sa.Column('phone_number', sa.String(length=20), nullable=True),
@ -80,7 +114,8 @@ def upgrade() -> None:
sa.Column('total_price', sa.Numeric(precision=10, scale=3), nullable=True),
sa.Column('created', sa.DateTime(timezone=True), nullable=False),
sa.Column('created_by_uuid', wuttjamaican.db.util.UUID(), nullable=False),
sa.ForeignKeyConstraint(['pending_customer_uuid'], ['sideshow_pending_customer.uuid'], name=op.f('fk_order_pending_customer_uuid_pending_customer')),
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_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.PrimaryKeyConstraint('uuid', name=op.f('pk_order'))
)
@ -91,6 +126,7 @@ def upgrade() -> None:
sa.Column('order_uuid', wuttjamaican.db.util.UUID(), nullable=False),
sa.Column('sequence', sa.Integer(), nullable=False),
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('product_scancode', sa.String(length=14), nullable=True),
sa.Column('product_brand', sa.String(length=100), nullable=True),
@ -115,7 +151,8 @@ def upgrade() -> None:
sa.Column('paid_amount', sa.Numeric(precision=8, scale=3), nullable=False),
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(['pending_product_uuid'], ['sideshow_pending_product.uuid'], name=op.f('fk_sideshow_order_item_pending_product_uuid_pending_product')),
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_product_pending.uuid'], name=op.f('fk_sideshow_order_item_pending_product_uuid_pending_product')),
sa.PrimaryKeyConstraint('uuid', name=op.f('pk_order_item'))
)
@ -134,6 +171,7 @@ def upgrade() -> None:
sa.Column('executed_by_uuid', wuttjamaican.db.util.UUID(), nullable=True),
sa.Column('store_id', sa.String(length=10), 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('customer_name', sa.String(length=100), nullable=True),
sa.Column('phone_number', sa.String(length=20), nullable=True),
@ -141,7 +179,8 @@ def upgrade() -> None:
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(['executed_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_batch_neworder_executed_by_uuid_user')),
sa.ForeignKeyConstraint(['pending_customer_uuid'], ['sideshow_pending_customer.uuid'], name=op.f('fk_sideshow_batch_neworder_pending_customer_uuid_pending_customer')),
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_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'))
)
@ -152,8 +191,9 @@ def upgrade() -> None:
sa.Column('sequence', sa.Integer(), nullable=False),
sa.Column('status_text', sa.String(length=255), nullable=True),
sa.Column('modified', sa.DateTime(timezone=True), 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('local_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_brand', sa.String(length=100), nullable=True),
sa.Column('product_description', sa.String(length=255), nullable=True),
@ -173,9 +213,10 @@ def upgrade() -> None:
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('total_price', sa.Numeric(precision=8, scale=3), nullable=True),
sa.Column('status_code', sa.Integer(), nullable=False),
sa.Column('status_code', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['batch_uuid'], ['sideshow_batch_neworder.uuid'], name=op.f('fk_sideshow_batch_neworder_row_batch_uuid_batch_neworder')),
sa.ForeignKeyConstraint(['pending_product_uuid'], ['sideshow_pending_product.uuid'], name=op.f('fk_sideshow_batch_neworder_row_pending_product_uuid_pending_product')),
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_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'))
)
@ -192,11 +233,17 @@ def downgrade() -> None:
# sideshow_order
op.drop_table('sideshow_order')
# sideshow_pending_product
op.drop_table('sideshow_pending_product')
# sideshow_product_local
op.drop_table('sideshow_product_local')
# sideshow_pending_customer
op.drop_table('sideshow_pending_customer')
# sideshow_product_pending
op.drop_table('sideshow_product_pending')
# sideshow_customer_local
op.drop_table('sideshow_customer_local')
# sideshow_customer_pending
op.drop_table('sideshow_customer_pending')
# enums
sa.Enum('PENDING', 'READY', 'RESOLVED', name='pendingproductstatus').drop(op.get_bind())

View file

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

View file

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

View file

@ -34,19 +34,13 @@ from wuttjamaican.db import model
from sideshow.enum import PendingCustomerStatus
class PendingCustomer(model.Base):
class CustomerMixin:
"""
A "pending" customer record, used when entering an :term:`order`
for new/unknown customer.
Base class for customer tables. This has shared columns, used by e.g.:
* :class:`LocalCustomer`
* :class:`PendingCustomer`
"""
__tablename__ = 'sideshow_pending_customer'
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.
""")
full_name = sa.Column(sa.String(length=100), nullable=True, doc="""
Full display name for the customer account.
@ -68,6 +62,74 @@ class PendingCustomer(model.Base):
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 code for the customer record.
""")
@ -107,6 +169,3 @@ class PendingCustomer(model.Base):
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch`
records associated with this customer.
""")
def __str__(self):
return self.full_name or ""

View file

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

View file

@ -34,18 +34,13 @@ from wuttjamaican.db import model
from sideshow.enum import PendingProductStatus
class PendingProduct(model.Base):
class ProductMixin:
"""
A "pending" product record, used when entering an :term:`order
item` for new/unknown product.
Base class for product tables. This has shared columns, used by e.g.:
* :class:`LocalProduct`
* :class:`PendingProduct`
"""
__tablename__ = 'sideshow_pending_product'
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.
""")
scancode = sa.Column(sa.String(length=14), nullable=True, doc="""
Scancode for the product, as string.
@ -117,6 +112,82 @@ class PendingProduct(model.Base):
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 code for the product record.
""")
@ -138,10 +209,8 @@ class PendingProduct(model.Base):
order_items = orm.relationship(
'OrderItem',
# TODO
# order_by='NewOrderBatchRow.id.desc()',
cascade_backrefs=False,
back_populates='pending_product',
cascade_backrefs=False,
doc="""
List of :class:`~sideshow.db.model.orders.OrderItem` records
associated with this product.
@ -149,25 +218,10 @@ class PendingProduct(model.Base):
new_order_batch_rows = orm.relationship(
'NewOrderBatchRow',
# TODO
# order_by='NewOrderBatchRow.id.desc()',
cascade_backrefs=False,
back_populates='pending_product',
cascade_backrefs=False,
doc="""
List of
:class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
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):
"""
Custom schema type for an :class:`~sideshow.db.model.orders.Order`
Schema type for an :class:`~sideshow.db.model.orders.Order`
reference field.
This is a subclass of
@ -51,9 +51,34 @@ class OrderRef(ObjectRef):
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):
"""
Custom schema type for a
Schema type for a
:class:`~sideshow.db.model.customers.PendingCustomer` reference
field.
@ -76,9 +101,33 @@ class PendingCustomerRef(ObjectRef):
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):
"""
Custom schema type for a
Schema type for a
:class:`~sideshow.db.model.products.PendingProduct` reference
field.

View file

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

View file

@ -7,15 +7,15 @@
<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_product"
v-model="simpleSettings['sideshow.orders.allow_unknown_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_product']"
<div v-show="simpleSettings['sideshow.orders.allow_unknown_products']"
style="padding-left: 2rem;">
<p class="block">

View file

@ -130,28 +130,20 @@
<b-field label="Customer">
<div style="display: flex; gap: 1rem; width: 100%;">
<b-autocomplete ref="customerAutocomplete"
<wutta-autocomplete ref="customerAutocomplete"
v-model="customerID"
:style="{'flex-grow': customerID ? '0' : '1'}"
expanded
:display="customerName"
service-url="${url(f'{route_prefix}.customer_autocomplete')}"
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"
% endif
>
</b-autocomplete>
:style="{'flex-grow': customerID ? '0' : '1'}"
expanded />
<b-button v-if="customerID"
@click="refreshCustomer"
icon-pack="fas"
icon-left="redo"
:disabled="refreshingCustomer">
{{ refreshingCustomer ? "Refreshig" : "Refresh" }}
{{ refreshingCustomer ? "Refreshing" : "Refresh" }}
</b-button>
</div>
</b-field>
@ -348,9 +340,9 @@
<${b}-modal
% if request.use_oruga:
v-model:active="showingItemDialog"
v-model:active="editItemShowDialog"
% else:
:active.sync="showingItemDialog"
:active.sync="editItemShowDialog"
% endif
:can-cancel="['escape', 'x']"
>
@ -382,21 +374,12 @@
<div style="flex-grow: 1;">
<b-field label="Product">
<b-autocomplete ref="productLookup"
<wutta-autocomplete ref="productAutocomplete"
v-model="productID"
## :style="{'flex-grow': customerID ? '0' : '1'}"
## expanded
## placeholder="Enter name or phone number"
## ## 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>
:display="productDisplay"
service-url="${url(f'{route_prefix}.product_autocomplete')}"
placeholder="Enter brand, description etc."
@input="productChanged" />
</b-field>
<div v-if="productID">
@ -443,16 +426,16 @@
</div>
</div>
<img v-if="productID"
:src="productImageURL"
style="max-height: 150px; max-width: 150px; "/>
## <img v-if="productID"
## :src="productImageURL"
## style="max-height: 150px; max-width: 150px; "/>
</div>
<br />
<div class="field">
<b-radio v-model="productIsKnown"
% if not allow_unknown_product:
% if not allow_unknown_products:
disabled
% endif
:native-value="false">
@ -705,7 +688,7 @@
</${b}-tabs>
<div class="buttons">
<b-button @click="showingItemDialog = false">
<b-button @click="editItemShowDialog = false">
Cancel
</b-button>
<b-button type="is-primary"
@ -713,7 +696,7 @@
:disabled="itemDialogSaveDisabled"
icon-pack="fas"
icon-left="save">
{{ itemDialogSaving ? "Working, please wait..." : (this.editingItem ? "Update Item" : "Add Item") }}
{{ itemDialogSaving ? "Working, please wait..." : (this.editItemRow ? "Update Item" : "Add Item") }}
</b-button>
</div>
@ -757,8 +740,10 @@
<${b}-table-column label="Unit Price"
v-slot="props">
<span :class="props.row.pricing_reflects_sale ? 'has-background-warning' : null">
{{ props.row.unit_price_display }}
<span
##:class="props.row.pricing_reflects_sale ? 'has-background-warning' : null"
>
{{ props.row.unit_price_quoted_display }}
</span>
</${b}-table-column>
@ -771,17 +756,15 @@
<${b}-table-column label="Vendor"
v-slot="props">
{{ props.row.vendor_display }}
{{ props.row.vendor_name }}
</${b}-table-column>
<${b}-table-column field="actions"
label="Actions"
v-slot="props">
<a href="#"
% if not request.use_oruga:
class="grid-action"
% endif
@click.prevent="showEditItemDialog(props.row)">
@click.prevent="editItemInit(props.row)">
% if request.use_oruga:
<span class="icon-text">
<o-icon icon="edit" />
@ -846,13 +829,14 @@
batchTotalPriceDisplay: ${json.dumps(normalized_batch['total_price_display'])|n},
customerPanelOpen: false,
customerIsKnown: ${json.dumps(bool(batch.customer_id))|n},
customerID: ${json.dumps(batch.customer_id)|n},
customerName: ${json.dumps(batch.customer_name)|n},
orderPhoneNumber: ${json.dumps(batch.phone_number)|n},
orderEmailAddress: ${json.dumps(batch.email_address)|n},
customerIsKnown: ${json.dumps(customer_is_known)|n},
customerID: ${json.dumps(customer_id)|n},
customerName: ${json.dumps(customer_name)|n},
orderPhoneNumber: ${json.dumps(phone_number)|n},
orderEmailAddress: ${json.dumps(email_address)|n},
refreshingCustomer: false,
newCustomerFullName: ${json.dumps(new_customer_full_name)|n},
newCustomerFirstName: ${json.dumps(new_customer_first_name)|n},
newCustomerLastName: ${json.dumps(new_customer_last_name)|n},
newCustomerPhone: ${json.dumps(new_customer_phone)|n},
@ -867,8 +851,8 @@
items: ${json.dumps(order_items)|n},
editingItem: null,
showingItemDialog: false,
editItemRow: null,
editItemShowDialog: false,
itemDialogSaving: false,
% if request.use_oruga:
itemDialogTab: 'product',
@ -921,21 +905,9 @@
customerPanelHeader() {
let text = "Customer"
if (this.customerIsKnown) {
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) {
text += ' <p class="' + this.customerHeaderClass + '" style="display: inline-block; float: right;">' + this.customerStatusText + '</p>'
@ -1077,41 +1049,29 @@
}
},
## TODO
## watch: {
##
## contactIsKnown: function(val) {
##
## // when user clicks "contact is known" then we want to
## // set focus to the autocomplete component
## if (val) {
## 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"
## // button, i.e. `val` is false, then we want to *clear
## // out* the existing contact selection. this is
## // primarily to avoid any ambiguity.
## } else if (this.contactUUID) {
## 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()
## }
## },
## },
watch: {
customerIsKnown: function(val) {
if (val) {
// user clicks "customer is in the system"
// clear customer
this.customerChanged(null)
// focus customer autocomplete
this.$nextTick(() => {
this.$refs.customerAutocomplete.focus()
})
} else {
// user clicks "customer is NOT in the system"
// remove true customer; set pending (or null)
this.setPendingCustomer()
}
},
},
methods: {
@ -1157,7 +1117,7 @@
this.submittingOrder = true
const params = {
action: 'submit_new_order',
action: 'submit_order',
}
this.submitBatchData(params, response => {
@ -1173,29 +1133,28 @@
customerChanged(customerID, callback) {
let params
if (!customerID) {
params = {
action: 'unassign_contact',
}
const params = {}
if (customerID) {
params.action = 'assign_customer'
params.customer_id = customerID
} else {
params = {
action: 'assign_contact',
customer_id: customerID,
params.action = 'unassign_customer'
}
}
this.submitBatchData(params, response => {
this.customerID = response.data.customer_id
this.customerName = response.data.customer_name
this.orderPhoneNumber = response.data.phone_number
this.orderEmailAddress = response.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
this.submitBatchData(params, ({data}) => {
this.customerID = data.customer_id
this.customerName = data.customer_name
this.orderPhoneNumber = data.phone_number
this.orderEmailAddress = data.email_address
if (callback) {
callback()
}
}, response => {
this.$buefy.toast.open({
message: "Update failed: " + (response.data.error || "(unknown error)"),
type: 'is-danger',
duration: 2000, // 2 seconds
})
})
},
@ -1234,7 +1193,8 @@
}
this.submitBatchData(params, response => {
this.customerName = response.data.new_customer_name
this.customerName = response.data.new_customer_full_name
this.newCustomerFullName = response.data.new_customer_full_name
this.newCustomerFirstName = response.data.new_customer_first_name
this.newCustomerLastName = response.data.new_customer_last_name
this.newCustomerPhone = response.data.phone_number
@ -1254,6 +1214,40 @@
},
// 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() {
if (this.productIsKnown) {
return this.productCasePriceDisplay
@ -1308,6 +1302,76 @@
}
},
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
## productLookupSelected(selected) {
## // TODO: this still is a hack somehow, am sure of it.
@ -1335,7 +1399,7 @@
showAddItemDialog() {
this.customerPanelOpen = false
this.editingItem = null
this.editItemRow = null
this.productIsKnown = true
## this.selectedProduct = null
this.productID = null
@ -1364,14 +1428,15 @@
% else:
this.itemDialogTabIndex = 0
% endif
this.showingItemDialog = true
this.editItemShowDialog = true
this.$nextTick(() => {
this.$refs.productLookup.focus()
// this.$refs.productLookup.focus()
this.$refs.productAutocomplete.focus()
})
},
showEditItemDialog(row) {
this.editingItem = row
editItemInit(row) {
this.editItemRow = row
this.productIsKnown = !!row.product_id
this.productID = row.product_id
@ -1397,8 +1462,7 @@
this.productDisplay = row.product_full_description
this.productScancode = row.product_scancode
this.productSize = row.product_size
this.productCaseQuantity = row.case_quantity
this.productURL = row.product_url
this.productCaseQuantity = row.case_size
this.productUnitPrice = row.unit_price_quoted
this.productUnitPriceDisplay = row.unit_price_quoted_display
this.productUnitRegularPriceDisplay = row.unit_price_reg_display
@ -1422,7 +1486,7 @@
% else:
this.itemDialogTabIndex = 1
% endif
this.showingItemDialog = true
this.editItemShowDialog = true
},
deleteItem(index) {
@ -1451,25 +1515,20 @@
itemDialogAttemptSave() {
this.itemDialogSaving = true
let params = {
product_is_known: this.productIsKnown,
const params = {
order_qty: this.productQuantity,
order_uom: this.productUOM,
}
% if allow_item_discounts:
params.discount_percent = this.productDiscountPercent
% endif
if (this.productIsKnown) {
params.product_uuid = this.productUUID
params.product_info = this.productID
} else {
params.pending_product = this.pendingProduct
params.product_info = this.pendingProduct
}
if (this.editingItem) {
if (this.editItemRow) {
params.action = 'update_item'
params.uuid = this.editingItem.uuid
params.uuid = this.editItemRow.uuid
} else {
params.action = 'add_item'
}
@ -1484,7 +1543,7 @@
// overwriting the item record, or else display will
// not update properly
for (let [key, value] of Object.entries(response.data.row)) {
this.editingItem[key] = value
this.editItemRow[key] = value
}
}
@ -1492,7 +1551,7 @@
this.batchTotalPriceDisplay = response.data.batch.total_price_display
this.itemDialogSaving = false
this.showingItemDialog = false
this.editItemShowDialog = false
}, response => {
this.itemDialogSaving = false
})

View file

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

View file

@ -27,7 +27,161 @@ Views for Customers
from wuttaweb.views import MasterView
from wuttaweb.forms.schema import UserRef, WuttaEnum
from sideshow.db.model import PendingCustomer
from sideshow.db.model import LocalCustomer, 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):
@ -71,12 +225,9 @@ class PendingCustomerView(MasterView):
'customer_id',
'full_name',
'first_name',
'middle_name',
'last_name',
'phone_number',
'phone_type',
'email_address',
'email_type',
'status',
'created',
'created_by',
@ -238,6 +389,9 @@ class PendingCustomerView(MasterView):
def defaults(config, **kwargs):
base = globals()
LocalCustomerView = kwargs.get('LocalCustomerView', base['LocalCustomerView'])
LocalCustomerView.defaults(config)
PendingCustomerView = kwargs.get('PendingCustomerView', base['PendingCustomerView'])
PendingCustomerView.defaults(config)

View file

@ -31,11 +31,13 @@ import colander
from sqlalchemy import orm
from wuttaweb.views import MasterView
from wuttaweb.forms.schema import UserRef, WuttaMoney, WuttaQuantity, WuttaEnum
from wuttaweb.forms.schema import UserRef, WuttaMoney, WuttaQuantity, WuttaEnum, WuttaDictEnum
from sideshow.db.model import Order, OrderItem
from sideshow.batch.neworder import NewOrderBatchHandler
from sideshow.web.forms.schema import OrderRef, PendingCustomerRef, PendingProductRef
from sideshow.web.forms.schema import (OrderRef,
LocalCustomerRef, LocalProductRef,
PendingCustomerRef, PendingProductRef)
log = logging.getLogger(__name__)
@ -82,6 +84,7 @@ class OrderView(MasterView):
'order_id',
'store_id',
'customer_id',
'local_customer',
'pending_customer',
'customer_name',
'phone_number',
@ -165,6 +168,20 @@ class OrderView(MasterView):
batch, which in turn creates a true
:class:`~sideshow.db.model.orders.Order`, and user is
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
self.creating = True
@ -188,22 +205,25 @@ class OrderView(MasterView):
data = dict(self.request.json_body)
action = data.pop('action')
json_actions = [
# 'assign_contact',
# 'unassign_contact',
'assign_customer',
'unassign_customer',
# 'update_phone_number',
# 'update_email_address',
'set_pending_customer',
# 'get_customer_info',
# # 'set_customer_data',
# 'get_product_info',
'get_product_info',
# 'get_past_items',
'add_item',
'update_item',
'delete_item',
'submit_new_order',
'submit_order',
]
if action in json_actions:
try:
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({'error': "unknown form action"})
@ -215,7 +235,7 @@ class OrderView(MasterView):
for row in batch.rows],
'default_uom_choices': self.get_default_uom_choices(),
'default_uom': None, # TODO?
'allow_unknown_product': (self.batch_handler.allow_unknown_product()
'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(),
})
@ -255,6 +275,96 @@ class OrderView(MasterView):
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):
""" """
required = []
@ -274,7 +384,10 @@ class OrderView(MasterView):
new batch for them.
This is a "batch action" method which may be called from
:meth:`create()`.
:meth:`create()`. See also:
* :meth:`cancel_order()`
* :meth:`submit_order()`
"""
# drop current batch
self.batch_handler.do_delete(batch, self.request.user)
@ -291,7 +404,10 @@ class OrderView(MasterView):
back to "List Orders" page.
This is a "batch action" method which may be called from
:meth:`create()`.
:meth:`create()`. See also:
* :meth:`start_over()`
* :meth:`submit_order()`
"""
self.batch_handler.do_delete(batch, self.request.user)
self.Session.flush()
@ -306,86 +422,193 @@ class OrderView(MasterView):
def get_context_customer(self, batch):
""" """
context = {
'customer_id': batch.customer_id,
'customer_is_known': True,
'customer_id': None,
'customer_name': batch.customer_name,
'phone_number': batch.phone_number,
'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
if pending:
context.update({
'new_customer_first_name': pending.first_name,
'new_customer_last_name': pending.last_name,
'new_customer_name': pending.full_name,
'new_customer_full_name': pending.full_name,
'new_customer_phone': pending.phone_number,
'new_customer_email': pending.email_address,
})
# figure out if customer is "known" from user's perspective.
# if we have an ID then it's definitely known, otherwise if we
# have a pending customer then it's definitely *not* known,
# but if no pending customer yet then we can still "assume" it
# is known, by default, until user specifies otherwise.
if batch.customer_id:
context['customer_is_known'] = True
else:
context['customer_is_known'] = not pending
# declare customer "not known" only if pending is in use
if (pending
and not batch.customer_id and not batch.local_customer
and batch.customer_name):
context['customer_is_known'] = False
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):
"""
This will set/update the batch pending customer info.
This calls
:meth:`~sideshow.batch.neworder.NewOrderBatchHandler.set_pending_customer()`
: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:`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
:meth:`create()`.
"""
data['created_by'] = self.request.user
try:
self.batch_handler.set_pending_customer(batch, data)
except Exception as error:
return {'error': self.app.render_error(error)}
product_id = data.get('product_id')
if not product_id:
return {'error': "Must specify a product ID"}
self.Session.flush()
context = self.get_context_customer(batch)
return context
use_local = self.batch_handler.use_local_products()
if use_local:
data = self.get_local_product_info(product_id)
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):
"""
This adds a row to the user's current new order batch.
This is a "batch action" method which may be called from
:meth:`create()`.
:meth:`create()`. See also:
* :meth:`update_item()`
* :meth:`delete_item()`
"""
order_qty = decimal.Decimal(data.get('order_qty') or '0')
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)
row = self.batch_handler.add_item(batch, data['product_info'],
data['order_qty'], data['order_uom'])
return {'batch': self.normalize_batch(batch),
'row': self.normalize_row(row)}
@ -395,15 +618,17 @@ class OrderView(MasterView):
This updates a row in the user's current new order batch.
This is a "batch action" method which may be called from
:meth:`create()`.
:meth:`create()`. See also:
* :meth:`add_item()`
* :meth:`delete_item()`
"""
model = self.app.model
enum = self.app.enum
session = self.Session()
uuid = data.get('uuid')
if not uuid:
return {'error': "Must specify a row UUID"}
return {'error': "Must specify row UUID"}
row = session.get(model.NewOrderBatchRow, uuid)
if not row:
@ -412,20 +637,8 @@ class OrderView(MasterView):
if row.batch is not batch:
return {'error': "Row is for wrong batch"}
order_qty = decimal.Decimal(data.get('order_qty') or '0')
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'])
self.batch_handler.update_item(row, data['product_info'],
data['order_qty'], data['order_uom'])
return {'batch': self.normalize_batch(batch),
'row': self.normalize_row(row)}
@ -435,7 +648,10 @@ class OrderView(MasterView):
This deletes a row from the user's current new order batch.
This is a "batch action" method which may be called from
:meth:`create()`.
:meth:`create()`. See also:
* :meth:`add_item()`
* :meth:`update_item()`
"""
model = self.app.model
session = self.app.get_session(batch)
@ -452,16 +668,18 @@ class OrderView(MasterView):
return {'error': "Row is for wrong batch"}
self.batch_handler.do_remove_row(row)
session.flush()
return {'batch': self.normalize_batch(batch)}
def submit_new_order(self, batch, data):
def submit_order(self, batch, data):
"""
This submits the user's current new order batch, hence
executing the batch and creating the true order.
This is a "batch action" method which may be called from
:meth:`create()`.
:meth:`create()`. See also:
* :meth:`start_over()`
* :meth:`cancel_order()`
"""
user = self.request.user
reason = self.batch_handler.why_not_execute(batch, user=user)
@ -502,6 +720,7 @@ class OrderView(MasterView):
data = {
'uuid': row.uuid.hex,
'sequence': row.sequence,
'product_id': None,
'product_scancode': row.product_scancode,
'product_brand': row.product_brand,
'product_description': row.product_description,
@ -509,8 +728,8 @@ class OrderView(MasterView):
'product_weighed': row.product_weighed,
'department_display': row.department_name,
'special_order': row.special_order,
'case_size': self.app.render_quantity(row.case_size),
'order_qty': self.app.render_quantity(row.order_qty),
'case_size': float(row.case_size) if row.case_size is not None else None,
'order_qty': float(row.order_qty),
'order_uom': row.order_uom,
'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,
@ -523,6 +742,33 @@ class OrderView(MasterView):
'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:
data['unit_price_reg'] = float(row.unit_price_reg)
data['unit_price_reg_display'] = self.app.render_currency(row.unit_price_reg)
@ -535,21 +781,8 @@ class OrderView(MasterView):
data['sale_ends'] = str(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:
pending = row.pending_product
# data['vendor_display'] = pending.vendor_name
data['pending_product'] = {
'uuid': pending.uuid.hex,
'scancode': pending.scancode,
@ -569,14 +802,15 @@ class OrderView(MasterView):
# display text for order qty/uom
if row.order_uom == enum.ORDER_UOM_CASE:
order_qty = self.app.render_quantity(row.order_qty)
if row.case_size is None:
case_qty = unit_qty = '??'
else:
case_qty = data['case_size']
case_qty = self.app.render_quantity(row.case_size)
unit_qty = self.app.render_quantity(row.order_qty * row.case_size)
CS = enum.ORDER_UOM[enum.ORDER_UOM_CASE]
EA = enum.ORDER_UOM[enum.ORDER_UOM_UNIT]
data['order_qty_display'] = (f"{data['order_qty']} {CS} "
data['order_qty_display'] = (f"{order_qty} {CS} "
f"(&times; {case_qty} = {unit_qty} {EA})")
else:
unit_qty = self.app.render_quantity(row.order_qty)
@ -592,8 +826,15 @@ class OrderView(MasterView):
def configure_form(self, f):
""" """
super().configure_form(f)
order = f.model_instance
# local_customer
f.set_node('local_customer', LocalCustomerRef(self.request))
# pending_customer
if order.customer_id or order.local_customer:
f.remove('pending_customer')
else:
f.set_node('pending_customer', PendingCustomerRef(self.request))
# total_price
@ -672,7 +913,7 @@ class OrderView(MasterView):
settings = [
# products
{'name': 'sideshow.orders.allow_unknown_product',
{'name': 'sideshow.orders.allow_unknown_products',
'type': bool,
'default': True},
]
@ -702,7 +943,9 @@ class OrderView(MasterView):
@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()
@ -716,6 +959,24 @@ class OrderView(MasterView):
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):
"""
@ -745,7 +1006,8 @@ class OrderItemView(MasterView):
'product_brand': "Brand",
'product_description': "Description",
'product_size': "Size",
'department_name': "Department",
'product_weighed': "Sold by Weight",
'department_id': "Department ID",
'order_uom': "Order UOM",
'status_code': "Status",
}
@ -773,6 +1035,7 @@ class OrderItemView(MasterView):
# 'customer_name',
'sequence',
'product_id',
'local_product',
'pending_product',
'product_scancode',
'product_brand',
@ -852,27 +1115,46 @@ class OrderItemView(MasterView):
enum = self.app.enum
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):
""" """
super().configure_form(f)
enum = self.app.enum
item = f.model_instance
# order
f.set_node('order', OrderRef(self.request))
# local_product
f.set_node('local_product', LocalProductRef(self.request))
# pending_product
if item.product_id or item.local_product:
f.remove('pending_product')
else:
f.set_node('pending_product', PendingProductRef(self.request))
# order_qty
f.set_node('order_qty', WuttaQuantity(self.request))
# order_uom
# TODO
#f.set_node('order_uom', WuttaEnum(self.request, enum.OrderUOM))
f.set_node('order_uom', WuttaDictEnum(self.request, enum.ORDER_UOM))
# case_size
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
f.set_node('unit_price_quoted', WuttaMoney(self.request))
@ -882,18 +1164,21 @@ class OrderItemView(MasterView):
# total_price
f.set_node('total_price', WuttaMoney(self.request))
# status
f.set_node('status_code', WuttaDictEnum(self.request, enum.ORDER_ITEM_STATUS))
# paid_amount
f.set_node('paid_amount', WuttaMoney(self.request))
def get_xref_buttons(self, item):
""" """
buttons = super().get_xref_buttons(item)
model = self.app.model
if self.request.has_perm('orders.view'):
url = self.request.route_url('orders.view', uuid=item.order_uuid)
buttons.append(
self.make_button("View the Order", primary=True, icon_left='eye', url=url))
self.make_button("View the Order", url=url,
primary=True, icon_left='eye'))
return buttons

View file

@ -25,9 +25,194 @@ Views for Products
"""
from wuttaweb.views import MasterView
from wuttaweb.forms.schema import UserRef, WuttaEnum, WuttaMoney
from wuttaweb.forms.schema import UserRef, WuttaEnum, WuttaMoney, WuttaQuantity
from sideshow.db.model import PendingProduct
from sideshow.db.model import LocalProduct, 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):
@ -249,6 +434,9 @@ class PendingProductView(MasterView):
def defaults(config, **kwargs):
base = globals()
LocalProductView = kwargs.get('LocalProductView', base['LocalProductView'])
LocalProductView.defaults(config)
PendingProductView = kwargs.get('PendingProductView', base['PendingProductView'])
PendingProductView.defaults(config)

View file

@ -2,6 +2,7 @@
import datetime
import decimal
from unittest.mock import patch
from wuttjamaican.testing import DataTestCase
@ -19,71 +20,133 @@ class TestNewOrderBatchHandler(DataTestCase):
def make_handler(self):
return mod.NewOrderBatchHandler(self.config)
def tets_allow_unknown_product(self):
def tets_use_local_customers(self):
handler = self.make_handler()
# true by default
self.assertTrue(handler.allow_unknown_product())
self.assertTrue(handler.use_local_customers())
# config can disable
config.setdefault('sideshow.orders.allow_unknown_product', 'false')
self.assertFalse(handler.allow_unknown_product())
config.setdefault('sideshow.orders.use_local_customers', 'false')
self.assertFalse(handler.use_local_customers())
def test_set_pending_customer(self):
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
handler = self.make_handler()
user = model.User(username='barney')
self.session.add(user)
batch = handler.make_batch(self.session, created_by=user, customer_id=42)
self.assertEqual(batch.customer_id, 42)
# customer starts blank
batch = handler.make_batch(self.session, created_by=user)
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.customer_name)
self.assertIsNone(batch.phone_number)
self.assertIsNone(batch.email_address)
# auto full_name
handler.set_pending_customer(batch, {
# pending, typical (nb. full name is automatic)
handler.set_customer(batch, {
'first_name': "Fred",
'last_name': "Flintstone",
'phone_number': '555-1234',
'email_address': 'fred@mailinator.com',
})
self.assertIsNone(batch.customer_id)
self.assertIsNone(batch.local_customer)
self.assertIsInstance(batch.pending_customer, model.PendingCustomer)
customer = batch.pending_customer
self.assertEqual(customer.full_name, "Fred Flintstone")
self.assertEqual(customer.first_name, "Fred")
self.assertEqual(customer.last_name, "Flintstone")
self.assertEqual(customer.full_name, "Fred Flintstone")
self.assertEqual(customer.phone_number, '555-1234')
self.assertEqual(customer.email_address, 'fred@mailinator.com')
self.assertEqual(batch.customer_name, "Fred Flintstone")
self.assertEqual(batch.phone_number, '555-1234')
self.assertEqual(batch.email_address, 'fred@mailinator.com')
# explicit full_name
batch = handler.make_batch(self.session, created_by=user, customer_id=42)
handler.set_pending_customer(batch, {
'full_name': "Freddy Flintstone",
'first_name': "Fred",
'last_name': "Flintstone",
'phone_number': '555-1234',
'email_address': 'fred@mailinator.com',
})
# pending, minimal
last_customer = customer # save ref to prev record
handler.set_customer(batch, {'full_name': "Wilma Flintstone"})
self.assertIsNone(batch.customer_id)
self.assertIsNone(batch.local_customer)
self.assertIsInstance(batch.pending_customer, model.PendingCustomer)
customer = batch.pending_customer
self.assertEqual(customer.full_name, "Freddy Flintstone")
self.assertEqual(customer.first_name, "Fred")
self.assertEqual(customer.last_name, "Flintstone")
self.assertEqual(customer.phone_number, '555-1234')
self.assertEqual(customer.email_address, 'fred@mailinator.com')
self.assertEqual(batch.customer_name, "Freddy Flintstone")
self.assertEqual(batch.phone_number, '555-1234')
self.assertEqual(batch.email_address, 'fred@mailinator.com')
self.assertIs(customer, last_customer)
self.assertEqual(customer.full_name, "Wilma Flintstone")
self.assertIsNone(customer.first_name)
self.assertIsNone(customer.last_name)
self.assertIsNone(customer.phone_number)
self.assertIsNone(customer.email_address)
self.assertEqual(batch.customer_name, "Wilma Flintstone")
self.assertIsNone(batch.phone_number)
self.assertIsNone(batch.email_address)
def test_add_pending_product(self):
# local customer
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
enum = self.app.enum
handler = self.make_handler()
@ -95,144 +158,290 @@ class TestNewOrderBatchHandler(DataTestCase):
self.session.add(batch)
self.assertEqual(len(batch.rows), 0)
# pending, typical
kw = dict(
scancode='07430500132',
scancode='07430500001',
brand_name='Bragg',
description='Vinegar',
size='32oz',
size='1oz',
case_size=12,
unit_cost=decimal.Decimal('3.99'),
unit_price_reg=decimal.Decimal('5.99'),
created_by=user,
unit_cost=decimal.Decimal('1.99'),
unit_price_reg=decimal.Decimal('2.99'),
)
row = handler.add_pending_product(batch, kw, 1, enum.ORDER_UOM_UNIT)
row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_UNIT)
# nb. this is the first row in batch
self.assertEqual(len(batch.rows), 1)
self.assertIs(batch.rows[0], row)
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_quoted, decimal.Decimal('5.99'))
self.assertEqual(row.case_price_quoted, decimal.Decimal('71.88'))
self.assertIsNone(row.product_id)
self.assertIsNone(row.local_product)
product = row.pending_product
self.assertIsInstance(product, model.PendingProduct)
self.assertEqual(product.scancode, '07430500132')
self.assertEqual(product.scancode, '07430500001')
self.assertEqual(product.brand_name, 'Bragg')
self.assertEqual(product.description, 'Vinegar')
self.assertEqual(product.size, '32oz')
self.assertEqual(product.size, '1oz')
self.assertEqual(product.case_size, 12)
self.assertEqual(product.unit_cost, decimal.Decimal('3.99'))
self.assertEqual(product.unit_price_reg, decimal.Decimal('5.99'))
self.assertIs(product.created_by, user)
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.total_price, decimal.Decimal('2.99'))
# error if unknown products not allowed
self.config.setdefault('sideshow.orders.allow_unknown_product', 'false')
self.assertRaises(TypeError, handler.add_pending_product, batch, kw, 1, enum.ORDER_UOM_UNIT)
def test_set_pending_product(self):
model = self.app.model
enum = self.app.enum
handler = self.make_handler()
user = model.User(username='barney')
self.session.add(user)
batch = handler.make_batch(self.session, created_by=user)
self.session.add(batch)
self.assertEqual(len(batch.rows), 0)
# start with mock product_id
row = handler.make_row(product_id=42, order_qty=1, order_uom=enum.ORDER_UOM_UNIT)
handler.add_row(batch, row)
self.session.flush()
self.assertEqual(row.product_id, 42)
self.assertIsNone(row.pending_product)
# 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
self.assertIsInstance(product, model.PendingProduct)
self.assertIsNone(product.scancode)
self.assertIsNone(product.brand_name)
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.assertIsNone(row.product_description)
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)
# set pending, which clears product_id
handler.set_pending_product(row, dict(
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,
))
self.session.flush()
self.assertIsNone(row.product_id)
self.assertIsInstance(row.pending_product, model.PendingProduct)
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_quoted, decimal.Decimal('5.99'))
self.assertEqual(row.case_price_quoted, decimal.Decimal('71.88'))
product = row.pending_product
self.assertIsInstance(product, model.PendingProduct)
self.assertEqual(product.scancode, '07430500132')
self.assertEqual(product.brand_name, 'Bragg')
self.assertEqual(product.description, 'Vinegar')
self.assertEqual(product.size, '32oz')
self.assertEqual(product.case_size, 12)
self.assertEqual(product.unit_cost, decimal.Decimal('3.99'))
self.assertEqual(product.unit_price_reg, decimal.Decimal('5.99'))
self.assertIs(product.created_by, user)
# 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_size, '16oz')
self.assertEqual(row.case_size, 12)
self.assertEqual(row.unit_cost, decimal.Decimal('2.19'))
self.assertEqual(row.unit_price_reg, decimal.Decimal('3.59'))
self.assertEqual(row.unit_price_quoted, decimal.Decimal('3.59'))
self.assertEqual(row.case_price_quoted, decimal.Decimal('43.08'))
product = row.pending_product
self.assertIsInstance(product, model.PendingProduct)
self.assertEqual(product.scancode, '07430500116')
self.assertEqual(product.brand_name, 'Bragg')
self.assertEqual(product.description, 'Vinegar')
self.assertEqual(product.size, '16oz')
self.assertEqual(product.case_size, 12)
self.assertEqual(product.unit_cost, decimal.Decimal('2.19'))
self.assertEqual(product.unit_price_reg, decimal.Decimal('3.59'))
self.assertIs(product.created_by, user)
self.assertIsNone(row.case_price_quoted)
self.assertIsNone(row.total_price)
# error if unknown products not allowed
self.config.setdefault('sideshow.orders.allow_unknown_product', 'false')
self.assertRaises(TypeError, handler.set_pending_product, row, dict(
scancode='07430500116',
size='16oz',
unit_cost=decimal.Decimal('2.19'),
unit_price_reg=decimal.Decimal('3.59'),
))
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.size, '2oz')
self.assertEqual(product.case_size, 12)
self.assertIsNone(product.unit_cost)
self.assertEqual(product.unit_price_reg, decimal.Decimal('2.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('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
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
enum = self.app.enum
handler = self.make_handler()
user = model.User(username='barney')
self.session.add(user)
batch = handler.make_batch(self.session, created_by=user)
self.session.add(batch)
self.assertEqual(len(batch.rows), 0)
# start with typical pending product
kw = dict(
scancode='07430500001',
brand_name='Bragg',
description='Vinegar',
size='1oz',
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_brand)
self.assertEqual(row.product_description, 'Vinegar')
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.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
local = model.LocalProduct(scancode='07430500002',
description='Vinegar',
size='2oz',
unit_price_reg=3.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.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_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, 2)
self.assertEqual(row.order_uom, enum.ORDER_UOM_CASE)
self.assertEqual(row.total_price, decimal.Decimal('71.76'))
# 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.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/ local, not found
mock_uuid = self.app.make_true_uuid()
self.assertRaises(ValueError, handler.update_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.update_item,
row, '42', 1, enum.ORDER_UOM_CASE)
def test_refresh_row(self):
model = self.app.model
@ -387,7 +596,7 @@ class TestNewOrderBatchHandler(DataTestCase):
unit_price_reg=decimal.Decimal('5.99'),
created_by=user,
)
row = handler.add_pending_product(batch, kw, 1, enum.ORDER_UOM_CASE)
row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_CASE)
self.session.add(row)
self.session.flush()
self.assertEqual(batch.row_count, 1)
@ -423,25 +632,70 @@ class TestNewOrderBatchHandler(DataTestCase):
self.assertNotIn(batch, self.session)
self.assertEqual(self.session.query(model.PendingCustomer).count(), 0)
# make new pending customer
customer = model.PendingCustomer(full_name="Fred Flintstone",
# make new pending customer, assigned to batch + order
customer = model.PendingCustomer(full_name="Wilma Flintstone",
status=enum.PendingCustomerStatus.PENDING,
created_by=user)
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()
# make 2 batches with same pending customer
batch1 = handler.make_batch(self.session, created_by=user, pending_customer=customer)
batch2 = handler.make_batch(self.session, created_by=user, pending_customer=customer)
self.session.add(batch1)
self.session.add(batch2)
# deleting batch will *not* delete pending customer
self.assertIn(batch, self.session)
self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
handler.do_delete(batch, user)
self.session.commit()
self.assertNotIn(batch, self.session)
self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
# deleting 1 will not delete pending customer
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.assertNotIn(batch, self.session)
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):
model = self.app.model
@ -492,6 +746,12 @@ class TestNewOrderBatchHandler(DataTestCase):
self.assertEqual(reason, "Must assign the customer")
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)
self.assertEqual(reason, "Must add at least one valid item")
@ -506,13 +766,206 @@ class TestNewOrderBatchHandler(DataTestCase):
unit_price_reg=decimal.Decimal('5.99'),
created_by=user,
)
row = handler.add_pending_product(batch, kw, 1, enum.ORDER_UOM_CASE)
row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_CASE)
self.session.add(row)
self.session.flush()
reason = handler.why_not_execute(batch)
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):
model = self.app.model
enum = self.app.enum
@ -534,7 +987,7 @@ class TestNewOrderBatchHandler(DataTestCase):
unit_price_reg=decimal.Decimal('5.99'),
created_by=user,
)
row = handler.add_pending_product(batch, kw, 1, enum.ORDER_UOM_CASE)
row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_CASE)
self.session.add(row)
self.session.flush()
@ -574,7 +1027,7 @@ class TestNewOrderBatchHandler(DataTestCase):
unit_price_reg=decimal.Decimal('5.99'),
created_by=user,
)
row = handler.add_pending_product(batch, kw, 1, enum.ORDER_UOM_CASE)
row = handler.add_item(batch, kw, 1, enum.ORDER_UOM_CASE)
self.session.add(row)
self.session.flush()

View file

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

View file

@ -32,6 +32,31 @@ class TestOrderRef(WebTestCase):
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):
def test_sort_query(self):
@ -60,6 +85,31 @@ class TestPendingCustomerRef(WebTestCase):
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):
def test_sort_query(self):

View file

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

View file

@ -16,6 +16,114 @@ class TestIncludeme(WebTestCase):
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):
def make_view(self):

View file

@ -13,7 +13,7 @@ from wuttaweb.forms.schema import WuttaMoney
from sideshow.batch.neworder import NewOrderBatchHandler
from sideshow.testing import WebTestCase
from sideshow.web.views import orders as mod
from sideshow.web.forms.schema import OrderRef
from sideshow.web.forms.schema import OrderRef, PendingProductRef
class TestIncludeme(WebTestCase):
@ -27,6 +27,9 @@ class TestOrderView(WebTestCase):
def make_view(self):
return mod.OrderView(self.request)
def make_handler(self):
return NewOrderBatchHandler(self.config)
def test_configure_grid(self):
model = self.app.model
view = self.make_view()
@ -40,6 +43,7 @@ class TestOrderView(WebTestCase):
def test_create(self):
self.pyramid_config.include('sideshow.web.views')
model = self.app.model
enum = self.app.enum
view = self.make_view()
user = model.User(username='barney')
@ -91,7 +95,7 @@ class TestOrderView(WebTestCase):
'customer_name': 'Fred Flintstone',
'phone_number': '555-1234',
'email_address': 'fred@mailinator.com',
'new_customer_name': 'Fred Flintstone',
'new_customer_full_name': 'Fred Flintstone',
'new_customer_first_name': 'Fred',
'new_customer_last_name': 'Flintstone',
'new_customer_phone': '555-1234',
@ -108,6 +112,40 @@ class TestOrderView(WebTestCase):
self.assertEqual(response.content_type, 'application/json')
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):
model = self.app.model
handler = NewOrderBatchHandler(self.config)
@ -137,6 +175,75 @@ class TestOrderView(WebTestCase):
self.assertEqual(self.session.query(model.NewOrderBatch).count(), 1)
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):
model = self.app.model
view = self.make_view()
@ -158,11 +265,13 @@ class TestOrderView(WebTestCase):
model = self.app.model
handler = NewOrderBatchHandler(self.config)
view = self.make_view()
view.batch_handler = handler
user = model.User(username='barney')
self.session.add(user)
# with true customer
# with external 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')
@ -175,21 +284,32 @@ class TestOrderView(WebTestCase):
'customer_name': 'Fred Flintstone',
'phone_number': '555-1234',
'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 local customer
local = model.LocalCustomer(full_name="Betty Boop")
self.session.add(local)
batch = handler.make_batch(self.session, created_by=user,
local_customer=local, customer_name='Betty Boop',
phone_number='555-8888')
self.session.add(batch)
self.session.flush()
context = view.get_context_customer(batch)
self.assertEqual(context, {
'customer_is_known': True,
'customer_id': local.uuid.hex,
'customer_name': 'Betty Boop',
'phone_number': '555-8888',
'email_address': None,
})
# with pending customer
batch = handler.make_batch(self.session, created_by=user)
self.session.add(batch)
handler.set_pending_customer(batch, dict(
handler.set_customer(batch, dict(
full_name="Fred Flintstone",
first_name="Fred", last_name="Flintstone",
phone_number='555-1234', email_address='fred@mailinator.com',
created_by=user,
))
self.session.flush()
context = view.get_context_customer(batch)
@ -199,7 +319,7 @@ class TestOrderView(WebTestCase):
'customer_name': 'Fred Flintstone',
'phone_number': '555-1234',
'email_address': 'fred@mailinator.com',
'new_customer_name': 'Fred Flintstone',
'new_customer_full_name': 'Fred Flintstone',
'new_customer_first_name': 'Fred',
'new_customer_last_name': 'Flintstone',
'new_customer_phone': '555-1234',
@ -217,11 +337,6 @@ class TestOrderView(WebTestCase):
'customer_name': None,
'phone_number': 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):
@ -268,6 +383,80 @@ class TestOrderView(WebTestCase):
self.session.flush()
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):
self.pyramid_config.add_route('orders.create', '/orders/new')
model = self.app.model
@ -301,19 +490,58 @@ class TestOrderView(WebTestCase):
'customer_name': 'Fred Flintstone',
'phone_number': '555-1234',
'email_address': 'fred@mailinator.com',
'new_customer_name': 'Fred Flintstone',
'new_customer_full_name': 'Fred Flintstone',
'new_customer_first_name': 'Fred',
'new_customer_last_name': 'Flintstone',
'new_customer_phone': '555-1234',
'new_customer_email': 'fred@mailinator.com',
})
# error
with patch.object(handler, 'set_pending_customer', side_effect=RuntimeError):
context = view.set_pending_customer(batch, data)
self.assertEqual(context, {
'error': 'RuntimeError',
})
def test_get_product_info(self):
model = self.app.model
handler = self.make_handler()
view = self.make_view()
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):
model = self.app.model
@ -326,7 +554,7 @@ class TestOrderView(WebTestCase):
self.session.commit()
data = {
'pending_product': {
'product_info': {
'scancode': '07430500132',
'brand_name': 'Bragg',
'description': 'Vinegar',
@ -353,15 +581,9 @@ class TestOrderView(WebTestCase):
row = batch.rows[0]
self.assertIsInstance(row.pending_product, model.PendingProduct)
# pending w/ invalid price
with patch.dict(data['pending_product'], unit_price_reg='invalid'):
result = 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):
# external product not yet supported
with patch.object(handler, 'use_local_products', return_value=False):
with patch.dict(data, product_info='42'):
self.assertRaises(NotImplementedError, view.add_item, batch, data)
def test_update_item(self):
@ -375,7 +597,7 @@ class TestOrderView(WebTestCase):
self.session.commit()
data = {
'pending_product': {
'product_info': {
'scancode': '07430500132',
'brand_name': 'Bragg',
'description': 'Vinegar',
@ -403,7 +625,7 @@ class TestOrderView(WebTestCase):
# missing row uuid
result = view.update_item(batch, data)
self.assertEqual(result, {'error': "Must specify a row UUID"})
self.assertEqual(result, {'error': "Must specify row UUID"})
# row not found
with patch.dict(data, uuid=self.app.make_true_uuid()):
@ -420,16 +642,18 @@ class TestOrderView(WebTestCase):
result = view.update_item(batch, data)
self.assertEqual(result, {'error': "Row is for wrong batch"})
# set row for remaining tests
data['uuid'] = row.uuid
# true product not yet supported
with patch.dict(data, product_is_known=True):
self.assertRaises(NotImplementedError, view.update_item, batch, data)
with patch.object(handler, 'use_local_products', return_value=False):
self.assertRaises(NotImplementedError, view.update_item, batch, {
'uuid': row.uuid,
'product_info': '42',
'order_qty': 1,
'order_uom': enum.ORDER_UOM_UNIT,
})
# update row, pending product
with patch.dict(data, order_qty=2):
with patch.dict(data['pending_product'], scancode='07430500116'):
with patch.dict(data, uuid=row.uuid, order_qty=2):
with patch.dict(data['product_info'], scancode='07430500116'):
self.assertEqual(row.product_scancode, '07430500132')
self.assertEqual(row.order_qty, 1)
result = view.update_item(batch, data)
@ -438,7 +662,7 @@ class TestOrderView(WebTestCase):
self.assertEqual(row.order_qty, 2)
self.assertEqual(row.pending_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):
model = self.app.model
@ -451,7 +675,7 @@ class TestOrderView(WebTestCase):
self.session.commit()
data = {
'pending_product': {
'product_info': {
'scancode': '07430500132',
'brand_name': 'Bragg',
'description': 'Vinegar',
@ -506,7 +730,7 @@ class TestOrderView(WebTestCase):
self.assertEqual(len(batch.rows), 0)
self.assertEqual(batch.row_count, 0)
def test_submit_new_order(self):
def test_submit_order(self):
self.pyramid_config.add_route('orders.view', '/orders/{uuid}')
model = self.app.model
enum = self.app.enum
@ -518,7 +742,7 @@ class TestOrderView(WebTestCase):
self.session.commit()
data = {
'pending_product': {
'product_info': {
'scancode': '07430500132',
'brand_name': 'Bragg',
'description': 'Vinegar',
@ -534,28 +758,33 @@ class TestOrderView(WebTestCase):
with patch.object(view, 'Session', return_value=self.session):
with patch.object(self.request, 'user', new=user):
batch = view.get_current_batch()
self.session.flush()
self.assertEqual(len(batch.rows), 0)
# add row w/ pending product
view.add_item(batch, data)
self.session.flush()
self.assertEqual(len(batch.rows), 1)
row = batch.rows[0]
self.assertIsInstance(row.pending_product, model.PendingProduct)
self.assertEqual(row.unit_price_quoted, decimal.Decimal('5.99'))
# execute not allowed yet (no customer)
result = view.submit_new_order(batch, {})
result = view.submit_order(batch, {})
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
batch.customer_id = 42
result = view.submit_new_order(batch, {})
view.set_pending_customer(batch, {'full_name': 'John Doe',
'phone_number': '555-1234'})
result = view.submit_order(batch, {})
self.assertEqual(sorted(result), ['next_url'])
self.assertIn('/orders/', result['next_url'])
# error (already executed)
result = view.submit_new_order(batch, {})
result = view.submit_order(batch, {})
self.assertEqual(result, {
'error': f"ValueError: batch has already been executed: {batch}",
})
@ -585,9 +814,8 @@ class TestOrderView(WebTestCase):
'size': '32oz',
'unit_price_reg': 5.99,
'case_size': 12,
'created_by': user,
}
row = handler.add_pending_product(batch, pending, 1, enum.ORDER_UOM_CASE)
row = handler.add_item(batch, pending, 1, enum.ORDER_UOM_CASE)
self.session.commit()
data = view.normalize_batch(batch)
@ -604,11 +832,15 @@ class TestOrderView(WebTestCase):
enum = self.app.enum
handler = NewOrderBatchHandler(self.config)
view = self.make_view()
view.batch_handler = handler
user = model.User(username='barney')
self.session.add(user)
batch = handler.make_batch(self.session, created_by=user)
self.session.add(batch)
self.session.flush()
# add 1st row w/ pending product
pending = {
'scancode': '07430500132',
'brand_name': 'Bragg',
@ -616,19 +848,22 @@ class TestOrderView(WebTestCase):
'size': '32oz',
'unit_price_reg': 5.99,
'case_size': 12,
'created_by': user,
'vendor_name': 'Acme Warehouse',
'vendor_item_code': '1234',
}
row = handler.add_pending_product(batch, pending, 2, enum.ORDER_UOM_CASE)
self.session.commit()
row1 = handler.add_item(batch, pending, 2, enum.ORDER_UOM_CASE)
# normal
data = view.normalize_row(row)
# typical, pending product
data = view.normalize_row(row1)
self.assertIsInstance(data, dict)
self.assertEqual(data['uuid'], row.uuid.hex)
self.assertEqual(data['uuid'], row1.uuid.hex)
self.assertEqual(data['sequence'], 1)
self.assertIsNone(data['product_id'])
self.assertEqual(data['product_scancode'], '07430500132')
self.assertEqual(data['case_size'], '12')
self.assertEqual(data['order_qty'], '2')
self.assertEqual(data['product_full_description'], 'Bragg Vinegar 32oz')
self.assertEqual(data['case_size'], 12)
self.assertEqual(data['vendor_name'], 'Acme Warehouse')
self.assertEqual(data['order_qty'], 2)
self.assertEqual(data['order_uom'], 'CS')
self.assertEqual(data['order_qty_display'], '2 Cases (&times; 12 = 24 Units)')
self.assertEqual(data['unit_price_reg'], 5.99)
@ -644,9 +879,9 @@ class TestOrderView(WebTestCase):
self.assertEqual(data['total_price'], 143.76)
self.assertEqual(data['total_price_display'], '$143.76')
self.assertIsNone(data['special_order'])
self.assertEqual(data['status_code'], row.STATUS_OK)
self.assertEqual(data['status_code'], row1.STATUS_OK)
self.assertEqual(data['pending_product'], {
'uuid': row.pending_product_uuid.hex,
'uuid': row1.pending_product_uuid.hex,
'scancode': '07430500132',
'brand_name': 'Bragg',
'description': 'Vinegar',
@ -654,44 +889,117 @@ class TestOrderView(WebTestCase):
'department_id': None,
'department_name': None,
'unit_price_reg': 5.99,
'vendor_name': None,
'vendor_item_code': None,
'vendor_name': 'Acme Warehouse',
'vendor_item_code': '1234',
'unit_cost': None,
'case_size': 12.0,
'notes': None,
'special_order': None,
})
# the next few tests will morph 1st row..
# unknown case size
row.pending_product.case_size = None
handler.refresh_row(row)
row1.pending_product.case_size = None
handler.refresh_row(row1)
self.session.flush()
data = view.normalize_row(row)
data = view.normalize_row(row1)
self.assertIsNone(data['case_size'])
self.assertEqual(data['order_qty_display'], '2 Cases (&times; ?? = ?? Units)')
# order by unit
row.order_uom = enum.ORDER_UOM_UNIT
handler.refresh_row(row)
row1.order_uom = enum.ORDER_UOM_UNIT
handler.refresh_row(row1)
self.session.flush()
data = view.normalize_row(row)
data = view.normalize_row(row1)
self.assertEqual(data['order_uom'], enum.ORDER_UOM_UNIT)
self.assertEqual(data['order_qty_display'], '2 Units')
# item on sale
row.pending_product.case_size = 12
row.unit_price_sale = decimal.Decimal('5.19')
row.sale_ends = datetime.datetime(2025, 1, 5, 20, 32)
handler.refresh_row(row, now=datetime.datetime(2025, 1, 5, 19))
row1.pending_product.case_size = 12
row1.unit_price_sale = decimal.Decimal('5.19')
row1.sale_ends = datetime.datetime(2099, 1, 5, 20, 32)
handler.refresh_row(row1)
self.session.flush()
data = view.normalize_row(row)
data = view.normalize_row(row1)
self.assertEqual(data['unit_price_sale'], 5.19)
self.assertEqual(data['unit_price_sale_display'], '$5.19')
self.assertEqual(data['sale_ends'], '2025-01-05 20:32:00')
self.assertEqual(data['sale_ends_display'], '2025-01-05')
self.assertEqual(data['sale_ends'], '2099-01-05 20:32:00')
self.assertEqual(data['sale_ends_display'], '2099-01-05')
self.assertEqual(data['unit_price_quoted'], 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_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):
model = self.app.model
view = self.make_view()
@ -715,13 +1023,31 @@ class TestOrderView(WebTestCase):
self.session.add(order)
self.session.commit()
# viewing
# viewing (no 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)
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)
def test_get_xref_buttons(self):
@ -831,6 +1157,46 @@ class TestOrderView(WebTestCase):
url = view.get_row_action_url_view(item, 0)
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):
@ -864,6 +1230,18 @@ class TestOrderItemView(WebTestCase):
self.assertEqual(view.render_status_code(None, None, enum.ORDER_ITEM_STATUS_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):
model = self.app.model
enum = self.app.enum
@ -871,12 +1249,24 @@ class TestOrderItemView(WebTestCase):
item = model.OrderItem(status_code=enum.ORDER_ITEM_STATUS_INITIATED)
# viewing
# viewing, w/ pending product
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.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):
self.pyramid_config.add_route('orders.view', '/orders/{uuid}')

View file

@ -16,6 +16,104 @@ class TestIncludeme(WebTestCase):
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):
def make_view(self):