diff --git a/modules/purchase_trade/__init__.py b/modules/purchase_trade/__init__.py
index b4be175..c003d0e 100755
--- a/modules/purchase_trade/__init__.py
+++ b/modules/purchase_trade/__init__.py
@@ -206,8 +206,9 @@ def register():
stock.AccountMove,
stock.ContainerType,
stock.ShipmentContainer,
- lot.Lot,
- lot.LotQt,
+ lot.Lot,
+ lot.LotQt,
+ stock.LotQt,
lot.LotReport,
lot.LotContext,
lot.LotShippingStart,
@@ -363,5 +364,6 @@ def register():
stock.ShipmentCOOReport,
stock.ShipmentPackingListReport,
stock.ShipmentLinkageReport,
+ stock.LotQtLinkageReport,
module='purchase_trade', type_='report')
diff --git a/modules/purchase_trade/stock.py b/modules/purchase_trade/stock.py
index aef9619..a00ed62 100755
--- a/modules/purchase_trade/stock.py
+++ b/modules/purchase_trade/stock.py
@@ -1927,6 +1927,13 @@ class ShipmentIn(metaclass=PoolMeta):
@property
def report_linkage_trading_unit(self):
+ purchase = self._get_report_linkage_purchase()
+ company = getattr(purchase, 'company', None) if purchase else None
+ if not company:
+ company = getattr(self, 'company', None)
+ party = getattr(company, 'party', None) if company else None
+ if party:
+ return self._report_rec_name(party)
sale = self._get_report_linkage_sale()
party = getattr(sale, 'party', None) if sale else None
return self._report_rec_name(party)
@@ -2827,17 +2834,222 @@ class ShipmentIn(metaclass=PoolMeta):
]
)
-class FindVessel(Wizard):
- __name__ = 'stock.shipment.in.vf'
- start_state = 'vf'
- vf = StateAction('purchase_trade.url_vessel_finder')
-
- def do_vf(self, action):
- action['url'] = self.record.get_vessel_url()
- return action, {}
-
-class ShipmentOut(metaclass=PoolMeta):
- __name__ = 'stock.shipment.out'
+class FindVessel(Wizard):
+ __name__ = 'stock.shipment.in.vf'
+ start_state = 'vf'
+ vf = StateAction('purchase_trade.url_vessel_finder')
+
+ def do_vf(self, action):
+ action['url'] = self.record.get_vessel_url()
+ 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_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'
from_location = fields.Many2One('stock.location', 'From location')
to_location = fields.Many2One('stock.location', 'To location')
@@ -4249,6 +4461,15 @@ class ShipmentLinkageReport(ShipmentTemplateReportMixin, BaseSupplierShipping):
'shipment_linkage_report_template', 'stock')
+class LotQtLinkageReport(ShipmentTemplateReportMixin, BaseSupplierShipping):
+ __name__ = 'lot.qt.linkage'
+
+ @classmethod
+ def _resolve_configured_report_path(cls, action):
+ return cls._resolve_template_path(
+ 'shipment_linkage_report_template', 'stock')
+
+
class ShipmentCOOReport(ShipmentTemplateReportMixin, BaseSupplierShipping):
__name__ = 'stock.shipment.in.coo'
diff --git a/modules/purchase_trade/stock.xml b/modules/purchase_trade/stock.xml
index 29cab59..ab0953f 100755
--- a/modules/purchase_trade/stock.xml
+++ b/modules/purchase_trade/stock.xml
@@ -250,13 +250,13 @@ this repository contains the full copyright notices and license terms. -->
Linkage
- stock.shipment.in
- stock.shipment.in.linkage
+ lot.qt
+ lot.qt.linkage
stock/linkage.fodt
form_print
- stock.shipment.in,-1
+ lot.qt,-1
diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py
index fbec6ef..cfbcc81 100644
--- a/modules/purchase_trade/tests/test_module.py
+++ b/modules/purchase_trade/tests/test_module.py
@@ -6506,6 +6506,84 @@ class PurchaseTradeTestCase(ModuleTestCase):
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_shipment_insurance_contact_surveyor_prefers_shipment_surveyor(self):
'insurance contact surveyor property uses shipment surveyor first'
ShipmentIn = Pool().get('stock.shipment.in')