Duplicate from LM

This commit is contained in:
2026-06-18 13:51:05 +02:00
parent 1c20181d06
commit 2a1512a436
4 changed files with 166 additions and 4 deletions

View File

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

View File

@@ -148,9 +148,19 @@ this repository contains the full copyright notices and license terms. -->
<field name="model">lot.report,-1</field>
<field name="action" ref="act_plan_transport"/>
</record>
<record model="ir.action.wizard" id="act_lot_report_custom_duplicate">
<field name="name">Custom Duplicate</field>
<field name="wiz_name">trade.custom_duplicate</field>
<field name="model">lot.report</field>
</record>
<record model="ir.action.keyword" id="act_lot_report_custom_duplicate_keyword">
<field name="keyword">form_action</field>
<field name="model">lot.report,-1</field>
<field name="action" ref="act_lot_report_custom_duplicate"/>
</record>
<record model="ir.ui.view" id="add_lot_view_form">
<field name="model">lot.add.lot</field>
<field name="model">lot.add.lot</field>
<field name="type">form</field>
<field name="name">lot_add_start_form</field>
</record>

View File

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

View File

@@ -48,4 +48,5 @@
<field name="mirror_clear_pricing_components"/>
<field name="source" invisible="1"/>
<field name="duplicated_record_id" invisible="1"/>
<field name="duplicate_pair_available" invisible="1"/>
</form>