From da01249f668ff2399d4cac905d961b55f5f21ede Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Wed, 8 Apr 2026 09:46:01 +0200 Subject: [PATCH] Add Packing list template --- modules/purchase_trade/__init__.py | 1 + modules/purchase_trade/configuration.py | 1 + modules/purchase_trade/docs/template-rules.md | 4 + modules/purchase_trade/stock.py | 130 ++ modules/purchase_trade/stock.xml | 12 + modules/purchase_trade/tests/test_module.py | 8 + .../view/template_configuration_form.xml | 2 + modules/stock/packing_list.fodt | 1698 +++++++++++++++++ 8 files changed, 1856 insertions(+) create mode 100644 modules/stock/packing_list.fodt diff --git a/modules/purchase_trade/__init__.py b/modules/purchase_trade/__init__.py index cf32aab..2374193 100755 --- a/modules/purchase_trade/__init__.py +++ b/modules/purchase_trade/__init__.py @@ -280,5 +280,6 @@ def register(): invoice.PurchaseReport, stock.ShipmentShippingReport, stock.ShipmentInsuranceReport, + stock.ShipmentPackingListReport, module='purchase_trade', type_='report') diff --git a/modules/purchase_trade/configuration.py b/modules/purchase_trade/configuration.py index 0e6d0c2..244cc50 100644 --- a/modules/purchase_trade/configuration.py +++ b/modules/purchase_trade/configuration.py @@ -16,3 +16,4 @@ class Configuration(ModelSingleton, ModelSQL, ModelView): purchase_report_template = fields.Char("Purchase Template") shipment_shipping_report_template = fields.Char("Shipping Template") shipment_insurance_report_template = fields.Char("Insurance Template") + shipment_packing_list_report_template = fields.Char("Packing List Template") diff --git a/modules/purchase_trade/docs/template-rules.md b/modules/purchase_trade/docs/template-rules.md index 2b531c3..bf97f3d 100644 --- a/modules/purchase_trade/docs/template-rules.md +++ b/modules/purchase_trade/docs/template-rules.md @@ -114,6 +114,10 @@ Derniere mise a jour: `2026-04-07` - `Payment` - `Purchase` - `Shipment` +- Dans la section `Shipment`, les templates metier attendus sont: + - `Shipping` + - `Insurance` + - `Packing List` - Regle: - si le champ de template correspondant est vide, le report doit echouer explicitement avec `No template found` diff --git a/modules/purchase_trade/stock.py b/modules/purchase_trade/stock.py index 89c5b42..b997423 100755 --- a/modules/purchase_trade/stock.py +++ b/modules/purchase_trade/stock.py @@ -533,6 +533,46 @@ class ShipmentIn(metaclass=PoolMeta): value = Decimal(str(value or 0)).quantize(Decimal('0.01')) return format(value, 'f') + @staticmethod + def _format_report_quantity(value, digits='0.001'): + if value in (None, ''): + return '' + quantity = Decimal(str(value or 0)).quantize(Decimal(digits)) + text = format(quantity, 'f') + return text.rstrip('0').rstrip('.') or '0' + + def _get_report_trade(self): + line = self._get_report_trade_line() + if not line: + return None + return getattr(line, 'sale', None) or getattr(line, 'purchase', None) + + def _get_report_weight_totals(self): + net = Decimal('0') + gross = Decimal('0') + for move in (self.incoming_moves or self.moves or []): + lot = getattr(move, 'lot', None) + if lot: + lot_net = ( + lot.get_current_quantity() + if hasattr(lot, 'get_current_quantity') + else lot.get_current_quantity_converted() + if hasattr(lot, 'get_current_quantity_converted') + else getattr(move, 'quantity', 0) + ) + lot_gross = ( + lot.get_current_gross_quantity() + if hasattr(lot, 'get_current_gross_quantity') + else lot_net + ) + net += Decimal(str(lot_net or 0)) + gross += Decimal(str(lot_gross or 0)) + else: + quantity = Decimal(str(getattr(move, 'quantity', 0) or 0)) + net += quantity + gross += quantity + return net, gross + @property def report_product_name(self): line = self._get_report_trade_line() @@ -652,6 +692,87 @@ class ShipmentIn(metaclass=PoolMeta): today = Date.today() date_text = today.strftime('%d-%m-%Y') if today else '' return ', '.join(part for part in [place, date_text] if part) + + @property + def report_packing_product_class(self): + return self.report_product_name + + @property + def report_packing_contract_number(self): + trade = self._get_report_trade() + return ( + getattr(trade, 'reference', None) + or getattr(trade, 'number', None) + or self.reference + or self.number + or '') + + @property + def report_packing_invoice_qty(self): + quantity = self.quantity if self.quantity not in (None, '') else 0 + return self._format_report_quantity(quantity) + + @property + def report_packing_invoice_qty_unit(self): + unit = self.unit + return ( + getattr(unit, 'symbol', None) + or getattr(unit, 'rec_name', None) + or '') + + @property + def report_packing_origin(self): + trade = self._get_report_trade() + return ( + getattr(trade, 'product_origin', None) + or getattr(self.from_location, 'name', None) + or '') + + @property + def report_packing_product(self): + return self.report_product_name + + @property + def report_packing_counterparty_name(self): + trade = self._get_report_trade() + party = getattr(trade, 'party', None) if trade else None + if party: + return party.rec_name or '' + return getattr(self.supplier, 'rec_name', '') or '' + + @property + def report_packing_ship_name(self): + if self.vessel and self.vessel.vessel_name: + return self.vessel.vessel_name + return self.transport_type or '' + + @property + def report_packing_loading_port(self): + return getattr(self.from_location, 'name', '') or '' + + @property + def report_packing_destination_port(self): + return getattr(self.to_location, 'name', '') or '' + + @property + def report_packing_chunk_number(self): + return self.bl_number or self.number or '' + + @property + def report_packing_chunk_date(self): + if self.bl_date: + return self.bl_date.strftime('%d-%m-%Y') + return '' + + @property + def report_packing_gross_weight(self): + _, gross = self._get_report_weight_totals() + return self._format_report_quantity(gross) + + @property + def report_packing_net_weight(self): + net, _ = self._get_report_weight_totals() + return self._format_report_quantity(net) def get_rec_name(self, name=None): if self.number: @@ -2153,3 +2274,12 @@ class ShipmentInsuranceReport(ShipmentTemplateReportMixin, BaseSupplierShipping) def _resolve_configured_report_path(cls, action): return cls._resolve_template_path( 'shipment_insurance_report_template', 'stock') + + +class ShipmentPackingListReport(ShipmentTemplateReportMixin, BaseSupplierShipping): + __name__ = 'stock.shipment.in.packing_list' + + @classmethod + def _resolve_configured_report_path(cls, action): + return cls._resolve_template_path( + 'shipment_packing_list_report_template', 'stock') diff --git a/modules/purchase_trade/stock.xml b/modules/purchase_trade/stock.xml index 9790592..4c55d88 100755 --- a/modules/purchase_trade/stock.xml +++ b/modules/purchase_trade/stock.xml @@ -72,6 +72,18 @@ this repository contains the full copyright notices and license terms. --> stock.shipment.in,-1 + + + Packing List + stock.shipment.in + stock.shipment.in.packing_list + stock/packing_list.fodt + + + form_print + stock.shipment.in,-1 + + Update with SoF PDF diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 284e3aa..50a68f9 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -510,11 +510,13 @@ class PurchaseTradeTestCase(ModuleTestCase): 'shipment report paths are resolved from purchase_trade configuration' shipping_report = Pool().get('stock.shipment.in.shipping', type='report') insurance_report = Pool().get('stock.shipment.in.insurance', type='report') + packing_report = Pool().get('stock.shipment.in.packing_list', type='report') config_model = Mock() config_model.search.return_value = [ Mock( shipment_shipping_report_template='si_custom.fodt', shipment_insurance_report_template='insurance_custom.fodt', + shipment_packing_list_report_template='packing_list_custom.fodt', ) ] @@ -535,6 +537,12 @@ class PurchaseTradeTestCase(ModuleTestCase): 'report': 'stock/insurance.fodt', }), 'stock/insurance_custom.fodt') + self.assertEqual( + packing_report._resolve_configured_report_path({ + 'name': 'Packing List', + 'report': 'stock/packing_list.fodt', + }), + 'stock/packing_list_custom.fodt') def test_shipment_insurance_helpers_use_fee_and_controller(self): 'shipment insurance helpers read insurance fee and shipment context' diff --git a/modules/purchase_trade/view/template_configuration_form.xml b/modules/purchase_trade/view/template_configuration_form.xml index 2153cda..c94eba3 100644 --- a/modules/purchase_trade/view/template_configuration_form.xml +++ b/modules/purchase_trade/view/template_configuration_form.xml @@ -29,4 +29,6 @@