Import Prices

This commit is contained in:
AzureAD\SylvainDUVERNAY
2026-05-06 14:47:57 +02:00
parent 17934fe210
commit 8fb6d681a0
2 changed files with 172 additions and 4 deletions

View File

@@ -15,6 +15,7 @@ from sql.functions import CurrentTimestamp, DateTrunc
from trytond.wizard import Button, StateTransition, StateView, Wizard
from itertools import chain, groupby
from operator import itemgetter
import calendar
import datetime
import logging
import re
@@ -162,10 +163,8 @@ class ImportPrices(Wizard):
if prices:
price = prices[0]
elif create_missing_price_index:
price, = Price.create([{
'price_index': price_index,
'price_desc': price_index,
}])
price, = Price.create([
cls._price_index_values(price_index)])
stats['created_indexes'].append(
cls._result_line(
row_number, price_index, None,
@@ -210,6 +209,70 @@ class ImportPrices(Wizard):
return stats
@classmethod
def _price_index_values(cls, price_index):
values = {
'price_index': price_index,
'price_desc': price_index,
'price_curve_type': 'future',
}
references = [
('price_type', 'price.fixtype', [('name', '=', 'Market price')]),
('price_currency', 'currency.currency', [('code', '=', 'USD')]),
('price_calendar', 'price.calendar', [('name', '=', 'Argus EU')]),
('price_unit', 'product.uom', [('name', '=', 'Mt')]),
]
for field, model_name, domain in references:
record = cls._first_record(model_name, domain)
if record:
values[field] = record.id
period = cls._period_from_price_index(price_index)
if period:
values['price_period'] = period.id
return values
@classmethod
def _period_from_price_index(cls, price_index):
match = re.search(r'(?<!\d)(20\d{2})[-_/\. ](0[1-9]|1[0-2])(?!\d)',
price_index)
if not match:
return None
year = int(match.group(1))
month = int(match.group(2))
month_name = cls._period_month_name(year, month)
periods = cls._period_model().search(
[('month_name', '=', month_name)], limit=1)
if periods:
return periods[0]
beg_date = datetime.date(year, month, 1)
end_date = datetime.date(
year, month, calendar.monthrange(year, month)[1])
period, = cls._period_model().create([{
'month_name': month_name,
'description': month_name,
'beg_date': beg_date,
'end_date': end_date,
'is_cotation': True,
}])
return period
@staticmethod
def _period_month_name(year, month):
return '%s%s' % (
calendar.month_abbr[month].upper(), str(year)[-2:])
@staticmethod
def _period_model():
return Pool().get('product.month')
@staticmethod
def _first_record(model_name, domain):
records = Pool().get(model_name).search(domain, limit=1)
return records[0] if records else None
@classmethod
def _price_value_values(cls, price, row, price_date):
return {

View File

@@ -106,6 +106,15 @@ class PurchaseTradeTestCase(ModuleTestCase):
pool.return_value.get.side_effect = {
'price.price': price_model,
'price.price_value': price_value_model,
'price.fixtype': _FakeRecordModel([
SimpleNamespace(id=10, name='Market price')]),
'currency.currency': _FakeRecordModel([
SimpleNamespace(id=20, code='USD')]),
'price.calendar': _FakeRecordModel([
SimpleNamespace(id=30, name='Argus EU')]),
'product.uom': _FakeRecordModel([
SimpleNamespace(id=40, name='Mt')]),
'product.month': _FakeRecordModel(),
}.__getitem__
stats = ImportPrices._import_rows([{
@@ -122,6 +131,74 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual(price_model.records[0].price_index, 'LME Copper')
self.assertEqual(price_value_model.records[0].price_value, 98.0)
def test_import_prices_defaults_created_price_index(self):
'import prices defaults newly created price index fields'
price_model = _FakePriceModel()
price_value_model = _FakePriceValueModel()
period_model = _FakeRecordModel()
with patch('trytond.modules.purchase_trade.pricing.Pool') as pool:
pool.return_value.get.side_effect = {
'price.price': price_model,
'price.price_value': price_value_model,
'price.fixtype': _FakeRecordModel([
SimpleNamespace(id=10, name='Market price')]),
'currency.currency': _FakeRecordModel([
SimpleNamespace(id=20, code='USD')]),
'price.calendar': _FakeRecordModel([
SimpleNamespace(id=30, name='Argus EU')]),
'product.uom': _FakeRecordModel([
SimpleNamespace(id=40, name='Mt')]),
'product.month': period_model,
}.__getitem__
ImportPrices._import_rows([{
'price_index': 'Argus Copper 2026-07',
'price_date': date(2026, 3, 27),
'price_value': '98',
}], create_missing_price_index=True)
price = price_model.records[0]
period = period_model.records[0]
self.assertEqual(price.price_type, 10)
self.assertEqual(price.price_currency, 20)
self.assertEqual(price.price_calendar, 30)
self.assertEqual(price.price_curve_type, 'future')
self.assertEqual(price.price_unit, 40)
self.assertEqual(price.price_period, period.id)
self.assertEqual(period.month_name, 'JUL26')
self.assertEqual(period.beg_date, date(2026, 7, 1))
self.assertEqual(period.end_date, date(2026, 7, 31))
self.assertTrue(period.is_cotation)
def test_import_prices_uses_existing_period_from_price_index(self):
'import prices reuses existing period parsed from the price index'
price_model = _FakePriceModel()
price_value_model = _FakePriceValueModel()
existing_period = SimpleNamespace(
id=55, month_name='APR25', description='APR25')
period_model = _FakeRecordModel([existing_period])
with patch('trytond.modules.purchase_trade.pricing.Pool') as pool:
pool.return_value.get.side_effect = {
'price.price': price_model,
'price.price_value': price_value_model,
'price.fixtype': _FakeRecordModel(),
'currency.currency': _FakeRecordModel(),
'price.calendar': _FakeRecordModel(),
'product.uom': _FakeRecordModel(),
'product.month': period_model,
}.__getitem__
ImportPrices._import_rows([{
'price_index': 'Argus Copper 2025-04',
'price_date': date(2026, 3, 27),
'price_value': '98',
}], create_missing_price_index=True)
self.assertEqual(price_model.records[0].price_period, 55)
self.assertEqual(period_model.records, [existing_period])
def test_import_prices_skips_existing_date_without_overwrite(self):
'import prices skips existing price dates unless overwrite is enabled'
price = SimpleNamespace(id=1, price_index='LME Copper')
@@ -278,4 +355,32 @@ class _FakePriceValueModel:
setattr(record, name, value)
class _FakeRecordModel:
def __init__(self, records=None):
self.records = records or []
def search(self, domain, limit=None):
records = self.records
for name, operator, value in domain:
self.assert_operator(operator)
records = [
record for record in records
if getattr(record, name, None) == value]
return records[:limit] if limit else records
def create(self, values):
records = []
for value in values:
record = SimpleNamespace(id=len(self.records) + 1, **value)
self.records.append(record)
records.append(record)
return records
@staticmethod
def assert_operator(operator):
if operator != '=':
raise AssertionError('Unsupported operator: %s' % operator)
del ModuleTestCase