fix: expose config for new order w/ pending product

- whether or not this feature should be allowed at all
- user must have permission also
- UI should honor both of the above
- also, which fields are required for new pending product
This commit is contained in:
Lance Edgar 2025-01-06 19:46:43 -06:00
parent e677cd5d8c
commit ebd22fe6ee
5 changed files with 177 additions and 28 deletions

View file

@ -44,6 +44,14 @@ class NewOrderBatchHandler(BatchHandler):
"""
model_class = NewOrderBatch
def allow_unknown_product(self):
"""
Returns a boolean indicating whether "unknown" (pending)
products are allowed when creating a new order.
"""
return self.config.get_bool('sideshow.orders.allow_unknown_product',
default=True)
def set_pending_customer(self, batch, data):
"""
Set (add or update) pending customer info for the batch.
@ -122,6 +130,9 @@ class NewOrderBatchHandler(BatchHandler):
: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
enum = self.app.enum
session = self.app.get_session(batch)
@ -167,6 +178,9 @@ class NewOrderBatchHandler(BatchHandler):
: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
enum = self.app.enum
session = self.app.get_session(row)

View file

@ -0,0 +1,41 @@
## -*- coding: utf-8; -*-
<%inherit file="/configure.mako" />
<%def name="form_content()">
<h3 class="block is-size-3">Products</h3>
<div class="block" style="padding-left: 2rem;">
<b-field message="If set, user can enter details of an arbitrary new &quot;pending&quot; product.">
<b-checkbox name="sideshow.orders.allow_unknown_product"
v-model="simpleSettings['sideshow.orders.allow_unknown_product']"
native-value="true"
@input="settingsNeedSaved = true">
Allow creating orders for "unknown" products
</b-checkbox>
</b-field>
<div v-show="simpleSettings['sideshow.orders.allow_unknown_product']"
style="padding-left: 2rem;">
<p class="block">
Require these fields for new product:
</p>
<div class="block"
style="margin-left: 2rem;">
% for field in pending_product_fields:
<b-field>
<b-checkbox name="sideshow.orders.unknown_product.fields.${field}.required"
v-model="simpleSettings['sideshow.orders.unknown_product.fields.${field}.required']"
native-value="true"
@input="settingsNeedSaved = true">
${field}
</b-checkbox>
</b-field>
% endfor
</div>
</div>
</div>
</%def>

View file

@ -402,8 +402,8 @@
<div v-if="productID">
<b-field grouped>
<b-field :label="productKeyLabel">
<span>{{ productKey }}</span>
<b-field label="Scancode">
<span>{{ productScancode }}</span>
</b-field>
<b-field label="Unit Size">
@ -493,12 +493,12 @@
<div style="display: flex; gap: 1rem;">
<b-field :label="productKeyLabel"
% if 'key' in pending_product_required_fields:
:type="pendingProduct[productKeyField] ? null : 'is-danger'"
<b-field label="Scancode"
% if 'scancode' in pending_product_required_fields:
:type="pendingProduct.scancode ? null : 'is-danger'"
% endif
style="width: 100%;">
<b-input v-model="pendingProduct[productKeyField]" />
<b-input v-model="pendingProduct.scancode" />
</b-field>
<b-field label="Department"
@ -725,9 +725,9 @@
:data="items"
:row-class="(row, i) => row.product_id ? null : 'has-text-success'">
<${b}-table-column :label="productKeyLabel"
<${b}-table-column label="Scancode"
v-slot="props">
{{ props.row.product_key }}
{{ props.row.product_scancode }}
</${b}-table-column>
<${b}-table-column label="Brand"
@ -876,16 +876,11 @@
itemDialogTabIndex: 0,
% endif
## TODO
productIsKnown: false,
productIsKnown: true,
selectedProduct: null,
productID: null,
productDisplay: null,
## TODO
productKey: null,
productKeyField: 'scancode',
productKeyLabel: "Scancode",
productScancode: null,
productSize: null,
productCaseQuantity: null,
productUnitPrice: null,
@ -1341,13 +1336,11 @@
showAddItemDialog() {
this.customerPanelOpen = false
this.editingItem = null
// TODO
// this.productIsKnown = true
this.productIsKnown = false
this.productIsKnown = true
## this.selectedProduct = null
this.productID = null
this.productDisplay = null
## this.productKey = null
this.productScancode = null
this.productSize = null
this.productCaseQuantity = null
this.productUnitPrice = null
@ -1402,7 +1395,7 @@
this.pendingProduct = pending
this.productDisplay = row.product_full_description
this.productKey = row.product_key
this.productScancode = row.product_scancode
this.productSize = row.product_size
this.productCaseQuantity = row.case_quantity
this.productURL = row.product_url

View file

@ -58,6 +58,7 @@ class OrderView(MasterView):
"""
model_class = Order
editable = False
configurable = True
labels = {
'order_id': "Order ID",
@ -122,15 +123,14 @@ class OrderView(MasterView):
PENDING_PRODUCT_ENTRY_FIELDS = [
'scancode',
'department_id',
'department_name',
'brand_name',
'description',
'size',
'department_name',
'vendor_name',
'vendor_item_code',
'unit_cost',
'case_size',
'unit_cost',
'unit_price_reg',
]
@ -213,10 +213,10 @@ class OrderView(MasterView):
'normalized_batch': self.normalize_batch(batch),
'order_items': [self.normalize_row(row)
for row in batch.rows],
'allow_unknown_product': True, # TODO
'default_uom_choices': self.get_default_uom_choices(),
'default_uom': None, # TODO?
'allow_unknown_product': (self.batch_handler.allow_unknown_product()
and self.has_perm('create_unknown_product')),
'pending_product_required_fields': self.get_pending_product_required_fields(),
})
return self.render_to_response('create', context)
@ -567,9 +567,6 @@ class OrderView(MasterView):
'special_order': pending.special_order,
}
# TODO: remove this
data['product_key'] = row.product_scancode
# display text for order qty/uom
if row.order_uom == enum.ORDER_UOM_CASE:
if row.case_size is None:
@ -670,6 +667,55 @@ class OrderView(MasterView):
""" """
return self.request.route_url('order_items.view', uuid=item.uuid)
def configure_get_simple_settings(self):
""" """
settings = [
# products
{'name': 'sideshow.orders.allow_unknown_product',
'type': bool,
'default': True},
]
# required fields for new product entry
for field in self.PENDING_PRODUCT_ENTRY_FIELDS:
setting = {'name': f'sideshow.orders.unknown_product.fields.{field}.required',
'type': bool}
if field == 'description':
setting['default'] = True
settings.append(setting)
return settings
def configure_get_context(self, **kwargs):
""" """
context = super().configure_get_context(**kwargs)
context['pending_product_fields'] = self.PENDING_PRODUCT_ENTRY_FIELDS
return context
@classmethod
def defaults(cls, config):
cls._order_defaults(config)
cls._defaults(config)
@classmethod
def _order_defaults(cls, config):
permission_prefix = cls.get_permission_prefix()
model_title = cls.get_model_title()
model_title_plural = cls.get_model_title_plural()
# fix perm group
config.add_wutta_permission_group(permission_prefix,
model_title_plural,
overwrite=False)
# extra perm required to create order with unknown/pending product
config.add_wutta_permission(permission_prefix,
f'{permission_prefix}.create_unknown_product',
f"Create new {model_title} for unknown/pending product")
class OrderItemView(MasterView):
"""

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8; -*-
import datetime
import decimal
from wuttjamaican.testing import DataTestCase
@ -18,6 +19,16 @@ class TestNewOrderBatchHandler(DataTestCase):
def make_handler(self):
return mod.NewOrderBatchHandler(self.config)
def tets_allow_unknown_product(self):
handler = self.make_handler()
# true by default
self.assertTrue(handler.allow_unknown_product())
# config can disable
config.setdefault('sideshow.orders.allow_unknown_product', 'false')
self.assertFalse(handler.allow_unknown_product())
def test_set_pending_customer(self):
model = self.app.model
handler = self.make_handler()
@ -119,6 +130,10 @@ class TestNewOrderBatchHandler(DataTestCase):
self.assertEqual(product.unit_price_reg, decimal.Decimal('5.99'))
self.assertIs(product.created_by, user)
# error if unknown products not allowed
self.config.setdefault('sideshow.orders.allow_unknown_product', 'false')
self.assertRaises(TypeError, handler.add_pending_product, batch, kw, 1, enum.ORDER_UOM_UNIT)
def test_set_pending_product(self):
model = self.app.model
enum = self.app.enum
@ -210,6 +225,15 @@ class TestNewOrderBatchHandler(DataTestCase):
self.assertEqual(product.unit_price_reg, decimal.Decimal('3.59'))
self.assertIs(product.created_by, user)
# error if unknown products not allowed
self.config.setdefault('sideshow.orders.allow_unknown_product', 'false')
self.assertRaises(TypeError, handler.set_pending_product, row, dict(
scancode='07430500116',
size='16oz',
unit_cost=decimal.Decimal('2.19'),
unit_price_reg=decimal.Decimal('3.59'),
))
def test_refresh_row(self):
model = self.app.model
enum = self.app.enum
@ -310,6 +334,37 @@ class TestNewOrderBatchHandler(DataTestCase):
self.assertEqual(row.case_price_quoted, decimal.Decimal('71.88'))
self.assertEqual(row.total_price, decimal.Decimal('143.76'))
# refreshed from pending product (sale price)
product = model.PendingProduct(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,
status=enum.PendingProductStatus.PENDING)
row = handler.make_row(pending_product=product, order_qty=2, order_uom=enum.ORDER_UOM_CASE,
unit_price_sale=decimal.Decimal('5.19'),
sale_ends=datetime.datetime(2099, 1, 1))
self.assertIsNone(row.status_code)
handler.add_row(batch, row)
self.assertEqual(row.status_code, row.STATUS_OK)
self.assertIsNone(row.product_id)
self.assertIs(row.pending_product, product)
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_sale, decimal.Decimal('5.19'))
self.assertEqual(row.sale_ends, datetime.datetime(2099, 1, 1))
self.assertEqual(row.unit_price_quoted, decimal.Decimal('5.19'))
self.assertEqual(row.case_price_quoted, decimal.Decimal('62.28'))
self.assertEqual(row.total_price, decimal.Decimal('124.56'))
def test_remove_row(self):
model = self.app.model
enum = self.app.enum