Check From To + pricing component domain filter

This commit is contained in:
2026-04-28 19:23:42 +02:00
parent aa9a18fb93
commit 30d4362c09
7 changed files with 276 additions and 12 deletions

View File

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

View File

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

View File

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

View File

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

View File

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