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> +