feat: add basic support for local customer, product lookups
also convert pending to local (where relevant) when executing batch
This commit is contained in:
parent
ebd22fe6ee
commit
a4ad23c7fa
|
@ -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`.
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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="""
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -7,15 +7,15 @@
|
|||
<div class="block" style="padding-left: 2rem;">
|
||||
|
||||
<b-field message="If set, user can enter details of an arbitrary new "pending" 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">
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"(× {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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 (× 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 (× ?? = ?? 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}')
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in a new issue