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