diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index ed27cd5..bed2e1d 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -2069,6 +2069,96 @@ class PurchaseTradeTestCase(ModuleTestCase): {'type': 'derivative'}, ]) + def test_purchase_derivative_pnl_uses_direction_and_market_delta(self): + 'purchase derivative pnl is entry versus market with long short direction' + Valuation = Pool().get('valuation.valuation') + today = datetime.date(2026, 7, 16) + currency = Mock(id=1) + company_currency = Mock(id=2) + unit = Mock(id=3) + price_index = Mock( + price_index='ICE', + get_price_per_qt=Mock(return_value=Decimal('100')), + get_price=Mock(return_value=Decimal('80'))) + party = Mock(id=4) + product = Mock(id=5) + purchase = Mock( + id=6, + currency=currency, + company=Mock(currency=company_currency)) + line = Mock(id=7, unit=unit, purchase=purchase) + + def derivative(direction): + return Mock( + price_index=price_index, + price=Decimal('100'), + direction=direction, + quantity=Decimal('10'), + party=party, + product=product) + + line.derivatives = [derivative('long'), derivative('short')] + + with patch('trytond.modules.purchase_trade.valuation.Pool') as PoolMock, patch.object( + Valuation, '_base_amount_values', + side_effect=[ + (Decimal('-200'), Decimal('1'), 2), + (Decimal('200'), Decimal('1'), 2), + ]): + PoolMock.return_value.get.return_value = Mock( + today=Mock(return_value=today)) + values = Valuation.create_pnl_der_from_line(line) + + self.assertEqual(values[0]['amount'], Decimal('-200.00')) + self.assertEqual(values[0]['base_amount'], Decimal('-200')) + self.assertIsNone(values[0]['mtm_price']) + self.assertIsNone(values[0]['mtm']) + self.assertEqual(values[1]['amount'], Decimal('200.00')) + self.assertEqual(values[1]['base_amount'], Decimal('200')) + self.assertIsNone(values[1]['mtm_price']) + self.assertIsNone(values[1]['mtm']) + + def test_sale_derivative_pnl_uses_direction_and_market_delta(self): + 'sale derivative pnl is stored in amount and base amount only' + Valuation = Pool().get('valuation.valuation') + today = datetime.date(2026, 7, 16) + currency = Mock(id=1) + company_currency = Mock(id=2) + unit = Mock(id=3) + price_index = Mock( + price_index='ICE', + get_price_per_qt=Mock(return_value=Decimal('100')), + get_price=Mock(return_value=Decimal('120'))) + derivative = Mock( + price_index=price_index, + price=Decimal('100'), + direction='long', + quantity=Decimal('10'), + party=Mock(id=4), + product=Mock(id=5)) + sale = Mock( + id=6, + currency=currency, + company=Mock(currency=company_currency)) + sale_line = Mock( + id=7, + unit=unit, + sale=sale, + derivatives=[derivative]) + + with patch('trytond.modules.purchase_trade.valuation.Pool') as PoolMock, patch.object( + Valuation, '_base_amount_values', + return_value=(Decimal('200'), Decimal('1'), 2)): + PoolMock.return_value.get.return_value = Mock( + today=Mock(return_value=today)) + values = Valuation.create_pnl_der_from_sale_line(sale_line) + + self.assertEqual(values[0]['price'], Decimal('100.0000')) + self.assertEqual(values[0]['amount'], Decimal('200.00')) + self.assertEqual(values[0]['base_amount'], Decimal('200')) + self.assertIsNone(values[0]['mtm_price']) + self.assertIsNone(values[0]['mtm']) + def test_update_daily_snapshot_generates_all_purchases_and_unmatched_sales(self): 'daily valuation cron snapshots purchases and unmatched sale lines' Valuation = Pool().get('valuation.valuation') diff --git a/modules/purchase_trade/valuation.py b/modules/purchase_trade/valuation.py index d2df730..95bad90 100644 --- a/modules/purchase_trade/valuation.py +++ b/modules/purchase_trade/valuation.py @@ -1596,17 +1596,14 @@ class ValuationBase(ModelSQL): @classmethod def create_pnl_der_from_line(cls, line): - Date = Pool().get('ir.date') der_lines = [] for d in line.derivatives or []: - price = Decimal(d.price_index.get_price_per_qt( - d.price, line.unit, line.purchase.currency - )) - - mtm_price = Decimal(d.price_index.get_price( - Date.today(), line.unit, line.purchase.currency, True - )) + currency = line.purchase.currency + price, _market_price, amount = cls._derivative_pnl_values( + d, line.unit, currency) + base_amount, rate, base_currency = cls._base_amount_values( + amount, currency, line.purchase.company.currency) der_lines.append({ 'purchase': line.purchase.id, @@ -1619,28 +1616,28 @@ class ValuationBase(ModelSQL): 'product': d.product.id, 'state': 'fixed', 'quantity': round(d.quantity, 5), - 'amount': round(price * d.quantity * Decimal(-1), 2), - 'mtm_price': round(mtm_price, 4), - 'mtm': round((price * d.quantity * Decimal(-1)) - (mtm_price * d.quantity * Decimal(-1)), 2), + 'amount': amount, + 'base_amount': base_amount, + 'base_currency': base_currency, + 'rate': rate, + 'mtm_price': None, + 'mtm': None, 'unit': line.unit.id, - 'currency': line.purchase.currency.id, + 'currency': currency.id, }) return der_lines @classmethod def create_pnl_der_from_sale_line(cls, sale_line): - Date = Pool().get('ir.date') der_lines = [] for d in sale_line.derivatives or []: - price = Decimal(d.price_index.get_price_per_qt( - d.price, sale_line.unit, sale_line.sale.currency - )) - - mtm_price = Decimal(d.price_index.get_price( - Date.today(), sale_line.unit, sale_line.sale.currency, True - )) + currency = sale_line.sale.currency + price, _market_price, amount = cls._derivative_pnl_values( + d, sale_line.unit, currency) + base_amount, rate, base_currency = cls._base_amount_values( + amount, currency, sale_line.sale.company.currency) der_lines.append({ 'sale': sale_line.sale.id, @@ -1653,14 +1650,31 @@ class ValuationBase(ModelSQL): 'product': d.product.id, 'state': 'fixed', 'quantity': round(d.quantity, 5), - 'amount': round(price * d.quantity * Decimal(-1), 2), - 'mtm_price': round(mtm_price, 4), - 'mtm': round((price * d.quantity * Decimal(-1)) - (mtm_price * d.quantity * Decimal(-1)), 2), + 'amount': amount, + 'base_amount': base_amount, + 'base_currency': base_currency, + 'rate': rate, + 'mtm_price': None, + 'mtm': None, 'unit': sale_line.unit.id, - 'currency': sale_line.sale.currency.id, + 'currency': currency.id, }) return der_lines + + @classmethod + def _derivative_pnl_values(cls, derivative, unit, currency): + Date = Pool().get('ir.date') + entry_price = Decimal(derivative.price_index.get_price_per_qt( + derivative.price, unit, currency)) + market_price = Decimal(derivative.price_index.get_price( + Date.today(), unit, currency, True)) + quantity = Decimal(derivative.quantity or 0) + direction = Decimal(1) + if getattr(derivative, 'direction', None) == 'short': + direction = Decimal(-1) + amount = round((market_price - entry_price) * quantity * direction, 2) + return round(entry_price, 4), round(market_price, 4), amount @classmethod def generate(cls, line, valuation_type='all'): diff --git a/modules/purchase_trade/view/purchase_line_form.xml b/modules/purchase_trade/view/purchase_line_form.xml index 3088db9..fc8a697 100755 --- a/modules/purchase_trade/view/purchase_line_form.xml +++ b/modules/purchase_trade/view/purchase_line_form.xml @@ -123,7 +123,7 @@ this repository contains the full copyright notices and license terms. --> + col_widths="min-content,1fr,min-content,1fr">