From c272630a8a8a17b149a27d3cbcb64c275778342c Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Thu, 4 Jun 2026 14:21:40 +0200 Subject: [PATCH] Invoice Melya template --- modules/account_invoice/invoice_melya.fodt | 353 +++++++++++--------- modules/purchase_trade/invoice.py | 18 +- modules/purchase_trade/stock.py | 67 +++- modules/purchase_trade/tests/test_module.py | 53 +++ 4 files changed, 309 insertions(+), 182 deletions(-) diff --git a/modules/account_invoice/invoice_melya.fodt b/modules/account_invoice/invoice_melya.fodt index dc1151e..81a37e9 100644 --- a/modules/account_invoice/invoice_melya.fodt +++ b/modules/account_invoice/invoice_melya.fodt @@ -1,24 +1,24 @@ - LibreOffice/7.6.0.3$Windows_X86_64 LibreOffice_project/69edd8b8ebc41d00b4de3915dc82f8f0fc3b6265Aksel Kamhi2026-03-11T17:00:00Z2026-05-20T09:52:10.3890000002025-12-11T15:32:00Z23PT1H10M0x010100C9130B1D3286474FB2D468C22F52A7B0 + LibreOffice/7.6.0.3$Windows_X86_64 LibreOffice_project/69edd8b8ebc41d00b4de3915dc82f8f0fc3b6265Aksel Kamhi2026-03-11T17:00:00Z2026-06-04T13:46:15.6090000002025-12-11T15:32:00Z24PT1H12M24S0x010100C9130B1D3286474FB2D468C22F52A7B0 - 31295 + 77525 0 24236 - 11721 + 12222 true false view2 - 4281 - 37405 + 5376 + 80850 0 - 31295 + 77525 24234 - 43014 + 89745 0 0 false @@ -91,7 +91,7 @@ false false false - 2635503 + 2747895 1803951 false false @@ -159,7 +159,7 @@ - + @@ -267,6 +267,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -382,19 +411,6 @@ - - - - - - - - - - - - - @@ -448,175 +464,193 @@ - + + + + - - - - - + + + + + - + - + - + - - - - + + + + - - - - - + - + - - + + - + + - - + - + - + - + - + + + + + + + + + - + - + - + - + - + - - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + @@ -625,14 +659,18 @@ - + + + - + + + @@ -641,14 +679,14 @@ - VAT NR. – NOT SUBJECT TO VAT IN SWITZERLAND - We certify this invoice authentic and definitivePage 1/1 + VAT NR. – NOT SUBJECT TO VAT IN SWITZERLAND + We certify this invoice authentic and definitivePage 1/3 - VAT NR. – NOT SUBJECT TO VAT IN SWITZERLAND - We certify this invoice authentic and definitivePage 1/1 + NOT SUBJECT TO SWISS VAT, transaction executed abroad-art7 al.1 let. AL TVA + We certify this invoice authentic and definitivePage 2/3 @@ -668,8 +706,8 @@ - - + + iVBORw0KGgoAAAANSUhEUgAAAXYAAACACAYAAAAf4kLVAAAACXBIWXMAABcSAAAXEwF+hOMT AAAgAElEQVR4nOy9d5xV1bn//167njYVpAlYsCGWoCKgsbdrLLkavT+MksRrEkuMsQYUJUTR iBqNEmu+N+aaXGNupIii2FBQFEQUUUBBUDoCMkw7Zbf1+2PttecMDMpYEHPneb3Oa2bOnLP3 @@ -1548,86 +1586,87 @@ AAAASUVORK5CYII= - + + + COMMERCIAL INVOICE - COMMERCIAL INVOICE - - Invoice: <invoice.number or ''> - Date: <format_date(invoice.invoice_date, invoice.party.lang) if invoice.invoice_date else ''> - - Reference: <invoice.report_contract_number or ''> + Invoice: <invoice.number or ''> + Date: <format_date(invoice.invoice_date, invoice.party.lang) if invoice.invoice_date else ''> + + Reference: <invoice.report_contract_number or ''> - + - - Buyer: - <'TAX ID: ' + invoice.party_tax_identifier.code if invoice.party_tax_identifier else ''> - <for each="line in invoice.report_address.splitlines()"><line></for> - - + + Buyer: + <'TAX ID: ' + invoice.party_tax_identifier.code if invoice.party_tax_identifier else ''> + <for each="line in invoice.report_address.splitlines()"><line></for> + + - + - Payment terms: - <invoice.report_payment_description or ''>  - <if test="invoice.report_melya_maturity_date">Maturity date: <format_date(invoice.report_melya_maturity_date, invoice.party.lang)></if> - - <if test="invoice.report_melya_bank_name">Bank: <invoice.report_melya_bank_name></if> - <for each="line in invoice.report_melya_bank_address.splitlines()"> - <line> - </for> - - <if test="invoice.report_melya_bank_iban">IBAN : <invoice.report_melya_bank_iban></if> - <if test="invoice.report_melya_bank_swift">SwifT Code: <invoice.report_melya_bank_swift></if> - + Payment terms: + <invoice.report_payment_description or ''>  + <if test="invoice.report_melya_maturity_date">Maturity date: <format_date(invoice.report_melya_maturity_date, invoice.party.lang)></if> + + <if test="invoice.report_melya_bank_name">Bank: <invoice.report_melya_bank_name></if> + <for each="line in invoice.report_melya_bank_address.splitlines()"> + <line> + </for> + + <if test="invoice.report_melya_bank_iban">IBAN : <invoice.report_melya_bank_iban></if> + <if test="invoice.report_melya_bank_swift">SwifT Code: <invoice.report_melya_bank_swift></if> + - - Shipment date: - <format_date(invoice.invoice_date, invoice.party.lang) if invoice.invoice_date else ''> + + Shipment date: + <format_date(invoice.report_bl_date, invoice.party.lang) if invoice.report_bl_date else ''> - - Transportation: - <invoice.report_shipment> + + Transportation: + <invoice.report_transportation or ''> - - Terms of delivery: - <invoice.report_incoterm or ''> + + Terms of delivery: + <invoice.report_incoterm or ''> + - From: - <invoice.report_loading_port or ''> + From: + <invoice.report_loading_port or ''> - To: - <invoice.report_discharge_port or ''> + To: + <invoice.report_discharge_port or ''> - Origin: - <invoice.report_origin or ''> + Origin: + <invoice.report_origin or ''> - + @@ -1635,101 +1674,101 @@ - Quantity + Quantity - Goods Description + Goods Description - Unit Price / mt + Unit Price / mt - US Dollar Amount + US Dollar Amount - <format_number_symbol(invoice.report_net, invoice.party.lang, invoice.lines[0].unit, digits=4) if invoice.lines else ''> + <format_number_symbol(invoice.report_net, invoice.party.lang, invoice.lines[0].unit, digits=4) if invoice.lines else ''> - <invoice.report_product_name or ''> - <invoice.report_product_description or ''> - + <invoice.report_product_name or ''> + <invoice.report_product_description or ''> + - <format_currency(invoice.lines[0].unit_price, invoice.party.lang, invoice.currency) if invoice.lines else ''> + <format_currency(invoice.lines[0].unit_price, invoice.party.lang, invoice.currency) if invoice.lines else ''> - <format_currency(invoice.untaxed_amount, invoice.party.lang, invoice.currency)> + <format_currency(invoice.untaxed_amount, invoice.party.lang, invoice.currency)> - + - <if test="invoice.report_melya_rate_rows"> + <if test="invoice.report_melya_rate_rows"> - + - <for each="pc in invoice.report_melya_rate_rows"> + <for each="pc in invoice.report_melya_rate_rows"> - + - <pc['label']> + <pc['label']> - <pc.get('amount_text', '')> + <pc.get('amount_text', '')> - </for> + </for> - + - </if> + </if> - + - Total <invoice.report_incoterm or ''>Value in <invoice.currency.name> + Total <invoice.report_incoterm or ''>Value in <invoice.currency.name> - <format_currency(invoice.untaxed_amount, invoice.party.lang, invoice.currency)> + <format_currency(invoice.untaxed_amount, invoice.party.lang, invoice.currency)> - + - + - - Net weight: <format_number_symbol(invoice.report_net, invoice.party.lang, invoice.lines[0].unit, digits=4) if invoice.lines else ''> - Gross weight: <format_number_symbol(invoice.report_gross, invoice.party.lang, invoice.lines[0].unit, digits=4) if invoice.lines else ''> - - Packaging: <invoice.report_packaging or ''> + + Net weight: <format_number_symbol(invoice.report_net, invoice.party.lang, invoice.lines[0].unit, digits=4) if invoice.lines else ''> + Gross weight: <format_number_symbol(invoice.report_gross, invoice.party.lang, invoice.lines[0].unit, digits=4) if invoice.lines else ''> + + Packaging: <invoice.report_packaging or ''> </for> diff --git a/modules/purchase_trade/invoice.py b/modules/purchase_trade/invoice.py index baadb78..debe7a7 100644 --- a/modules/purchase_trade/invoice.py +++ b/modules/purchase_trade/invoice.py @@ -1404,23 +1404,11 @@ class Invoice(metaclass=PoolMeta): shipment = self._get_report_shipment() if not shipment: return '' - supplier = getattr(shipment, 'supplier', None) + if getattr(shipment, 'transport_type', None) == 'truck': + return 'By Truck' vessel = getattr(shipment, 'vessel', None) - supplier_name = ( - getattr(supplier, 'name', None) - or getattr(supplier, 'rec_name', None) - or '') vessel_name = getattr(vessel, 'vessel_name', None) or '' - note = getattr(shipment, 'note', None) or '' - - if supplier_name and vessel_name: - transport = f"BY {supplier_name} ({vessel_name})" - elif supplier_name: - transport = f"BY {supplier_name}" - else: - transport = vessel_name - - return ' '.join(part for part in [transport, note] if part).strip() + return vessel_name @property def report_bl_date(self): diff --git a/modules/purchase_trade/stock.py b/modules/purchase_trade/stock.py index e205967..1e130aa 100755 --- a/modules/purchase_trade/stock.py +++ b/modules/purchase_trade/stock.py @@ -684,7 +684,8 @@ class ShipmentIn(metaclass=PoolMeta): ('truck', 'Truck'), ('other', 'Other'), ], 'Transport type') - vessel = fields.Many2One('trade.vessel',"Vessel") + vessel = fields.Many2One('trade.vessel', "Vessel", + states={'invisible': Eval('transport_type') == 'truck'}) info = fields.Function(fields.Text("Info",states={'invisible': ~Eval('info',False)}),'get_info') anim = fields.Function(fields.Text(""),'get_anim') carte = fields.Function(fields.Text("",states={'invisible': ~Eval('info',False)}),'get_imo') @@ -725,12 +726,17 @@ class ShipmentIn(metaclass=PoolMeta): travel_nb = fields.Char("Travel nb") receive_date = fields.Date("Reception date") receive_nb = fields.Char("Reception nb") - cargo_mode = fields.Selection([ - ('bulk', 'Bulk'), - ('container', 'Container'), - ], 'Cargo Mode', required=True) - - vessel_type = fields.Function(fields.Many2One('stock.vessel.type',"Vessel type"),'get_vessel_type') + cargo_mode = fields.Selection([ + ('none', ''), + ('bulk', 'Bulk'), + ('container', 'Container'), + ], 'Cargo Mode', required=True, + states={'invisible': Eval('transport_type') == 'truck'}) + + vessel_type = fields.Function( + fields.Many2One('stock.vessel.type', "Vessel type", + states={'invisible': Eval('transport_type') == 'truck'}), + 'get_vessel_type') container = fields.One2Many( 'stock.shipment.container', 'shipment', @@ -760,6 +766,35 @@ class ShipmentIn(metaclass=PoolMeta): 'send': {}, }) + @staticmethod + def _normalize_transport_values(values, record=None): + values = dict(values) + transport_type = values.get( + 'transport_type', getattr(record, 'transport_type', None)) + if transport_type == 'truck': + values['vessel'] = None + values['cargo_mode'] = 'none' + elif values.get('cargo_mode') == 'none': + values['cargo_mode'] = 'bulk' + return values + + @classmethod + def create(cls, vlist): + return super().create([ + cls._normalize_transport_values(values) for values in vlist]) + + @classmethod + def write(cls, *args): + new_args = [] + for index in range(0, len(args), 2): + records = args[index] + values = args[index + 1] + for record in records: + new_args.extend([ + [record], + cls._normalize_transport_values(values, record)]) + super().write(*new_args) + @classmethod def _stock_move_origin(cls, move): return 'stock.move,%s' % move.id @@ -1693,9 +1728,21 @@ class ShipmentIn(metaclass=PoolMeta): def get_sh(self, name): return self.id - @classmethod - def default_transport_type(cls): - return 'vessel' + @classmethod + def default_transport_type(cls): + return 'vessel' + + @classmethod + def default_cargo_mode(cls): + return 'bulk' + + @fields.depends('transport_type', 'cargo_mode', 'vessel') + def on_change_transport_type(self): + if self.transport_type == 'truck': + self.vessel = None + self.cargo_mode = 'none' + elif self.cargo_mode == 'none': + self.cargo_mode = 'bulk' @classmethod def default_dashboard(cls): diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 64cfa0d..20b7fa3 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -5081,6 +5081,59 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertEqual(invoice.report_si_number, 'SI-B') self.assertEqual(invoice.report_si_reference, 'REF-B') + def test_shipment_in_truck_defaults_empty_cargo_mode(self): + 'truck shipment clears vessel data and uses empty cargo mode' + ShipmentIn = Pool().get('stock.shipment.in') + + shipment = ShipmentIn() + shipment.transport_type = 'truck' + shipment.vessel = Mock() + shipment.cargo_mode = 'bulk' + + shipment.on_change_transport_type() + + self.assertIsNone(shipment.vessel) + self.assertEqual(shipment.cargo_mode, 'none') + self.assertEqual( + ShipmentIn._normalize_transport_values({ + 'transport_type': 'truck', + 'cargo_mode': 'bulk', + 'vessel': 42, + }), + { + 'transport_type': 'truck', + 'cargo_mode': 'none', + 'vessel': None, + }) + + def test_invoice_report_transportation_uses_truck_label(self): + 'invoice_melya Transportation displays By Truck for truck shipments' + Invoice = Pool().get('account.invoice') + + invoice = Invoice() + invoice._get_report_shipment = Mock(return_value=Mock( + transport_type='truck', + supplier=Mock(rec_name='SUPPLIER'), + vessel=Mock(vessel_name='VESSEL A'), + note='ignored', + )) + + self.assertEqual(invoice.report_transportation, 'By Truck') + + def test_invoice_report_transportation_uses_vessel_name_only(self): + 'invoice_melya Transportation displays only vessel name for vessels' + Invoice = Pool().get('account.invoice') + + invoice = Invoice() + invoice._get_report_shipment = Mock(return_value=Mock( + transport_type='vessel', + supplier=Mock(rec_name='SUPPLIER'), + vessel=Mock(vessel_name='VESSEL A'), + note='ignored', + )) + + self.assertEqual(invoice.report_transportation, 'VESSEL A') + def test_invoice_report_shipment_is_blank_if_invoice_mixes_shipments(self): 'invoice shipment fields stay empty when multiple shipments are invoiced together' Invoice = Pool().get('account.invoice')