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