Import Price - New Excel file structure

This commit is contained in:
AzureAD\SylvainDUVERNAY
2026-06-01 21:43:46 +02:00
parent 3535567376
commit 77fe2f0589
6 changed files with 131 additions and 5 deletions

Binary file not shown.

View File

@@ -69,12 +69,20 @@ class ImportPricesStart(ModelView):
"Import Prices"
__name__ = 'purchase_trade.import_prices.start'
file_structure = fields.Selection([
('historical', "Historical Prices"),
('forward', "Forward Prices"),
], "Excel file structure", required=True)
file_ = fields.Binary('Excel file', required=True, filename='filename')
filename = fields.Char('Filename')
create_missing_price_index = fields.Boolean(
"Create price index if missing")
overwrite_existing_price = fields.Boolean("Overwrite existing price")
@staticmethod
def default_file_structure():
return 'historical'
class ImportPricesResult(ModelView):
"Import Prices Result"
@@ -112,7 +120,8 @@ class ImportPrices(Wizard):
}
def transition_import_(self):
rows = self._read_xlsx(self.start.file_)
rows = self._read_xlsx(
self.start.file_, file_structure=self.start.file_structure)
stats = self._import_rows(
rows,
create_missing_price_index=(
@@ -142,7 +151,8 @@ class ImportPrices(Wizard):
'errors': [],
}
for row_number, row in enumerate(rows, start=2):
for index, row in enumerate(rows, start=2):
row_number = row.get('_row_number', index)
price_index = (row.get('price_index') or '').strip()
try:
price_date = cls._as_date(row.get('price_date'))
@@ -341,7 +351,7 @@ class ImportPrices(Wizard):
return ', '.join(parts) or 'price imported'
@classmethod
def _read_xlsx(cls, data):
def _read_xlsx(cls, data, file_structure='historical'):
try:
with zipfile.ZipFile(BytesIO(data)) as workbook:
shared_strings = cls._read_shared_strings(workbook)
@@ -355,6 +365,12 @@ class ImportPrices(Wizard):
if not rows:
return []
if file_structure == 'forward':
return cls._read_forward_price_rows(rows, shared_strings, ns)
return cls._read_historical_price_rows(rows, shared_strings, ns)
@classmethod
def _read_historical_price_rows(cls, rows, shared_strings, ns):
headers = {}
for cell in rows[0].findall('s:c', ns):
index = cls._cell_column_index(cell.get('r'))
@@ -381,6 +397,59 @@ class ImportPrices(Wizard):
result.append(values)
return result
@classmethod
def _read_forward_price_rows(cls, rows, shared_strings, ns):
headers = {}
month_terms = {}
for cell in rows[0].findall('s:c', ns):
index = cls._cell_column_index(cell.get('r'))
header = cls._cell_value(cell, shared_strings)
normalized = cls._normalize_header(header)
if index == 1 and normalized == 'priceindex':
headers[index] = 'price_index'
elif index == 2 and normalized == 'pricedate':
headers[index] = 'price_date'
elif index and index >= 3 and header not in (None, ''):
month_terms[index] = cls._as_month_term(header)
missing = []
if headers.get(1) != 'price_index':
missing.append('price_index')
if headers.get(2) != 'price_date':
missing.append('price_date')
if missing:
raise UserError(
"Missing columns in Excel file: %s" % ', '.join(missing))
if not month_terms:
raise UserError(
"Missing month term columns in Excel file.")
result = []
for excel_row_number, sheet_row in enumerate(rows[1:], start=2):
row_values = {}
for cell in sheet_row.findall('s:c', ns):
index = cls._cell_column_index(cell.get('r'))
row_values[index] = cls._cell_value(cell, shared_strings)
price_index = (row_values.get(1) or '').strip()
price_date = row_values.get(2)
if not price_index and price_date in (None, ''):
continue
for index, month_term in sorted(month_terms.items()):
price_value = row_values.get(index)
if price_value in (None, ''):
continue
result.append({
'_row_number': excel_row_number,
'price_index': (
'%s %s' % (price_index, month_term)
if price_index else ''),
'price_date': price_date,
'price_value': price_value,
})
return result
@staticmethod
def _read_shared_strings(workbook):
try:
@@ -473,8 +542,20 @@ class ImportPrices(Wizard):
except ValueError:
pass
raise UserError("Invalid price_date: %s" % text)
class MtmScenario(ModelSQL, ModelView):
@classmethod
def _as_month_term(cls, value):
if isinstance(value, datetime.date):
return value.strftime('%Y-%m')
text = str(value).strip()
match = re.match(r'^(20\d{2})[-_/\. ](0[1-9]|1[0-2])$', text)
if match:
return '%s-%s' % (match.group(1), match.group(2))
if re.match(r'^\d+(\.\d+)?$', text):
return cls._as_date(text).strftime('%Y-%m')
raise UserError("Invalid month term: %s" % text)
class MtmScenario(ModelSQL, ModelView):
"MtM Scenario"
__name__ = 'mtm.scenario'

View File

@@ -5,6 +5,7 @@ from decimal import Decimal
from datetime import date
from types import SimpleNamespace
from unittest.mock import Mock, patch
from xml.etree import ElementTree
from trytond.model.exceptions import ValidationError
from trytond.pool import Pool
@@ -308,6 +309,47 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertIn('Errors:', message)
self.assertIn('Row 2 - LME Copper / 2026-03-27', message)
def test_import_prices_reads_forward_price_matrix(self):
'import prices expands forward month columns into price rows'
ns = {'s': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'}
sheet = ElementTree.fromstring('''
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData>
<row r="1">
<c r="A1" t="inlineStr"><is><t>price_index</t></is></c>
<c r="B1" t="inlineStr"><is><t>price_date</t></is></c>
<c r="C1" t="inlineStr"><is><t>2026-06</t></is></c>
<c r="D1" t="inlineStr"><is><t>2026-07</t></is></c>
<c r="E1" t="inlineStr"><is><t>2026-08</t></is></c>
</row>
<row r="2">
<c r="A2" t="inlineStr"><is><t>CFR Chile</t></is></c>
<c r="B2" t="inlineStr"><is><t>2026-05-01</t></is></c>
<c r="C2"><v>400</v></c>
<c r="D2"><v>410</v></c>
<c r="E2"><v>420</v></c>
</row>
<row r="3">
<c r="A3" t="inlineStr"><is><t>FOB Peru</t></is></c>
<c r="B3" t="inlineStr"><is><t>2026-05-01</t></is></c>
<c r="C3"><v>390</v></c>
<c r="D3"><v>400</v></c>
<c r="E3"><v>410</v></c>
</row>
</sheetData>
</worksheet>
''')
rows = sheet.findall('.//s:sheetData/s:row', ns)
result = ImportPrices._read_forward_price_rows(rows, [], ns)
self.assertEqual(len(result), 6)
self.assertEqual(result[0]['price_index'], 'CFR Chile 2026-06')
self.assertEqual(result[0]['price_date'], '2026-05-01')
self.assertEqual(result[0]['price_value'], '400')
self.assertEqual(result[-1]['price_index'], 'FOB Peru 2026-08')
self.assertEqual(result[-1]['_row_number'], 3)
def test_price_value_unique_rejects_duplicate_create(self):
'price value uniqueness rejects duplicate model creation'
price_value = SimpleNamespace(

View File

@@ -1,4 +1,7 @@
<form col="4">
<label name="file_structure"/>
<field name="file_structure"/>
<newline/>
<label name="file_"/>
<field name="file_"/>
<newline/>