Merge branch 'main' of https://gitea.open-squared.tech/admin/tradon
This commit is contained in:
@@ -133,6 +133,9 @@ class ImportPrices(Wizard):
|
||||
'openprice': 'open_price',
|
||||
'pricevalue': 'price_value',
|
||||
}
|
||||
HISTORICAL_REQUIRED_COLUMNS = {
|
||||
'price_index', 'price_date', 'high_price', 'low_price',
|
||||
'open_price', 'price_value'}
|
||||
PRICE_INDEX_MAPPING_COLUMNS = (
|
||||
'excel_tab', 'original_price_index', 'source', 'column_destination',
|
||||
'standard_price_index', 'index_description')
|
||||
@@ -308,17 +311,28 @@ class ImportPrices(Wizard):
|
||||
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 {
|
||||
'price': price.id,
|
||||
'price_date': price_date,
|
||||
'high_price': cls._as_float(row.get('high_price')),
|
||||
'low_price': cls._as_float(row.get('low_price')),
|
||||
'mid_price': cls._as_float(row.get('mid_price')),
|
||||
@classmethod
|
||||
def _price_value_values(cls, price, row, price_date):
|
||||
high_price = cls._as_float(row.get('high_price'))
|
||||
low_price = cls._as_float(row.get('low_price'))
|
||||
mid_price = cls._as_float(row.get('mid_price'))
|
||||
if not row.get('_preserve_mid_price'):
|
||||
mid_price = cls._mid_price(low_price, high_price, mid_price)
|
||||
return {
|
||||
'price': price.id,
|
||||
'price_date': price_date,
|
||||
'high_price': high_price,
|
||||
'low_price': low_price,
|
||||
'mid_price': mid_price,
|
||||
'open_price': cls._as_float(row.get('open_price')),
|
||||
'price_value': cls._as_float(row.get('price_value')),
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _mid_price(low_price, high_price, fallback=None):
|
||||
if low_price is not None and high_price is not None:
|
||||
return (low_price + high_price) / 2
|
||||
return fallback
|
||||
|
||||
@staticmethod
|
||||
def _result_line(row_number, price_index, price_date, detail):
|
||||
@@ -378,7 +392,7 @@ class ImportPrices(Wizard):
|
||||
parts.append('%s=%s' % (name, value))
|
||||
return ', '.join(parts) or 'price imported'
|
||||
|
||||
@classmethod
|
||||
@classmethod
|
||||
def _read_xlsx(cls, data, file_structure='historical'):
|
||||
try:
|
||||
with zipfile.ZipFile(BytesIO(data)) as workbook:
|
||||
@@ -389,21 +403,25 @@ class ImportPrices(Wizard):
|
||||
sheet_name = cls._first_sheet_name(workbook)
|
||||
sheet = ElementTree.fromstring(workbook.read(sheet_name))
|
||||
except (KeyError, zipfile.BadZipFile, ElementTree.ParseError):
|
||||
raise UserError("The selected file is not a valid Excel .xlsx file.")
|
||||
|
||||
ns = {'s': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'}
|
||||
rows = sheet.findall('.//s:sheetData/s:row', ns)
|
||||
raise UserError("The selected file is not a valid Excel .xlsx file.")
|
||||
|
||||
ns = {'s': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'}
|
||||
rows = sheet.findall('.//s:sheetData/s:row', ns)
|
||||
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)
|
||||
if file_structure == 'historical':
|
||||
return cls._read_historical_price_rows(rows, shared_strings, ns)
|
||||
raise UserError("Unknown Excel file structure: %s" % file_structure)
|
||||
|
||||
@classmethod
|
||||
def _read_historical_all_sources_price_rows(cls, workbook, shared_strings):
|
||||
def _read_historical_all_sources_price_rows(
|
||||
cls, workbook, shared_strings, sheet_rows=None):
|
||||
ns = {'s': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'}
|
||||
sheet_rows = cls._workbook_sheet_rows(workbook, ns)
|
||||
if sheet_rows is None:
|
||||
sheet_rows = cls._workbook_sheet_rows(workbook, ns)
|
||||
mapping = cls._price_index_mapping()
|
||||
return cls._read_historical_all_sources_rows(
|
||||
sheet_rows, mapping, shared_strings, ns)
|
||||
@@ -450,15 +468,18 @@ class ImportPrices(Wizard):
|
||||
key = (price_index, price_date, excel_row_number)
|
||||
result = result_by_key.setdefault(key, {
|
||||
'_row_number': excel_row_number,
|
||||
'_preserve_mid_price': True,
|
||||
'_source': mapping_row.get('source'),
|
||||
'_price_desc': mapping_row.get('index_description'),
|
||||
'price_index': price_index,
|
||||
'price_date': price_date,
|
||||
})
|
||||
result[mapping_row['column_destination']] = price_value
|
||||
if (mapping_row['column_destination'] == 'mid_price'
|
||||
and not result.get('price_value')):
|
||||
result['price_value'] = price_value
|
||||
if mapping_row['column_destination'] == 'mid_price':
|
||||
if not result.get('price_value'):
|
||||
result['price_value'] = price_value
|
||||
if not result.get('open_price'):
|
||||
result['open_price'] = price_value
|
||||
return list(result_by_key.values())
|
||||
|
||||
@classmethod
|
||||
@@ -514,7 +535,9 @@ class ImportPrices(Wizard):
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def _workbook_sheet_rows(cls, workbook, ns):
|
||||
def _workbook_sheet_rows(cls, workbook, ns=None):
|
||||
ns = ns or {
|
||||
's': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'}
|
||||
sheet_rows = {}
|
||||
for sheet_name, sheet_path in cls._worksheet_paths(workbook):
|
||||
sheet = ElementTree.fromstring(workbook.read(sheet_path))
|
||||
@@ -567,7 +590,7 @@ class ImportPrices(Wizard):
|
||||
if normalized in cls.REQUIRED_COLUMNS:
|
||||
headers[index] = cls.REQUIRED_COLUMNS[normalized]
|
||||
|
||||
missing = set(cls.REQUIRED_COLUMNS.values()) - set(headers.values())
|
||||
missing = cls.HISTORICAL_REQUIRED_COLUMNS - set(headers.values())
|
||||
if missing:
|
||||
raise UserError(
|
||||
"Missing columns in Excel file: %s"
|
||||
@@ -732,10 +755,10 @@ class ImportPrices(Wizard):
|
||||
return (
|
||||
datetime.date(1899, 12, 30)
|
||||
+ datetime.timedelta(days=int(float(text))))
|
||||
for fmt in ('%Y-%m-%d', '%d/%m/%Y', '%m/%d/%Y'):
|
||||
try:
|
||||
return datetime.datetime.strptime(text, fmt).date()
|
||||
except ValueError:
|
||||
for fmt in ('%Y-%m-%d', '%d/%m/%Y', '%m/%d/%Y', '%d.%m.%Y'):
|
||||
try:
|
||||
return datetime.datetime.strptime(text, fmt).date()
|
||||
except ValueError:
|
||||
pass
|
||||
raise UserError("Invalid price_date: %s" % text)
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
import datetime
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from contextlib import nullcontext
|
||||
from decimal import Decimal
|
||||
from io import BytesIO
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import ANY, Mock, call, patch
|
||||
from xml.etree import ElementTree
|
||||
@@ -5232,6 +5234,57 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
self.assertEqual(result[0]['mid_price'], '75.25')
|
||||
self.assertEqual(result[0]['high_price'], '75.25')
|
||||
|
||||
def test_import_historical_prices_computes_mid_from_low_high(self):
|
||||
'historical import computes mid price from low and high prices'
|
||||
ImportPrices = Pool().get('purchase_trade.import_prices')
|
||||
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>High Price</t></is></c>
|
||||
<c r="D1" t="inlineStr"><is><t>Low Price</t></is></c>
|
||||
<c r="E1" t="inlineStr"><is><t>Open Price</t></is></c>
|
||||
<c r="F1" t="inlineStr"><is><t>Price Value</t></is></c>
|
||||
</row>
|
||||
<row r="2">
|
||||
<c r="A2" t="inlineStr"><is><t>ARGUS-CFR Brasil</t></is></c>
|
||||
<c r="B2"><v>46191</v></c>
|
||||
<c r="C2"><v>482</v></c>
|
||||
<c r="D2"><v>460</v></c>
|
||||
<c r="E2"><v>470</v></c>
|
||||
<c r="F2"><v>470</v></c>
|
||||
</row>
|
||||
</sheetData>
|
||||
</worksheet>
|
||||
''')
|
||||
rows = sheet.findall('.//s:sheetData/s:row', ns)
|
||||
|
||||
result = ImportPrices._read_historical_price_rows(rows, [], ns)
|
||||
values = ImportPrices._price_value_values(
|
||||
Mock(id=1), result[0], datetime.date(2026, 6, 18))
|
||||
|
||||
self.assertNotIn('mid_price', result[0])
|
||||
self.assertEqual(values['mid_price'], 471)
|
||||
|
||||
def test_import_forward_price_values_compute_mid_from_low_high(self):
|
||||
'forward import value builder computes mid price from low and high'
|
||||
ImportPrices = Pool().get('purchase_trade.import_prices')
|
||||
|
||||
values = ImportPrices._price_value_values(
|
||||
Mock(id=1), {
|
||||
'high_price': '482',
|
||||
'low_price': '460',
|
||||
'mid_price': '0',
|
||||
'open_price': '470',
|
||||
'price_value': '470',
|
||||
},
|
||||
datetime.date(2026, 6, 18))
|
||||
|
||||
self.assertEqual(values['mid_price'], 471)
|
||||
|
||||
@with_transaction()
|
||||
def test_import_historical_all_sources_uses_mapping_table(self):
|
||||
'all sources import maps source columns to standard price values'
|
||||
@@ -5253,7 +5306,7 @@ description</t></is></c>
|
||||
<row r="3">
|
||||
<c r="A3"><v>46191</v></c>
|
||||
<c r="B3"><v>470</v></c>
|
||||
<c r="C3"><v>480</v></c>
|
||||
<c r="C3"><v>482</v></c>
|
||||
<c r="D3"><v>460</v></c>
|
||||
</row>
|
||||
</sheetData>
|
||||
@@ -5303,10 +5356,75 @@ description</t></is></c>
|
||||
self.assertEqual(result[0]['price_date'], '46191')
|
||||
self.assertEqual(result[0]['mid_price'], '470')
|
||||
self.assertEqual(result[0]['price_value'], '470')
|
||||
self.assertEqual(result[0]['high_price'], '480')
|
||||
self.assertEqual(result[0]['open_price'], '470')
|
||||
self.assertEqual(result[0]['high_price'], '482')
|
||||
self.assertEqual(result[0]['low_price'], '460')
|
||||
self.assertEqual(result[0]['_source'], 'ARGUS')
|
||||
self.assertEqual(result[0]['_price_desc'], 'Brazil CFR')
|
||||
values = ImportPrices._price_value_values(
|
||||
Mock(id=1), result[0], datetime.date(2026, 6, 18))
|
||||
self.assertEqual(values['mid_price'], 470)
|
||||
|
||||
def test_import_xlsx_uses_selected_parser(self):
|
||||
'price import selects parser from requested file structure'
|
||||
ImportPrices = Pool().get('purchase_trade.import_prices')
|
||||
workbook = BytesIO()
|
||||
with zipfile.ZipFile(workbook, 'w') as archive:
|
||||
archive.writestr('xl/workbook.xml', '''
|
||||
<workbook
|
||||
xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||||
<sheets>
|
||||
<sheet name="Prices" sheetId="1" r:id="rId1"/>
|
||||
</sheets>
|
||||
</workbook>
|
||||
''')
|
||||
archive.writestr('xl/_rels/workbook.xml.rels', '''
|
||||
<Relationships
|
||||
xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1" Target="worksheets/sheet1.xml"/>
|
||||
</Relationships>
|
||||
''')
|
||||
archive.writestr('xl/worksheets/sheet1.xml', '''
|
||||
<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>
|
||||
</row>
|
||||
<row r="2">
|
||||
<c r="A2" t="inlineStr"><is><t>ARGUS-CFR Brasil</t></is></c>
|
||||
<c r="B2"><v>46191</v></c>
|
||||
</row>
|
||||
</sheetData>
|
||||
</worksheet>
|
||||
''')
|
||||
data = workbook.getvalue()
|
||||
|
||||
with patch.object(
|
||||
ImportPrices, '_read_historical_price_rows',
|
||||
return_value=['historical']) as historical, \
|
||||
patch.object(
|
||||
ImportPrices, '_read_historical_all_sources_price_rows',
|
||||
return_value=['all_sources']) as all_sources, \
|
||||
patch.object(
|
||||
ImportPrices, '_read_forward_price_rows',
|
||||
return_value=['forward']) as forward:
|
||||
self.assertEqual(
|
||||
ImportPrices._read_xlsx(
|
||||
data, file_structure='historical'),
|
||||
['historical'])
|
||||
self.assertEqual(
|
||||
ImportPrices._read_xlsx(
|
||||
data, file_structure='historical_all_sources'),
|
||||
['all_sources'])
|
||||
self.assertEqual(
|
||||
ImportPrices._read_xlsx(data, file_structure='forward'),
|
||||
['forward'])
|
||||
|
||||
self.assertEqual(historical.call_count, 1)
|
||||
self.assertEqual(all_sources.call_count, 1)
|
||||
self.assertEqual(forward.call_count, 1)
|
||||
|
||||
def test_import_historical_all_sources_requires_exact_tab_match(self):
|
||||
'all sources import ignores similarly named source sheets'
|
||||
@@ -5337,6 +5455,14 @@ description</t></is></c>
|
||||
self.assertEqual(values['price_desc'], 'Brazil CFR')
|
||||
self.assertEqual(values['price_area'], 8)
|
||||
|
||||
def test_import_price_date_accepts_dot_format(self):
|
||||
'price import accepts dd.mm.yyyy dates'
|
||||
ImportPrices = Pool().get('purchase_trade.import_prices')
|
||||
|
||||
result = ImportPrices._as_date('18.06.2026')
|
||||
|
||||
self.assertEqual(result, datetime.date(2026, 6, 18))
|
||||
|
||||
def test_pricing_component_matrix_returns_generic_line_price(self):
|
||||
'matrix pricing can use an unconditional matrix line as a component price'
|
||||
Component = Pool().get('pricing.component')
|
||||
|
||||
Reference in New Issue
Block a user