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:
Lance Edgar 2025-02-01 19:39:02 -06:00
parent aa31d23fc8
commit 3ca89a8479
5 changed files with 633 additions and 54 deletions

View file

@ -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):
"""

View file

@ -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

View file

@ -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),

View file

@ -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

View file

@ -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