Create contract
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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_):
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user