From fe3a3ea71e53915ef5d84cc7e95041e7e6a58df8 Mon Sep 17 00:00:00 2001 From: laurentbarontini Date: Sun, 10 May 2026 16:00:38 +0200 Subject: [PATCH] Create contract --- modules/purchase_trade/docs/business-rules.md | 35 ++++++++- modules/purchase_trade/lot.py | 69 ++++++++++++----- modules/purchase_trade/service.py | 35 ++++++--- modules/purchase_trade/tests/test_module.py | 75 +++++++++++++++++++ .../view/contract_detail_tree.xml | 1 + 5 files changed, 184 insertions(+), 31 deletions(-) diff --git a/modules/purchase_trade/docs/business-rules.md b/modules/purchase_trade/docs/business-rules.md index 38c6838..eee54bf 100644 --- a/modules/purchase_trade/docs/business-rules.md +++ b/modules/purchase_trade/docs/business-rules.md @@ -1,8 +1,8 @@ # Business Rules - Purchase Trade Statut: `draft` -Version: `v0.7` -Derniere mise a jour: `2026-04-26` +Version: `v0.8` +Derniere mise a jour: `2026-05-10` Owner metier: `a completer` Owner technique: `a completer` @@ -731,6 +731,37 @@ Owner technique: `a completer` - Priorite: - `importante` +### BR-PT-024 - Create Contracts propage les lieux stock selon le flux miroir + +- Intent: eviter une ressaisie des lieux logistiques quand un contrat miroir + est cree depuis une quantite ouverte. +- Description: + - Les champs concernes sont les `stock.location` `from_location` et + `to_location` des contrats achat et vente. + - Si le contrat source est en flux direct fournisseur -> client + (`from_location.type = supplier` et `to_location.type = customer`), le + contrat cree reprend le meme couple `from_location` / `to_location`. + - Ce flux correspond au mode `Dropship` affiche sur les shipments. + - Si un contrat vente est cree depuis un achat dont `to_location.type = + storage`, le `from_location` de la vente est pre-rempli avec ce + `to_location` achat. + - Si un contrat achat est cree depuis une vente dont `from_location.type = + storage`, le `to_location` de l'achat est pre-rempli avec ce + `from_location` vente. +- Resultat attendu: + - achat supplier -> customer vers vente: + `sale.from_location = purchase.from_location` et + `sale.to_location = purchase.to_location`. + - vente supplier -> customer vers achat: + `purchase.from_location = sale.from_location` et + `purchase.to_location = sale.to_location`. + - achat vers stock puis vente: + `sale.from_location = purchase.to_location`. + - vente depuis stock puis achat: + `purchase.to_location = sale.from_location`. +- Priorite: + - `importante` + ## 4) Exemples concrets ### Exemple E1 - Augmentation simple diff --git a/modules/purchase_trade/lot.py b/modules/purchase_trade/lot.py index 41a9330..cbb73f2 100755 --- a/modules/purchase_trade/lot.py +++ b/modules/purchase_trade/lot.py @@ -3576,7 +3576,9 @@ class ContractDetail(ModelView): qt_unit = fields.Many2One('product.uom',"Unit") tol_min = fields.Numeric("Tol - in %", required=True) tol_max = fields.Numeric("Tol + in %", required=True) - crop = fields.Many2One('purchase.crop',"Crop") + crop = fields.Many2One( + 'purchase.crop', "Crop", + states={'invisible': Eval('company_visible')}) del_period = fields.Many2One('product.month',"Delivery Period") from_del = fields.Date("From") to_del = fields.Date("To") @@ -3594,12 +3596,13 @@ class ContractDetail(ModelView): to_location = fields.Many2One( 'stock.location', "To location", domain=[('type', 'in', ['storage', 'customer'])]) - payment_term = fields.Many2One('account.invoice.payment_term',"Payment Term", required=True) - - @classmethod - def default_category(cls): - lqt = cls._get_lqt_from_context() - if lqt and lqt.lot_p: + payment_term = fields.Many2One('account.invoice.payment_term',"Payment Term", required=True) + company_visible = fields.Boolean("Visible") + + @classmethod + def default_category(cls): + lqt = cls._get_lqt_from_context() + if lqt and lqt.lot_p: return 1 else: return 2 @@ -3622,8 +3625,17 @@ class ContractDetail(ModelView): return result @classmethod - def default_price_type(cls): - return 'priced' + def default_price_type(cls): + return 'priced' + + @classmethod + def default_company_visible(cls): + Company = Pool().get('company.company') + company_id = Transaction().context.get('company') + if not company_id: + return False + company = Company(company_id) + return bool(company.party and company.party.name == 'MELYA') @classmethod def _get_lqt_from_context(cls): @@ -3684,14 +3696,35 @@ class ContractDetail(ModelView): return getattr(lqt.lot_p.line.purchase if lqt.lot_p else lqt.lot_s.sale_line.sale, 'tol_max', None) if lqt else None @classmethod - def default_del_period(cls): - lqt = cls._get_lqt_from_context() - if lqt and getattr(lqt.lot_p.line.purchase if lqt.lot_p else lqt.lot_s.sale_line.sale, 'del_period', None): - return lqt.lot_p.line.purchase.del_period.id if lqt.lot_p else lqt.lot_s.sale_line.sale.del_period.id - - @fields.depends('del_period') - def on_change_del_period(self): - if self.del_period: - self.from_del = self.del_period.beg_date + def default_del_period(cls): + lqt = cls._get_lqt_from_context() + if lqt and getattr(lqt.lot_p.line.purchase if lqt.lot_p else lqt.lot_s.sale_line.sale, 'del_period', None): + return lqt.lot_p.line.purchase.del_period.id if lqt.lot_p else lqt.lot_s.sale_line.sale.del_period.id + + @classmethod + def _get_base_contract(cls): + lqt = cls._get_lqt_from_context() + if lqt and lqt.lot_p: + return lqt.lot_p.line.purchase, 'Sale' + if lqt and lqt.lot_s: + return lqt.lot_s.sale_line.sale, 'Purchase' + return None, None + + @classmethod + def default_from_location(cls): + base, type_ = cls._get_base_contract() + from_location, _ = ContractFactory._get_mirror_locations(base, type_) + return getattr(from_location, 'id', None) + + @classmethod + def default_to_location(cls): + base, type_ = cls._get_base_contract() + _, to_location = ContractFactory._get_mirror_locations(base, type_) + return getattr(to_location, 'id', None) + + @fields.depends('del_period') + def on_change_del_period(self): + if self.del_period: + self.from_del = self.del_period.beg_date self.to_del = self.del_period.end_date diff --git a/modules/purchase_trade/service.py b/modules/purchase_trade/service.py index a0c3c5f..55348e3 100644 --- a/modules/purchase_trade/service.py +++ b/modules/purchase_trade/service.py @@ -112,20 +112,33 @@ class ContractFactory: # Helpers # ------------------------------------------------------------------------- + @classmethod + def _apply_locations(cls, contract, base, type_): + from_location, to_location = cls._get_mirror_locations(base, type_) + if from_location: + contract.from_location = from_location + if to_location: + contract.to_location = to_location + @staticmethod - def _apply_locations(contract, base, type_): - if not (base.from_location and base.to_location): - return + def _get_mirror_locations(base, type_): + from_location = getattr(base, 'from_location', None) + to_location = getattr(base, 'to_location', None) + if not (from_location or to_location): + return None, None + + if (from_location and to_location + and getattr(from_location, 'type', None) == 'supplier' + and getattr(to_location, 'type', None) == 'customer'): + return from_location, to_location if type_ == 'Purchase': - contract.to_location = base.from_location - else: - contract.from_location = base.to_location - - if (base.from_location.type == 'supplier' - and base.to_location.type == 'customer'): - contract.from_location = base.from_location - contract.to_location = base.to_location + if getattr(from_location, 'type', None) == 'storage': + return None, from_location + elif type_ == 'Sale': + if getattr(to_location, 'type', None) == 'storage': + return to_location, None + return None, None @staticmethod def _apply_party_data(contract, party, type_): diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 8d9bc5f..da8539d 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -2255,6 +2255,81 @@ class PurchaseTradeTestCase(ModuleTestCase): with self.assertRaises(UserError): ContractFactory._get_line_sources(contract_detail, sources, ct) + def test_contract_detail_defaults_sale_locations_from_dropship_purchase(self): + 'create contracts copies supplier-to-customer locations from purchase to sale' + supplier = Mock(id=1, type='supplier') + customer = Mock(id=2, type='customer') + purchase = Mock(from_location=supplier, to_location=customer) + lqt = Mock( + lot_p=Mock(line=Mock(purchase=purchase)), + lot_s=None) + + with patch.object( + lot_module.ContractDetail, '_get_lqt_from_context', + return_value=lqt): + self.assertEqual(lot_module.ContractDetail.default_from_location(), 1) + self.assertEqual(lot_module.ContractDetail.default_to_location(), 2) + + def test_contract_detail_defaults_sale_from_purchase_stock_destination(self): + 'create contracts uses purchase stock destination as sale source' + supplier = Mock(id=1, type='supplier') + storage = Mock(id=3, type='storage') + purchase = Mock(from_location=supplier, to_location=storage) + lqt = Mock( + lot_p=Mock(line=Mock(purchase=purchase)), + lot_s=None) + + with patch.object( + lot_module.ContractDetail, '_get_lqt_from_context', + return_value=lqt): + self.assertEqual(lot_module.ContractDetail.default_from_location(), 3) + self.assertIsNone(lot_module.ContractDetail.default_to_location()) + + def test_contract_detail_defaults_purchase_locations_from_dropship_sale(self): + 'create contracts copies supplier-to-customer locations from sale to purchase' + supplier = Mock(id=1, type='supplier') + customer = Mock(id=2, type='customer') + sale = Mock(from_location=supplier, to_location=customer) + lqt = Mock( + lot_p=None, + lot_s=Mock(sale_line=Mock(sale=sale))) + + with patch.object( + lot_module.ContractDetail, '_get_lqt_from_context', + return_value=lqt): + self.assertEqual(lot_module.ContractDetail.default_from_location(), 1) + self.assertEqual(lot_module.ContractDetail.default_to_location(), 2) + + def test_contract_detail_defaults_purchase_to_sale_stock_source(self): + 'create contracts uses sale stock source as purchase destination' + storage = Mock(id=3, type='storage') + customer = Mock(id=2, type='customer') + sale = Mock(from_location=storage, to_location=customer) + lqt = Mock( + lot_p=None, + lot_s=Mock(sale_line=Mock(sale=sale))) + + with patch.object( + lot_module.ContractDetail, '_get_lqt_from_context', + return_value=lqt): + self.assertIsNone(lot_module.ContractDetail.default_from_location()) + self.assertEqual(lot_module.ContractDetail.default_to_location(), 3) + + def test_contract_detail_hides_melya_company_fields(self): + 'create contracts mirrors Melya company field visibility' + company = Mock(party=Mock(name='MELYA')) + company_model = Mock(return_value=company) + transaction = Mock() + transaction.context = {'company': 42} + + with patch('trytond.modules.purchase_trade.lot.Pool') as PoolMock, \ + patch('trytond.modules.purchase_trade.lot.Transaction', + return_value=transaction): + PoolMock.return_value.get.return_value = company_model + + self.assertTrue( + lot_module.ContractDetail.default_company_visible()) + def test_sale_report_price_lines_basis_displays_premium_only(self): 'basis report pricing displays only the premium in templates' Sale = Pool().get('sale.sale') diff --git a/modules/purchase_trade/view/contract_detail_tree.xml b/modules/purchase_trade/view/contract_detail_tree.xml index 8188e9e..205f48d 100755 --- a/modules/purchase_trade/view/contract_detail_tree.xml +++ b/modules/purchase_trade/view/contract_detail_tree.xml @@ -18,4 +18,5 @@ +