Price composition

This commit is contained in:
2026-05-20 14:29:31 +02:00
parent 9ad0217a69
commit ea3cb65d7d
4 changed files with 61 additions and 36 deletions

View File

@@ -1646,7 +1646,7 @@
<table:table-column table:style-name="Tableau3.B"/> <table:table-column table:style-name="Tableau3.B"/>
<table:table-row> <table:table-row>
<table:table-cell table:style-name="Tableau3.A1" office:value-type="string"> <table:table-cell table:style-name="Tableau3.A1" office:value-type="string">
<text:p text:style-name="P24"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T8"><text:placeholder text:placeholder-type="text">&lt;if test=&quot;invoice.report_rate_lines&quot;&gt;</text:placeholder></text:span></text:span></text:p> <text:p text:style-name="P24"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T8"><text:placeholder text:placeholder-type="text">&lt;if test=&quot;invoice.report_rate_rows&quot;&gt;</text:placeholder></text:span></text:span></text:p>
</table:table-cell> </table:table-cell>
<table:table-cell table:style-name="Tableau3.A1" office:value-type="string"> <table:table-cell table:style-name="Tableau3.A1" office:value-type="string">
<text:p text:style-name="P9"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T8"/></text:span></text:p> <text:p text:style-name="P9"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T8"/></text:span></text:p>
@@ -1654,7 +1654,7 @@
</table:table-row> </table:table-row>
<table:table-row> <table:table-row>
<table:table-cell table:style-name="Tableau3.A1" office:value-type="string"> <table:table-cell table:style-name="Tableau3.A1" office:value-type="string">
<text:p text:style-name="P11"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T8"><text:placeholder text:placeholder-type="text">&lt;for each=&quot;pc in invoice.report_rate_lines.splitlines()&quot;&gt;</text:placeholder></text:span></text:span><text:soft-page-break/></text:p> <text:p text:style-name="P11"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T8"><text:placeholder text:placeholder-type="text">&lt;for each=&quot;pc in invoice.report_rate_rows&quot;&gt;</text:placeholder></text:span></text:span><text:soft-page-break/></text:p>
</table:table-cell> </table:table-cell>
<table:table-cell table:style-name="Tableau3.A1" office:value-type="string"> <table:table-cell table:style-name="Tableau3.A1" office:value-type="string">
<text:p text:style-name="P9"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T9"/></text:span></text:p> <text:p text:style-name="P9"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T9"/></text:span></text:p>
@@ -1662,10 +1662,10 @@
</table:table-row> </table:table-row>
<table:table-row> <table:table-row>
<table:table-cell table:style-name="Tableau3.A1" office:value-type="string"> <table:table-cell table:style-name="Tableau3.A1" office:value-type="string">
<text:p text:style-name="P12"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T8"><text:placeholder text:placeholder-type="text">&lt;pc&gt;</text:placeholder></text:span></text:span><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T8"><text:s/>Value in </text:span></text:span><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T8"><text:placeholder text:placeholder-type="text">&lt;invoice.currency.name&gt;</text:placeholder></text:span></text:span></text:p> <text:p text:style-name="P12"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T8"><text:placeholder text:placeholder-type="text">&lt;pc[&apos;label&apos;]&gt;</text:placeholder></text:span></text:span></text:p>
</table:table-cell> </table:table-cell>
<table:table-cell table:style-name="Tableau3.A1" office:value-type="string"> <table:table-cell table:style-name="Tableau3.A1" office:value-type="string">
<text:p text:style-name="P13"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T8"><text:placeholder text:placeholder-type="text">&lt;&apos;&apos;&gt;</text:placeholder></text:span></text:span></text:p> <text:p text:style-name="P13"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T8"><text:placeholder text:placeholder-type="text">&lt;format_currency(pc[&apos;amount&apos;], invoice.party.lang, invoice.currency) if pc[&apos;amount&apos;] is not None else &apos;&apos;&gt;</text:placeholder></text:span></text:span></text:p>
</table:table-cell> </table:table-cell>
</table:table-row> </table:table-row>
<table:table-row> <table:table-row>

View File

@@ -916,9 +916,22 @@ class Invoice(metaclass=PoolMeta):
@property @property
def report_rate_lines(self): def report_rate_lines(self):
price_composition_lines = self._get_report_price_composition_lines() rows = self.report_rate_rows
if price_composition_lines: if rows:
return price_composition_lines return '\n'.join(row['label'] for row in rows)
return ''
@property
def report_rate_rows(self):
price_composition_rows = self._get_report_price_composition_rows()
if price_composition_rows:
return price_composition_rows
return [{
'label': line,
'amount': None,
} for line in self._get_report_rate_line_details()]
def _get_report_rate_line_details(self):
details = [] details = []
for line in self._get_report_invoice_lines(): for line in self._get_report_invoice_lines():
currency = getattr(line, 'report_rate_currency_upper', '') or '' currency = getattr(line, 'report_rate_currency_upper', '') or ''
@@ -941,19 +954,18 @@ class Invoice(metaclass=PoolMeta):
] if part) ] if part)
if detail: if detail:
details.append(detail) details.append(detail)
return '\n'.join(details) return details
def _get_report_price_composition_lines(self): def _get_report_price_composition_rows(self):
sale = self._get_report_sale() sale = self._get_report_sale()
if sale and getattr(sale, 'report_price_composition_lines', None): if sale and getattr(sale, 'report_price_composition_rows', None):
return sale.report_price_composition_lines return sale.report_price_composition_rows
details = []
for line in self._get_report_invoice_lines(): for line in self._get_report_invoice_lines():
origin = getattr(line, 'origin', None) origin = getattr(line, 'origin', None)
sale = getattr(origin, 'sale', None) sale = getattr(origin, 'sale', None)
if sale and getattr(sale, 'report_price_composition_lines', None): if sale and getattr(sale, 'report_price_composition_rows', None):
return sale.report_price_composition_lines return sale.report_price_composition_rows
return '\n'.join(details) return []
@property @property
def report_positive_rate_lines(self): def report_positive_rate_lines(self):

View File

@@ -848,34 +848,34 @@ class Sale(metaclass=PoolMeta):
@property @property
def report_price_composition_lines(self): def report_price_composition_lines(self):
return '\n'.join(
row['label'] for row in self.report_price_composition_rows)
@property
def report_price_composition_rows(self):
lines = [] lines = []
for line in self._get_report_lines(): for line in self._get_report_lines():
currency = self._get_report_display_currency(line) currency = self._get_report_display_currency(line)
unit = self._get_report_display_unit(line)
currency_name = ( currency_name = (
currency.rec_name.upper() currency.rec_name.upper()
if currency and currency.rec_name else '') 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 []: for composition in getattr(line, 'price_composition', []) or []:
component = getattr(composition, 'component', '') or '' component = getattr(composition, 'component', '') or ''
price = getattr(composition, 'price', None) price = getattr(composition, 'price', None)
if price in (None, ''): if price in (None, ''):
continue continue
price_text = self._format_report_number( label = ' '.join(
price, strip_trailing_zeros=False)
detail = ' '.join(
part for part in [ part for part in [
component, component,
'value in',
currency_name, currency_name,
price_text,
'PER' if unit_name else '',
unit_name,
] if part) ] if part)
if detail: if label:
lines.append(detail) lines.append({
return '\n'.join(lines) 'label': label,
'amount': Decimal(str(price or 0)),
})
return lines
@property @property
def report_trade_blocks(self): def report_trade_blocks(self):

View File

@@ -2612,25 +2612,38 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual( self.assertEqual(
sale.report_price_composition_lines.splitlines(), sale.report_price_composition_lines.splitlines(),
[ [
'FOB EUR 2500.0000 PER MT', 'FOB value in EUR',
'FREIGHT EUR 25.0000 PER MT', 'FREIGHT value in EUR',
])
self.assertEqual(
sale.report_price_composition_rows,
[
{'label': 'FOB value in EUR', 'amount': Decimal('2500')},
{'label': 'FREIGHT value in EUR', 'amount': Decimal('25')},
]) ])
def test_invoice_report_rate_lines_prefers_sale_price_composition(self): def test_invoice_report_rate_lines_prefers_sale_price_composition(self):
'invoice_melya rate block uses sale price composition when available' 'invoice_melya rate block uses sale price composition when available'
Invoice = Pool().get('account.invoice') Invoice = Pool().get('account.invoice')
sale = Mock( sale = Mock(
report_price_composition_lines=( report_price_composition_rows=[
'FOB EUR 2500.0000 PER MT\n' {'label': 'FOB value in EUR', 'amount': Decimal('2500')},
'FREIGHT EUR 25.0000 PER MT')) {'label': 'FREIGHT value in EUR', 'amount': Decimal('25')},
])
invoice = Invoice() invoice = Invoice()
invoice.sales = [sale] invoice.sales = [sale]
self.assertEqual( self.assertEqual(
invoice.report_rate_lines.splitlines(), invoice.report_rate_lines.splitlines(),
[ [
'FOB EUR 2500.0000 PER MT', 'FOB value in EUR',
'FREIGHT EUR 25.0000 PER MT', 'FREIGHT value in EUR',
])
self.assertEqual(
invoice.report_rate_rows,
[
{'label': 'FOB value in EUR', 'amount': Decimal('2500')},
{'label': 'FREIGHT value in EUR', 'amount': Decimal('25')},
]) ])
def test_sale_report_converts_mixed_units_for_total_and_words(self): def test_sale_report_converts_mixed_units_for_total_and_words(self):