diff --git a/modules/purchase_trade/docs/template-properties.md b/modules/purchase_trade/docs/template-properties.md
index abb272f..2c7331c 100644
--- a/modules/purchase_trade/docs/template-properties.md
+++ b/modules/purchase_trade/docs/template-properties.md
@@ -243,13 +243,17 @@ Source code: `modules/purchase_trade/sale.py`
- `report_gross`
- `report_net`
- `report_qt`
+- `report_total_quantity`
+- `report_quantity_unit_upper`
- `report_nb_bale`
- `report_deal`
- `report_packing`
- `report_price`
+- `report_price_lines`
- `report_delivery`
- `report_payment_date`
- `report_shipment`
+- `report_shipment_periods`
Usage typique:
- base de travail pour les templates de type `sale_ict.fodt`
diff --git a/modules/purchase_trade/sale.py b/modules/purchase_trade/sale.py
index eb5b115..19f32c0 100755
--- a/modules/purchase_trade/sale.py
+++ b/modules/purchase_trade/sale.py
@@ -319,47 +319,107 @@ class Sale(metaclass=PoolMeta):
def default_tol_min(cls):
return 0
- @classmethod
- def default_tol_max(cls):
- return 0
-
- @property
- def report_terms(self):
- if self.lines:
- return self.lines[0].note
- else:
- return ''
-
- @property
- def report_gross(self):
- if self.lines:
- return sum([l.get_current_gross_quantity() for l in self.lines[0].lots if l.lot_type == 'physic'])
- else:
- return ''
-
- @property
- def report_net(self):
- if self.lines:
- return sum([l.get_current_quantity() for l in self.lines[0].lots if l.lot_type == 'physic'])
- else:
- return ''
-
- @property
- def report_qt(self):
- if self.lines:
- return quantity_to_words(self.lines[0].quantity)
- else:
- return ''
-
- @property
+ @classmethod
+ def default_tol_max(cls):
+ return 0
+
+ def _get_report_lines(self):
+ return [line for line in self.lines if getattr(line, 'type', None) == 'line']
+
+ def _get_report_first_line(self):
+ lines = self._get_report_lines()
+ if lines:
+ return lines[0]
+
+ @staticmethod
+ def _format_report_number(value, digits='0.0000', keep_trailing_decimal=False,
+ strip_trailing_zeros=True):
+ value = Decimal(str(value or 0)).quantize(Decimal(digits))
+ text = format(value, 'f')
+ if strip_trailing_zeros:
+ text = text.rstrip('0').rstrip('.')
+ if keep_trailing_decimal and '.' not in text:
+ text += '.0'
+ return text or '0'
+
+ def _format_report_price_words(self, line):
+ if getattr(line, 'linked_price', None):
+ return amount_to_currency_words(line.linked_price, 'USC', 'USC')
+ return amount_to_currency_words(line.unit_price)
+
+ def _format_report_price_line(self, line):
+ currency = getattr(line, 'linked_currency', None) or self.currency
+ unit = getattr(line, 'linked_unit', None) or getattr(line, 'unit', None)
+ pricing_text = getattr(line, 'get_pricing_text', '') or ''
+ parts = [
+ (currency.rec_name.upper() if currency and currency.rec_name else '').strip(),
+ self._format_report_number(
+ line.linked_price if getattr(line, 'linked_price', None)
+ else line.unit_price,
+ strip_trailing_zeros=False),
+ 'PER',
+ (unit.rec_name.upper() if unit and unit.rec_name else '').strip(),
+ f"({self._format_report_price_words(line)})",
+ ]
+ if pricing_text:
+ parts.append(pricing_text)
+ return ' '.join(part for part in parts if part)
+
+ @property
+ def report_terms(self):
+ line = self._get_report_first_line()
+ if line:
+ return line.note
+ return ''
+
+ @property
+ def report_gross(self):
+ line = self._get_report_first_line()
+ if line:
+ return sum([l.get_current_gross_quantity() for l in line.lots if l.lot_type == 'physic'])
+ return ''
+
+ @property
+ def report_net(self):
+ line = self._get_report_first_line()
+ if line:
+ return sum([l.get_current_quantity() for l in line.lots if l.lot_type == 'physic'])
+ return ''
+
+ @property
+ def report_total_quantity(self):
+ lines = self._get_report_lines()
+ if lines:
+ total = sum(Decimal(str(line.quantity or 0)) for line in lines)
+ return self._format_report_number(total, keep_trailing_decimal=True)
+ return '0.0'
+
+ @property
+ def report_quantity_unit_upper(self):
+ line = self._get_report_first_line()
+ if line and line.unit:
+ return line.unit.rec_name.upper()
+ return ''
+
+ @property
+ def report_qt(self):
+ lines = self._get_report_lines()
+ if lines:
+ total = sum(Decimal(str(line.quantity or 0)) for line in lines)
+ return quantity_to_words(total)
+ return ''
+
+ @property
def report_nb_bale(self):
- text_bale = 'NB BALES: '
nb_bale = 0
- if self.lines:
- for line in self.lines:
+ lines = self._get_report_lines()
+ if lines:
+ for line in lines:
if line.lots:
nb_bale += sum([l.lot_qt for l in line.lots if l.lot_type == 'physic'])
- return text_bale + str(int(nb_bale))
+ if nb_bale:
+ return 'NB BALES: ' + str(int(nb_bale))
+ return ''
@property
def report_crop_name(self):
@@ -375,29 +435,37 @@ class Sale(metaclass=PoolMeta):
''
@property
- def report_packing(self):
- nb_packing = 0
- unit = ''
- if self.lines:
- for line in self.lines:
- if line.lots:
- nb_packing += sum([l.lot_qt for l in line.lots if l.lot_type == 'physic'])
- if len(line.lots)>1:
- unit = line.lots[1].lot_unit.name
- return str(int(nb_packing)) + unit
+ def report_packing(self):
+ nb_packing = 0
+ unit = ''
+ lines = self._get_report_lines()
+ if lines:
+ for line in lines:
+ if line.lots:
+ nb_packing += sum([l.lot_qt for l in line.lots if l.lot_type == 'physic'])
+ if len(line.lots)>1:
+ unit = line.lots[1].lot_unit.name
+ return str(int(nb_packing)) + unit
@property
- def report_price(self):
- if self.lines:
- if self.lines[0].price_type == 'priced':
- if self.lines[0].linked_price:
- return amount_to_currency_words(self.lines[0].linked_price,'USC','USC')
+ def report_price(self):
+ line = self._get_report_first_line()
+ if line:
+ if line.price_type == 'priced':
+ if line.linked_price:
+ return amount_to_currency_words(line.linked_price,'USC','USC')
else:
- return amount_to_currency_words(self.lines[0].unit_price)
- elif self.lines[0].price_type == 'basis':
- return amount_to_currency_words(self.lines[0].unit_price) + ' ' + self.lines[0].get_pricing_text
- else:
- return ''
+ return amount_to_currency_words(line.unit_price)
+ elif line.price_type == 'basis':
+ return amount_to_currency_words(line.unit_price) + ' ' + line.get_pricing_text
+ return ''
+
+ @property
+ def report_price_lines(self):
+ lines = self._get_report_lines()
+ if lines:
+ return '\n'.join(self._format_report_price_line(line) for line in lines)
+ return ''
@property
def report_delivery(self):
@@ -413,20 +481,33 @@ class Sale(metaclass=PoolMeta):
@property
def report_delivery_period_description(self):
- if self.lines and self.lines[0].del_period:
- return self.lines[0].del_period.description or ''
+ line = self._get_report_first_line()
+ if line and line.del_period:
+ return line.del_period.description or ''
+ return ''
+
+ @property
+ def report_shipment_periods(self):
+ periods = []
+ for line in self._get_report_lines():
+ period = line.del_period.description if line.del_period else ''
+ if period and period not in periods:
+ periods.append(period)
+ if periods:
+ return '\n'.join(periods)
return ''
@property
- def report_payment_date(self):
- if self.lines:
- if self.lc_date:
- return format_date_en(self.lc_date)
- Date = Pool().get('ir.date')
- payment_date = self.lines[0].sale.payment_term.lines[0].get_date(Date.today(),self.lines[0])
- if payment_date:
- payment_date = format_date_en(payment_date)
- return payment_date
+ def report_payment_date(self):
+ line = self._get_report_first_line()
+ if line:
+ if self.lc_date:
+ return format_date_en(self.lc_date)
+ Date = Pool().get('ir.date')
+ payment_date = line.sale.payment_term.lines[0].get_date(Date.today(), line)
+ if payment_date:
+ payment_date = format_date_en(payment_date)
+ return payment_date
@property
def report_shipment(self):
diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py
index f3ffece..3981557 100644
--- a/modules/purchase_trade/tests/test_module.py
+++ b/modules/purchase_trade/tests/test_module.py
@@ -154,5 +154,46 @@ class PurchaseTradeTestCase(ModuleTestCase):
sale.crop.name = 'Main Crop'
self.assertEqual(sale.report_crop_name, 'Main Crop')
+ def test_sale_report_multi_line_helpers_aggregate_all_lines(self):
+ 'sale report helpers aggregate quantity, price lines and shipment periods'
+ Sale = Pool().get('sale.sale')
+
+ def make_line(quantity, period, linked_price):
+ line = Mock()
+ line.type = 'line'
+ line.quantity = quantity
+ line.note = ''
+ line.price_type = 'priced'
+ line.unit_price = Decimal('0')
+ line.linked_price = Decimal(linked_price)
+ line.linked_currency = Mock(rec_name='USC')
+ line.linked_unit = Mock(rec_name='POUND')
+ line.unit = Mock(rec_name='MT')
+ line.del_period = Mock(description=period)
+ line.get_pricing_text = f'ON ICE Cotton #2 {period}'
+ line.lots = []
+ return line
+
+ sale = Sale()
+ sale.currency = Mock(rec_name='USD')
+ sale.lines = [
+ make_line('1000', 'MARCH 2026', '72.5000'),
+ make_line('1000', 'MAY 2026', '70.2500'),
+ ]
+
+ self.assertEqual(sale.report_total_quantity, '2000.0')
+ self.assertEqual(sale.report_quantity_unit_upper, 'MT')
+ self.assertEqual(sale.report_qt, 'TWO THOUSAND METRIC TONS')
+ self.assertEqual(sale.report_nb_bale, '')
+ self.assertEqual(
+ sale.report_shipment_periods.splitlines(),
+ ['MARCH 2026', 'MAY 2026'])
+ self.assertEqual(
+ sale.report_price_lines.splitlines(),
+ [
+ 'USC 72.5000 PER POUND (SEVENTY TWO USC AND FIFTY CENTS) ON ICE Cotton #2 MARCH 2026',
+ 'USC 70.2500 PER POUND (SEVENTY USC AND TWENTY FIVE CENTS) ON ICE Cotton #2 MAY 2026',
+ ])
+
del ModuleTestCase
diff --git a/modules/sale/sale_ict.fodt b/modules/sale/sale_ict.fodt
index e89e81c..eb63c65 100644
--- a/modules/sale/sale_ict.fodt
+++ b/modules/sale/sale_ict.fodt
@@ -4091,7 +4091,7 @@
- ABOUT <sum(line.quantity for line in sale.lines)><sale.lines[0].unit.rec_name.upper() if sale.lines and sale.lines[0].unit else ''>(<sale.report_qt>)
+ ABOUT <sale.report_total_quantity><sale.report_quantity_unit_upper>(<sale.report_qt>)
<sale.report_nb_bale>
@@ -4110,27 +4110,13 @@
- <sale.lines[0].linked_currency.rec_name.upper() if sale.lines[0].linked_currency else sale.currency.rec_name.upper()>
-
- <sale.lines[0].linked_price if sale.lines[0].linked_price else sale.lines[0].unit_price>
-
-
-
- PER
-
- <sale.lines[0].linked_unit.rec_name.upper() if sale.lines[0].linked_unit else sale.lines[0].unit.rec_name.upper()>
-
-
-
-
- (
-
- <sale.report_price>
-
- )
-
- <sale.lines[0].get_pricing_text>
-
+ <for each="line in sale.report_price_lines.splitlines()">
+
+
+ <line>
+
+
+ </for>
@@ -4199,12 +4185,18 @@
+
+ <for each="line in sale.report_shipment_periods.splitlines()">
+
- <sale.report_delivery_period_description>
+ <line>
+
+ </for>
+