Pricing adj
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user