diff --git a/modules/purchase_trade/duplicate.py b/modules/purchase_trade/duplicate.py
index 7e7d9a2..16a71e0 100644
--- a/modules/purchase_trade/duplicate.py
+++ b/modules/purchase_trade/duplicate.py
@@ -64,6 +64,66 @@ class TradeCustomDuplicateStart(ModelView):
Bool(Eval('create_matched_mirror'))
& (Eval('mirror_mode') == 'new_mirror')),
})
+ mirror_currency = fields.Many2One('currency.currency', "Mirror Currency",
+ states={
+ 'invisible': ~Bool(Eval('create_matched_mirror')),
+ 'required': Bool(Eval('create_matched_mirror')),
+ })
+ mirror_payment_term = fields.Many2One(
+ 'account.invoice.payment_term', "Mirror Payment Term",
+ states={
+ 'invisible': ~Bool(Eval('create_matched_mirror')),
+ 'required': Bool(Eval('create_matched_mirror')),
+ })
+ mirror_incoterm = fields.Many2One('incoterm.incoterm', "Mirror Incoterm",
+ states={
+ 'invisible': ~Bool(Eval('create_matched_mirror')),
+ 'required': Bool(Eval('create_matched_mirror')),
+ })
+ mirror_quantity = fields.Numeric("Mirror Contractual Quantity",
+ digits='unit',
+ states={
+ 'invisible': ~Bool(Eval('create_matched_mirror')),
+ 'required': Bool(Eval('create_matched_mirror')),
+ })
+ mirror_unit = fields.Many2One('product.uom', "Mirror Unit",
+ readonly=True,
+ states={
+ 'invisible': ~Bool(Eval('create_matched_mirror')),
+ 'required': Bool(Eval('create_matched_mirror')),
+ })
+ mirror_unit_price = fields.Numeric("Mirror Price", digits=(16, 4),
+ states={
+ 'invisible': ~Bool(Eval('create_matched_mirror')),
+ 'required': Bool(Eval('create_matched_mirror')),
+ })
+ mirror_price_type = fields.Selection(PRICE_TYPES, "Mirror Price Type",
+ states={
+ 'invisible': ~Bool(Eval('create_matched_mirror')),
+ 'required': Bool(Eval('create_matched_mirror')),
+ })
+ mirror_clear_fees = fields.Boolean("Mirror Clear Fees",
+ states={
+ 'invisible': ~Bool(Eval('create_matched_mirror')),
+ })
+ mirror_clear_pricing_components = fields.Boolean(
+ "Mirror Clear Pricing Components",
+ states={
+ 'invisible': ~Bool(Eval('create_matched_mirror')),
+ })
+
+ @fields.depends('source', 'mirror_mode')
+ def on_change_mirror_mode(self):
+ record = TradeCustomDuplicate._reference_record(self.source)
+ if not record or not self.mirror_mode:
+ return
+ line = TradeCustomDuplicate._trade_line(record)
+ if not line:
+ return
+ defaults = TradeCustomDuplicate._mirror_defaults(
+ record, line, self.mirror_mode, strict=False, as_ids=False)
+ for field, value in defaults.items():
+ setattr(self, field, value)
class TradeCustomDuplicate(Wizard):
@@ -101,6 +161,15 @@ class TradeCustomDuplicate(Wizard):
def _opposite_type(source_model):
return 'Sale' if source_model == 'purchase.purchase' else 'Purchase'
+ @staticmethod
+ def _reference_record(reference):
+ if not reference:
+ return None
+ if isinstance(reference, str):
+ model, record_id = reference.split(',', 1)
+ return Pool().get(model)(int(record_id))
+ return reference
+
def default_start(self, fields):
if len(self.records) != 1:
raise UserError("Please select a single purchase or sale.")
@@ -113,6 +182,8 @@ class TradeCustomDuplicate(Wizard):
matched_record = self._matched_counterpart_record(
record, line, strict=False)
mirror_mode = 'duplicate_pair' if matched_record else 'new_mirror'
+ mirror_defaults = self._mirror_defaults(
+ record, line, mirror_mode, strict=False)
return {
'source_model': source_model,
@@ -132,9 +203,57 @@ class TradeCustomDuplicate(Wizard):
'clear_pricing_components': False,
'create_matched_mirror': False,
'mirror_mode': mirror_mode,
- 'mirror_party': (
- matched_record.party.id
- if matched_record and matched_record.party else None),
+ **mirror_defaults,
+ }
+
+ @staticmethod
+ def _field_id(record, field):
+ value = getattr(record, field, None)
+ return value.id if value else None
+
+ @staticmethod
+ def _field_value(record, field, as_ids=True):
+ value = getattr(record, field, None)
+ if as_ids:
+ return value.id if value else None
+ return value
+
+ @staticmethod
+ def _line_quantity_value(line):
+ return (
+ getattr(line, 'quantity_theorical', None)
+ or getattr(line, 'quantity', None))
+
+ @staticmethod
+ def _mirror_defaults(record, line, mirror_mode, strict=True, as_ids=True):
+ mirror_record = record
+ mirror_line = line
+ if mirror_mode == 'duplicate_pair':
+ mirror_line, mirror_record = TradeCustomDuplicate._matched_counterpart(
+ line, record=record, strict=strict)
+ if not mirror_line or not mirror_record:
+ mirror_record = record
+ mirror_line = line
+ return {
+ 'mirror_party': TradeCustomDuplicate._field_value(
+ mirror_record, 'party', as_ids=as_ids),
+ 'mirror_currency': TradeCustomDuplicate._field_value(
+ mirror_record, 'currency', as_ids=as_ids),
+ 'mirror_payment_term': TradeCustomDuplicate._field_value(
+ mirror_record, 'payment_term', as_ids=as_ids),
+ 'mirror_incoterm': TradeCustomDuplicate._field_value(
+ mirror_record, 'incoterm', as_ids=as_ids),
+ 'mirror_quantity': TradeCustomDuplicate._line_quantity_value(
+ mirror_line),
+ 'mirror_unit': (
+ mirror_line.unit.id if (
+ as_ids and getattr(mirror_line, 'unit', None))
+ else getattr(mirror_line, 'unit', None)),
+ 'mirror_unit_price': getattr(mirror_line, 'unit_price', None),
+ 'mirror_price_type': (
+ getattr(mirror_line, 'price_type', None) or 'priced'),
+ 'mirror_clear_fees': False,
+ 'mirror_clear_pricing_components': False,
}
@staticmethod
@@ -152,6 +271,40 @@ class TradeCustomDuplicate(Wizard):
new_record.save()
return Model(new_record.id)
+ @staticmethod
+ def _mirror_options(options, fallback_record=None, fallback_line=None):
+ return SimpleNamespace(
+ party=(
+ options.mirror_party
+ or getattr(fallback_record, 'party', None)),
+ currency=(
+ options.mirror_currency
+ or getattr(fallback_record, 'currency', None)),
+ payment_term=(
+ options.mirror_payment_term
+ or getattr(fallback_record, 'payment_term', None)),
+ incoterm=(
+ options.mirror_incoterm
+ or getattr(fallback_record, 'incoterm', None)),
+ quantity=(
+ options.mirror_quantity
+ or TradeCustomDuplicate._line_quantity_value(fallback_line)),
+ unit=(
+ options.mirror_unit
+ or getattr(fallback_line, 'unit', None)),
+ unit_price=(
+ options.mirror_unit_price
+ if options.mirror_unit_price is not None
+ else getattr(fallback_line, 'unit_price', None)),
+ price_type=(
+ options.mirror_price_type
+ or getattr(fallback_line, 'price_type', None)
+ or 'priced'),
+ clear_fees=options.mirror_clear_fees,
+ clear_pricing_components=(
+ options.mirror_clear_pricing_components),
+ )
+
@staticmethod
def _apply_party_addresses(record, party):
addresses = list(getattr(party, 'addresses', None) or [])
@@ -482,20 +635,9 @@ class TradeCustomDuplicate(Wizard):
@staticmethod
def _pair_duplicate_options(matched_record, matched_line, options):
- return SimpleNamespace(
- party=options.mirror_party or matched_record.party,
- currency=matched_record.currency or options.currency,
- payment_term=(
- matched_record.payment_term or options.payment_term),
- incoterm=matched_record.incoterm or options.incoterm,
- quantity=options.quantity,
- unit_price=getattr(matched_line, 'unit_price', None),
- price_type=(
- getattr(matched_line, 'price_type', None)
- or options.price_type),
- clear_fees=options.clear_fees,
- clear_pricing_components=options.clear_pricing_components,
- )
+ return TradeCustomDuplicate._mirror_options(
+ options, fallback_record=matched_record,
+ fallback_line=matched_line)
@staticmethod
def _match_duplicate_pair_lots(source_line, source_lot, counterpart_line,
@@ -552,18 +694,20 @@ class TradeCustomDuplicate(Wizard):
if source_lot and counterpart_lot:
TradeCustomDuplicate._match_duplicate_pair_lots(
new_line, source_lot, new_matched_line, counterpart_lot,
- options.quantity)
+ mirror_options.quantity)
return new_matched_record
@staticmethod
def _create_matched_mirror(record, line, options):
+ mirror_options = TradeCustomDuplicate._mirror_options(
+ options, fallback_record=record, fallback_line=line)
source_model = record.__name__
mirror_type = TradeCustomDuplicate._opposite_type(source_model)
source_lot = TradeCustomDuplicate._first_virtual_lot(line)
if not source_lot:
raise UserError(
"The duplicated line has no virtual lot to match.")
- if not options.unit:
+ if not mirror_options.unit:
raise UserError(
"The duplicated line has no unit for mirror contract creation.")
@@ -574,15 +718,16 @@ class TradeCustomDuplicate(Wizard):
record, mirror_type)
detail = ContractDetail()
- detail.party = options.mirror_party
- detail.currency = options.currency
- detail.currency_unit = "%s_%s" % (options.currency.id, options.unit.id)
- detail.incoterm = options.incoterm
- detail.payment_term = options.payment_term
- detail.quantity = options.quantity
- detail.unit = options.unit
- detail.price = options.unit_price
- detail.price_type = options.price_type
+ detail.party = mirror_options.party
+ detail.currency = mirror_options.currency
+ detail.currency_unit = "%s_%s" % (
+ mirror_options.currency.id, mirror_options.unit.id)
+ detail.incoterm = mirror_options.incoterm
+ detail.payment_term = mirror_options.payment_term
+ detail.quantity = mirror_options.quantity
+ detail.unit = mirror_options.unit
+ detail.price = mirror_options.unit_price
+ detail.price_type = mirror_options.price_type
detail.reference = getattr(record, 'reference', None)
detail.from_location = from_location
detail.to_location = to_location
@@ -595,8 +740,8 @@ class TradeCustomDuplicate(Wizard):
ct.type = mirror_type
ct.matched = True
ct.product = line.product
- ct.unit = line.unit
- ct.quantity = options.quantity
+ ct.unit = mirror_options.unit
+ ct.quantity = mirror_options.quantity
ct.lot = source_lot
with Transaction().set_context(active_ids=[]):
diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py
index f3da85b..6009c52 100644
--- a/modules/purchase_trade/tests/test_module.py
+++ b/modules/purchase_trade/tests/test_module.py
@@ -6207,9 +6207,24 @@ class PurchaseTradeTestCase(ModuleTestCase):
'custom duplicate can default mirror party from the matched sale'
purchase_party = SimpleNamespace(id=1)
sale_party = SimpleNamespace(id=2)
+ sale_currency = SimpleNamespace(id=30)
+ sale_payment_term = SimpleNamespace(id=31)
+ sale_incoterm = SimpleNamespace(id=32)
+ sale_unit = SimpleNamespace(id=33)
purchase_lot = SimpleNamespace(id=10)
- sale = SimpleNamespace(party=sale_party)
- sale_line = SimpleNamespace(id=20, sale=sale)
+ sale = SimpleNamespace(
+ party=sale_party,
+ currency=sale_currency,
+ payment_term=sale_payment_term,
+ incoterm=sale_incoterm)
+ sale_line = SimpleNamespace(
+ id=20,
+ sale=sale,
+ unit=sale_unit,
+ quantity=Decimal('99'),
+ quantity_theorical=Decimal('99'),
+ unit_price=Decimal('52'),
+ price_type='basis')
lot_qt = SimpleNamespace(lot_s=SimpleNamespace(sale_line=sale_line))
class FakeLotQt:
@@ -6248,6 +6263,110 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual(values['mirror_mode'], 'duplicate_pair')
self.assertEqual(values['mirror_party'], sale_party.id)
+ self.assertEqual(values['mirror_currency'], sale_currency.id)
+ self.assertEqual(values['mirror_payment_term'], sale_payment_term.id)
+ self.assertEqual(values['mirror_incoterm'], sale_incoterm.id)
+ self.assertEqual(values['mirror_quantity'], Decimal('99'))
+ self.assertEqual(values['mirror_unit'], sale_unit.id)
+ self.assertEqual(values['mirror_unit_price'], Decimal('52'))
+ self.assertEqual(values['mirror_price_type'], 'basis')
+
+ def test_custom_duplicate_defaults_new_mirror_from_source_purchase(self):
+ 'new mirror options default from the duplicated source contract'
+ 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)
+
+ class FakeLotQt:
+ @classmethod
+ def search(cls, domain):
+ return []
+
+ pool = Mock()
+ pool.get.side_effect = lambda name: {'lot.qt': FakeLotQt}[name]
+ 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',
+ )
+ record = SimpleNamespace(
+ __name__='purchase.purchase',
+ id=12,
+ lines=[line],
+ party=purchase_party,
+ currency=purchase_currency,
+ payment_term=purchase_payment_term,
+ incoterm=purchase_incoterm,
+ btb=None,
+ )
+ wizard = duplicate_module.TradeCustomDuplicate()
+ wizard.records = [record]
+
+ with patch.object(duplicate_module, 'Pool', return_value=pool):
+ values = wizard.default_start(None)
+
+ self.assertEqual(values['mirror_mode'], 'new_mirror')
+ self.assertEqual(values['mirror_party'], purchase_party.id)
+ self.assertEqual(values['mirror_currency'], purchase_currency.id)
+ self.assertEqual(values['mirror_payment_term'], purchase_payment_term.id)
+ self.assertEqual(values['mirror_incoterm'], purchase_incoterm.id)
+ self.assertEqual(values['mirror_quantity'], Decimal('100'))
+ self.assertEqual(values['mirror_unit'], unit.id)
+ self.assertEqual(values['mirror_unit_price'], Decimal('25'))
+ self.assertEqual(values['mirror_price_type'], 'priced')
+
+ 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)
+ 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',
+ )
+ record = SimpleNamespace(
+ __name__='purchase.purchase',
+ id=12,
+ lines=[line],
+ party=purchase_party,
+ currency=purchase_currency,
+ payment_term=purchase_payment_term,
+ incoterm=purchase_incoterm,
+ )
+ start = duplicate_module.TradeCustomDuplicateStart()
+ start.source = record
+ start.mirror_mode = 'new_mirror'
+ start.mirror_party = SimpleNamespace(id=99)
+
+ start.on_change_mirror_mode()
+
+ self.assertIs(start.mirror_party, purchase_party)
+ self.assertIs(start.mirror_currency, purchase_currency)
+ self.assertIs(start.mirror_payment_term, purchase_payment_term)
+ self.assertIs(start.mirror_incoterm, purchase_incoterm)
+ self.assertEqual(start.mirror_quantity, Decimal('100'))
+ self.assertIs(start.mirror_unit, unit)
+ self.assertEqual(start.mirror_unit_price, Decimal('25'))
+ self.assertEqual(start.mirror_price_type, 'priced')
def test_custom_duplicate_defaults_mirror_from_back_to_back_sale(self):
'custom duplicate can use the back-to-back sale as matched pair'
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 e74d4fc..9261d63 100644
--- a/modules/purchase_trade/view/trade_custom_duplicate_start_form.xml
+++ b/modules/purchase_trade/view/trade_custom_duplicate_start_form.xml
@@ -28,6 +28,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+