Pricing adj

This commit is contained in:
2026-06-14 09:48:39 +02:00
parent 8a60a8e67f
commit 9c92904b3e
6 changed files with 234 additions and 73 deletions

View File

@@ -68,17 +68,22 @@ class Price(
amount = Decimal(self.get_price_per_qt(price,unit,currency)) * Decimal(Uom.compute_qty(self.price_unit, float(self.price_ct_size * nb_ct), unit))
return round(amount,4)
def get_price(self,dt,unit,currency,last=False):
price = float(0)
PV = Pool().get('price.price_value')
if self.price_values:
dt = dt.strftime("%Y-%m-%d")
pv = PV.search([('price','=',self.id),('price_date','=',dt)])
if not pv and last:
pv = PV.search([('price','=',self.id)],order=[('price_date', 'DESC')])
if pv:
price = self.get_price_per_qt(pv[0].price_value,unit,currency)
return round(price,4)
def get_price(self,dt,unit,currency,last=False, relative_last=False):
price = float(0)
PV = Pool().get('price.price_value')
if self.price_values:
dt = dt.strftime("%Y-%m-%d")
pv = PV.search([('price','=',self.id),('price_date','=',dt)])
if not pv and last:
pv = PV.search([('price','=',self.id)],order=[('price_date', 'DESC')])
elif not pv and relative_last:
pv = PV.search([
('price','=',self.id),
('price_date','<=',dt),
], order=[('price_date', 'DESC')])
if pv:
price = self.get_price_per_qt(pv[0].price_value,unit,currency)
return round(price,4)
class FixType(ModelSQL,ModelView):
"Fixation type"

View File

@@ -919,6 +919,7 @@ class Component(ModelSQL, ModelView):
'to_p': getattr(trigger, 'to_p', None),
'average': getattr(trigger, 'average', None),
'last': getattr(trigger, 'last', None),
'relative_last': getattr(trigger, 'relative_last', None),
'application_period': cls._record_id(
getattr(trigger, 'application_period', None)),
'from_a': getattr(trigger, 'from_a', None),
@@ -1077,11 +1078,14 @@ class Component(ModelSQL, ModelView):
price_qt *= Decimal(str(rate))
return round(price_qt, 4)
def get_price(self, price_date, unit, currency, last=False):
if self.price_source_type == 'curve' and self.price_index:
PI = Pool().get('price.price')
pi = PI(self.price_index)
return pi.get_price(price_date, unit, currency, last)
def get_price(
self, price_date, unit, currency, last=False,
relative_last=False):
if self.price_source_type == 'curve' and self.price_index:
PI = Pool().get('price.price')
pi = PI(self.price_index)
return pi.get_price(
price_date, unit, currency, last, relative_last)
if self.price_source_type == 'matrix' and self.price_matrix:
if not self._matrix_valid_on(price_date):
return Decimal(0)
@@ -1531,7 +1535,8 @@ class Trigger(ModelSQL,ModelView):
'readonly': Eval('pricing_period') != None,
})
average = fields.Boolean("Avg")
last = fields.Boolean("Latest")
last = fields.Boolean("AL", help="Absolute Last")
relative_last = fields.Boolean("RL", help="Relative Last")
application_period = fields.Many2One('pricing.period',"Application period")
from_a = fields.Date("From",
states={
@@ -1561,26 +1566,26 @@ class Trigger(ModelSQL,ModelView):
if not self.application_period and not self.to_a:
self.to_a = self.to_p
def getDateWithEstTrigger(self, period):
PP = Pool().get('pricing.period')
if period == 1:
pp = PP(self.pricing_period)
else:
pp = PP(self.application_period)
def getDateWithEstTrigger(self, period, cal=None):
PP = Pool().get('pricing.period')
if period == 1:
pp = PP(self.pricing_period)
else:
pp = PP(self.application_period)
CO = Pool().get('pricing.component')
co = CO(self.component)
if co.line:
d = co.getEstimatedTriggerPurchase(pp.trigger)
else:
d = co.getEstimatedTriggerSale(pp.trigger)
date_from,date_to,dates = pp.getDates(d)
return date_from,date_to,d,pp.include,dates
def getApplicationListDates(self, cal):
ld = []
dates = []
if self.application_period:
date_from, date_to, d, include,dates = self.getDateWithEstTrigger(2)
else:
d = co.getEstimatedTriggerSale(pp.trigger)
date_from,date_to,dates = pp.getDates(d, cal)
return date_from,date_to,d,pp.include,dates
def getApplicationListDates(self, cal):
ld = []
dates = []
if self.application_period:
date_from, date_to, d, include,dates = self.getDateWithEstTrigger(2, cal)
else:
date_from = self.from_a or self.from_p
date_to = self.to_a or self.to_p
@@ -1589,11 +1594,11 @@ class Trigger(ModelSQL,ModelView):
ld, lprice = self.getListDates(date_from,date_to,d,include,cal,2,dates)
return ld, lprice
def getPricingListDates(self,cal):
ld = []
dates = []
if self.pricing_period:
date_from, date_to, d, include,dates = self.getDateWithEstTrigger(1)
def getPricingListDates(self,cal):
ld = []
dates = []
if self.pricing_period:
date_from, date_to, d, include,dates = self.getDateWithEstTrigger(1, cal)
else:
date_from = self.from_p#datetime.datetime(self.from_p.year, self.from_p.month, self.from_p.day)
date_to = self.to_p#datetime.datetime(self.to_p.year, self.to_p.month, self.to_p.day)
@@ -1636,10 +1641,11 @@ class Trigger(ModelSQL,ModelView):
val['date'] = current_date
line = pc.line if pc.line else pc.sale_line
val['price'] = pc.get_price(
current_date,
line.unit,
line.currency,
self.last)
current_date,
line.unit,
line.currency,
self.last,
self.relative_last)
val['avg'] = val['price']
val['avg_minus_1'] = val['price']
val['isAvg'] = self.average
@@ -1677,12 +1683,22 @@ class Period(ModelSQL,ModelView):
def default_nbme(cls):
return 0
def getDates(self,t):
date_from = None
date_to = None
dates = []
if t:
if self.every:
@staticmethod
def _calendar_is_quote(cal, date):
if not cal:
return True
if not hasattr(cal, 'IsQuote'):
Calendar = Pool().get('price.calendar')
cal = Calendar(cal)
result = cal.IsQuote(date)
return True if result is None else bool(result)
def getDates(self,t, cal=None):
date_from = None
date_to = None
dates = []
if t:
if self.every:
if t:
j = self.every
if j not in WEEKDAY_MAP:
@@ -1705,14 +1721,23 @@ class Period(ModelSQL,ModelView):
current += datetime.timedelta(days=7)
elif self.nb_quotation < 0:
days_to_sub = (t.weekday() - weekday_target) % 7
current = t - datetime.timedelta(days=days_to_sub)
while len(dates) < -self.nb_quotation:
dates.append(datetime.datetime(current.year, current.month, current.day))
current -= datetime.timedelta(days=7)
else:
if self.startday == 'before':
date_from = t - datetime.timedelta(days=(self.nbds if self.nbds else 0))
current = t - datetime.timedelta(days=days_to_sub)
while len(dates) < -self.nb_quotation:
dates.append(datetime.datetime(current.year, current.month, current.day))
current -= datetime.timedelta(days=7)
elif self.nb_quotation:
current = t - datetime.timedelta(days=1)
while len(dates) < abs(self.nb_quotation):
date = datetime.datetime(
current.year, current.month, current.day)
if self._calendar_is_quote(cal, date):
dates.append(date)
current -= datetime.timedelta(days=1)
dates.reverse()
else:
if self.startday == 'before':
date_from = t - datetime.timedelta(days=(self.nbds if self.nbds else 0))
elif self.startday == 'after':
date_from = t + datetime.timedelta(days=(self.nbds if self.nbds else 0))
elif self.startday == 'first':

View File

@@ -3,7 +3,7 @@
import datetime
from decimal import Decimal
from unittest.mock import Mock, patch
from unittest.mock import ANY, Mock, patch
from xml.etree import ElementTree
from trytond.pool import Pool
@@ -3321,6 +3321,89 @@ class PurchaseTradeTestCase(ModuleTestCase):
])
self.assertEqual(len(prices), 3)
def test_pricing_period_every_positive_returns_weekdays_after_trigger(self):
'Every Friday and 4 returns the four Fridays from/after trigger date'
Period = Pool().get('pricing.period')
period = Period()
period.trigger = 'bldate'
period.every = 'friday'
period.nb_quotation = 4
_, _, dates = period.getDates(datetime.date(2026, 4, 1))
self.assertEqual(
[date.date() for date in dates],
[
datetime.date(2026, 4, 3),
datetime.date(2026, 4, 10),
datetime.date(2026, 4, 17),
datetime.date(2026, 4, 24),
])
def test_pricing_period_every_negative_returns_weekdays_before_trigger(self):
'Every Friday and -4 returns the four Fridays before trigger date'
Period = Pool().get('pricing.period')
period = Period()
period.trigger = 'bldate'
period.every = 'friday'
period.nb_quotation = -4
_, _, dates = period.getDates(datetime.date(2026, 4, 30))
self.assertEqual(
[date.date() for date in dates],
[
datetime.date(2026, 4, 24),
datetime.date(2026, 4, 17),
datetime.date(2026, 4, 10),
datetime.date(2026, 4, 3),
])
def test_pricing_period_blank_every_uses_available_dates_before_trigger(self):
'Blank Every and Nb quotation returns available dates before trigger'
Period = Pool().get('pricing.period')
calendar = Mock()
excluded = {
datetime.date(2026, 4, 8),
datetime.date(2026, 4, 10),
}
calendar.IsQuote = Mock(
side_effect=lambda date: date.date() not in excluded)
period = Period()
period.trigger = 'bldate'
period.every = None
period.nb_quotation = 4
_, _, dates = period.getDates(datetime.date(2026, 4, 13), calendar)
self.assertEqual(
[date.date() for date in dates],
[
datetime.date(2026, 4, 6),
datetime.date(2026, 4, 7),
datetime.date(2026, 4, 9),
datetime.date(2026, 4, 12),
])
def test_pricing_period_blank_every_negative_uses_available_dates_before_trigger(self):
'Blank Every and negative Nb quotation also returns dates before trigger'
Period = Pool().get('pricing.period')
period = Period()
period.trigger = 'bldate'
period.every = None
period.nb_quotation = -4
_, _, dates = period.getDates(datetime.date(2026, 4, 13))
self.assertEqual(
[date.date() for date in dates],
[
datetime.date(2026, 4, 9),
datetime.date(2026, 4, 10),
datetime.date(2026, 4, 11),
datetime.date(2026, 4, 12),
])
def test_purchase_line_get_avg_accepts_decimal_matrix_prices(self):
'purchase pricing averages Decimal matrix prices without float mixing'
PurchaseLine = Pool().get('purchase.line')
@@ -3388,12 +3471,13 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual(trigger.from_a, datetime.date(2026, 4, 1))
self.assertEqual(trigger.to_a, datetime.date(2026, 4, 30))
def test_pricing_trigger_average_defaults_to_true_and_last_label_is_latest(self):
'pricing trigger defaults to Avg and labels last as Latest'
def test_pricing_trigger_average_defaults_to_true_and_last_modes(self):
'pricing trigger defaults to Avg and exposes AL/RL last modes'
Trigger = Pool().get('pricing.trigger')
self.assertTrue(Trigger.default_average())
self.assertEqual(Trigger.last.string, 'Latest')
self.assertEqual(Trigger.last.string, 'AL')
self.assertEqual(Trigger.relative_last.string, 'RL')
@with_transaction()
def test_import_forward_prices_copies_value_to_ohl_fields(self):
@@ -3457,6 +3541,48 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual(price, Decimal('45.0000'))
def test_pricing_component_curve_passes_relative_last(self):
'curve pricing can request the price relative last before target date'
Component = Pool().get('pricing.component')
price_index = Mock()
component = Component()
component.price_source_type = 'curve'
component.price_index = price_index
price_model = Mock(return_value=price_index)
price_index.get_price = Mock(return_value=Decimal('101'))
with patch('trytond.modules.purchase_trade.pricing.Pool') as PricingPool:
PricingPool.return_value.get.return_value = price_model
price = component.get_price(
datetime.date(2026, 4, 1), Mock(), Mock(), False, True)
self.assertEqual(price, Decimal('101'))
price_index.get_price.assert_called_once_with(
datetime.date(2026, 4, 1), ANY, ANY, False, True)
def test_price_get_price_relative_last_uses_latest_before_date(self):
'relative last picks latest available price before target date'
Price = Pool().get('price.price')
price = Price()
price.id = 42
price.price_values = [Mock()]
price.get_price_per_qt = Mock(return_value=Decimal('88'))
previous_value = Mock(price_value=Decimal('88'))
price_value_model = Mock()
price_value_model.search = Mock(side_effect=[[], [previous_value]])
with patch('trytond.modules.price.price.Pool') as PricePool:
PricePool.return_value.get.return_value = price_value_model
result = price.get_price(
datetime.date(2026, 4, 10), Mock(), Mock(),
last=False, relative_last=True)
self.assertEqual(result, Decimal('88'))
price_value_model.search.assert_any_call([
('price', '=', 42),
('price_date', '<=', '2026-04-10'),
], order=[('price_date', 'DESC')])
def test_pricing_component_matrix_prefers_specific_line_over_generic(self):
'matrix pricing keeps generic lines as fallback below matching lines'
Component = Pool().get('pricing.component')

View File

@@ -5,11 +5,13 @@
<field name="from_p"/>
<label name="to_p"/>
<field name="to_p"/>
<label name="average"/>
<field name="average"/>
<label name="last"/>
<field name="last"/>
<label name="application_period"/>
<label name="average"/>
<field name="average"/>
<label name="last"/>
<field name="last"/>
<label name="relative_last"/>
<field name="relative_last"/>
<label name="application_period"/>
<field name="application_period"/>
<label name="from_a"/>
<field name="from_a"/>

View File

@@ -1,10 +1,11 @@
<tree editable="1">
<field name="pricing_period"/>
<field name="from_p"/>
<field name="to_p"/>
<field name="average"/>
<field name="last"/>
<field name="application_period"/>
<field name="to_p"/>
<field name="average"/>
<field name="last"/>
<field name="relative_last"/>
<field name="application_period"/>
<field name="from_a"/>
<field name="to_a"/>
</tree>

View File

@@ -1,9 +1,11 @@
<tree editable="1">
<field name="pricing_period"/>
<field name="from_p"/>
<field name="to_p"/>
<field name="average"/>
<field name="application_period"/>
<field name="from_p"/>
<field name="to_p"/>
<field name="average"/>
<field name="last"/>
<field name="relative_last"/>
<field name="application_period"/>
<field name="from_a"/>
<field name="to_a"/>
</tree>