diff --git a/modules/purchase_trade/pricing.py b/modules/purchase_trade/pricing.py index 6191627..b426033 100755 --- a/modules/purchase_trade/pricing.py +++ b/modules/purchase_trade/pricing.py @@ -325,20 +325,20 @@ class Component(ModelSQL, ModelView): super(Component, cls).delete(components) -class Pricing(ModelSQL,ModelView): - "Pricing" - __name__ = 'pricing.pricing' - - pricing_date = fields.Date("Date") - price_component = fields.Many2One('pricing.component', "Component")#, domain=[('id', 'in', Eval('line.price_components'))], ondelete='CASCADE') - quantity = fields.Numeric("Qt",digits='unit') - settl_price = fields.Numeric("Settl. price",digits='unit') - fixed_qt = fields.Numeric("Fixed qt",digits='unit',readonly=True) - fixed_qt_price = fields.Numeric("Fixed qt price",digits='unit',readonly=True) - unfixed_qt = fields.Numeric("Unfixed qt",digits='unit',readonly=True) - unfixed_qt_price = fields.Numeric("Unfixed qt price",digits='unit',readonly=True) - eod_price = fields.Numeric("EOD price",digits='unit',readonly=True) - last = fields.Boolean("Last") +class Pricing(ModelSQL,ModelView): + "Pricing" + __name__ = 'pricing.pricing' + + pricing_date = fields.Date("Date") + price_component = fields.Many2One('pricing.component', "Component")#, domain=[('id', 'in', Eval('line.price_components'))], ondelete='CASCADE') + quantity = fields.Numeric("Qt",digits='unit') + settl_price = fields.Numeric("Settl. price",digits='unit') + fixed_qt = fields.Numeric("Fixed qt",digits='unit') + fixed_qt_price = fields.Numeric("Fixed qt price",digits='unit') + unfixed_qt = fields.Numeric("Unfixed qt",digits='unit') + unfixed_qt_price = fields.Numeric("Unfixed qt price",digits='unit') + eod_price = fields.Numeric("EOD price",digits='unit',readonly=True) + last = fields.Boolean("Last") @classmethod def default_fixed_qt(cls): @@ -364,13 +364,86 @@ class Pricing(ModelSQL,ModelView): def default_settl_price(cls): return Decimal(0) - @classmethod - def default_eod_price(cls): - return Decimal(0) - - def get_fixed_price(self): - price = Decimal(0) - Pricing = Pool().get('pricing.pricing') + @classmethod + def default_eod_price(cls): + return Decimal(0) + + @staticmethod + def _weighted_average_price(fixed_qt, fixed_price, unfixed_qt, unfixed_price): + fixed_qt = Decimal(str(fixed_qt or 0)) + fixed_price = Decimal(str(fixed_price or 0)) + unfixed_qt = Decimal(str(unfixed_qt or 0)) + unfixed_price = Decimal(str(unfixed_price or 0)) + total_qty = fixed_qt + unfixed_qt + if total_qty == 0: + return Decimal(0) + return round( + ((fixed_qt * fixed_price) + (unfixed_qt * unfixed_price)) / total_qty, + 4, + ) + + def compute_eod_price(self): + if getattr(self, 'sale_line', None) and hasattr(self, 'get_eod_price_sale'): + return self.get_eod_price_sale() + if getattr(self, 'line', None) and hasattr(self, 'get_eod_price_purchase'): + return self.get_eod_price_purchase() + return self._weighted_average_price( + self.fixed_qt, + self.fixed_qt_price, + self.unfixed_qt, + self.unfixed_qt_price, + ) + + @fields.depends('fixed_qt', 'fixed_qt_price', 'unfixed_qt', 'unfixed_qt_price') + def on_change_fixed_qt(self): + self.eod_price = self.compute_eod_price() + + @fields.depends('fixed_qt', 'fixed_qt_price', 'unfixed_qt', 'unfixed_qt_price') + def on_change_fixed_qt_price(self): + self.eod_price = self.compute_eod_price() + + @fields.depends('fixed_qt', 'fixed_qt_price', 'unfixed_qt', 'unfixed_qt_price') + def on_change_unfixed_qt(self): + self.eod_price = self.compute_eod_price() + + @fields.depends('fixed_qt', 'fixed_qt_price', 'unfixed_qt', 'unfixed_qt_price') + def on_change_unfixed_qt_price(self): + self.eod_price = self.compute_eod_price() + + @classmethod + def create(cls, vlist): + records = super(Pricing, cls).create(vlist) + cls._sync_eod_price(records) + return records + + @classmethod + def write(cls, *args): + super(Pricing, cls).write(*args) + if Transaction().context.get('skip_pricing_eod_sync'): + return + records = [] + actions = iter(args) + for record_set, values in zip(actions, actions): + if values: + records.extend(record_set) + cls._sync_eod_price(records) + + @classmethod + def _sync_eod_price(cls, records): + if not records: + return + with Transaction().set_context(skip_pricing_eod_sync=True): + for record in records: + eod_price = record.compute_eod_price() + if Decimal(str(record.eod_price or 0)) == Decimal(str(eod_price or 0)): + continue + super(Pricing, cls).write([record], { + 'eod_price': eod_price, + }) + + def get_fixed_price(self): + price = Decimal(0) + Pricing = Pool().get('pricing.pricing') pricings = Pricing.search(['price_component','=',self.price_component.id],order=[('pricing_date', 'ASC')]) if pricings: cumul_qt = Decimal(0) diff --git a/modules/purchase_trade/purchase.py b/modules/purchase_trade/purchase.py index 4c7dac4..4cf041b 100755 --- a/modules/purchase_trade/purchase.py +++ b/modules/purchase_trade/purchase.py @@ -155,14 +155,17 @@ class Pricing(metaclass=PoolMeta): line = fields.Many2One('purchase.line',"Lines") unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit_purchase') - def get_unit_purchase(self,name): - if self.line: - return self.line.unit - - def get_eod_price_purchase(self): - if self.line: - return round((self.fixed_qt * self.fixed_qt_price + self.unfixed_qt * self.unfixed_qt_price)/Decimal(self.line.quantity_theorical),4) - return Decimal(0) + def get_unit_purchase(self,name): + if self.line: + return self.line.unit + + def get_eod_price_purchase(self): + return self._weighted_average_price( + self.fixed_qt, + self.fixed_qt_price, + self.unfixed_qt, + self.unfixed_qt_price, + ) class Summary(ModelSQL,ModelView): "Pricing summary" diff --git a/modules/purchase_trade/sale.py b/modules/purchase_trade/sale.py index 085c898..76736db 100755 --- a/modules/purchase_trade/sale.py +++ b/modules/purchase_trade/sale.py @@ -119,14 +119,17 @@ class Pricing(metaclass=PoolMeta): sale_line = fields.Many2One('sale.line',"Lines") unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit_sale') - def get_unit_sale(self,name): - if self.sale_line: - return self.sale_line.unit - - def get_eod_price_sale(self): - if self.sale_line: - return round((self.fixed_qt * self.fixed_qt_price + self.unfixed_qt * self.unfixed_qt_price)/Decimal(self.sale_line.quantity),4) - return Decimal(0) + def get_unit_sale(self,name): + if self.sale_line: + return self.sale_line.unit + + def get_eod_price_sale(self): + return self._weighted_average_price( + self.fixed_qt, + self.fixed_qt_price, + self.unfixed_qt, + self.unfixed_qt_price, + ) class Summary(ModelSQL,ModelView): "Pricing summary" diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 5c7f2c6..f2dcae1 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -246,6 +246,50 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertEqual(pricing_model.save.call_args_list[0].args[0][0].unfixed_qt, Decimal('10')) self.assertEqual(pricing_model.save.call_args_list[1].args[0][0].unfixed_qt, Decimal('12')) + def test_pricing_manual_fields_are_editable_except_eod(self): + 'manual pricing rows can edit fixed and unfixed values while eod stays computed' + Pricing = Pool().get('pricing.pricing') + + self.assertFalse(Pricing.fixed_qt.readonly) + self.assertFalse(Pricing.fixed_qt_price.readonly) + self.assertFalse(Pricing.unfixed_qt.readonly) + self.assertFalse(Pricing.unfixed_qt_price.readonly) + self.assertTrue(Pricing.eod_price.readonly) + + def test_pricing_eod_uses_weighted_average_for_manual_rows(self): + 'manual pricing eod uses the weighted average of fixed and unfixed legs' + Pricing = Pool().get('pricing.pricing') + + pricing = Pricing() + pricing.fixed_qt = Decimal('4') + pricing.fixed_qt_price = Decimal('100') + pricing.unfixed_qt = Decimal('6') + pricing.unfixed_qt_price = Decimal('110') + + self.assertEqual(pricing.compute_eod_price(), Decimal('106.0000')) + + def test_sale_and_purchase_eod_use_same_weighted_formula(self): + 'auto sale/purchase eod helpers use the same weighted average formula' + Pricing = Pool().get('pricing.pricing') + + sale_pricing = Pricing() + sale_pricing.sale_line = Mock(quantity=Decimal('999')) + sale_pricing.fixed_qt = Decimal('4') + sale_pricing.fixed_qt_price = Decimal('100') + sale_pricing.unfixed_qt = Decimal('6') + sale_pricing.unfixed_qt_price = Decimal('110') + + purchase_pricing = Pricing() + purchase_pricing.line = Mock(quantity_theorical=Decimal('999')) + purchase_pricing.fixed_qt = Decimal('4') + purchase_pricing.fixed_qt_price = Decimal('100') + purchase_pricing.unfixed_qt = Decimal('6') + purchase_pricing.unfixed_qt_price = Decimal('110') + + self.assertEqual(sale_pricing.get_eod_price_sale(), Decimal('106.0000')) + self.assertEqual( + purchase_pricing.get_eod_price_purchase(), Decimal('106.0000')) + def test_sale_and_purchase_trader_operator_domains_use_explicit_categories(self): 'sale and purchase trader/operator fields are filtered by TRADER/OPERATOR categories' Sale = Pool().get('sale.sale')