Import Prices

This commit is contained in:
AzureAD\SylvainDUVERNAY
2026-05-06 14:21:51 +02:00
parent 3a0ea871c2
commit ef54b5d94b
2 changed files with 180 additions and 64 deletions

View File

@@ -117,9 +117,16 @@ class ImportPrices(Wizard):
create_missing_price_index=(
self.start.create_missing_price_index),
overwrite_existing_price=self.start.overwrite_existing_price)
self.result.message = self._format_result(stats)
self._result_message = self._format_result(stats)
return 'result'
def default_result(self, fields):
return {
'message': getattr(
self, '_result_message',
'No import result was produced.'),
}
@classmethod
def _import_rows(
cls, rows, create_missing_price_index=False,
@@ -127,64 +134,87 @@ class ImportPrices(Wizard):
Price = Pool().get('price.price')
PriceValue = Pool().get('price.price_value')
stats = {
'created_indexes': 0,
'imported': 0,
'updated': 0,
'created_indexes': [],
'imported': [],
'updated': [],
'skipped': [],
'errors': [],
}
for row_number, row in enumerate(rows, start=2):
price_index = (row.get('price_index') or '').strip()
price_date = row.get('price_date')
if not price_index:
stats['skipped'].append(
cls._skip(row_number, '', 'missing price_index'))
continue
if not price_date:
stats['skipped'].append(
cls._skip(row_number, price_index, 'missing price_date'))
continue
try:
price_date = cls._as_date(row.get('price_date'))
if not price_index:
stats['skipped'].append(
cls._result_line(
row_number, '', None, 'missing price_index'))
continue
if not price_date:
stats['skipped'].append(
cls._result_line(
row_number, price_index, None,
'missing price_date'))
continue
prices = Price.search([('price_index', '=', price_index)], limit=1)
if prices:
price = prices[0]
elif create_missing_price_index:
price, = Price.create([{
'price_index': price_index,
'price_desc': price_index,
}])
stats['created_indexes'] += 1
else:
stats['skipped'].append(
cls._skip(row_number, price_index, 'price_index missing'))
continue
values = cls._price_value_values(price, row)
existing = PriceValue.search([
('price', '=', price.id),
('price_date', '=', price_date),
], limit=1)
if existing:
if overwrite_existing_price:
PriceValue.write(existing, values)
stats['updated'] += 1
prices = Price.search(
[('price_index', '=', price_index)], limit=1)
if prices:
price = prices[0]
elif create_missing_price_index:
price, = Price.create([{
'price_index': price_index,
'price_desc': price_index,
}])
stats['created_indexes'].append(
cls._result_line(
row_number, price_index, None,
'price index created'))
else:
stats['skipped'].append(cls._skip(
row_number, price_index,
'price_date already exists'))
continue
stats['skipped'].append(
cls._result_line(
row_number, price_index, None,
'price_index missing'))
continue
PriceValue.create([values])
stats['imported'] += 1
values = cls._price_value_values(price, row, price_date)
existing = PriceValue.search([
('price', '=', price.id),
('price_date', '=', price_date),
], limit=1)
if existing:
if overwrite_existing_price:
PriceValue.write(existing, values)
stats['updated'].append(
cls._result_line(
row_number, price_index, price_date,
cls._price_summary(values)))
else:
stats['skipped'].append(cls._result_line(
row_number, price_index, price_date,
'price_date already exists'))
continue
PriceValue.create([values])
stats['imported'].append(
cls._result_line(
row_number, price_index, price_date,
cls._price_summary(values)))
except Exception as exception:
stats['errors'].append(
cls._result_line(
row_number, price_index, row.get('price_date'),
str(exception)))
continue
return stats
@classmethod
def _price_value_values(cls, price, row):
def _price_value_values(cls, price, row, price_date):
return {
'price': price.id,
'price_date': row['price_date'],
'price_date': price_date,
'high_price': cls._as_float(row.get('high_price')),
'low_price': cls._as_float(row.get('low_price')),
'open_price': cls._as_float(row.get('open_price')),
@@ -192,31 +222,61 @@ class ImportPrices(Wizard):
}
@staticmethod
def _skip(row_number, price_index, reason):
def _result_line(row_number, price_index, price_date, detail):
return {
'row': row_number,
'price_index': price_index,
'reason': reason,
'price_date': price_date,
'detail': detail,
}
@classmethod
def _format_result(cls, stats):
lines = [
'Import completed.',
f"Created price indexes: {stats['created_indexes']}",
f"Imported prices: {stats['imported']}",
f"Updated prices: {stats['updated']}",
f"Created price indexes: {len(stats['created_indexes'])}",
f"Imported prices: {len(stats['imported'])}",
f"Updated prices: {len(stats['updated'])}",
f"Skipped rows: {len(stats['skipped'])}",
f"Errors: {len(stats['errors'])}",
]
if stats['skipped']:
sections = [
('Created price indexes', stats['created_indexes']),
('Successfully imported prices', stats['imported']),
('Updated existing prices', stats['updated']),
('Skipped records', stats['skipped']),
('Errors', stats['errors']),
]
for title, items in sections:
lines.append('')
lines.append('Skipped details:')
for skip in stats['skipped']:
label = skip['price_index'] or '<empty>'
lines.append(
f"Row {skip['row']} - {label}: {skip['reason']}")
lines.append('%s:' % title)
if not items:
lines.append('- None')
continue
for item in items:
lines.append('- %s' % cls._format_result_line(item))
return '\n'.join(lines)
@staticmethod
def _format_result_line(item):
label = item['price_index'] or '<empty price_index>'
price_date = item['price_date']
if isinstance(price_date, datetime.date):
price_date = price_date.isoformat()
if price_date:
label = '%s / %s' % (label, price_date)
return 'Row %(row)s - ' % item + '%s: %s' % (label, item['detail'])
@staticmethod
def _price_summary(values):
parts = []
for name in ('price_value', 'open_price', 'low_price', 'high_price'):
value = values.get(name)
if value is not None:
parts.append('%s=%s' % (name, value))
return ', '.join(parts) or 'price imported'
@classmethod
def _read_xlsx(cls, data):
try:
@@ -255,7 +315,6 @@ class ImportPrices(Wizard):
if field:
values[field] = cls._cell_value(cell, shared_strings)
if any(v not in (None, '') for v in values.values()):
values['price_date'] = cls._as_date(values.get('price_date'))
result.append(values)
return result

View File

@@ -93,9 +93,9 @@ class PurchaseTradeTestCase(ModuleTestCase):
'price_value': '98',
}])
self.assertEqual(stats['imported'], 0)
self.assertEqual(len(stats['imported']), 0)
self.assertEqual(len(stats['skipped']), 1)
self.assertEqual(stats['skipped'][0]['reason'], 'price_index missing')
self.assertEqual(stats['skipped'][0]['detail'], 'price_index missing')
def test_import_prices_creates_index_and_imports_new_price(self):
'import prices creates missing index when requested'
@@ -117,8 +117,8 @@ class PurchaseTradeTestCase(ModuleTestCase):
'price_value': '98',
}], create_missing_price_index=True)
self.assertEqual(stats['created_indexes'], 1)
self.assertEqual(stats['imported'], 1)
self.assertEqual(len(stats['created_indexes']), 1)
self.assertEqual(len(stats['imported']), 1)
self.assertEqual(price_model.records[0].price_index, 'LME Copper')
self.assertEqual(price_value_model.records[0].price_value, 98.0)
@@ -143,10 +143,10 @@ class PurchaseTradeTestCase(ModuleTestCase):
'price_value': '98',
}])
self.assertEqual(stats['updated'], 0)
self.assertEqual(len(stats['updated']), 0)
self.assertEqual(price_value_model.records[0].price_value, 97.0)
self.assertEqual(
stats['skipped'][0]['reason'], 'price_date already exists')
stats['skipped'][0]['detail'], 'price_date already exists')
def test_import_prices_overwrites_existing_date_when_requested(self):
'import prices updates existing price dates when requested'
@@ -169,9 +169,66 @@ class PurchaseTradeTestCase(ModuleTestCase):
'price_value': '98',
}], overwrite_existing_price=True)
self.assertEqual(stats['updated'], 1)
self.assertEqual(len(stats['updated']), 1)
self.assertEqual(price_value_model.records[0].price_value, 98.0)
def test_import_prices_collects_row_errors(self):
'import prices reports invalid row values without hiding the result'
price = SimpleNamespace(id=1, price_index='LME Copper')
price_model = _FakePriceModel([price])
price_value_model = _FakePriceValueModel()
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,
}.__getitem__
stats = ImportPrices._import_rows([{
'price_index': 'LME Copper',
'price_date': date(2026, 3, 27),
'price_value': 'not-a-number',
}])
self.assertEqual(len(stats['errors']), 1)
self.assertIn('not-a-number', stats['errors'][0]['detail'])
def test_import_prices_result_lists_all_outcome_sections(self):
'import prices formats detailed result sections'
message = ImportPrices._format_result({
'created_indexes': [{
'row': 2,
'price_index': 'LME Copper',
'price_date': None,
'detail': 'price index created',
}],
'imported': [{
'row': 2,
'price_index': 'LME Copper',
'price_date': date(2026, 3, 27),
'detail': 'price_value=98.0',
}],
'updated': [],
'skipped': [{
'row': 3,
'price_index': 'LME Copper',
'price_date': date(2026, 3, 27),
'detail': 'price_date already exists',
}],
'errors': [{
'row': 4,
'price_index': 'LME Zinc',
'price_date': 'bad-date',
'detail': 'Invalid price_date: bad-date',
}],
})
self.assertIn('Created price indexes:', message)
self.assertIn('Successfully imported prices:', message)
self.assertIn('Skipped records:', message)
self.assertIn('Errors:', message)
self.assertIn('Row 2 - LME Copper / 2026-03-27', message)
class _FakePriceModel: