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')