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::
|
.. glossary::
|
||||||
:sorted:
|
:sorted:
|
||||||
|
|
||||||
|
external customer
|
||||||
|
A customer account from an external system. Sideshow can be
|
||||||
|
configured to lookup customer data from external system(s) when
|
||||||
|
creating an :term:`order`.
|
||||||
|
|
||||||
|
See also :term:`local customer` and :term:`pending customer`.
|
||||||
|
|
||||||
|
external product
|
||||||
|
A product record from an external system. Sideshow can be
|
||||||
|
configured to lookup customer data from external system(s) when
|
||||||
|
creating an :term:`order`.
|
||||||
|
|
||||||
|
See also :term:`local product` and :term:`pending product`.
|
||||||
|
|
||||||
|
local customer
|
||||||
|
A customer account in the :term:`app database`. By default,
|
||||||
|
Sideshow will use its native "Local Customers" table for lookup
|
||||||
|
when creating an :term:`order`.
|
||||||
|
|
||||||
|
The data model for this is
|
||||||
|
:class:`~sideshow.db.model.customers.LocalCustomer`.
|
||||||
|
|
||||||
|
See also :term:`external customer` and :term:`pending customer`.
|
||||||
|
|
||||||
|
local product
|
||||||
|
A product record in the :term:`app database`. By default,
|
||||||
|
Sideshow will use its native "Local Products" table for lookup
|
||||||
|
when creating an :term:`order`.
|
||||||
|
|
||||||
|
The data model for this is
|
||||||
|
:class:`~sideshow.db.model.products.LocalProduct`.
|
||||||
|
|
||||||
|
See also :term:`external product` and :term:`pending product`.
|
||||||
|
|
||||||
|
new order batch
|
||||||
|
When user is creating a new order, under the hood a :term:`batch`
|
||||||
|
is employed to keep track of user input. When user ultimately
|
||||||
|
"submits" the order, the batch is executed which creates a true
|
||||||
|
:term:`order`.
|
||||||
|
|
||||||
order
|
order
|
||||||
This is the central focus of the app; it refers to a customer
|
This is the central focus of the app; it refers to a customer
|
||||||
case/special order which is tracked over time, from placement to
|
case/special order which is tracked over time, from placement to
|
||||||
|
@ -20,17 +60,19 @@ Glossary
|
||||||
sibling items.
|
sibling items.
|
||||||
|
|
||||||
pending customer
|
pending customer
|
||||||
Generally refers to a "new / unknown" customer, e.g. for whom a
|
A "temporary" customer record used when creating an :term:`order`
|
||||||
new order is being created. This allows the order lifecycle to
|
for new/unknown customer.
|
||||||
get going before the customer has a proper account in the system.
|
|
||||||
|
|
||||||
See :class:`~sideshow.db.model.customers.PendingCustomer` for the
|
The data model for this is
|
||||||
data model.
|
:class:`~sideshow.db.model.customers.PendingCustomer`.
|
||||||
|
|
||||||
|
See also :term:`external customer` and :term:`pending customer`.
|
||||||
|
|
||||||
pending product
|
pending product
|
||||||
Generally refers to a "new / unknown" product, e.g. for which a
|
A "temporary" product record used when creating an :term:`order`
|
||||||
new order is being created. This allows the order lifecycle to
|
for new/unknown product.
|
||||||
get going before the product has a true record in the system.
|
|
||||||
|
|
||||||
See :class:`~sideshow.db.model.products.PendingProduct` for the
|
The data model for this is
|
||||||
data model.
|
: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 datetime
|
||||||
import decimal
|
import decimal
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from wuttjamaican.batch import BatchHandler
|
from wuttjamaican.batch import BatchHandler
|
||||||
|
|
||||||
from sideshow.db.model import NewOrderBatch
|
from sideshow.db.model import NewOrderBatch
|
||||||
|
@ -34,7 +36,8 @@ from sideshow.db.model import NewOrderBatch
|
||||||
|
|
||||||
class NewOrderBatchHandler(BatchHandler):
|
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
|
This is responsible for business logic around the creation of new
|
||||||
:term:`orders <order>`. A
|
:term:`orders <order>`. A
|
||||||
|
@ -44,149 +47,195 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
"""
|
"""
|
||||||
model_class = NewOrderBatch
|
model_class = NewOrderBatch
|
||||||
|
|
||||||
def allow_unknown_product(self):
|
def use_local_customers(self):
|
||||||
"""
|
"""
|
||||||
Returns a boolean indicating whether "unknown" (pending)
|
Returns boolean indicating whether :term:`local customer`
|
||||||
products are allowed when creating a new order.
|
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)
|
default=True)
|
||||||
|
|
||||||
def set_pending_customer(self, batch, data):
|
def use_local_products(self):
|
||||||
"""
|
"""
|
||||||
Set (add or update) pending customer info for the batch.
|
Returns boolean indicating whether :term:`local product`
|
||||||
|
records should be used. This is true by default, but may be
|
||||||
This will clear the
|
false for :term:`external product` lookups.
|
||||||
: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.
|
|
||||||
"""
|
"""
|
||||||
model = self.app.model
|
return self.config.get_bool('sideshow.orders.use_local_products',
|
||||||
enum = self.app.enum
|
default=True)
|
||||||
|
|
||||||
# remove customer account if set
|
def allow_unknown_products(self):
|
||||||
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):
|
|
||||||
"""
|
"""
|
||||||
Add a new row to the batch, for the given "pending" product
|
Returns boolean indicating whether :term:`pending products
|
||||||
and order quantity.
|
<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:
|
:param batch:
|
||||||
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch` to
|
: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
|
:param customer_info: Customer ID string, or dict of
|
||||||
new :class:`~sideshow.db.model.products.PendingProduct`.
|
: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
|
:param user:
|
||||||
order.
|
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
|
||||||
|
is performing the action. This is used to set
|
||||||
:param order_uom: UOM for the order quantity; must be a code
|
:attr:`~sideshow.db.model.customers.PendingCustomer.created_by`
|
||||||
from :data:`~sideshow.enum.ORDER_UOM`.
|
on the pending customer, if applicable. If not specified,
|
||||||
|
the batch creator is assumed.
|
||||||
:returns:
|
|
||||||
:class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
|
|
||||||
which was added to the batch.
|
|
||||||
"""
|
"""
|
||||||
if not self.allow_unknown_product():
|
|
||||||
raise TypeError("unknown/pending product not allowed for new orders")
|
|
||||||
|
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
session = self.app.get_session(batch)
|
session = self.app.get_session(batch)
|
||||||
|
use_local = self.use_local_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()
|
session.flush()
|
||||||
# nb. this may convert float to decimal etc.
|
|
||||||
session.refresh(product)
|
|
||||||
|
|
||||||
# make/add new row, w/ pending product
|
def add_item(self, batch, product_info, order_qty, order_uom, user=None):
|
||||||
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):
|
|
||||||
"""
|
"""
|
||||||
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
|
See also :meth:`update_item()`.
|
||||||
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.product_id`
|
|
||||||
and set the
|
|
||||||
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.pending_product`,
|
|
||||||
creating a new record if needed. It then updates the pending
|
|
||||||
product record per the given ``data``, and finally calls
|
|
||||||
:meth:`refresh_row()`.
|
|
||||||
|
|
||||||
Note that this does not update order quantity for the item.
|
:param batch:
|
||||||
|
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch` to
|
||||||
|
update.
|
||||||
|
|
||||||
See also :meth:`add_pending_product()` to add a new row
|
:param product_info: Product ID string, or dict of
|
||||||
instead of updating.
|
: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`
|
:class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
|
||||||
to be updated.
|
instance.
|
||||||
|
|
||||||
:param data: Dict of field data for the
|
|
||||||
:class:`~sideshow.db.model.products.PendingProduct` record.
|
|
||||||
"""
|
"""
|
||||||
if not self.allow_unknown_product():
|
|
||||||
raise TypeError("unknown/pending product not allowed for new orders")
|
|
||||||
|
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
session = self.app.get_session(row)
|
session = self.app.get_session(batch)
|
||||||
|
use_local = self.use_local_products()
|
||||||
|
row = self.make_row()
|
||||||
|
|
||||||
# values for these fields can be used as-is
|
# set product info
|
||||||
simple_fields = [
|
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',
|
'scancode',
|
||||||
'brand_name',
|
'brand_name',
|
||||||
'description',
|
'description',
|
||||||
|
@ -197,49 +246,134 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
'special_order',
|
'special_order',
|
||||||
'vendor_name',
|
'vendor_name',
|
||||||
'vendor_item_code',
|
'vendor_item_code',
|
||||||
'notes',
|
|
||||||
'unit_cost',
|
|
||||||
'case_size',
|
'case_size',
|
||||||
'case_cost',
|
'unit_cost',
|
||||||
'unit_price_reg',
|
'unit_price_reg',
|
||||||
|
'notes',
|
||||||
]
|
]
|
||||||
|
for key in fields:
|
||||||
|
setattr(pending, key, product_info.get(key))
|
||||||
|
|
||||||
# clear true product id
|
# nb. this may convert float to decimal etc.
|
||||||
row.product_id = None
|
session.add(pending)
|
||||||
|
|
||||||
# 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
|
|
||||||
session.flush()
|
session.flush()
|
||||||
|
session.refresh(pending)
|
||||||
|
row.pending_product = pending
|
||||||
|
|
||||||
# update pending product
|
# set order info
|
||||||
for field in simple_fields:
|
row.order_qty = order_qty
|
||||||
if field in data:
|
row.order_uom = order_uom
|
||||||
setattr(product, field, data[field])
|
|
||||||
|
# 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.
|
# nb. this may convert float to decimal etc.
|
||||||
session.flush()
|
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
|
# refresh per new info
|
||||||
self.refresh_row(row)
|
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
|
Refresh data for the row. This is called when adding a new
|
||||||
new row to the batch, or anytime the row is updated (e.g. when
|
row to the batch, or anytime the row is updated (e.g. when
|
||||||
changing order quantity).
|
changing order quantity).
|
||||||
|
|
||||||
This calls one of the following to update product-related
|
This calls one of the following to update product-related
|
||||||
attributes 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_pending_product()`
|
||||||
* :meth:`refresh_row_from_true_product()`
|
|
||||||
|
|
||||||
It then re-calculates the row's
|
It then re-calculates the row's
|
||||||
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.total_price`
|
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.total_price`
|
||||||
|
@ -253,7 +387,7 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
row.status_text = None
|
row.status_text = None
|
||||||
|
|
||||||
# ensure product
|
# 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
|
row.status_code = row.STATUS_MISSING_PRODUCT
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -264,7 +398,9 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
|
|
||||||
# update product attrs on row
|
# update product attrs on row
|
||||||
if row.product_id:
|
if row.product_id:
|
||||||
self.refresh_row_from_true_product(row)
|
self.refresh_row_from_external_product(row)
|
||||||
|
elif row.local_product:
|
||||||
|
self.refresh_row_from_local_product(row)
|
||||||
else:
|
else:
|
||||||
self.refresh_row_from_pending_product(row)
|
self.refresh_row_from_pending_product(row)
|
||||||
|
|
||||||
|
@ -276,7 +412,7 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
row.case_price_quoted = None
|
row.case_price_quoted = None
|
||||||
if row.unit_price_sale is not None and (
|
if row.unit_price_sale is not None and (
|
||||||
not row.sale_ends
|
not row.sale_ends
|
||||||
or row.sale_ends > (now or datetime.datetime.now())):
|
or row.sale_ends > datetime.datetime.now()):
|
||||||
row.unit_price_quoted = row.unit_price_sale
|
row.unit_price_quoted = row.unit_price_sale
|
||||||
else:
|
else:
|
||||||
row.unit_price_quoted = row.unit_price_reg
|
row.unit_price_quoted = row.unit_price_reg
|
||||||
|
@ -304,16 +440,15 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
# all ok
|
# all ok
|
||||||
row.status_code = row.STATUS_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
|
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.
|
record.
|
||||||
|
|
||||||
This is called automatically from :meth:`refresh_row()`.
|
This is called automatically from :meth:`refresh_row()`.
|
||||||
"""
|
"""
|
||||||
product = row.pending_product
|
product = row.local_product
|
||||||
|
|
||||||
row.product_scancode = product.scancode
|
row.product_scancode = product.scancode
|
||||||
row.product_brand = product.brand_name
|
row.product_brand = product.brand_name
|
||||||
row.product_description = product.description
|
row.product_description = product.description
|
||||||
|
@ -326,10 +461,31 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
row.unit_cost = product.unit_cost
|
row.unit_cost = product.unit_cost
|
||||||
row.unit_price_reg = product.unit_price_reg
|
row.unit_price_reg = product.unit_price_reg
|
||||||
|
|
||||||
def refresh_row_from_true_product(self, row):
|
def refresh_row_from_pending_product(self, row):
|
||||||
"""
|
"""
|
||||||
Update product-related attributes on the row, from its "true"
|
Update product-related attributes on the row, from its
|
||||||
product record indicated by
|
: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`.
|
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.product_id`.
|
||||||
|
|
||||||
This is called automatically from :meth:`refresh_row()`.
|
This is called automatically from :meth:`refresh_row()`.
|
||||||
|
@ -354,31 +510,39 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
|
|
||||||
def do_delete(self, batch, user, **kwargs):
|
def do_delete(self, batch, user, **kwargs):
|
||||||
"""
|
"""
|
||||||
Delete the given batch entirely.
|
Delete a batch completely.
|
||||||
|
|
||||||
If the batch has a
|
If the batch has :term:`pending customer` or :term:`pending
|
||||||
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`
|
product` records, they are also deleted - unless still
|
||||||
record, that is deleted also.
|
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 = 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
|
# continue with normal deletion
|
||||||
super().do_delete(batch, user, **kwargs)
|
super().do_delete(batch, user, **kwargs)
|
||||||
|
|
||||||
def why_not_execute(self, batch, **kwargs):
|
def why_not_execute(self, batch, **kwargs):
|
||||||
"""
|
"""
|
||||||
By default this checks to ensure the batch has a customer and
|
By default this checks to ensure the batch has a customer with
|
||||||
at least one item.
|
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"
|
return "Must assign the customer"
|
||||||
|
|
||||||
|
if not batch.phone_number:
|
||||||
|
return "Customer phone number is required"
|
||||||
|
|
||||||
rows = self.get_effective_rows(batch)
|
rows = self.get_effective_rows(batch)
|
||||||
if not rows:
|
if not rows:
|
||||||
return "Must add at least one valid item"
|
return "Must add at least one valid item"
|
||||||
|
@ -395,17 +559,113 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
|
|
||||||
def execute(self, batch, user=None, progress=None, **kwargs):
|
def execute(self, batch, user=None, progress=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
By default, this will call :meth:`make_new_order()` and return
|
Execute the batch; this should make a proper :term:`order`.
|
||||||
the new :class:`~sideshow.db.model.orders.Order` instance.
|
|
||||||
|
By default, this will call:
|
||||||
|
|
||||||
|
* :meth:`make_local_customer()`
|
||||||
|
* :meth:`make_local_products()`
|
||||||
|
* :meth:`make_new_order()`
|
||||||
|
|
||||||
|
And will return the new
|
||||||
|
:class:`~sideshow.db.model.orders.Order` instance.
|
||||||
|
|
||||||
Note that callers should use
|
Note that callers should use
|
||||||
:meth:`~wuttjamaican:wuttjamaican.batch.BatchHandler.do_execute()`
|
:meth:`~wuttjamaican:wuttjamaican.batch.BatchHandler.do_execute()`
|
||||||
instead, which calls this method automatically.
|
instead, which calls this method automatically.
|
||||||
"""
|
"""
|
||||||
rows = self.get_effective_rows(batch)
|
rows = self.get_effective_rows(batch)
|
||||||
|
self.make_local_customer(batch)
|
||||||
|
self.make_local_products(batch, rows)
|
||||||
order = self.make_new_order(batch, rows, user=user, progress=progress, **kwargs)
|
order = self.make_new_order(batch, rows, user=user, progress=progress, **kwargs)
|
||||||
return order
|
return order
|
||||||
|
|
||||||
|
def make_local_customer(self, batch):
|
||||||
|
"""
|
||||||
|
If applicable, this converts the batch :term:`pending
|
||||||
|
customer` into a :term:`local customer`.
|
||||||
|
|
||||||
|
This is called automatically from :meth:`execute()`.
|
||||||
|
|
||||||
|
This logic will happen only if :meth:`use_local_customers()`
|
||||||
|
returns true, and the batch has pending instead of local
|
||||||
|
customer (so far).
|
||||||
|
|
||||||
|
It will create a new
|
||||||
|
:class:`~sideshow.db.model.customers.LocalCustomer` record and
|
||||||
|
populate it from the batch
|
||||||
|
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatch.pending_customer`.
|
||||||
|
The latter is then deleted.
|
||||||
|
"""
|
||||||
|
if not self.use_local_customers():
|
||||||
|
return
|
||||||
|
|
||||||
|
# nothing to do if no pending customer
|
||||||
|
pending = batch.pending_customer
|
||||||
|
if not pending:
|
||||||
|
return
|
||||||
|
|
||||||
|
session = self.app.get_session(batch)
|
||||||
|
|
||||||
|
# maybe convert pending to local customer
|
||||||
|
if not batch.local_customer:
|
||||||
|
model = self.app.model
|
||||||
|
inspector = sa.inspect(model.LocalCustomer)
|
||||||
|
local = model.LocalCustomer()
|
||||||
|
for prop in inspector.column_attrs:
|
||||||
|
if hasattr(pending, prop.key):
|
||||||
|
setattr(local, prop.key, getattr(pending, prop.key))
|
||||||
|
session.add(local)
|
||||||
|
batch.local_customer = local
|
||||||
|
|
||||||
|
# remove pending customer
|
||||||
|
batch.pending_customer = None
|
||||||
|
session.delete(pending)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
def make_local_products(self, batch, rows):
|
||||||
|
"""
|
||||||
|
If applicable, this converts all :term:`pending products
|
||||||
|
<pending product>` into :term:`local products <local
|
||||||
|
product>`.
|
||||||
|
|
||||||
|
This is called automatically from :meth:`execute()`.
|
||||||
|
|
||||||
|
This logic will happen only if :meth:`use_local_products()`
|
||||||
|
returns true, and the batch has pending instead of local items
|
||||||
|
(so far).
|
||||||
|
|
||||||
|
For each affected row, it will create a new
|
||||||
|
:class:`~sideshow.db.model.products.LocalProduct` record and
|
||||||
|
populate it from the row
|
||||||
|
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.pending_product`.
|
||||||
|
The latter is then deleted.
|
||||||
|
"""
|
||||||
|
if not self.use_local_products():
|
||||||
|
return
|
||||||
|
|
||||||
|
model = self.app.model
|
||||||
|
session = self.app.get_session(batch)
|
||||||
|
inspector = sa.inspect(model.LocalProduct)
|
||||||
|
for row in rows:
|
||||||
|
|
||||||
|
if row.local_product or not row.pending_product:
|
||||||
|
continue
|
||||||
|
|
||||||
|
pending = row.pending_product
|
||||||
|
local = model.LocalProduct()
|
||||||
|
|
||||||
|
for prop in inspector.column_attrs:
|
||||||
|
if hasattr(pending, prop.key):
|
||||||
|
setattr(local, prop.key, getattr(pending, prop.key))
|
||||||
|
session.add(local)
|
||||||
|
|
||||||
|
row.local_product = local
|
||||||
|
row.pending_product = None
|
||||||
|
session.delete(pending)
|
||||||
|
|
||||||
|
session.flush()
|
||||||
|
|
||||||
def make_new_order(self, batch, rows, user=None, progress=None, **kwargs):
|
def make_new_order(self, batch, rows, user=None, progress=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Create a new :term:`order` from the batch data.
|
Create a new :term:`order` from the batch data.
|
||||||
|
@ -429,6 +689,7 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
batch_fields = [
|
batch_fields = [
|
||||||
'store_id',
|
'store_id',
|
||||||
'customer_id',
|
'customer_id',
|
||||||
|
'local_customer',
|
||||||
'pending_customer',
|
'pending_customer',
|
||||||
'customer_name',
|
'customer_name',
|
||||||
'phone_number',
|
'phone_number',
|
||||||
|
@ -437,7 +698,9 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
]
|
]
|
||||||
|
|
||||||
row_fields = [
|
row_fields = [
|
||||||
'pending_product_uuid',
|
'product_id',
|
||||||
|
'local_product',
|
||||||
|
'pending_product',
|
||||||
'product_scancode',
|
'product_scancode',
|
||||||
'product_brand',
|
'product_brand',
|
||||||
'product_description',
|
'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='pendingcustomerstatus').create(op.get_bind())
|
||||||
sa.Enum('PENDING', 'READY', 'RESOLVED', name='pendingproductstatus').create(op.get_bind())
|
sa.Enum('PENDING', 'READY', 'RESOLVED', name='pendingproductstatus').create(op.get_bind())
|
||||||
|
|
||||||
# sideshow_pending_customer
|
# sideshow_customer_pending
|
||||||
op.create_table('sideshow_pending_customer',
|
op.create_table('sideshow_customer_pending',
|
||||||
sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False),
|
sa.Column('uuid', wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
sa.Column('customer_id', sa.String(length=20), nullable=True),
|
sa.Column('customer_id', sa.String(length=20), nullable=True),
|
||||||
sa.Column('full_name', sa.String(length=100), nullable=True),
|
sa.Column('full_name', sa.String(length=100), nullable=True),
|
||||||
|
@ -38,12 +38,24 @@ def upgrade() -> None:
|
||||||
sa.Column('status', postgresql.ENUM('PENDING', 'READY', 'RESOLVED', name='pendingcustomerstatus', create_type=False), nullable=False),
|
sa.Column('status', postgresql.ENUM('PENDING', 'READY', 'RESOLVED', name='pendingcustomerstatus', create_type=False), nullable=False),
|
||||||
sa.Column('created', sa.DateTime(timezone=True), nullable=False),
|
sa.Column('created', sa.DateTime(timezone=True), nullable=False),
|
||||||
sa.Column('created_by_uuid', wuttjamaican.db.util.UUID(), nullable=False),
|
sa.Column('created_by_uuid', wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_pending_customer_created_by_uuid_user')),
|
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_pending_customer'))
|
sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_customer_pending'))
|
||||||
)
|
)
|
||||||
|
|
||||||
# sideshow_pending_product
|
# sideshow_customer_local
|
||||||
op.create_table('sideshow_pending_product',
|
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('uuid', wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
sa.Column('product_id', sa.String(length=20), nullable=True),
|
sa.Column('product_id', sa.String(length=20), nullable=True),
|
||||||
sa.Column('scancode', sa.String(length=14), nullable=True),
|
sa.Column('scancode', sa.String(length=14), nullable=True),
|
||||||
|
@ -63,8 +75,29 @@ def upgrade() -> None:
|
||||||
sa.Column('status', postgresql.ENUM('PENDING', 'READY', 'RESOLVED', name='pendingproductstatus', create_type=False), nullable=False),
|
sa.Column('status', postgresql.ENUM('PENDING', 'READY', 'RESOLVED', name='pendingproductstatus', create_type=False), nullable=False),
|
||||||
sa.Column('created', sa.DateTime(timezone=True), nullable=False),
|
sa.Column('created', sa.DateTime(timezone=True), nullable=False),
|
||||||
sa.Column('created_by_uuid', wuttjamaican.db.util.UUID(), nullable=False),
|
sa.Column('created_by_uuid', wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_pending_product_created_by_uuid_user')),
|
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_pending_product'))
|
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
|
# sideshow_order
|
||||||
|
@ -73,6 +106,7 @@ def upgrade() -> None:
|
||||||
sa.Column('order_id', sa.Integer(), nullable=False),
|
sa.Column('order_id', sa.Integer(), nullable=False),
|
||||||
sa.Column('store_id', sa.String(length=10), nullable=True),
|
sa.Column('store_id', sa.String(length=10), nullable=True),
|
||||||
sa.Column('customer_id', sa.String(length=20), nullable=True),
|
sa.Column('customer_id', sa.String(length=20), nullable=True),
|
||||||
|
sa.Column('local_customer_uuid', wuttjamaican.db.util.UUID(), nullable=True),
|
||||||
sa.Column('pending_customer_uuid', wuttjamaican.db.util.UUID(), nullable=True),
|
sa.Column('pending_customer_uuid', wuttjamaican.db.util.UUID(), nullable=True),
|
||||||
sa.Column('customer_name', sa.String(length=100), nullable=True),
|
sa.Column('customer_name', sa.String(length=100), nullable=True),
|
||||||
sa.Column('phone_number', sa.String(length=20), nullable=True),
|
sa.Column('phone_number', sa.String(length=20), nullable=True),
|
||||||
|
@ -80,7 +114,8 @@ def upgrade() -> None:
|
||||||
sa.Column('total_price', sa.Numeric(precision=10, scale=3), nullable=True),
|
sa.Column('total_price', sa.Numeric(precision=10, scale=3), nullable=True),
|
||||||
sa.Column('created', sa.DateTime(timezone=True), nullable=False),
|
sa.Column('created', sa.DateTime(timezone=True), nullable=False),
|
||||||
sa.Column('created_by_uuid', wuttjamaican.db.util.UUID(), nullable=False),
|
sa.Column('created_by_uuid', wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['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.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_order_created_by_uuid_user')),
|
||||||
sa.PrimaryKeyConstraint('uuid', name=op.f('pk_order'))
|
sa.PrimaryKeyConstraint('uuid', name=op.f('pk_order'))
|
||||||
)
|
)
|
||||||
|
@ -91,6 +126,7 @@ def upgrade() -> None:
|
||||||
sa.Column('order_uuid', wuttjamaican.db.util.UUID(), nullable=False),
|
sa.Column('order_uuid', wuttjamaican.db.util.UUID(), nullable=False),
|
||||||
sa.Column('sequence', sa.Integer(), nullable=False),
|
sa.Column('sequence', sa.Integer(), nullable=False),
|
||||||
sa.Column('product_id', sa.String(length=20), nullable=True),
|
sa.Column('product_id', sa.String(length=20), nullable=True),
|
||||||
|
sa.Column('local_product_uuid', wuttjamaican.db.util.UUID(), nullable=True),
|
||||||
sa.Column('pending_product_uuid', wuttjamaican.db.util.UUID(), nullable=True),
|
sa.Column('pending_product_uuid', wuttjamaican.db.util.UUID(), nullable=True),
|
||||||
sa.Column('product_scancode', sa.String(length=14), nullable=True),
|
sa.Column('product_scancode', sa.String(length=14), nullable=True),
|
||||||
sa.Column('product_brand', sa.String(length=100), nullable=True),
|
sa.Column('product_brand', sa.String(length=100), nullable=True),
|
||||||
|
@ -115,7 +151,8 @@ def upgrade() -> None:
|
||||||
sa.Column('paid_amount', sa.Numeric(precision=8, scale=3), nullable=False),
|
sa.Column('paid_amount', sa.Numeric(precision=8, scale=3), nullable=False),
|
||||||
sa.Column('payment_transaction_number', sa.String(length=20), nullable=True),
|
sa.Column('payment_transaction_number', sa.String(length=20), nullable=True),
|
||||||
sa.ForeignKeyConstraint(['order_uuid'], ['sideshow_order.uuid'], name=op.f('fk_sideshow_order_item_order_uuid_order')),
|
sa.ForeignKeyConstraint(['order_uuid'], ['sideshow_order.uuid'], name=op.f('fk_sideshow_order_item_order_uuid_order')),
|
||||||
sa.ForeignKeyConstraint(['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'))
|
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('executed_by_uuid', wuttjamaican.db.util.UUID(), nullable=True),
|
||||||
sa.Column('store_id', sa.String(length=10), nullable=True),
|
sa.Column('store_id', sa.String(length=10), nullable=True),
|
||||||
sa.Column('customer_id', sa.String(length=20), nullable=True),
|
sa.Column('customer_id', sa.String(length=20), nullable=True),
|
||||||
|
sa.Column('local_customer_uuid', wuttjamaican.db.util.UUID(), nullable=True),
|
||||||
sa.Column('pending_customer_uuid', wuttjamaican.db.util.UUID(), nullable=True),
|
sa.Column('pending_customer_uuid', wuttjamaican.db.util.UUID(), nullable=True),
|
||||||
sa.Column('customer_name', sa.String(length=100), nullable=True),
|
sa.Column('customer_name', sa.String(length=100), nullable=True),
|
||||||
sa.Column('phone_number', sa.String(length=20), nullable=True),
|
sa.Column('phone_number', sa.String(length=20), nullable=True),
|
||||||
|
@ -141,7 +179,8 @@ def upgrade() -> None:
|
||||||
sa.Column('total_price', sa.Numeric(precision=10, scale=3), nullable=True),
|
sa.Column('total_price', sa.Numeric(precision=10, scale=3), nullable=True),
|
||||||
sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_batch_neworder_created_by_uuid_user')),
|
sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_batch_neworder_created_by_uuid_user')),
|
||||||
sa.ForeignKeyConstraint(['executed_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_batch_neworder_executed_by_uuid_user')),
|
sa.ForeignKeyConstraint(['executed_by_uuid'], ['user.uuid'], name=op.f('fk_sideshow_batch_neworder_executed_by_uuid_user')),
|
||||||
sa.ForeignKeyConstraint(['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'))
|
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('sequence', sa.Integer(), nullable=False),
|
||||||
sa.Column('status_text', sa.String(length=255), nullable=True),
|
sa.Column('status_text', sa.String(length=255), nullable=True),
|
||||||
sa.Column('modified', sa.DateTime(timezone=True), nullable=True),
|
sa.Column('modified', sa.DateTime(timezone=True), nullable=True),
|
||||||
sa.Column('pending_product_uuid', wuttjamaican.db.util.UUID(), nullable=True),
|
|
||||||
sa.Column('product_id', sa.String(length=20), nullable=True),
|
sa.Column('product_id', sa.String(length=20), nullable=True),
|
||||||
|
sa.Column('local_product_uuid', wuttjamaican.db.util.UUID(), nullable=True),
|
||||||
|
sa.Column('pending_product_uuid', wuttjamaican.db.util.UUID(), nullable=True),
|
||||||
sa.Column('product_scancode', sa.String(length=14), nullable=True),
|
sa.Column('product_scancode', sa.String(length=14), nullable=True),
|
||||||
sa.Column('product_brand', sa.String(length=100), nullable=True),
|
sa.Column('product_brand', sa.String(length=100), nullable=True),
|
||||||
sa.Column('product_description', sa.String(length=255), nullable=True),
|
sa.Column('product_description', sa.String(length=255), nullable=True),
|
||||||
|
@ -173,9 +213,10 @@ def upgrade() -> None:
|
||||||
sa.Column('case_price_quoted', sa.Numeric(precision=8, scale=3), nullable=True),
|
sa.Column('case_price_quoted', sa.Numeric(precision=8, scale=3), nullable=True),
|
||||||
sa.Column('discount_percent', sa.Numeric(precision=5, scale=3), nullable=True),
|
sa.Column('discount_percent', sa.Numeric(precision=5, scale=3), nullable=True),
|
||||||
sa.Column('total_price', sa.Numeric(precision=8, scale=3), nullable=True),
|
sa.Column('total_price', sa.Numeric(precision=8, scale=3), nullable=True),
|
||||||
sa.Column('status_code', sa.Integer(), nullable=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(['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'))
|
sa.PrimaryKeyConstraint('uuid', name=op.f('pk_sideshow_batch_neworder_row'))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -192,11 +233,17 @@ def downgrade() -> None:
|
||||||
# sideshow_order
|
# sideshow_order
|
||||||
op.drop_table('sideshow_order')
|
op.drop_table('sideshow_order')
|
||||||
|
|
||||||
# sideshow_pending_product
|
# sideshow_product_local
|
||||||
op.drop_table('sideshow_pending_product')
|
op.drop_table('sideshow_product_local')
|
||||||
|
|
||||||
# sideshow_pending_customer
|
# sideshow_product_pending
|
||||||
op.drop_table('sideshow_pending_customer')
|
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
|
# enums
|
||||||
sa.Enum('PENDING', 'READY', 'RESOLVED', name='pendingproductstatus').drop(op.get_bind())
|
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.Order`
|
||||||
* :class:`~sideshow.db.model.orders.OrderItem`
|
* :class:`~sideshow.db.model.orders.OrderItem`
|
||||||
|
* :class:`~sideshow.db.model.customers.LocalCustomer`
|
||||||
|
* :class:`~sideshow.db.model.products.LocalProduct`
|
||||||
* :class:`~sideshow.db.model.customers.PendingCustomer`
|
* :class:`~sideshow.db.model.customers.PendingCustomer`
|
||||||
* :class:`~sideshow.db.model.products.PendingProduct`
|
* :class:`~sideshow.db.model.products.PendingProduct`
|
||||||
|
|
||||||
|
@ -45,8 +47,8 @@ And the :term:`batch` models:
|
||||||
from wuttjamaican.db.model import *
|
from wuttjamaican.db.model import *
|
||||||
|
|
||||||
# sideshow models
|
# sideshow models
|
||||||
from .customers import PendingCustomer
|
from .customers import LocalCustomer, PendingCustomer
|
||||||
from .products import PendingProduct
|
from .products import LocalProduct, PendingProduct
|
||||||
from .orders import Order, OrderItem
|
from .orders import Order, OrderItem
|
||||||
|
|
||||||
# batch models
|
# batch models
|
||||||
|
|
|
@ -58,7 +58,8 @@ class NewOrderBatch(model.BatchMixin, model.Base):
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def __table_args__(cls):
|
def __table_args__(cls):
|
||||||
return cls.__default_table_args__() + (
|
return cls.__default_table_args__() + (
|
||||||
sa.ForeignKeyConstraint(['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
|
STATUS_OK = 1
|
||||||
|
@ -72,11 +73,25 @@ class NewOrderBatch(model.BatchMixin, model.Base):
|
||||||
""")
|
""")
|
||||||
|
|
||||||
customer_id = sa.Column(sa.String(length=20), nullable=True, doc="""
|
customer_id = sa.Column(sa.String(length=20), nullable=True, doc="""
|
||||||
ID of the proper customer account to which the order pertains, if
|
Proper account ID for the :term:`external customer` to which the
|
||||||
applicable.
|
order pertains, if applicable.
|
||||||
|
|
||||||
This will be set only when an "existing" customer account can be
|
See also :attr:`local_customer` and :attr:`pending_customer`.
|
||||||
selected for the order. See also :attr:`pending_customer`.
|
""")
|
||||||
|
|
||||||
|
local_customer_uuid = sa.Column(model.UUID(), nullable=True)
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def local_customer(cls):
|
||||||
|
return orm.relationship(
|
||||||
|
'LocalCustomer',
|
||||||
|
back_populates='new_order_batches',
|
||||||
|
doc="""
|
||||||
|
Reference to the
|
||||||
|
:class:`~sideshow.db.model.customers.LocalCustomer` record
|
||||||
|
for the order, if applicable.
|
||||||
|
|
||||||
|
See also :attr:`customer_id` and :attr:`pending_customer`.
|
||||||
""")
|
""")
|
||||||
|
|
||||||
pending_customer_uuid = sa.Column(model.UUID(), nullable=True)
|
pending_customer_uuid = sa.Column(model.UUID(), nullable=True)
|
||||||
|
@ -91,8 +106,7 @@ class NewOrderBatch(model.BatchMixin, model.Base):
|
||||||
:class:`~sideshow.db.model.customers.PendingCustomer`
|
:class:`~sideshow.db.model.customers.PendingCustomer`
|
||||||
record for the order, if applicable.
|
record for the order, if applicable.
|
||||||
|
|
||||||
This is set only when making an order for a "new /
|
See also :attr:`customer_id` and :attr:`local_customer`.
|
||||||
unknown" customer. See also :attr:`customer_id`.
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
customer_name = sa.Column(sa.String(length=100), nullable=True, doc="""
|
customer_name = sa.Column(sa.String(length=100), nullable=True, doc="""
|
||||||
|
@ -126,7 +140,8 @@ class NewOrderBatchRow(model.BatchRowMixin, model.Base):
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def __table_args__(cls):
|
def __table_args__(cls):
|
||||||
return cls.__default_table_args__() + (
|
return cls.__default_table_args__() + (
|
||||||
sa.ForeignKeyConstraint(['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
|
STATUS_OK = 1
|
||||||
|
@ -158,11 +173,25 @@ class NewOrderBatchRow(model.BatchRowMixin, model.Base):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
product_id = sa.Column(sa.String(length=20), nullable=True, doc="""
|
product_id = sa.Column(sa.String(length=20), nullable=True, doc="""
|
||||||
ID of the true product which the order item represents, if
|
Proper ID for the :term:`external product` which the order item
|
||||||
applicable.
|
represents, if applicable.
|
||||||
|
|
||||||
This will be set only when an "existing" product can be selected
|
See also :attr:`local_product` and :attr:`pending_product`.
|
||||||
for the order. See also :attr:`pending_product`.
|
""")
|
||||||
|
|
||||||
|
local_product_uuid = sa.Column(model.UUID(), nullable=True)
|
||||||
|
|
||||||
|
@declared_attr
|
||||||
|
def local_product(cls):
|
||||||
|
return orm.relationship(
|
||||||
|
'LocalProduct',
|
||||||
|
back_populates='new_order_batch_rows',
|
||||||
|
doc="""
|
||||||
|
Reference to the
|
||||||
|
:class:`~sideshow.db.model.products.LocalProduct` record
|
||||||
|
for the order item, if applicable.
|
||||||
|
|
||||||
|
See also :attr:`product_id` and :attr:`pending_product`.
|
||||||
""")
|
""")
|
||||||
|
|
||||||
pending_product_uuid = sa.Column(model.UUID(), nullable=True)
|
pending_product_uuid = sa.Column(model.UUID(), nullable=True)
|
||||||
|
@ -177,8 +206,7 @@ class NewOrderBatchRow(model.BatchRowMixin, model.Base):
|
||||||
:class:`~sideshow.db.model.products.PendingProduct` record
|
:class:`~sideshow.db.model.products.PendingProduct` record
|
||||||
for the order item, if applicable.
|
for the order item, if applicable.
|
||||||
|
|
||||||
This is set only when making an order for a "new /
|
See also :attr:`product_id` and :attr:`local_product`.
|
||||||
unknown" product. See also :attr:`product_id`.
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
product_scancode = sa.Column(sa.String(length=14), nullable=True, doc="""
|
product_scancode = sa.Column(sa.String(length=14), nullable=True, doc="""
|
||||||
|
|
|
@ -34,19 +34,13 @@ from wuttjamaican.db import model
|
||||||
from sideshow.enum import PendingCustomerStatus
|
from sideshow.enum import PendingCustomerStatus
|
||||||
|
|
||||||
|
|
||||||
class PendingCustomer(model.Base):
|
class CustomerMixin:
|
||||||
"""
|
"""
|
||||||
A "pending" customer record, used when entering an :term:`order`
|
Base class for customer tables. This has shared columns, used by e.g.:
|
||||||
for new/unknown customer.
|
|
||||||
|
* :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_name = sa.Column(sa.String(length=100), nullable=True, doc="""
|
||||||
Full display name for the customer account.
|
Full display name for the customer account.
|
||||||
|
@ -68,6 +62,74 @@ class PendingCustomer(model.Base):
|
||||||
Email address for the customer.
|
Email address for the customer.
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.full_name or ""
|
||||||
|
|
||||||
|
|
||||||
|
class LocalCustomer(CustomerMixin, model.Base):
|
||||||
|
"""
|
||||||
|
This table contains the :term:`local customer` records.
|
||||||
|
|
||||||
|
Sideshow will do customer lookups against this table by default,
|
||||||
|
unless it's configured to use :term:`external customers <external
|
||||||
|
customer>` instead.
|
||||||
|
|
||||||
|
Also by default, when a :term:`new order batch` with a
|
||||||
|
:term:`pending customer` is executed, a new record is added to
|
||||||
|
this local customers table, for lookup next time.
|
||||||
|
"""
|
||||||
|
__tablename__ = 'sideshow_customer_local'
|
||||||
|
|
||||||
|
uuid = model.uuid_column()
|
||||||
|
|
||||||
|
external_id = sa.Column(sa.String(length=20), nullable=True, doc="""
|
||||||
|
ID of the proper customer account associated with this record, if
|
||||||
|
applicable.
|
||||||
|
""")
|
||||||
|
|
||||||
|
orders = orm.relationship(
|
||||||
|
'Order',
|
||||||
|
order_by='Order.order_id.desc()',
|
||||||
|
back_populates='local_customer',
|
||||||
|
cascade_backrefs=False,
|
||||||
|
doc="""
|
||||||
|
List of :class:`~sideshow.db.model.orders.Order` records
|
||||||
|
associated with this customer.
|
||||||
|
""")
|
||||||
|
|
||||||
|
new_order_batches = orm.relationship(
|
||||||
|
'NewOrderBatch',
|
||||||
|
order_by='NewOrderBatch.id.desc()',
|
||||||
|
back_populates='local_customer',
|
||||||
|
cascade_backrefs=False,
|
||||||
|
doc="""
|
||||||
|
List of
|
||||||
|
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch`
|
||||||
|
records associated with this customer.
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
class PendingCustomer(CustomerMixin, model.Base):
|
||||||
|
"""
|
||||||
|
This table contains the :term:`pending customer` records, used
|
||||||
|
when creating an :term:`order` for new/unknown customer.
|
||||||
|
|
||||||
|
Sideshow will automatically create and (hopefully) delete these
|
||||||
|
records as needed.
|
||||||
|
|
||||||
|
By default, when a :term:`new order batch` with a pending customer
|
||||||
|
is executed, a new record is added to the :term:`local customers
|
||||||
|
<local customer>` table, for lookup next time.
|
||||||
|
"""
|
||||||
|
__tablename__ = 'sideshow_customer_pending'
|
||||||
|
|
||||||
|
uuid = model.uuid_column()
|
||||||
|
|
||||||
|
customer_id = sa.Column(sa.String(length=20), nullable=True, doc="""
|
||||||
|
ID of the proper customer account associated with this record, if
|
||||||
|
applicable.
|
||||||
|
""")
|
||||||
|
|
||||||
status = sa.Column(sa.Enum(PendingCustomerStatus), nullable=False, doc="""
|
status = sa.Column(sa.Enum(PendingCustomerStatus), nullable=False, doc="""
|
||||||
Status code for the customer record.
|
Status code for the customer record.
|
||||||
""")
|
""")
|
||||||
|
@ -107,6 +169,3 @@ class PendingCustomer(model.Base):
|
||||||
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch`
|
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch`
|
||||||
records associated with this customer.
|
records associated with this customer.
|
||||||
""")
|
""")
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.full_name or ""
|
|
||||||
|
|
|
@ -63,14 +63,26 @@ class Order(model.Base):
|
||||||
""")
|
""")
|
||||||
|
|
||||||
customer_id = sa.Column(sa.String(length=20), nullable=True, doc="""
|
customer_id = sa.Column(sa.String(length=20), nullable=True, doc="""
|
||||||
ID of the proper customer account to which the order pertains, if
|
Proper account ID for the :term:`external customer` to which the
|
||||||
applicable.
|
order pertains, if applicable.
|
||||||
|
|
||||||
This will be set only when an "existing" customer account can be
|
See also :attr:`local_customer` and :attr:`pending_customer`.
|
||||||
assigned for the order. See also :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(
|
pending_customer = orm.relationship(
|
||||||
'PendingCustomer',
|
'PendingCustomer',
|
||||||
cascade_backrefs=False,
|
cascade_backrefs=False,
|
||||||
|
@ -80,8 +92,7 @@ class Order(model.Base):
|
||||||
:class:`~sideshow.db.model.customers.PendingCustomer` record
|
:class:`~sideshow.db.model.customers.PendingCustomer` record
|
||||||
for the order, if applicable.
|
for the order, if applicable.
|
||||||
|
|
||||||
This is set only when the order is for a "new / unknown"
|
See also :attr:`customer_id` and :attr:`local_customer`.
|
||||||
customer. See also :attr:`customer_id`.
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
customer_name = sa.Column(sa.String(length=100), nullable=True, doc="""
|
customer_name = sa.Column(sa.String(length=100), nullable=True, doc="""
|
||||||
|
@ -158,14 +169,26 @@ class OrderItem(model.Base):
|
||||||
""")
|
""")
|
||||||
|
|
||||||
product_id = sa.Column(sa.String(length=20), nullable=True, doc="""
|
product_id = sa.Column(sa.String(length=20), nullable=True, doc="""
|
||||||
ID of the true product which the order item represents, if
|
Proper ID for the :term:`external product` which the order item
|
||||||
applicable.
|
represents, if applicable.
|
||||||
|
|
||||||
This will be set only when an "existing" product can be selected
|
See also :attr:`local_product` and :attr:`pending_product`.
|
||||||
for the order. See also :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(
|
pending_product = orm.relationship(
|
||||||
'PendingProduct',
|
'PendingProduct',
|
||||||
cascade_backrefs=False,
|
cascade_backrefs=False,
|
||||||
|
@ -175,8 +198,7 @@ class OrderItem(model.Base):
|
||||||
:class:`~sideshow.db.model.products.PendingProduct` record for
|
:class:`~sideshow.db.model.products.PendingProduct` record for
|
||||||
the order item, if applicable.
|
the order item, if applicable.
|
||||||
|
|
||||||
This is set only when the order item is for a "new / unknown"
|
See also :attr:`product_id` and :attr:`local_product`.
|
||||||
product. See also :attr:`product_id`.
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
product_scancode = sa.Column(sa.String(length=14), nullable=True, doc="""
|
product_scancode = sa.Column(sa.String(length=14), nullable=True, doc="""
|
||||||
|
@ -310,5 +332,15 @@ class OrderItem(model.Base):
|
||||||
applicable/known.
|
applicable/known.
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_description(self):
|
||||||
|
""" """
|
||||||
|
fields = [
|
||||||
|
self.product_brand or '',
|
||||||
|
self.product_description or '',
|
||||||
|
self.product_size or '']
|
||||||
|
fields = [f.strip() for f in fields if f.strip()]
|
||||||
|
return ' '.join(fields)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return 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
|
from sideshow.enum import PendingProductStatus
|
||||||
|
|
||||||
|
|
||||||
class PendingProduct(model.Base):
|
class ProductMixin:
|
||||||
"""
|
"""
|
||||||
A "pending" product record, used when entering an :term:`order
|
Base class for product tables. This has shared columns, used by e.g.:
|
||||||
item` for new/unknown product.
|
|
||||||
|
* :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 = sa.Column(sa.String(length=14), nullable=True, doc="""
|
||||||
Scancode for the product, as string.
|
Scancode for the product, as string.
|
||||||
|
@ -117,6 +112,82 @@ class PendingProduct(model.Base):
|
||||||
Arbitrary notes regarding the product, if applicable.
|
Arbitrary notes regarding the product, if applicable.
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_description(self):
|
||||||
|
""" """
|
||||||
|
fields = [
|
||||||
|
self.brand_name or '',
|
||||||
|
self.description or '',
|
||||||
|
self.size or '']
|
||||||
|
fields = [f.strip() for f in fields if f.strip()]
|
||||||
|
return ' '.join(fields)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.full_description
|
||||||
|
|
||||||
|
|
||||||
|
class LocalProduct(ProductMixin, model.Base):
|
||||||
|
"""
|
||||||
|
This table contains the :term:`local product` records.
|
||||||
|
|
||||||
|
Sideshow will do customer lookups against this table by default,
|
||||||
|
unless it's configured to use :term:`external products <external
|
||||||
|
product>` instead.
|
||||||
|
|
||||||
|
Also by default, when a :term:`new order batch` with
|
||||||
|
:term:`pending product(s) <pending product>` is executed, new
|
||||||
|
record(s) will be added to this local products table, for lookup
|
||||||
|
next time.
|
||||||
|
"""
|
||||||
|
__tablename__ = 'sideshow_product_local'
|
||||||
|
|
||||||
|
uuid = model.uuid_column()
|
||||||
|
|
||||||
|
external_id = sa.Column(sa.String(length=20), nullable=True, doc="""
|
||||||
|
ID of the true external product associated with this record, if
|
||||||
|
applicable.
|
||||||
|
""")
|
||||||
|
|
||||||
|
order_items = orm.relationship(
|
||||||
|
'OrderItem',
|
||||||
|
back_populates='local_product',
|
||||||
|
cascade_backrefs=False,
|
||||||
|
doc="""
|
||||||
|
List of :class:`~sideshow.db.model.orders.OrderItem` records
|
||||||
|
associated with this product.
|
||||||
|
""")
|
||||||
|
|
||||||
|
new_order_batch_rows = orm.relationship(
|
||||||
|
'NewOrderBatchRow',
|
||||||
|
back_populates='local_product',
|
||||||
|
cascade_backrefs=False,
|
||||||
|
doc="""
|
||||||
|
List of
|
||||||
|
:class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
|
||||||
|
records associated with this product.
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
class PendingProduct(ProductMixin, model.Base):
|
||||||
|
"""
|
||||||
|
This table contains the :term:`pending product` records, used when
|
||||||
|
creating an :term:`order` for new/unknown product(s).
|
||||||
|
|
||||||
|
Sideshow will automatically create and (hopefully) delete these
|
||||||
|
records as needed.
|
||||||
|
|
||||||
|
By default, when a :term:`new order batch` with pending product(s)
|
||||||
|
is executed, new record(s) will be added to the :term:`local
|
||||||
|
products <local product>` table, for lookup next time.
|
||||||
|
"""
|
||||||
|
__tablename__ = 'sideshow_product_pending'
|
||||||
|
|
||||||
|
uuid = model.uuid_column()
|
||||||
|
|
||||||
|
product_id = sa.Column(sa.String(length=20), nullable=True, doc="""
|
||||||
|
ID of the true product associated with this record, if applicable.
|
||||||
|
""")
|
||||||
|
|
||||||
status = sa.Column(sa.Enum(PendingProductStatus), nullable=False, doc="""
|
status = sa.Column(sa.Enum(PendingProductStatus), nullable=False, doc="""
|
||||||
Status code for the product record.
|
Status code for the product record.
|
||||||
""")
|
""")
|
||||||
|
@ -138,10 +209,8 @@ class PendingProduct(model.Base):
|
||||||
|
|
||||||
order_items = orm.relationship(
|
order_items = orm.relationship(
|
||||||
'OrderItem',
|
'OrderItem',
|
||||||
# TODO
|
|
||||||
# order_by='NewOrderBatchRow.id.desc()',
|
|
||||||
cascade_backrefs=False,
|
|
||||||
back_populates='pending_product',
|
back_populates='pending_product',
|
||||||
|
cascade_backrefs=False,
|
||||||
doc="""
|
doc="""
|
||||||
List of :class:`~sideshow.db.model.orders.OrderItem` records
|
List of :class:`~sideshow.db.model.orders.OrderItem` records
|
||||||
associated with this product.
|
associated with this product.
|
||||||
|
@ -149,25 +218,10 @@ class PendingProduct(model.Base):
|
||||||
|
|
||||||
new_order_batch_rows = orm.relationship(
|
new_order_batch_rows = orm.relationship(
|
||||||
'NewOrderBatchRow',
|
'NewOrderBatchRow',
|
||||||
# TODO
|
|
||||||
# order_by='NewOrderBatchRow.id.desc()',
|
|
||||||
cascade_backrefs=False,
|
|
||||||
back_populates='pending_product',
|
back_populates='pending_product',
|
||||||
|
cascade_backrefs=False,
|
||||||
doc="""
|
doc="""
|
||||||
List of
|
List of
|
||||||
:class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
|
:class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
|
||||||
records associated with this product.
|
records associated with this product.
|
||||||
""")
|
""")
|
||||||
|
|
||||||
@property
|
|
||||||
def full_description(self):
|
|
||||||
""" """
|
|
||||||
fields = [
|
|
||||||
self.brand_name or '',
|
|
||||||
self.description or '',
|
|
||||||
self.size or '']
|
|
||||||
fields = [f.strip() for f in fields if f.strip()]
|
|
||||||
return ' '.join(fields)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.full_description
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ from wuttaweb.forms.schema import ObjectRef
|
||||||
|
|
||||||
class OrderRef(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.
|
reference field.
|
||||||
|
|
||||||
This is a subclass of
|
This is a subclass of
|
||||||
|
@ -51,9 +51,34 @@ class OrderRef(ObjectRef):
|
||||||
return self.request.route_url('orders.view', uuid=order.uuid)
|
return self.request.route_url('orders.view', uuid=order.uuid)
|
||||||
|
|
||||||
|
|
||||||
|
class LocalCustomerRef(ObjectRef):
|
||||||
|
"""
|
||||||
|
Schema type for a
|
||||||
|
:class:`~sideshow.db.model.customers.LocalCustomer` reference
|
||||||
|
field.
|
||||||
|
|
||||||
|
This is a subclass of
|
||||||
|
:class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model_class(self):
|
||||||
|
""" """
|
||||||
|
model = self.app.model
|
||||||
|
return model.LocalCustomer
|
||||||
|
|
||||||
|
def sort_query(self, query):
|
||||||
|
""" """
|
||||||
|
return query.order_by(self.model_class.full_name)
|
||||||
|
|
||||||
|
def get_object_url(self, customer):
|
||||||
|
""" """
|
||||||
|
return self.request.route_url('local_customers.view', uuid=customer.uuid)
|
||||||
|
|
||||||
|
|
||||||
class PendingCustomerRef(ObjectRef):
|
class PendingCustomerRef(ObjectRef):
|
||||||
"""
|
"""
|
||||||
Custom schema type for a
|
Schema type for a
|
||||||
:class:`~sideshow.db.model.customers.PendingCustomer` reference
|
:class:`~sideshow.db.model.customers.PendingCustomer` reference
|
||||||
field.
|
field.
|
||||||
|
|
||||||
|
@ -76,9 +101,33 @@ class PendingCustomerRef(ObjectRef):
|
||||||
return self.request.route_url('pending_customers.view', uuid=customer.uuid)
|
return self.request.route_url('pending_customers.view', uuid=customer.uuid)
|
||||||
|
|
||||||
|
|
||||||
|
class LocalProductRef(ObjectRef):
|
||||||
|
"""
|
||||||
|
Schema type for a
|
||||||
|
:class:`~sideshow.db.model.products.LocalProduct` reference field.
|
||||||
|
|
||||||
|
This is a subclass of
|
||||||
|
:class:`~wuttaweb:wuttaweb.forms.schema.ObjectRef`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model_class(self):
|
||||||
|
""" """
|
||||||
|
model = self.app.model
|
||||||
|
return model.LocalProduct
|
||||||
|
|
||||||
|
def sort_query(self, query):
|
||||||
|
""" """
|
||||||
|
return query.order_by(self.model_class.scancode)
|
||||||
|
|
||||||
|
def get_object_url(self, product):
|
||||||
|
""" """
|
||||||
|
return self.request.route_url('local_products.view', uuid=product.uuid)
|
||||||
|
|
||||||
|
|
||||||
class PendingProductRef(ObjectRef):
|
class PendingProductRef(ObjectRef):
|
||||||
"""
|
"""
|
||||||
Custom schema type for a
|
Schema type for a
|
||||||
:class:`~sideshow.db.model.products.PendingProduct` reference
|
:class:`~sideshow.db.model.products.PendingProduct` reference
|
||||||
field.
|
field.
|
||||||
|
|
||||||
|
|
|
@ -36,14 +36,15 @@ class SideshowMenuHandler(base.MenuHandler):
|
||||||
""" """
|
""" """
|
||||||
return [
|
return [
|
||||||
self.make_orders_menu(request),
|
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_batch_menu(request),
|
||||||
self.make_admin_menu(request),
|
self.make_admin_menu(request),
|
||||||
]
|
]
|
||||||
|
|
||||||
def make_orders_menu(self, request, **kwargs):
|
def make_orders_menu(self, request, **kwargs):
|
||||||
"""
|
"""
|
||||||
Generate a typical Orders menu.
|
Generate the Orders menu.
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
'title': "Orders",
|
'title': "Orders",
|
||||||
|
@ -55,34 +56,55 @@ class SideshowMenuHandler(base.MenuHandler):
|
||||||
'perm': 'orders.create',
|
'perm': 'orders.create',
|
||||||
},
|
},
|
||||||
{'type': 'sep'},
|
{'type': 'sep'},
|
||||||
{
|
|
||||||
'title': "All Orders",
|
|
||||||
'route': 'orders',
|
|
||||||
'perm': 'orders.list',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
'title': "All Order Items",
|
'title': "All Order Items",
|
||||||
'route': 'order_items',
|
'route': 'order_items',
|
||||||
'perm': 'order_items.list',
|
'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 {
|
return {
|
||||||
'title': "Pending",
|
'title': "Customers",
|
||||||
'type': 'menu',
|
'type': 'menu',
|
||||||
'items': [
|
'items': [
|
||||||
{
|
{
|
||||||
'title': "Customers",
|
'title': "Local Customers",
|
||||||
|
'route': 'local_customers',
|
||||||
|
'perm': 'local_customers.list',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': "Pending Customers",
|
||||||
'route': 'pending_customers',
|
'route': 'pending_customers',
|
||||||
'perm': 'pending_customers.list',
|
'perm': 'pending_customers.list',
|
||||||
},
|
},
|
||||||
{
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def make_products_menu(self, request, **kwargs):
|
||||||
|
"""
|
||||||
|
Generate the Products menu.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
'title': "Products",
|
'title': "Products",
|
||||||
|
'type': 'menu',
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'title': "Local Products",
|
||||||
|
'route': 'local_products',
|
||||||
|
'perm': 'local_products.list',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': "Pending Products",
|
||||||
'route': 'pending_products',
|
'route': 'pending_products',
|
||||||
'perm': 'pending_products.list',
|
'perm': 'pending_products.list',
|
||||||
},
|
},
|
||||||
|
@ -91,7 +113,7 @@ class SideshowMenuHandler(base.MenuHandler):
|
||||||
|
|
||||||
def make_batch_menu(self, request, **kwargs):
|
def make_batch_menu(self, request, **kwargs):
|
||||||
"""
|
"""
|
||||||
Generate a typical Batch menu.
|
Generate the Batch menu.
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
'title': "Batches",
|
'title': "Batches",
|
||||||
|
|
|
@ -7,15 +7,15 @@
|
||||||
<div class="block" style="padding-left: 2rem;">
|
<div class="block" style="padding-left: 2rem;">
|
||||||
|
|
||||||
<b-field message="If set, user can enter details of an arbitrary new "pending" product.">
|
<b-field message="If set, user can enter details of an arbitrary new "pending" product.">
|
||||||
<b-checkbox name="sideshow.orders.allow_unknown_product"
|
<b-checkbox name="sideshow.orders.allow_unknown_products"
|
||||||
v-model="simpleSettings['sideshow.orders.allow_unknown_product']"
|
v-model="simpleSettings['sideshow.orders.allow_unknown_products']"
|
||||||
native-value="true"
|
native-value="true"
|
||||||
@input="settingsNeedSaved = true">
|
@input="settingsNeedSaved = true">
|
||||||
Allow creating orders for "unknown" products
|
Allow creating orders for "unknown" products
|
||||||
</b-checkbox>
|
</b-checkbox>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<div v-show="simpleSettings['sideshow.orders.allow_unknown_product']"
|
<div v-show="simpleSettings['sideshow.orders.allow_unknown_products']"
|
||||||
style="padding-left: 2rem;">
|
style="padding-left: 2rem;">
|
||||||
|
|
||||||
<p class="block">
|
<p class="block">
|
||||||
|
|
|
@ -130,28 +130,20 @@
|
||||||
|
|
||||||
<b-field label="Customer">
|
<b-field label="Customer">
|
||||||
<div style="display: flex; gap: 1rem; width: 100%;">
|
<div style="display: flex; gap: 1rem; width: 100%;">
|
||||||
<b-autocomplete ref="customerAutocomplete"
|
<wutta-autocomplete ref="customerAutocomplete"
|
||||||
v-model="customerID"
|
v-model="customerID"
|
||||||
:style="{'flex-grow': customerID ? '0' : '1'}"
|
:display="customerName"
|
||||||
expanded
|
service-url="${url(f'{route_prefix}.customer_autocomplete')}"
|
||||||
placeholder="Enter name or phone number"
|
placeholder="Enter name or phone number"
|
||||||
## TODO
|
|
||||||
## serviceUrl="${url(f'{route_prefix}.customer_autocomplete')}"
|
|
||||||
% if request.use_oruga:
|
|
||||||
## :assigned-label="customerName"
|
|
||||||
@update:model-value="customerChanged"
|
|
||||||
% else:
|
|
||||||
## :initial-label="customerName"
|
|
||||||
@input="customerChanged"
|
@input="customerChanged"
|
||||||
% endif
|
:style="{'flex-grow': customerID ? '0' : '1'}"
|
||||||
>
|
expanded />
|
||||||
</b-autocomplete>
|
|
||||||
<b-button v-if="customerID"
|
<b-button v-if="customerID"
|
||||||
@click="refreshCustomer"
|
@click="refreshCustomer"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
icon-left="redo"
|
icon-left="redo"
|
||||||
:disabled="refreshingCustomer">
|
:disabled="refreshingCustomer">
|
||||||
{{ refreshingCustomer ? "Refreshig" : "Refresh" }}
|
{{ refreshingCustomer ? "Refreshing" : "Refresh" }}
|
||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
@ -348,9 +340,9 @@
|
||||||
|
|
||||||
<${b}-modal
|
<${b}-modal
|
||||||
% if request.use_oruga:
|
% if request.use_oruga:
|
||||||
v-model:active="showingItemDialog"
|
v-model:active="editItemShowDialog"
|
||||||
% else:
|
% else:
|
||||||
:active.sync="showingItemDialog"
|
:active.sync="editItemShowDialog"
|
||||||
% endif
|
% endif
|
||||||
:can-cancel="['escape', 'x']"
|
:can-cancel="['escape', 'x']"
|
||||||
>
|
>
|
||||||
|
@ -382,21 +374,12 @@
|
||||||
|
|
||||||
<div style="flex-grow: 1;">
|
<div style="flex-grow: 1;">
|
||||||
<b-field label="Product">
|
<b-field label="Product">
|
||||||
<b-autocomplete ref="productLookup"
|
<wutta-autocomplete ref="productAutocomplete"
|
||||||
v-model="productID"
|
v-model="productID"
|
||||||
## :style="{'flex-grow': customerID ? '0' : '1'}"
|
:display="productDisplay"
|
||||||
## expanded
|
service-url="${url(f'{route_prefix}.product_autocomplete')}"
|
||||||
## placeholder="Enter name or phone number"
|
placeholder="Enter brand, description etc."
|
||||||
## ## serviceUrl="${url(f'{route_prefix}.customer_autocomplete')}"
|
@input="productChanged" />
|
||||||
## % if request.use_oruga:
|
|
||||||
## ## :assigned-label="customerName"
|
|
||||||
## @update:model-value="customerChanged"
|
|
||||||
## % else:
|
|
||||||
## ## :initial-label="customerName"
|
|
||||||
## @input="customerChanged"
|
|
||||||
## % endif
|
|
||||||
>
|
|
||||||
</b-autocomplete>
|
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<div v-if="productID">
|
<div v-if="productID">
|
||||||
|
@ -443,16 +426,16 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img v-if="productID"
|
## <img v-if="productID"
|
||||||
:src="productImageURL"
|
## :src="productImageURL"
|
||||||
style="max-height: 150px; max-width: 150px; "/>
|
## style="max-height: 150px; max-width: 150px; "/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<b-radio v-model="productIsKnown"
|
<b-radio v-model="productIsKnown"
|
||||||
% if not allow_unknown_product:
|
% if not allow_unknown_products:
|
||||||
disabled
|
disabled
|
||||||
% endif
|
% endif
|
||||||
:native-value="false">
|
:native-value="false">
|
||||||
|
@ -705,7 +688,7 @@
|
||||||
</${b}-tabs>
|
</${b}-tabs>
|
||||||
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<b-button @click="showingItemDialog = false">
|
<b-button @click="editItemShowDialog = false">
|
||||||
Cancel
|
Cancel
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
|
@ -713,7 +696,7 @@
|
||||||
:disabled="itemDialogSaveDisabled"
|
:disabled="itemDialogSaveDisabled"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
icon-left="save">
|
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>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -757,8 +740,10 @@
|
||||||
|
|
||||||
<${b}-table-column label="Unit Price"
|
<${b}-table-column label="Unit Price"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
<span :class="props.row.pricing_reflects_sale ? 'has-background-warning' : null">
|
<span
|
||||||
{{ props.row.unit_price_display }}
|
##:class="props.row.pricing_reflects_sale ? 'has-background-warning' : null"
|
||||||
|
>
|
||||||
|
{{ props.row.unit_price_quoted_display }}
|
||||||
</span>
|
</span>
|
||||||
</${b}-table-column>
|
</${b}-table-column>
|
||||||
|
|
||||||
|
@ -771,17 +756,15 @@
|
||||||
|
|
||||||
<${b}-table-column label="Vendor"
|
<${b}-table-column label="Vendor"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.vendor_display }}
|
{{ props.row.vendor_name }}
|
||||||
</${b}-table-column>
|
</${b}-table-column>
|
||||||
|
|
||||||
<${b}-table-column field="actions"
|
<${b}-table-column field="actions"
|
||||||
label="Actions"
|
label="Actions"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
<a href="#"
|
<a href="#"
|
||||||
% if not request.use_oruga:
|
@click.prevent="editItemInit(props.row)">
|
||||||
class="grid-action"
|
|
||||||
% endif
|
|
||||||
@click.prevent="showEditItemDialog(props.row)">
|
|
||||||
% if request.use_oruga:
|
% if request.use_oruga:
|
||||||
<span class="icon-text">
|
<span class="icon-text">
|
||||||
<o-icon icon="edit" />
|
<o-icon icon="edit" />
|
||||||
|
@ -846,13 +829,14 @@
|
||||||
batchTotalPriceDisplay: ${json.dumps(normalized_batch['total_price_display'])|n},
|
batchTotalPriceDisplay: ${json.dumps(normalized_batch['total_price_display'])|n},
|
||||||
|
|
||||||
customerPanelOpen: false,
|
customerPanelOpen: false,
|
||||||
customerIsKnown: ${json.dumps(bool(batch.customer_id))|n},
|
customerIsKnown: ${json.dumps(customer_is_known)|n},
|
||||||
customerID: ${json.dumps(batch.customer_id)|n},
|
customerID: ${json.dumps(customer_id)|n},
|
||||||
customerName: ${json.dumps(batch.customer_name)|n},
|
customerName: ${json.dumps(customer_name)|n},
|
||||||
orderPhoneNumber: ${json.dumps(batch.phone_number)|n},
|
orderPhoneNumber: ${json.dumps(phone_number)|n},
|
||||||
orderEmailAddress: ${json.dumps(batch.email_address)|n},
|
orderEmailAddress: ${json.dumps(email_address)|n},
|
||||||
refreshingCustomer: false,
|
refreshingCustomer: false,
|
||||||
|
|
||||||
|
newCustomerFullName: ${json.dumps(new_customer_full_name)|n},
|
||||||
newCustomerFirstName: ${json.dumps(new_customer_first_name)|n},
|
newCustomerFirstName: ${json.dumps(new_customer_first_name)|n},
|
||||||
newCustomerLastName: ${json.dumps(new_customer_last_name)|n},
|
newCustomerLastName: ${json.dumps(new_customer_last_name)|n},
|
||||||
newCustomerPhone: ${json.dumps(new_customer_phone)|n},
|
newCustomerPhone: ${json.dumps(new_customer_phone)|n},
|
||||||
|
@ -867,8 +851,8 @@
|
||||||
|
|
||||||
items: ${json.dumps(order_items)|n},
|
items: ${json.dumps(order_items)|n},
|
||||||
|
|
||||||
editingItem: null,
|
editItemRow: null,
|
||||||
showingItemDialog: false,
|
editItemShowDialog: false,
|
||||||
itemDialogSaving: false,
|
itemDialogSaving: false,
|
||||||
% if request.use_oruga:
|
% if request.use_oruga:
|
||||||
itemDialogTab: 'product',
|
itemDialogTab: 'product',
|
||||||
|
@ -921,21 +905,9 @@
|
||||||
customerPanelHeader() {
|
customerPanelHeader() {
|
||||||
let text = "Customer"
|
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) {
|
if (this.customerName) {
|
||||||
text = "Customer: " + this.customerName
|
text = "Customer: " + this.customerName
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.customerPanelOpen) {
|
if (!this.customerPanelOpen) {
|
||||||
text += ' <p class="' + this.customerHeaderClass + '" style="display: inline-block; float: right;">' + this.customerStatusText + '</p>'
|
text += ' <p class="' + this.customerHeaderClass + '" style="display: inline-block; float: right;">' + this.customerStatusText + '</p>'
|
||||||
|
@ -1077,41 +1049,29 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
## TODO
|
watch: {
|
||||||
## watch: {
|
|
||||||
##
|
customerIsKnown: function(val) {
|
||||||
## contactIsKnown: function(val) {
|
|
||||||
##
|
if (val) {
|
||||||
## // when user clicks "contact is known" then we want to
|
// user clicks "customer is in the system"
|
||||||
## // set focus to the autocomplete component
|
|
||||||
## if (val) {
|
// clear customer
|
||||||
## this.$nextTick(() => {
|
this.customerChanged(null)
|
||||||
## this.$refs.customerAutocomplete.focus()
|
|
||||||
## })
|
// focus customer autocomplete
|
||||||
##
|
this.$nextTick(() => {
|
||||||
## // if user has already specified a proper contact,
|
this.$refs.customerAutocomplete.focus()
|
||||||
## // 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
|
} else {
|
||||||
## // out* the existing contact selection. this is
|
// user clicks "customer is NOT in the system"
|
||||||
## // primarily to avoid any ambiguity.
|
|
||||||
## } else if (this.contactUUID) {
|
// remove true customer; set pending (or null)
|
||||||
## this.$refs.customerAutocomplete.clearSelection()
|
this.setPendingCustomer()
|
||||||
## }
|
}
|
||||||
## },
|
},
|
||||||
##
|
},
|
||||||
## productIsKnown(newval, oldval) {
|
|
||||||
## // TODO: seems like this should be better somehow?
|
|
||||||
## // e.g. maybe we should not be clearing *everything*
|
|
||||||
## // in case user accidentally clicks, and then clicks
|
|
||||||
## // "is known" again? and if we *should* clear all,
|
|
||||||
## // why does that require 2 steps?
|
|
||||||
## if (!newval) {
|
|
||||||
## this.selectedProduct = null
|
|
||||||
## this.clearProduct()
|
|
||||||
## }
|
|
||||||
## },
|
|
||||||
## },
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
|
@ -1157,7 +1117,7 @@
|
||||||
this.submittingOrder = true
|
this.submittingOrder = true
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
action: 'submit_new_order',
|
action: 'submit_order',
|
||||||
}
|
}
|
||||||
|
|
||||||
this.submitBatchData(params, response => {
|
this.submitBatchData(params, response => {
|
||||||
|
@ -1173,29 +1133,28 @@
|
||||||
|
|
||||||
customerChanged(customerID, callback) {
|
customerChanged(customerID, callback) {
|
||||||
|
|
||||||
let params
|
const params = {}
|
||||||
if (!customerID) {
|
if (customerID) {
|
||||||
params = {
|
params.action = 'assign_customer'
|
||||||
action: 'unassign_contact',
|
params.customer_id = customerID
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
params = {
|
params.action = 'unassign_customer'
|
||||||
action: 'assign_contact',
|
|
||||||
customer_id: customerID,
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
this.submitBatchData(params, response => {
|
this.submitBatchData(params, ({data}) => {
|
||||||
this.customerID = response.data.customer_id
|
this.customerID = data.customer_id
|
||||||
this.customerName = response.data.customer_name
|
this.customerName = data.customer_name
|
||||||
this.orderPhoneNumber = response.data.phone_number
|
this.orderPhoneNumber = data.phone_number
|
||||||
this.orderEmailAddress = response.data.email_address
|
this.orderEmailAddress = data.email_address
|
||||||
this.addOtherPhoneNumber = response.data.add_phone_number
|
|
||||||
this.addOtherEmailAddress = response.data.add_email_address
|
|
||||||
this.contactPhones = response.data.contact_phones
|
|
||||||
this.contactEmails = response.data.contact_emails
|
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||
|
}, response => {
|
||||||
|
this.$buefy.toast.open({
|
||||||
|
message: "Update failed: " + (response.data.error || "(unknown error)"),
|
||||||
|
type: 'is-danger',
|
||||||
|
duration: 2000, // 2 seconds
|
||||||
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1234,7 +1193,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
this.submitBatchData(params, response => {
|
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.newCustomerFirstName = response.data.new_customer_first_name
|
||||||
this.newCustomerLastName = response.data.new_customer_last_name
|
this.newCustomerLastName = response.data.new_customer_last_name
|
||||||
this.newCustomerPhone = response.data.phone_number
|
this.newCustomerPhone = response.data.phone_number
|
||||||
|
@ -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() {
|
getCasePriceDisplay() {
|
||||||
if (this.productIsKnown) {
|
if (this.productIsKnown) {
|
||||||
return this.productCasePriceDisplay
|
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
|
## TODO
|
||||||
## productLookupSelected(selected) {
|
## productLookupSelected(selected) {
|
||||||
## // TODO: this still is a hack somehow, am sure of it.
|
## // TODO: this still is a hack somehow, am sure of it.
|
||||||
|
@ -1335,7 +1399,7 @@
|
||||||
|
|
||||||
showAddItemDialog() {
|
showAddItemDialog() {
|
||||||
this.customerPanelOpen = false
|
this.customerPanelOpen = false
|
||||||
this.editingItem = null
|
this.editItemRow = null
|
||||||
this.productIsKnown = true
|
this.productIsKnown = true
|
||||||
## this.selectedProduct = null
|
## this.selectedProduct = null
|
||||||
this.productID = null
|
this.productID = null
|
||||||
|
@ -1364,14 +1428,15 @@
|
||||||
% else:
|
% else:
|
||||||
this.itemDialogTabIndex = 0
|
this.itemDialogTabIndex = 0
|
||||||
% endif
|
% endif
|
||||||
this.showingItemDialog = true
|
this.editItemShowDialog = true
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.productLookup.focus()
|
// this.$refs.productLookup.focus()
|
||||||
|
this.$refs.productAutocomplete.focus()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
showEditItemDialog(row) {
|
editItemInit(row) {
|
||||||
this.editingItem = row
|
this.editItemRow = row
|
||||||
|
|
||||||
this.productIsKnown = !!row.product_id
|
this.productIsKnown = !!row.product_id
|
||||||
this.productID = row.product_id
|
this.productID = row.product_id
|
||||||
|
@ -1397,8 +1462,7 @@
|
||||||
this.productDisplay = row.product_full_description
|
this.productDisplay = row.product_full_description
|
||||||
this.productScancode = row.product_scancode
|
this.productScancode = row.product_scancode
|
||||||
this.productSize = row.product_size
|
this.productSize = row.product_size
|
||||||
this.productCaseQuantity = row.case_quantity
|
this.productCaseQuantity = row.case_size
|
||||||
this.productURL = row.product_url
|
|
||||||
this.productUnitPrice = row.unit_price_quoted
|
this.productUnitPrice = row.unit_price_quoted
|
||||||
this.productUnitPriceDisplay = row.unit_price_quoted_display
|
this.productUnitPriceDisplay = row.unit_price_quoted_display
|
||||||
this.productUnitRegularPriceDisplay = row.unit_price_reg_display
|
this.productUnitRegularPriceDisplay = row.unit_price_reg_display
|
||||||
|
@ -1422,7 +1486,7 @@
|
||||||
% else:
|
% else:
|
||||||
this.itemDialogTabIndex = 1
|
this.itemDialogTabIndex = 1
|
||||||
% endif
|
% endif
|
||||||
this.showingItemDialog = true
|
this.editItemShowDialog = true
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteItem(index) {
|
deleteItem(index) {
|
||||||
|
@ -1451,25 +1515,20 @@
|
||||||
itemDialogAttemptSave() {
|
itemDialogAttemptSave() {
|
||||||
this.itemDialogSaving = true
|
this.itemDialogSaving = true
|
||||||
|
|
||||||
let params = {
|
const params = {
|
||||||
product_is_known: this.productIsKnown,
|
|
||||||
order_qty: this.productQuantity,
|
order_qty: this.productQuantity,
|
||||||
order_uom: this.productUOM,
|
order_uom: this.productUOM,
|
||||||
}
|
}
|
||||||
|
|
||||||
% if allow_item_discounts:
|
|
||||||
params.discount_percent = this.productDiscountPercent
|
|
||||||
% endif
|
|
||||||
|
|
||||||
if (this.productIsKnown) {
|
if (this.productIsKnown) {
|
||||||
params.product_uuid = this.productUUID
|
params.product_info = this.productID
|
||||||
} else {
|
} else {
|
||||||
params.pending_product = this.pendingProduct
|
params.product_info = this.pendingProduct
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.editingItem) {
|
if (this.editItemRow) {
|
||||||
params.action = 'update_item'
|
params.action = 'update_item'
|
||||||
params.uuid = this.editingItem.uuid
|
params.uuid = this.editItemRow.uuid
|
||||||
} else {
|
} else {
|
||||||
params.action = 'add_item'
|
params.action = 'add_item'
|
||||||
}
|
}
|
||||||
|
@ -1484,7 +1543,7 @@
|
||||||
// overwriting the item record, or else display will
|
// overwriting the item record, or else display will
|
||||||
// not update properly
|
// not update properly
|
||||||
for (let [key, value] of Object.entries(response.data.row)) {
|
for (let [key, value] of Object.entries(response.data.row)) {
|
||||||
this.editingItem[key] = value
|
this.editItemRow[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1492,7 +1551,7 @@
|
||||||
this.batchTotalPriceDisplay = response.data.batch.total_price_display
|
this.batchTotalPriceDisplay = response.data.batch.total_price_display
|
||||||
|
|
||||||
this.itemDialogSaving = false
|
this.itemDialogSaving = false
|
||||||
this.showingItemDialog = false
|
this.editItemShowDialog = false
|
||||||
}, response => {
|
}, response => {
|
||||||
this.itemDialogSaving = false
|
this.itemDialogSaving = false
|
||||||
})
|
})
|
||||||
|
|
|
@ -29,7 +29,7 @@ from wuttaweb.forms.schema import WuttaMoney
|
||||||
|
|
||||||
from sideshow.db.model import NewOrderBatch
|
from sideshow.db.model import NewOrderBatch
|
||||||
from sideshow.batch.neworder import NewOrderBatchHandler
|
from sideshow.batch.neworder import NewOrderBatchHandler
|
||||||
from sideshow.web.forms.schema import PendingCustomerRef
|
from sideshow.web.forms.schema import LocalCustomerRef, PendingCustomerRef
|
||||||
|
|
||||||
|
|
||||||
class NewOrderBatchView(BatchMasterView):
|
class NewOrderBatchView(BatchMasterView):
|
||||||
|
@ -87,6 +87,7 @@ class NewOrderBatchView(BatchMasterView):
|
||||||
'id',
|
'id',
|
||||||
'store_id',
|
'store_id',
|
||||||
'customer_id',
|
'customer_id',
|
||||||
|
'local_customer',
|
||||||
'pending_customer',
|
'pending_customer',
|
||||||
'customer_name',
|
'customer_name',
|
||||||
'phone_number',
|
'phone_number',
|
||||||
|
@ -115,9 +116,11 @@ class NewOrderBatchView(BatchMasterView):
|
||||||
'product_description',
|
'product_description',
|
||||||
'product_size',
|
'product_size',
|
||||||
'special_order',
|
'special_order',
|
||||||
|
'unit_price_quoted',
|
||||||
|
'case_size',
|
||||||
|
'case_price_quoted',
|
||||||
'order_qty',
|
'order_qty',
|
||||||
'order_uom',
|
'order_uom',
|
||||||
'case_size',
|
|
||||||
'total_price',
|
'total_price',
|
||||||
'status_code',
|
'status_code',
|
||||||
]
|
]
|
||||||
|
@ -138,6 +141,9 @@ class NewOrderBatchView(BatchMasterView):
|
||||||
""" """
|
""" """
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
|
|
||||||
|
# local_customer
|
||||||
|
f.set_node('local_customer', LocalCustomerRef(self.request))
|
||||||
|
|
||||||
# pending_customer
|
# pending_customer
|
||||||
f.set_node('pending_customer', PendingCustomerRef(self.request))
|
f.set_node('pending_customer', PendingCustomerRef(self.request))
|
||||||
|
|
||||||
|
@ -153,6 +159,14 @@ class NewOrderBatchView(BatchMasterView):
|
||||||
# order_uom
|
# order_uom
|
||||||
#g.set_renderer('order_uom', self.grid_render_enum, enum=enum.ORDER_UOM)
|
#g.set_renderer('order_uom', self.grid_render_enum, enum=enum.ORDER_UOM)
|
||||||
|
|
||||||
|
# unit_price_quoted
|
||||||
|
g.set_label('unit_price_quoted', "Unit Price", column_only=True)
|
||||||
|
g.set_renderer('unit_price_quoted', 'currency')
|
||||||
|
|
||||||
|
# case_price_quoted
|
||||||
|
g.set_label('case_price_quoted', "Case Price", column_only=True)
|
||||||
|
g.set_renderer('case_price_quoted', 'currency')
|
||||||
|
|
||||||
# total_price
|
# total_price
|
||||||
g.set_renderer('total_price', 'currency')
|
g.set_renderer('total_price', 'currency')
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,161 @@ Views for Customers
|
||||||
from wuttaweb.views import MasterView
|
from wuttaweb.views import MasterView
|
||||||
from wuttaweb.forms.schema import UserRef, WuttaEnum
|
from wuttaweb.forms.schema import UserRef, WuttaEnum
|
||||||
|
|
||||||
from sideshow.db.model import 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):
|
class PendingCustomerView(MasterView):
|
||||||
|
@ -71,12 +225,9 @@ class PendingCustomerView(MasterView):
|
||||||
'customer_id',
|
'customer_id',
|
||||||
'full_name',
|
'full_name',
|
||||||
'first_name',
|
'first_name',
|
||||||
'middle_name',
|
|
||||||
'last_name',
|
'last_name',
|
||||||
'phone_number',
|
'phone_number',
|
||||||
'phone_type',
|
|
||||||
'email_address',
|
'email_address',
|
||||||
'email_type',
|
|
||||||
'status',
|
'status',
|
||||||
'created',
|
'created',
|
||||||
'created_by',
|
'created_by',
|
||||||
|
@ -238,6 +389,9 @@ class PendingCustomerView(MasterView):
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
|
LocalCustomerView = kwargs.get('LocalCustomerView', base['LocalCustomerView'])
|
||||||
|
LocalCustomerView.defaults(config)
|
||||||
|
|
||||||
PendingCustomerView = kwargs.get('PendingCustomerView', base['PendingCustomerView'])
|
PendingCustomerView = kwargs.get('PendingCustomerView', base['PendingCustomerView'])
|
||||||
PendingCustomerView.defaults(config)
|
PendingCustomerView.defaults(config)
|
||||||
|
|
||||||
|
|
|
@ -31,11 +31,13 @@ import colander
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
|
|
||||||
from wuttaweb.views import MasterView
|
from wuttaweb.views import MasterView
|
||||||
from wuttaweb.forms.schema import UserRef, WuttaMoney, WuttaQuantity, WuttaEnum
|
from wuttaweb.forms.schema import UserRef, WuttaMoney, WuttaQuantity, WuttaEnum, WuttaDictEnum
|
||||||
|
|
||||||
from sideshow.db.model import Order, OrderItem
|
from sideshow.db.model import Order, OrderItem
|
||||||
from sideshow.batch.neworder import NewOrderBatchHandler
|
from sideshow.batch.neworder import NewOrderBatchHandler
|
||||||
from sideshow.web.forms.schema import OrderRef, PendingCustomerRef, PendingProductRef
|
from sideshow.web.forms.schema import (OrderRef,
|
||||||
|
LocalCustomerRef, LocalProductRef,
|
||||||
|
PendingCustomerRef, PendingProductRef)
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -82,6 +84,7 @@ class OrderView(MasterView):
|
||||||
'order_id',
|
'order_id',
|
||||||
'store_id',
|
'store_id',
|
||||||
'customer_id',
|
'customer_id',
|
||||||
|
'local_customer',
|
||||||
'pending_customer',
|
'pending_customer',
|
||||||
'customer_name',
|
'customer_name',
|
||||||
'phone_number',
|
'phone_number',
|
||||||
|
@ -165,6 +168,20 @@ class OrderView(MasterView):
|
||||||
batch, which in turn creates a true
|
batch, which in turn creates a true
|
||||||
:class:`~sideshow.db.model.orders.Order`, and user is
|
:class:`~sideshow.db.model.orders.Order`, and user is
|
||||||
redirected to the "view order" page.
|
redirected to the "view order" page.
|
||||||
|
|
||||||
|
See also these methods which may be called from this one,
|
||||||
|
based on user actions:
|
||||||
|
|
||||||
|
* :meth:`start_over()`
|
||||||
|
* :meth:`cancel_order()`
|
||||||
|
* :meth:`assign_customer()`
|
||||||
|
* :meth:`unassign_customer()`
|
||||||
|
* :meth:`set_pending_customer()`
|
||||||
|
* :meth:`get_product_info()`
|
||||||
|
* :meth:`add_item()`
|
||||||
|
* :meth:`update_item()`
|
||||||
|
* :meth:`delete_item()`
|
||||||
|
* :meth:`submit_order()`
|
||||||
"""
|
"""
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
self.creating = True
|
self.creating = True
|
||||||
|
@ -188,22 +205,25 @@ class OrderView(MasterView):
|
||||||
data = dict(self.request.json_body)
|
data = dict(self.request.json_body)
|
||||||
action = data.pop('action')
|
action = data.pop('action')
|
||||||
json_actions = [
|
json_actions = [
|
||||||
# 'assign_contact',
|
'assign_customer',
|
||||||
# 'unassign_contact',
|
'unassign_customer',
|
||||||
# 'update_phone_number',
|
# 'update_phone_number',
|
||||||
# 'update_email_address',
|
# 'update_email_address',
|
||||||
'set_pending_customer',
|
'set_pending_customer',
|
||||||
# 'get_customer_info',
|
# 'get_customer_info',
|
||||||
# # 'set_customer_data',
|
# # 'set_customer_data',
|
||||||
# 'get_product_info',
|
'get_product_info',
|
||||||
# 'get_past_items',
|
# 'get_past_items',
|
||||||
'add_item',
|
'add_item',
|
||||||
'update_item',
|
'update_item',
|
||||||
'delete_item',
|
'delete_item',
|
||||||
'submit_new_order',
|
'submit_order',
|
||||||
]
|
]
|
||||||
if action in json_actions:
|
if action in json_actions:
|
||||||
|
try:
|
||||||
result = getattr(self, action)(batch, data)
|
result = getattr(self, action)(batch, data)
|
||||||
|
except Exception as error:
|
||||||
|
result = {'error': self.app.render_error(error)}
|
||||||
return self.json_response(result)
|
return self.json_response(result)
|
||||||
|
|
||||||
return self.json_response({'error': "unknown form action"})
|
return self.json_response({'error': "unknown form action"})
|
||||||
|
@ -215,7 +235,7 @@ class OrderView(MasterView):
|
||||||
for row in batch.rows],
|
for row in batch.rows],
|
||||||
'default_uom_choices': self.get_default_uom_choices(),
|
'default_uom_choices': self.get_default_uom_choices(),
|
||||||
'default_uom': None, # TODO?
|
'default_uom': None, # TODO?
|
||||||
'allow_unknown_product': (self.batch_handler.allow_unknown_product()
|
'allow_unknown_products': (self.batch_handler.allow_unknown_products()
|
||||||
and self.has_perm('create_unknown_product')),
|
and self.has_perm('create_unknown_product')),
|
||||||
'pending_product_required_fields': self.get_pending_product_required_fields(),
|
'pending_product_required_fields': self.get_pending_product_required_fields(),
|
||||||
})
|
})
|
||||||
|
@ -255,6 +275,96 @@ class OrderView(MasterView):
|
||||||
|
|
||||||
return batch
|
return batch
|
||||||
|
|
||||||
|
def customer_autocomplete(self):
|
||||||
|
"""
|
||||||
|
AJAX view for customer autocomplete, when entering new order.
|
||||||
|
|
||||||
|
This should invoke a configured handler for the autocomplete
|
||||||
|
behavior, but that is not yet implemented. For now it uses
|
||||||
|
built-in logic only, which queries the
|
||||||
|
:class:`~sideshow.db.model.customers.LocalCustomer` table.
|
||||||
|
"""
|
||||||
|
session = self.Session()
|
||||||
|
term = self.request.GET.get('term', '').strip()
|
||||||
|
if not term:
|
||||||
|
return []
|
||||||
|
return self.mock_autocomplete_customers(session, term, user=self.request.user)
|
||||||
|
|
||||||
|
# TODO: move this to some handler
|
||||||
|
def mock_autocomplete_customers(self, session, term, user=None):
|
||||||
|
""" """
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
# base query
|
||||||
|
query = session.query(model.LocalCustomer)
|
||||||
|
|
||||||
|
# filter query
|
||||||
|
criteria = [model.LocalCustomer.full_name.ilike(f'%{word}%')
|
||||||
|
for word in term.split()]
|
||||||
|
query = query.filter(sa.and_(*criteria))
|
||||||
|
|
||||||
|
# sort query
|
||||||
|
query = query.order_by(model.LocalCustomer.full_name)
|
||||||
|
|
||||||
|
# get data
|
||||||
|
# TODO: need max_results option
|
||||||
|
customers = query.all()
|
||||||
|
|
||||||
|
# get results
|
||||||
|
def result(customer):
|
||||||
|
return {'value': customer.uuid.hex,
|
||||||
|
'label': customer.full_name}
|
||||||
|
return [result(c) for c in customers]
|
||||||
|
|
||||||
|
def product_autocomplete(self):
|
||||||
|
"""
|
||||||
|
AJAX view for product autocomplete, when entering new order.
|
||||||
|
|
||||||
|
This should invoke a configured handler for the autocomplete
|
||||||
|
behavior, but that is not yet implemented. For now it uses
|
||||||
|
built-in logic only, which queries the
|
||||||
|
:class:`~sideshow.db.model.products.LocalProduct` table.
|
||||||
|
"""
|
||||||
|
session = self.Session()
|
||||||
|
term = self.request.GET.get('term', '').strip()
|
||||||
|
if not term:
|
||||||
|
return []
|
||||||
|
return self.mock_autocomplete_products(session, term, user=self.request.user)
|
||||||
|
|
||||||
|
# TODO: move this to some handler
|
||||||
|
def mock_autocomplete_products(self, session, term, user=None):
|
||||||
|
""" """
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
# base query
|
||||||
|
query = session.query(model.LocalProduct)
|
||||||
|
|
||||||
|
# filter query
|
||||||
|
criteria = []
|
||||||
|
for word in term.split():
|
||||||
|
criteria.append(sa.or_(
|
||||||
|
model.LocalProduct.brand_name.ilike(f'%{word}%'),
|
||||||
|
model.LocalProduct.description.ilike(f'%{word}%')))
|
||||||
|
query = query.filter(sa.and_(*criteria))
|
||||||
|
|
||||||
|
# sort query
|
||||||
|
query = query.order_by(model.LocalProduct.brand_name,
|
||||||
|
model.LocalProduct.description)
|
||||||
|
|
||||||
|
# get data
|
||||||
|
# TODO: need max_results option
|
||||||
|
products = query.all()
|
||||||
|
|
||||||
|
# get results
|
||||||
|
def result(product):
|
||||||
|
return {'value': product.uuid.hex,
|
||||||
|
'label': product.full_description}
|
||||||
|
return [result(c) for c in products]
|
||||||
|
|
||||||
def get_pending_product_required_fields(self):
|
def get_pending_product_required_fields(self):
|
||||||
""" """
|
""" """
|
||||||
required = []
|
required = []
|
||||||
|
@ -274,7 +384,10 @@ class OrderView(MasterView):
|
||||||
new batch for them.
|
new batch for them.
|
||||||
|
|
||||||
This is a "batch action" method which may be called from
|
This is a "batch action" method which may be called from
|
||||||
:meth:`create()`.
|
:meth:`create()`. See also:
|
||||||
|
|
||||||
|
* :meth:`cancel_order()`
|
||||||
|
* :meth:`submit_order()`
|
||||||
"""
|
"""
|
||||||
# drop current batch
|
# drop current batch
|
||||||
self.batch_handler.do_delete(batch, self.request.user)
|
self.batch_handler.do_delete(batch, self.request.user)
|
||||||
|
@ -291,7 +404,10 @@ class OrderView(MasterView):
|
||||||
back to "List Orders" page.
|
back to "List Orders" page.
|
||||||
|
|
||||||
This is a "batch action" method which may be called from
|
This is a "batch action" method which may be called from
|
||||||
:meth:`create()`.
|
:meth:`create()`. See also:
|
||||||
|
|
||||||
|
* :meth:`start_over()`
|
||||||
|
* :meth:`submit_order()`
|
||||||
"""
|
"""
|
||||||
self.batch_handler.do_delete(batch, self.request.user)
|
self.batch_handler.do_delete(batch, self.request.user)
|
||||||
self.Session.flush()
|
self.Session.flush()
|
||||||
|
@ -306,86 +422,193 @@ class OrderView(MasterView):
|
||||||
def get_context_customer(self, batch):
|
def get_context_customer(self, batch):
|
||||||
""" """
|
""" """
|
||||||
context = {
|
context = {
|
||||||
'customer_id': batch.customer_id,
|
'customer_is_known': True,
|
||||||
|
'customer_id': None,
|
||||||
'customer_name': batch.customer_name,
|
'customer_name': batch.customer_name,
|
||||||
'phone_number': batch.phone_number,
|
'phone_number': batch.phone_number,
|
||||||
'email_address': batch.email_address,
|
'email_address': batch.email_address,
|
||||||
'new_customer_name': None,
|
|
||||||
'new_customer_first_name': None,
|
|
||||||
'new_customer_last_name': None,
|
|
||||||
'new_customer_phone': None,
|
|
||||||
'new_customer_email': None,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# customer_id
|
||||||
|
use_local = self.batch_handler.use_local_customers()
|
||||||
|
if use_local:
|
||||||
|
local = batch.local_customer
|
||||||
|
if local:
|
||||||
|
context['customer_id'] = local.uuid.hex
|
||||||
|
else: # use external
|
||||||
|
context['customer_id'] = batch.customer_id
|
||||||
|
|
||||||
|
# pending customer
|
||||||
pending = batch.pending_customer
|
pending = batch.pending_customer
|
||||||
if pending:
|
if pending:
|
||||||
context.update({
|
context.update({
|
||||||
'new_customer_first_name': pending.first_name,
|
'new_customer_first_name': pending.first_name,
|
||||||
'new_customer_last_name': pending.last_name,
|
'new_customer_last_name': pending.last_name,
|
||||||
'new_customer_name': pending.full_name,
|
'new_customer_full_name': pending.full_name,
|
||||||
'new_customer_phone': pending.phone_number,
|
'new_customer_phone': pending.phone_number,
|
||||||
'new_customer_email': pending.email_address,
|
'new_customer_email': pending.email_address,
|
||||||
})
|
})
|
||||||
|
|
||||||
# figure out if customer is "known" from user's perspective.
|
# declare customer "not known" only if pending is in use
|
||||||
# if we have an ID then it's definitely known, otherwise if we
|
if (pending
|
||||||
# have a pending customer then it's definitely *not* known,
|
and not batch.customer_id and not batch.local_customer
|
||||||
# but if no pending customer yet then we can still "assume" it
|
and batch.customer_name):
|
||||||
# is known, by default, until user specifies otherwise.
|
context['customer_is_known'] = False
|
||||||
if batch.customer_id:
|
|
||||||
context['customer_is_known'] = True
|
|
||||||
else:
|
|
||||||
context['customer_is_known'] = not pending
|
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def assign_customer(self, batch, data):
|
||||||
|
"""
|
||||||
|
Assign the true customer account for a batch.
|
||||||
|
|
||||||
|
This calls
|
||||||
|
:meth:`~sideshow.batch.neworder.NewOrderBatchHandler.set_customer()`
|
||||||
|
for the heavy lifting.
|
||||||
|
|
||||||
|
This is a "batch action" method which may be called from
|
||||||
|
:meth:`create()`. See also:
|
||||||
|
|
||||||
|
* :meth:`unassign_customer()`
|
||||||
|
* :meth:`set_pending_customer()`
|
||||||
|
"""
|
||||||
|
customer_id = data.get('customer_id')
|
||||||
|
if not customer_id:
|
||||||
|
return {'error': "Must provide customer_id"}
|
||||||
|
|
||||||
|
self.batch_handler.set_customer(batch, customer_id)
|
||||||
|
return self.get_context_customer(batch)
|
||||||
|
|
||||||
|
def unassign_customer(self, batch, data):
|
||||||
|
"""
|
||||||
|
Clear the customer info for a batch.
|
||||||
|
|
||||||
|
This calls
|
||||||
|
:meth:`~sideshow.batch.neworder.NewOrderBatchHandler.set_customer()`
|
||||||
|
for the heavy lifting.
|
||||||
|
|
||||||
|
This is a "batch action" method which may be called from
|
||||||
|
:meth:`create()`. See also:
|
||||||
|
|
||||||
|
* :meth:`assign_customer()`
|
||||||
|
* :meth:`set_pending_customer()`
|
||||||
|
"""
|
||||||
|
self.batch_handler.set_customer(batch, None)
|
||||||
|
return self.get_context_customer(batch)
|
||||||
|
|
||||||
def set_pending_customer(self, batch, data):
|
def set_pending_customer(self, batch, data):
|
||||||
"""
|
"""
|
||||||
This will set/update the batch pending customer info.
|
This will set/update the batch pending customer info.
|
||||||
|
|
||||||
This calls
|
This calls
|
||||||
:meth:`~sideshow.batch.neworder.NewOrderBatchHandler.set_pending_customer()`
|
:meth:`~sideshow.batch.neworder.NewOrderBatchHandler.set_customer()`
|
||||||
for the heavy lifting.
|
for the heavy lifting.
|
||||||
|
|
||||||
|
This is a "batch action" method which may be called from
|
||||||
|
:meth:`create()`. See also:
|
||||||
|
|
||||||
|
* :meth:`assign_customer()`
|
||||||
|
* :meth:`unassign_customer()`
|
||||||
|
"""
|
||||||
|
self.batch_handler.set_customer(batch, data, user=self.request.user)
|
||||||
|
return self.get_context_customer(batch)
|
||||||
|
|
||||||
|
def get_product_info(self, batch, data):
|
||||||
|
"""
|
||||||
|
Fetch data for a specific product. (Nothing is modified.)
|
||||||
|
|
||||||
|
Depending on config, this will fetch a :term:`local product`
|
||||||
|
or :term:`external product` to get the data.
|
||||||
|
|
||||||
|
This should invoke a configured handler for the query
|
||||||
|
behavior, but that is not yet implemented. For now it uses
|
||||||
|
built-in logic only, which queries the
|
||||||
|
:class:`~sideshow.db.model.products.LocalProduct` table.
|
||||||
|
|
||||||
This is a "batch action" method which may be called from
|
This is a "batch action" method which may be called from
|
||||||
:meth:`create()`.
|
:meth:`create()`.
|
||||||
"""
|
"""
|
||||||
data['created_by'] = self.request.user
|
product_id = data.get('product_id')
|
||||||
try:
|
if not product_id:
|
||||||
self.batch_handler.set_pending_customer(batch, data)
|
return {'error': "Must specify a product ID"}
|
||||||
except Exception as error:
|
|
||||||
return {'error': self.app.render_error(error)}
|
|
||||||
|
|
||||||
self.Session.flush()
|
use_local = self.batch_handler.use_local_products()
|
||||||
context = self.get_context_customer(batch)
|
if use_local:
|
||||||
return context
|
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):
|
def add_item(self, batch, data):
|
||||||
"""
|
"""
|
||||||
This adds a row to the user's current new order batch.
|
This adds a row to the user's current new order batch.
|
||||||
|
|
||||||
This is a "batch action" method which may be called from
|
This is a "batch action" method which may be called from
|
||||||
:meth:`create()`.
|
:meth:`create()`. See also:
|
||||||
|
|
||||||
|
* :meth:`update_item()`
|
||||||
|
* :meth:`delete_item()`
|
||||||
"""
|
"""
|
||||||
order_qty = decimal.Decimal(data.get('order_qty') or '0')
|
row = self.batch_handler.add_item(batch, data['product_info'],
|
||||||
order_uom = data['order_uom']
|
data['order_qty'], data['order_uom'])
|
||||||
|
|
||||||
if data.get('product_is_known'):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
else: # unknown product; add pending
|
|
||||||
pending = data['pending_product']
|
|
||||||
|
|
||||||
for field in ('unit_cost', 'unit_price_reg', 'case_size'):
|
|
||||||
if field in pending:
|
|
||||||
try:
|
|
||||||
pending[field] = decimal.Decimal(pending[field])
|
|
||||||
except decimal.InvalidOperation:
|
|
||||||
return {'error': f"Invalid entry for field: {field}"}
|
|
||||||
|
|
||||||
pending['created_by'] = self.request.user
|
|
||||||
row = self.batch_handler.add_pending_product(batch, pending,
|
|
||||||
order_qty, order_uom)
|
|
||||||
|
|
||||||
return {'batch': self.normalize_batch(batch),
|
return {'batch': self.normalize_batch(batch),
|
||||||
'row': self.normalize_row(row)}
|
'row': self.normalize_row(row)}
|
||||||
|
@ -395,15 +618,17 @@ class OrderView(MasterView):
|
||||||
This updates a row in the user's current new order batch.
|
This updates a row in the user's current new order batch.
|
||||||
|
|
||||||
This is a "batch action" method which may be called from
|
This is a "batch action" method which may be called from
|
||||||
:meth:`create()`.
|
:meth:`create()`. See also:
|
||||||
|
|
||||||
|
* :meth:`add_item()`
|
||||||
|
* :meth:`delete_item()`
|
||||||
"""
|
"""
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
enum = self.app.enum
|
|
||||||
session = self.Session()
|
session = self.Session()
|
||||||
|
|
||||||
uuid = data.get('uuid')
|
uuid = data.get('uuid')
|
||||||
if not uuid:
|
if not uuid:
|
||||||
return {'error': "Must specify a row UUID"}
|
return {'error': "Must specify row UUID"}
|
||||||
|
|
||||||
row = session.get(model.NewOrderBatchRow, uuid)
|
row = session.get(model.NewOrderBatchRow, uuid)
|
||||||
if not row:
|
if not row:
|
||||||
|
@ -412,20 +637,8 @@ class OrderView(MasterView):
|
||||||
if row.batch is not batch:
|
if row.batch is not batch:
|
||||||
return {'error': "Row is for wrong batch"}
|
return {'error': "Row is for wrong batch"}
|
||||||
|
|
||||||
order_qty = decimal.Decimal(data.get('order_qty') or '0')
|
self.batch_handler.update_item(row, data['product_info'],
|
||||||
order_uom = data['order_uom']
|
data['order_qty'], data['order_uom'])
|
||||||
|
|
||||||
if data.get('product_is_known'):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
else: # pending product
|
|
||||||
|
|
||||||
# set these first, since row will be refreshed below
|
|
||||||
row.order_qty = order_qty
|
|
||||||
row.order_uom = order_uom
|
|
||||||
|
|
||||||
# nb. this will refresh the row
|
|
||||||
self.batch_handler.set_pending_product(row, data['pending_product'])
|
|
||||||
|
|
||||||
return {'batch': self.normalize_batch(batch),
|
return {'batch': self.normalize_batch(batch),
|
||||||
'row': self.normalize_row(row)}
|
'row': self.normalize_row(row)}
|
||||||
|
@ -435,7 +648,10 @@ class OrderView(MasterView):
|
||||||
This deletes a row from the user's current new order batch.
|
This deletes a row from the user's current new order batch.
|
||||||
|
|
||||||
This is a "batch action" method which may be called from
|
This is a "batch action" method which may be called from
|
||||||
:meth:`create()`.
|
:meth:`create()`. See also:
|
||||||
|
|
||||||
|
* :meth:`add_item()`
|
||||||
|
* :meth:`update_item()`
|
||||||
"""
|
"""
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
session = self.app.get_session(batch)
|
session = self.app.get_session(batch)
|
||||||
|
@ -452,16 +668,18 @@ class OrderView(MasterView):
|
||||||
return {'error': "Row is for wrong batch"}
|
return {'error': "Row is for wrong batch"}
|
||||||
|
|
||||||
self.batch_handler.do_remove_row(row)
|
self.batch_handler.do_remove_row(row)
|
||||||
session.flush()
|
|
||||||
return {'batch': self.normalize_batch(batch)}
|
return {'batch': self.normalize_batch(batch)}
|
||||||
|
|
||||||
def submit_new_order(self, batch, data):
|
def submit_order(self, batch, data):
|
||||||
"""
|
"""
|
||||||
This submits the user's current new order batch, hence
|
This submits the user's current new order batch, hence
|
||||||
executing the batch and creating the true order.
|
executing the batch and creating the true order.
|
||||||
|
|
||||||
This is a "batch action" method which may be called from
|
This is a "batch action" method which may be called from
|
||||||
:meth:`create()`.
|
:meth:`create()`. See also:
|
||||||
|
|
||||||
|
* :meth:`start_over()`
|
||||||
|
* :meth:`cancel_order()`
|
||||||
"""
|
"""
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
reason = self.batch_handler.why_not_execute(batch, user=user)
|
reason = self.batch_handler.why_not_execute(batch, user=user)
|
||||||
|
@ -502,6 +720,7 @@ class OrderView(MasterView):
|
||||||
data = {
|
data = {
|
||||||
'uuid': row.uuid.hex,
|
'uuid': row.uuid.hex,
|
||||||
'sequence': row.sequence,
|
'sequence': row.sequence,
|
||||||
|
'product_id': None,
|
||||||
'product_scancode': row.product_scancode,
|
'product_scancode': row.product_scancode,
|
||||||
'product_brand': row.product_brand,
|
'product_brand': row.product_brand,
|
||||||
'product_description': row.product_description,
|
'product_description': row.product_description,
|
||||||
|
@ -509,8 +728,8 @@ class OrderView(MasterView):
|
||||||
'product_weighed': row.product_weighed,
|
'product_weighed': row.product_weighed,
|
||||||
'department_display': row.department_name,
|
'department_display': row.department_name,
|
||||||
'special_order': row.special_order,
|
'special_order': row.special_order,
|
||||||
'case_size': self.app.render_quantity(row.case_size),
|
'case_size': float(row.case_size) if row.case_size is not None else None,
|
||||||
'order_qty': self.app.render_quantity(row.order_qty),
|
'order_qty': float(row.order_qty),
|
||||||
'order_uom': row.order_uom,
|
'order_uom': row.order_uom,
|
||||||
'order_uom_choices': self.get_default_uom_choices(),
|
'order_uom_choices': self.get_default_uom_choices(),
|
||||||
'unit_price_quoted': float(row.unit_price_quoted) if row.unit_price_quoted is not None else None,
|
'unit_price_quoted': float(row.unit_price_quoted) if row.unit_price_quoted is not None else None,
|
||||||
|
@ -523,6 +742,33 @@ class OrderView(MasterView):
|
||||||
'status_text': row.status_text,
|
'status_text': row.status_text,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use_local = self.batch_handler.use_local_products()
|
||||||
|
|
||||||
|
# product_id
|
||||||
|
if use_local:
|
||||||
|
if row.local_product:
|
||||||
|
data['product_id'] = row.local_product.uuid.hex
|
||||||
|
else:
|
||||||
|
data['product_id'] = row.product_id
|
||||||
|
|
||||||
|
# product_full_description
|
||||||
|
if use_local:
|
||||||
|
if row.local_product:
|
||||||
|
data['product_full_description'] = row.local_product.full_description
|
||||||
|
else: # use external
|
||||||
|
pass # TODO
|
||||||
|
if not data.get('product_id') and row.pending_product:
|
||||||
|
data['product_full_description'] = row.pending_product.full_description
|
||||||
|
|
||||||
|
# vendor_name
|
||||||
|
if use_local:
|
||||||
|
if row.local_product:
|
||||||
|
data['vendor_name'] = row.local_product.vendor_name
|
||||||
|
else: # use external
|
||||||
|
pass # TODO
|
||||||
|
if not data.get('product_id') and row.pending_product:
|
||||||
|
data['vendor_name'] = row.pending_product.vendor_name
|
||||||
|
|
||||||
if row.unit_price_reg:
|
if row.unit_price_reg:
|
||||||
data['unit_price_reg'] = float(row.unit_price_reg)
|
data['unit_price_reg'] = float(row.unit_price_reg)
|
||||||
data['unit_price_reg_display'] = self.app.render_currency(row.unit_price_reg)
|
data['unit_price_reg_display'] = self.app.render_currency(row.unit_price_reg)
|
||||||
|
@ -535,21 +781,8 @@ class OrderView(MasterView):
|
||||||
data['sale_ends'] = str(row.sale_ends)
|
data['sale_ends'] = str(row.sale_ends)
|
||||||
data['sale_ends_display'] = self.app.render_date(row.sale_ends)
|
data['sale_ends_display'] = self.app.render_date(row.sale_ends)
|
||||||
|
|
||||||
# if row.unit_price_sale and row.unit_price_quoted == row.unit_price_sale:
|
|
||||||
# data['pricing_reflects_sale'] = True
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
if row.pending_product:
|
|
||||||
data['product_full_description'] = row.pending_product.full_description
|
|
||||||
# else:
|
|
||||||
# data['product_full_description'] = row.product_description
|
|
||||||
|
|
||||||
# if row.pending_product:
|
|
||||||
# data['vendor_display'] = row.pending_product.vendor_name
|
|
||||||
|
|
||||||
if row.pending_product:
|
if row.pending_product:
|
||||||
pending = row.pending_product
|
pending = row.pending_product
|
||||||
# data['vendor_display'] = pending.vendor_name
|
|
||||||
data['pending_product'] = {
|
data['pending_product'] = {
|
||||||
'uuid': pending.uuid.hex,
|
'uuid': pending.uuid.hex,
|
||||||
'scancode': pending.scancode,
|
'scancode': pending.scancode,
|
||||||
|
@ -569,14 +802,15 @@ class OrderView(MasterView):
|
||||||
|
|
||||||
# display text for order qty/uom
|
# display text for order qty/uom
|
||||||
if row.order_uom == enum.ORDER_UOM_CASE:
|
if row.order_uom == enum.ORDER_UOM_CASE:
|
||||||
|
order_qty = self.app.render_quantity(row.order_qty)
|
||||||
if row.case_size is None:
|
if row.case_size is None:
|
||||||
case_qty = unit_qty = '??'
|
case_qty = unit_qty = '??'
|
||||||
else:
|
else:
|
||||||
case_qty = data['case_size']
|
case_qty = self.app.render_quantity(row.case_size)
|
||||||
unit_qty = self.app.render_quantity(row.order_qty * row.case_size)
|
unit_qty = self.app.render_quantity(row.order_qty * row.case_size)
|
||||||
CS = enum.ORDER_UOM[enum.ORDER_UOM_CASE]
|
CS = enum.ORDER_UOM[enum.ORDER_UOM_CASE]
|
||||||
EA = enum.ORDER_UOM[enum.ORDER_UOM_UNIT]
|
EA = enum.ORDER_UOM[enum.ORDER_UOM_UNIT]
|
||||||
data['order_qty_display'] = (f"{data['order_qty']} {CS} "
|
data['order_qty_display'] = (f"{order_qty} {CS} "
|
||||||
f"(× {case_qty} = {unit_qty} {EA})")
|
f"(× {case_qty} = {unit_qty} {EA})")
|
||||||
else:
|
else:
|
||||||
unit_qty = self.app.render_quantity(row.order_qty)
|
unit_qty = self.app.render_quantity(row.order_qty)
|
||||||
|
@ -592,8 +826,15 @@ class OrderView(MasterView):
|
||||||
def configure_form(self, f):
|
def configure_form(self, f):
|
||||||
""" """
|
""" """
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
|
order = f.model_instance
|
||||||
|
|
||||||
|
# local_customer
|
||||||
|
f.set_node('local_customer', LocalCustomerRef(self.request))
|
||||||
|
|
||||||
# pending_customer
|
# pending_customer
|
||||||
|
if order.customer_id or order.local_customer:
|
||||||
|
f.remove('pending_customer')
|
||||||
|
else:
|
||||||
f.set_node('pending_customer', PendingCustomerRef(self.request))
|
f.set_node('pending_customer', PendingCustomerRef(self.request))
|
||||||
|
|
||||||
# total_price
|
# total_price
|
||||||
|
@ -672,7 +913,7 @@ class OrderView(MasterView):
|
||||||
settings = [
|
settings = [
|
||||||
|
|
||||||
# products
|
# products
|
||||||
{'name': 'sideshow.orders.allow_unknown_product',
|
{'name': 'sideshow.orders.allow_unknown_products',
|
||||||
'type': bool,
|
'type': bool,
|
||||||
'default': True},
|
'default': True},
|
||||||
]
|
]
|
||||||
|
@ -702,7 +943,9 @@ class OrderView(MasterView):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _order_defaults(cls, config):
|
def _order_defaults(cls, config):
|
||||||
|
route_prefix = cls.get_route_prefix()
|
||||||
permission_prefix = cls.get_permission_prefix()
|
permission_prefix = cls.get_permission_prefix()
|
||||||
|
url_prefix = cls.get_url_prefix()
|
||||||
model_title = cls.get_model_title()
|
model_title = cls.get_model_title()
|
||||||
model_title_plural = cls.get_model_title_plural()
|
model_title_plural = cls.get_model_title_plural()
|
||||||
|
|
||||||
|
@ -716,6 +959,24 @@ class OrderView(MasterView):
|
||||||
f'{permission_prefix}.create_unknown_product',
|
f'{permission_prefix}.create_unknown_product',
|
||||||
f"Create new {model_title} for unknown/pending product")
|
f"Create new {model_title} for unknown/pending product")
|
||||||
|
|
||||||
|
# customer autocomplete
|
||||||
|
config.add_route(f'{route_prefix}.customer_autocomplete',
|
||||||
|
f'{url_prefix}/customer-autocomplete',
|
||||||
|
request_method='GET')
|
||||||
|
config.add_view(cls, attr='customer_autocomplete',
|
||||||
|
route_name=f'{route_prefix}.customer_autocomplete',
|
||||||
|
renderer='json',
|
||||||
|
permission=f'{permission_prefix}.list')
|
||||||
|
|
||||||
|
# product autocomplete
|
||||||
|
config.add_route(f'{route_prefix}.product_autocomplete',
|
||||||
|
f'{url_prefix}/product-autocomplete',
|
||||||
|
request_method='GET')
|
||||||
|
config.add_view(cls, attr='product_autocomplete',
|
||||||
|
route_name=f'{route_prefix}.product_autocomplete',
|
||||||
|
renderer='json',
|
||||||
|
permission=f'{permission_prefix}.list')
|
||||||
|
|
||||||
|
|
||||||
class OrderItemView(MasterView):
|
class OrderItemView(MasterView):
|
||||||
"""
|
"""
|
||||||
|
@ -745,7 +1006,8 @@ class OrderItemView(MasterView):
|
||||||
'product_brand': "Brand",
|
'product_brand': "Brand",
|
||||||
'product_description': "Description",
|
'product_description': "Description",
|
||||||
'product_size': "Size",
|
'product_size': "Size",
|
||||||
'department_name': "Department",
|
'product_weighed': "Sold by Weight",
|
||||||
|
'department_id': "Department ID",
|
||||||
'order_uom': "Order UOM",
|
'order_uom': "Order UOM",
|
||||||
'status_code': "Status",
|
'status_code': "Status",
|
||||||
}
|
}
|
||||||
|
@ -773,6 +1035,7 @@ class OrderItemView(MasterView):
|
||||||
# 'customer_name',
|
# 'customer_name',
|
||||||
'sequence',
|
'sequence',
|
||||||
'product_id',
|
'product_id',
|
||||||
|
'local_product',
|
||||||
'pending_product',
|
'pending_product',
|
||||||
'product_scancode',
|
'product_scancode',
|
||||||
'product_brand',
|
'product_brand',
|
||||||
|
@ -852,27 +1115,46 @@ class OrderItemView(MasterView):
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
return enum.ORDER_ITEM_STATUS[value]
|
return enum.ORDER_ITEM_STATUS[value]
|
||||||
|
|
||||||
|
def get_instance_title(self, item):
|
||||||
|
""" """
|
||||||
|
enum = self.app.enum
|
||||||
|
title = str(item)
|
||||||
|
status = enum.ORDER_ITEM_STATUS[item.status_code]
|
||||||
|
return f"({status}) {title}"
|
||||||
|
|
||||||
def configure_form(self, f):
|
def configure_form(self, f):
|
||||||
""" """
|
""" """
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
|
item = f.model_instance
|
||||||
|
|
||||||
# order
|
# order
|
||||||
f.set_node('order', OrderRef(self.request))
|
f.set_node('order', OrderRef(self.request))
|
||||||
|
|
||||||
|
# local_product
|
||||||
|
f.set_node('local_product', LocalProductRef(self.request))
|
||||||
|
|
||||||
# pending_product
|
# pending_product
|
||||||
|
if item.product_id or item.local_product:
|
||||||
|
f.remove('pending_product')
|
||||||
|
else:
|
||||||
f.set_node('pending_product', PendingProductRef(self.request))
|
f.set_node('pending_product', PendingProductRef(self.request))
|
||||||
|
|
||||||
# order_qty
|
# order_qty
|
||||||
f.set_node('order_qty', WuttaQuantity(self.request))
|
f.set_node('order_qty', WuttaQuantity(self.request))
|
||||||
|
|
||||||
# order_uom
|
# order_uom
|
||||||
# TODO
|
f.set_node('order_uom', WuttaDictEnum(self.request, enum.ORDER_UOM))
|
||||||
#f.set_node('order_uom', WuttaEnum(self.request, enum.OrderUOM))
|
|
||||||
|
|
||||||
# case_size
|
# case_size
|
||||||
f.set_node('case_size', WuttaQuantity(self.request))
|
f.set_node('case_size', WuttaQuantity(self.request))
|
||||||
|
|
||||||
|
# unit_cost
|
||||||
|
f.set_node('unit_cost', WuttaMoney(self.request, scale=4))
|
||||||
|
|
||||||
|
# unit_price_reg
|
||||||
|
f.set_node('unit_price_reg', WuttaMoney(self.request))
|
||||||
|
|
||||||
# unit_price_quoted
|
# unit_price_quoted
|
||||||
f.set_node('unit_price_quoted', WuttaMoney(self.request))
|
f.set_node('unit_price_quoted', WuttaMoney(self.request))
|
||||||
|
|
||||||
|
@ -882,18 +1164,21 @@ class OrderItemView(MasterView):
|
||||||
# total_price
|
# total_price
|
||||||
f.set_node('total_price', WuttaMoney(self.request))
|
f.set_node('total_price', WuttaMoney(self.request))
|
||||||
|
|
||||||
|
# status
|
||||||
|
f.set_node('status_code', WuttaDictEnum(self.request, enum.ORDER_ITEM_STATUS))
|
||||||
|
|
||||||
# paid_amount
|
# paid_amount
|
||||||
f.set_node('paid_amount', WuttaMoney(self.request))
|
f.set_node('paid_amount', WuttaMoney(self.request))
|
||||||
|
|
||||||
def get_xref_buttons(self, item):
|
def get_xref_buttons(self, item):
|
||||||
""" """
|
""" """
|
||||||
buttons = super().get_xref_buttons(item)
|
buttons = super().get_xref_buttons(item)
|
||||||
model = self.app.model
|
|
||||||
|
|
||||||
if self.request.has_perm('orders.view'):
|
if self.request.has_perm('orders.view'):
|
||||||
url = self.request.route_url('orders.view', uuid=item.order_uuid)
|
url = self.request.route_url('orders.view', uuid=item.order_uuid)
|
||||||
buttons.append(
|
buttons.append(
|
||||||
self.make_button("View the Order", primary=True, icon_left='eye', url=url))
|
self.make_button("View the Order", url=url,
|
||||||
|
primary=True, icon_left='eye'))
|
||||||
|
|
||||||
return buttons
|
return buttons
|
||||||
|
|
||||||
|
|
|
@ -25,9 +25,194 @@ Views for Products
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from wuttaweb.views import MasterView
|
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):
|
class PendingProductView(MasterView):
|
||||||
|
@ -249,6 +434,9 @@ class PendingProductView(MasterView):
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
|
LocalProductView = kwargs.get('LocalProductView', base['LocalProductView'])
|
||||||
|
LocalProductView.defaults(config)
|
||||||
|
|
||||||
PendingProductView = kwargs.get('PendingProductView', base['PendingProductView'])
|
PendingProductView = kwargs.get('PendingProductView', base['PendingProductView'])
|
||||||
PendingProductView.defaults(config)
|
PendingProductView.defaults(config)
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from wuttjamaican.testing import DataTestCase
|
from wuttjamaican.testing import DataTestCase
|
||||||
|
|
||||||
|
@ -19,71 +20,133 @@ class TestNewOrderBatchHandler(DataTestCase):
|
||||||
def make_handler(self):
|
def make_handler(self):
|
||||||
return mod.NewOrderBatchHandler(self.config)
|
return mod.NewOrderBatchHandler(self.config)
|
||||||
|
|
||||||
def tets_allow_unknown_product(self):
|
def tets_use_local_customers(self):
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
|
|
||||||
# true by default
|
# true by default
|
||||||
self.assertTrue(handler.allow_unknown_product())
|
self.assertTrue(handler.use_local_customers())
|
||||||
|
|
||||||
# config can disable
|
# config can disable
|
||||||
config.setdefault('sideshow.orders.allow_unknown_product', 'false')
|
config.setdefault('sideshow.orders.use_local_customers', 'false')
|
||||||
self.assertFalse(handler.allow_unknown_product())
|
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
|
model = self.app.model
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
|
|
||||||
user = model.User(username='barney')
|
user = model.User(username='barney')
|
||||||
self.session.add(user)
|
self.session.add(user)
|
||||||
|
|
||||||
batch = handler.make_batch(self.session, created_by=user, customer_id=42)
|
# customer starts blank
|
||||||
self.assertEqual(batch.customer_id, 42)
|
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.pending_customer)
|
||||||
self.assertIsNone(batch.customer_name)
|
self.assertIsNone(batch.customer_name)
|
||||||
self.assertIsNone(batch.phone_number)
|
self.assertIsNone(batch.phone_number)
|
||||||
self.assertIsNone(batch.email_address)
|
self.assertIsNone(batch.email_address)
|
||||||
|
|
||||||
# auto full_name
|
# pending, typical (nb. full name is automatic)
|
||||||
handler.set_pending_customer(batch, {
|
handler.set_customer(batch, {
|
||||||
'first_name': "Fred",
|
'first_name': "Fred",
|
||||||
'last_name': "Flintstone",
|
'last_name': "Flintstone",
|
||||||
'phone_number': '555-1234',
|
'phone_number': '555-1234',
|
||||||
'email_address': 'fred@mailinator.com',
|
'email_address': 'fred@mailinator.com',
|
||||||
})
|
})
|
||||||
self.assertIsNone(batch.customer_id)
|
self.assertIsNone(batch.customer_id)
|
||||||
|
self.assertIsNone(batch.local_customer)
|
||||||
self.assertIsInstance(batch.pending_customer, model.PendingCustomer)
|
self.assertIsInstance(batch.pending_customer, model.PendingCustomer)
|
||||||
customer = batch.pending_customer
|
customer = batch.pending_customer
|
||||||
self.assertEqual(customer.full_name, "Fred Flintstone")
|
|
||||||
self.assertEqual(customer.first_name, "Fred")
|
self.assertEqual(customer.first_name, "Fred")
|
||||||
self.assertEqual(customer.last_name, "Flintstone")
|
self.assertEqual(customer.last_name, "Flintstone")
|
||||||
|
self.assertEqual(customer.full_name, "Fred Flintstone")
|
||||||
self.assertEqual(customer.phone_number, '555-1234')
|
self.assertEqual(customer.phone_number, '555-1234')
|
||||||
self.assertEqual(customer.email_address, 'fred@mailinator.com')
|
self.assertEqual(customer.email_address, 'fred@mailinator.com')
|
||||||
self.assertEqual(batch.customer_name, "Fred Flintstone")
|
self.assertEqual(batch.customer_name, "Fred Flintstone")
|
||||||
self.assertEqual(batch.phone_number, '555-1234')
|
self.assertEqual(batch.phone_number, '555-1234')
|
||||||
self.assertEqual(batch.email_address, 'fred@mailinator.com')
|
self.assertEqual(batch.email_address, 'fred@mailinator.com')
|
||||||
|
|
||||||
# explicit full_name
|
# pending, minimal
|
||||||
batch = handler.make_batch(self.session, created_by=user, customer_id=42)
|
last_customer = customer # save ref to prev record
|
||||||
handler.set_pending_customer(batch, {
|
handler.set_customer(batch, {'full_name': "Wilma Flintstone"})
|
||||||
'full_name': "Freddy Flintstone",
|
|
||||||
'first_name': "Fred",
|
|
||||||
'last_name': "Flintstone",
|
|
||||||
'phone_number': '555-1234',
|
|
||||||
'email_address': 'fred@mailinator.com',
|
|
||||||
})
|
|
||||||
self.assertIsNone(batch.customer_id)
|
self.assertIsNone(batch.customer_id)
|
||||||
|
self.assertIsNone(batch.local_customer)
|
||||||
self.assertIsInstance(batch.pending_customer, model.PendingCustomer)
|
self.assertIsInstance(batch.pending_customer, model.PendingCustomer)
|
||||||
customer = batch.pending_customer
|
customer = batch.pending_customer
|
||||||
self.assertEqual(customer.full_name, "Freddy Flintstone")
|
self.assertIs(customer, last_customer)
|
||||||
self.assertEqual(customer.first_name, "Fred")
|
self.assertEqual(customer.full_name, "Wilma Flintstone")
|
||||||
self.assertEqual(customer.last_name, "Flintstone")
|
self.assertIsNone(customer.first_name)
|
||||||
self.assertEqual(customer.phone_number, '555-1234')
|
self.assertIsNone(customer.last_name)
|
||||||
self.assertEqual(customer.email_address, 'fred@mailinator.com')
|
self.assertIsNone(customer.phone_number)
|
||||||
self.assertEqual(batch.customer_name, "Freddy Flintstone")
|
self.assertIsNone(customer.email_address)
|
||||||
self.assertEqual(batch.phone_number, '555-1234')
|
self.assertEqual(batch.customer_name, "Wilma Flintstone")
|
||||||
self.assertEqual(batch.email_address, 'fred@mailinator.com')
|
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
|
model = self.app.model
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
|
@ -95,144 +158,290 @@ class TestNewOrderBatchHandler(DataTestCase):
|
||||||
self.session.add(batch)
|
self.session.add(batch)
|
||||||
self.assertEqual(len(batch.rows), 0)
|
self.assertEqual(len(batch.rows), 0)
|
||||||
|
|
||||||
|
# pending, typical
|
||||||
kw = dict(
|
kw = dict(
|
||||||
scancode='07430500132',
|
scancode='07430500001',
|
||||||
brand_name='Bragg',
|
brand_name='Bragg',
|
||||||
description='Vinegar',
|
description='Vinegar',
|
||||||
size='32oz',
|
size='1oz',
|
||||||
case_size=12,
|
case_size=12,
|
||||||
unit_cost=decimal.Decimal('3.99'),
|
unit_cost=decimal.Decimal('1.99'),
|
||||||
unit_price_reg=decimal.Decimal('5.99'),
|
unit_price_reg=decimal.Decimal('2.99'),
|
||||||
created_by=user,
|
|
||||||
)
|
)
|
||||||
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.assertEqual(len(batch.rows), 1)
|
||||||
self.assertIs(batch.rows[0], row)
|
self.assertIs(batch.rows[0], row)
|
||||||
|
self.assertIsNone(row.product_id)
|
||||||
self.assertEqual(row.product_scancode, '07430500132')
|
self.assertIsNone(row.local_product)
|
||||||
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
|
product = row.pending_product
|
||||||
self.assertIsInstance(product, model.PendingProduct)
|
self.assertIsInstance(product, model.PendingProduct)
|
||||||
self.assertEqual(product.scancode, '07430500132')
|
self.assertEqual(product.scancode, '07430500001')
|
||||||
self.assertEqual(product.brand_name, 'Bragg')
|
self.assertEqual(product.brand_name, 'Bragg')
|
||||||
self.assertEqual(product.description, 'Vinegar')
|
self.assertEqual(product.description, 'Vinegar')
|
||||||
self.assertEqual(product.size, '32oz')
|
self.assertEqual(product.size, '1oz')
|
||||||
self.assertEqual(product.case_size, 12)
|
self.assertEqual(product.case_size, 12)
|
||||||
self.assertEqual(product.unit_cost, decimal.Decimal('3.99'))
|
self.assertEqual(product.unit_cost, decimal.Decimal('1.99'))
|
||||||
self.assertEqual(product.unit_price_reg, decimal.Decimal('5.99'))
|
self.assertEqual(product.unit_price_reg, decimal.Decimal('2.99'))
|
||||||
self.assertIs(product.created_by, user)
|
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
|
# pending, minimal
|
||||||
self.config.setdefault('sideshow.orders.allow_unknown_product', 'false')
|
row = handler.add_item(batch, {'description': "Tangerines"}, 1, enum.ORDER_UOM_UNIT)
|
||||||
self.assertRaises(TypeError, handler.add_pending_product, batch, kw, 1, enum.ORDER_UOM_UNIT)
|
self.assertIsNone(row.product_id)
|
||||||
|
self.assertIsNone(row.local_product)
|
||||||
def test_set_pending_product(self):
|
product = row.pending_product
|
||||||
model = self.app.model
|
self.assertIsInstance(product, model.PendingProduct)
|
||||||
enum = self.app.enum
|
self.assertIsNone(product.scancode)
|
||||||
handler = self.make_handler()
|
self.assertIsNone(product.brand_name)
|
||||||
|
self.assertEqual(product.description, 'Tangerines')
|
||||||
user = model.User(username='barney')
|
self.assertIsNone(product.size)
|
||||||
self.session.add(user)
|
self.assertIsNone(product.case_size)
|
||||||
|
self.assertIsNone(product.unit_cost)
|
||||||
batch = handler.make_batch(self.session, created_by=user)
|
self.assertIsNone(product.unit_price_reg)
|
||||||
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)
|
|
||||||
self.assertIsNone(row.product_scancode)
|
self.assertIsNone(row.product_scancode)
|
||||||
self.assertIsNone(row.product_brand)
|
self.assertIsNone(row.product_brand)
|
||||||
self.assertIsNone(row.product_description)
|
self.assertEqual(row.product_description, 'Tangerines')
|
||||||
self.assertIsNone(row.product_size)
|
self.assertIsNone(row.product_size)
|
||||||
self.assertIsNone(row.case_size)
|
self.assertIsNone(row.case_size)
|
||||||
self.assertIsNone(row.unit_cost)
|
self.assertIsNone(row.unit_cost)
|
||||||
self.assertIsNone(row.unit_price_reg)
|
self.assertIsNone(row.unit_price_reg)
|
||||||
self.assertIsNone(row.unit_price_quoted)
|
self.assertIsNone(row.unit_price_quoted)
|
||||||
|
self.assertIsNone(row.case_price_quoted)
|
||||||
# set pending, which clears product_id
|
self.assertIsNone(row.total_price)
|
||||||
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)
|
|
||||||
|
|
||||||
# error if unknown products not allowed
|
# error if unknown products not allowed
|
||||||
self.config.setdefault('sideshow.orders.allow_unknown_product', 'false')
|
self.config.setdefault('sideshow.orders.allow_unknown_products', 'false')
|
||||||
self.assertRaises(TypeError, handler.set_pending_product, row, dict(
|
self.assertRaises(TypeError, handler.add_item, batch, kw, 1, enum.ORDER_UOM_UNIT)
|
||||||
scancode='07430500116',
|
|
||||||
size='16oz',
|
# local product
|
||||||
unit_cost=decimal.Decimal('2.19'),
|
local = model.LocalProduct(scancode='07430500002',
|
||||||
unit_price_reg=decimal.Decimal('3.59'),
|
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):
|
def test_refresh_row(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
@ -387,7 +596,7 @@ class TestNewOrderBatchHandler(DataTestCase):
|
||||||
unit_price_reg=decimal.Decimal('5.99'),
|
unit_price_reg=decimal.Decimal('5.99'),
|
||||||
created_by=user,
|
created_by=user,
|
||||||
)
|
)
|
||||||
row = handler.add_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.add(row)
|
||||||
self.session.flush()
|
self.session.flush()
|
||||||
self.assertEqual(batch.row_count, 1)
|
self.assertEqual(batch.row_count, 1)
|
||||||
|
@ -423,25 +632,70 @@ class TestNewOrderBatchHandler(DataTestCase):
|
||||||
self.assertNotIn(batch, self.session)
|
self.assertNotIn(batch, self.session)
|
||||||
self.assertEqual(self.session.query(model.PendingCustomer).count(), 0)
|
self.assertEqual(self.session.query(model.PendingCustomer).count(), 0)
|
||||||
|
|
||||||
# make new pending customer
|
# make new pending customer, assigned to batch + order
|
||||||
customer = model.PendingCustomer(full_name="Fred Flintstone",
|
customer = model.PendingCustomer(full_name="Wilma Flintstone",
|
||||||
status=enum.PendingCustomerStatus.PENDING,
|
status=enum.PendingCustomerStatus.PENDING,
|
||||||
created_by=user)
|
created_by=user)
|
||||||
self.session.add(customer)
|
self.session.add(customer)
|
||||||
|
batch = handler.make_batch(self.session, created_by=user, pending_customer=customer)
|
||||||
|
self.session.add(batch)
|
||||||
|
order = model.Order(order_id=77, created_by=user, pending_customer=customer)
|
||||||
|
self.session.add(order)
|
||||||
|
self.session.flush()
|
||||||
|
|
||||||
# make 2 batches with same pending customer
|
# deleting batch will *not* delete pending customer
|
||||||
batch1 = handler.make_batch(self.session, created_by=user, pending_customer=customer)
|
self.assertIn(batch, self.session)
|
||||||
batch2 = handler.make_batch(self.session, created_by=user, pending_customer=customer)
|
self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
|
||||||
self.session.add(batch1)
|
handler.do_delete(batch, user)
|
||||||
self.session.add(batch2)
|
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
self.assertNotIn(batch, self.session)
|
||||||
|
self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
|
||||||
|
|
||||||
# deleting 1 will not delete pending customer
|
# make new pending product, associate w/ batch + order
|
||||||
self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
|
batch = handler.make_batch(self.session, created_by=user)
|
||||||
handler.do_delete(batch1, user)
|
self.session.add(batch)
|
||||||
|
handler.set_customer(batch, {'full_name': "Jack Black"})
|
||||||
|
row = handler.add_item(batch, dict(
|
||||||
|
scancode='07430500132',
|
||||||
|
brand_name='Bragg',
|
||||||
|
description='Vinegar',
|
||||||
|
size='32oz',
|
||||||
|
unit_price_reg=5.99,
|
||||||
|
), 1, enum.ORDER_UOM_UNIT)
|
||||||
|
product = row.pending_product
|
||||||
|
order = model.Order(order_id=33, created_by=user)
|
||||||
|
item = model.OrderItem(pending_product=product, order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||||
|
status_code=enum.ORDER_ITEM_STATUS_INITIATED)
|
||||||
|
order.items.append(item)
|
||||||
|
self.session.add(order)
|
||||||
|
self.session.flush()
|
||||||
|
|
||||||
|
# deleting batch will *not* delete pending product
|
||||||
|
self.assertIn(batch, self.session)
|
||||||
|
self.assertEqual(self.session.query(model.PendingProduct).count(), 1)
|
||||||
|
handler.do_delete(batch, user)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
self.assertNotIn(batch, self.session)
|
||||||
self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
|
self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
|
||||||
self.assertIs(batch2.pending_customer, customer)
|
|
||||||
|
# make another batch w/ same pending product
|
||||||
|
batch = handler.make_batch(self.session, created_by=user)
|
||||||
|
self.session.add(batch)
|
||||||
|
row = handler.make_row(pending_product=product,
|
||||||
|
order_qty=1, order_uom=enum.ORDER_UOM_UNIT)
|
||||||
|
handler.add_row(batch, row)
|
||||||
|
|
||||||
|
# also delete the associated order
|
||||||
|
self.session.delete(order)
|
||||||
|
self.session.flush()
|
||||||
|
|
||||||
|
# deleting this batch *will* delete pending product
|
||||||
|
self.assertIn(batch, self.session)
|
||||||
|
self.assertEqual(self.session.query(model.PendingProduct).count(), 1)
|
||||||
|
handler.do_delete(batch, user)
|
||||||
|
self.session.commit()
|
||||||
|
self.assertNotIn(batch, self.session)
|
||||||
|
self.assertEqual(self.session.query(model.PendingProduct).count(), 0)
|
||||||
|
|
||||||
def test_get_effective_rows(self):
|
def test_get_effective_rows(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
@ -492,6 +746,12 @@ class TestNewOrderBatchHandler(DataTestCase):
|
||||||
self.assertEqual(reason, "Must assign the customer")
|
self.assertEqual(reason, "Must assign the customer")
|
||||||
|
|
||||||
batch.customer_id = 42
|
batch.customer_id = 42
|
||||||
|
batch.customer_name = "Fred Flintstone"
|
||||||
|
|
||||||
|
reason = handler.why_not_execute(batch)
|
||||||
|
self.assertEqual(reason, "Customer phone number is required")
|
||||||
|
|
||||||
|
batch.phone_number = '555-1234'
|
||||||
|
|
||||||
reason = handler.why_not_execute(batch)
|
reason = handler.why_not_execute(batch)
|
||||||
self.assertEqual(reason, "Must add at least one valid item")
|
self.assertEqual(reason, "Must add at least one valid item")
|
||||||
|
@ -506,13 +766,206 @@ class TestNewOrderBatchHandler(DataTestCase):
|
||||||
unit_price_reg=decimal.Decimal('5.99'),
|
unit_price_reg=decimal.Decimal('5.99'),
|
||||||
created_by=user,
|
created_by=user,
|
||||||
)
|
)
|
||||||
row = handler.add_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.add(row)
|
||||||
self.session.flush()
|
self.session.flush()
|
||||||
|
|
||||||
reason = handler.why_not_execute(batch)
|
reason = handler.why_not_execute(batch)
|
||||||
self.assertIsNone(reason)
|
self.assertIsNone(reason)
|
||||||
|
|
||||||
|
def test_make_local_customer(self):
|
||||||
|
model = self.app.model
|
||||||
|
enum = self.app.enum
|
||||||
|
handler = self.make_handler()
|
||||||
|
|
||||||
|
user = model.User(username='barney')
|
||||||
|
self.session.add(user)
|
||||||
|
|
||||||
|
# make a typical batch
|
||||||
|
batch = handler.make_batch(self.session, created_by=user)
|
||||||
|
self.session.add(batch)
|
||||||
|
handler.set_customer(batch, {'first_name': "John", 'last_name': "Doe",
|
||||||
|
'phone_number': '555-1234'})
|
||||||
|
row = handler.add_item(batch, dict(
|
||||||
|
scancode='07430500132',
|
||||||
|
brand_name='Bragg',
|
||||||
|
description='Vinegar',
|
||||||
|
size='32oz',
|
||||||
|
unit_price_reg=5.99,
|
||||||
|
), 1, enum.ORDER_UOM_UNIT)
|
||||||
|
self.session.flush()
|
||||||
|
|
||||||
|
# making local customer removes pending customer
|
||||||
|
self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
|
||||||
|
self.assertEqual(self.session.query(model.LocalCustomer).count(), 0)
|
||||||
|
self.assertIsNotNone(batch.pending_customer)
|
||||||
|
self.assertIsNone(batch.local_customer)
|
||||||
|
handler.make_local_customer(batch)
|
||||||
|
self.assertEqual(self.session.query(model.PendingCustomer).count(), 0)
|
||||||
|
self.assertEqual(self.session.query(model.LocalCustomer).count(), 1)
|
||||||
|
self.assertIsNone(batch.pending_customer)
|
||||||
|
local = batch.local_customer
|
||||||
|
self.assertIsNotNone(local)
|
||||||
|
self.assertEqual(local.first_name, "John")
|
||||||
|
self.assertEqual(local.last_name, "Doe")
|
||||||
|
self.assertEqual(local.full_name, "John Doe")
|
||||||
|
self.assertEqual(local.phone_number, '555-1234')
|
||||||
|
|
||||||
|
# trying again does nothing
|
||||||
|
handler.make_local_customer(batch)
|
||||||
|
self.assertEqual(self.session.query(model.PendingCustomer).count(), 0)
|
||||||
|
self.assertEqual(self.session.query(model.LocalCustomer).count(), 1)
|
||||||
|
self.assertIsNone(batch.pending_customer)
|
||||||
|
local = batch.local_customer
|
||||||
|
self.assertIsNotNone(local)
|
||||||
|
self.assertEqual(local.first_name, "John")
|
||||||
|
self.assertEqual(local.last_name, "Doe")
|
||||||
|
self.assertEqual(local.full_name, "John Doe")
|
||||||
|
self.assertEqual(local.phone_number, '555-1234')
|
||||||
|
|
||||||
|
# make another typical batch
|
||||||
|
batch = handler.make_batch(self.session, created_by=user)
|
||||||
|
self.session.add(batch)
|
||||||
|
handler.set_customer(batch, {'first_name': "Chuck", 'last_name': "Norris",
|
||||||
|
'phone_number': '555-1234'})
|
||||||
|
row = handler.add_item(batch, dict(
|
||||||
|
scancode='07430500132',
|
||||||
|
brand_name='Bragg',
|
||||||
|
description='Vinegar',
|
||||||
|
size='32oz',
|
||||||
|
unit_price_reg=5.99,
|
||||||
|
), 1, enum.ORDER_UOM_UNIT)
|
||||||
|
self.session.flush()
|
||||||
|
|
||||||
|
# should do nothing if local customers disabled
|
||||||
|
with patch.object(handler, 'use_local_customers', return_value=False):
|
||||||
|
self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
|
||||||
|
self.assertEqual(self.session.query(model.LocalCustomer).count(), 1)
|
||||||
|
self.assertIsNotNone(batch.pending_customer)
|
||||||
|
self.assertIsNone(batch.local_customer)
|
||||||
|
handler.make_local_customer(batch)
|
||||||
|
self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
|
||||||
|
self.assertEqual(self.session.query(model.LocalCustomer).count(), 1)
|
||||||
|
self.assertIsNotNone(batch.pending_customer)
|
||||||
|
self.assertIsNone(batch.local_customer)
|
||||||
|
|
||||||
|
# but things happen by default, since local customers enabled
|
||||||
|
self.assertEqual(self.session.query(model.PendingCustomer).count(), 1)
|
||||||
|
self.assertEqual(self.session.query(model.LocalCustomer).count(), 1)
|
||||||
|
self.assertIsNotNone(batch.pending_customer)
|
||||||
|
self.assertIsNone(batch.local_customer)
|
||||||
|
handler.make_local_customer(batch)
|
||||||
|
self.assertEqual(self.session.query(model.PendingCustomer).count(), 0)
|
||||||
|
self.assertEqual(self.session.query(model.LocalCustomer).count(), 2)
|
||||||
|
self.assertIsNone(batch.pending_customer)
|
||||||
|
local = batch.local_customer
|
||||||
|
self.assertIsNotNone(local)
|
||||||
|
self.assertEqual(local.first_name, "Chuck")
|
||||||
|
self.assertEqual(local.last_name, "Norris")
|
||||||
|
self.assertEqual(local.full_name, "Chuck Norris")
|
||||||
|
self.assertEqual(local.phone_number, '555-1234')
|
||||||
|
|
||||||
|
def test_make_local_products(self):
|
||||||
|
model = self.app.model
|
||||||
|
enum = self.app.enum
|
||||||
|
handler = self.make_handler()
|
||||||
|
|
||||||
|
user = model.User(username='barney')
|
||||||
|
self.session.add(user)
|
||||||
|
|
||||||
|
# make a batch w/ one each local + pending products
|
||||||
|
batch = handler.make_batch(self.session, created_by=user)
|
||||||
|
self.session.add(batch)
|
||||||
|
handler.set_customer(batch, {'full_name': "John Doe"})
|
||||||
|
local = model.LocalProduct(scancode='07430500116',
|
||||||
|
brand_name='Bragg',
|
||||||
|
description='Vinegar',
|
||||||
|
size='16oz',
|
||||||
|
unit_price_reg=3.59)
|
||||||
|
self.session.add(local)
|
||||||
|
self.session.flush()
|
||||||
|
row1 = handler.add_item(batch, local.uuid.hex, 1, enum.ORDER_UOM_UNIT)
|
||||||
|
row2 = handler.add_item(batch, dict(
|
||||||
|
scancode='07430500132',
|
||||||
|
brand_name='Bragg',
|
||||||
|
description='Vinegar',
|
||||||
|
size='32oz',
|
||||||
|
unit_price_reg=5.99,
|
||||||
|
), 1, enum.ORDER_UOM_UNIT)
|
||||||
|
self.session.flush()
|
||||||
|
|
||||||
|
# making local product removes pending product
|
||||||
|
self.assertEqual(self.session.query(model.PendingProduct).count(), 1)
|
||||||
|
self.assertEqual(self.session.query(model.LocalProduct).count(), 1)
|
||||||
|
self.assertIsNotNone(row2.pending_product)
|
||||||
|
self.assertIsNone(row2.local_product)
|
||||||
|
handler.make_local_products(batch, batch.rows)
|
||||||
|
self.assertEqual(self.session.query(model.PendingProduct).count(), 0)
|
||||||
|
self.assertEqual(self.session.query(model.LocalProduct).count(), 2)
|
||||||
|
self.assertIsNone(row2.pending_product)
|
||||||
|
local = row2.local_product
|
||||||
|
self.assertIsNotNone(local)
|
||||||
|
self.assertEqual(local.scancode, '07430500132')
|
||||||
|
self.assertEqual(local.brand_name, 'Bragg')
|
||||||
|
self.assertEqual(local.description, 'Vinegar')
|
||||||
|
self.assertEqual(local.size, '32oz')
|
||||||
|
self.assertEqual(local.unit_price_reg, decimal.Decimal('5.99'))
|
||||||
|
|
||||||
|
# trying again does nothing
|
||||||
|
handler.make_local_products(batch, batch.rows)
|
||||||
|
self.assertEqual(self.session.query(model.PendingProduct).count(), 0)
|
||||||
|
self.assertEqual(self.session.query(model.LocalProduct).count(), 2)
|
||||||
|
self.assertIsNone(row2.pending_product)
|
||||||
|
local = row2.local_product
|
||||||
|
self.assertIsNotNone(local)
|
||||||
|
self.assertEqual(local.scancode, '07430500132')
|
||||||
|
self.assertEqual(local.brand_name, 'Bragg')
|
||||||
|
self.assertEqual(local.description, 'Vinegar')
|
||||||
|
self.assertEqual(local.size, '32oz')
|
||||||
|
self.assertEqual(local.unit_price_reg, decimal.Decimal('5.99'))
|
||||||
|
|
||||||
|
# make another typical batch
|
||||||
|
batch = handler.make_batch(self.session, created_by=user)
|
||||||
|
self.session.add(batch)
|
||||||
|
handler.set_customer(batch, {'full_name': "Chuck Norris"})
|
||||||
|
row = handler.add_item(batch, dict(
|
||||||
|
scancode='07430500164',
|
||||||
|
brand_name='Bragg',
|
||||||
|
description='Vinegar',
|
||||||
|
size='64oz',
|
||||||
|
unit_price_reg=9.99,
|
||||||
|
), 1, enum.ORDER_UOM_UNIT)
|
||||||
|
self.session.flush()
|
||||||
|
|
||||||
|
# should do nothing if local products disabled
|
||||||
|
with patch.object(handler, 'use_local_products', return_value=False):
|
||||||
|
self.assertEqual(self.session.query(model.PendingProduct).count(), 1)
|
||||||
|
self.assertEqual(self.session.query(model.LocalProduct).count(), 2)
|
||||||
|
self.assertIsNotNone(row.pending_product)
|
||||||
|
self.assertIsNone(row.local_product)
|
||||||
|
handler.make_local_products(batch, batch.rows)
|
||||||
|
self.assertEqual(self.session.query(model.PendingProduct).count(), 1)
|
||||||
|
self.assertEqual(self.session.query(model.LocalProduct).count(), 2)
|
||||||
|
self.assertIsNotNone(row.pending_product)
|
||||||
|
self.assertIsNone(row.local_product)
|
||||||
|
|
||||||
|
# but things happen by default, since local products enabled
|
||||||
|
self.assertEqual(self.session.query(model.PendingProduct).count(), 1)
|
||||||
|
self.assertEqual(self.session.query(model.LocalProduct).count(), 2)
|
||||||
|
self.assertIsNotNone(row.pending_product)
|
||||||
|
self.assertIsNone(row.local_product)
|
||||||
|
handler.make_local_products(batch, batch.rows)
|
||||||
|
self.assertEqual(self.session.query(model.PendingProduct).count(), 0)
|
||||||
|
self.assertEqual(self.session.query(model.LocalProduct).count(), 3)
|
||||||
|
self.assertIsNone(row.pending_product)
|
||||||
|
local = row.local_product
|
||||||
|
self.assertIsNotNone(local)
|
||||||
|
self.assertEqual(local.scancode, '07430500164')
|
||||||
|
self.assertEqual(local.brand_name, 'Bragg')
|
||||||
|
self.assertEqual(local.description, 'Vinegar')
|
||||||
|
self.assertEqual(local.size, '64oz')
|
||||||
|
self.assertEqual(local.unit_price_reg, decimal.Decimal('9.99'))
|
||||||
|
|
||||||
def test_make_new_order(self):
|
def test_make_new_order(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
|
@ -534,7 +987,7 @@ class TestNewOrderBatchHandler(DataTestCase):
|
||||||
unit_price_reg=decimal.Decimal('5.99'),
|
unit_price_reg=decimal.Decimal('5.99'),
|
||||||
created_by=user,
|
created_by=user,
|
||||||
)
|
)
|
||||||
row = handler.add_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.add(row)
|
||||||
self.session.flush()
|
self.session.flush()
|
||||||
|
|
||||||
|
@ -574,7 +1027,7 @@ class TestNewOrderBatchHandler(DataTestCase):
|
||||||
unit_price_reg=decimal.Decimal('5.99'),
|
unit_price_reg=decimal.Decimal('5.99'),
|
||||||
created_by=user,
|
created_by=user,
|
||||||
)
|
)
|
||||||
row = handler.add_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.add(row)
|
||||||
self.session.flush()
|
self.session.flush()
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,19 @@ class TestOrder(DataTestCase):
|
||||||
|
|
||||||
class TestOrderItem(DataTestCase):
|
class TestOrderItem(DataTestCase):
|
||||||
|
|
||||||
|
def test_full_description(self):
|
||||||
|
|
||||||
|
item = mod.OrderItem()
|
||||||
|
self.assertEqual(item.full_description, "")
|
||||||
|
|
||||||
|
item = mod.OrderItem(product_description="Vinegar")
|
||||||
|
self.assertEqual(item.full_description, "Vinegar")
|
||||||
|
|
||||||
|
item = mod.OrderItem(product_brand='Bragg',
|
||||||
|
product_description='Vinegar',
|
||||||
|
product_size='32oz')
|
||||||
|
self.assertEqual(item.full_description, "Bragg Vinegar 32oz")
|
||||||
|
|
||||||
def test_str(self):
|
def test_str(self):
|
||||||
|
|
||||||
item = mod.OrderItem()
|
item = mod.OrderItem()
|
||||||
|
@ -27,8 +40,7 @@ class TestOrderItem(DataTestCase):
|
||||||
item = mod.OrderItem(product_description="Vinegar")
|
item = mod.OrderItem(product_description="Vinegar")
|
||||||
self.assertEqual(str(item), "Vinegar")
|
self.assertEqual(str(item), "Vinegar")
|
||||||
|
|
||||||
product = PendingProduct(brand_name="Bragg",
|
item = mod.OrderItem(product_brand='Bragg',
|
||||||
description="Vinegar",
|
product_description='Vinegar',
|
||||||
size="32oz")
|
product_size='32oz')
|
||||||
item = mod.OrderItem(pending_product=product)
|
|
||||||
self.assertEqual(str(item), "Bragg Vinegar 32oz")
|
self.assertEqual(str(item), "Bragg Vinegar 32oz")
|
||||||
|
|
|
@ -32,6 +32,31 @@ class TestOrderRef(WebTestCase):
|
||||||
self.assertIn(f'/orders/{order.uuid}', url)
|
self.assertIn(f'/orders/{order.uuid}', url)
|
||||||
|
|
||||||
|
|
||||||
|
class TestLocalCustomerRef(WebTestCase):
|
||||||
|
|
||||||
|
def test_sort_query(self):
|
||||||
|
typ = mod.LocalCustomerRef(self.request, session=self.session)
|
||||||
|
query = typ.get_query()
|
||||||
|
self.assertIsInstance(query, orm.Query)
|
||||||
|
sorted_query = typ.sort_query(query)
|
||||||
|
self.assertIsInstance(sorted_query, orm.Query)
|
||||||
|
self.assertIsNot(sorted_query, query)
|
||||||
|
|
||||||
|
def test_get_object_url(self):
|
||||||
|
self.pyramid_config.add_route('local_customers.view', '/local/customers/{uuid}')
|
||||||
|
model = self.app.model
|
||||||
|
enum = self.app.enum
|
||||||
|
|
||||||
|
customer = model.LocalCustomer()
|
||||||
|
self.session.add(customer)
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
typ = mod.LocalCustomerRef(self.request, session=self.session)
|
||||||
|
url = typ.get_object_url(customer)
|
||||||
|
self.assertIsNotNone(url)
|
||||||
|
self.assertIn(f'/local/customers/{customer.uuid}', url)
|
||||||
|
|
||||||
|
|
||||||
class TestPendingCustomerRef(WebTestCase):
|
class TestPendingCustomerRef(WebTestCase):
|
||||||
|
|
||||||
def test_sort_query(self):
|
def test_sort_query(self):
|
||||||
|
@ -60,6 +85,31 @@ class TestPendingCustomerRef(WebTestCase):
|
||||||
self.assertIn(f'/pending/customers/{customer.uuid}', url)
|
self.assertIn(f'/pending/customers/{customer.uuid}', url)
|
||||||
|
|
||||||
|
|
||||||
|
class TestLocalProductRef(WebTestCase):
|
||||||
|
|
||||||
|
def test_sort_query(self):
|
||||||
|
typ = mod.LocalProductRef(self.request, session=self.session)
|
||||||
|
query = typ.get_query()
|
||||||
|
self.assertIsInstance(query, orm.Query)
|
||||||
|
sorted_query = typ.sort_query(query)
|
||||||
|
self.assertIsInstance(sorted_query, orm.Query)
|
||||||
|
self.assertIsNot(sorted_query, query)
|
||||||
|
|
||||||
|
def test_get_object_url(self):
|
||||||
|
self.pyramid_config.add_route('local_products.view', '/local/products/{uuid}')
|
||||||
|
model = self.app.model
|
||||||
|
enum = self.app.enum
|
||||||
|
|
||||||
|
product = model.LocalProduct()
|
||||||
|
self.session.add(product)
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
typ = mod.LocalProductRef(self.request, session=self.session)
|
||||||
|
url = typ.get_object_url(product)
|
||||||
|
self.assertIsNotNone(url)
|
||||||
|
self.assertIn(f'/local/products/{product.uuid}', url)
|
||||||
|
|
||||||
|
|
||||||
class TestPendingProductRef(WebTestCase):
|
class TestPendingProductRef(WebTestCase):
|
||||||
|
|
||||||
def test_sort_query(self):
|
def test_sort_query(self):
|
||||||
|
|
|
@ -9,4 +9,4 @@ class TestSideshowMenuHandler(WebTestCase):
|
||||||
def test_make_menus(self):
|
def test_make_menus(self):
|
||||||
handler = mod.SideshowMenuHandler(self.config)
|
handler = mod.SideshowMenuHandler(self.config)
|
||||||
menus = handler.make_menus(self.request)
|
menus = handler.make_menus(self.request)
|
||||||
self.assertEqual(len(menus), 4)
|
self.assertEqual(len(menus), 5)
|
||||||
|
|
|
@ -16,6 +16,114 @@ class TestIncludeme(WebTestCase):
|
||||||
mod.includeme(self.pyramid_config)
|
mod.includeme(self.pyramid_config)
|
||||||
|
|
||||||
|
|
||||||
|
class TestLocalCustomerView(WebTestCase):
|
||||||
|
|
||||||
|
def make_view(self):
|
||||||
|
return mod.LocalCustomerView(self.request)
|
||||||
|
|
||||||
|
def test_configure_grid(self):
|
||||||
|
model = self.app.model
|
||||||
|
view = self.make_view()
|
||||||
|
grid = view.make_grid(model_class=model.LocalCustomer)
|
||||||
|
self.assertNotIn('full_name', grid.linked_columns)
|
||||||
|
view.configure_grid(grid)
|
||||||
|
self.assertIn('full_name', grid.linked_columns)
|
||||||
|
|
||||||
|
def test_configure_form(self):
|
||||||
|
model = self.app.model
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
# creating
|
||||||
|
with patch.object(view, 'creating', new=True):
|
||||||
|
form = view.make_form(model_class=model.LocalCustomer)
|
||||||
|
view.configure_form(form)
|
||||||
|
self.assertNotIn('external_id', form)
|
||||||
|
self.assertNotIn('full_name', form)
|
||||||
|
self.assertNotIn('orders', form)
|
||||||
|
self.assertNotIn('new_order_batches', form)
|
||||||
|
|
||||||
|
user = model.User(username='barney')
|
||||||
|
self.session.add(user)
|
||||||
|
customer = model.LocalCustomer()
|
||||||
|
self.session.add(customer)
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
# viewing
|
||||||
|
with patch.object(view, 'viewing', new=True):
|
||||||
|
form = view.make_form(model_instance=customer)
|
||||||
|
view.configure_form(form)
|
||||||
|
self.assertIn('external_id', form)
|
||||||
|
self.assertIn('full_name', form)
|
||||||
|
self.assertIn('orders', form)
|
||||||
|
self.assertIn('new_order_batches', form)
|
||||||
|
|
||||||
|
def test_make_orders_grid(self):
|
||||||
|
model = self.app.model
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
user = model.User(username='barney')
|
||||||
|
self.session.add(user)
|
||||||
|
customer = model.LocalCustomer()
|
||||||
|
self.session.add(customer)
|
||||||
|
order = model.Order(order_id=42, local_customer=customer, created_by=user)
|
||||||
|
self.session.add(order)
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
# no view perm
|
||||||
|
grid = view.make_orders_grid(customer)
|
||||||
|
self.assertEqual(len(grid.actions), 0)
|
||||||
|
|
||||||
|
# with view perm
|
||||||
|
with patch.object(self.request, 'is_root', new=True):
|
||||||
|
grid = view.make_orders_grid(customer)
|
||||||
|
self.assertEqual(len(grid.actions), 1)
|
||||||
|
self.assertEqual(grid.actions[0].key, 'view')
|
||||||
|
|
||||||
|
def test_make_new_order_batches_grid(self):
|
||||||
|
model = self.app.model
|
||||||
|
handler = NewOrderBatchHandler(self.config)
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
user = model.User(username='barney')
|
||||||
|
self.session.add(user)
|
||||||
|
customer = model.LocalCustomer()
|
||||||
|
self.session.add(customer)
|
||||||
|
batch = handler.make_batch(self.session, local_customer=customer, created_by=user)
|
||||||
|
self.session.add(batch)
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
# no view perm
|
||||||
|
grid = view.make_new_order_batches_grid(customer)
|
||||||
|
self.assertEqual(len(grid.actions), 0)
|
||||||
|
|
||||||
|
# with view perm
|
||||||
|
with patch.object(self.request, 'is_root', new=True):
|
||||||
|
grid = view.make_new_order_batches_grid(customer)
|
||||||
|
self.assertEqual(len(grid.actions), 1)
|
||||||
|
self.assertEqual(grid.actions[0].key, 'view')
|
||||||
|
|
||||||
|
def test_objectify(self):
|
||||||
|
model = self.app.model
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
user = model.User(username='barney')
|
||||||
|
self.session.add(user)
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
with patch.object(view, 'creating', new=True):
|
||||||
|
with patch.object(self.request, 'user', new=user):
|
||||||
|
form = view.make_model_form()
|
||||||
|
with patch.object(form, 'validated', create=True, new={
|
||||||
|
'first_name': 'Chuck',
|
||||||
|
'last_name': 'Norris',
|
||||||
|
}):
|
||||||
|
customer = view.objectify(form)
|
||||||
|
self.assertIsInstance(customer, model.LocalCustomer)
|
||||||
|
self.assertEqual(customer.first_name, 'Chuck')
|
||||||
|
self.assertEqual(customer.last_name, 'Norris')
|
||||||
|
self.assertEqual(customer.full_name, 'Chuck Norris')
|
||||||
|
|
||||||
|
|
||||||
class TestPendingCustomerView(WebTestCase):
|
class TestPendingCustomerView(WebTestCase):
|
||||||
|
|
||||||
def make_view(self):
|
def make_view(self):
|
||||||
|
|
|
@ -13,7 +13,7 @@ from wuttaweb.forms.schema import WuttaMoney
|
||||||
from sideshow.batch.neworder import NewOrderBatchHandler
|
from sideshow.batch.neworder import NewOrderBatchHandler
|
||||||
from sideshow.testing import WebTestCase
|
from sideshow.testing import WebTestCase
|
||||||
from sideshow.web.views import orders as mod
|
from sideshow.web.views import orders as mod
|
||||||
from sideshow.web.forms.schema import OrderRef
|
from sideshow.web.forms.schema import OrderRef, PendingProductRef
|
||||||
|
|
||||||
|
|
||||||
class TestIncludeme(WebTestCase):
|
class TestIncludeme(WebTestCase):
|
||||||
|
@ -27,6 +27,9 @@ class TestOrderView(WebTestCase):
|
||||||
def make_view(self):
|
def make_view(self):
|
||||||
return mod.OrderView(self.request)
|
return mod.OrderView(self.request)
|
||||||
|
|
||||||
|
def make_handler(self):
|
||||||
|
return NewOrderBatchHandler(self.config)
|
||||||
|
|
||||||
def test_configure_grid(self):
|
def test_configure_grid(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
@ -40,6 +43,7 @@ class TestOrderView(WebTestCase):
|
||||||
def test_create(self):
|
def test_create(self):
|
||||||
self.pyramid_config.include('sideshow.web.views')
|
self.pyramid_config.include('sideshow.web.views')
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
enum = self.app.enum
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
|
||||||
user = model.User(username='barney')
|
user = model.User(username='barney')
|
||||||
|
@ -91,7 +95,7 @@ class TestOrderView(WebTestCase):
|
||||||
'customer_name': 'Fred Flintstone',
|
'customer_name': 'Fred Flintstone',
|
||||||
'phone_number': '555-1234',
|
'phone_number': '555-1234',
|
||||||
'email_address': 'fred@mailinator.com',
|
'email_address': 'fred@mailinator.com',
|
||||||
'new_customer_name': 'Fred Flintstone',
|
'new_customer_full_name': 'Fred Flintstone',
|
||||||
'new_customer_first_name': 'Fred',
|
'new_customer_first_name': 'Fred',
|
||||||
'new_customer_last_name': 'Flintstone',
|
'new_customer_last_name': 'Flintstone',
|
||||||
'new_customer_phone': '555-1234',
|
'new_customer_phone': '555-1234',
|
||||||
|
@ -108,6 +112,40 @@ class TestOrderView(WebTestCase):
|
||||||
self.assertEqual(response.content_type, 'application/json')
|
self.assertEqual(response.content_type, 'application/json')
|
||||||
self.assertEqual(response.json_body, {'error': 'unknown form action'})
|
self.assertEqual(response.json_body, {'error': 'unknown form action'})
|
||||||
|
|
||||||
|
# add item
|
||||||
|
with patch.multiple(self.request, create=True,
|
||||||
|
method='POST',
|
||||||
|
json_body={'action': 'add_item',
|
||||||
|
'product_info': {
|
||||||
|
'scancode': '07430500132',
|
||||||
|
'description': 'Vinegar',
|
||||||
|
'unit_price_reg': 5.99,
|
||||||
|
},
|
||||||
|
'order_qty': 1,
|
||||||
|
'order_uom': enum.ORDER_UOM_UNIT}):
|
||||||
|
response = view.create()
|
||||||
|
self.assertIsInstance(response, Response)
|
||||||
|
self.assertEqual(response.content_type, 'application/json')
|
||||||
|
data = response.json_body
|
||||||
|
self.assertEqual(sorted(data), ['batch', 'row'])
|
||||||
|
|
||||||
|
# add item, w/ error
|
||||||
|
with patch.object(NewOrderBatchHandler, 'add_item', side_effect=RuntimeError):
|
||||||
|
with patch.multiple(self.request, create=True,
|
||||||
|
method='POST',
|
||||||
|
json_body={'action': 'add_item',
|
||||||
|
'product_info': {
|
||||||
|
'scancode': '07430500116',
|
||||||
|
'description': 'Vinegar',
|
||||||
|
'unit_price_reg': 3.59,
|
||||||
|
},
|
||||||
|
'order_qty': 1,
|
||||||
|
'order_uom': enum.ORDER_UOM_UNIT}):
|
||||||
|
response = view.create()
|
||||||
|
self.assertIsInstance(response, Response)
|
||||||
|
self.assertEqual(response.content_type, 'application/json')
|
||||||
|
self.assertEqual(response.json_body, {'error': 'RuntimeError'})
|
||||||
|
|
||||||
def test_get_current_batch(self):
|
def test_get_current_batch(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
handler = NewOrderBatchHandler(self.config)
|
handler = NewOrderBatchHandler(self.config)
|
||||||
|
@ -137,6 +175,75 @@ class TestOrderView(WebTestCase):
|
||||||
self.assertEqual(self.session.query(model.NewOrderBatch).count(), 1)
|
self.assertEqual(self.session.query(model.NewOrderBatch).count(), 1)
|
||||||
self.assertIs(batch2, batch)
|
self.assertIs(batch2, batch)
|
||||||
|
|
||||||
|
def test_customer_autocomplete(self):
|
||||||
|
model = self.app.model
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
with patch.object(view, 'Session', return_value=self.session):
|
||||||
|
|
||||||
|
# empty results by default
|
||||||
|
self.assertEqual(view.customer_autocomplete(), [])
|
||||||
|
with patch.object(self.request, 'GET', new={'term': 'foo'}, create=True):
|
||||||
|
self.assertEqual(view.customer_autocomplete(), [])
|
||||||
|
|
||||||
|
# add a customer
|
||||||
|
customer = model.LocalCustomer(full_name="Chuck Norris")
|
||||||
|
self.session.add(customer)
|
||||||
|
self.session.flush()
|
||||||
|
|
||||||
|
# search for chuck finds chuck
|
||||||
|
with patch.object(self.request, 'GET', new={'term': 'chuck'}, create=True):
|
||||||
|
result = view.customer_autocomplete()
|
||||||
|
self.assertEqual(len(result), 1)
|
||||||
|
self.assertEqual(result[0], {
|
||||||
|
'value': customer.uuid.hex,
|
||||||
|
'label': "Chuck Norris",
|
||||||
|
})
|
||||||
|
|
||||||
|
# search for sally finds nothing
|
||||||
|
with patch.object(self.request, 'GET', new={'term': 'sally'}, create=True):
|
||||||
|
result = view.customer_autocomplete()
|
||||||
|
self.assertEqual(result, [])
|
||||||
|
|
||||||
|
def test_product_autocomplete(self):
|
||||||
|
model = self.app.model
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
with patch.object(view, 'Session', return_value=self.session):
|
||||||
|
|
||||||
|
# empty results by default
|
||||||
|
self.assertEqual(view.product_autocomplete(), [])
|
||||||
|
with patch.object(self.request, 'GET', new={'term': 'foo'}, create=True):
|
||||||
|
self.assertEqual(view.product_autocomplete(), [])
|
||||||
|
|
||||||
|
# add a product
|
||||||
|
product = model.LocalProduct(brand_name="Bragg's", description="Vinegar")
|
||||||
|
self.session.add(product)
|
||||||
|
self.session.flush()
|
||||||
|
|
||||||
|
# search for vinegar finds product
|
||||||
|
with patch.object(self.request, 'GET', new={'term': 'vinegar'}, create=True):
|
||||||
|
result = view.product_autocomplete()
|
||||||
|
self.assertEqual(len(result), 1)
|
||||||
|
self.assertEqual(result[0], {
|
||||||
|
'value': product.uuid.hex,
|
||||||
|
'label': "Bragg's Vinegar",
|
||||||
|
})
|
||||||
|
|
||||||
|
# search for brag finds product
|
||||||
|
with patch.object(self.request, 'GET', new={'term': 'brag'}, create=True):
|
||||||
|
result = view.product_autocomplete()
|
||||||
|
self.assertEqual(len(result), 1)
|
||||||
|
self.assertEqual(result[0], {
|
||||||
|
'value': product.uuid.hex,
|
||||||
|
'label': "Bragg's Vinegar",
|
||||||
|
})
|
||||||
|
|
||||||
|
# search for juice finds nothing
|
||||||
|
with patch.object(self.request, 'GET', new={'term': 'juice'}, create=True):
|
||||||
|
result = view.product_autocomplete()
|
||||||
|
self.assertEqual(result, [])
|
||||||
|
|
||||||
def test_get_pending_product_required_fields(self):
|
def test_get_pending_product_required_fields(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
@ -158,11 +265,13 @@ class TestOrderView(WebTestCase):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
handler = NewOrderBatchHandler(self.config)
|
handler = NewOrderBatchHandler(self.config)
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
view.batch_handler = handler
|
||||||
|
|
||||||
user = model.User(username='barney')
|
user = model.User(username='barney')
|
||||||
self.session.add(user)
|
self.session.add(user)
|
||||||
|
|
||||||
# with true customer
|
# with external customer
|
||||||
|
with patch.object(handler, 'use_local_customers', return_value=False):
|
||||||
batch = handler.make_batch(self.session, created_by=user,
|
batch = handler.make_batch(self.session, created_by=user,
|
||||||
customer_id=42, customer_name='Fred Flintstone',
|
customer_id=42, customer_name='Fred Flintstone',
|
||||||
phone_number='555-1234', email_address='fred@mailinator.com')
|
phone_number='555-1234', email_address='fred@mailinator.com')
|
||||||
|
@ -175,21 +284,32 @@ class TestOrderView(WebTestCase):
|
||||||
'customer_name': 'Fred Flintstone',
|
'customer_name': 'Fred Flintstone',
|
||||||
'phone_number': '555-1234',
|
'phone_number': '555-1234',
|
||||||
'email_address': 'fred@mailinator.com',
|
'email_address': 'fred@mailinator.com',
|
||||||
'new_customer_name': None,
|
})
|
||||||
'new_customer_first_name': None,
|
|
||||||
'new_customer_last_name': None,
|
# with local customer
|
||||||
'new_customer_phone': None,
|
local = model.LocalCustomer(full_name="Betty Boop")
|
||||||
'new_customer_email': None,
|
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
|
# with pending customer
|
||||||
batch = handler.make_batch(self.session, created_by=user)
|
batch = handler.make_batch(self.session, created_by=user)
|
||||||
self.session.add(batch)
|
self.session.add(batch)
|
||||||
handler.set_pending_customer(batch, dict(
|
handler.set_customer(batch, dict(
|
||||||
full_name="Fred Flintstone",
|
full_name="Fred Flintstone",
|
||||||
first_name="Fred", last_name="Flintstone",
|
first_name="Fred", last_name="Flintstone",
|
||||||
phone_number='555-1234', email_address='fred@mailinator.com',
|
phone_number='555-1234', email_address='fred@mailinator.com',
|
||||||
created_by=user,
|
|
||||||
))
|
))
|
||||||
self.session.flush()
|
self.session.flush()
|
||||||
context = view.get_context_customer(batch)
|
context = view.get_context_customer(batch)
|
||||||
|
@ -199,7 +319,7 @@ class TestOrderView(WebTestCase):
|
||||||
'customer_name': 'Fred Flintstone',
|
'customer_name': 'Fred Flintstone',
|
||||||
'phone_number': '555-1234',
|
'phone_number': '555-1234',
|
||||||
'email_address': 'fred@mailinator.com',
|
'email_address': 'fred@mailinator.com',
|
||||||
'new_customer_name': 'Fred Flintstone',
|
'new_customer_full_name': 'Fred Flintstone',
|
||||||
'new_customer_first_name': 'Fred',
|
'new_customer_first_name': 'Fred',
|
||||||
'new_customer_last_name': 'Flintstone',
|
'new_customer_last_name': 'Flintstone',
|
||||||
'new_customer_phone': '555-1234',
|
'new_customer_phone': '555-1234',
|
||||||
|
@ -217,11 +337,6 @@ class TestOrderView(WebTestCase):
|
||||||
'customer_name': None,
|
'customer_name': None,
|
||||||
'phone_number': None,
|
'phone_number': None,
|
||||||
'email_address': None,
|
'email_address': None,
|
||||||
'new_customer_name': None,
|
|
||||||
'new_customer_first_name': None,
|
|
||||||
'new_customer_last_name': None,
|
|
||||||
'new_customer_phone': None,
|
|
||||||
'new_customer_email': None,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
def test_start_over(self):
|
def test_start_over(self):
|
||||||
|
@ -268,6 +383,80 @@ class TestOrderView(WebTestCase):
|
||||||
self.session.flush()
|
self.session.flush()
|
||||||
self.assertEqual(self.session.query(model.NewOrderBatch).count(), 0)
|
self.assertEqual(self.session.query(model.NewOrderBatch).count(), 0)
|
||||||
|
|
||||||
|
def test_assign_customer(self):
|
||||||
|
self.pyramid_config.add_route('orders.create', '/orders/new')
|
||||||
|
model = self.app.model
|
||||||
|
handler = NewOrderBatchHandler(self.config)
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
user = model.User(username='barney')
|
||||||
|
self.session.add(user)
|
||||||
|
weirdal = model.LocalCustomer(full_name="Weird Al")
|
||||||
|
self.session.add(weirdal)
|
||||||
|
self.session.flush()
|
||||||
|
|
||||||
|
with patch.object(view, 'batch_handler', create=True, new=handler):
|
||||||
|
with patch.object(view, 'Session', return_value=self.session):
|
||||||
|
with patch.object(self.request, 'user', new=user):
|
||||||
|
batch = view.get_current_batch()
|
||||||
|
|
||||||
|
# normal
|
||||||
|
self.assertIsNone(batch.local_customer)
|
||||||
|
self.assertIsNone(batch.pending_customer)
|
||||||
|
context = view.assign_customer(batch, {'customer_id': weirdal.uuid.hex})
|
||||||
|
self.assertIsNone(batch.pending_customer)
|
||||||
|
self.assertIs(batch.local_customer, weirdal)
|
||||||
|
self.assertEqual(context, {
|
||||||
|
'customer_is_known': True,
|
||||||
|
'customer_id': weirdal.uuid.hex,
|
||||||
|
'customer_name': 'Weird Al',
|
||||||
|
'phone_number': None,
|
||||||
|
'email_address': None,
|
||||||
|
})
|
||||||
|
|
||||||
|
# missing customer_id
|
||||||
|
context = view.assign_customer(batch, {})
|
||||||
|
self.assertEqual(context, {'error': "Must provide customer_id"})
|
||||||
|
|
||||||
|
def test_unassign_customer(self):
|
||||||
|
self.pyramid_config.add_route('orders.create', '/orders/new')
|
||||||
|
model = self.app.model
|
||||||
|
handler = NewOrderBatchHandler(self.config)
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
user = model.User(username='barney')
|
||||||
|
self.session.add(user)
|
||||||
|
self.session.flush()
|
||||||
|
|
||||||
|
with patch.object(view, 'batch_handler', create=True, new=handler):
|
||||||
|
with patch.object(view, 'Session', return_value=self.session):
|
||||||
|
with patch.object(self.request, 'user', new=user):
|
||||||
|
batch = view.get_current_batch()
|
||||||
|
view.set_pending_customer(batch, {'first_name': 'Jack',
|
||||||
|
'last_name': 'Black'})
|
||||||
|
|
||||||
|
# normal
|
||||||
|
self.assertIsNone(batch.local_customer)
|
||||||
|
self.assertIsNotNone(batch.pending_customer)
|
||||||
|
self.assertEqual(batch.customer_name, 'Jack Black')
|
||||||
|
context = view.unassign_customer(batch, {})
|
||||||
|
# nb. pending record remains, but not used
|
||||||
|
self.assertIsNotNone(batch.pending_customer)
|
||||||
|
self.assertIsNone(batch.customer_name)
|
||||||
|
self.assertIsNone(batch.local_customer)
|
||||||
|
self.assertEqual(context, {
|
||||||
|
'customer_is_known': True,
|
||||||
|
'customer_id': None,
|
||||||
|
'customer_name': None,
|
||||||
|
'phone_number': None,
|
||||||
|
'email_address': None,
|
||||||
|
'new_customer_full_name': 'Jack Black',
|
||||||
|
'new_customer_first_name': 'Jack',
|
||||||
|
'new_customer_last_name': 'Black',
|
||||||
|
'new_customer_phone': None,
|
||||||
|
'new_customer_email': None,
|
||||||
|
})
|
||||||
|
|
||||||
def test_set_pending_customer(self):
|
def test_set_pending_customer(self):
|
||||||
self.pyramid_config.add_route('orders.create', '/orders/new')
|
self.pyramid_config.add_route('orders.create', '/orders/new')
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
@ -301,19 +490,58 @@ class TestOrderView(WebTestCase):
|
||||||
'customer_name': 'Fred Flintstone',
|
'customer_name': 'Fred Flintstone',
|
||||||
'phone_number': '555-1234',
|
'phone_number': '555-1234',
|
||||||
'email_address': 'fred@mailinator.com',
|
'email_address': 'fred@mailinator.com',
|
||||||
'new_customer_name': 'Fred Flintstone',
|
'new_customer_full_name': 'Fred Flintstone',
|
||||||
'new_customer_first_name': 'Fred',
|
'new_customer_first_name': 'Fred',
|
||||||
'new_customer_last_name': 'Flintstone',
|
'new_customer_last_name': 'Flintstone',
|
||||||
'new_customer_phone': '555-1234',
|
'new_customer_phone': '555-1234',
|
||||||
'new_customer_email': 'fred@mailinator.com',
|
'new_customer_email': 'fred@mailinator.com',
|
||||||
})
|
})
|
||||||
|
|
||||||
# error
|
def test_get_product_info(self):
|
||||||
with patch.object(handler, 'set_pending_customer', side_effect=RuntimeError):
|
model = self.app.model
|
||||||
context = view.set_pending_customer(batch, data)
|
handler = self.make_handler()
|
||||||
self.assertEqual(context, {
|
view = self.make_view()
|
||||||
'error': 'RuntimeError',
|
|
||||||
})
|
user = model.User(username='barney')
|
||||||
|
self.session.add(user)
|
||||||
|
local = model.LocalProduct(scancode='07430500132',
|
||||||
|
brand_name='Bragg',
|
||||||
|
description='Vinegar',
|
||||||
|
size='32oz',
|
||||||
|
case_size=12,
|
||||||
|
unit_price_reg=decimal.Decimal('5.99'))
|
||||||
|
self.session.add(local)
|
||||||
|
self.session.flush()
|
||||||
|
|
||||||
|
with patch.object(view, 'Session', return_value=self.session):
|
||||||
|
with patch.object(view, 'batch_handler', create=True, new=handler):
|
||||||
|
with patch.object(self.request, 'user', new=user):
|
||||||
|
batch = view.get_current_batch()
|
||||||
|
|
||||||
|
# typical, for local product
|
||||||
|
context = view.get_product_info(batch, {'product_id': local.uuid.hex})
|
||||||
|
self.assertEqual(context['product_id'], local.uuid.hex)
|
||||||
|
self.assertEqual(context['scancode'], '07430500132')
|
||||||
|
self.assertEqual(context['brand_name'], 'Bragg')
|
||||||
|
self.assertEqual(context['description'], 'Vinegar')
|
||||||
|
self.assertEqual(context['size'], '32oz')
|
||||||
|
self.assertEqual(context['full_description'], 'Bragg Vinegar 32oz')
|
||||||
|
self.assertEqual(context['case_size'], 12)
|
||||||
|
self.assertEqual(context['unit_price_reg'], 5.99)
|
||||||
|
|
||||||
|
# error if local product missing
|
||||||
|
mock_uuid = self.app.make_true_uuid()
|
||||||
|
context = view.get_product_info(batch, {'product_id': mock_uuid.hex})
|
||||||
|
self.assertEqual(context, {'error': "Product not found"})
|
||||||
|
|
||||||
|
# error if no product_id
|
||||||
|
context = view.get_product_info(batch, {})
|
||||||
|
self.assertEqual(context, {'error': "Must specify a product ID"})
|
||||||
|
|
||||||
|
# external lookup not implemented (yet)
|
||||||
|
with patch.object(handler, 'use_local_products', return_value=False):
|
||||||
|
self.assertRaises(NotImplementedError, view.get_product_info,
|
||||||
|
batch, {'product_id': '42'})
|
||||||
|
|
||||||
def test_add_item(self):
|
def test_add_item(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
@ -326,7 +554,7 @@ class TestOrderView(WebTestCase):
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'pending_product': {
|
'product_info': {
|
||||||
'scancode': '07430500132',
|
'scancode': '07430500132',
|
||||||
'brand_name': 'Bragg',
|
'brand_name': 'Bragg',
|
||||||
'description': 'Vinegar',
|
'description': 'Vinegar',
|
||||||
|
@ -353,15 +581,9 @@ class TestOrderView(WebTestCase):
|
||||||
row = batch.rows[0]
|
row = batch.rows[0]
|
||||||
self.assertIsInstance(row.pending_product, model.PendingProduct)
|
self.assertIsInstance(row.pending_product, model.PendingProduct)
|
||||||
|
|
||||||
# pending w/ invalid price
|
# external product not yet supported
|
||||||
with patch.dict(data['pending_product'], unit_price_reg='invalid'):
|
with patch.object(handler, 'use_local_products', return_value=False):
|
||||||
result = view.add_item(batch, data)
|
with patch.dict(data, product_info='42'):
|
||||||
self.assertEqual(result, {'error': "Invalid entry for field: unit_price_reg"})
|
|
||||||
self.session.flush()
|
|
||||||
self.assertEqual(len(batch.rows), 1) # still just the 1st row
|
|
||||||
|
|
||||||
# true product not yet supported
|
|
||||||
with patch.dict(data, product_is_known=True):
|
|
||||||
self.assertRaises(NotImplementedError, view.add_item, batch, data)
|
self.assertRaises(NotImplementedError, view.add_item, batch, data)
|
||||||
|
|
||||||
def test_update_item(self):
|
def test_update_item(self):
|
||||||
|
@ -375,7 +597,7 @@ class TestOrderView(WebTestCase):
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'pending_product': {
|
'product_info': {
|
||||||
'scancode': '07430500132',
|
'scancode': '07430500132',
|
||||||
'brand_name': 'Bragg',
|
'brand_name': 'Bragg',
|
||||||
'description': 'Vinegar',
|
'description': 'Vinegar',
|
||||||
|
@ -403,7 +625,7 @@ class TestOrderView(WebTestCase):
|
||||||
|
|
||||||
# missing row uuid
|
# missing row uuid
|
||||||
result = view.update_item(batch, data)
|
result = view.update_item(batch, data)
|
||||||
self.assertEqual(result, {'error': "Must specify a row UUID"})
|
self.assertEqual(result, {'error': "Must specify row UUID"})
|
||||||
|
|
||||||
# row not found
|
# row not found
|
||||||
with patch.dict(data, uuid=self.app.make_true_uuid()):
|
with patch.dict(data, uuid=self.app.make_true_uuid()):
|
||||||
|
@ -420,16 +642,18 @@ class TestOrderView(WebTestCase):
|
||||||
result = view.update_item(batch, data)
|
result = view.update_item(batch, data)
|
||||||
self.assertEqual(result, {'error': "Row is for wrong batch"})
|
self.assertEqual(result, {'error': "Row is for wrong batch"})
|
||||||
|
|
||||||
# set row for remaining tests
|
|
||||||
data['uuid'] = row.uuid
|
|
||||||
|
|
||||||
# true product not yet supported
|
# true product not yet supported
|
||||||
with patch.dict(data, product_is_known=True):
|
with patch.object(handler, 'use_local_products', return_value=False):
|
||||||
self.assertRaises(NotImplementedError, view.update_item, batch, data)
|
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
|
# update row, pending product
|
||||||
with patch.dict(data, order_qty=2):
|
with patch.dict(data, uuid=row.uuid, order_qty=2):
|
||||||
with patch.dict(data['pending_product'], scancode='07430500116'):
|
with patch.dict(data['product_info'], scancode='07430500116'):
|
||||||
self.assertEqual(row.product_scancode, '07430500132')
|
self.assertEqual(row.product_scancode, '07430500132')
|
||||||
self.assertEqual(row.order_qty, 1)
|
self.assertEqual(row.order_qty, 1)
|
||||||
result = view.update_item(batch, data)
|
result = view.update_item(batch, data)
|
||||||
|
@ -438,7 +662,7 @@ class TestOrderView(WebTestCase):
|
||||||
self.assertEqual(row.order_qty, 2)
|
self.assertEqual(row.order_qty, 2)
|
||||||
self.assertEqual(row.pending_product.scancode, '07430500116')
|
self.assertEqual(row.pending_product.scancode, '07430500116')
|
||||||
self.assertEqual(result['row']['product_scancode'], '07430500116')
|
self.assertEqual(result['row']['product_scancode'], '07430500116')
|
||||||
self.assertEqual(result['row']['order_qty'], '2')
|
self.assertEqual(result['row']['order_qty'], 2)
|
||||||
|
|
||||||
def test_delete_item(self):
|
def test_delete_item(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
@ -451,7 +675,7 @@ class TestOrderView(WebTestCase):
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'pending_product': {
|
'product_info': {
|
||||||
'scancode': '07430500132',
|
'scancode': '07430500132',
|
||||||
'brand_name': 'Bragg',
|
'brand_name': 'Bragg',
|
||||||
'description': 'Vinegar',
|
'description': 'Vinegar',
|
||||||
|
@ -506,7 +730,7 @@ class TestOrderView(WebTestCase):
|
||||||
self.assertEqual(len(batch.rows), 0)
|
self.assertEqual(len(batch.rows), 0)
|
||||||
self.assertEqual(batch.row_count, 0)
|
self.assertEqual(batch.row_count, 0)
|
||||||
|
|
||||||
def test_submit_new_order(self):
|
def test_submit_order(self):
|
||||||
self.pyramid_config.add_route('orders.view', '/orders/{uuid}')
|
self.pyramid_config.add_route('orders.view', '/orders/{uuid}')
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
|
@ -518,7 +742,7 @@ class TestOrderView(WebTestCase):
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'pending_product': {
|
'product_info': {
|
||||||
'scancode': '07430500132',
|
'scancode': '07430500132',
|
||||||
'brand_name': 'Bragg',
|
'brand_name': 'Bragg',
|
||||||
'description': 'Vinegar',
|
'description': 'Vinegar',
|
||||||
|
@ -534,28 +758,33 @@ class TestOrderView(WebTestCase):
|
||||||
with patch.object(view, 'Session', return_value=self.session):
|
with patch.object(view, 'Session', return_value=self.session):
|
||||||
with patch.object(self.request, 'user', new=user):
|
with patch.object(self.request, 'user', new=user):
|
||||||
batch = view.get_current_batch()
|
batch = view.get_current_batch()
|
||||||
self.session.flush()
|
|
||||||
self.assertEqual(len(batch.rows), 0)
|
self.assertEqual(len(batch.rows), 0)
|
||||||
|
|
||||||
# add row w/ pending product
|
# add row w/ pending product
|
||||||
view.add_item(batch, data)
|
view.add_item(batch, data)
|
||||||
self.session.flush()
|
self.assertEqual(len(batch.rows), 1)
|
||||||
row = batch.rows[0]
|
row = batch.rows[0]
|
||||||
self.assertIsInstance(row.pending_product, model.PendingProduct)
|
self.assertIsInstance(row.pending_product, model.PendingProduct)
|
||||||
self.assertEqual(row.unit_price_quoted, decimal.Decimal('5.99'))
|
self.assertEqual(row.unit_price_quoted, decimal.Decimal('5.99'))
|
||||||
|
|
||||||
# execute not allowed yet (no customer)
|
# execute not allowed yet (no customer)
|
||||||
result = view.submit_new_order(batch, {})
|
result = view.submit_order(batch, {})
|
||||||
self.assertEqual(result, {'error': "Must assign the customer"})
|
self.assertEqual(result, {'error': "Must assign the customer"})
|
||||||
|
|
||||||
|
# execute not allowed yet (no phone number)
|
||||||
|
view.set_pending_customer(batch, {'full_name': 'John Doe'})
|
||||||
|
result = view.submit_order(batch, {})
|
||||||
|
self.assertEqual(result, {'error': "Customer phone number is required"})
|
||||||
|
|
||||||
# submit/execute ok
|
# submit/execute ok
|
||||||
batch.customer_id = 42
|
view.set_pending_customer(batch, {'full_name': 'John Doe',
|
||||||
result = view.submit_new_order(batch, {})
|
'phone_number': '555-1234'})
|
||||||
|
result = view.submit_order(batch, {})
|
||||||
self.assertEqual(sorted(result), ['next_url'])
|
self.assertEqual(sorted(result), ['next_url'])
|
||||||
self.assertIn('/orders/', result['next_url'])
|
self.assertIn('/orders/', result['next_url'])
|
||||||
|
|
||||||
# error (already executed)
|
# error (already executed)
|
||||||
result = view.submit_new_order(batch, {})
|
result = view.submit_order(batch, {})
|
||||||
self.assertEqual(result, {
|
self.assertEqual(result, {
|
||||||
'error': f"ValueError: batch has already been executed: {batch}",
|
'error': f"ValueError: batch has already been executed: {batch}",
|
||||||
})
|
})
|
||||||
|
@ -585,9 +814,8 @@ class TestOrderView(WebTestCase):
|
||||||
'size': '32oz',
|
'size': '32oz',
|
||||||
'unit_price_reg': 5.99,
|
'unit_price_reg': 5.99,
|
||||||
'case_size': 12,
|
'case_size': 12,
|
||||||
'created_by': user,
|
|
||||||
}
|
}
|
||||||
row = handler.add_pending_product(batch, pending, 1, enum.ORDER_UOM_CASE)
|
row = handler.add_item(batch, pending, 1, enum.ORDER_UOM_CASE)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
data = view.normalize_batch(batch)
|
data = view.normalize_batch(batch)
|
||||||
|
@ -604,11 +832,15 @@ class TestOrderView(WebTestCase):
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
handler = NewOrderBatchHandler(self.config)
|
handler = NewOrderBatchHandler(self.config)
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
view.batch_handler = handler
|
||||||
|
|
||||||
user = model.User(username='barney')
|
user = model.User(username='barney')
|
||||||
self.session.add(user)
|
self.session.add(user)
|
||||||
batch = handler.make_batch(self.session, created_by=user)
|
batch = handler.make_batch(self.session, created_by=user)
|
||||||
self.session.add(batch)
|
self.session.add(batch)
|
||||||
|
self.session.flush()
|
||||||
|
|
||||||
|
# add 1st row w/ pending product
|
||||||
pending = {
|
pending = {
|
||||||
'scancode': '07430500132',
|
'scancode': '07430500132',
|
||||||
'brand_name': 'Bragg',
|
'brand_name': 'Bragg',
|
||||||
|
@ -616,19 +848,22 @@ class TestOrderView(WebTestCase):
|
||||||
'size': '32oz',
|
'size': '32oz',
|
||||||
'unit_price_reg': 5.99,
|
'unit_price_reg': 5.99,
|
||||||
'case_size': 12,
|
'case_size': 12,
|
||||||
'created_by': user,
|
'vendor_name': 'Acme Warehouse',
|
||||||
|
'vendor_item_code': '1234',
|
||||||
}
|
}
|
||||||
row = handler.add_pending_product(batch, pending, 2, enum.ORDER_UOM_CASE)
|
row1 = handler.add_item(batch, pending, 2, enum.ORDER_UOM_CASE)
|
||||||
self.session.commit()
|
|
||||||
|
|
||||||
# normal
|
# typical, pending product
|
||||||
data = view.normalize_row(row)
|
data = view.normalize_row(row1)
|
||||||
self.assertIsInstance(data, dict)
|
self.assertIsInstance(data, dict)
|
||||||
self.assertEqual(data['uuid'], row.uuid.hex)
|
self.assertEqual(data['uuid'], row1.uuid.hex)
|
||||||
self.assertEqual(data['sequence'], 1)
|
self.assertEqual(data['sequence'], 1)
|
||||||
|
self.assertIsNone(data['product_id'])
|
||||||
self.assertEqual(data['product_scancode'], '07430500132')
|
self.assertEqual(data['product_scancode'], '07430500132')
|
||||||
self.assertEqual(data['case_size'], '12')
|
self.assertEqual(data['product_full_description'], 'Bragg Vinegar 32oz')
|
||||||
self.assertEqual(data['order_qty'], '2')
|
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_uom'], 'CS')
|
||||||
self.assertEqual(data['order_qty_display'], '2 Cases (× 12 = 24 Units)')
|
self.assertEqual(data['order_qty_display'], '2 Cases (× 12 = 24 Units)')
|
||||||
self.assertEqual(data['unit_price_reg'], 5.99)
|
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'], 143.76)
|
||||||
self.assertEqual(data['total_price_display'], '$143.76')
|
self.assertEqual(data['total_price_display'], '$143.76')
|
||||||
self.assertIsNone(data['special_order'])
|
self.assertIsNone(data['special_order'])
|
||||||
self.assertEqual(data['status_code'], row.STATUS_OK)
|
self.assertEqual(data['status_code'], row1.STATUS_OK)
|
||||||
self.assertEqual(data['pending_product'], {
|
self.assertEqual(data['pending_product'], {
|
||||||
'uuid': row.pending_product_uuid.hex,
|
'uuid': row1.pending_product_uuid.hex,
|
||||||
'scancode': '07430500132',
|
'scancode': '07430500132',
|
||||||
'brand_name': 'Bragg',
|
'brand_name': 'Bragg',
|
||||||
'description': 'Vinegar',
|
'description': 'Vinegar',
|
||||||
|
@ -654,44 +889,117 @@ class TestOrderView(WebTestCase):
|
||||||
'department_id': None,
|
'department_id': None,
|
||||||
'department_name': None,
|
'department_name': None,
|
||||||
'unit_price_reg': 5.99,
|
'unit_price_reg': 5.99,
|
||||||
'vendor_name': None,
|
'vendor_name': 'Acme Warehouse',
|
||||||
'vendor_item_code': None,
|
'vendor_item_code': '1234',
|
||||||
'unit_cost': None,
|
'unit_cost': None,
|
||||||
'case_size': 12.0,
|
'case_size': 12.0,
|
||||||
'notes': None,
|
'notes': None,
|
||||||
'special_order': None,
|
'special_order': None,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# the next few tests will morph 1st row..
|
||||||
|
|
||||||
# unknown case size
|
# unknown case size
|
||||||
row.pending_product.case_size = None
|
row1.pending_product.case_size = None
|
||||||
handler.refresh_row(row)
|
handler.refresh_row(row1)
|
||||||
self.session.flush()
|
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)')
|
self.assertEqual(data['order_qty_display'], '2 Cases (× ?? = ?? Units)')
|
||||||
|
|
||||||
# order by unit
|
# order by unit
|
||||||
row.order_uom = enum.ORDER_UOM_UNIT
|
row1.order_uom = enum.ORDER_UOM_UNIT
|
||||||
handler.refresh_row(row)
|
handler.refresh_row(row1)
|
||||||
self.session.flush()
|
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')
|
self.assertEqual(data['order_qty_display'], '2 Units')
|
||||||
|
|
||||||
# item on sale
|
# item on sale
|
||||||
row.pending_product.case_size = 12
|
row1.pending_product.case_size = 12
|
||||||
row.unit_price_sale = decimal.Decimal('5.19')
|
row1.unit_price_sale = decimal.Decimal('5.19')
|
||||||
row.sale_ends = datetime.datetime(2025, 1, 5, 20, 32)
|
row1.sale_ends = datetime.datetime(2099, 1, 5, 20, 32)
|
||||||
handler.refresh_row(row, now=datetime.datetime(2025, 1, 5, 19))
|
handler.refresh_row(row1)
|
||||||
self.session.flush()
|
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'], 5.19)
|
||||||
self.assertEqual(data['unit_price_sale_display'], '$5.19')
|
self.assertEqual(data['unit_price_sale_display'], '$5.19')
|
||||||
self.assertEqual(data['sale_ends'], '2025-01-05 20:32:00')
|
self.assertEqual(data['sale_ends'], '2099-01-05 20:32:00')
|
||||||
self.assertEqual(data['sale_ends_display'], '2025-01-05')
|
self.assertEqual(data['sale_ends_display'], '2099-01-05')
|
||||||
self.assertEqual(data['unit_price_quoted'], 5.19)
|
self.assertEqual(data['unit_price_quoted'], 5.19)
|
||||||
self.assertEqual(data['unit_price_quoted_display'], '$5.19')
|
self.assertEqual(data['unit_price_quoted_display'], '$5.19')
|
||||||
self.assertEqual(data['case_price_quoted'], 62.28)
|
self.assertEqual(data['case_price_quoted'], 62.28)
|
||||||
self.assertEqual(data['case_price_quoted_display'], '$62.28')
|
self.assertEqual(data['case_price_quoted_display'], '$62.28')
|
||||||
|
|
||||||
|
# add 2nd row w/ local product
|
||||||
|
local = model.LocalProduct(brand_name="Lay's",
|
||||||
|
description="Potato Chips",
|
||||||
|
vendor_name='Acme Distribution',
|
||||||
|
unit_price_reg=3.29)
|
||||||
|
self.session.add(local)
|
||||||
|
self.session.flush()
|
||||||
|
row2 = handler.add_item(batch, local.uuid.hex, 1, enum.ORDER_UOM_UNIT)
|
||||||
|
|
||||||
|
# typical, local product
|
||||||
|
data = view.normalize_row(row2)
|
||||||
|
self.assertEqual(data['uuid'], row2.uuid.hex)
|
||||||
|
self.assertEqual(data['sequence'], 2)
|
||||||
|
self.assertEqual(data['product_id'], local.uuid.hex)
|
||||||
|
self.assertIsNone(data['product_scancode'])
|
||||||
|
self.assertEqual(data['product_full_description'], "Lay's Potato Chips")
|
||||||
|
self.assertIsNone(data['case_size'])
|
||||||
|
self.assertEqual(data['vendor_name'], 'Acme Distribution')
|
||||||
|
self.assertEqual(data['order_qty'], 1)
|
||||||
|
self.assertEqual(data['order_uom'], 'EA')
|
||||||
|
self.assertEqual(data['order_qty_display'], '1 Units')
|
||||||
|
self.assertEqual(data['unit_price_reg'], 3.29)
|
||||||
|
self.assertEqual(data['unit_price_reg_display'], '$3.29')
|
||||||
|
self.assertNotIn('unit_price_sale', data)
|
||||||
|
self.assertNotIn('unit_price_sale_display', data)
|
||||||
|
self.assertNotIn('sale_ends', data)
|
||||||
|
self.assertNotIn('sale_ends_display', data)
|
||||||
|
self.assertEqual(data['unit_price_quoted'], 3.29)
|
||||||
|
self.assertEqual(data['unit_price_quoted_display'], '$3.29')
|
||||||
|
self.assertIsNone(data['case_price_quoted'])
|
||||||
|
self.assertEqual(data['case_price_quoted_display'], '')
|
||||||
|
self.assertEqual(data['total_price'], 3.29)
|
||||||
|
self.assertEqual(data['total_price_display'], '$3.29')
|
||||||
|
self.assertIsNone(data['special_order'])
|
||||||
|
self.assertEqual(data['status_code'], row2.STATUS_OK)
|
||||||
|
self.assertNotIn('pending_product', data)
|
||||||
|
|
||||||
|
# the next few tests will morph 2nd row..
|
||||||
|
|
||||||
|
# typical, external product
|
||||||
|
row2.product_id = '42'
|
||||||
|
with patch.object(handler, 'use_local_products', return_value=False):
|
||||||
|
data = view.normalize_row(row2)
|
||||||
|
self.assertEqual(data['uuid'], row2.uuid.hex)
|
||||||
|
self.assertEqual(data['sequence'], 2)
|
||||||
|
self.assertEqual(data['product_id'], '42')
|
||||||
|
self.assertIsNone(data['product_scancode'])
|
||||||
|
self.assertNotIn('product_full_description', data) # TODO
|
||||||
|
self.assertIsNone(data['case_size'])
|
||||||
|
self.assertNotIn('vendor_name', data) # TODO
|
||||||
|
self.assertEqual(data['order_qty'], 1)
|
||||||
|
self.assertEqual(data['order_uom'], 'EA')
|
||||||
|
self.assertEqual(data['order_qty_display'], '1 Units')
|
||||||
|
self.assertEqual(data['unit_price_reg'], 3.29)
|
||||||
|
self.assertEqual(data['unit_price_reg_display'], '$3.29')
|
||||||
|
self.assertNotIn('unit_price_sale', data)
|
||||||
|
self.assertNotIn('unit_price_sale_display', data)
|
||||||
|
self.assertNotIn('sale_ends', data)
|
||||||
|
self.assertNotIn('sale_ends_display', data)
|
||||||
|
self.assertEqual(data['unit_price_quoted'], 3.29)
|
||||||
|
self.assertEqual(data['unit_price_quoted_display'], '$3.29')
|
||||||
|
self.assertIsNone(data['case_price_quoted'])
|
||||||
|
self.assertEqual(data['case_price_quoted_display'], '')
|
||||||
|
self.assertEqual(data['total_price'], 3.29)
|
||||||
|
self.assertEqual(data['total_price_display'], '$3.29')
|
||||||
|
self.assertIsNone(data['special_order'])
|
||||||
|
self.assertEqual(data['status_code'], row2.STATUS_OK)
|
||||||
|
self.assertNotIn('pending_product', data)
|
||||||
|
|
||||||
def test_get_instance_title(self):
|
def test_get_instance_title(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
@ -715,13 +1023,31 @@ class TestOrderView(WebTestCase):
|
||||||
self.session.add(order)
|
self.session.add(order)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
# viewing
|
# viewing (no customer)
|
||||||
with patch.object(view, 'viewing', new=True):
|
with patch.object(view, 'viewing', new=True):
|
||||||
form = view.make_form(model_instance=order)
|
form = view.make_form(model_instance=order)
|
||||||
# nb. this is to avoid include/exclude ambiguity
|
# nb. this is to avoid include/exclude ambiguity
|
||||||
form.remove('items')
|
form.remove('items')
|
||||||
view.configure_form(form)
|
view.configure_form(form)
|
||||||
schema = form.get_schema()
|
schema = form.get_schema()
|
||||||
|
self.assertIn('pending_customer', form)
|
||||||
|
self.assertIsInstance(schema['total_price'].typ, WuttaMoney)
|
||||||
|
|
||||||
|
# assign local customer
|
||||||
|
local = model.LocalCustomer(first_name='Jack', last_name='Black',
|
||||||
|
phone_number='555-1234')
|
||||||
|
self.session.add(local)
|
||||||
|
order.local_customer = local
|
||||||
|
self.session.flush()
|
||||||
|
|
||||||
|
# viewing (local customer)
|
||||||
|
with patch.object(view, 'viewing', new=True):
|
||||||
|
form = view.make_form(model_instance=order)
|
||||||
|
# nb. this is to avoid include/exclude ambiguity
|
||||||
|
form.remove('items')
|
||||||
|
view.configure_form(form)
|
||||||
|
self.assertNotIn('pending_customer', form)
|
||||||
|
schema = form.get_schema()
|
||||||
self.assertIsInstance(schema['total_price'].typ, WuttaMoney)
|
self.assertIsInstance(schema['total_price'].typ, WuttaMoney)
|
||||||
|
|
||||||
def test_get_xref_buttons(self):
|
def test_get_xref_buttons(self):
|
||||||
|
@ -831,6 +1157,46 @@ class TestOrderView(WebTestCase):
|
||||||
url = view.get_row_action_url_view(item, 0)
|
url = view.get_row_action_url_view(item, 0)
|
||||||
self.assertIn(f'/order-items/{item.uuid}', url)
|
self.assertIn(f'/order-items/{item.uuid}', url)
|
||||||
|
|
||||||
|
def test_configure(self):
|
||||||
|
self.pyramid_config.add_route('home', '/')
|
||||||
|
self.pyramid_config.add_route('login', '/auth/login')
|
||||||
|
self.pyramid_config.add_route('orders', '/orders/')
|
||||||
|
model = self.app.model
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
with patch.object(view, 'Session', return_value=self.session):
|
||||||
|
with patch.multiple(self.config, usedb=True, preferdb=True):
|
||||||
|
|
||||||
|
# sanity check
|
||||||
|
allowed = self.config.get_bool('sideshow.orders.allow_unknown_products',
|
||||||
|
session=self.session)
|
||||||
|
self.assertIsNone(allowed)
|
||||||
|
self.assertEqual(self.session.query(model.Setting).count(), 0)
|
||||||
|
|
||||||
|
# fetch initial page
|
||||||
|
response = view.configure()
|
||||||
|
self.assertIsInstance(response, Response)
|
||||||
|
self.assertNotIsInstance(response, HTTPFound)
|
||||||
|
self.session.flush()
|
||||||
|
allowed = self.config.get_bool('sideshow.orders.allow_unknown_products',
|
||||||
|
session=self.session)
|
||||||
|
self.assertIsNone(allowed)
|
||||||
|
self.assertEqual(self.session.query(model.Setting).count(), 0)
|
||||||
|
|
||||||
|
# post new settings
|
||||||
|
with patch.multiple(self.request, create=True,
|
||||||
|
method='POST',
|
||||||
|
POST={
|
||||||
|
'sideshow.orders.allow_unknown_products': 'true',
|
||||||
|
}):
|
||||||
|
response = view.configure()
|
||||||
|
self.assertIsInstance(response, HTTPFound)
|
||||||
|
self.session.flush()
|
||||||
|
allowed = self.config.get_bool('sideshow.orders.allow_unknown_products',
|
||||||
|
session=self.session)
|
||||||
|
self.assertTrue(allowed)
|
||||||
|
self.assertTrue(self.session.query(model.Setting).count() > 1)
|
||||||
|
|
||||||
|
|
||||||
class TestOrderItemView(WebTestCase):
|
class TestOrderItemView(WebTestCase):
|
||||||
|
|
||||||
|
@ -864,6 +1230,18 @@ class TestOrderItemView(WebTestCase):
|
||||||
self.assertEqual(view.render_status_code(None, None, enum.ORDER_ITEM_STATUS_INITIATED),
|
self.assertEqual(view.render_status_code(None, None, enum.ORDER_ITEM_STATUS_INITIATED),
|
||||||
'initiated')
|
'initiated')
|
||||||
|
|
||||||
|
def test_get_instance_title(self):
|
||||||
|
model = self.app.model
|
||||||
|
enum = self.app.enum
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
item = model.OrderItem(product_brand='Bragg',
|
||||||
|
product_description='Vinegar',
|
||||||
|
product_size='32oz',
|
||||||
|
status_code=enum.ORDER_ITEM_STATUS_INITIATED)
|
||||||
|
title = view.get_instance_title(item)
|
||||||
|
self.assertEqual(title, "(initiated) Bragg Vinegar 32oz")
|
||||||
|
|
||||||
def test_configure_form(self):
|
def test_configure_form(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
|
@ -871,12 +1249,24 @@ class TestOrderItemView(WebTestCase):
|
||||||
|
|
||||||
item = model.OrderItem(status_code=enum.ORDER_ITEM_STATUS_INITIATED)
|
item = model.OrderItem(status_code=enum.ORDER_ITEM_STATUS_INITIATED)
|
||||||
|
|
||||||
# viewing
|
# viewing, w/ pending product
|
||||||
with patch.object(view, 'viewing', new=True):
|
with patch.object(view, 'viewing', new=True):
|
||||||
form = view.make_form(model_instance=item)
|
form = view.make_form(model_instance=item)
|
||||||
view.configure_form(form)
|
view.configure_form(form)
|
||||||
schema = form.get_schema()
|
schema = form.get_schema()
|
||||||
self.assertIsInstance(schema['order'].typ, OrderRef)
|
self.assertIsInstance(schema['order'].typ, OrderRef)
|
||||||
|
self.assertIn('pending_product', form)
|
||||||
|
self.assertIsInstance(schema['pending_product'].typ, PendingProductRef)
|
||||||
|
|
||||||
|
# viewing, w/ local product
|
||||||
|
local = model.LocalProduct()
|
||||||
|
item.local_product = local
|
||||||
|
with patch.object(view, 'viewing', new=True):
|
||||||
|
form = view.make_form(model_instance=item)
|
||||||
|
view.configure_form(form)
|
||||||
|
schema = form.get_schema()
|
||||||
|
self.assertIsInstance(schema['order'].typ, OrderRef)
|
||||||
|
self.assertNotIn('pending_product', form)
|
||||||
|
|
||||||
def test_get_xref_buttons(self):
|
def test_get_xref_buttons(self):
|
||||||
self.pyramid_config.add_route('orders.view', '/orders/{uuid}')
|
self.pyramid_config.add_route('orders.view', '/orders/{uuid}')
|
||||||
|
|
|
@ -16,6 +16,104 @@ class TestIncludeme(WebTestCase):
|
||||||
mod.includeme(self.pyramid_config)
|
mod.includeme(self.pyramid_config)
|
||||||
|
|
||||||
|
|
||||||
|
class TestLocalProductView(WebTestCase):
|
||||||
|
|
||||||
|
def make_view(self):
|
||||||
|
return mod.LocalProductView(self.request)
|
||||||
|
|
||||||
|
def test_configure_grid(self):
|
||||||
|
model = self.app.model
|
||||||
|
view = self.make_view()
|
||||||
|
grid = view.make_grid(model_class=model.LocalProduct)
|
||||||
|
self.assertNotIn('scancode', grid.linked_columns)
|
||||||
|
self.assertNotIn('brand_name', grid.linked_columns)
|
||||||
|
self.assertNotIn('description', grid.linked_columns)
|
||||||
|
view.configure_grid(grid)
|
||||||
|
self.assertIn('scancode', grid.linked_columns)
|
||||||
|
self.assertIn('brand_name', grid.linked_columns)
|
||||||
|
self.assertIn('description', grid.linked_columns)
|
||||||
|
|
||||||
|
def test_configure_form(self):
|
||||||
|
model = self.app.model
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
# creating
|
||||||
|
with patch.object(view, 'creating', new=True):
|
||||||
|
form = view.make_form(model_class=model.LocalProduct)
|
||||||
|
self.assertIn('external_id', form)
|
||||||
|
view.configure_form(form)
|
||||||
|
self.assertNotIn('external_id', form)
|
||||||
|
|
||||||
|
user = model.User(username='barney')
|
||||||
|
self.session.add(user)
|
||||||
|
product = model.LocalProduct()
|
||||||
|
self.session.add(product)
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
# viewing
|
||||||
|
with patch.object(view, 'viewing', new=True):
|
||||||
|
form = view.make_form(model_instance=product)
|
||||||
|
self.assertNotIn('external_id', form.readonly_fields)
|
||||||
|
self.assertNotIn('local_products.view.orders', form.grid_vue_context)
|
||||||
|
view.configure_form(form)
|
||||||
|
self.assertIn('external_id', form.readonly_fields)
|
||||||
|
self.assertIn('local_products.view.orders', form.grid_vue_context)
|
||||||
|
|
||||||
|
def test_make_orders_grid(self):
|
||||||
|
model = self.app.model
|
||||||
|
enum = self.app.enum
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
user = model.User(username='barney')
|
||||||
|
self.session.add(user)
|
||||||
|
order = model.Order(order_id=42, customer_id=42, created_by=user)
|
||||||
|
product = model.LocalProduct()
|
||||||
|
self.session.add(product)
|
||||||
|
item = model.OrderItem(local_product=product,
|
||||||
|
order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||||
|
status_code=enum.ORDER_ITEM_STATUS_INITIATED)
|
||||||
|
order.items.append(item)
|
||||||
|
self.session.add(order)
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
# no view perm
|
||||||
|
grid = view.make_orders_grid(product)
|
||||||
|
self.assertEqual(len(grid.actions), 0)
|
||||||
|
|
||||||
|
# with view perm
|
||||||
|
with patch.object(self.request, 'is_root', new=True):
|
||||||
|
grid = view.make_orders_grid(product)
|
||||||
|
self.assertEqual(len(grid.actions), 1)
|
||||||
|
self.assertEqual(grid.actions[0].key, 'view')
|
||||||
|
|
||||||
|
def test_make_new_order_batches_grid(self):
|
||||||
|
model = self.app.model
|
||||||
|
enum = self.app.enum
|
||||||
|
handler = NewOrderBatchHandler(self.config)
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
user = model.User(username='barney')
|
||||||
|
self.session.add(user)
|
||||||
|
batch = handler.make_batch(self.session, created_by=user)
|
||||||
|
self.session.add(batch)
|
||||||
|
product = model.LocalProduct()
|
||||||
|
self.session.add(product)
|
||||||
|
row = handler.make_row(local_product=product,
|
||||||
|
order_qty=1, order_uom=enum.ORDER_UOM_UNIT)
|
||||||
|
handler.add_row(batch, row)
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
# no view perm
|
||||||
|
grid = view.make_new_order_batches_grid(product)
|
||||||
|
self.assertEqual(len(grid.actions), 0)
|
||||||
|
|
||||||
|
# with view perm
|
||||||
|
with patch.object(self.request, 'is_root', new=True):
|
||||||
|
grid = view.make_new_order_batches_grid(product)
|
||||||
|
self.assertEqual(len(grid.actions), 1)
|
||||||
|
self.assertEqual(grid.actions[0].key, 'view')
|
||||||
|
|
||||||
|
|
||||||
class TestPendingProductView(WebTestCase):
|
class TestPendingProductView(WebTestCase):
|
||||||
|
|
||||||
def make_view(self):
|
def make_view(self):
|
||||||
|
|
Loading…
Reference in a new issue