From 5a644d430b785365cea28a383ef8ce7733aec55e Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Tue, 14 Jul 2026 09:54:00 +0200 Subject: [PATCH 1/8] Refine linkage report spacing --- modules/purchase_trade/stock.py | 14 ++++++++------ modules/stock/linkage.fodt | 31 ++++++++++++++++++------------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/modules/purchase_trade/stock.py b/modules/purchase_trade/stock.py index 4bcd10f..8df4610 100755 --- a/modules/purchase_trade/stock.py +++ b/modules/purchase_trade/stock.py @@ -1732,9 +1732,11 @@ class ShipmentIn(metaclass=PoolMeta): rows = [] if purchase_lines: - rows.append(('Purchases', '', purchase_total, purchase_total)) + rows.append(( + 'Purchases', '', purchase_total, purchase_total, + 'major')) if sale_lines: - rows.append(('Sales', '', sale_total, sale_total)) + rows.append(('Sales', '', sale_total, sale_total, 'major')) fee_total = Decimal('0') fee_rows = {} @@ -1744,12 +1746,12 @@ class ShipmentIn(metaclass=PoolMeta): fee_total += amount fee_rows[label] = fee_rows.get(label, Decimal('0')) + amount for label, amount in fee_rows.items(): - rows.append(('Costs', label, amount, amount)) + rows.append(('Costs', label, amount, amount, 'cost')) if rows: - rows.append(('Costs', 'Total', fee_total, fee_total)) + rows.append(('Costs', 'Total', fee_total, fee_total, 'cost')) pnl = purchase_total + sale_total + fee_total - rows.append(('P&L', '', pnl, pnl)) + rows.append(('P&L', '', pnl, pnl, 'pnl')) return rows def _get_report_linkage_movement_rows(self): @@ -2013,7 +2015,7 @@ class ShipmentIn(metaclass=PoolMeta): def report_linkage_summary_rows(self): return self._report_table_rows( self._get_report_linkage_summary_rows(), - ('group', 'cost_type', 'estimated', 'validated'), + ('group', 'cost_type', 'estimated', 'validated', 'kind'), {'estimated', 'validated'}) @property diff --git a/modules/stock/linkage.fodt b/modules/stock/linkage.fodt index 8e84c77..f817030 100644 --- a/modules/stock/linkage.fodt +++ b/modules/stock/linkage.fodt @@ -1184,30 +1184,31 @@ - - - - - + + + + + - - + + - + - + - - + + + @@ -1432,7 +1433,12 @@ GroupCost TypeEstimatedValidated <for each="row in shipment.report_linkage_summary_rows"> +<if test="row['kind'] == 'pnl'"> +<row['group']><row['cost_type']><row['estimated']><row['validated']> +</if> +<if test="row['kind'] != 'pnl'"> <row['group']><row['cost_type']><row['estimated']><row['validated']> +</if> </for> @@ -1684,8 +1690,7 @@ -OliverGyslerJeremieCOLLOTStephaneMonnard -SteveZaccarini +OliverGyslerJeremieCOLLOTStephaneMonnardSteveZaccarini </for> From e4590e61775c8b9cf0975e934d616f6baa5831a0 Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Tue, 14 Jul 2026 10:41:50 +0200 Subject: [PATCH 2/8] Polish linkage report table spacing --- modules/purchase_trade/stock.py | 14 ++++++---- modules/purchase_trade/tests/test_module.py | 2 +- modules/stock/linkage.fodt | 31 +++++++++++++++------ 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/modules/purchase_trade/stock.py b/modules/purchase_trade/stock.py index 8df4610..c6053f6 100755 --- a/modules/purchase_trade/stock.py +++ b/modules/purchase_trade/stock.py @@ -1721,6 +1721,8 @@ class ShipmentIn(metaclass=PoolMeta): getattr(product, 'name', None) or cls._report_rec_name(product) or 'Fee') + if label.startswith('[') and '] ' in label: + label = label.split('] ', 1)[1] return label def _get_report_linkage_summary_rows(self): @@ -1844,10 +1846,10 @@ class ShipmentIn(metaclass=PoolMeta): -self._report_line_amount(line))) for fee in self._get_report_linkage_fees(): rows.append(self._get_report_linkage_fee_detail_row(fee)) - total = sum((row[-1] for row in rows), Decimal('0')) + total = sum((row[7] for row in rows), Decimal('0')) if rows: - rows.append(('A', 'Total', '', '', '', '', total, total)) - rows.append(('P&L', '', '', '', '', '', total, total)) + rows.append(('A', 'Total', '', '', '', '', total, total, 'total')) + rows.append(('P&L', '', '', '', '', '', total, total, 'pnl')) return rows def _get_report_linkage_detail_row(self, group, label, trade, line, amount): @@ -1867,6 +1869,7 @@ class ShipmentIn(metaclass=PoolMeta): quantity, amount, amount, + 'line', ) def _get_report_linkage_fee_detail_row(self, fee): @@ -1888,7 +1891,7 @@ class ShipmentIn(metaclass=PoolMeta): quantity = self._report_line_quantity(trade_line) return ( 'A', - self._report_rec_name(product) or 'Fee', + self._report_linkage_fee_label(fee), self._report_rec_name(getattr(fee, 'supplier', None)), self._report_number(trade), ' '.join(part for part in [ @@ -1898,6 +1901,7 @@ class ShipmentIn(metaclass=PoolMeta): self._format_report_quantity(quantity or 0), amount, amount, + 'fee', ) @classmethod @@ -2168,7 +2172,7 @@ class ShipmentIn(metaclass=PoolMeta): return self._report_table_rows( self._get_report_linkage_detail_rows(), ('group', 'cost_type', 'counterpart', 'source', 'unit_price', - 'quantity', 'estimated', 'validated'), + 'quantity', 'estimated', 'validated', 'kind'), {'estimated', 'validated'}) def get_rec_name(self, name=None): diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index c80c34a..d2e287f 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -6669,7 +6669,7 @@ description shipment.bl_date = datetime.date(2026, 4, 30) freight_product = SimpleNamespace( rec_name='[Maritime Freight] Maritime freight', - name='Maritime freight') + name='[Maritime Freight] Maritime freight') shipment.fees = [ SimpleNamespace( id=201, product=freight_product, price=Decimal('10'), diff --git a/modules/stock/linkage.fodt b/modules/stock/linkage.fodt index f817030..b9c0161 100644 --- a/modules/stock/linkage.fodt +++ b/modules/stock/linkage.fodt @@ -1147,8 +1147,10 @@ + + @@ -1184,26 +1186,29 @@ - - + + + - - + + - + + + @@ -1227,7 +1232,7 @@ <shipment.report_linkage_title or ''> -/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoM +/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoM DAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsN FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAAR CAAyAqADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAA @@ -1436,14 +1441,17 @@ <if test="row['kind'] == 'pnl'"> <row['group']><row['cost_type']><row['estimated']><row['validated']> </if> -<if test="row['kind'] != 'pnl'"> +<if test="row['kind'] == 'cost'"> +<row['group']><row['cost_type']><row['estimated']><row['validated']> +</if> +<if test="row['kind'] != 'pnl' and row['kind'] != 'cost'"> <row['group']><row['cost_type']><row['estimated']><row['validated']> </if> </for> - + 3- Loading and Discharge movements @@ -1478,7 +1486,7 @@ <shipment.report_linkage_title or ''> -/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoM +/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoM DAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsN FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAAR CAAyAqADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAA @@ -1684,7 +1692,12 @@ GroupCost TypeCounterpartSourceUnit PriceQuantityEstimatedValidated <for each="row in shipment.report_linkage_detail_rows"> +<if test="row['kind'] == 'pnl'"> +<row['group']><row['cost_type']><row['counterpart']><row['source']><row['unit_price']><row['quantity']><row['estimated']><row['validated']> +</if> +<if test="row['kind'] != 'pnl'"> <row['group']><row['cost_type']><row['counterpart']><row['source']><row['unit_price']><row['quantity']><row['estimated']><row['validated']> +</if> </for> From 7fc134f7a62b2b36423a0eae45cfe6115d0a17e9 Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Tue, 14 Jul 2026 11:26:18 +0200 Subject: [PATCH 3/8] Bug lot.qt --- modules/purchase_trade/stock.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/purchase_trade/stock.py b/modules/purchase_trade/stock.py index c6053f6..cff242b 100755 --- a/modules/purchase_trade/stock.py +++ b/modules/purchase_trade/stock.py @@ -27,6 +27,7 @@ import re import html from trytond.exceptions import UserError from trytond.modules.stock.shipment import SupplierShipping as BaseSupplierShipping +from . import lot as purchase_trade_lot logger = logging.getLogger(__name__) @@ -2898,7 +2899,7 @@ class FindVessel(Wizard): return action, {} -class LotQt(metaclass=PoolMeta): +class LotQt(purchase_trade_lot.LotQt): __name__ = 'lot.qt' @staticmethod From 07ee8b4f99aa2519a8ef01065da82e273c28aa35 Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Tue, 14 Jul 2026 11:39:39 +0200 Subject: [PATCH 4/8] Bug lot.qt --- modules/purchase_trade/__init__.py | 6 ++++-- modules/purchase_trade/stock.py | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/modules/purchase_trade/__init__.py b/modules/purchase_trade/__init__.py index f52d2fe..e11277b 100755 --- a/modules/purchase_trade/__init__.py +++ b/modules/purchase_trade/__init__.py @@ -207,10 +207,12 @@ def register(): stock.AccountMoveLine, stock.AccountMove, stock.ContainerType, - stock.ShipmentContainer, + stock.ShipmentContainer, lot.Lot, lot.LotQt, - stock.LotQt, + # Temporarily disabled: added by 1b292e4 for linkage reports, but it + # may leave lot.qt without the base ModelSQL fields in some pools. + # stock.LotQt, lot.LotReport, lot.LotContext, lot.LotShippingStart, diff --git a/modules/purchase_trade/stock.py b/modules/purchase_trade/stock.py index cff242b..c6053f6 100755 --- a/modules/purchase_trade/stock.py +++ b/modules/purchase_trade/stock.py @@ -27,7 +27,6 @@ import re import html from trytond.exceptions import UserError from trytond.modules.stock.shipment import SupplierShipping as BaseSupplierShipping -from . import lot as purchase_trade_lot logger = logging.getLogger(__name__) @@ -2899,7 +2898,7 @@ class FindVessel(Wizard): return action, {} -class LotQt(purchase_trade_lot.LotQt): +class LotQt(metaclass=PoolMeta): __name__ = 'lot.qt' @staticmethod From cf4bd94d0328f12f69085085e885c90fa4b2089b Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Tue, 14 Jul 2026 12:02:07 +0200 Subject: [PATCH 5/8] Bug lot.qt --- modules/purchase_trade/__init__.py | 10 +- modules/purchase_trade/stock.py | 214 +------------------- modules/purchase_trade/tests/test_module.py | 80 +------- 3 files changed, 8 insertions(+), 296 deletions(-) diff --git a/modules/purchase_trade/__init__.py b/modules/purchase_trade/__init__.py index e11277b..d13a854 100755 --- a/modules/purchase_trade/__init__.py +++ b/modules/purchase_trade/__init__.py @@ -204,15 +204,12 @@ def register(): stock.ImportSoFStart, stock.RevaluateStart, stock.Account, - stock.AccountMoveLine, - stock.AccountMove, - stock.ContainerType, + stock.AccountMoveLine, + stock.AccountMove, + stock.ContainerType, stock.ShipmentContainer, lot.Lot, lot.LotQt, - # Temporarily disabled: added by 1b292e4 for linkage reports, but it - # may leave lot.qt without the base ModelSQL fields in some pools. - # stock.LotQt, lot.LotReport, lot.LotContext, lot.LotShippingStart, @@ -368,7 +365,6 @@ def register(): stock.ShipmentCOOReport, stock.ShipmentPackingListReport, stock.ShipmentLinkageReport, - stock.LotQtLinkageReport, stock.LotReportLinkageReport, module='purchase_trade', type_='report') diff --git a/modules/purchase_trade/stock.py b/modules/purchase_trade/stock.py index c6053f6..ffd8929 100755 --- a/modules/purchase_trade/stock.py +++ b/modules/purchase_trade/stock.py @@ -2898,214 +2898,6 @@ class FindVessel(Wizard): return action, {} -class LotQt(metaclass=PoolMeta): - __name__ = 'lot.qt' - - @staticmethod - def _report_record_key(record): - return ShipmentIn._report_record_key(record) - - @classmethod - def _report_unique_records(cls, records): - return ShipmentIn._report_unique_records(records) - - @staticmethod - def _report_linkage_lot_purchase_line(lot): - return ShipmentIn._report_linkage_lot_purchase_line(lot) - - @staticmethod - def _report_linkage_lot_sale_line(lot): - return ShipmentIn._report_linkage_lot_sale_line(lot) - - @staticmethod - def _report_rec_name(record): - return ShipmentIn._report_rec_name(record) - - @classmethod - def _report_number(cls, record): - return ShipmentIn._report_number(record) - - @staticmethod - def _report_date(value): - return ShipmentIn._report_date(value) - - @staticmethod - def _report_amount(value): - return ShipmentIn._report_amount(value) - - @staticmethod - def _report_price(value): - return ShipmentIn._report_price(value) - - @classmethod - def _report_currency_code(cls, record): - return ShipmentIn._report_currency_code(record) - - @classmethod - def _report_unit_symbol(cls, line): - return ShipmentIn._report_unit_symbol(line) - - @classmethod - def _report_line_quantity(cls, line): - return ShipmentIn._report_line_quantity(line) - - @classmethod - def _report_line_amount(cls, line): - return ShipmentIn._report_line_amount(line) - - @classmethod - def _report_fee_amount(cls, fee): - return ShipmentIn._report_fee_amount(fee) - - @classmethod - def _report_linkage_fee_label(cls, fee): - return ShipmentIn._report_linkage_fee_label(fee) - - @classmethod - def _report_column(cls, rows, index, amount=False): - return ShipmentIn._report_column(rows, index, amount=amount) - - @classmethod - def _report_table_rows(cls, rows, names, amount_names=()): - return ShipmentIn._report_table_rows(rows, names, amount_names) - - @staticmethod - def _format_report_quantity(value, digits='0.001'): - return ShipmentIn._format_report_quantity(value, digits=digits) - - @staticmethod - def _get_report_unit_text(unit): - return ShipmentIn._get_report_unit_text(unit) - - def _get_report_primary_move(self): - return getattr(self, 'lot_move', None) - - def _get_report_linkage_lots(self): - return self._report_unique_records(( - getattr(self, 'lot_p', None), - getattr(self, 'lot_s', None), - )) - - _get_report_linkage_purchase_lines = ( - ShipmentIn._get_report_linkage_purchase_lines) - _get_report_linkage_sale_lines = ShipmentIn._get_report_linkage_sale_lines - _get_report_linkage_purchase = ShipmentIn._get_report_linkage_purchase - _get_report_linkage_sale = ShipmentIn._get_report_linkage_sale - _get_report_linkage_product = ShipmentIn._get_report_linkage_product - - def _get_report_linkage_fees(self): - fees = [] - seen = set() - - def add_fee(fee): - if not fee: - return - key = self._report_record_key(fee) - if key in seen: - return - seen.add(key) - fees.append(fee) - - shipment = getattr(self, 'lot_shipment_in', None) - for fee in getattr(shipment, 'fees', []) or []: - add_fee(fee) - for line in ( - self._get_report_linkage_purchase_lines() - + self._get_report_linkage_sale_lines()): - for fee in getattr(line, 'fees', []) or []: - add_fee(fee) - return fees - - _get_report_linkage_summary_rows = ( - ShipmentIn._get_report_linkage_summary_rows) - _get_report_linkage_movement_rows = ( - ShipmentIn._get_report_linkage_movement_rows) - _get_report_linkage_movement_row = ( - ShipmentIn._get_report_linkage_movement_row) - _get_report_linkage_pricing_rows = ( - ShipmentIn._get_report_linkage_pricing_rows) - _get_report_linkage_detail_rows = ( - ShipmentIn._get_report_linkage_detail_rows) - _get_report_linkage_detail_row = ( - ShipmentIn._get_report_linkage_detail_row) - _get_report_linkage_fee_detail_row = ( - ShipmentIn._get_report_linkage_fee_detail_row) - - report_linkage_title = ShipmentIn.report_linkage_title - report_linkage_desk = ShipmentIn.report_linkage_desk - report_linkage_book = ShipmentIn.report_linkage_book - report_linkage_strategy = ShipmentIn.report_linkage_strategy - - @property - def report_linkage_bl_date(self): - shipment = getattr(self, 'lot_shipment_in', None) - bl_date = getattr(shipment, 'bl_date', None) - if not bl_date: - move = getattr(self, 'lot_move', None) - bl_date = getattr(move, 'bldate', None) - return bl_date and bl_date.strftime('%A, %B %d, %Y') or '' - - report_linkage_trading_unit = ShipmentIn.report_linkage_trading_unit - report_linkage_finance_user = ShipmentIn.report_linkage_finance_user - report_linkage_summary_groups = ShipmentIn.report_linkage_summary_groups - report_linkage_summary_cost_types = ( - ShipmentIn.report_linkage_summary_cost_types) - report_linkage_summary_estimated = ( - ShipmentIn.report_linkage_summary_estimated) - report_linkage_summary_validated = ( - ShipmentIn.report_linkage_summary_validated) - report_linkage_summary_rows = ShipmentIn.report_linkage_summary_rows - report_linkage_movement_types = ShipmentIn.report_linkage_movement_types - report_linkage_movement_references = ( - ShipmentIn.report_linkage_movement_references) - report_linkage_movement_counterparts = ( - ShipmentIn.report_linkage_movement_counterparts) - report_linkage_movement_commodities = ( - ShipmentIn.report_linkage_movement_commodities) - report_linkage_movement_quantities = ( - ShipmentIn.report_linkage_movement_quantities) - report_linkage_movement_deliveries = ( - ShipmentIn.report_linkage_movement_deliveries) - report_linkage_movement_basis = ShipmentIn.report_linkage_movement_basis - report_linkage_movement_periods = ( - ShipmentIn.report_linkage_movement_periods) - report_linkage_movement_from_dates = ( - ShipmentIn.report_linkage_movement_from_dates) - report_linkage_movement_to_dates = ( - ShipmentIn.report_linkage_movement_to_dates) - report_linkage_movement_rows = ShipmentIn.report_linkage_movement_rows - report_linkage_bank_deal = ShipmentIn.report_linkage_bank_deal - report_linkage_bank_type = ShipmentIn.report_linkage_bank_type - report_linkage_bank_name = ShipmentIn.report_linkage_bank_name - report_linkage_lc_type = ShipmentIn.report_linkage_lc_type - report_linkage_lc_number = ShipmentIn.report_linkage_lc_number - report_linkage_lc_amount = ShipmentIn.report_linkage_lc_amount - report_linkage_lc_expiry_date = ShipmentIn.report_linkage_lc_expiry_date - report_linkage_pricing_deals = ShipmentIn.report_linkage_pricing_deals - report_linkage_pricing_sides = ShipmentIn.report_linkage_pricing_sides - report_linkage_pricing_types = ShipmentIn.report_linkage_pricing_types - report_linkage_pricing_symbols = ShipmentIn.report_linkage_pricing_symbols - report_linkage_pricing_periods = ShipmentIn.report_linkage_pricing_periods - report_linkage_pricing_input_prices = ( - ShipmentIn.report_linkage_pricing_input_prices) - report_linkage_pricing_rows = ShipmentIn.report_linkage_pricing_rows - report_linkage_detail_groups = ShipmentIn.report_linkage_detail_groups - report_linkage_detail_cost_types = ( - ShipmentIn.report_linkage_detail_cost_types) - report_linkage_detail_counterparts = ( - ShipmentIn.report_linkage_detail_counterparts) - report_linkage_detail_sources = ShipmentIn.report_linkage_detail_sources - report_linkage_detail_unit_prices = ( - ShipmentIn.report_linkage_detail_unit_prices) - report_linkage_detail_quantities = ( - ShipmentIn.report_linkage_detail_quantities) - report_linkage_detail_estimated = ( - ShipmentIn.report_linkage_detail_estimated) - report_linkage_detail_validated = ( - ShipmentIn.report_linkage_detail_validated) - report_linkage_detail_rows = ShipmentIn.report_linkage_detail_rows - - class ShipmentOut(metaclass=PoolMeta): __name__ = 'stock.shipment.out' @@ -4639,9 +4431,7 @@ class ShipmentLinkageReport(ShipmentTemplateReportMixin, BaseSupplierShipping): 'shipment_linkage_report_template', 'stock') -class LotQtLinkageReport(ShipmentTemplateReportMixin, BaseSupplierShipping): - __name__ = 'lot.qt.linkage' - +class LinkageTemplateReport(ShipmentTemplateReportMixin, BaseSupplierShipping): @classmethod def _resolve_configured_report_path(cls, action): return cls._resolve_template_path( @@ -4858,7 +4648,7 @@ class LotReportLinkageRecord: report_linkage_detail_rows = ShipmentIn.report_linkage_detail_rows -class LotReportLinkageReport(LotQtLinkageReport): +class LotReportLinkageReport(LinkageTemplateReport): __name__ = 'lot.report.linkage' @classmethod diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index d2e287f..d6fc592 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -6764,83 +6764,9 @@ description self.assertIn('80.000 USD/MT', shipment.report_linkage_pricing_input_prices) self.assertIn('100.000 USD/MT', shipment.report_linkage_pricing_input_prices) - def test_lotqt_linkage_helpers_use_single_matching(self): - 'lot.qt linkage helpers expose only the selected matching' - LotQt = Pool().get('lot.qt') - lotqt = LotQt() - - currency = SimpleNamespace(code='USD') - unit = SimpleNamespace(symbol='MT') - product = SimpleNamespace( - rec_name='Sulphuric Acid', - name='Sulphuric Acid', - code='H2SO4') - purchase = SimpleNamespace( - id=1, - number='P-10', - reference='26.0001', - currency=currency, - party=SimpleNamespace(rec_name='SUPPLIER SA'), - to_location=SimpleNamespace(rec_name='Odda'), - incoterm=SimpleNamespace(code='FOB'), - operator=SimpleNamespace(rec_name='Operator A')) - sale = SimpleNamespace( - id=2, - number='S-20', - reference='26.0001', - currency=currency, - party=SimpleNamespace(rec_name='CLIENT SA'), - to_location=SimpleNamespace(rec_name='Mejillones'), - incoterm=SimpleNamespace(code='CFR')) - period = SimpleNamespace(rec_name='Chile FY26') - purchase_line = SimpleNamespace( - id=11, - product=product, - purchase=purchase, - quantity_theorical=Decimal('100'), - quantity=Decimal('100'), - unit_price=Decimal('80'), - unit=unit, - price_type='fixed', - del_period=period, - period_at='laycan', - from_del=datetime.date(2026, 4, 30), - to_del=datetime.date(2026, 5, 8), - fees=[], - mtm=[]) - sale_line = SimpleNamespace( - id=12, - product=product, - sale=sale, - quantity_theorical=Decimal('100'), - quantity=Decimal('100'), - unit_price=Decimal('100'), - unit=unit, - price_type='fixed', - del_period=period, - period_at='laycan', - from_del=datetime.date(2026, 4, 30), - to_del=datetime.date(2026, 4, 30), - fees=[], - mtm=[]) - lotqt.lot_p = SimpleNamespace(id=101, line=purchase_line) - lotqt.lot_s = SimpleNamespace(id=102, sale_line=sale_line) - lotqt.lot_shipment_in = SimpleNamespace( - bl_date=datetime.date(2026, 4, 30), - fees=[]) - - self.assertEqual( - lotqt.report_linkage_title, 'Linkage P-10/S-20 26.0001') - self.assertEqual(lotqt.report_linkage_desk, 'Sulphuric Acid') - self.assertEqual(lotqt.report_linkage_book, 'H2SO4 Chile FY26') - self.assertEqual( - lotqt.report_linkage_bl_date, 'Thursday, April 30, 2026') - self.assertIn('Purchases', lotqt.report_linkage_summary_groups) - self.assertIn('Sales', lotqt.report_linkage_summary_groups) - self.assertIn('P-10', lotqt.report_linkage_movement_references) - self.assertIn('S-20', lotqt.report_linkage_movement_references) - self.assertIn('80.000 USD/MT', lotqt.report_linkage_pricing_input_prices) - self.assertIn('100.000 USD/MT', lotqt.report_linkage_pricing_input_prices) + def test_linkage_report_does_not_extend_lotqt_model(self): + 'linkage report must not alter the lot.qt ORM model' + self.assertFalse(hasattr(stock_module, 'LotQt')) def test_lot_report_linkage_rejects_non_physical_lines(self): 'lot.report linkage report is only available for physical lines' From ac3198ee703028952d82ddffd890bd50ad57bef1 Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Tue, 14 Jul 2026 13:29:44 +0200 Subject: [PATCH 6/8] Bug lot.qt --- modules/purchase_trade/lot.py | 20 +++++---- modules/purchase_trade/tests/test_module.py | 46 ++++++++++++++++++++- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/modules/purchase_trade/lot.py b/modules/purchase_trade/lot.py index ab48007..1492f5f 100755 --- a/modules/purchase_trade/lot.py +++ b/modules/purchase_trade/lot.py @@ -4233,7 +4233,7 @@ class LotUnship(Wizard): lot = Lot(r.id) if lqt and r.r_lot_type == 'virtual': shipment = None - qt = lqt.lot_quantity + qt = Decimal(str(lqt.lot_quantity or 0)) mode = 'both' if not lqt.lot_p: lot = lqt.lot_s @@ -4246,13 +4246,17 @@ class LotUnship(Wizard): shipment = lqt.lot_shipment_internal elif lqt.lot_shipment_out: shipment = lqt.lot_shipment_out - #Decrease forecasted virtual part shipped - lot.updateVirtualPart(-qt,lqt.lot_shipment_origin,lqt.lot_s,mode) - #Increase forecasted virtual part non shipped - if lqt.is_planned_transport(): - LotQt.restore_planned_open_quantity(lqt, qt) - elif not lot.updateVirtualPart(qt,None,lqt.lot_s,mode): - lot.createVirtualPart(qt,None,lqt.lot_s,mode) + if not shipment: + continue + # Restore the selected scheduled quantity only once, then + # remove its shipment-linked lot.qt so it cannot be + # unlinked again from the shipment planned tab. + if qt: + if lqt.is_planned_transport(): + LotQt.restore_planned_open_quantity(lqt, qt) + elif not lot.updateVirtualPart(qt,None,lqt.lot_s,mode): + lot.createVirtualPart(qt,None,lqt.lot_s,mode) + LotQt.delete([lqt]) if lqt.lot_p and lqt.lot_p.line: affected_lines.append(lqt.lot_p.line) if lqt.lot_s and lqt.lot_s.sale_line: diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index d6fc592..35c5e33 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -704,6 +704,9 @@ class PurchaseTradeTestCase(ModuleTestCase): lot_quantity=Decimal('40'), lot_p=lot, lot_s=sale_lot, + lot_shipment_in=Mock(), + lot_shipment_internal=None, + lot_shipment_out=None, lot_shipment_origin='stock.shipment.in,12', ) lqt.is_planned_transport.return_value = True @@ -722,14 +725,53 @@ class PurchaseTradeTestCase(ModuleTestCase): state = wizard.transition_start() self.assertEqual(state, 'end') - lot.updateVirtualPart.assert_called_once_with( - Decimal('-40'), 'stock.shipment.in,12', sale_lot, 'both') + lot.updateVirtualPart.assert_not_called() LotQt.restore_planned_open_quantity.assert_called_once_with( lqt, Decimal('40')) + LotQt.delete.assert_called_once_with([lqt]) lot.createVirtualPart.assert_not_called() Lot.assert_lines_quantity_consistency.assert_called_once_with([ purchase_line, sale_line]) + def test_lot_unship_deletes_zero_scheduled_lotqt_without_restore(self): + 'unlinking a residual zero scheduled lot.qt only cleans the shipment tab' + wizard = lot_module.LotUnship() + wizard.records = [Mock(id=10000007, r_lot_type='virtual')] + purchase_line = Mock() + sale_line = Mock() + lot = Mock(line=purchase_line) + sale_lot = Mock(sale_line=sale_line) + lqt = Mock( + lot_quantity=Decimal('0'), + lot_p=lot, + lot_s=sale_lot, + lot_shipment_in=Mock(), + lot_shipment_internal=None, + lot_shipment_out=None, + ) + lqt.is_planned_transport.return_value = True + Lot = Mock() + Lot.return_value = Mock() + Lot.skip_quantity_consistency.return_value.__enter__ = Mock() + Lot.skip_quantity_consistency.return_value.__exit__ = Mock() + LotQt = Mock() + LotQt.return_value = lqt + Move = Mock() + + pool = Mock() + pool.get.side_effect = [Lot, LotQt, Move] + with patch('trytond.modules.purchase_trade.lot.Pool', + return_value=pool): + state = wizard.transition_start() + + self.assertEqual(state, 'end') + LotQt.restore_planned_open_quantity.assert_not_called() + lot.updateVirtualPart.assert_not_called() + lot.createVirtualPart.assert_not_called() + LotQt.delete.assert_called_once_with([lqt]) + Lot.assert_lines_quantity_consistency.assert_called_once_with([ + purchase_line, sale_line]) + def test_add_physical_lots_requires_linked_transport(self): 'physical lots can only be added from a scheduled quantity' LotQt = Pool().get('lot.qt') From eb38977dca5dddc7ced6a0ba85ab409462114ab9 Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Tue, 14 Jul 2026 13:47:40 +0200 Subject: [PATCH 7/8] Bug lot.qt --- modules/purchase_trade/lot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/purchase_trade/lot.py b/modules/purchase_trade/lot.py index 1492f5f..19a9ce8 100755 --- a/modules/purchase_trade/lot.py +++ b/modules/purchase_trade/lot.py @@ -4248,6 +4248,10 @@ class LotUnship(Wizard): shipment = lqt.lot_shipment_out if not shipment: continue + if lqt.lot_p and lqt.lot_p.line: + affected_lines.append(lqt.lot_p.line) + if lqt.lot_s and lqt.lot_s.sale_line: + affected_lines.append(lqt.lot_s.sale_line) # Restore the selected scheduled quantity only once, then # remove its shipment-linked lot.qt so it cannot be # unlinked again from the shipment planned tab. @@ -4257,10 +4261,6 @@ class LotUnship(Wizard): elif not lot.updateVirtualPart(qt,None,lqt.lot_s,mode): lot.createVirtualPart(qt,None,lqt.lot_s,mode) LotQt.delete([lqt]) - if lqt.lot_p and lqt.lot_p.line: - affected_lines.append(lqt.lot_p.line) - if lqt.lot_s and lqt.lot_s.sale_line: - affected_lines.append(lqt.lot_s.sale_line) if lot and r.r_lot_type == 'physic': lot = Lot(r.r_lot_p) if lot.lot_type == 'physic': From 3dfbb252810c0165542106c4069eb0e33805eb5e Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Tue, 14 Jul 2026 15:30:04 +0200 Subject: [PATCH 8/8] Certification --- modules/purchase_trade/view/purchase_form.xml | 3 ++- modules/purchase_trade/view/sale_form.xml | 3 ++- modules/stock/.~lock.linkage.fodt# | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 modules/stock/.~lock.linkage.fodt# diff --git a/modules/purchase_trade/view/purchase_form.xml b/modules/purchase_trade/view/purchase_form.xml index 66717fb..ff24cba 100755 --- a/modules/purchase_trade/view/purchase_form.xml +++ b/modules/purchase_trade/view/purchase_form.xml @@ -53,8 +53,9 @@ this repository contains the full copyright notices and license terms. -->