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:
parent
e677cd5d8c
commit
ebd22fe6ee
|
@ -44,6 +44,14 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
"""
|
"""
|
||||||
model_class = NewOrderBatch
|
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):
|
def set_pending_customer(self, batch, data):
|
||||||
"""
|
"""
|
||||||
Set (add or update) pending customer info for the batch.
|
Set (add or update) pending customer info for the batch.
|
||||||
|
@ -122,6 +130,9 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
:class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
|
:class:`~sideshow.db.model.batch.neworder.NewOrderBatchRow`
|
||||||
which was added to the batch.
|
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)
|
||||||
|
@ -167,6 +178,9 @@ class NewOrderBatchHandler(BatchHandler):
|
||||||
:param data: Dict of field data for the
|
:param data: Dict of field data for the
|
||||||
:class:`~sideshow.db.model.products.PendingProduct` record.
|
: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(row)
|
||||||
|
|
41
src/sideshow/web/templates/orders/configure.mako
Normal file
41
src/sideshow/web/templates/orders/configure.mako
Normal 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 "pending" 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>
|
|
@ -402,8 +402,8 @@
|
||||||
<div v-if="productID">
|
<div v-if="productID">
|
||||||
|
|
||||||
<b-field grouped>
|
<b-field grouped>
|
||||||
<b-field :label="productKeyLabel">
|
<b-field label="Scancode">
|
||||||
<span>{{ productKey }}</span>
|
<span>{{ productScancode }}</span>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field label="Unit Size">
|
<b-field label="Unit Size">
|
||||||
|
@ -493,12 +493,12 @@
|
||||||
|
|
||||||
<div style="display: flex; gap: 1rem;">
|
<div style="display: flex; gap: 1rem;">
|
||||||
|
|
||||||
<b-field :label="productKeyLabel"
|
<b-field label="Scancode"
|
||||||
% if 'key' in pending_product_required_fields:
|
% if 'scancode' in pending_product_required_fields:
|
||||||
:type="pendingProduct[productKeyField] ? null : 'is-danger'"
|
:type="pendingProduct.scancode ? null : 'is-danger'"
|
||||||
% endif
|
% endif
|
||||||
style="width: 100%;">
|
style="width: 100%;">
|
||||||
<b-input v-model="pendingProduct[productKeyField]" />
|
<b-input v-model="pendingProduct.scancode" />
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field label="Department"
|
<b-field label="Department"
|
||||||
|
@ -725,9 +725,9 @@
|
||||||
:data="items"
|
:data="items"
|
||||||
:row-class="(row, i) => row.product_id ? null : 'has-text-success'">
|
:row-class="(row, i) => row.product_id ? null : 'has-text-success'">
|
||||||
|
|
||||||
<${b}-table-column :label="productKeyLabel"
|
<${b}-table-column label="Scancode"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
{{ props.row.product_key }}
|
{{ props.row.product_scancode }}
|
||||||
</${b}-table-column>
|
</${b}-table-column>
|
||||||
|
|
||||||
<${b}-table-column label="Brand"
|
<${b}-table-column label="Brand"
|
||||||
|
@ -876,16 +876,11 @@
|
||||||
itemDialogTabIndex: 0,
|
itemDialogTabIndex: 0,
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
## TODO
|
productIsKnown: true,
|
||||||
productIsKnown: false,
|
|
||||||
|
|
||||||
selectedProduct: null,
|
selectedProduct: null,
|
||||||
productID: null,
|
productID: null,
|
||||||
productDisplay: null,
|
productDisplay: null,
|
||||||
## TODO
|
productScancode: null,
|
||||||
productKey: null,
|
|
||||||
productKeyField: 'scancode',
|
|
||||||
productKeyLabel: "Scancode",
|
|
||||||
productSize: null,
|
productSize: null,
|
||||||
productCaseQuantity: null,
|
productCaseQuantity: null,
|
||||||
productUnitPrice: null,
|
productUnitPrice: null,
|
||||||
|
@ -1341,13 +1336,11 @@
|
||||||
showAddItemDialog() {
|
showAddItemDialog() {
|
||||||
this.customerPanelOpen = false
|
this.customerPanelOpen = false
|
||||||
this.editingItem = null
|
this.editingItem = null
|
||||||
// TODO
|
this.productIsKnown = true
|
||||||
// this.productIsKnown = true
|
|
||||||
this.productIsKnown = false
|
|
||||||
## this.selectedProduct = null
|
## this.selectedProduct = null
|
||||||
this.productID = null
|
this.productID = null
|
||||||
this.productDisplay = null
|
this.productDisplay = null
|
||||||
## this.productKey = null
|
this.productScancode = null
|
||||||
this.productSize = null
|
this.productSize = null
|
||||||
this.productCaseQuantity = null
|
this.productCaseQuantity = null
|
||||||
this.productUnitPrice = null
|
this.productUnitPrice = null
|
||||||
|
@ -1402,7 +1395,7 @@
|
||||||
this.pendingProduct = pending
|
this.pendingProduct = pending
|
||||||
|
|
||||||
this.productDisplay = row.product_full_description
|
this.productDisplay = row.product_full_description
|
||||||
this.productKey = row.product_key
|
this.productScancode = row.product_scancode
|
||||||
this.productSize = row.product_size
|
this.productSize = row.product_size
|
||||||
this.productCaseQuantity = row.case_quantity
|
this.productCaseQuantity = row.case_quantity
|
||||||
this.productURL = row.product_url
|
this.productURL = row.product_url
|
||||||
|
|
|
@ -58,6 +58,7 @@ class OrderView(MasterView):
|
||||||
"""
|
"""
|
||||||
model_class = Order
|
model_class = Order
|
||||||
editable = False
|
editable = False
|
||||||
|
configurable = True
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
'order_id': "Order ID",
|
'order_id': "Order ID",
|
||||||
|
@ -122,15 +123,14 @@ class OrderView(MasterView):
|
||||||
|
|
||||||
PENDING_PRODUCT_ENTRY_FIELDS = [
|
PENDING_PRODUCT_ENTRY_FIELDS = [
|
||||||
'scancode',
|
'scancode',
|
||||||
'department_id',
|
|
||||||
'department_name',
|
|
||||||
'brand_name',
|
'brand_name',
|
||||||
'description',
|
'description',
|
||||||
'size',
|
'size',
|
||||||
|
'department_name',
|
||||||
'vendor_name',
|
'vendor_name',
|
||||||
'vendor_item_code',
|
'vendor_item_code',
|
||||||
'unit_cost',
|
|
||||||
'case_size',
|
'case_size',
|
||||||
|
'unit_cost',
|
||||||
'unit_price_reg',
|
'unit_price_reg',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -213,10 +213,10 @@ class OrderView(MasterView):
|
||||||
'normalized_batch': self.normalize_batch(batch),
|
'normalized_batch': self.normalize_batch(batch),
|
||||||
'order_items': [self.normalize_row(row)
|
'order_items': [self.normalize_row(row)
|
||||||
for row in batch.rows],
|
for row in batch.rows],
|
||||||
|
|
||||||
'allow_unknown_product': True, # TODO
|
|
||||||
'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()
|
||||||
|
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(),
|
||||||
})
|
})
|
||||||
return self.render_to_response('create', context)
|
return self.render_to_response('create', context)
|
||||||
|
@ -567,9 +567,6 @@ class OrderView(MasterView):
|
||||||
'special_order': pending.special_order,
|
'special_order': pending.special_order,
|
||||||
}
|
}
|
||||||
|
|
||||||
# TODO: remove this
|
|
||||||
data['product_key'] = row.product_scancode
|
|
||||||
|
|
||||||
# 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:
|
||||||
if row.case_size is None:
|
if row.case_size is None:
|
||||||
|
@ -670,6 +667,55 @@ class OrderView(MasterView):
|
||||||
""" """
|
""" """
|
||||||
return self.request.route_url('order_items.view', uuid=item.uuid)
|
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):
|
class OrderItemView(MasterView):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# -*- coding: utf-8; -*-
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
|
|
||||||
from wuttjamaican.testing import DataTestCase
|
from wuttjamaican.testing import DataTestCase
|
||||||
|
@ -18,6 +19,16 @@ 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):
|
||||||
|
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):
|
def test_set_pending_customer(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
|
@ -119,6 +130,10 @@ class TestNewOrderBatchHandler(DataTestCase):
|
||||||
self.assertEqual(product.unit_price_reg, decimal.Decimal('5.99'))
|
self.assertEqual(product.unit_price_reg, decimal.Decimal('5.99'))
|
||||||
self.assertIs(product.created_by, user)
|
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):
|
def test_set_pending_product(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
|
@ -210,6 +225,15 @@ class TestNewOrderBatchHandler(DataTestCase):
|
||||||
self.assertEqual(product.unit_price_reg, decimal.Decimal('3.59'))
|
self.assertEqual(product.unit_price_reg, decimal.Decimal('3.59'))
|
||||||
self.assertIs(product.created_by, user)
|
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):
|
def test_refresh_row(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
|
@ -310,6 +334,37 @@ class TestNewOrderBatchHandler(DataTestCase):
|
||||||
self.assertEqual(row.case_price_quoted, decimal.Decimal('71.88'))
|
self.assertEqual(row.case_price_quoted, decimal.Decimal('71.88'))
|
||||||
self.assertEqual(row.total_price, decimal.Decimal('143.76'))
|
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):
|
def test_remove_row(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
enum = self.app.enum
|
enum = self.app.enum
|
||||||
|
|
Loading…
Reference in a new issue