diff --git a/modules/purchase_trade/__init__.py b/modules/purchase_trade/__init__.py index b7bbb09..450f2b7 100755 --- a/modules/purchase_trade/__init__.py +++ b/modules/purchase_trade/__init__.py @@ -7,10 +7,10 @@ from . import ( account, configuration, purchase, - sale, - global_reporting, - ctrm_reporting, - stock, + sale, + global_reporting, + ctrm_reporting, + stock, derivative, lot, pricing, @@ -36,6 +36,8 @@ from . import ( backtoback, service, invoice, + process_documentation, + routes, ) def register(): @@ -57,43 +59,44 @@ def register(): lc.LCMT700, lc.LCMessage, lc.CreateLCStart, - global_reporting.GRConfiguration, - ctrm_reporting.CTRMPhysicalPosition, - ctrm_reporting.CTRMPhysicalPositionContext, - ctrm_reporting.CTRMFinancialPosition, - ctrm_reporting.CTRMFinancialPositionContext, - ctrm_reporting.CTRMNetPosition, - ctrm_reporting.CTRMNetPositionContext, - ctrm_reporting.CTRMRealizedPnl, - ctrm_reporting.CTRMRealizedPnlContext, - ctrm_reporting.CTRMMtmPnl, - ctrm_reporting.CTRMMtmPnlContext, - ctrm_reporting.CTRMPnlExplain, - ctrm_reporting.CTRMPnlExplainContext, - ctrm_reporting.CTRMPnlDimension, - ctrm_reporting.CTRMPnlDimensionContext, - ctrm_reporting.CTRMCreditRisk, - ctrm_reporting.CTRMCreditRiskContext, - ctrm_reporting.CTRMShippingLogistics, - ctrm_reporting.CTRMShippingLogisticsContext, - ctrm_reporting.CTRMInventory, - ctrm_reporting.CTRMInventoryContext, - ctrm_reporting.CTRMContractPerformance, - ctrm_reporting.CTRMContractPerformanceContext, - ctrm_reporting.CTRMScheduling, - ctrm_reporting.CTRMSchedulingContext, - ctrm_reporting.CTRMAccruals, - ctrm_reporting.CTRMAccrualsContext, - ctrm_reporting.CTRMSettlements, - ctrm_reporting.CTRMSettlementsContext, - ctrm_reporting.CTRMCashFlow, - ctrm_reporting.CTRMCashFlowContext, - ctrm_reporting.CTRMGLReconciliation, - ctrm_reporting.CTRMGLReconciliationContext, - configuration.Configuration, - pricing.ImportPricesStart, - pricing.ImportPricesResult, - module='purchase_trade', type_='model') + global_reporting.GRConfiguration, + ctrm_reporting.CTRMPhysicalPosition, + ctrm_reporting.CTRMPhysicalPositionContext, + ctrm_reporting.CTRMFinancialPosition, + ctrm_reporting.CTRMFinancialPositionContext, + ctrm_reporting.CTRMNetPosition, + ctrm_reporting.CTRMNetPositionContext, + ctrm_reporting.CTRMRealizedPnl, + ctrm_reporting.CTRMRealizedPnlContext, + ctrm_reporting.CTRMMtmPnl, + ctrm_reporting.CTRMMtmPnlContext, + ctrm_reporting.CTRMPnlExplain, + ctrm_reporting.CTRMPnlExplainContext, + ctrm_reporting.CTRMPnlDimension, + ctrm_reporting.CTRMPnlDimensionContext, + ctrm_reporting.CTRMCreditRisk, + ctrm_reporting.CTRMCreditRiskContext, + ctrm_reporting.CTRMShippingLogistics, + ctrm_reporting.CTRMShippingLogisticsContext, + ctrm_reporting.CTRMInventory, + ctrm_reporting.CTRMInventoryContext, + ctrm_reporting.CTRMContractPerformance, + ctrm_reporting.CTRMContractPerformanceContext, + ctrm_reporting.CTRMScheduling, + ctrm_reporting.CTRMSchedulingContext, + ctrm_reporting.CTRMAccruals, + ctrm_reporting.CTRMAccrualsContext, + ctrm_reporting.CTRMSettlements, + ctrm_reporting.CTRMSettlementsContext, + ctrm_reporting.CTRMCashFlow, + ctrm_reporting.CTRMCashFlowContext, + ctrm_reporting.CTRMGLReconciliation, + ctrm_reporting.CTRMGLReconciliationContext, + configuration.Configuration, + process_documentation.ProcessDocumentation, + pricing.ImportPricesStart, + pricing.ImportPricesResult, + module='purchase_trade', type_='model') Pool.register( incoming.ImportSwift, incoming.PrepareDocuments, @@ -117,20 +120,20 @@ def register(): dashboard.Demos, party.Party, party.PartyExecution, - party.PartyExecutionSla, - party.PartyExecutionPlace, - payment_term.PaymentTerm, - payment_term.PaymentTermLine, - stock.CharterRateType, - stock.CharterConditionRate, - stock.CharterCondition, - stock.CharterParty, - purchase.ContractClause, - purchase.ContractTemplate, - purchase.ContractTemplateLine, - purchase.ContractClauseSelection, - purchase.Purchase, - purchase.Line, + party.PartyExecutionSla, + party.PartyExecutionPlace, + payment_term.PaymentTerm, + payment_term.PaymentTermLine, + stock.CharterRateType, + stock.CharterConditionRate, + stock.CharterCondition, + stock.CharterParty, + purchase.ContractClause, + purchase.ContractTemplate, + purchase.ContractTemplateLine, + purchase.ContractClauseSelection, + purchase.Purchase, + purchase.Line, purchase.Estimated, purchase.Component, purchase.Pricing, @@ -145,10 +148,10 @@ def register(): open_position.OpenPosition, open_position.OpenPositionReport, open_position.OpenPositionContext, - optional.OptionalScenario, - fee.Fee, - fee.FeeRule, - fee.FeeLots, + optional.OptionalScenario, + fee.Fee, + fee.FeeRule, + fee.FeeLots, purchase.FeeLots, valuation.Valuation, valuation.ValuationLine, @@ -218,10 +221,10 @@ def register(): purchase.ContractDocumentType, purchase.DocTemplate, purchase.DocTypeTemplate, - purchase.PurchaseStrategy, - purchase.PriceComposition, - purchase.PremiumComposition, - purchase.QualityAnalysis, + purchase.PurchaseStrategy, + purchase.PriceComposition, + purchase.PremiumComposition, + purchase.QualityAnalysis, purchase.Assay, purchase.AssayLine, purchase.AssayElement, @@ -239,13 +242,13 @@ def register(): Pool.register( account.PhysicalTradeIFRS, module='purchase_trade', type_='model') - Pool.register( - configuration.AccountConfiguration, - configuration.AccountConfigurationDefaultAccount, - invoice.InvoiceLineLotWeight, - invoice.InvoicePaddingReport, - invoice.InvoicePaddingContext, - module='purchase_trade', type_='model') + Pool.register( + configuration.AccountConfiguration, + configuration.AccountConfigurationDefaultAccount, + invoice.InvoiceLineLotWeight, + invoice.InvoicePaddingReport, + invoice.InvoicePaddingContext, + module='purchase_trade', type_='model') Pool.register( invoice.Invoice, invoice.InvoiceLine, @@ -270,12 +273,12 @@ def register(): purchase.PurchaseAssociation, purchase.PurchaseCrop, module='lot', type_='model') - Pool.register( - sale.ContractClauseSelection, - sale.CharterCondition, - sale.Sale, - sale.SaleLine, - sale.SaleCreatePurchaseInput, + Pool.register( + sale.ContractClauseSelection, + sale.CharterCondition, + sale.Sale, + sale.SaleLine, + sale.SaleCreatePurchaseInput, sale.Derivative, sale.Valuation, sale.ValuationLine, @@ -294,10 +297,10 @@ def register(): sale.SaleStrategy, sale.OpenPosition, sale.Backtoback, - sale.AnalyticDimensionAssignment, - sale.PriceComposition, - sale.PremiumComposition, - module='sale', type_='model') + sale.AnalyticDimensionAssignment, + sale.PriceComposition, + sale.PremiumComposition, + module='sale', type_='model') Pool.register( lot.LotShipping, lot.LotMatching, diff --git a/modules/purchase_trade/pricing.py b/modules/purchase_trade/pricing.py index 93d5e4c..7e73a68 100755 --- a/modules/purchase_trade/pricing.py +++ b/modules/purchase_trade/pricing.py @@ -2,6 +2,7 @@ # this repository contains the full copyright notices and license terms. from trytond.model import fields from trytond.pool import Pool, PoolMeta +from trytond.exceptions import UserError from trytond.pyson import Bool, Eval, Id from trytond.model import (ModelSQL, ModelView) from trytond.exceptions import UserError @@ -68,12 +69,19 @@ 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" @@ -111,7 +119,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,6 +151,7 @@ class ImportPrices(Wizard): } for row_number, 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')) @@ -217,7 +227,7 @@ class ImportPrices(Wizard): } references = [ ('price_type', 'price.fixtype', [('name', '=', 'Market price')]), - ('price_currency', 'currency.currency', [('code', '=', 'USD')]), + ('price_currency', 'currency.currency', [('name', '=', 'USD')]), ('price_calendar', 'price.calendar', [('name', '=', 'Argus EU')]), ('price_unit', 'product.uom', [('name', '=', 'Mt')]), ] @@ -340,7 +350,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) @@ -354,6 +364,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')) @@ -380,6 +396,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: @@ -472,6 +541,18 @@ class ImportPrices(Wizard): except ValueError: pass raise UserError("Invalid price_date: %s" % text) + + @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' @@ -511,13 +592,13 @@ class MtmStrategy(ModelSQL, ModelView): Currency = pool.get('currency.currency') total = Decimal(0) - scenario = self.scenario - dt = scenario.valuation_date - - for comp in self.components: - value = Decimal(0) - - if comp.price_source_type == 'curve' and comp.price_index: + scenario = self.scenario + dt = scenario.valuation_date + + for comp in self.components: + value = Decimal(0) + + if comp.price_source_type == 'curve' and comp.price_index: value = Decimal( comp.price_index.get_price( dt, @@ -526,22 +607,22 @@ class MtmStrategy(ModelSQL, ModelView): last=scenario.use_last_price ) ) - - elif comp.price_source_type == 'matrix' and comp.price_matrix: - value = self._get_matrix_price(comp, line, dt) - - elif comp.price_source_type == 'fixed': - value = Decimal(comp.get_price( - dt, - line.unit, - self.currency, - last=scenario.use_last_price - )) - - if comp.ratio: - value *= Decimal(comp.ratio) / Decimal(100) - - total += value * qty + + elif comp.price_source_type == 'matrix' and comp.price_matrix: + value = self._get_matrix_price(comp, line, dt) + + elif comp.price_source_type == 'fixed': + value = Decimal(comp.get_price( + dt, + line.unit, + self.currency, + last=scenario.use_last_price + )) + + if comp.ratio: + value *= Decimal(comp.ratio) / Decimal(100) + + total += value * qty return Decimal(str(total)).quantize(Decimal("0.01")) @@ -699,198 +780,198 @@ class Component(ModelSQL, ModelView): required=False, ondelete='CASCADE' ) - price_source_type = fields.Selection([ - ('curve', 'Curve'), - ('matrix', 'Matrix'), - ('fixed', 'Fixed'), - ], "Price Source", required=True) - - fix_type = fields.Many2One('price.fixtype',"Fixation type") - ratio = fields.Numeric("%",digits=(16,7)) - price_index = fields.Many2One( - 'price.price', "Curve", - states={ - 'readonly': Eval('price_source_type') != 'curve', - 'required': Eval('price_source_type') == 'curve', - }, - depends=['price_source_type']) - price_matrix = fields.Many2One( - 'price.matrix', "Price Matrix", - states={ - 'readonly': Eval('price_source_type') != 'matrix', - 'required': Eval('price_source_type') == 'matrix', - }, - depends=['price_source_type']) - fixed_price = fields.Numeric( - "Fixed Price", digits=(16, 6), - states={ - 'readonly': Eval('price_source_type') != 'fixed', - 'required': Eval('price_source_type') == 'fixed', - }, - depends=['price_source_type']) - fixed_currency = fields.Many2One('currency.currency', "Fixed Curr.") - currency = fields.Function( - fields.Many2One( - 'currency.currency', "Curr.", - states={ - 'required': Eval('price_source_type') == 'fixed', - }, - depends=['price_source_type']), - 'get_cur', setter='set_cur') - auto = fields.Boolean("Auto") - fallback = fields.Boolean("Fallback") - calendar = fields.Many2One('price.calendar',"Calendar") - nbdays = fields.Function(fields.Integer("Nb days"),'get_nbdays') - triggers = fields.One2Many('pricing.trigger','component',"Period rules") + price_source_type = fields.Selection([ + ('curve', 'Curve'), + ('matrix', 'Matrix'), + ('fixed', 'Fixed'), + ], "Price Source", required=True) + + fix_type = fields.Many2One('price.fixtype',"Fixation type") + ratio = fields.Numeric("%",digits=(16,7)) + price_index = fields.Many2One( + 'price.price', "Curve", + states={ + 'readonly': Eval('price_source_type') != 'curve', + 'required': Eval('price_source_type') == 'curve', + }, + depends=['price_source_type']) + price_matrix = fields.Many2One( + 'price.matrix', "Price Matrix", + states={ + 'readonly': Eval('price_source_type') != 'matrix', + 'required': Eval('price_source_type') == 'matrix', + }, + depends=['price_source_type']) + fixed_price = fields.Numeric( + "Fixed Price", digits=(16, 6), + states={ + 'readonly': Eval('price_source_type') != 'fixed', + 'required': Eval('price_source_type') == 'fixed', + }, + depends=['price_source_type']) + fixed_currency = fields.Many2One('currency.currency', "Fixed Curr.") + currency = fields.Function( + fields.Many2One( + 'currency.currency', "Curr.", + states={ + 'required': Eval('price_source_type') == 'fixed', + }, + depends=['price_source_type']), + 'get_cur', setter='set_cur') + auto = fields.Boolean("Auto") + fallback = fields.Boolean("Fallback") + calendar = fields.Many2One('price.calendar',"Calendar") + nbdays = fields.Function(fields.Integer("Nb days"),'get_nbdays') + triggers = fields.One2Many('pricing.trigger','component',"Period rules") pricing_date = fields.Date("Pricing date max") - def get_rec_name(self, name=None): - if self.price_index: - return '[' + self.fix_type.name + '] ' + self.price_index.price_index - if self.price_matrix: - return '[' + self.fix_type.name + '] ' + self.price_matrix.name - if self.price_source_type == 'fixed': - return '[' + self.fix_type.name + '] Fixed' - else: - return '[' + self.fix_type.name + '] ' - - def get_cur(self, name=None): - if self.price_index: - PI = Pool().get('price.price') - pi = PI(self.price_index) - return pi.price_currency - if self.price_matrix: - return self.price_matrix.currency - if self.price_source_type == 'fixed': - return self.fixed_currency - - @fields.depends( - 'price_source_type', 'price_index', 'price_matrix', - 'fixed_currency') - def on_change_with_currency(self): - if self.price_source_type == 'curve' and self.price_index: - return self.get_cur() - if self.price_source_type == 'matrix' and self.price_matrix: - return self.get_cur() - if self.price_source_type == 'fixed': - return self.fixed_currency - - @classmethod - def set_cur(cls, components, name, value): - cls.write(components, {'fixed_currency': value}) - - def get_calendar(self): - if self.calendar: - return self.calendar - if self.price_matrix: - return self.price_matrix.calendar - - @staticmethod - def _record_id(record): - return getattr(record, 'id', record) - - @classmethod - def _matches_optional_field(cls, matrix_line, field_name, target): - value = getattr(matrix_line, field_name, None) - if not value: - return True, 0 - if target and cls._record_id(value) == cls._record_id(target): - return True, 1 - return False, 0 - - def _matrix_valid_on(self, price_date): - if not self.price_matrix: - return False - if hasattr(price_date, 'date'): - price_date = price_date.date() - valid_from = getattr(self.price_matrix, 'valid_from', None) - valid_to = getattr(self.price_matrix, 'valid_to', None) - if valid_from and price_date < valid_from: - return False - if valid_to and price_date > valid_to: - return False - return True - - def _get_matrix_owner_values(self): - line = getattr(self, 'line', None) or getattr(self, 'sale_line', None) - document = getattr(line, 'purchase', None) or getattr(line, 'sale', None) - return { - 'origin': getattr(document, 'from_location', None), - 'destination': getattr(document, 'to_location', None), - 'product': getattr(line, 'product', None), - 'quality': getattr(line, 'quality', None), - } - - def _get_matrix_line(self): - MatrixLine = Pool().get('price.matrix.line') - lines = MatrixLine.search([('matrix', '=', self.price_matrix.id)]) - owner_values = self._get_matrix_owner_values() - best_line = None - best_score = -1 - for line in lines: - score = 0 - for field_name, target in owner_values.items(): - matches, field_score = self._matches_optional_field( - line, field_name, target) - if not matches: - break - score += field_score - else: - if score > best_score: - best_line = line - best_score = score - return best_line - - def _convert_matrix_price(self, price, unit, currency): - price_qt = Decimal(str(price or 0)) - matrix_unit = getattr(self.price_matrix, 'unit', None) - matrix_currency = getattr(self.price_matrix, 'currency', None) - if matrix_unit and unit: - Uom = Pool().get('product.uom') - price_qt *= Decimal(str( - Uom.compute_qty(unit, float(1), matrix_unit))) - if matrix_currency and currency and currency != matrix_currency: - Currency = Pool().get('currency.currency') - rates = Currency._get_rate([matrix_currency]) - rate = rates.get(matrix_currency.id) - if rate: - price_qt *= Decimal(str(rate)) - return round(price_qt, 4) - - def _convert_fixed_price(self, price, currency): - price_qt = Decimal(str(price or 0)) - price_currency = getattr(self, 'fixed_currency', None) - if price_currency and currency and currency != price_currency: - Currency = Pool().get('currency.currency') - rates = Currency._get_rate([price_currency]) - rate = rates.get(price_currency.id) - if rate: - price_qt *= Decimal(str(rate)) - return round(price_qt, 4) - - def get_price(self, price_date, unit, currency, last=False): - if self.price_source_type == 'curve' and self.price_index: - PI = Pool().get('price.price') - pi = PI(self.price_index) - return pi.get_price(price_date, unit, currency, last) - if self.price_source_type == 'matrix' and self.price_matrix: - if not self._matrix_valid_on(price_date): - return Decimal(0) - line = self._get_matrix_line() - if line: - return self._convert_matrix_price( - line.price_value, unit, currency) - if self.price_source_type == 'fixed': - return self._convert_fixed_price(self.fixed_price, currency) - return Decimal(0) - - def get_nbdays(self, name): - days = 0 - if self.triggers: - for t in self.triggers: - l,l2 = t.getApplicationListDates(self.get_calendar()) - days += len(l) - return days + def get_rec_name(self, name=None): + if self.price_index: + return '[' + self.fix_type.name + '] ' + self.price_index.price_index + if self.price_matrix: + return '[' + self.fix_type.name + '] ' + self.price_matrix.name + if self.price_source_type == 'fixed': + return '[' + self.fix_type.name + '] Fixed' + else: + return '[' + self.fix_type.name + '] ' + + def get_cur(self, name=None): + if self.price_index: + PI = Pool().get('price.price') + pi = PI(self.price_index) + return pi.price_currency + if self.price_matrix: + return self.price_matrix.currency + if self.price_source_type == 'fixed': + return self.fixed_currency + + @fields.depends( + 'price_source_type', 'price_index', 'price_matrix', + 'fixed_currency') + def on_change_with_currency(self): + if self.price_source_type == 'curve' and self.price_index: + return self.get_cur() + if self.price_source_type == 'matrix' and self.price_matrix: + return self.get_cur() + if self.price_source_type == 'fixed': + return self.fixed_currency + + @classmethod + def set_cur(cls, components, name, value): + cls.write(components, {'fixed_currency': value}) + + def get_calendar(self): + if self.calendar: + return self.calendar + if self.price_matrix: + return self.price_matrix.calendar + + @staticmethod + def _record_id(record): + return getattr(record, 'id', record) + + @classmethod + def _matches_optional_field(cls, matrix_line, field_name, target): + value = getattr(matrix_line, field_name, None) + if not value: + return True, 0 + if target and cls._record_id(value) == cls._record_id(target): + return True, 1 + return False, 0 + + def _matrix_valid_on(self, price_date): + if not self.price_matrix: + return False + if hasattr(price_date, 'date'): + price_date = price_date.date() + valid_from = getattr(self.price_matrix, 'valid_from', None) + valid_to = getattr(self.price_matrix, 'valid_to', None) + if valid_from and price_date < valid_from: + return False + if valid_to and price_date > valid_to: + return False + return True + + def _get_matrix_owner_values(self): + line = getattr(self, 'line', None) or getattr(self, 'sale_line', None) + document = getattr(line, 'purchase', None) or getattr(line, 'sale', None) + return { + 'origin': getattr(document, 'from_location', None), + 'destination': getattr(document, 'to_location', None), + 'product': getattr(line, 'product', None), + 'quality': getattr(line, 'quality', None), + } + + def _get_matrix_line(self): + MatrixLine = Pool().get('price.matrix.line') + lines = MatrixLine.search([('matrix', '=', self.price_matrix.id)]) + owner_values = self._get_matrix_owner_values() + best_line = None + best_score = -1 + for line in lines: + score = 0 + for field_name, target in owner_values.items(): + matches, field_score = self._matches_optional_field( + line, field_name, target) + if not matches: + break + score += field_score + else: + if score > best_score: + best_line = line + best_score = score + return best_line + + def _convert_matrix_price(self, price, unit, currency): + price_qt = Decimal(str(price or 0)) + matrix_unit = getattr(self.price_matrix, 'unit', None) + matrix_currency = getattr(self.price_matrix, 'currency', None) + if matrix_unit and unit: + Uom = Pool().get('product.uom') + price_qt *= Decimal(str( + Uom.compute_qty(unit, float(1), matrix_unit))) + if matrix_currency and currency and currency != matrix_currency: + Currency = Pool().get('currency.currency') + rates = Currency._get_rate([matrix_currency]) + rate = rates.get(matrix_currency.id) + if rate: + price_qt *= Decimal(str(rate)) + return round(price_qt, 4) + + def _convert_fixed_price(self, price, currency): + price_qt = Decimal(str(price or 0)) + price_currency = getattr(self, 'fixed_currency', None) + if price_currency and currency and currency != price_currency: + Currency = Pool().get('currency.currency') + rates = Currency._get_rate([price_currency]) + rate = rates.get(price_currency.id) + if rate: + price_qt *= Decimal(str(rate)) + return round(price_qt, 4) + + def get_price(self, price_date, unit, currency, last=False): + if self.price_source_type == 'curve' and self.price_index: + PI = Pool().get('price.price') + pi = PI(self.price_index) + return pi.get_price(price_date, unit, currency, last) + if self.price_source_type == 'matrix' and self.price_matrix: + if not self._matrix_valid_on(price_date): + return Decimal(0) + line = self._get_matrix_line() + if line: + return self._convert_matrix_price( + line.price_value, unit, currency) + if self.price_source_type == 'fixed': + return self._convert_fixed_price(self.fixed_price, currency) + return Decimal(0) + + def get_nbdays(self, name): + days = 0 + if self.triggers: + for t in self.triggers: + l,l2 = t.getApplicationListDates(self.get_calendar()) + days += len(l) + return days @classmethod def delete(cls, components): @@ -1239,9 +1320,9 @@ class Trigger(ModelSQL,ModelView): states={ 'readonly': Eval('pricing_period') != None, }) - average = fields.Boolean("Avg") - last = fields.Boolean("Latest") - application_period = fields.Many2One('pricing.period',"Application period") + average = fields.Boolean("Avg") + last = fields.Boolean("Latest") + application_period = fields.Many2One('pricing.period',"Application period") from_a = fields.Date("From", states={ 'readonly': Eval('application_period') != None, @@ -1249,26 +1330,26 @@ class Trigger(ModelSQL,ModelView): to_a = fields.Date("To", states={ 'readonly': Eval('application_period') != None, - }) - - @classmethod - def default_average(cls): - return True - - @fields.depends('pricing_period') - def on_change_with_application_period(self): - if not self.application_period and self.pricing_period: - return self.pricing_period - - @fields.depends('from_p', 'from_a', 'application_period') - def on_change_from_p(self): - if not self.application_period and not self.from_a: - self.from_a = self.from_p - - @fields.depends('to_p', 'to_a', 'application_period') - def on_change_to_p(self): - if not self.application_period and not self.to_a: - self.to_a = self.to_p + }) + + @classmethod + def default_average(cls): + return True + + @fields.depends('pricing_period') + def on_change_with_application_period(self): + if not self.application_period and self.pricing_period: + return self.pricing_period + + @fields.depends('from_p', 'from_a', 'application_period') + def on_change_from_p(self): + if not self.application_period and not self.from_a: + self.from_a = self.from_p + + @fields.depends('to_p', 'to_a', 'application_period') + def on_change_to_p(self): + if not self.application_period and not self.to_a: + self.to_a = self.to_p def getDateWithEstTrigger(self, period): PP = Pool().get('pricing.period') @@ -1285,73 +1366,73 @@ class Trigger(ModelSQL,ModelView): date_from,date_to,dates = pp.getDates(d) return date_from,date_to,d,pp.include,dates - def getApplicationListDates(self, cal): - ld = [] - dates = [] - if self.application_period: - date_from, date_to, d, include,dates = self.getDateWithEstTrigger(2) - else: - date_from = self.from_a or self.from_p - date_to = self.to_a or self.to_p - d = None - include = False - ld, lprice = self.getListDates(date_from,date_to,d,include,cal,2,dates) - return ld, lprice - - def getPricingListDates(self,cal): - ld = [] - dates = [] - if self.pricing_period: - date_from, date_to, d, include,dates = self.getDateWithEstTrigger(1) - else: - date_from = self.from_p#datetime.datetime(self.from_p.year, self.from_p.month, self.from_p.day) - date_to = self.to_p#datetime.datetime(self.to_p.year, self.to_p.month, self.to_p.day) + def getApplicationListDates(self, cal): + ld = [] + dates = [] + if self.application_period: + date_from, date_to, d, include,dates = self.getDateWithEstTrigger(2) + else: + date_from = self.from_a or self.from_p + date_to = self.to_a or self.to_p + d = None + include = False + ld, lprice = self.getListDates(date_from,date_to,d,include,cal,2,dates) + return ld, lprice + + def getPricingListDates(self,cal): + ld = [] + dates = [] + if self.pricing_period: + date_from, date_to, d, include,dates = self.getDateWithEstTrigger(1) + else: + date_from = self.from_p#datetime.datetime(self.from_p.year, self.from_p.month, self.from_p.day) + date_to = self.to_p#datetime.datetime(self.to_p.year, self.to_p.month, self.to_p.day) d = None include = False ld, lprice = self.getListDates(date_from,date_to,d,include,cal,1,dates) return ld, lprice - def getListDates(self,df,dt,t,i,cal,pricing,dates): - l = [] - lprice = [] - def append_date(date): - if cal: - if not cal.IsQuote(date): - return - l.append(date) - if pricing == 1: - lprice.append(self.getprice(date)) - - if cal: - CAL = Pool().get('price.calendar') - cal = CAL(cal) - if dates: - for d in dates: - append_date(d) - return l, lprice - if df and dt: - current_date = datetime.datetime(df.year,df.month,df.day) - dt = datetime.datetime(dt.year,dt.month,dt.day) - while current_date <= dt: - if i or (not i and current_date != t): - append_date(current_date) - current_date += datetime.timedelta(days=1) - return l, lprice + def getListDates(self,df,dt,t,i,cal,pricing,dates): + l = [] + lprice = [] + def append_date(date): + if cal: + if not cal.IsQuote(date): + return + l.append(date) + if pricing == 1: + lprice.append(self.getprice(date)) - def getprice(self,current_date): - PC = Pool().get('pricing.component') - pc = PC(self.component) - val = {} - val['date'] = current_date - line = pc.line if pc.line else pc.sale_line - val['price'] = pc.get_price( - current_date, - line.unit, - line.currency, - self.last) - val['avg'] = val['price'] - val['avg_minus_1'] = val['price'] - val['isAvg'] = self.average + if cal: + CAL = Pool().get('price.calendar') + cal = CAL(cal) + if dates: + for d in dates: + append_date(d) + return l, lprice + if df and dt: + current_date = datetime.datetime(df.year,df.month,df.day) + dt = datetime.datetime(dt.year,dt.month,dt.day) + while current_date <= dt: + if i or (not i and current_date != t): + append_date(current_date) + current_date += datetime.timedelta(days=1) + return l, lprice + + def getprice(self,current_date): + PC = Pool().get('pricing.component') + pc = PC(self.component) + val = {} + val['date'] = current_date + line = pc.line if pc.line else pc.sale_line + val['price'] = pc.get_price( + current_date, + line.unit, + line.currency, + self.last) + val['avg'] = val['price'] + val['avg_minus_1'] = val['price'] + val['isAvg'] = self.average return val class Period(ModelSQL,ModelView): diff --git a/modules/purchase_trade/view/import_prices_start_form.xml b/modules/purchase_trade/view/import_prices_start_form.xml index 399f7f3..17a97e8 100644 --- a/modules/purchase_trade/view/import_prices_start_form.xml +++ b/modules/purchase_trade/view/import_prices_start_form.xml @@ -1,4 +1,7 @@
diff --git a/modules/trade_finance/view/blocking_reason_tree.xml b/modules/trade_finance/view/blocking_reason_tree.xml new file mode 100644 index 0000000..2b4c872 --- /dev/null +++ b/modules/trade_finance/view/blocking_reason_tree.xml @@ -0,0 +1,5 @@ +