feat: allow re-order past product for new orders
assuming batch has a customer set, with order history nb. this only uses past *products* and not order qty/uom
This commit is contained in:
parent
aa31d23fc8
commit
3ca89a8479
|
@ -26,6 +26,7 @@ New Order Batch Handler
|
|||
|
||||
import datetime
|
||||
import decimal
|
||||
from collections import OrderedDict
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
@ -379,6 +380,21 @@ class NewOrderBatchHandler(BatchHandler):
|
|||
'label': product.full_description}
|
||||
return [result(c) for c in products]
|
||||
|
||||
def get_default_uom_choices(self):
|
||||
"""
|
||||
Returns a list of ordering UOM choices which should be
|
||||
presented to the user by default.
|
||||
|
||||
The built-in logic here will return everything from
|
||||
:data:`~sideshow.enum.ORDER_UOM`.
|
||||
|
||||
:returns: List of dicts, each with ``key`` and ``value``
|
||||
corresponding to the UOM code and label, respectively.
|
||||
"""
|
||||
enum = self.app.enum
|
||||
return [{'key': key, 'value': val}
|
||||
for key, val in enum.ORDER_UOM.items()]
|
||||
|
||||
def get_product_info_external(self, session, product_id, user=None):
|
||||
"""
|
||||
Returns basic info for an :term:`external product` as pertains
|
||||
|
@ -388,7 +404,8 @@ class NewOrderBatchHandler(BatchHandler):
|
|||
choose order quantity and UOM based on case size, pricing
|
||||
etc., this method is called to retrieve the product info.
|
||||
|
||||
There is no default logic here; subclass must implement.
|
||||
There is no default logic here; subclass must implement. See
|
||||
also :meth:`get_product_info_local()`.
|
||||
|
||||
:param session: Current app :term:`db session`.
|
||||
|
||||
|
@ -444,21 +461,58 @@ class NewOrderBatchHandler(BatchHandler):
|
|||
|
||||
def get_product_info_local(self, session, uuid, user=None):
|
||||
"""
|
||||
Returns basic info for a
|
||||
:class:`~sideshow.db.model.products.LocalProduct` as pertains
|
||||
to ordering.
|
||||
Returns basic info for a :term:`local product` as pertains to
|
||||
ordering.
|
||||
|
||||
When user has located a product via search, and must then
|
||||
choose order quantity and UOM based on case size, pricing
|
||||
etc., this method is called to retrieve the product info.
|
||||
|
||||
See :meth:`get_product_info_external()` for more explanation.
|
||||
|
||||
This method will locate the
|
||||
:class:`~sideshow.db.model.products.LocalProduct` record, then
|
||||
(if found) it calls :meth:`normalize_local_product()` and
|
||||
returns the result.
|
||||
|
||||
:param session: Current :term:`db session`.
|
||||
|
||||
:param uuid: UUID for the desired
|
||||
:class:`~sideshow.db.model.products.LocalProduct`.
|
||||
|
||||
:param user:
|
||||
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
|
||||
is performing the action, if known.
|
||||
|
||||
:returns: Dict of product info.
|
||||
"""
|
||||
model = self.app.model
|
||||
product = session.get(model.LocalProduct, uuid)
|
||||
if not product:
|
||||
raise ValueError(f"Local Product not found: {uuid}")
|
||||
|
||||
return self.normalize_local_product(product)
|
||||
|
||||
def normalize_local_product(self, product):
|
||||
"""
|
||||
Returns a normalized dict of info for the given :term:`local
|
||||
product`.
|
||||
|
||||
This is called by:
|
||||
|
||||
* :meth:`get_product_info_local()`
|
||||
* :meth:`get_past_products()`
|
||||
|
||||
:param product:
|
||||
:class:`~sideshow.db.model.products.LocalProduct` instance.
|
||||
|
||||
:returns: Dict of product info.
|
||||
|
||||
The keys for this dict should essentially one-to-one for the
|
||||
product fields, with one exception:
|
||||
|
||||
* ``product_id`` will be set to the product UUID as string
|
||||
"""
|
||||
return {
|
||||
'product_id': product.uuid.hex,
|
||||
'scancode': product.scancode,
|
||||
|
@ -476,6 +530,109 @@ class NewOrderBatchHandler(BatchHandler):
|
|||
'vendor_item_code': product.vendor_item_code,
|
||||
}
|
||||
|
||||
def get_past_orders(self, batch):
|
||||
"""
|
||||
Retrieve a (possibly empty) list of past :term:`orders
|
||||
<order>` for the batch customer.
|
||||
|
||||
This is called by :meth:`get_past_products()`.
|
||||
|
||||
:param batch:
|
||||
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch`
|
||||
instance.
|
||||
|
||||
:returns: List of :class:`~sideshow.db.model.orders.Order`
|
||||
records.
|
||||
"""
|
||||
model = self.app.model
|
||||
session = self.app.get_session(batch)
|
||||
orders = session.query(model.Order)
|
||||
|
||||
if batch.customer_id:
|
||||
orders = orders.filter(model.Order.customer_id == batch.customer_id)
|
||||
elif batch.local_customer:
|
||||
orders = orders.filter(model.Order.local_customer == batch.local_customer)
|
||||
else:
|
||||
raise ValueError(f"batch has no customer: {batch}")
|
||||
|
||||
orders = orders.order_by(model.Order.created.desc())
|
||||
return orders.all()
|
||||
|
||||
def get_past_products(self, batch, user=None):
|
||||
"""
|
||||
Retrieve a (possibly empty) list of products which have been
|
||||
previously ordered by the batch customer.
|
||||
|
||||
Note that this does not return :term:`order items <order
|
||||
item>`, nor does it return true product records, but rather it
|
||||
returns a list of dicts. Each will have product info but will
|
||||
*not* have order quantity etc.
|
||||
|
||||
This method calls :meth:`get_past_orders()` and then iterates
|
||||
through each order item therein. Any duplicated products
|
||||
encountered will be skipped, so the final list contains unique
|
||||
products.
|
||||
|
||||
Each dict in the result is obtained by calling one of:
|
||||
|
||||
* :meth:`normalize_local_product()`
|
||||
* :meth:`get_product_info_external()`
|
||||
|
||||
:param batch:
|
||||
:class:`~sideshow.db.model.batch.neworder.NewOrderBatch`
|
||||
instance.
|
||||
|
||||
:param user:
|
||||
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
|
||||
is performing the action, if known.
|
||||
|
||||
:returns: List of product info dicts.
|
||||
"""
|
||||
model = self.app.model
|
||||
session = self.app.get_session(batch)
|
||||
use_local = self.use_local_products()
|
||||
user = user or batch.created_by
|
||||
products = OrderedDict()
|
||||
|
||||
# track down all order items for batch contact
|
||||
for order in self.get_past_orders(batch):
|
||||
for item in order.items:
|
||||
|
||||
# nb. we only need the first match for each product
|
||||
if use_local:
|
||||
product = item.local_product
|
||||
if product and product.uuid not in products:
|
||||
products[product.uuid] = self.normalize_local_product(product)
|
||||
elif item.product_id and item.product_id not in products:
|
||||
products[item.product_id] = self.get_product_info_external(
|
||||
session, item.product_id, user=user)
|
||||
|
||||
products = list(products.values())
|
||||
for product in products:
|
||||
|
||||
price = product['unit_price_reg']
|
||||
|
||||
if 'unit_price_reg_display' not in product:
|
||||
product['unit_price_reg_display'] = self.app.render_currency(price)
|
||||
|
||||
if 'unit_price_quoted' not in product:
|
||||
product['unit_price_quoted'] = price
|
||||
|
||||
if 'unit_price_quoted_display' not in product:
|
||||
product['unit_price_quoted_display'] = product['unit_price_reg_display']
|
||||
|
||||
if ('case_price_quoted' not in product
|
||||
and product.get('unit_price_quoted') is not None
|
||||
and product.get('case_size') is not None):
|
||||
product['case_price_quoted'] = product['unit_price_quoted'] * product['case_size']
|
||||
|
||||
if ('case_price_quoted_display' not in product
|
||||
and 'case_price_quoted' in product):
|
||||
product['case_price_quoted_display'] = self.app.render_currency(
|
||||
product['case_price_quoted'])
|
||||
|
||||
return products
|
||||
|
||||
def add_item(self, batch, product_info, order_qty, order_uom,
|
||||
discount_percent=None, user=None):
|
||||
"""
|
||||
|
|
|
@ -355,6 +355,135 @@
|
|||
@click="showAddItemDialog()">
|
||||
Add Item
|
||||
</b-button>
|
||||
% if allow_past_item_reorder:
|
||||
<b-button v-if="customerIsKnown && customerID"
|
||||
icon-pack="fas"
|
||||
icon-left="plus"
|
||||
@click="showAddPastItem()">
|
||||
Add Past Item
|
||||
</b-button>
|
||||
|
||||
<${b}-modal
|
||||
% if request.use_oruga:
|
||||
v-model:active="pastItemsShowDialog"
|
||||
% else:
|
||||
:active.sync="pastItemsShowDialog"
|
||||
% endif
|
||||
>
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
|
||||
<${b}-table :data="pastItems"
|
||||
icon-pack="fas"
|
||||
:loading="pastItemsLoading"
|
||||
% if request.use_oruga:
|
||||
v-model:selected="pastItemsSelected"
|
||||
% else:
|
||||
:selected.sync="pastItemsSelected"
|
||||
% endif
|
||||
sortable
|
||||
paginated
|
||||
per-page="5"
|
||||
## :debounce-search="1000"
|
||||
>
|
||||
|
||||
<${b}-table-column label="Scancode"
|
||||
field="key"
|
||||
v-slot="props"
|
||||
sortable>
|
||||
{{ props.row.scancode }}
|
||||
</${b}-table-column>
|
||||
|
||||
<${b}-table-column label="Brand"
|
||||
field="brand_name"
|
||||
v-slot="props"
|
||||
sortable
|
||||
searchable>
|
||||
{{ props.row.brand_name }}
|
||||
</${b}-table-column>
|
||||
|
||||
<${b}-table-column label="Description"
|
||||
field="description"
|
||||
v-slot="props"
|
||||
sortable
|
||||
searchable>
|
||||
{{ props.row.description }}
|
||||
{{ props.row.size }}
|
||||
</${b}-table-column>
|
||||
|
||||
<${b}-table-column label="Unit Price"
|
||||
field="unit_price_reg_display"
|
||||
v-slot="props"
|
||||
sortable>
|
||||
{{ props.row.unit_price_reg_display }}
|
||||
</${b}-table-column>
|
||||
|
||||
<${b}-table-column label="Sale Price"
|
||||
field="sale_price"
|
||||
v-slot="props"
|
||||
sortable>
|
||||
<span class="has-background-warning">
|
||||
{{ props.row.sale_price_display }}
|
||||
</span>
|
||||
</${b}-table-column>
|
||||
|
||||
<${b}-table-column label="Sale Ends"
|
||||
field="sale_ends"
|
||||
v-slot="props"
|
||||
sortable>
|
||||
<span class="has-background-warning">
|
||||
{{ props.row.sale_ends_display }}
|
||||
</span>
|
||||
</${b}-table-column>
|
||||
|
||||
<${b}-table-column label="Department"
|
||||
field="department_name"
|
||||
v-slot="props"
|
||||
sortable
|
||||
searchable>
|
||||
{{ props.row.department_name }}
|
||||
</${b}-table-column>
|
||||
|
||||
<${b}-table-column label="Vendor"
|
||||
field="vendor_name"
|
||||
v-slot="props"
|
||||
sortable
|
||||
searchable>
|
||||
{{ props.row.vendor_name }}
|
||||
</${b}-table-column>
|
||||
|
||||
<template #empty>
|
||||
<div class="content has-text-grey has-text-centered">
|
||||
<p>
|
||||
<b-icon
|
||||
pack="fas"
|
||||
icon="sad-tear"
|
||||
size="is-large">
|
||||
</b-icon>
|
||||
</p>
|
||||
<p>Nothing here.</p>
|
||||
</div>
|
||||
</template>
|
||||
</${b}-table>
|
||||
|
||||
<div class="buttons">
|
||||
<b-button @click="pastItemsShowDialog = false">
|
||||
Cancel
|
||||
</b-button>
|
||||
<b-button type="is-primary"
|
||||
icon-pack="fas"
|
||||
icon-left="plus"
|
||||
@click="pastItemsAddSelected()"
|
||||
:disabled="!pastItemsSelected">
|
||||
Add Selected Item
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</${b}-modal>
|
||||
|
||||
% endif
|
||||
</div>
|
||||
|
||||
<${b}-modal
|
||||
|
@ -942,6 +1071,13 @@
|
|||
## departmentOptions: ${json.dumps(department_options)|n},
|
||||
departmentOptions: [],
|
||||
|
||||
% if allow_past_item_reorder:
|
||||
pastItemsShowDialog: false,
|
||||
pastItemsLoading: false,
|
||||
pastItems: [],
|
||||
pastItemsSelected: null,
|
||||
% endif
|
||||
|
||||
// nb. hack to force refresh for vue3
|
||||
refreshProductDescription: 1,
|
||||
refreshTotalPrice: 1,
|
||||
|
@ -1218,6 +1354,7 @@
|
|||
|
||||
customerChanged(customerID, callback) {
|
||||
this.customerLoading = true
|
||||
this.pastItems = []
|
||||
|
||||
const params = {}
|
||||
if (customerID) {
|
||||
|
@ -1272,8 +1409,10 @@
|
|||
% if allow_item_discounts:
|
||||
|
||||
updateDiscount(deptID) {
|
||||
// nb. our map requires ID is string
|
||||
deptID = deptID.toString()
|
||||
if (deptID) {
|
||||
// nb. our map requires ID as string
|
||||
deptID = deptID.toString()
|
||||
}
|
||||
const i = Object.keys(this.deptItemDiscounts).indexOf(deptID)
|
||||
if (i == -1) {
|
||||
this.productDiscountPercent = this.defaultItemDiscount
|
||||
|
@ -1601,7 +1740,7 @@
|
|||
## this.productSpecialOrder = row.special_order
|
||||
|
||||
this.productQuantity = row.order_qty
|
||||
this.productUnitChoices = row.order_uom_choices
|
||||
this.productUnitChoices = row?.order_uom_choices || this.defaultUnitChoices
|
||||
this.productUOM = row.order_uom
|
||||
|
||||
% if allow_item_discounts:
|
||||
|
@ -1643,6 +1782,71 @@
|
|||
})
|
||||
},
|
||||
|
||||
% if allow_past_item_reorder:
|
||||
|
||||
showAddPastItem() {
|
||||
this.pastItemsSelected = null
|
||||
if (!this.pastItems.length) {
|
||||
this.pastItemsLoading = true
|
||||
const params = {action: 'get_past_products'}
|
||||
this.submitBatchData(params, ({data}) => {
|
||||
this.pastItems = data
|
||||
this.pastItemsLoading = false
|
||||
})
|
||||
}
|
||||
this.pastItemsShowDialog = true
|
||||
},
|
||||
|
||||
pastItemsAddSelected() {
|
||||
this.pastItemsShowDialog = false
|
||||
const selected = this.pastItemsSelected
|
||||
|
||||
this.editItemRow = null
|
||||
this.productIsKnown = true
|
||||
this.productID = selected.product_id
|
||||
|
||||
this.selectedProduct = {
|
||||
product_id: selected.product_id,
|
||||
full_description: selected.full_description,
|
||||
// url: selected.product_url,
|
||||
}
|
||||
|
||||
this.productDisplay = selected.full_description
|
||||
this.productScancode = selected.scancode
|
||||
this.productSize = selected.size
|
||||
this.productCaseQuantity = selected.case_size
|
||||
this.productUnitPrice = selected.unit_price_quoted
|
||||
this.productUnitPriceDisplay = selected.unit_price_quoted_display
|
||||
this.productUnitRegularPriceDisplay = selected.unit_price_reg_display
|
||||
this.productCasePrice = selected.case_price_quoted
|
||||
this.productCasePriceDisplay = selected.case_price_quoted_display
|
||||
this.productSalePrice = selected.unit_price_sale
|
||||
this.productSalePriceDisplay = selected.unit_price_sale_display
|
||||
this.productSaleEndsDisplay = selected.sale_ends_display
|
||||
this.productSpecialOrder = selected.special_order
|
||||
|
||||
this.productQuantity = 1
|
||||
this.productUnitChoices = selected?.order_uom_choices || this.defaultUnitChoices
|
||||
this.productUOM = selected?.order_uom || this.defaultUOM
|
||||
|
||||
% if allow_item_discounts:
|
||||
this.updateDiscount(selected.department_id)
|
||||
% endif
|
||||
|
||||
// nb. hack to force refresh for vue3
|
||||
this.refreshProductDescription += 1
|
||||
this.refreshTotalPrice += 1
|
||||
|
||||
% if request.use_oruga:
|
||||
this.itemDialogTab = 'quantity'
|
||||
% else:
|
||||
this.itemDialogTabIndex = 1
|
||||
% endif
|
||||
this.editItemShowDialog = true
|
||||
},
|
||||
|
||||
% endif
|
||||
|
||||
itemDialogAttemptSave() {
|
||||
this.itemDialogSaving = true
|
||||
this.editItemLoading = true
|
||||
|
|
|
@ -37,6 +37,7 @@ from webhelpers2.html import tags, HTML
|
|||
|
||||
from wuttaweb.views import MasterView
|
||||
from wuttaweb.forms.schema import UserRef, WuttaMoney, WuttaQuantity, WuttaEnum, WuttaDictEnum
|
||||
from wuttaweb.util import make_json_safe
|
||||
|
||||
from sideshow.db.model import Order, OrderItem
|
||||
from sideshow.batch.neworder import NewOrderBatchHandler
|
||||
|
@ -66,13 +67,13 @@ class OrderView(MasterView):
|
|||
.. attribute:: order_handler
|
||||
|
||||
Reference to the :term:`order handler` as returned by
|
||||
:meth:`get_order_handler()`. This gets set in the constructor.
|
||||
:meth:`~sideshow.app.SideshowAppProvider.get_order_handler()`.
|
||||
This gets set in the constructor.
|
||||
|
||||
.. attribute:: batch_handler
|
||||
|
||||
Reference to the :term:`new order batch` handler, as returned
|
||||
by :meth:`get_batch_handler()`. This gets set in the
|
||||
constructor.
|
||||
Reference to the :term:`new order batch` handler. This gets
|
||||
set in the constructor.
|
||||
"""
|
||||
model_class = Order
|
||||
editable = False
|
||||
|
@ -158,22 +159,7 @@ class OrderView(MasterView):
|
|||
def __init__(self, request, context=None):
|
||||
super().__init__(request, context=context)
|
||||
self.order_handler = self.app.get_order_handler()
|
||||
|
||||
def get_batch_handler(self):
|
||||
"""
|
||||
Returns the configured :term:`handler` for :term:`new order
|
||||
batches <new order batch>`.
|
||||
|
||||
You normally would not need to call this, and can use
|
||||
:attr:`batch_handler` instead.
|
||||
|
||||
:returns:
|
||||
:class:`~sideshow.batch.neworder.NewOrderBatchHandler`
|
||||
instance.
|
||||
"""
|
||||
if hasattr(self, 'batch_handler'):
|
||||
return self.batch_handler
|
||||
return self.app.get_batch_handler('neworder')
|
||||
self.batch_handler = self.app.get_batch_handler('neworder')
|
||||
|
||||
def configure_grid(self, g):
|
||||
""" """
|
||||
|
@ -229,7 +215,6 @@ class OrderView(MasterView):
|
|||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
session = self.Session()
|
||||
self.batch_handler = self.get_batch_handler()
|
||||
batch = self.get_current_batch()
|
||||
self.creating = True
|
||||
|
||||
|
@ -259,7 +244,7 @@ class OrderView(MasterView):
|
|||
# 'get_customer_info',
|
||||
# # 'set_customer_data',
|
||||
'get_product_info',
|
||||
# 'get_past_items',
|
||||
'get_past_products',
|
||||
'add_item',
|
||||
'update_item',
|
||||
'delete_item',
|
||||
|
@ -280,13 +265,14 @@ class OrderView(MasterView):
|
|||
'normalized_batch': self.normalize_batch(batch),
|
||||
'order_items': [self.normalize_row(row)
|
||||
for row in batch.rows],
|
||||
'default_uom_choices': self.get_default_uom_choices(),
|
||||
'default_uom_choices': self.batch_handler.get_default_uom_choices(),
|
||||
'default_uom': None, # TODO?
|
||||
'expose_store_id': self.order_handler.expose_store_id(),
|
||||
'allow_item_discounts': self.batch_handler.allow_item_discounts(),
|
||||
'allow_unknown_products': (self.batch_handler.allow_unknown_products()
|
||||
and self.has_perm('create_unknown_product')),
|
||||
'pending_product_required_fields': self.get_pending_product_required_fields(),
|
||||
'allow_past_item_reorder': True, # TODO: make configurable?
|
||||
})
|
||||
|
||||
if context['expose_store_id']:
|
||||
|
@ -364,7 +350,7 @@ class OrderView(MasterView):
|
|||
if not term:
|
||||
return []
|
||||
|
||||
handler = self.get_batch_handler()
|
||||
handler = self.batch_handler
|
||||
if handler.use_local_customers():
|
||||
return handler.autocomplete_customers_local(session, term, user=self.request.user)
|
||||
else:
|
||||
|
@ -388,7 +374,7 @@ class OrderView(MasterView):
|
|||
if not term:
|
||||
return []
|
||||
|
||||
handler = self.get_batch_handler()
|
||||
handler = self.batch_handler
|
||||
if handler.use_local_products():
|
||||
return handler.autocomplete_products_local(session, term, user=self.request.user)
|
||||
else:
|
||||
|
@ -597,18 +583,20 @@ class OrderView(MasterView):
|
|||
|
||||
def get_product_info(self, batch, data):
|
||||
"""
|
||||
Fetch data for a specific product. (Nothing is modified.)
|
||||
Fetch data for a specific product.
|
||||
|
||||
Depending on config, this will fetch a :term:`local product`
|
||||
or :term:`external product` to get the data.
|
||||
Depending on config, this calls one of the following to get
|
||||
its primary 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.
|
||||
* :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.get_product_info_local()`
|
||||
* :meth:`~sideshow.batch.neworder.NewOrderBatchHandler.get_product_info_external()`
|
||||
|
||||
It then may supplement the data with additional fields.
|
||||
|
||||
This is a "batch action" method which may be called from
|
||||
:meth:`create()`.
|
||||
|
||||
:returns: Dict of product info.
|
||||
"""
|
||||
product_id = data.get('product_id')
|
||||
if not product_id:
|
||||
|
@ -656,6 +644,22 @@ class OrderView(MasterView):
|
|||
|
||||
return data
|
||||
|
||||
def get_past_products(self, batch, data):
|
||||
"""
|
||||
Fetch past products for convenient re-ordering.
|
||||
|
||||
This essentially calls
|
||||
:meth:`~sideshow.batch.neworder.NewOrderBatchHandler.get_past_products()`
|
||||
on the :attr:`batch_handler` and returns the result.
|
||||
|
||||
This is a "batch action" method which may be called from
|
||||
:meth:`create()`.
|
||||
|
||||
:returns: List of product info dicts.
|
||||
"""
|
||||
past_products = self.batch_handler.get_past_products(batch)
|
||||
return make_json_safe(past_products)
|
||||
|
||||
def add_item(self, batch, data):
|
||||
"""
|
||||
This adds a row to the user's current new order batch.
|
||||
|
@ -772,12 +776,6 @@ class OrderView(MasterView):
|
|||
'status_text': batch.status_text,
|
||||
}
|
||||
|
||||
def get_default_uom_choices(self):
|
||||
""" """
|
||||
enum = self.app.enum
|
||||
return [{'key': key, 'value': val}
|
||||
for key, val in enum.ORDER_UOM.items()]
|
||||
|
||||
def normalize_row(self, row):
|
||||
""" """
|
||||
data = {
|
||||
|
@ -798,7 +796,6 @@ class OrderView(MasterView):
|
|||
'case_size': float(row.case_size) if row.case_size is not None else None,
|
||||
'order_qty': float(row.order_qty),
|
||||
'order_uom': row.order_uom,
|
||||
'order_uom_choices': self.get_default_uom_choices(),
|
||||
'discount_percent': self.app.render_quantity(row.discount_percent),
|
||||
'unit_price_quoted': float(row.unit_price_quoted) if row.unit_price_quoted is not None else None,
|
||||
'unit_price_quoted_display': self.app.render_currency(row.unit_price_quoted),
|
||||
|
|
|
@ -4,6 +4,8 @@ import datetime
|
|||
import decimal
|
||||
from unittest.mock import patch
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from wuttjamaican.testing import DataTestCase
|
||||
|
||||
from sideshow.batch import neworder as mod
|
||||
|
@ -267,6 +269,14 @@ class TestNewOrderBatchHandler(DataTestCase):
|
|||
# search for juice finds nothing
|
||||
self.assertEqual(handler.autocomplete_products_local(self.session, 'juice'), [])
|
||||
|
||||
def test_get_default_uom_choices(self):
|
||||
enum = self.app.enum
|
||||
handler = self.make_handler()
|
||||
|
||||
uoms = handler.get_default_uom_choices()
|
||||
self.assertEqual(uoms, [{'key': key, 'value': val}
|
||||
for key, val in enum.ORDER_UOM.items()])
|
||||
|
||||
def test_get_product_info_external(self):
|
||||
handler = self.make_handler()
|
||||
self.assertRaises(NotImplementedError, handler.get_product_info_external,
|
||||
|
@ -308,6 +318,174 @@ class TestNewOrderBatchHandler(DataTestCase):
|
|||
mock_uuid = self.app.make_true_uuid()
|
||||
self.assertRaises(ValueError, handler.get_product_info_local, self.session, mock_uuid.hex)
|
||||
|
||||
def test_normalize_local_product(self):
|
||||
model = self.app.model
|
||||
handler = self.make_handler()
|
||||
|
||||
product = model.LocalProduct(scancode='07430500132',
|
||||
brand_name="Bragg's",
|
||||
description="Apple Cider Vinegar",
|
||||
size="32oz",
|
||||
department_name="Grocery",
|
||||
case_size=12,
|
||||
unit_price_reg=5.99,
|
||||
vendor_name="UNFI",
|
||||
vendor_item_code='1234')
|
||||
self.session.add(product)
|
||||
self.session.flush()
|
||||
|
||||
info = handler.normalize_local_product(product)
|
||||
self.assertIsInstance(info, dict)
|
||||
self.assertEqual(info['product_id'], product.uuid.hex)
|
||||
for prop in sa.inspect(model.LocalProduct).column_attrs:
|
||||
if prop.key == 'uuid':
|
||||
continue
|
||||
if prop.key not in info:
|
||||
continue
|
||||
self.assertEqual(info[prop.key], getattr(product, prop.key))
|
||||
|
||||
def test_get_past_orders(self):
|
||||
model = self.app.model
|
||||
handler = self.make_handler()
|
||||
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
batch = handler.make_batch(self.session, created_by=user)
|
||||
self.session.add(batch)
|
||||
self.session.flush()
|
||||
|
||||
# ..will test local customers first
|
||||
|
||||
# error if no customer
|
||||
self.assertRaises(ValueError, handler.get_past_orders, batch)
|
||||
|
||||
# empty history for customer
|
||||
customer = model.LocalCustomer(full_name='Fred Flintstone')
|
||||
batch.local_customer = customer
|
||||
self.session.flush()
|
||||
orders = handler.get_past_orders(batch)
|
||||
self.assertEqual(len(orders), 0)
|
||||
|
||||
# mock historical order
|
||||
order = model.Order(order_id=42, local_customer=customer, created_by=user)
|
||||
self.session.add(order)
|
||||
self.session.flush()
|
||||
|
||||
# that should now be returned
|
||||
orders = handler.get_past_orders(batch)
|
||||
self.assertEqual(len(orders), 1)
|
||||
self.assertIs(orders[0], order)
|
||||
|
||||
# ..now we test external customers, w/ new batch
|
||||
with patch.object(handler, 'use_local_customers', return_value=False):
|
||||
batch2 = handler.make_batch(self.session, created_by=user)
|
||||
self.session.add(batch2)
|
||||
self.session.flush()
|
||||
|
||||
# error if no customer
|
||||
self.assertRaises(ValueError, handler.get_past_orders, batch2)
|
||||
|
||||
# empty history for customer
|
||||
batch2.customer_id = '123'
|
||||
self.session.flush()
|
||||
orders = handler.get_past_orders(batch2)
|
||||
self.assertEqual(len(orders), 0)
|
||||
|
||||
# mock historical order
|
||||
order2 = model.Order(order_id=42, customer_id='123', created_by=user)
|
||||
self.session.add(order2)
|
||||
self.session.flush()
|
||||
|
||||
# that should now be returned
|
||||
orders = handler.get_past_orders(batch2)
|
||||
self.assertEqual(len(orders), 1)
|
||||
self.assertIs(orders[0], order2)
|
||||
|
||||
def test_get_past_products(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
handler = self.make_handler()
|
||||
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
batch = handler.make_batch(self.session, created_by=user)
|
||||
self.session.add(batch)
|
||||
self.session.flush()
|
||||
|
||||
# (nb. this all assumes local customers)
|
||||
|
||||
# ..will test local products first
|
||||
|
||||
# error if no customer
|
||||
self.assertRaises(ValueError, handler.get_past_products, batch)
|
||||
|
||||
# empty history for customer
|
||||
customer = model.LocalCustomer(full_name='Fred Flintstone')
|
||||
batch.local_customer = customer
|
||||
self.session.flush()
|
||||
products = handler.get_past_products(batch)
|
||||
self.assertEqual(len(products), 0)
|
||||
|
||||
# mock historical order
|
||||
order = model.Order(order_id=42, local_customer=customer, created_by=user)
|
||||
product = model.LocalProduct(scancode='07430500132', description='Vinegar',
|
||||
unit_price_reg=5.99, case_size=12)
|
||||
item = model.OrderItem(local_product=product, order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_READY)
|
||||
order.items.append(item)
|
||||
self.session.add(order)
|
||||
self.session.flush()
|
||||
self.session.refresh(product)
|
||||
|
||||
# that should now be returned
|
||||
products = handler.get_past_products(batch)
|
||||
self.assertEqual(len(products), 1)
|
||||
self.assertEqual(products[0]['product_id'], product.uuid.hex)
|
||||
self.assertEqual(products[0]['scancode'], '07430500132')
|
||||
self.assertEqual(products[0]['description'], 'Vinegar')
|
||||
self.assertEqual(products[0]['case_price_quoted'], decimal.Decimal('71.88'))
|
||||
self.assertEqual(products[0]['case_price_quoted_display'], '$71.88')
|
||||
|
||||
# ..now we test external products, w/ new batch
|
||||
with patch.object(handler, 'use_local_products', return_value=False):
|
||||
batch2 = handler.make_batch(self.session, created_by=user)
|
||||
self.session.add(batch2)
|
||||
self.session.flush()
|
||||
|
||||
# error if no customer
|
||||
self.assertRaises(ValueError, handler.get_past_products, batch2)
|
||||
|
||||
# empty history for customer
|
||||
batch2.local_customer = customer
|
||||
self.session.flush()
|
||||
products = handler.get_past_products(batch2)
|
||||
self.assertEqual(len(products), 0)
|
||||
|
||||
# mock historical order
|
||||
order2 = model.Order(order_id=44, local_customer=customer, created_by=user)
|
||||
self.session.add(order2)
|
||||
item2 = model.OrderItem(product_id='07430500116',
|
||||
order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_READY)
|
||||
order2.items.append(item2)
|
||||
self.session.flush()
|
||||
|
||||
# its product should now be returned
|
||||
with patch.object(handler, 'get_product_info_external', return_value={
|
||||
'product_id': '07430500116',
|
||||
'scancode': '07430500116',
|
||||
'description': 'VINEGAR',
|
||||
'unit_price_reg': decimal.Decimal('3.99'),
|
||||
'case_size': 12,
|
||||
}):
|
||||
products = handler.get_past_products(batch2)
|
||||
self.assertEqual(len(products), 1)
|
||||
self.assertEqual(products[0]['product_id'], '07430500116')
|
||||
self.assertEqual(products[0]['scancode'], '07430500116')
|
||||
self.assertEqual(products[0]['description'], 'VINEGAR')
|
||||
self.assertEqual(products[0]['case_price_quoted'], decimal.Decimal('47.88'))
|
||||
self.assertEqual(products[0]['case_price_quoted_display'], '$47.88')
|
||||
|
||||
def test_add_item(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
|
|
|
@ -16,6 +16,7 @@ from sideshow.orders import OrderHandler
|
|||
from sideshow.testing import WebTestCase
|
||||
from sideshow.web.views import orders as mod
|
||||
from sideshow.web.forms.schema import OrderRef, PendingProductRef
|
||||
from sideshow.config import SideshowConfig
|
||||
|
||||
|
||||
class TestIncludeme(WebTestCase):
|
||||
|
@ -26,6 +27,11 @@ class TestIncludeme(WebTestCase):
|
|||
|
||||
class TestOrderView(WebTestCase):
|
||||
|
||||
def make_config(self, **kw):
|
||||
config = super().make_config(**kw)
|
||||
SideshowConfig().configure(config)
|
||||
return config
|
||||
|
||||
def make_view(self):
|
||||
return mod.OrderView(self.request)
|
||||
|
||||
|
@ -661,6 +667,51 @@ class TestOrderView(WebTestCase):
|
|||
context = view.get_product_info(batch, {'product_id': '42'})
|
||||
self.assertEqual(context, {'error': "something smells fishy"})
|
||||
|
||||
def test_get_past_products(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
view = self.make_view()
|
||||
handler = view.batch_handler
|
||||
|
||||
user = model.User(username='barney')
|
||||
self.session.add(user)
|
||||
batch = handler.make_batch(self.session, created_by=user)
|
||||
self.session.add(batch)
|
||||
self.session.flush()
|
||||
|
||||
# (nb. this all assumes local customers and products)
|
||||
|
||||
# error if no customer
|
||||
self.assertRaises(ValueError, view.get_past_products, batch, {})
|
||||
|
||||
# empty history for customer
|
||||
customer = model.LocalCustomer(full_name='Fred Flintstone')
|
||||
batch.local_customer = customer
|
||||
self.session.flush()
|
||||
products = view.get_past_products(batch, {})
|
||||
self.assertEqual(len(products), 0)
|
||||
|
||||
# mock historical order
|
||||
order = model.Order(order_id=42, local_customer=customer, created_by=user)
|
||||
product = model.LocalProduct(scancode='07430500132', description='Vinegar',
|
||||
unit_price_reg=5.99, case_size=12)
|
||||
item = model.OrderItem(local_product=product, order_qty=1, order_uom=enum.ORDER_UOM_UNIT,
|
||||
status_code=enum.ORDER_ITEM_STATUS_READY)
|
||||
order.items.append(item)
|
||||
self.session.add(order)
|
||||
self.session.flush()
|
||||
self.session.refresh(product)
|
||||
|
||||
# that should now be returned
|
||||
products = view.get_past_products(batch, {})
|
||||
self.assertEqual(len(products), 1)
|
||||
self.assertEqual(products[0]['product_id'], product.uuid.hex)
|
||||
self.assertEqual(products[0]['scancode'], '07430500132')
|
||||
self.assertEqual(products[0]['description'], 'Vinegar')
|
||||
# nb. this is a float, since result is JSON-safe
|
||||
self.assertEqual(products[0]['case_price_quoted'], 71.88)
|
||||
self.assertEqual(products[0]['case_price_quoted_display'], '$71.88')
|
||||
|
||||
def test_add_item(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
|
@ -911,14 +962,6 @@ class TestOrderView(WebTestCase):
|
|||
'error': f"ValueError: batch has already been executed: {batch}",
|
||||
})
|
||||
|
||||
def test_get_default_uom_choices(self):
|
||||
enum = self.app.enum
|
||||
view = self.make_view()
|
||||
|
||||
uoms = view.get_default_uom_choices()
|
||||
self.assertEqual(uoms, [{'key': key, 'value': val}
|
||||
for key, val in enum.ORDER_UOM.items()])
|
||||
|
||||
def test_normalize_batch(self):
|
||||
model = self.app.model
|
||||
enum = self.app.enum
|
||||
|
|
Loading…
Reference in a new issue