From 9ad0217a69c7db6ce1b2c845a6b738a06fb5b14b Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Wed, 20 May 2026 14:17:36 +0200 Subject: [PATCH] Price composition --- modules/purchase_trade/invoice.py | 15 +++++++ modules/purchase_trade/sale.py | 31 +++++++++++++++ modules/purchase_trade/tests/test_module.py | 44 +++++++++++++++++++++ 3 files changed, 90 insertions(+) diff --git a/modules/purchase_trade/invoice.py b/modules/purchase_trade/invoice.py index 068f7f0..3f59d4b 100644 --- a/modules/purchase_trade/invoice.py +++ b/modules/purchase_trade/invoice.py @@ -916,6 +916,9 @@ class Invoice(metaclass=PoolMeta): @property def report_rate_lines(self): + price_composition_lines = self._get_report_price_composition_lines() + if price_composition_lines: + return price_composition_lines details = [] for line in self._get_report_invoice_lines(): currency = getattr(line, 'report_rate_currency_upper', '') or '' @@ -940,6 +943,18 @@ class Invoice(metaclass=PoolMeta): details.append(detail) return '\n'.join(details) + def _get_report_price_composition_lines(self): + sale = self._get_report_sale() + if sale and getattr(sale, 'report_price_composition_lines', None): + return sale.report_price_composition_lines + details = [] + for line in self._get_report_invoice_lines(): + origin = getattr(line, 'origin', None) + sale = getattr(origin, 'sale', None) + if sale and getattr(sale, 'report_price_composition_lines', None): + return sale.report_price_composition_lines + return '\n'.join(details) + @property def report_positive_rate_lines(self): sale = self._get_report_sale() diff --git a/modules/purchase_trade/sale.py b/modules/purchase_trade/sale.py index 9e66a4e..96fc817 100755 --- a/modules/purchase_trade/sale.py +++ b/modules/purchase_trade/sale.py @@ -846,6 +846,37 @@ class Sale(metaclass=PoolMeta): return '\n'.join(self._format_report_price_line(line) for line in lines) return '' + @property + def report_price_composition_lines(self): + lines = [] + for line in self._get_report_lines(): + currency = self._get_report_display_currency(line) + unit = self._get_report_display_unit(line) + currency_name = ( + currency.rec_name.upper() + if currency and currency.rec_name else '') + unit_name = ( + unit.rec_name.upper() + if unit and unit.rec_name else '') + for composition in getattr(line, 'price_composition', []) or []: + component = getattr(composition, 'component', '') or '' + price = getattr(composition, 'price', None) + if price in (None, ''): + continue + price_text = self._format_report_number( + price, strip_trailing_zeros=False) + detail = ' '.join( + part for part in [ + component, + currency_name, + price_text, + 'PER' if unit_name else '', + unit_name, + ] if part) + if detail: + lines.append(detail) + return '\n'.join(lines) + @property def report_trade_blocks(self): lines = self._get_report_lines() diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 5279059..98dc60c 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -2589,6 +2589,50 @@ class PurchaseTradeTestCase(ModuleTestCase): 'USC 70.2500 PER POUND (SEVENTY USC AND TWENTY FIVE CENTS) ON ICE Cotton #2 MAY 2026', ]) + def test_sale_report_price_composition_lines_uses_composition(self): + 'sale report exposes price composition lines for invoice_melya' + Sale = Pool().get('sale.sale') + + line = Mock() + line.type = 'line' + line.price_type = 'priced' + line.linked_currency = None + line.linked_unit = None + line.unit = Mock(rec_name='MT') + line.unit_price = Decimal('2525') + line.price_composition = [ + Mock(component='FOB', price=Decimal('2500')), + Mock(component='FREIGHT', price=Decimal('25')), + ] + + sale = Sale() + sale.currency = Mock(rec_name='EUR') + sale.lines = [line] + + self.assertEqual( + sale.report_price_composition_lines.splitlines(), + [ + 'FOB EUR 2500.0000 PER MT', + 'FREIGHT EUR 25.0000 PER MT', + ]) + + def test_invoice_report_rate_lines_prefers_sale_price_composition(self): + 'invoice_melya rate block uses sale price composition when available' + Invoice = Pool().get('account.invoice') + sale = Mock( + report_price_composition_lines=( + 'FOB EUR 2500.0000 PER MT\n' + 'FREIGHT EUR 25.0000 PER MT')) + invoice = Invoice() + invoice.sales = [sale] + + self.assertEqual( + invoice.report_rate_lines.splitlines(), + [ + 'FOB EUR 2500.0000 PER MT', + 'FREIGHT EUR 25.0000 PER MT', + ]) + def test_sale_report_converts_mixed_units_for_total_and_words(self): 'sale report totals prefer the virtual lot unit as common unit' Sale = Pool().get('sale.sale')