Create contract

This commit is contained in:
2026-05-10 16:00:38 +02:00
parent 8e890f3c43
commit fe3a3ea71e
5 changed files with 184 additions and 31 deletions

View File

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

View File

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

View File

@@ -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_):

View File

@@ -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')

View File

@@ -18,4 +18,5 @@
<field name="from_del"/>
<field name="to_del"/>
<field name="category" tree_invisible="1"/>
<field name="company_visible" tree_invisible="1"/>
</tree>