Add insurance template

This commit is contained in:
2026-04-06 17:30:50 +02:00
parent 845b9cf749
commit ec359f6b8a
12 changed files with 1472 additions and 26 deletions

View File

@@ -1528,12 +1528,9 @@
<text:p text:style-name="P3"/>
<text:h text:style-name="P27" text:outline-level="1"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T3">COMMERCIAL INVOICE</text:span></text:span></text:h>
<text:p text:style-name="P2"/>
<text:p text:style-name="P8"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T4">Invoice Nr: <text:tab/><text:tab/><text:tab/>Date:</text:span></text:span></text:p>
<text:p text:style-name="P20"/>
<text:p text:style-name="P8"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text">&lt;invoice.number or &apos;&apos;&gt;</text:placeholder></text:span></text:span><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T5"><text:s text:c="48"/></text:span></text:span><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text">&lt;format_date(invoice.invoice_date, invoice.party.lang) if invoice.invoice_date else &apos;&apos;&gt;</text:placeholder></text:span></text:span></text:p>
<text:p text:style-name="P8"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T4">Invoice: <text:placeholder text:placeholder-type="text">&lt;invoice.number or &apos;&apos;&gt;</text:placeholder><text:tab/><text:tab/>Date: <text:placeholder text:placeholder-type="text">&lt;format_date(invoice.invoice_date, invoice.party.lang) if invoice.invoice_date else &apos;&apos;&gt;</text:placeholder></text:span></text:span></text:p>
<text:p text:style-name="P3"/>
<text:p text:style-name="P8"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T4">Order reference:</text:span></text:span></text:p>
<text:p text:style-name="P14"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T4"><text:placeholder text:placeholder-type="text">&lt;invoice.report_contract_number or &apos;&apos;&gt;</text:placeholder></text:span></text:span></text:p>
<text:p text:style-name="P8"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T4">Reference: <text:placeholder text:placeholder-type="text">&lt;invoice.report_contract_number or &apos;&apos;&gt;</text:placeholder></text:span></text:span></text:p>
</table:table-cell>
</table:table-row>
<table:table-row>
@@ -1626,6 +1623,7 @@
<text:p text:style-name="P17"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T9"><text:s/></text:span></text:span><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T12"><text:s/></text:span></text:span><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T12"><text:placeholder text:placeholder-type="text">&lt;format_number_symbol(invoice.report_net, invoice.party.lang, invoice.lines[0].unit, digits=2) if invoice.lines else &apos;&apos;&gt;</text:placeholder></text:span></text:span></text:p>
</table:table-cell>
<table:table-cell table:style-name="Tableau2.A1" office:value-type="string">
<text:p text:style-name="P15"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T9"><text:placeholder text:placeholder-type="text">&lt;invoice.report_product_name or &apos;&apos;&gt;</text:placeholder></text:span></text:span></text:p>
<text:p text:style-name="P15"><text:span text:style-name="Police_20_par_20_défaut"><text:span text:style-name="T9"><text:placeholder text:placeholder-type="text">&lt;invoice.report_product_description or &apos;&apos;&gt;</text:placeholder></text:span></text:span></text:p>
<text:p text:style-name="P6"/>
</table:table-cell>
@@ -1707,4 +1705,4 @@
<text:p text:style-name="Standard"><text:placeholder text:placeholder-type="text">&lt;/for&gt;</text:placeholder></text:p>
</office:text>
</office:body>
</office:document>
</office:document>

View File

@@ -278,5 +278,7 @@ def register():
invoice.InvoiceReport,
invoice.SaleReport,
invoice.PurchaseReport,
stock.ShipmentShippingReport,
stock.ShipmentInsuranceReport,
module='purchase_trade', type_='report')

View File

@@ -13,3 +13,5 @@ class Configuration(ModelSingleton, ModelSQL, ModelView):
invoice_cndn_report_template = fields.Char("CN/DN Template")
invoice_prepayment_report_template = fields.Char("Prepayment Template")
purchase_report_template = fields.Char("Purchase Template")
shipment_shipping_report_template = fields.Char("Shipping Template")
shipment_insurance_report_template = fields.Char("Insurance Template")

View File

@@ -29,6 +29,8 @@ Derniere mise a jour: `2026-03-27`
- reutiliser si possible les proprietes `report_*` deja presentes sur `sale.sale`
- Pour un achat:
- reutiliser si possible les proprietes `report_*` deja presentes sur `purchase.purchase`
- Pour un shipment entrant:
- reutiliser si possible les proprietes `report_*` exposees sur `stock.shipment.in`
## 4) Propriete disponibles sur `account.invoice`
@@ -288,8 +290,30 @@ Usage typique:
- `modules/account_invoice/invoice_ict.fodt`
- `modules/account_invoice/invoice_ict_final.fodt`
- `modules/sale/sale_ict.fodt`
- `modules/stock/insurance.fodt`
## 9) Recommandations
## 9) Proprietes utiles deja presentes sur `stock.shipment.in`
Source code: `modules/purchase_trade/stock.py`
- `report_product_name`
- `report_product_description`
- `report_insurance_footer_ref`
- `report_insurance_certificate_number`
- `report_insurance_account_of`
- `report_insurance_goods_description`
- `report_insurance_loading_port`
- `report_insurance_discharge_port`
- `report_insurance_transport`
- `report_insurance_amount`
- `report_insurance_surveyor`
- `report_insurance_issue_place_and_date`
Usage typique:
- templates shipment relies a l'assurance
- templates qui lisent le fee `Insurance` d'un `stock.shipment.in`
## 10) Recommandations
- Avant d'ajouter une nouvelle expression dans un `.fodt`, verifier si une
propriete `report_*` existe deja ici.

View File

@@ -198,6 +198,13 @@ class Invoice(metaclass=PoolMeta):
return line.product.description or ''
return ''
@property
def report_product_name(self):
line = self._get_report_trade_line()
if line and line.product:
return line.product.name or ''
return ''
@property
def report_description_upper(self):
if self.lines:
@@ -598,6 +605,15 @@ class InvoiceLine(metaclass=PoolMeta):
return origin.product.description or ''
return ''
@property
def report_product_name(self):
if self.product:
return self.product.name or ''
origin = getattr(self, 'origin', None)
if origin and getattr(origin, 'product', None):
return origin.product.name or ''
return ''
@property
def report_description_upper(self):
return Invoice._clean_report_description(self.description)

View File

@@ -492,6 +492,20 @@ class Sale(metaclass=PoolMeta):
return 'NB BALES: ' + str(int(nb_bale))
return ''
@property
def report_product_name(self):
line = self._get_report_first_line()
if line and line.product:
return line.product.name or ''
return ''
@property
def report_product_description(self):
line = self._get_report_first_line()
if line and line.product:
return line.product.description or ''
return ''
@property
def report_crop_name(self):
if self.crop:

View File

@@ -6,7 +6,7 @@ from trytond.pyson import Bool, Eval, Id
from trytond.model import (ModelSQL, ModelView)
from trytond.tools import is_full_text, lstrip_wildcard
from trytond.transaction import Transaction, inactive_records
from decimal import getcontext, Decimal, ROUND_HALF_UP
from decimal import getcontext, Decimal, ROUND_HALF_UP
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
from sql.conditionals import Case
from sql import Column, Literal
@@ -23,8 +23,10 @@ import io
import base64
import logging
import json
import re
import html
import re
import html
from trytond.exceptions import UserError
from trytond.modules.stock.shipment import SupplierShipping as BaseSupplierShipping
logger = logging.getLogger(__name__)
@@ -387,8 +389,8 @@ class ShipmentWR(ModelSQL,ModelView):
shipment_in = fields.Many2One('stock.shipment.in',"Shipment In")
wr = fields.Many2One('weight.report',"WR")
class ShipmentIn(metaclass=PoolMeta):
__name__ = 'stock.shipment.in'
class ShipmentIn(metaclass=PoolMeta):
__name__ = 'stock.shipment.in'
from_location = fields.Many2One('stock.location', 'From location')
to_location = fields.Many2One('stock.location', 'To location')
@@ -459,9 +461,134 @@ class ShipmentIn(metaclass=PoolMeta):
'send': {},
})
def get_vessel_type(self,name=None):
if self.vessel:
return self.vessel.vessel_type
def get_vessel_type(self,name=None):
if self.vessel:
return self.vessel.vessel_type
def _get_report_primary_move(self):
moves = list(self.incoming_moves or self.moves or [])
return moves[0] if moves else None
def _get_report_primary_lot(self):
move = self._get_report_primary_move()
return getattr(move, 'lot', None) if move else None
def _get_report_trade_line(self):
lot = self._get_report_primary_lot()
if not lot:
return None
return getattr(lot, 'sale_line', None) or getattr(lot, 'line', None)
def _get_report_insurance_fee(self):
for fee in self.fees or []:
product = getattr(fee, 'product', None)
name = ((getattr(product, 'name', '') or '')).strip().lower()
if 'insurance' in name:
return fee
return None
@staticmethod
def _format_report_amount(value):
if value in (None, ''):
return ''
value = Decimal(str(value or 0)).quantize(Decimal('0.01'))
return format(value, 'f')
@property
def report_product_name(self):
line = self._get_report_trade_line()
product = getattr(line, 'product', None) if line else None
if product:
return product.name or ''
move = self._get_report_primary_move()
product = getattr(move, 'product', None) if move else None
return getattr(product, 'name', '') or ''
@property
def report_product_description(self):
line = self._get_report_trade_line()
product = getattr(line, 'product', None) if line else None
if product:
return product.description or ''
move = self._get_report_primary_move()
product = getattr(move, 'product', None) if move else None
return getattr(product, 'description', '') or ''
@property
def report_insurance_footer_ref(self):
return self.bl_number or self.number or ''
@property
def report_insurance_certificate_number(self):
return self.bl_number or self.number or ''
@property
def report_insurance_account_of(self):
line = self._get_report_trade_line()
trade = getattr(line, 'sale', None) or getattr(line, 'purchase', None)
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_insurance_goods_description(self):
name = self.report_product_name
description = self.report_product_description
if description and description != name:
return ' - '.join(part for part in [name, description] if part)
return name or description
@property
def report_insurance_loading_port(self):
return getattr(self.from_location, 'name', '') or ''
@property
def report_insurance_discharge_port(self):
return getattr(self.to_location, 'name', '') or ''
@property
def report_insurance_transport(self):
if self.vessel and self.vessel.vessel_name:
return self.vessel.vessel_name
return self.transport_type or ''
@property
def report_insurance_amount(self):
fee = self._get_report_insurance_fee()
if not fee:
return ''
currency = getattr(fee, 'currency', None)
currency_text = (
getattr(currency, 'rec_name', None)
or getattr(currency, 'code', None)
or getattr(currency, 'symbol', None)
or '')
amount = self._format_report_amount(fee.get_amount())
return ' '.join(part for part in [currency_text, amount] if part)
@property
def report_insurance_surveyor(self):
if self.controller:
return self.controller.rec_name or ''
fee = self._get_report_insurance_fee()
supplier = getattr(fee, 'supplier', None) if fee else None
return getattr(supplier, 'rec_name', '') or ''
@property
def report_insurance_issue_place_and_date(self):
Date = Pool().get('ir.date')
address = None
if self.company and getattr(self.company, 'party', None):
address = self.company.party.address_get()
place = (
getattr(address, 'city', None)
or getattr(self.company.party, 'rec_name', None)
if self.company and getattr(self.company, 'party', None) else ''
) or ''
today = Date.today()
date_text = today.strftime('%d-%m-%Y') if today else ''
return ', '.join(part for part in [place, date_text] if part)
def get_rec_name(self, name=None):
if self.number:
@@ -1888,7 +2015,7 @@ class Revaluate(Wizard):
return 'end'
class RevaluateStart(ModelView):
class RevaluateStart(ModelView):
"Revaluate"
__name__ = 'account.revaluate.start'
revaluation_date = fields.Date(
@@ -1902,5 +2029,64 @@ class RevaluateStart(ModelView):
return Date.today()
@classmethod
def default_delete_after(cls):
return False
def default_delete_after(cls):
return False
class ShipmentTemplateReportMixin:
@classmethod
def _get_purchase_trade_configuration(cls):
Configuration = Pool().get('purchase_trade.configuration')
configurations = Configuration.search([], limit=1)
return configurations[0] if configurations else None
@classmethod
def _get_action_report_path(cls, action):
if isinstance(action, dict):
return action.get('report') or ''
return getattr(action, 'report', '') or ''
@classmethod
def _resolve_template_path(cls, field_name, default_prefix):
config = cls._get_purchase_trade_configuration()
template = getattr(config, field_name, '') if config else ''
template = (template or '').strip()
if not template:
raise UserError('No template found')
if '/' not in template:
return f'{default_prefix}/{template}'
return template
@classmethod
def _get_resolved_action(cls, action):
report_path = cls._resolve_configured_report_path(action)
if isinstance(action, dict):
resolved = dict(action)
resolved['report'] = report_path
return resolved
setattr(action, 'report', report_path)
return action
@classmethod
def _execute(cls, records, header, data, action):
resolved_action = cls._get_resolved_action(action)
return super()._execute(records, header, data, resolved_action)
class ShipmentShippingReport(ShipmentTemplateReportMixin, BaseSupplierShipping):
__name__ = 'stock.shipment.in.shipping'
@classmethod
def _resolve_configured_report_path(cls, action):
return cls._resolve_template_path(
'shipment_shipping_report_template', 'stock')
class ShipmentInsuranceReport(ShipmentTemplateReportMixin, BaseSupplierShipping):
__name__ = 'stock.shipment.in.insurance'
@classmethod
def _resolve_configured_report_path(cls, action):
return cls._resolve_template_path(
'shipment_insurance_report_template', 'stock')

View File

@@ -56,10 +56,22 @@ this repository contains the full copyright notices and license terms. -->
<field name="model">stock.shipment.in,-1</field>
<field name="action" ref="act_vf"/>
</record>
<record model="ir.action.url" id="url_vessel_finder">
<field name="name">Find Vessel</field>
<field name="url">https://www.vesselfinder.com</field>
</record>
<record model="ir.action.url" id="url_vessel_finder">
<field name="name">Find Vessel</field>
<field name="url">https://www.vesselfinder.com</field>
</record>
<record model="ir.action.report" id="report_shipment_in_insurance">
<field name="name">Insurance</field>
<field name="model">stock.shipment.in</field>
<field name="report_name">stock.shipment.in.insurance</field>
<field name="report">stock/insurance.fodt</field>
</record>
<record model="ir.action.keyword" id="report_shipment_in_insurance_keyword">
<field name="keyword">form_print</field>
<field name="model">stock.shipment.in,-1</field>
<field name="action" ref="report_shipment_in_insurance"/>
</record>
<record model="ir.action.wizard" id="act_update_sof">
<field name="name">Update with SoF PDF</field>
@@ -126,4 +138,4 @@ this repository contains the full copyright notices and license terms. -->
id="menu_revaluate"/>
</data>
</tryton>
</tryton>

View File

@@ -419,6 +419,96 @@ class PurchaseTradeTestCase(ModuleTestCase):
}),
'purchase/purchase_melya.fodt')
def test_shipment_reports_use_templates_from_configuration(self):
'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')
config_model = Mock()
config_model.search.return_value = [
Mock(
shipment_shipping_report_template='si_custom.fodt',
shipment_insurance_report_template='insurance_custom.fodt',
)
]
with patch(
'trytond.modules.purchase_trade.stock.Pool'
) as PoolMock:
PoolMock.return_value.get.return_value = config_model
self.assertEqual(
shipping_report._resolve_configured_report_path({
'name': 'Shipping instructions',
'report': 'stock/si.fodt',
}),
'stock/si_custom.fodt')
self.assertEqual(
insurance_report._resolve_configured_report_path({
'name': 'Insurance',
'report': 'stock/insurance.fodt',
}),
'stock/insurance_custom.fodt')
def test_shipment_insurance_helpers_use_fee_and_controller(self):
'shipment insurance helpers read insurance fee and shipment context'
ShipmentIn = Pool().get('stock.shipment.in')
shipment = ShipmentIn()
shipment.number = 'IN/0001'
shipment.bl_number = 'BL-001'
shipment.from_location = Mock(name='LIVERPOOL')
shipment.to_location = Mock(name='LE HAVRE')
shipment.vessel = Mock(vessel_name='MV ATLANTIC')
shipment.controller = Mock(rec_name='CONTROL UNION')
shipment.supplier = Mock(rec_name='MELYA SA')
sale_party = Mock(rec_name='SGT FR')
sale = Mock(party=sale_party)
product = Mock(name='COTTON UPLAND', description='RAW WHITE COTTON')
line = Mock(product=product, sale=sale)
lot = Mock(sale_line=line, line=None)
move = Mock(lot=lot, product=product)
shipment.incoming_moves = [move]
shipment.moves = [move]
insurance_fee = Mock()
insurance_fee.product = Mock(name='Insurance')
insurance_fee.currency = Mock(rec_name='USD')
insurance_fee.get_amount.return_value = Decimal('1234.56')
insurance_fee.supplier = Mock(rec_name='HELVETIA')
shipment.fees = [insurance_fee]
with patch(
'trytond.modules.purchase_trade.stock.Pool'
) as PoolMock:
date_model = Mock()
date_model.today.return_value = datetime.date(2026, 4, 6)
config_model = Mock()
PoolMock.return_value.get.side_effect = lambda name: {
'ir.date': date_model,
'purchase_trade.configuration': config_model,
}[name]
shipment.company = Mock(
party=Mock(
rec_name='MELYA SA',
address_get=Mock(return_value=Mock(city='GENEVA')),
)
)
self.assertEqual(
shipment.report_insurance_certificate_number, 'BL-001')
self.assertEqual(
shipment.report_insurance_account_of, 'SGT FR')
self.assertEqual(
shipment.report_insurance_goods_description,
'COTTON UPLAND - RAW WHITE COTTON')
self.assertEqual(
shipment.report_insurance_amount, 'USD 1234.56')
self.assertEqual(
shipment.report_insurance_surveyor, 'CONTROL UNION')
self.assertEqual(
shipment.report_insurance_issue_place_and_date,
'GENEVA, 06-04-2026')
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')
@@ -466,6 +556,27 @@ class PurchaseTradeTestCase(ModuleTestCase):
'USC 70.2500 PER POUND (SEVENTY USC AND TWENTY FIVE CENTS) ON ICE Cotton #2 MAY 2026',
])
def test_report_product_fields_expose_name_and_description(self):
'sale and invoice templates use stable product name/description helpers'
Sale = Pool().get('sale.sale')
Invoice = Pool().get('account.invoice')
product = Mock(name='COTTON UPLAND', description='RAW WHITE COTTON')
sale_line = Mock(product=product)
invoice_line = Mock(product=product)
sale = Sale()
sale.lines = [sale_line]
invoice = Invoice()
invoice.lines = [invoice_line]
invoice._get_report_trade_line = Mock(return_value=sale_line)
self.assertEqual(sale.report_product_name, 'COTTON UPLAND')
self.assertEqual(sale.report_product_description, 'RAW WHITE COTTON')
self.assertEqual(invoice.report_product_name, 'COTTON UPLAND')
self.assertEqual(invoice.report_product_description, 'RAW WHITE COTTON')
def test_contract_factory_uses_one_line_per_selected_source(self):
'matched multi-lot contract creation keeps one generated line per source lot'
contract_detail = Mock(quantity=Decimal('2000'))

View File

@@ -19,4 +19,10 @@
<separator id="purchase_templates" string="Purchase" colspan="4"/>
<label name="purchase_report_template"/>
<field name="purchase_report_template" colspan="3"/>
<separator id="shipment_templates" string="Shipment" colspan="4"/>
<label name="shipment_shipping_report_template"/>
<field name="shipment_shipping_report_template" colspan="3"/>
<label name="shipment_insurance_report_template"/>
<field name="shipment_insurance_report_template" colspan="3"/>
</form>

View File

@@ -1763,8 +1763,8 @@
<text:p text:style-name="P25" loext:marker-style-name="T39"/>
<text:p text:style-name="P24" loext:marker-style-name="T39"/>
<text:p text:style-name="P24" loext:marker-style-name="T39"/>
<text:p text:style-name="P39" loext:marker-style-name="T26"><text:span text:style-name="T26"><text:s text:c="2"/></text:span><text:span text:style-name="T42"><text:placeholder text:placeholder-type="text">&lt;sale.lines[0].product.name if sale.lines and sale.lines[0].product else &apos;&apos;&gt;</text:placeholder></text:span></text:p>
<text:p text:style-name="P30" loext:marker-style-name="T26"/>
<text:p text:style-name="P39" loext:marker-style-name="T26"><text:span text:style-name="T26"><text:s text:c="2"/></text:span><text:span text:style-name="T42"><text:placeholder text:placeholder-type="text">&lt;sale.report_product_name or &apos;&apos;&gt;</text:placeholder></text:span></text:p>
<text:p text:style-name="P30" loext:marker-style-name="T26"><text:span text:style-name="T42"><text:placeholder text:placeholder-type="text">&lt;sale.report_product_description or &apos;&apos;&gt;</text:placeholder></text:span></text:p>
<text:p text:style-name="P27" loext:marker-style-name="T42"><text:span text:style-name="T42"><text:s/></text:span></text:p>
</table:table-cell>
<table:table-cell table:style-name="Tableau1.B2" office:value-type="string">
@@ -1803,7 +1803,7 @@
<text:p text:style-name="P2"/>
<text:p text:style-name="P7">PAYMENT TERMS:<text:tab/><text:placeholder text:placeholder-type="text">&lt;sale.payment_term.description if sale.payment_term else &apos;&apos;&gt;</text:placeholder></text:p>
<text:p text:style-name="P2"/>
<text:p text:style-name="P8">Bank: UBS SWITZERLAND AG</text:p>
<text:p text:style-name="P8">BANK: UBS SWITZERLAND AG</text:p>
<text:p text:style-name="P8">Case Postale</text:p>
<text:p text:style-name="P8">CH-1211 Geneve 2</text:p>
<text:p text:style-name="P2"/>

1075
modules/stock/insurance.fodt Normal file

File diff suppressed because it is too large Load Diff