From 2a1512a43673521d9b024b87584880185b928d27 Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Thu, 18 Jun 2026 13:51:05 +0200 Subject: [PATCH] Duplicate from LM --- modules/purchase_trade/duplicate.py | 40 +++++- modules/purchase_trade/lot.xml | 12 +- modules/purchase_trade/tests/test_module.py | 117 ++++++++++++++++++ .../trade_custom_duplicate_start_form.xml | 1 + 4 files changed, 166 insertions(+), 4 deletions(-) diff --git a/modules/purchase_trade/duplicate.py b/modules/purchase_trade/duplicate.py index 16a71e0..595233e 100644 --- a/modules/purchase_trade/duplicate.py +++ b/modules/purchase_trade/duplicate.py @@ -39,6 +39,8 @@ class TradeCustomDuplicateStart(ModelView): ('sale.sale', 'Sale'), ], readonly=True) duplicated_record_id = fields.Integer("Duplicated Record ID", readonly=True) + duplicate_pair_available = fields.Boolean( + "Duplicate Pair Available", readonly=True) party = fields.Many2One('party.party', "Counterparty", required=True) currency = fields.Many2One('currency.currency', "Currency", required=True) payment_term = fields.Many2One( @@ -56,6 +58,7 @@ class TradeCustomDuplicateStart(ModelView): states={ 'invisible': ~Bool(Eval('create_matched_mirror')), 'required': Bool(Eval('create_matched_mirror')), + 'readonly': ~Bool(Eval('duplicate_pair_available')), }) mirror_party = fields.Many2One('party.party', "Mirror Counterparty", states={ @@ -161,6 +164,33 @@ class TradeCustomDuplicate(Wizard): def _opposite_type(source_model): return 'Sale' if source_model == 'purchase.purchase' else 'Purchase' + @staticmethod + def _record_from_lot_report(record): + if getattr(record, '__name__', None) != 'lot.report': + return record + if getattr(record, 'r_purchase', None): + return record.r_purchase + if getattr(record, 'r_sale', None): + return record.r_sale + lot = ( + getattr(record, 'r_lot_p', None) + or getattr(record, 'r_lot_s', None) + or getattr(record, 'r_lot_himself', None)) + if lot: + line = getattr(lot, 'line', None) + if line and getattr(line, 'purchase', None): + return line.purchase + sale_line = getattr(lot, 'sale_line', None) + if sale_line and getattr(sale_line, 'sale', None): + return sale_line.sale + raise UserError( + "Custom duplicate could not find a purchase or sale from this " + "Lots Management line.") + + @staticmethod + def _source_record(record): + return TradeCustomDuplicate._record_from_lot_report(record) + @staticmethod def _reference_record(reference): if not reference: @@ -172,15 +202,18 @@ class TradeCustomDuplicate(Wizard): def default_start(self, fields): if len(self.records) != 1: - raise UserError("Please select a single purchase or sale.") + raise UserError( + "Please select a single purchase, sale, or Lots Management " + "line.") - record = self.records[0] + record = self._source_record(self.records[0]) source_model = self._source_model(record) line = self._trade_line(record) if not line: raise UserError("The selected contract has no trade line to duplicate.") matched_record = self._matched_counterpart_record( record, line, strict=False) + duplicate_pair_available = bool(matched_record) mirror_mode = 'duplicate_pair' if matched_record else 'new_mirror' mirror_defaults = self._mirror_defaults( record, line, mirror_mode, strict=False) @@ -202,6 +235,7 @@ class TradeCustomDuplicate(Wizard): 'clear_fees': False, 'clear_pricing_components': False, 'create_matched_mirror': False, + 'duplicate_pair_available': duplicate_pair_available, 'mirror_mode': mirror_mode, **mirror_defaults, } @@ -748,7 +782,7 @@ class TradeCustomDuplicate(Wizard): ContractFactory.create_contracts([detail], type_=mirror_type, ct=ct) def transition_duplicate(self): - record = self.records[0] + record = self._source_record(self.records[0]) options = self.start new_record = self._copy_contract(record, options) new_line = self._apply_line_options(new_record, options) diff --git a/modules/purchase_trade/lot.xml b/modules/purchase_trade/lot.xml index b8e0e29..0ad25e8 100755 --- a/modules/purchase_trade/lot.xml +++ b/modules/purchase_trade/lot.xml @@ -148,9 +148,19 @@ this repository contains the full copyright notices and license terms. --> lot.report,-1 + + Custom Duplicate + trade.custom_duplicate + lot.report + + + form_action + lot.report,-1 + + - lot.add.lot + lot.add.lot form lot_add_start_form diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 6009c52..859a3bc 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -6324,6 +6324,123 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertEqual(values['mirror_unit_price'], Decimal('25')) self.assertEqual(values['mirror_price_type'], 'priced') + def test_custom_duplicate_starts_from_lot_report_purchase(self): + 'custom duplicate can resolve a Lots Management row to its purchase' + purchase_party = SimpleNamespace(id=1) + purchase_currency = SimpleNamespace(id=4) + purchase_payment_term = SimpleNamespace(id=5) + purchase_incoterm = SimpleNamespace(id=6) + unit = SimpleNamespace(id=3) + line = SimpleNamespace( + __name__='purchase.line', + id=11, + type='line', + product=Mock(), + lots=[], + unit=unit, + quantity=Decimal('100'), + quantity_theorical=Decimal('100'), + unit_price=Decimal('25'), + price_type='priced', + ) + purchase = SimpleNamespace( + __name__='purchase.purchase', + id=12, + lines=[line], + party=purchase_party, + currency=purchase_currency, + payment_term=purchase_payment_term, + incoterm=purchase_incoterm, + btb=None, + ) + line.purchase = purchase + lot_report = SimpleNamespace( + __name__='lot.report', + r_purchase=purchase, + r_sale=None, + r_lot_p=None, + r_lot_s=None, + r_lot_himself=None, + ) + wizard = duplicate_module.TradeCustomDuplicate() + wizard.records = [lot_report] + + values = wizard.default_start(None) + + self.assertEqual(values['source_model'], 'purchase.purchase') + self.assertEqual(values['source'], 'purchase.purchase,12') + self.assertFalse(values['duplicate_pair_available']) + self.assertEqual(values['mirror_mode'], 'new_mirror') + + def test_custom_duplicate_lot_report_marks_pair_available(self): + 'Lots Management custom duplicate enables pair mode when matched' + purchase_party = SimpleNamespace(id=1) + sale_party = SimpleNamespace(id=2) + unit = SimpleNamespace(id=3) + sale = SimpleNamespace( + party=sale_party, + currency=SimpleNamespace(id=30), + payment_term=SimpleNamespace(id=31), + incoterm=SimpleNamespace(id=32)) + sale_line = SimpleNamespace( + id=20, + sale=sale, + unit=unit, + quantity=Decimal('100'), + quantity_theorical=Decimal('100'), + unit_price=Decimal('50'), + price_type='priced') + physical_lot = SimpleNamespace( + id=10, + lot_type='physic', + sale_line=sale_line) + line = SimpleNamespace( + __name__='purchase.line', + id=11, + type='line', + product=Mock(), + lots=[physical_lot], + unit=unit, + quantity=Decimal('100'), + quantity_theorical=Decimal('100'), + unit_price=Decimal('25'), + price_type='priced', + ) + purchase = SimpleNamespace( + __name__='purchase.purchase', + id=12, + lines=[line], + party=purchase_party, + currency=SimpleNamespace(id=4), + payment_term=SimpleNamespace(id=5), + incoterm=SimpleNamespace(id=6), + ) + lot_report = SimpleNamespace( + __name__='lot.report', + r_purchase=purchase, + r_sale=None, + r_lot_p=physical_lot, + r_lot_s=None, + r_lot_himself=None, + ) + + class FakeLotQt: + @classmethod + def search(cls, domain): + return [] + + pool = Mock() + pool.get.side_effect = lambda name: {'lot.qt': FakeLotQt}[name] + wizard = duplicate_module.TradeCustomDuplicate() + wizard.records = [lot_report] + + with patch.object(duplicate_module, 'Pool', return_value=pool): + values = wizard.default_start(None) + + self.assertTrue(values['duplicate_pair_available']) + self.assertEqual(values['mirror_mode'], 'duplicate_pair') + self.assertEqual(values['mirror_party'], sale_party.id) + def test_custom_duplicate_mirror_mode_change_refreshes_source_defaults(self): 'changing mirror mode refreshes mirror fields from the source contract' purchase_party = SimpleNamespace(id=1) diff --git a/modules/purchase_trade/view/trade_custom_duplicate_start_form.xml b/modules/purchase_trade/view/trade_custom_duplicate_start_form.xml index 9261d63..97e3dba 100644 --- a/modules/purchase_trade/view/trade_custom_duplicate_start_form.xml +++ b/modules/purchase_trade/view/trade_custom_duplicate_start_form.xml @@ -48,4 +48,5 @@ +