New file import format (ARGUS +ICIS)
This commit is contained in:
@@ -85,6 +85,7 @@ class ImportPricesStart(ModelView):
|
|||||||
|
|
||||||
file_structure = fields.Selection([
|
file_structure = fields.Selection([
|
||||||
('historical', "Historical Prices"),
|
('historical', "Historical Prices"),
|
||||||
|
('historical_all_sources', "Historical Prices (All Sources)"),
|
||||||
('forward', "Forward Prices"),
|
('forward', "Forward Prices"),
|
||||||
], "Excel file structure", required=True)
|
], "Excel file structure", required=True)
|
||||||
file_ = fields.Binary('Excel file', required=True, filename='filename')
|
file_ = fields.Binary('Excel file', required=True, filename='filename')
|
||||||
@@ -132,6 +133,11 @@ class ImportPrices(Wizard):
|
|||||||
'openprice': 'open_price',
|
'openprice': 'open_price',
|
||||||
'pricevalue': 'price_value',
|
'pricevalue': 'price_value',
|
||||||
}
|
}
|
||||||
|
PRICE_INDEX_MAPPING_COLUMNS = (
|
||||||
|
'excel_tab', 'original_price_index', 'source', 'column_destination',
|
||||||
|
'standard_price_index', 'index_description')
|
||||||
|
ALL_SOURCES_VALUE_COLUMNS = {
|
||||||
|
'price_value', 'open_price', 'low_price', 'mid_price', 'high_price'}
|
||||||
|
|
||||||
def transition_import_(self):
|
def transition_import_(self):
|
||||||
rows = self._read_xlsx(
|
rows = self._read_xlsx(
|
||||||
@@ -188,7 +194,7 @@ class ImportPrices(Wizard):
|
|||||||
price = prices[0]
|
price = prices[0]
|
||||||
elif create_missing_price_index:
|
elif create_missing_price_index:
|
||||||
price, = Price.create([
|
price, = Price.create([
|
||||||
cls._price_index_values(price_index)])
|
cls._price_index_values(price_index, row)])
|
||||||
stats['created_indexes'].append(
|
stats['created_indexes'].append(
|
||||||
cls._result_line(
|
cls._result_line(
|
||||||
row_number, price_index, None,
|
row_number, price_index, None,
|
||||||
@@ -234,10 +240,11 @@ class ImportPrices(Wizard):
|
|||||||
return stats
|
return stats
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _price_index_values(cls, price_index):
|
def _price_index_values(cls, price_index, row=None):
|
||||||
|
row = row or {}
|
||||||
values = {
|
values = {
|
||||||
'price_index': price_index,
|
'price_index': price_index,
|
||||||
'price_desc': price_index,
|
'price_desc': row.get('_price_desc') or price_index,
|
||||||
'price_curve_type': 'future',
|
'price_curve_type': 'future',
|
||||||
}
|
}
|
||||||
references = [
|
references = [
|
||||||
@@ -246,6 +253,10 @@ class ImportPrices(Wizard):
|
|||||||
('price_calendar', 'price.calendar', [('name', '=', 'Argus EU')]),
|
('price_calendar', 'price.calendar', [('name', '=', 'Argus EU')]),
|
||||||
('price_unit', 'product.uom', [('name', '=', 'Mt')]),
|
('price_unit', 'product.uom', [('name', '=', 'Mt')]),
|
||||||
]
|
]
|
||||||
|
source = row.get('_source')
|
||||||
|
if source:
|
||||||
|
references.append(
|
||||||
|
('price_area', 'price.area', [('name', '=', source)]))
|
||||||
for field, model_name, domain in references:
|
for field, model_name, domain in references:
|
||||||
record = cls._first_record(model_name, domain)
|
record = cls._first_record(model_name, domain)
|
||||||
if record:
|
if record:
|
||||||
@@ -372,6 +383,9 @@ class ImportPrices(Wizard):
|
|||||||
try:
|
try:
|
||||||
with zipfile.ZipFile(BytesIO(data)) as workbook:
|
with zipfile.ZipFile(BytesIO(data)) as workbook:
|
||||||
shared_strings = cls._read_shared_strings(workbook)
|
shared_strings = cls._read_shared_strings(workbook)
|
||||||
|
if file_structure == 'historical_all_sources':
|
||||||
|
return cls._read_historical_all_sources_price_rows(
|
||||||
|
workbook, shared_strings)
|
||||||
sheet_name = cls._first_sheet_name(workbook)
|
sheet_name = cls._first_sheet_name(workbook)
|
||||||
sheet = ElementTree.fromstring(workbook.read(sheet_name))
|
sheet = ElementTree.fromstring(workbook.read(sheet_name))
|
||||||
except (KeyError, zipfile.BadZipFile, ElementTree.ParseError):
|
except (KeyError, zipfile.BadZipFile, ElementTree.ParseError):
|
||||||
@@ -386,6 +400,163 @@ class ImportPrices(Wizard):
|
|||||||
return cls._read_forward_price_rows(rows, shared_strings, ns)
|
return cls._read_forward_price_rows(rows, shared_strings, ns)
|
||||||
return cls._read_historical_price_rows(rows, shared_strings, ns)
|
return cls._read_historical_price_rows(rows, shared_strings, ns)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _read_historical_all_sources_price_rows(cls, workbook, shared_strings):
|
||||||
|
ns = {'s': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'}
|
||||||
|
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)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _read_historical_all_sources_rows(
|
||||||
|
cls, sheet_rows, mapping, shared_strings, ns):
|
||||||
|
result_by_key = {}
|
||||||
|
mapping_by_tab = {}
|
||||||
|
for mapping_row in mapping:
|
||||||
|
tab = mapping_row.get('excel_tab')
|
||||||
|
destination = mapping_row.get('column_destination')
|
||||||
|
if not tab or destination not in cls.ALL_SOURCES_VALUE_COLUMNS:
|
||||||
|
continue
|
||||||
|
mapping_by_tab.setdefault(tab, []).append(mapping_row)
|
||||||
|
|
||||||
|
for tab, tab_mapping in mapping_by_tab.items():
|
||||||
|
sheet_name = cls._matching_sheet_name(sheet_rows, tab)
|
||||||
|
if not sheet_name:
|
||||||
|
continue
|
||||||
|
rows = sheet_rows[sheet_name]
|
||||||
|
header_row_number, mapped_columns = cls._mapped_columns(
|
||||||
|
rows, tab_mapping, shared_strings, ns)
|
||||||
|
if not mapped_columns:
|
||||||
|
continue
|
||||||
|
date_column = cls._date_column(
|
||||||
|
rows[header_row_number - 1], mapped_columns, shared_strings,
|
||||||
|
ns)
|
||||||
|
|
||||||
|
for excel_row_number, sheet_row in enumerate(
|
||||||
|
rows[header_row_number:], start=header_row_number + 1):
|
||||||
|
row_values = cls._row_values(sheet_row, shared_strings, ns)
|
||||||
|
price_date = row_values.get(date_column)
|
||||||
|
if price_date in (None, ''):
|
||||||
|
continue
|
||||||
|
for column, mapping_row in mapped_columns.items():
|
||||||
|
price_value = row_values.get(column)
|
||||||
|
if price_value in (None, ''):
|
||||||
|
continue
|
||||||
|
price_index = (
|
||||||
|
mapping_row.get('standard_price_index') or '').strip()
|
||||||
|
if not price_index:
|
||||||
|
continue
|
||||||
|
key = (price_index, price_date, excel_row_number)
|
||||||
|
result = result_by_key.setdefault(key, {
|
||||||
|
'_row_number': excel_row_number,
|
||||||
|
'_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
|
||||||
|
return list(result_by_key.values())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _price_index_mapping(cls):
|
||||||
|
cursor = Transaction().connection.cursor()
|
||||||
|
columns = ', '.join(cls.PRICE_INDEX_MAPPING_COLUMNS)
|
||||||
|
cursor.execute(
|
||||||
|
'SELECT %s FROM public."ext_H2SO4_PriceIndexMapping"'
|
||||||
|
% columns)
|
||||||
|
return [
|
||||||
|
dict(zip(cls.PRICE_INDEX_MAPPING_COLUMNS, row))
|
||||||
|
for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _mapped_columns(cls, rows, mapping, shared_strings, ns):
|
||||||
|
mapping_by_header = {}
|
||||||
|
for mapping_row in mapping:
|
||||||
|
header = cls._normalize_mapping_value(
|
||||||
|
mapping_row.get('original_price_index'))
|
||||||
|
mapping_by_header[header] = mapping_row
|
||||||
|
|
||||||
|
hits_by_row = {}
|
||||||
|
columns_by_row = {}
|
||||||
|
for row_number, sheet_row in enumerate(rows, start=1):
|
||||||
|
for cell in sheet_row.findall('s:c', ns):
|
||||||
|
header = cls._normalize_mapping_value(
|
||||||
|
cls._cell_value(cell, shared_strings))
|
||||||
|
mapping_row = mapping_by_header.get(header)
|
||||||
|
if not mapping_row:
|
||||||
|
continue
|
||||||
|
column = cls._cell_column_index(cell.get('r'))
|
||||||
|
hits_by_row[row_number] = hits_by_row.get(row_number, 0) + 1
|
||||||
|
columns_by_row.setdefault(row_number, {})[column] = mapping_row
|
||||||
|
if not hits_by_row:
|
||||||
|
return None, {}
|
||||||
|
header_row_number = max(hits_by_row, key=hits_by_row.get)
|
||||||
|
return header_row_number, columns_by_row[header_row_number]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _date_column(cls, header_row, mapped_columns, shared_strings, ns):
|
||||||
|
for cell in header_row.findall('s:c', ns):
|
||||||
|
if cls._normalize_header(
|
||||||
|
cls._cell_value(cell, shared_strings)) == 'date':
|
||||||
|
return cls._cell_column_index(cell.get('r'))
|
||||||
|
return min(mapped_columns) - 1
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _row_values(cls, sheet_row, shared_strings, ns):
|
||||||
|
values = {}
|
||||||
|
for cell in sheet_row.findall('s:c', ns):
|
||||||
|
values[cls._cell_column_index(cell.get('r'))] = cls._cell_value(
|
||||||
|
cell, shared_strings)
|
||||||
|
return values
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _workbook_sheet_rows(cls, workbook, ns):
|
||||||
|
sheet_rows = {}
|
||||||
|
for sheet_name, sheet_path in cls._worksheet_paths(workbook):
|
||||||
|
sheet = ElementTree.fromstring(workbook.read(sheet_path))
|
||||||
|
sheet_rows[sheet_name] = sheet.findall('.//s:sheetData/s:row', ns)
|
||||||
|
return sheet_rows
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _worksheet_paths(cls, workbook):
|
||||||
|
workbook_xml = ElementTree.fromstring(workbook.read('xl/workbook.xml'))
|
||||||
|
rels_xml = ElementTree.fromstring(
|
||||||
|
workbook.read('xl/_rels/workbook.xml.rels'))
|
||||||
|
wb_ns = {
|
||||||
|
's': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
||||||
|
'r': (
|
||||||
|
'http://schemas.openxmlformats.org/officeDocument/2006/'
|
||||||
|
'relationships'),
|
||||||
|
}
|
||||||
|
rel_ns = {
|
||||||
|
'r': (
|
||||||
|
'http://schemas.openxmlformats.org/package/2006/'
|
||||||
|
'relationships'),
|
||||||
|
}
|
||||||
|
rels = {
|
||||||
|
rel.get('Id'): rel.get('Target')
|
||||||
|
for rel in rels_xml.findall('r:Relationship', rel_ns)}
|
||||||
|
for sheet in workbook_xml.findall('.//s:sheet', wb_ns):
|
||||||
|
rel_id = sheet.get(
|
||||||
|
'{http://schemas.openxmlformats.org/officeDocument/2006/'
|
||||||
|
'relationships}id')
|
||||||
|
target = rels[rel_id]
|
||||||
|
if not target.startswith('/'):
|
||||||
|
target = 'xl/' + target
|
||||||
|
yield sheet.get('name'), target.lstrip('/')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _matching_sheet_name(cls, sheet_rows, mapping_tab):
|
||||||
|
normalized_tab = cls._normalize_mapping_value(mapping_tab)
|
||||||
|
for sheet_name in sheet_rows:
|
||||||
|
if cls._normalize_mapping_value(sheet_name) == normalized_tab:
|
||||||
|
return sheet_name
|
||||||
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _read_historical_price_rows(cls, rows, shared_strings, ns):
|
def _read_historical_price_rows(cls, rows, shared_strings, ns):
|
||||||
headers = {}
|
headers = {}
|
||||||
@@ -540,6 +711,10 @@ class ImportPrices(Wizard):
|
|||||||
def _normalize_header(value):
|
def _normalize_header(value):
|
||||||
return re.sub(r'[^a-z0-9]', '', (value or '').strip().lower())
|
return re.sub(r'[^a-z0-9]', '', (value or '').strip().lower())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_mapping_value(value):
|
||||||
|
return re.sub(r'\s+', ' ', (value or '').strip()).lower()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _as_float(value):
|
def _as_float(value):
|
||||||
if value in (None, ''):
|
if value in (None, ''):
|
||||||
|
|||||||
@@ -5190,6 +5190,111 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
self.assertEqual(result[0]['mid_price'], '75.25')
|
self.assertEqual(result[0]['mid_price'], '75.25')
|
||||||
self.assertEqual(result[0]['high_price'], '75.25')
|
self.assertEqual(result[0]['high_price'], '75.25')
|
||||||
|
|
||||||
|
@with_transaction()
|
||||||
|
def test_import_historical_all_sources_uses_mapping_table(self):
|
||||||
|
'all sources import maps source columns to standard price values'
|
||||||
|
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>Copyright</t></is></c>
|
||||||
|
</row>
|
||||||
|
<row r="2">
|
||||||
|
<c r="A2" t="inlineStr"><is><t>combined_x000D_
|
||||||
|
description</t></is></c>
|
||||||
|
<c r="B2" t="inlineStr"><is><t>Original Mid</t></is></c>
|
||||||
|
<c r="C2" t="inlineStr"><is><t>Original High</t></is></c>
|
||||||
|
<c r="D2" t="inlineStr"><is><t>Original Low</t></is></c>
|
||||||
|
</row>
|
||||||
|
<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="D3"><v>460</v></c>
|
||||||
|
</row>
|
||||||
|
</sheetData>
|
||||||
|
</worksheet>
|
||||||
|
''')
|
||||||
|
rows = sheet.findall('.//s:sheetData/s:row', ns)
|
||||||
|
mapping = [
|
||||||
|
{
|
||||||
|
'excel_tab': 'Argus Download',
|
||||||
|
'original_price_index': 'Original Mid',
|
||||||
|
'source': 'ARGUS',
|
||||||
|
'column_destination': 'mid_price',
|
||||||
|
'standard_price_index': 'ARGUS-CFR Brasil',
|
||||||
|
'index_description': 'Brazil CFR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'excel_tab': 'Argus Download',
|
||||||
|
'original_price_index': 'Original High',
|
||||||
|
'source': 'ARGUS',
|
||||||
|
'column_destination': 'high_price',
|
||||||
|
'standard_price_index': 'ARGUS-CFR Brasil',
|
||||||
|
'index_description': 'Brazil CFR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'excel_tab': 'Argus Download',
|
||||||
|
'original_price_index': 'Original Low',
|
||||||
|
'source': 'ARGUS',
|
||||||
|
'column_destination': 'low_price',
|
||||||
|
'standard_price_index': 'ARGUS-CFR Brasil',
|
||||||
|
'index_description': 'Brazil CFR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'excel_tab': 'Acuity Download',
|
||||||
|
'original_price_index': 'Missing Header',
|
||||||
|
'source': 'ACUITY',
|
||||||
|
'column_destination': 'mid_price',
|
||||||
|
'standard_price_index': 'ACUITY-Missing',
|
||||||
|
'index_description': 'Missing',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
result = ImportPrices._read_historical_all_sources_rows(
|
||||||
|
{'Argus Download': rows}, mapping, [], ns)
|
||||||
|
|
||||||
|
self.assertEqual(len(result), 1)
|
||||||
|
self.assertEqual(result[0]['price_index'], 'ARGUS-CFR Brasil')
|
||||||
|
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]['low_price'], '460')
|
||||||
|
self.assertEqual(result[0]['_source'], 'ARGUS')
|
||||||
|
self.assertEqual(result[0]['_price_desc'], 'Brazil CFR')
|
||||||
|
|
||||||
|
def test_import_historical_all_sources_requires_exact_tab_match(self):
|
||||||
|
'all sources import ignores similarly named source sheets'
|
||||||
|
ImportPrices = Pool().get('purchase_trade.import_prices')
|
||||||
|
|
||||||
|
result = ImportPrices._matching_sheet_name(
|
||||||
|
{
|
||||||
|
'Acuity Download CODELCO': [],
|
||||||
|
},
|
||||||
|
'Acuity Download')
|
||||||
|
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
def test_import_price_index_values_uses_mapping_metadata(self):
|
||||||
|
'created price indexes use mapped description and source area'
|
||||||
|
ImportPrices = Pool().get('purchase_trade.import_prices')
|
||||||
|
area = Mock(id=8)
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
ImportPrices, '_first_record',
|
||||||
|
side_effect=lambda model, domain: (
|
||||||
|
area if model == 'price.area' else None)):
|
||||||
|
values = ImportPrices._price_index_values(
|
||||||
|
'ARGUS-CFR Brasil',
|
||||||
|
{'_price_desc': 'Brazil CFR', '_source': 'ARGUS'})
|
||||||
|
|
||||||
|
self.assertEqual(values['price_index'], 'ARGUS-CFR Brasil')
|
||||||
|
self.assertEqual(values['price_desc'], 'Brazil CFR')
|
||||||
|
self.assertEqual(values['price_area'], 8)
|
||||||
|
|
||||||
def test_pricing_component_matrix_returns_generic_line_price(self):
|
def test_pricing_component_matrix_returns_generic_line_price(self):
|
||||||
'matrix pricing can use an unconditional matrix line as a component price'
|
'matrix pricing can use an unconditional matrix line as a component price'
|
||||||
Component = Pool().get('pricing.component')
|
Component = Pool().get('pricing.component')
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user