From b7b54c8b7b4bb349a1d0dab4a409091d7d6ab911 Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Fri, 26 Jun 2026 14:28:26 +0200 Subject: [PATCH] Invoice Melya template --- modules/purchase_trade/invoice.py | 77 ++++++++++++++++----- modules/purchase_trade/tests/test_module.py | 37 ++++++---- 2 files changed, 82 insertions(+), 32 deletions(-) diff --git a/modules/purchase_trade/invoice.py b/modules/purchase_trade/invoice.py index a7ea4c9..e8804a8 100644 --- a/modules/purchase_trade/invoice.py +++ b/modules/purchase_trade/invoice.py @@ -177,7 +177,7 @@ class Invoice(metaclass=PoolMeta): Snapshot = Pool().get('account.invoice.line.lot.weight') Lot = Pool().get('lot.lot') for line in self._get_report_invoice_lines(): - if (getattr(line, 'description', None) != 'Pro forma' + if (getattr(line, 'description', None) not in {'Pro forma', 'Final'} or Decimal(str(getattr(line, 'quantity', 0) or 0)) <= 0 or not getattr(line, 'lot', None)): continue @@ -961,6 +961,49 @@ class Invoice(metaclass=PoolMeta): def report_purchase_commission_total_display(self): return self._get_report_commission_total_display_for('purchase') + @staticmethod + def _same_report_invoice_line(left, right): + if not left or not right: + return False + left_id = getattr(left, 'id', None) + right_id = getattr(right, 'id', None) + if left_id is not None and right_id is not None: + return left_id == right_id + return left is right + + def _get_report_invoice_line_lots(self, invoice_line): + lots = [] + seen = set() + + def add_lot(lot): + if not lot or getattr(lot, 'lot_type', None) == 'virtual': + return + lot_id = getattr(lot, 'id', None) + key = lot_id if lot_id is not None else id(lot) + if key in seen: + return + seen.add(key) + lots.append(lot) + + add_lot(getattr(invoice_line, 'lot', None)) + trade = self._get_report_trade() + trade_lines = getattr(trade, 'lines', []) if trade else [] + for line in trade_lines or []: + for lot in getattr(line, 'lots', []) or []: + if getattr(lot, 'lot_type', None) != 'physic': + continue + refs = [ + getattr(lot, 'sale_invoice_line', None), + getattr(lot, 'sale_invoice_line_prov', None), + getattr(lot, 'invoice_line', None), + getattr(lot, 'invoice_line_prov', None), + ] + if any( + self._same_report_invoice_line(ref, invoice_line) + for ref in refs): + add_lot(lot) + return lots + @staticmethod def _get_report_related_lots(line): lots = [] @@ -1044,7 +1087,7 @@ class Invoice(metaclass=PoolMeta): net_total += padding gross_total += padding return net_total * sign, gross_total * sign - lots = self._get_report_preferred_lots(line) + lots = self._get_report_invoice_line_lots(line) if lots and self._report_invoice_line_reuses_lot(line): quantity = self._get_report_invoice_line_quantity_from_line(line) return quantity, quantity @@ -1093,7 +1136,11 @@ class Invoice(metaclass=PoolMeta): @staticmethod def _get_report_line_lot_keys(line): keys = [] - for lot in Invoice._get_report_preferred_lots(line): + invoice = getattr(line, 'invoice', None) + lots = ( + invoice._get_report_invoice_line_lots(line) + if invoice else Invoice._get_report_preferred_lots(line)) + for lot in lots: lot_id = getattr(lot, 'id', None) keys.append(lot_id if lot_id is not None else id(lot)) return tuple(sorted(keys)) @@ -1112,7 +1159,7 @@ class Invoice(metaclass=PoolMeta): def _get_report_reused_lot_lines(self): groups = {} for line in self._get_report_invoice_lines(): - lots = self._get_report_preferred_lots(line) + lots = self._get_report_invoice_line_lots(line) if not lots: continue for lot in lots: @@ -1237,7 +1284,10 @@ class Invoice(metaclass=PoolMeta): snapshots = Invoice._get_report_invoice_line_weight_snapshots(line) if snapshots and getattr(snapshots[0], 'unit', None): return snapshots[0].unit - lots = Invoice._get_report_preferred_lots(line) + invoice = getattr(line, 'invoice', None) + lots = ( + invoice._get_report_invoice_line_lots(line) + if invoice else Invoice._get_report_preferred_lots(line)) if lots and getattr(lots[0], 'lot_unit_line', None): return lots[0].lot_unit_line return getattr(line, 'unit', None) @@ -1290,13 +1340,13 @@ class Invoice(metaclass=PoolMeta): return text.upper() if text else '' def _get_report_purchase(self): - purchases = list(self.purchases or []) + purchases = list(getattr(self, 'purchases', None) or []) return purchases[0] if purchases else None def _get_report_sale(self): # Bridge invoice templates to the originating sale so FODT files can # reuse stable sale.report_* properties instead of complex expressions. - sales = list(self.sales or []) + sales = list(getattr(self, 'sales', None) or []) return sales[0] if sales else None def _get_report_trade(self): @@ -1347,7 +1397,7 @@ class Invoice(metaclass=PoolMeta): shipments = [] seen = set() for line in self._get_report_invoice_lines(): - for lot in self._get_report_preferred_lots(line): + for lot in self._get_report_invoice_line_lots(line): shipment = self._get_report_lot_shipment(lot) if not shipment: continue @@ -1367,15 +1417,6 @@ class Invoice(metaclass=PoolMeta): if not invoice_lines: return [] - def _same_invoice_line(left, right): - if not left or not right: - return False - left_id = getattr(left, 'id', None) - right_id = getattr(right, 'id', None) - if left_id is not None and right_id is not None: - return left_id == right_id - return left is right - trade = self._get_report_trade() trade_lines = getattr(trade, 'lines', []) if trade else [] lots = [] @@ -1390,7 +1431,7 @@ class Invoice(metaclass=PoolMeta): getattr(lot, 'invoice_line_prov', None), ] if any( - _same_invoice_line(ref, invoice_line) + self._same_report_invoice_line(ref, invoice_line) for ref in refs for invoice_line in invoice_lines): lots.append(lot) return lots diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index cf40d7d..7af826b 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -5,7 +5,7 @@ import datetime from contextlib import nullcontext from decimal import Decimal from types import SimpleNamespace -from unittest.mock import ANY, Mock, patch +from unittest.mock import ANY, Mock, call, patch from xml.etree import ElementTree from trytond.pool import Pool @@ -8257,8 +8257,8 @@ class PurchaseTradeTestCase(ModuleTestCase): invoice.report_quantity_lines, '910.00 LBS (910.00 LBS)') - def test_invoice_validation_creates_weight_snapshot_for_proforma_lines(self): - 'invoice validation snapshots lot weights for pro forma invoice lines' + def test_invoice_validation_creates_weight_snapshot_for_report_lines(self): + 'invoice validation snapshots lot weights for pro forma and final lines' Invoice = Pool().get('account.invoice') lot_ref = Mock(id=10) @@ -8293,8 +8293,9 @@ class PurchaseTradeTestCase(ModuleTestCase): invoice._create_lot_weight_snapshots() - snapshot_model.create_for_invoice_line.assert_called_once_with( - proforma, lot) + self.assertEqual( + snapshot_model.create_for_invoice_line.call_args_list, + [call(proforma, lot), call(final, lot)]) def test_invoice_report_lbs_converts_kilogram_to_lbs(self): 'invoice lbs helper converts kilogram quantities with the proper uom ratio' @@ -8429,8 +8430,8 @@ class PurchaseTradeTestCase(ModuleTestCase): '999990.0 KILOGRAM (2204597.95 LBS)', ]) - def test_invoice_report_weights_use_single_virtual_lot_when_no_physical(self): - 'invoice uses the unique virtual lot hist when no physical lot exists' + def test_invoice_report_weights_ignore_origin_virtual_lot(self): + 'invoice report falls back to invoice quantity instead of origin virtual lot' Invoice = Pool().get('account.invoice') virtual = Mock(id=1, lot_type='virtual', lot_unit_line=Mock(rec_name='LBS')) @@ -8448,13 +8449,14 @@ class PurchaseTradeTestCase(ModuleTestCase): ) invoice = Invoice() invoice.lines = [line] + line.invoice = invoice - self.assertEqual(invoice.report_net, Decimal('930')) - self.assertEqual(invoice.report_gross, Decimal('930')) - self.assertEqual(invoice.report_weight_unit_upper, 'LBS') + self.assertEqual(invoice.report_net, Decimal('1000')) + self.assertEqual(invoice.report_gross, Decimal('1000')) + self.assertEqual(invoice.report_weight_unit_upper, 'MT') - def test_invoice_report_weights_prefer_physical_lots_over_virtual(self): - 'invoice uses physical lot hist values whenever physical lots exist' + def test_invoice_report_weights_use_lot_linked_to_invoice_line(self): + 'invoice uses the physical lot explicitly linked to the invoice line' Invoice = Pool().get('account.invoice') virtual = Mock(id=1, lot_type='virtual', lot_unit_line=Mock(rec_name='LBS')) @@ -8467,16 +8469,23 @@ class PurchaseTradeTestCase(ModuleTestCase): Decimal('950'), Decimal('980'), ) - origin = Mock(lots=[virtual, physical]) + physical.sale_invoice_line = None line = Mock( + id=100, type='line', quantity=Decimal('1000'), lot=virtual, - origin=origin, unit=Mock(rec_name='MT'), ) + physical.sale_invoice_line_prov = line + physical.invoice_line = None + physical.invoice_line_prov = None + origin = Mock(lots=[virtual, physical]) + line.origin = origin invoice = Invoice() invoice.lines = [line] + invoice.sales = [Mock(lines=[origin])] + line.invoice = invoice self.assertEqual(invoice.report_net, Decimal('950')) self.assertEqual(invoice.report_gross, Decimal('980'))