Check From To + pricing component domain filter
This commit is contained in:
@@ -485,6 +485,43 @@ Owner technique: `a completer`
|
||||
- Priorite:
|
||||
- `importante`
|
||||
|
||||
### BR-PT-014 - Delivery period: From doit rester inferieur ou egal a To
|
||||
|
||||
- Intent: eviter les periodes de livraison incoherentes sur les lignes achat et
|
||||
vente.
|
||||
- Description:
|
||||
- Les champs `from_del` et `to_del` sont presents sur `purchase.line` et
|
||||
`sale.line`.
|
||||
- Si les deux dates sont renseignees, `from_del` ne doit jamais etre
|
||||
posterieur a `to_del`.
|
||||
- Resultat attendu:
|
||||
- la sauvegarde d'une `purchase.line` ou `sale.line` est bloquee si
|
||||
`from_del > to_del`
|
||||
- une date ouverte reste autorisee si seulement une des deux bornes est
|
||||
renseignee
|
||||
- Priorite:
|
||||
- `importante`
|
||||
|
||||
### BR-PT-015 - Pricing manuel: composant limite a la ligne courante
|
||||
|
||||
- Intent: eviter qu'une ligne de pricing saisie manuellement utilise un
|
||||
composant rattache a une autre ligne de contrat.
|
||||
- Description:
|
||||
- Dans l'onglet `Pricing dates` d'une `purchase.line`, le champ
|
||||
`pricing.pricing.price_component` doit proposer uniquement les composants
|
||||
dont `pricing.component.line` est la ligne achat courante.
|
||||
- Dans l'onglet `Pricing dates` d'une `sale.line`, il doit proposer
|
||||
uniquement les composants dont `pricing.component.sale_line` est la ligne
|
||||
vente courante.
|
||||
- Une ligne de pricing sans composant reste possible pour le mode manuel sans
|
||||
component.
|
||||
- Resultat attendu:
|
||||
- le domaine UI filtre les composants sur la ligne courante
|
||||
- une validation serveur bloque aussi un composant appartenant a une autre
|
||||
ligne
|
||||
- Priorite:
|
||||
- `importante`
|
||||
|
||||
## 4) Exemples concrets
|
||||
|
||||
### Exemple E1 - Augmentation simple
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
from trytond.model import fields
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
from trytond.pyson import Bool, Eval, Id
|
||||
from trytond.model import (ModelSQL, ModelView)
|
||||
from trytond.model import (ModelSQL, ModelView)
|
||||
from trytond.exceptions import UserError
|
||||
from trytond.tools import is_full_text, lstrip_wildcard
|
||||
from trytond.transaction import Transaction, inactive_records
|
||||
from decimal import getcontext, Decimal, ROUND_HALF_UP
|
||||
@@ -330,7 +331,14 @@ class Pricing(ModelSQL,ModelView):
|
||||
__name__ = 'pricing.pricing'
|
||||
|
||||
pricing_date = fields.Date("Date")
|
||||
price_component = fields.Many2One('pricing.component', "Component")#, domain=[('id', 'in', Eval('line.price_components'))], ondelete='CASCADE')
|
||||
price_component = fields.Many2One(
|
||||
'pricing.component', "Component",
|
||||
domain=[
|
||||
'OR',
|
||||
('line', '=', Eval('line', -1)),
|
||||
('sale_line', '=', Eval('sale_line', -1)),
|
||||
],
|
||||
depends=['line', 'sale_line'])#, ondelete='CASCADE')
|
||||
quantity = fields.Numeric("Qt",digits='unit')
|
||||
settl_price = fields.Numeric("Settl. price",digits='unit')
|
||||
fixed_qt = fields.Numeric("Fixed qt",digits='unit', readonly=True)
|
||||
@@ -433,6 +441,32 @@ class Pricing(ModelSQL,ModelView):
|
||||
cls._sync_manual_last(records)
|
||||
cls._sync_eod_price(records)
|
||||
|
||||
@staticmethod
|
||||
def _component_matches_owner(record):
|
||||
def record_id(value):
|
||||
return getattr(value, 'id', value)
|
||||
|
||||
component = getattr(record, 'price_component', None)
|
||||
if not component:
|
||||
return True
|
||||
line = getattr(record, 'line', None)
|
||||
if line:
|
||||
return record_id(getattr(component, 'line', None)) == record_id(line)
|
||||
sale_line = getattr(record, 'sale_line', None)
|
||||
if sale_line:
|
||||
return (
|
||||
record_id(getattr(component, 'sale_line', None))
|
||||
== record_id(sale_line))
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def validate(cls, records):
|
||||
super(Pricing, cls).validate(records)
|
||||
for record in records:
|
||||
if not cls._component_matches_owner(record):
|
||||
raise UserError(
|
||||
"Pricing component must belong to the related line.")
|
||||
|
||||
@classmethod
|
||||
def _sync_eod_price(cls, records):
|
||||
if not records:
|
||||
|
||||
@@ -1094,6 +1094,13 @@ class Line(metaclass=PoolMeta):
|
||||
return configurations[0].pricing_rule or ''
|
||||
return ''
|
||||
|
||||
@staticmethod
|
||||
def _has_invalid_delivery_period(line):
|
||||
return (
|
||||
bool(line.from_del)
|
||||
and bool(line.to_del)
|
||||
and line.from_del > line.to_del)
|
||||
|
||||
quantity_theorical = fields.Numeric("Contractual Qt", digits='unit', readonly=False)
|
||||
price_type = fields.Selection([
|
||||
('cash', 'Cash Price'),
|
||||
@@ -1558,13 +1565,16 @@ class Line(metaclass=PoolMeta):
|
||||
# OpenPosition.delete(op)
|
||||
super(Line, cls).delete(lines)
|
||||
|
||||
@classmethod
|
||||
def validate(cls, lines):
|
||||
super(Line, cls).validate(lines)
|
||||
for line in lines:
|
||||
if line.price_components:
|
||||
for pc in line.price_components:
|
||||
if pc.triggers:
|
||||
@classmethod
|
||||
def validate(cls, lines):
|
||||
super(Line, cls).validate(lines)
|
||||
for line in lines:
|
||||
if cls._has_invalid_delivery_period(line):
|
||||
raise UserError(
|
||||
"Delivery period From date must be before To date.")
|
||||
if line.price_components:
|
||||
for pc in line.price_components:
|
||||
if pc.triggers:
|
||||
for tr in pc.triggers:
|
||||
line.check_from_to(tr)
|
||||
line.check_pricing()
|
||||
|
||||
@@ -1077,6 +1077,13 @@ class SaleLine(metaclass=PoolMeta):
|
||||
return configurations[0].pricing_rule or ''
|
||||
return ''
|
||||
|
||||
@staticmethod
|
||||
def _has_invalid_delivery_period(line):
|
||||
return (
|
||||
bool(line.from_del)
|
||||
and bool(line.to_del)
|
||||
and line.from_del > line.to_del)
|
||||
|
||||
del_period = fields.Many2One('product.month',"Delivery Period")
|
||||
lots = fields.One2Many('lot.lot','sale_line',"Lots",readonly=True)
|
||||
fees = fields.One2Many('fee.fee', 'sale_line', 'Fees')
|
||||
@@ -1676,13 +1683,16 @@ class SaleLine(metaclass=PoolMeta):
|
||||
default.setdefault('price_pricing', None)
|
||||
return super().copy(lines, default=default)
|
||||
|
||||
@classmethod
|
||||
@classmethod
|
||||
def validate(cls, salelines):
|
||||
LotQtHist = Pool().get('lot.qt.hist')
|
||||
LotQtType = Pool().get('lot.qt.type')
|
||||
Pnl = Pool().get('valuation.valuation')
|
||||
super(SaleLine, cls).validate(salelines)
|
||||
for line in salelines:
|
||||
if cls._has_invalid_delivery_period(line):
|
||||
raise UserError(
|
||||
"Delivery period From date must be before To date.")
|
||||
if line.price_components:
|
||||
for pc in line.price_components:
|
||||
if pc.triggers:
|
||||
|
||||
@@ -6,6 +6,7 @@ from decimal import Decimal
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from trytond.pool import Pool
|
||||
from trytond.pyson import Eval
|
||||
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
|
||||
from trytond.exceptions import UserError
|
||||
from trytond.modules.purchase_trade import valuation as valuation_module
|
||||
@@ -523,6 +524,61 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
self.assertTrue(Pricing.unfixed_qt_price.readonly)
|
||||
self.assertTrue(Pricing.eod_price.readonly)
|
||||
|
||||
def test_pricing_component_domain_is_limited_to_owner_line(self):
|
||||
'pricing component choices are limited to the purchase or sale line'
|
||||
Pricing = Pool().get('pricing.pricing')
|
||||
|
||||
self.assertEqual(Pricing.price_component.domain, [
|
||||
'OR',
|
||||
('line', '=', Eval('line', -1)),
|
||||
('sale_line', '=', Eval('sale_line', -1)),
|
||||
])
|
||||
|
||||
def test_pricing_component_must_belong_to_pricing_owner(self):
|
||||
'pricing rows reject components from another purchase or sale line'
|
||||
Pricing = Pool().get('pricing.pricing')
|
||||
owner = Mock(id=10)
|
||||
other = Mock(id=11)
|
||||
|
||||
self.assertTrue(Pricing._component_matches_owner(Mock(
|
||||
line=owner,
|
||||
sale_line=None,
|
||||
price_component=Mock(line=owner, sale_line=None),
|
||||
)))
|
||||
self.assertFalse(Pricing._component_matches_owner(Mock(
|
||||
line=owner,
|
||||
sale_line=None,
|
||||
price_component=Mock(line=other, sale_line=None),
|
||||
)))
|
||||
self.assertTrue(Pricing._component_matches_owner(Mock(
|
||||
line=None,
|
||||
sale_line=owner,
|
||||
price_component=Mock(line=None, sale_line=owner),
|
||||
)))
|
||||
self.assertFalse(Pricing._component_matches_owner(Mock(
|
||||
line=None,
|
||||
sale_line=owner,
|
||||
price_component=Mock(line=None, sale_line=other),
|
||||
)))
|
||||
|
||||
def test_purchase_and_sale_delivery_period_reject_from_after_to(self):
|
||||
'delivery period dates must be chronological on purchase and sale lines'
|
||||
PurchaseLine = Pool().get('purchase.line')
|
||||
SaleLine = Pool().get('sale.line')
|
||||
invalid = Mock(
|
||||
from_del=datetime.date(2026, 5, 1),
|
||||
to_del=datetime.date(2026, 4, 30),
|
||||
)
|
||||
valid = Mock(
|
||||
from_del=datetime.date(2026, 4, 1),
|
||||
to_del=datetime.date(2026, 4, 30),
|
||||
)
|
||||
|
||||
self.assertTrue(PurchaseLine._has_invalid_delivery_period(invalid))
|
||||
self.assertTrue(SaleLine._has_invalid_delivery_period(invalid))
|
||||
self.assertFalse(PurchaseLine._has_invalid_delivery_period(valid))
|
||||
self.assertFalse(SaleLine._has_invalid_delivery_period(valid))
|
||||
|
||||
def test_pricing_eod_uses_weighted_average_for_manual_rows(self):
|
||||
'manual pricing eod uses the weighted average of fixed and unfixed legs'
|
||||
Pricing = Pool().get('pricing.pricing')
|
||||
|
||||
Reference in New Issue
Block a user