Add manual pricing

This commit is contained in:
2026-04-09 22:24:49 +02:00
parent 229b6037fb
commit 0def187750
4 changed files with 160 additions and 37 deletions

View File

@@ -325,20 +325,20 @@ class Component(ModelSQL, ModelView):
super(Component, cls).delete(components) super(Component, cls).delete(components)
class Pricing(ModelSQL,ModelView): class Pricing(ModelSQL,ModelView):
"Pricing" "Pricing"
__name__ = 'pricing.pricing' __name__ = 'pricing.pricing'
pricing_date = fields.Date("Date") pricing_date = fields.Date("Date")
price_component = fields.Many2One('pricing.component', "Component")#, domain=[('id', 'in', Eval('line.price_components'))], ondelete='CASCADE') price_component = fields.Many2One('pricing.component', "Component")#, domain=[('id', 'in', Eval('line.price_components'))], ondelete='CASCADE')
quantity = fields.Numeric("Qt",digits='unit') quantity = fields.Numeric("Qt",digits='unit')
settl_price = fields.Numeric("Settl. price",digits='unit') settl_price = fields.Numeric("Settl. price",digits='unit')
fixed_qt = fields.Numeric("Fixed qt",digits='unit',readonly=True) fixed_qt = fields.Numeric("Fixed qt",digits='unit')
fixed_qt_price = fields.Numeric("Fixed qt price",digits='unit',readonly=True) fixed_qt_price = fields.Numeric("Fixed qt price",digits='unit')
unfixed_qt = fields.Numeric("Unfixed qt",digits='unit',readonly=True) unfixed_qt = fields.Numeric("Unfixed qt",digits='unit')
unfixed_qt_price = fields.Numeric("Unfixed qt price",digits='unit',readonly=True) unfixed_qt_price = fields.Numeric("Unfixed qt price",digits='unit')
eod_price = fields.Numeric("EOD price",digits='unit',readonly=True) eod_price = fields.Numeric("EOD price",digits='unit',readonly=True)
last = fields.Boolean("Last") last = fields.Boolean("Last")
@classmethod @classmethod
def default_fixed_qt(cls): def default_fixed_qt(cls):
@@ -364,13 +364,86 @@ class Pricing(ModelSQL,ModelView):
def default_settl_price(cls): def default_settl_price(cls):
return Decimal(0) return Decimal(0)
@classmethod @classmethod
def default_eod_price(cls): def default_eod_price(cls):
return Decimal(0) return Decimal(0)
def get_fixed_price(self): @staticmethod
price = Decimal(0) def _weighted_average_price(fixed_qt, fixed_price, unfixed_qt, unfixed_price):
Pricing = Pool().get('pricing.pricing') 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')]) pricings = Pricing.search(['price_component','=',self.price_component.id],order=[('pricing_date', 'ASC')])
if pricings: if pricings:
cumul_qt = Decimal(0) cumul_qt = Decimal(0)

View File

@@ -155,14 +155,17 @@ class Pricing(metaclass=PoolMeta):
line = fields.Many2One('purchase.line',"Lines") line = fields.Many2One('purchase.line',"Lines")
unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit_purchase') unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit_purchase')
def get_unit_purchase(self,name): def get_unit_purchase(self,name):
if self.line: if self.line:
return self.line.unit return self.line.unit
def get_eod_price_purchase(self): def get_eod_price_purchase(self):
if self.line: return self._weighted_average_price(
return round((self.fixed_qt * self.fixed_qt_price + self.unfixed_qt * self.unfixed_qt_price)/Decimal(self.line.quantity_theorical),4) self.fixed_qt,
return Decimal(0) self.fixed_qt_price,
self.unfixed_qt,
self.unfixed_qt_price,
)
class Summary(ModelSQL,ModelView): class Summary(ModelSQL,ModelView):
"Pricing summary" "Pricing summary"

View File

@@ -119,14 +119,17 @@ class Pricing(metaclass=PoolMeta):
sale_line = fields.Many2One('sale.line',"Lines") sale_line = fields.Many2One('sale.line',"Lines")
unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit_sale') unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit_sale')
def get_unit_sale(self,name): def get_unit_sale(self,name):
if self.sale_line: if self.sale_line:
return self.sale_line.unit return self.sale_line.unit
def get_eod_price_sale(self): def get_eod_price_sale(self):
if self.sale_line: return self._weighted_average_price(
return round((self.fixed_qt * self.fixed_qt_price + self.unfixed_qt * self.unfixed_qt_price)/Decimal(self.sale_line.quantity),4) self.fixed_qt,
return Decimal(0) self.fixed_qt_price,
self.unfixed_qt,
self.unfixed_qt_price,
)
class Summary(ModelSQL,ModelView): class Summary(ModelSQL,ModelView):
"Pricing summary" "Pricing summary"

View File

@@ -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[0].args[0][0].unfixed_qt, Decimal('10'))
self.assertEqual(pricing_model.save.call_args_list[1].args[0][0].unfixed_qt, Decimal('12')) 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): 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 and purchase trader/operator fields are filtered by TRADER/OPERATOR categories'
Sale = Pool().get('sale.sale') Sale = Pool().get('sale.sale')