Add Packing list template

This commit is contained in:
2026-04-08 09:46:01 +02:00
parent 50a8c6328f
commit da01249f66
8 changed files with 1856 additions and 0 deletions

View File

@@ -280,5 +280,6 @@ def register():
invoice.PurchaseReport,
stock.ShipmentShippingReport,
stock.ShipmentInsuranceReport,
stock.ShipmentPackingListReport,
module='purchase_trade', type_='report')

View File

@@ -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")

View File

@@ -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`

View File

@@ -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()
@@ -653,6 +693,87 @@ class ShipmentIn(metaclass=PoolMeta):
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:
return self.number + '[' + (self.vessel.vessel_name if self.vessel else '') + (('-' + self.travel_nb) if self.travel_nb else '') + ']'
@@ -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')

View File

@@ -73,6 +73,18 @@ this repository contains the full copyright notices and license terms. -->
<field name="action" ref="report_shipment_in_insurance"/>
</record>
<record model="ir.action.report" id="report_shipment_in_packing_list">
<field name="name">Packing List</field>
<field name="model">stock.shipment.in</field>
<field name="report_name">stock.shipment.in.packing_list</field>
<field name="report">stock/packing_list.fodt</field>
</record>
<record model="ir.action.keyword" id="report_shipment_in_packing_list_keyword">
<field name="keyword">form_print</field>
<field name="model">stock.shipment.in,-1</field>
<field name="action" ref="report_shipment_in_packing_list"/>
</record>
<record model="ir.action.wizard" id="act_update_sof">
<field name="name">Update with SoF PDF</field>
<field name="wiz_name">sof.update</field>

View File

@@ -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'

View File

@@ -29,4 +29,6 @@
<field name="shipment_shipping_report_template" colspan="3"/>
<label name="shipment_insurance_report_template"/>
<field name="shipment_insurance_report_template" colspan="3"/>
<label name="shipment_packing_list_report_template"/>
<field name="shipment_packing_list_report_template" colspan="3"/>
</form>

File diff suppressed because it is too large Load Diff