feat: allow basic support for item discounts

This commit is contained in:
Lance Edgar 2025-01-25 23:33:49 -06:00
parent f8f745c243
commit bdf9e46be5
8 changed files with 229 additions and 25 deletions

View file

@ -30,6 +30,7 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
intersphinx_mapping = { intersphinx_mapping = {
'pyramid': ('https://docs.pylonsproject.org/projects/pyramid/en/latest/', None), 'pyramid': ('https://docs.pylonsproject.org/projects/pyramid/en/latest/', None),
'python': ('https://docs.python.org/3/', None),
'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None), 'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None),
'wuttaweb': ('https://rattailproject.org/docs/wuttaweb/', None), 'wuttaweb': ('https://rattailproject.org/docs/wuttaweb/', None),
} }

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Sideshow -- Case/Special Order Tracker # Sideshow -- Case/Special Order Tracker
# Copyright © 2024 Lance Edgar # Copyright © 2024-2025 Lance Edgar
# #
# This file is part of Sideshow. # This file is part of Sideshow.
# #
@ -80,6 +80,32 @@ class NewOrderBatchHandler(BatchHandler):
return self.config.get_bool('sideshow.orders.allow_unknown_products', return self.config.get_bool('sideshow.orders.allow_unknown_products',
default=True) default=True)
def allow_item_discounts(self):
"""
Returns boolean indicating whether per-item discounts are
allowed when creating an order.
"""
return self.config.get_bool('sideshow.orders.allow_item_discounts',
default=False)
def allow_item_discounts_if_on_sale(self):
"""
Returns boolean indicating whether per-item discounts are
allowed even when the item is already on sale.
"""
return self.config.get_bool('sideshow.orders.allow_item_discounts_if_on_sale',
default=False)
def get_default_item_discount(self):
"""
Returns the default item discount percentage, e.g. 15.
:rtype: :class:`~python:decimal.Decimal` or ``None``
"""
discount = self.config.get('sideshow.orders.default_item_discount')
if discount:
return decimal.Decimal(discount)
def autocomplete_customers_external(self, session, term, user=None): def autocomplete_customers_external(self, session, term, user=None):
""" """
Return autocomplete search results for :term:`external Return autocomplete search results for :term:`external
@ -430,7 +456,8 @@ class NewOrderBatchHandler(BatchHandler):
'vendor_item_code': product.vendor_item_code, 'vendor_item_code': product.vendor_item_code,
} }
def add_item(self, batch, product_info, order_qty, order_uom, user=None): def add_item(self, batch, product_info, order_qty, order_uom,
discount_percent=None, user=None):
""" """
Add a new item/row to the batch, for given product and quantity. Add a new item/row to the batch, for given product and quantity.
@ -451,6 +478,10 @@ class NewOrderBatchHandler(BatchHandler):
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_uom` :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_uom`
value for the new row. value for the new row.
:param discount_percent: Sets the
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.discount_percent`
for the row, if allowed.
:param user: :param user:
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
is performing the action. This is used to set is performing the action. This is used to set
@ -518,12 +549,17 @@ class NewOrderBatchHandler(BatchHandler):
row.order_qty = order_qty row.order_qty = order_qty
row.order_uom = order_uom row.order_uom = order_uom
# discount
if self.allow_item_discounts():
row.discount_percent = discount_percent or 0
# add row to batch # add row to batch
self.add_row(batch, row) self.add_row(batch, row)
session.flush() session.flush()
return row return row
def update_item(self, row, product_info, order_qty, order_uom, user=None): def update_item(self, row, product_info, order_qty, order_uom,
discount_percent=None, user=None):
""" """
Update an item/row, per given product and quantity. Update an item/row, per given product and quantity.
@ -544,6 +580,10 @@ class NewOrderBatchHandler(BatchHandler):
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_uom` :attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.order_uom`
value for the row. value for the row.
:param discount_percent: Sets the
:attr:`~sideshow.db.model.batch.neworder.NewOrderBatchRow.discount_percent`
for the row, if allowed.
:param user: :param user:
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who :class:`~wuttjamaican:wuttjamaican.db.model.auth.User` who
is performing the action. This is used to set is performing the action. This is used to set
@ -608,6 +648,10 @@ class NewOrderBatchHandler(BatchHandler):
row.order_qty = order_qty row.order_qty = order_qty
row.order_uom = order_uom row.order_uom = order_uom
# discount
if self.allow_item_discounts():
row.discount_percent = discount_percent or 0
# nb. this may convert float to decimal etc. # nb. this may convert float to decimal etc.
session.flush() session.flush()
session.refresh(row) session.refresh(row)
@ -675,12 +719,19 @@ class NewOrderBatchHandler(BatchHandler):
# update row total price # update row total price
row.total_price = None row.total_price = None
if row.order_uom == enum.ORDER_UOM_CASE: if row.order_uom == enum.ORDER_UOM_CASE:
# TODO: why are we not using case price again?
# if row.case_price_quoted:
# row.total_price = row.case_price_quoted * row.order_qty
if row.unit_price_quoted is not None and row.case_size is not None: if row.unit_price_quoted is not None and row.case_size is not None:
row.total_price = row.unit_price_quoted * row.case_size * row.order_qty row.total_price = row.unit_price_quoted * row.case_size * row.order_qty
else: # ORDER_UOM_UNIT (or similar) else: # ORDER_UOM_UNIT (or similar)
if row.unit_price_quoted is not None: if row.unit_price_quoted is not None:
row.total_price = row.unit_price_quoted * row.order_qty row.total_price = row.unit_price_quoted * row.order_qty
if row.total_price is not None: if row.total_price is not None:
if row.discount_percent and self.allow_item_discounts():
row.total_price = (float(row.total_price)
* (100 - float(row.discount_percent))
/ 100.0)
row.total_price = decimal.Decimal(f'{row.total_price:0.2f}') row.total_price = decimal.Decimal(f'{row.total_price:0.2f}')
# update batch if total price changed # update batch if total price changed
@ -971,7 +1022,7 @@ class NewOrderBatchHandler(BatchHandler):
'unit_price_reg', 'unit_price_reg',
'unit_price_sale', 'unit_price_sale',
'sale_ends', 'sale_ends',
# 'discount_percent', 'discount_percent',
'total_price', 'total_price',
'special_order', 'special_order',
] ]

View file

@ -19,6 +19,36 @@
<h3 class="block is-size-3">Products</h3> <h3 class="block is-size-3">Products</h3>
<div class="block" style="padding-left: 2rem;"> <div class="block" style="padding-left: 2rem;">
<b-field>
<b-checkbox name="sideshow.orders.allow_item_discounts"
v-model="simpleSettings['sideshow.orders.allow_item_discounts']"
native-value="true"
@input="settingsNeedSaved = true">
Allow per-item discounts
</b-checkbox>
</b-field>
<b-field v-show="simpleSettings['sideshow.orders.allow_item_discounts']">
<b-checkbox name="sideshow.orders.allow_item_discounts_if_on_sale"
v-model="simpleSettings['sideshow.orders.allow_item_discounts_if_on_sale']"
native-value="true"
@input="settingsNeedSaved = true">
Allow discount even if item is on sale
</b-checkbox>
</b-field>
<div v-show="simpleSettings['sideshow.orders.allow_item_discounts']"
class="level-left block">
<div class="level-item">Default item discount</div>
<div class="level-item">
<b-input name="sideshow.orders.default_item_discount"
v-model="simpleSettings['sideshow.orders.default_item_discount']"
@input="settingsNeedSaved = true"
style="width: 5rem;" />
</div>
<div class="level-item">%</div>
</div>
<b-field label="Product Source"> <b-field label="Product Source">
<b-select name="sideshow.orders.use_local_products" <b-select name="sideshow.orders.use_local_products"
v-model="simpleSettings['sideshow.orders.use_local_products']" v-model="simpleSettings['sideshow.orders.use_local_products']"

View file

@ -663,11 +663,11 @@
<b-field label="Discount" horizontal> <b-field label="Discount" horizontal>
<div class="level"> <div class="level">
<div class="level-item"> <div class="level-item">
<numeric-input v-model="productDiscountPercent" ## TODO: needs numeric-input component
<b-input v-model="productDiscountPercent"
@input="refreshTotalPrice += 1" @input="refreshTotalPrice += 1"
style="width: 5rem;" style="width: 5rem;"
:disabled="!allowItemDiscount"> :disabled="!allowItemDiscount" />
</numeric-input>
</div> </div>
<div class="level-item"> <div class="level-item">
<span>&nbsp;%</span> <span>&nbsp;%</span>
@ -749,6 +749,13 @@
</span> </span>
</${b}-table-column> </${b}-table-column>
% if allow_item_discounts:
<${b}-table-column label="Discount"
v-slot="props">
{{ props.row.discount_percent }}{{ props.row.discount_percent ? " %" : "" }}
</${b}-table-column>
% endif
<${b}-table-column label="Total" <${b}-table-column label="Total"
v-slot="props"> v-slot="props">
<span :class="props.row.pricing_reflects_sale ? 'has-background-warning' : null"> <span :class="props.row.pricing_reflects_sale ? 'has-background-warning' : null">
@ -890,6 +897,11 @@
productUOM: defaultUOM, productUOM: defaultUOM,
productCaseSize: null, productCaseSize: null,
% if allow_item_discounts:
productDiscountPercent: ${json.dumps(default_item_discount)|n},
allowDiscountsIfOnSale: ${json.dumps(allow_item_discounts_if_on_sale)|n},
% endif
pendingProduct: {}, pendingProduct: {},
pendingProductRequiredFields: ${json.dumps(pending_product_required_fields)|n}, pendingProductRequiredFields: ${json.dumps(pending_product_required_fields)|n},
## TODO ## TODO
@ -1011,6 +1023,19 @@
return text return text
}, },
% if allow_item_discounts:
allowItemDiscount() {
if (!this.allowDiscountsIfOnSale) {
if (this.productSalePriceDisplay) {
return false
}
}
return true
},
% endif
pendingProductGrossMargin() { pendingProductGrossMargin() {
let cost = this.pendingProduct.unit_cost let cost = this.pendingProduct.unit_cost
let price = this.pendingProduct.unit_price_reg let price = this.pendingProduct.unit_price_reg
@ -1324,6 +1349,10 @@
this.productSalePriceDisplay = null this.productSalePriceDisplay = null
this.productSaleEndsDisplay = null this.productSaleEndsDisplay = null
this.productUnitChoices = this.defaultUnitChoices this.productUnitChoices = this.defaultUnitChoices
% if allow_item_discounts:
this.productDiscountPercent = ${json.dumps(default_item_discount)|n}
% endif
}, },
productChanged(productID) { productChanged(productID) {
@ -1360,6 +1389,10 @@
this.productSalePriceDisplay = data.unit_price_sale_display this.productSalePriceDisplay = data.unit_price_sale_display
this.productSaleEndsDisplay = data.sale_ends_display this.productSaleEndsDisplay = data.sale_ends_display
% if allow_item_discounts:
this.productDiscountPercent = this.allowItemDiscount ? data.default_item_discount : null
% endif
// this.setProductUnitChoices(data.uom_choices) // this.setProductUnitChoices(data.uom_choices)
% if request.use_oruga: % if request.use_oruga:
@ -1434,6 +1467,10 @@
this.productUnitChoices = this.defaultUnitChoices this.productUnitChoices = this.defaultUnitChoices
this.productUOM = this.defaultUOM this.productUOM = this.defaultUOM
% if allow_item_discounts:
this.productDiscountPercent = ${json.dumps(default_item_discount)|n}
% endif
% if request.use_oruga: % if request.use_oruga:
this.itemDialogTab = 'product' this.itemDialogTab = 'product'
% else: % else:
@ -1488,6 +1525,10 @@
this.productUnitChoices = row.order_uom_choices this.productUnitChoices = row.order_uom_choices
this.productUOM = row.order_uom this.productUOM = row.order_uom
% if allow_item_discounts:
this.productDiscountPercent = row.discount_percent
% endif
// nb. hack to force refresh for vue3 // nb. hack to force refresh for vue3
this.refreshProductDescription += 1 this.refreshProductDescription += 1
this.refreshTotalPrice += 1 this.refreshTotalPrice += 1
@ -1538,6 +1579,10 @@
params.product_info = this.pendingProduct params.product_info = this.pendingProduct
} }
% if allow_item_discounts:
params.discount_percent = this.productDiscountPercent
% endif
if (this.editItemRow) { if (this.editItemRow) {
params.action = 'update_item' params.action = 'update_item'
params.uuid = this.editItemRow.uuid params.uuid = this.editItemRow.uuid

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Sideshow -- Case/Special Order Tracker # Sideshow -- Case/Special Order Tracker
# Copyright © 2024 Lance Edgar # Copyright © 2024-2025 Lance Edgar
# #
# This file is part of Sideshow. # This file is part of Sideshow.
# #
@ -121,6 +121,7 @@ class NewOrderBatchView(BatchMasterView):
'case_price_quoted', 'case_price_quoted',
'order_qty', 'order_qty',
'order_uom', 'order_uom',
'discount_percent',
'total_price', 'total_price',
'status_code', 'status_code',
] ]
@ -167,6 +168,10 @@ class NewOrderBatchView(BatchMasterView):
g.set_label('case_price_quoted', "Case Price", column_only=True) g.set_label('case_price_quoted', "Case Price", column_only=True)
g.set_renderer('case_price_quoted', 'currency') g.set_renderer('case_price_quoted', 'currency')
# discount_percent
g.set_renderer('discount_percent', 'percent')
g.set_label('discount_percent', "Disc. %", column_only=True)
# total_price # total_price
g.set_renderer('total_price', 'currency') g.set_renderer('total_price', 'currency')

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Sideshow -- Case/Special Order Tracker # Sideshow -- Case/Special Order Tracker
# Copyright © 2024 Lance Edgar # Copyright © 2024-2025 Lance Edgar
# #
# This file is part of Sideshow. # This file is part of Sideshow.
# #
@ -135,6 +135,7 @@ class OrderView(MasterView):
'special_order', 'special_order',
'order_qty', 'order_qty',
'order_uom', 'order_uom',
'discount_percent',
'total_price', 'total_price',
'status_code', 'status_code',
] ]
@ -284,10 +285,19 @@ 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_item_discounts': self.batch_handler.allow_item_discounts(),
'allow_unknown_products': (self.batch_handler.allow_unknown_products() '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(),
}) })
if context['allow_item_discounts']:
context['allow_item_discounts_if_on_sale'] = self.batch_handler\
.allow_item_discounts_if_on_sale()
# nb. render quantity so that '10.0' => '10'
context['default_item_discount'] = self.app.render_quantity(
self.batch_handler.get_default_item_discount())
return self.render_to_response('create', context) return self.render_to_response('create', context)
def get_current_batch(self): def get_current_batch(self):
@ -564,11 +574,15 @@ class OrderView(MasterView):
if 'case_price_quoted' in data and 'case_price_quoted_display' not in data: 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']) data['case_price_quoted_display'] = self.app.render_currency(data['case_price_quoted'])
if 'default_item_discount' not in data:
data['default_item_discount'] = self.batch_handler.get_default_item_discount()
decimal_fields = [ decimal_fields = [
'case_size', 'case_size',
'unit_price_reg', 'unit_price_reg',
'unit_price_quoted', 'unit_price_quoted',
'case_price_quoted', 'case_price_quoted',
'default_item_discount',
] ]
for field in decimal_fields: for field in decimal_fields:
@ -589,8 +603,11 @@ class OrderView(MasterView):
* :meth:`update_item()` * :meth:`update_item()`
* :meth:`delete_item()` * :meth:`delete_item()`
""" """
kw = {'user': self.request.user}
if 'discount_percent' in data and self.batch_handler.allow_item_discounts():
kw['discount_percent'] = data['discount_percent']
row = self.batch_handler.add_item(batch, data['product_info'], row = self.batch_handler.add_item(batch, data['product_info'],
data['order_qty'], data['order_uom']) data['order_qty'], data['order_uom'], **kw)
return {'batch': self.normalize_batch(batch), return {'batch': self.normalize_batch(batch),
'row': self.normalize_row(row)} 'row': self.normalize_row(row)}
@ -619,8 +636,11 @@ 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"}
kw = {'user': self.request.user}
if 'discount_percent' in data and self.batch_handler.allow_item_discounts():
kw['discount_percent'] = data['discount_percent']
self.batch_handler.update_item(row, data['product_info'], self.batch_handler.update_item(row, data['product_info'],
data['order_qty'], data['order_uom']) data['order_qty'], data['order_uom'], **kw)
return {'batch': self.normalize_batch(batch), return {'batch': self.normalize_batch(batch),
'row': self.normalize_row(row)} 'row': self.normalize_row(row)}
@ -715,6 +735,7 @@ class OrderView(MasterView):
'order_qty': float(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(),
'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': 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), 'unit_price_quoted_display': self.app.render_currency(row.unit_price_quoted),
'case_price_quoted': float(row.case_price_quoted) if row.case_price_quoted is not None else None, 'case_price_quoted': float(row.case_price_quoted) if row.case_price_quoted is not None else None,
@ -857,6 +878,10 @@ class OrderView(MasterView):
# order_uom # order_uom
#g.set_renderer('order_uom', self.grid_render_enum, enum=enum.OrderUOM) #g.set_renderer('order_uom', self.grid_render_enum, enum=enum.OrderUOM)
# discount_percent
g.set_renderer('discount_percent', 'percent')
g.set_label('discount_percent', "Disc. %", column_only=True)
# total_price # total_price
g.set_renderer('total_price', g.render_currency) g.set_renderer('total_price', g.render_currency)
@ -895,6 +920,12 @@ class OrderView(MasterView):
'default': 'true'}, 'default': 'true'},
# products # products
{'name': 'sideshow.orders.allow_item_discounts',
'type': bool},
{'name': 'sideshow.orders.allow_item_discounts_if_on_sale',
'type': bool},
{'name': 'sideshow.orders.default_item_discount',
'type': float},
{'name': 'sideshow.orders.use_local_products', {'name': 'sideshow.orders.use_local_products',
# nb. this is really a bool but we present as string in config UI # nb. this is really a bool but we present as string in config UI
#'type': bool, #'type': bool,

View file

@ -20,36 +20,66 @@ class TestNewOrderBatchHandler(DataTestCase):
def make_handler(self): def make_handler(self):
return mod.NewOrderBatchHandler(self.config) return mod.NewOrderBatchHandler(self.config)
def tets_use_local_customers(self): def test_use_local_customers(self):
handler = self.make_handler() handler = self.make_handler()
# true by default # true by default
self.assertTrue(handler.use_local_customers()) self.assertTrue(handler.use_local_customers())
# config can disable # config can disable
config.setdefault('sideshow.orders.use_local_customers', 'false') self.config.setdefault('sideshow.orders.use_local_customers', 'false')
self.assertFalse(handler.use_local_customers()) self.assertFalse(handler.use_local_customers())
def tets_use_local_products(self): def test_use_local_products(self):
handler = self.make_handler() handler = self.make_handler()
# true by default # true by default
self.assertTrue(handler.use_local_products()) self.assertTrue(handler.use_local_products())
# config can disable # config can disable
config.setdefault('sideshow.orders.use_local_products', 'false') self.config.setdefault('sideshow.orders.use_local_products', 'false')
self.assertFalse(handler.use_local_products()) self.assertFalse(handler.use_local_products())
def tets_allow_unknown_products(self): def test_allow_unknown_products(self):
handler = self.make_handler() handler = self.make_handler()
# true by default # true by default
self.assertTrue(handler.allow_unknown_products()) self.assertTrue(handler.allow_unknown_products())
# config can disable # config can disable
config.setdefault('sideshow.orders.allow_unknown_products', 'false') self.config.setdefault('sideshow.orders.allow_unknown_products', 'false')
self.assertFalse(handler.allow_unknown_products()) self.assertFalse(handler.allow_unknown_products())
def test_allow_item_discounts(self):
handler = self.make_handler()
# false by default
self.assertFalse(handler.allow_item_discounts())
# config can enable
self.config.setdefault('sideshow.orders.allow_item_discounts', 'true')
self.assertTrue(handler.allow_item_discounts())
def test_allow_item_discounts_if_on_sale(self):
handler = self.make_handler()
# false by default
self.assertFalse(handler.allow_item_discounts_if_on_sale())
# config can enable
self.config.setdefault('sideshow.orders.allow_item_discounts_if_on_sale', 'true')
self.assertTrue(handler.allow_item_discounts_if_on_sale())
def test_get_default_item_discount(self):
handler = self.make_handler()
# null by default
self.assertIsNone(handler.get_default_item_discount())
# config can define
self.config.setdefault('sideshow.orders.default_item_discount', '15')
self.assertEqual(handler.get_default_item_discount(), decimal.Decimal('15.00'))
def test_autocomplete_customers_external(self): def test_autocomplete_customers_external(self):
handler = self.make_handler() handler = self.make_handler()
self.assertRaises(NotImplementedError, handler.autocomplete_customers_external, self.assertRaises(NotImplementedError, handler.autocomplete_customers_external,
@ -327,7 +357,7 @@ class TestNewOrderBatchHandler(DataTestCase):
self.config.setdefault('sideshow.orders.allow_unknown_products', 'false') self.config.setdefault('sideshow.orders.allow_unknown_products', 'false')
self.assertRaises(TypeError, handler.add_item, batch, kw, 1, enum.ORDER_UOM_UNIT) self.assertRaises(TypeError, handler.add_item, batch, kw, 1, enum.ORDER_UOM_UNIT)
# local product # local product w/ discount
local = model.LocalProduct(scancode='07430500002', local = model.LocalProduct(scancode='07430500002',
description='Vinegar', description='Vinegar',
size='2oz', size='2oz',
@ -335,7 +365,9 @@ class TestNewOrderBatchHandler(DataTestCase):
case_size=12) case_size=12)
self.session.add(local) self.session.add(local)
self.session.flush() self.session.flush()
row = handler.add_item(batch, local.uuid.hex, 1, enum.ORDER_UOM_CASE) with patch.object(handler, 'allow_item_discounts', return_value=True):
row = handler.add_item(batch, local.uuid.hex, 1, enum.ORDER_UOM_CASE,
discount_percent=15)
self.session.flush() self.session.flush()
self.session.refresh(row) self.session.refresh(row)
self.session.refresh(local) self.session.refresh(local)
@ -359,7 +391,8 @@ class TestNewOrderBatchHandler(DataTestCase):
self.assertEqual(row.unit_price_reg, decimal.Decimal('2.99')) self.assertEqual(row.unit_price_reg, decimal.Decimal('2.99'))
self.assertEqual(row.unit_price_quoted, 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.case_price_quoted, decimal.Decimal('35.88'))
self.assertEqual(row.total_price, decimal.Decimal('35.88')) self.assertEqual(row.discount_percent, decimal.Decimal('15.00'))
self.assertEqual(row.total_price, decimal.Decimal('30.50'))
# local product, not found # local product, not found
mock_uuid = self.app.make_true_uuid() mock_uuid = self.app.make_true_uuid()
@ -511,8 +544,10 @@ class TestNewOrderBatchHandler(DataTestCase):
self.config.setdefault('sideshow.orders.allow_unknown_products', 'false') self.config.setdefault('sideshow.orders.allow_unknown_products', 'false')
self.assertRaises(TypeError, handler.update_item, row, kw, 1, enum.ORDER_UOM_UNIT) self.assertRaises(TypeError, handler.update_item, row, kw, 1, enum.ORDER_UOM_UNIT)
# update w/ local product # update w/ local product and discount percent
handler.update_item(row, local.uuid.hex, 1, enum.ORDER_UOM_CASE) with patch.object(handler, 'allow_item_discounts', return_value=True):
handler.update_item(row, local.uuid.hex, 1, enum.ORDER_UOM_CASE,
discount_percent=15)
self.assertIsNone(row.product_id) self.assertIsNone(row.product_id)
# nb. pending remains intact here # nb. pending remains intact here
self.assertIsNotNone(row.pending_product) self.assertIsNotNone(row.pending_product)
@ -536,7 +571,8 @@ class TestNewOrderBatchHandler(DataTestCase):
self.assertEqual(row.case_price_quoted, decimal.Decimal('47.88')) self.assertEqual(row.case_price_quoted, decimal.Decimal('47.88'))
self.assertEqual(row.order_qty, 1) self.assertEqual(row.order_qty, 1)
self.assertEqual(row.order_uom, enum.ORDER_UOM_CASE) self.assertEqual(row.order_uom, enum.ORDER_UOM_CASE)
self.assertEqual(row.total_price, decimal.Decimal('47.88')) self.assertEqual(row.discount_percent, decimal.Decimal('15.00'))
self.assertEqual(row.total_price, decimal.Decimal('40.70'))
# update w/ local, not found # update w/ local, not found
mock_uuid = self.app.make_true_uuid() mock_uuid = self.app.make_true_uuid()

View file

@ -52,6 +52,7 @@ class TestOrderView(WebTestCase):
self.pyramid_config.include('sideshow.web.views') self.pyramid_config.include('sideshow.web.views')
self.config.setdefault('wutta.batch.neworder.handler.spec', self.config.setdefault('wutta.batch.neworder.handler.spec',
'sideshow.batch.neworder:NewOrderBatchHandler') 'sideshow.batch.neworder:NewOrderBatchHandler')
self.config.setdefault('sideshow.orders.allow_item_discounts', 'true')
model = self.app.model model = self.app.model
enum = self.app.enum enum = self.app.enum
view = self.make_view() view = self.make_view()
@ -577,6 +578,7 @@ class TestOrderView(WebTestCase):
def test_add_item(self): def test_add_item(self):
model = self.app.model model = self.app.model
enum = self.app.enum enum = self.app.enum
self.config.setdefault('sideshow.orders.allow_item_discounts', 'true')
handler = NewOrderBatchHandler(self.config) handler = NewOrderBatchHandler(self.config)
view = self.make_view() view = self.make_view()
@ -594,6 +596,7 @@ class TestOrderView(WebTestCase):
}, },
'order_qty': 1, 'order_qty': 1,
'order_uom': enum.ORDER_UOM_UNIT, 'order_uom': enum.ORDER_UOM_UNIT,
'discount_percent': 10,
} }
with patch.object(view, 'batch_handler', create=True, new=handler): with patch.object(view, 'batch_handler', create=True, new=handler):
@ -620,6 +623,7 @@ class TestOrderView(WebTestCase):
def test_update_item(self): def test_update_item(self):
model = self.app.model model = self.app.model
enum = self.app.enum enum = self.app.enum
self.config.setdefault('sideshow.orders.allow_item_discounts', 'true')
handler = NewOrderBatchHandler(self.config) handler = NewOrderBatchHandler(self.config)
view = self.make_view() view = self.make_view()
@ -638,6 +642,7 @@ class TestOrderView(WebTestCase):
}, },
'order_qty': 1, 'order_qty': 1,
'order_uom': enum.ORDER_UOM_CASE, 'order_uom': enum.ORDER_UOM_CASE,
'discount_percent': 15,
} }
with patch.object(view, 'batch_handler', create=True, new=handler): with patch.object(view, 'batch_handler', create=True, new=handler):