diff --git a/modules/purchase_trade/__init__.py b/modules/purchase_trade/__init__.py
index 995673a..8c2f420 100755
--- a/modules/purchase_trade/__init__.py
+++ b/modules/purchase_trade/__init__.py
@@ -210,9 +210,10 @@ def register():
purchase.ContractDocumentType,
purchase.DocTemplate,
purchase.DocTypeTemplate,
- purchase.PurchaseStrategy,
- purchase.PriceComposition,
- purchase.QualityAnalysis,
+ purchase.PurchaseStrategy,
+ purchase.PriceComposition,
+ purchase.PremiumComposition,
+ purchase.QualityAnalysis,
purchase.Assay,
purchase.AssayLine,
purchase.AssayElement,
@@ -283,9 +284,10 @@ def register():
sale.SaleStrategy,
sale.OpenPosition,
sale.Backtoback,
- sale.AnalyticDimensionAssignment,
- sale.PriceComposition,
- module='sale', type_='model')
+ sale.AnalyticDimensionAssignment,
+ sale.PriceComposition,
+ sale.PremiumComposition,
+ module='sale', type_='model')
Pool.register(
lot.LotShipping,
lot.LotMatching,
diff --git a/modules/purchase_trade/pricing.py b/modules/purchase_trade/pricing.py
index dc082a6..93d5e4c 100755
--- a/modules/purchase_trade/pricing.py
+++ b/modules/purchase_trade/pricing.py
@@ -1239,9 +1239,9 @@ class Trigger(ModelSQL,ModelView):
states={
'readonly': Eval('pricing_period') != None,
})
- average = fields.Boolean("Avg")
- last = fields.Boolean("Last")
- application_period = fields.Many2One('pricing.period',"Application period")
+ average = fields.Boolean("Avg")
+ last = fields.Boolean("Latest")
+ application_period = fields.Many2One('pricing.period',"Application period")
from_a = fields.Date("From",
states={
'readonly': Eval('application_period') != None,
@@ -1249,8 +1249,12 @@ class Trigger(ModelSQL,ModelView):
to_a = fields.Date("To",
states={
'readonly': Eval('application_period') != None,
- })
-
+ })
+
+ @classmethod
+ def default_average(cls):
+ return True
+
@fields.depends('pricing_period')
def on_change_with_application_period(self):
if not self.application_period and self.pricing_period:
diff --git a/modules/purchase_trade/purchase.py b/modules/purchase_trade/purchase.py
index df0e6c6..783ce76 100755
--- a/modules/purchase_trade/purchase.py
+++ b/modules/purchase_trade/purchase.py
@@ -733,13 +733,57 @@ class Purchase(metaclass=PoolMeta):
logger.info("EFP_PRICE:%s",line.unit_price)
Line.save([line])
-class PriceComposition(ModelSQL,ModelView):
- "Price Composition"
- __name__ = 'price.composition'
+class PriceComposition(ModelSQL,ModelView):
+ "Price Composition"
+ __name__ = 'price.composition'
line = fields.Many2One('purchase.line',"Purchase line")
- component = fields.Char("Component")
- price = fields.Numeric("Price")
+ component = fields.Char("Component")
+ price = fields.Numeric("Price")
+
+class PremiumComposition(ModelSQL, ModelView):
+ "Premium Composition"
+ __name__ = 'premium.composition'
+
+ line = fields.Many2One('purchase.line', "Purchase line", ondelete='CASCADE')
+ type = fields.Many2One('price.fixtype', "Type")
+ premium = fields.Numeric("Premium/Discount", digits=(16, 6))
+ currency = fields.Many2One('currency.currency', "Curr.")
+ reference = fields.Text("Reference")
+ base_amount = fields.Function(
+ fields.Numeric("Base Amt", digits=(16, 6)), 'get_base_amount')
+
+ def _get_owner_line(self):
+ return getattr(self, 'line', None) or getattr(self, 'sale_line', None)
+
+ def _get_contract(self):
+ line = self._get_owner_line()
+ if line:
+ return getattr(line, 'purchase', None) or getattr(line, 'sale', None)
+
+ def _get_contract_currency(self):
+ contract = self._get_contract()
+ return getattr(contract, 'currency', None)
+
+ def _get_contract_date(self):
+ contract = self._get_contract()
+ return (
+ getattr(contract, 'purchase_date', None)
+ or getattr(contract, 'sale_date', None))
+
+ def get_base_amount(self, name=None):
+ amount = Decimal(str(self.premium or 0))
+ currency = getattr(self, 'currency', None)
+ contract_currency = self._get_contract_currency()
+ if not currency or not contract_currency or currency == contract_currency:
+ return round(amount, 6)
+ Currency = Pool().get('currency.currency')
+ contract_date = self._get_contract_date()
+ if contract_date:
+ with Transaction().set_context(date=contract_date):
+ return round(Currency.compute(
+ currency, amount, contract_currency), 6)
+ return round(Currency.compute(currency, amount, contract_currency), 6)
class AssayImporter:
@@ -1362,7 +1406,9 @@ class Line(metaclass=PoolMeta):
concentration = fields.Numeric("Concentration")
price_components = fields.One2Many('pricing.component','line',"Components")
price_pricing = fields.One2Many('pricing.pricing','line',"Pricing")
- price_summary = fields.One2Many('purchase.pricing.summary','line',"Summary")
+ price_summary = fields.One2Many('purchase.pricing.summary','line',"Summary")
+ premium_decomposition = fields.One2Many(
+ 'premium.composition', 'line', "Premium decomposition")
estimated_date = fields.One2Many('pricing.estimated','line',"Estimated date")
optional = fields.One2Many('optional.scenario','line',"Optionals Scenarios")
lots = fields.One2Many('lot.lot','line',"Lots",readonly=True)
@@ -1412,7 +1458,9 @@ class Line(metaclass=PoolMeta):
'invisible': (~Eval('enable_linked_currency')),
'required': Eval('enable_linked_currency'),
}, depends=['enable_linked_currency'])
- premium = fields.Numeric("Premium/Discount",digits='unit')
+ premium = fields.Function(
+ fields.Numeric("Premium/Discount",digits='unit'),
+ 'get_premium', setter='set_premium')
fee_ = fields.Many2One('fee.fee',"Fee")
pricing_rule = fields.Text("Pricing description")
@@ -1671,13 +1719,32 @@ class Line(metaclass=PoolMeta):
return price
return round(price / factor, 4)
+ def get_premium(self, name=None):
+ return round(sum(
+ Decimal(str(getattr(premium, 'base_amount', None)
+ if getattr(premium, 'base_amount', None) is not None
+ else premium.get_base_amount('base_amount') or 0))
+ for premium in (self.premium_decomposition or [])), 4)
+
+ @classmethod
+ def set_premium(cls, lines, name, value):
+ Premium = Pool().get('premium.composition')
+ value = Decimal(str(value or 0))
+ for line in lines:
+ existing = Premium.search([('line', '=', line.id)])
+ if existing:
+ Premium.delete(existing)
+ if value:
+ Premium.create([{
+ 'line': line.id,
+ 'premium': value,
+ 'currency': (
+ line.purchase.currency.id
+ if line.purchase and line.purchase.currency else None),
+ }])
+
def _get_premium_price(self):
- premium = Decimal(self.premium or 0)
- if not premium:
- return Decimal(0)
- if self.enable_linked_currency and self.linked_currency:
- return self._linked_to_line_price(premium)
- return premium
+ return Decimal(self.premium or 0)
def _get_amount_quantity(self):
if getattr(self, 'finished', False):
@@ -1760,7 +1827,7 @@ class Line(metaclass=PoolMeta):
+ Decimal(lot_premium or 0)),
4)
- @fields.depends('id','unit','quantity','unit_price','price_pricing','price_type','price_components','estimated_date','lots','fees','enable_linked_currency','linked_price','linked_currency','linked_unit')
+ @fields.depends('id','unit','quantity','unit_price','price_pricing','price_type','price_components','estimated_date','lots','fees','enable_linked_currency','linked_price','linked_currency','linked_unit','premium_decomposition')
def on_change_with_unit_price(self, name=None):
Date = Pool().get('ir.date')
logger.info("ONCHANGEUNITPRICE:%s",self.unit_price)
@@ -1781,7 +1848,8 @@ class Line(metaclass=PoolMeta):
'type', 'quantity', 'quantity_theorical', 'finished',
'unit_price', 'unit', 'product', 'lots',
'purchase', '_parent_purchase.currency', '_parent_purchase.wb',
- 'premium', 'enable_linked_currency', 'linked_currency', 'linked_unit')
+ 'premium_decomposition', 'enable_linked_currency',
+ 'linked_currency', 'linked_unit')
def on_change_with_amount(self):
if self.type == 'line' and self.unit_price is not None:
quantity = self._get_amount_quantity()
@@ -1797,7 +1865,8 @@ class Line(metaclass=PoolMeta):
@fields.depends(
'unit', 'product', 'price_type', 'enable_linked_currency',
- 'linked_currency', 'linked_unit', 'linked_price', 'premium',
+ 'linked_currency', 'linked_unit', 'linked_price',
+ 'premium_decomposition',
methods=['on_change_with_unit_price', 'on_change_with_amount'])
def _recompute_trade_price_fields(self):
self.unit_price = self.on_change_with_unit_price()
@@ -1807,6 +1876,10 @@ class Line(metaclass=PoolMeta):
def on_change_premium(self):
self._recompute_trade_price_fields()
+ @fields.depends(methods=['_recompute_trade_price_fields'])
+ def on_change_premium_decomposition(self):
+ self._recompute_trade_price_fields()
+
@fields.depends(methods=['_recompute_trade_price_fields'])
def on_change_price_type(self):
self._recompute_trade_price_fields()
diff --git a/modules/purchase_trade/purchase.xml b/modules/purchase_trade/purchase.xml
index 54943e3..180a4b4 100755
--- a/modules/purchase_trade/purchase.xml
+++ b/modules/purchase_trade/purchase.xml
@@ -156,13 +156,23 @@ this repository contains the full copyright notices and license terms. -->
mtm_tree
-
- price.composition
- tree
- price_composition_tree
-
-
-
+
+ price.composition
+ tree
+ price_composition_tree
+
+
+ premium.composition
+ tree
+ premium_composition_tree
+
+
+ premium.composition
+ form
+ premium_composition_form
+
+
+
quality.analysis
tree
quality_analysis_tree
diff --git a/modules/purchase_trade/sale.py b/modules/purchase_trade/sale.py
index 155e886..bb5b64d 100755
--- a/modules/purchase_trade/sale.py
+++ b/modules/purchase_trade/sale.py
@@ -1249,6 +1249,11 @@ class PriceComposition(metaclass=PoolMeta):
__name__ = 'price.composition'
sale_line = fields.Many2One('sale.line',"Sale line")
+
+class PremiumComposition(metaclass=PoolMeta):
+ __name__ = 'premium.composition'
+
+ sale_line = fields.Many2One('sale.line', "Sale line", ondelete='CASCADE')
class SaleLine(metaclass=PoolMeta):
__name__ = 'sale.line'
@@ -1421,7 +1426,6 @@ class SaleLine(metaclass=PoolMeta):
lots = fields.One2Many('lot.lot','sale_line',"Lots",readonly=True)
fees = fields.One2Many('fee.fee', 'sale_line', 'Fees')
quantity_theorical = fields.Numeric("Th. quantity", digits='unit', readonly=False)
- premium = fields.Numeric("Premium/Discount",digits='unit')
price_type = fields.Selection([
('cash', 'Cash Price'),
('priced', 'Priced'),
@@ -1449,6 +1453,8 @@ class SaleLine(metaclass=PoolMeta):
derivatives = fields.One2Many('derivative.derivative','sale_line',"Derivatives")
price_pricing = fields.One2Many('pricing.pricing','sale_line',"Pricing")
price_summary = fields.One2Many('sale.pricing.summary','sale_line',"Summary")
+ premium_decomposition = fields.One2Many(
+ 'premium.composition', 'sale_line', "Premium decomposition")
estimated_date = fields.One2Many('pricing.estimated','sale_line',"Estimated date")
tol_min = fields.Numeric("Tol - in %",states={
'readonly': (Eval('inherit_tol')),
@@ -1492,7 +1498,9 @@ class SaleLine(metaclass=PoolMeta):
'invisible': (~Eval('enable_linked_currency')),
'required': Eval('enable_linked_currency'),
}, depends=['enable_linked_currency'])
- premium = fields.Numeric("Premium/Discount",digits='unit')
+ premium = fields.Function(
+ fields.Numeric("Premium/Discount",digits='unit'),
+ 'get_premium', setter='set_premium')
fee_ = fields.Many2One('fee.fee',"Fee")
attributes = fields.Dict(
@@ -1760,13 +1768,32 @@ class SaleLine(metaclass=PoolMeta):
return price
return round(price / factor, 4)
+ def get_premium(self, name=None):
+ return round(sum(
+ Decimal(str(getattr(premium, 'base_amount', None)
+ if getattr(premium, 'base_amount', None) is not None
+ else premium.get_base_amount('base_amount') or 0))
+ for premium in (self.premium_decomposition or [])), 4)
+
+ @classmethod
+ def set_premium(cls, lines, name, value):
+ Premium = Pool().get('premium.composition')
+ value = Decimal(str(value or 0))
+ for line in lines:
+ existing = Premium.search([('sale_line', '=', line.id)])
+ if existing:
+ Premium.delete(existing)
+ if value:
+ Premium.create([{
+ 'sale_line': line.id,
+ 'premium': value,
+ 'currency': (
+ line.sale.currency.id
+ if line.sale and line.sale.currency else None),
+ }])
+
def _get_premium_price(self):
- premium = Decimal(self.premium or 0)
- if not premium:
- return Decimal(0)
- if self.enable_linked_currency and self.linked_currency:
- return self._linked_to_line_price(premium)
- return premium
+ return Decimal(self.premium or 0)
def _get_amount_quantity(self):
if getattr(self, 'finished', False):
@@ -1852,7 +1879,7 @@ class SaleLine(metaclass=PoolMeta):
unit_price = self.unit_price
return unit_price
- @fields.depends('id','unit','quantity','unit_price','price_pricing','price_type','price_components','estimated_date','lots','fees','enable_linked_currency','linked_price','linked_currency','linked_unit')
+ @fields.depends('id','unit','quantity','unit_price','price_pricing','price_type','price_components','estimated_date','lots','fees','enable_linked_currency','linked_price','linked_currency','linked_unit','premium_decomposition')
def on_change_with_unit_price(self, name=None):
Date = Pool().get('ir.date')
logger.info("ONCHANGEUNITPRICE:%s",self.unit_price)
@@ -1872,7 +1899,8 @@ class SaleLine(metaclass=PoolMeta):
'type', 'quantity', 'quantity_theorical', 'finished',
'unit_price', 'unit', 'product', 'lots',
'sale', '_parent_sale.currency', '_parent_sale.wb',
- 'premium', 'enable_linked_currency', 'linked_currency', 'linked_unit')
+ 'premium_decomposition', 'enable_linked_currency',
+ 'linked_currency', 'linked_unit')
def on_change_with_amount(self):
if self.type == 'line':
currency = self.sale.currency if self.sale else None
@@ -1885,7 +1913,8 @@ class SaleLine(metaclass=PoolMeta):
@fields.depends(
'unit', 'product', 'price_type', 'enable_linked_currency',
- 'linked_currency', 'linked_unit', 'linked_price', 'premium',
+ 'linked_currency', 'linked_unit', 'linked_price',
+ 'premium_decomposition',
methods=['on_change_with_unit_price', 'on_change_with_amount'])
def _recompute_trade_price_fields(self):
self.unit_price = self.on_change_with_unit_price()
@@ -1895,6 +1924,10 @@ class SaleLine(metaclass=PoolMeta):
def on_change_premium(self):
self._recompute_trade_price_fields()
+ @fields.depends(methods=['_recompute_trade_price_fields'])
+ def on_change_premium_decomposition(self):
+ self._recompute_trade_price_fields()
+
@fields.depends(methods=['_recompute_trade_price_fields'])
def on_change_price_type(self):
self._recompute_trade_price_fields()
diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py
index 6c0e4ea..55c3342 100644
--- a/modules/purchase_trade/tests/test_module.py
+++ b/modules/purchase_trade/tests/test_module.py
@@ -1907,6 +1907,13 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual(trigger.from_a, datetime.date(2026, 4, 1))
self.assertEqual(trigger.to_a, datetime.date(2026, 4, 30))
+ def test_pricing_trigger_average_defaults_to_true_and_last_label_is_latest(self):
+ 'pricing trigger defaults to Avg and labels last as Latest'
+ Trigger = Pool().get('pricing.trigger')
+
+ self.assertTrue(Trigger.default_average())
+ self.assertEqual(Trigger.last.string, 'Latest')
+
def test_pricing_component_matrix_returns_generic_line_price(self):
'matrix pricing can use an unconditional matrix line as a component price'
Component = Pool().get('pricing.component')
@@ -2041,6 +2048,37 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual(Sale.default_tolerance_option(), '')
self.assertEqual(Purchase.default_tolerance_option(), '')
+ def test_purchase_line_premium_sums_decomposition_base_amounts(self):
+ 'purchase line premium is the sum of premium decomposition base amounts'
+ Line = Pool().get('purchase.line')
+ line = Line()
+ line.premium_decomposition = [
+ Mock(base_amount=Decimal('12.50')),
+ Mock(base_amount=Decimal('-2.25')),
+ ]
+
+ self.assertEqual(line.get_premium('premium'), Decimal('10.2500'))
+
+ def test_premium_composition_base_amount_converts_on_contract_date(self):
+ 'premium composition base amount converts to contract currency at spot date'
+ Premium = Pool().get('premium.composition')
+ premium = Premium()
+ source_currency = Mock(id=1)
+ target_currency = Mock(id=2)
+ premium.premium = Decimal('100')
+ premium.currency = source_currency
+ premium.line = Mock(purchase=Mock(
+ currency=target_currency,
+ purchase_date=datetime.date(2026, 5, 1)))
+ currency_model = Mock(
+ compute=Mock(return_value=Decimal('120')))
+
+ with patch('trytond.modules.purchase_trade.purchase.Pool') as PoolMock:
+ PoolMock.return_value.get.return_value = currency_model
+ amount = premium.get_base_amount('base_amount')
+
+ self.assertEqual(amount, Decimal('120.000000'))
+
def test_fee_rule_matches_purchase_line_conditions(self):
'fee rule matches purchase line conditions'
party = Mock(id=1)
diff --git a/modules/purchase_trade/view/premium_composition_form.xml b/modules/purchase_trade/view/premium_composition_form.xml
new file mode 100644
index 0000000..a4d98ec
--- /dev/null
+++ b/modules/purchase_trade/view/premium_composition_form.xml
@@ -0,0 +1,15 @@
+
diff --git a/modules/purchase_trade/view/premium_composition_tree.xml b/modules/purchase_trade/view/premium_composition_tree.xml
new file mode 100644
index 0000000..daab6b2
--- /dev/null
+++ b/modules/purchase_trade/view/premium_composition_tree.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/modules/purchase_trade/view/purchase_line_form.xml b/modules/purchase_trade/view/purchase_line_form.xml
index a51747a..59062de 100755
--- a/modules/purchase_trade/view/purchase_line_form.xml
+++ b/modules/purchase_trade/view/purchase_line_form.xml
@@ -90,9 +90,12 @@ this repository contains the full copyright notices and license terms. -->
-
-
-
+
+
+
+
+
+
diff --git a/modules/purchase_trade/view/sale_line_form.xml b/modules/purchase_trade/view/sale_line_form.xml
index b2678f3..7d2a490 100755
--- a/modules/purchase_trade/view/sale_line_form.xml
+++ b/modules/purchase_trade/view/sale_line_form.xml
@@ -75,9 +75,12 @@ this repository contains the full copyright notices and license terms. -->
-
-
-
+
+
+
+
+
+