diff --git a/modules/purchase_trade/lot.py b/modules/purchase_trade/lot.py index 408ffb0..66bb4d6 100755 --- a/modules/purchase_trade/lot.py +++ b/modules/purchase_trade/lot.py @@ -4386,17 +4386,31 @@ class ContractDetail(ModelView): return getattr(line.purchase.currency if lqt.lot_p else line.sale.currency, 'id', None) @classmethod - def default_unit(cls): - lqt = cls._get_lqt_from_context() - if not lqt: - return - line = lqt.lot_p.line if lqt.lot_p else lqt.lot_s.sale_line - if getattr(line, 'enable_linked_currency', False): - return line.linked_unit.id - return getattr(line.unit, 'id', None) - - @classmethod - def default_tol_min(cls): + def default_unit(cls): + lqt = cls._get_lqt_from_context() + if not lqt: + return + line = lqt.lot_p.line if lqt.lot_p else lqt.lot_s.sale_line + if getattr(line, 'enable_linked_currency', False): + return line.linked_unit.id + return getattr(line.unit, 'id', None) + + @classmethod + def default_currency_unit(cls): + lqt = cls._get_lqt_from_context() + if not lqt: + return + line = lqt.lot_p.line if lqt.lot_p else lqt.lot_s.sale_line + if getattr(line, 'enable_linked_currency', False): + return "0_%s" % line.linked_unit.id + contract = line.purchase if lqt.lot_p else line.sale + currency = getattr(contract, 'currency', None) + unit = getattr(line, 'unit', None) + if currency and unit: + return "%s_%s" % (currency.id, unit.id) + + @classmethod + def default_tol_min(cls): lqt = cls._get_lqt_from_context() return getattr(lqt.lot_p.line.purchase if lqt.lot_p else lqt.lot_s.sale_line.sale, 'tol_min', None) if lqt else None diff --git a/modules/purchase_trade/service.py b/modules/purchase_trade/service.py index ef736e1..f254c98 100644 --- a/modules/purchase_trade/service.py +++ b/modules/purchase_trade/service.py @@ -46,16 +46,19 @@ class ContractFactory: contract = Purchase() if type_ == 'Purchase' else Sale() # ---------- CONTRACT ---------- - parts = c.currency_unit.split("_") + parts = cls._get_currency_unit_parts(c, base_contract) contract.currency = int(parts[0]) or 1 contract.party = c.party contract.crop = cls._get_crop(c, base_contract) contract.tol_min = cls._get_tolerance(c, 'tol_min') contract.tol_max = cls._get_tolerance(c, 'tol_max') - contract.payment_term = c.payment_term + contract.payment_term = cls._get_payment_term( + c, base_contract, type_) contract.reference = c.reference - contract.from_location = c.from_location - contract.to_location = c.to_location + from_location, to_location = cls._get_locations( + c, base_contract, type_) + contract.from_location = from_location + contract.to_location = to_location context = Transaction().context contract.company = context.get('company') if context else None if type_ == 'Purchase': @@ -63,18 +66,18 @@ class ContractFactory: else: contract.sale_date = Date.today() - cls._apply_locations(contract, base_contract, type_) - cls._apply_party_data(contract, c.party, type_) - cls._apply_payment_term(contract, c.party, type_) + cls._apply_party_data(contract, c.party, base_contract, type_) if type_ == 'Sale': contract.product_origin = getattr(base_contract, 'product_origin', None) - contract.incoterm = c.incoterm + contract.incoterm = cls._get_contract_value( + c, base_contract, 'incoterm') - if c.party.addresses: - contract.invoice_address = c.party.addresses[0] + addresses = getattr(c.party, 'addresses', None) + if addresses: + contract.invoice_address = addresses[0] if type_ == 'Sale': - contract.shipment_address = c.party.addresses[0] + contract.shipment_address = addresses[0] contract.save() @@ -147,19 +150,74 @@ class ContractFactory: return to_location, None return None, None - @staticmethod - def _apply_party_data(contract, party, type_): - if party.wb: - contract.wb = party.wb - if party.association: - contract.association = party.association + @classmethod + def _get_locations(cls, contract_detail, base, type_): + mirror_from, mirror_to = cls._get_mirror_locations(base, type_) + return ( + getattr(contract_detail, 'from_location', None) + or mirror_from + or getattr(base, 'from_location', None), + getattr(contract_detail, 'to_location', None) + or mirror_to + or getattr(base, 'to_location', None), + ) + + @classmethod + def _apply_party_data(cls, contract, party, base, type_): + Contract = Pool().get( + 'purchase.purchase' if type_ == 'Purchase' else 'sale.sale') + contract.wb = cls._get_header_value( + 'wb', party, base, Contract.default_wb) + contract.association = cls._get_header_value( + 'association', party, base, Contract.default_association) + contract.certif = cls._get_header_value( + 'certif', party, base, Contract.default_certif) @staticmethod - def _apply_payment_term(contract, party, type_): - if type_ == 'Purchase' and party.supplier_payment_term: - contract.payment_term = party.supplier_payment_term - elif type_ == 'Sale' and party.customer_payment_term: - contract.payment_term = party.customer_payment_term + def _get_header_value(field, party, base, default_getter=None): + value = getattr(party, field, None) + if value: + return value + value = getattr(base, field, None) + if value: + return value + if default_getter: + return default_getter() + + @staticmethod + def _get_payment_term(contract_detail, base, type_): + value = getattr(contract_detail, 'payment_term', None) + if value: + return value + party = getattr(contract_detail, 'party', None) + field = ( + 'supplier_payment_term' + if type_ == 'Purchase' else 'customer_payment_term') + value = getattr(party, field, None) + if value: + return value + return getattr(base, 'payment_term', None) + + @staticmethod + def _get_contract_value(contract_detail, base, field): + return ( + getattr(contract_detail, field, None) + or getattr(base, field, None)) + + @staticmethod + def _get_currency_unit_parts(contract_detail, base): + value = getattr(contract_detail, 'currency_unit', None) + if value: + return value.split("_") + currency = ( + getattr(contract_detail, 'currency', None) + or getattr(base, 'currency', None)) + unit = getattr(contract_detail, 'unit', None) + currency_id = getattr(currency, 'id', currency) + unit_id = getattr(unit, 'id', unit) + if currency_id and unit_id: + return [str(currency_id), str(unit_id)] + return ["1", str(unit_id or 1)] @staticmethod def _apply_price(line, c, parts): diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index ef693d6..a5a0dee 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -2786,6 +2786,65 @@ class PurchaseTradeTestCase(ModuleTestCase): ContractFactory._get_crop(contract_detail, base_contract), crop) + def test_contract_factory_locations_fallback_to_source_contract(self): + 'create contracts keeps required locations when wizard line is empty' + source_from = Mock(type='supplier') + source_to = Mock(type='storage') + contract_detail = Mock(from_location=None, to_location=None) + base_contract = Mock( + from_location=source_from, to_location=source_to) + + from_location, to_location = ContractFactory._get_locations( + contract_detail, base_contract, 'Sale') + + self.assertEqual(from_location, source_to) + self.assertEqual(to_location, source_to) + + def test_contract_factory_required_header_fields_fallback_to_defaults(self): + 'create contracts fills required header fields outside the normal form' + contract = Mock() + party = Mock(wb=None, association=None, certif=None) + base_contract = Mock(wb=None, association=None, certif=None) + Contract = Mock() + Contract.default_wb.return_value = 1 + Contract.default_association.return_value = 2 + Contract.default_certif.return_value = 3 + + with patch('trytond.modules.purchase_trade.service.Pool') as PoolMock: + PoolMock.return_value.get.return_value = Contract + + ContractFactory._apply_party_data( + contract, party, base_contract, 'Purchase') + + self.assertEqual(contract.wb, 1) + self.assertEqual(contract.association, 2) + self.assertEqual(contract.certif, 3) + + def test_contract_factory_incoterm_fallback_to_source_contract(self): + 'create contracts keeps required incoterm when wizard line is empty' + incoterm = Mock() + contract_detail = Mock(incoterm=None) + base_contract = Mock(incoterm=incoterm) + + self.assertEqual( + ContractFactory._get_contract_value( + contract_detail, base_contract, 'incoterm'), + incoterm) + + def test_contract_factory_currency_unit_fallback_to_hidden_defaults(self): + 'create contracts can price even when currency/unit selector is empty' + contract_detail = Mock( + currency_unit=None, + currency=Mock(id=7), + unit=Mock(id=9), + ) + base_contract = Mock(currency=Mock(id=3)) + + self.assertEqual( + ContractFactory._get_currency_unit_parts( + contract_detail, base_contract), + ['7', '9']) + def test_contract_factory_delivery_dates_fallback_to_period(self): 'hidden delivery dates are still derived from delivery period' period = Mock(beg_date='2026-05-01', end_date='2026-05-31')