from trytond.model import fields from trytond.report import Report from trytond.pool import Pool, PoolMeta from trytond.pyson import Bool, Eval, Id, If, PYSONEncoder from trytond.model import (ModelSQL, ModelView) from trytond.tools import is_full_text, lstrip_wildcard from trytond.transaction import Transaction, inactive_records from decimal import getcontext, Decimal, ROUND_HALF_UP from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr from sql.conditionals import Case from sql import Column, Literal from sql.functions import CurrentTimestamp, DateTrunc from trytond.wizard import Button, StateAction, StateTransition, StateView, Wizard from itertools import chain, groupby from operator import itemgetter import datetime import logging import re from collections import defaultdict from trytond.exceptions import UserWarning, UserError logger = logging.getLogger(__name__) VALTYPE = [ ('priced', 'Price'), ('pur. priced', 'Pur. price'), ('pur. efp', 'Pur. efp'), ('sale priced', 'Sale price'), ('sale efp', 'Sale efp'), ('line fee', 'Line fee'), ('pur. fee', 'Pur. fee'), ('sale fee', 'Sale fee'), ('shipment fee', 'Shipment fee'), ('market', 'Market'), ('derivative', 'Derivative'), ] VALUATION_TYPE_GROUPS = [ ('all', 'All'), ('fees', 'PnL Fees'), ('goods', 'PnL Goods'), ('derivatives', 'PnL Derivatives'), ] class ValuationBase(ModelSQL): purchase = fields.Many2One('purchase.purchase',"Purchase") line = fields.Many2One('purchase.line',"Purch. Line") date = fields.Date("Date") type = fields.Selection(VALTYPE, "Type") reference = fields.Char("Reference") counterparty = fields.Many2One('party.party',"Counterparty") product = fields.Many2One('product.product',"Product") state = fields.Char("State") price = fields.Numeric("Price",digits=(16,4)) currency = fields.Many2One('currency.currency',"Cur") quantity = fields.Numeric("Quantity",digits=(16,5)) unit = fields.Many2One('product.uom',"Unit") amount = fields.Numeric("Amount",digits=(16,2)) mtm = fields.Numeric("Mtm",digits=(16,2)) strategy = fields.Many2One('mtm.strategy',"Strategy") lot = fields.Many2One('lot.lot',"Lot") base_amount = fields.Numeric("Base Amount",digits=(16,2)) rate = fields.Numeric("Rate", digits=(16,6)) @classmethod def _get_generate_types(cls, valuation_type='all'): type_map = { 'all': None, 'fees': {'line fee', 'pur. fee', 'sale fee', 'shipment fee'}, 'goods': { 'priced', 'pur. priced', 'pur. efp', 'sale priced', 'sale efp', 'market', }, 'derivatives': {'derivative'}, } return type_map.get(valuation_type, None) @classmethod def _filter_values_by_types(cls, values, selected_types): if selected_types is None: return values return [value for value in values if value.get('type') in selected_types] @classmethod def _delete_existing(cls, line, selected_types=None): Date = Pool().get('ir.date') Valuation = Pool().get('valuation.valuation') ValuationLine = Pool().get('valuation.valuation.line') valuation_domain = [ ('line', '=', line.id), ('date', '=', Date.today()), ] valuation_line_domain = [('line', '=', line.id)] if selected_types is not None: valuation_domain.append(('type', 'in', list(selected_types))) valuation_line_domain.append(('type', 'in', list(selected_types))) valuations = Valuation.search(valuation_domain) if valuations: Valuation.delete(valuations) valuation_lines = ValuationLine.search(valuation_line_domain) if valuation_lines: ValuationLine.delete(valuation_lines) @classmethod def _base_pnl(cls, *, line, lot, pnl_type, sale=None): Date = Pool().get('ir.date') values = { 'purchase': line.purchase.id, 'line': line.id, 'type': pnl_type, 'date': Date.today(), 'lot': lot.id, } if sale: values['sale'] = sale.id return values @classmethod def _build_basis_pnl(cls, *, line, lot, sale_line, pc, sign): Currency = Pool().get('currency.currency') Date = Pool().get('ir.date') values = cls._base_pnl( line=line, lot=lot, sale=sale_line.sale if sale_line else None, pnl_type='sale priced' if sale_line else 'pur. priced' ) qty = lot.get_current_quantity_converted() price = pc.price logger.info("TERMS:%s",line.terms) if line.terms: c = [t for t in line.terms if t.component == pc.price_component] logger.info("COMPONENTS:%s",c) if c: price = c[0].manual_price values.update({ 'reference': f"{pc.get_name()} / {pc.ratio}%", 'price': round(price, 4), 'counterparty': sale_line.sale.party.id if sale_line else line.purchase.party.id, 'product': sale_line.product.id if sale_line else line.product.id, }) # State if pc.unfixed_qt == 0: values['state'] = 'fixed' elif pc.fixed_qt == 0: values['state'] = 'unfixed' else: base = sale_line.quantity_theorical if sale_line else line.quantity_theorical values['state'] = f"part. fixed {round(pc.fixed_qt / Decimal(base) * 100, 0)}%" if price != None: amount = round(price * qty * Decimal(sign), 2) base_amount = amount currency = sale_line.sale.currency.id if sale_line else line.purchase.currency.id rate = Decimal(1) if line.purchase.company.currency != currency: with Transaction().set_context(date=Date.today()): base_amount = Currency.compute(currency,amount, line.purchase.company.currency) rate = round(amount / (base_amount if base_amount else 1),6) last_price = pc.get_last_price() # mtm = round(Decimal(last_price) * qty * Decimal(sign), 2) if last_price else Decimal(0) values.update({ 'quantity': round(qty, 5), 'amount': amount, 'base_amount': base_amount, 'rate': rate, 'mtm': None, #round(amount - (mtm * pc.ratio / 100), 2), 'unit': sale_line.unit.id if sale_line else line.unit.id, 'currency': currency, }) return values @classmethod def _build_simple_pnl(cls, *, line, lot, sale_line, price, state, sign, pnl_type): Currency = Pool().get('currency.currency') Date = Pool().get('ir.date') values = cls._base_pnl( line=line, lot=lot, sale=sale_line.sale if sale_line else None, pnl_type=pnl_type ) qty = lot.get_current_quantity_converted() amount = round(price * qty * Decimal(sign), 2) base_amount = amount currency = sale_line.sale.currency.id if sale_line else line.purchase.currency.id company_currency = sale_line.sale.company.currency if sale_line else line.purchase.company.currency rate = Decimal(1) if line.purchase.company.currency != currency: with Transaction().set_context(date=Date.today()): base_amount = Currency.compute(currency,amount, company_currency) if base_amount and amount: rate = round(amount / base_amount,6) values.update({ 'price': round(price, 4), 'quantity': round(qty, 5), 'amount': amount, 'base_amount': base_amount, 'rate': rate, 'mtm': Decimal(0), 'state': state, 'unit': sale_line.unit.id if sale_line else line.unit.id, 'currency': currency, 'counterparty': sale_line.sale.party.id if sale_line else line.purchase.party.id, 'product': sale_line.product.id if sale_line else line.product.id, 'reference': ( 'Sale/Physic' if lot.lot_type == 'physic' else 'Sale/Open' if sale_line else 'Purchase/Physic' ), }) return values @classmethod def create_pnl_price_from_line(cls, line): price_lines = [] LotQt = Pool().get('lot.qt') for lot in line.lots: if line.price_type == 'basis': for pc in line.price_summary or []: values = cls._build_basis_pnl(line=line, lot=lot, sale_line=None, pc=pc, sign=-1) if line.mtm: for strat in line.mtm: values['mtm'] = strat.get_mtm(line,values['quantity']) values['strategy'] = strat if values: price_lines.append(values) else: if values: price_lines.append(values) elif line.price_type in ('priced', 'efp') and lot.lot_price: values = cls._build_simple_pnl( line=line, lot=lot, sale_line=None, price=lot.lot_price, state='fixed' if line.price_type == 'priced' else 'not fixed', sign=-1, pnl_type=f'pur. {line.price_type}' ) if line.mtm: for strat in line.mtm: values['mtm'] = strat.get_mtm(line,values['quantity']) values['strategy'] = strat if values: price_lines.append(values) else: if values: price_lines.append(values) sale_lots = [lot] if lot.sale_line else [ lqt.lot_s for lqt in LotQt.search([ ('lot_p', '=', lot.id), ('lot_s', '>', 0), ('lot_quantity', '>', 0), ]) ] for sl in sale_lots: sl_line = sl.sale_line if not sl_line: continue if sl_line.price_type == 'basis': for pc in sl_line.price_summary or []: values = cls._build_basis_pnl(line=line, lot=sl, sale_line=sl_line, pc=pc, sign=+1) if sl_line.mtm: for strat in line.mtm: values['mtm'] = strat.get_mtm(sl_line,values['quantity']) values['strategy'] = strat if values: price_lines.append(values) else: if values: price_lines.append(values) elif sl_line.price_type in ('priced', 'efp'): values = cls._build_simple_pnl( line=line, lot=sl, sale_line=sl_line, price=sl.lot_price_sale, state='fixed' if sl_line.price_type == 'priced' else 'not fixed', sign=+1, pnl_type=f'sale {sl_line.price_type}' ) if sl_line.mtm: for strat in sl_line.mtm: values['mtm'] = strat.get_mtm(sl_line,values['quantity']) values['strategy'] = strat if values: price_lines.append(values) else: if values: price_lines.append(values) return price_lines @classmethod def group_fees_by_type_supplier(cls,line,fees): grouped = defaultdict(list) # Regrouper par (type, supplier) for fee in fees: key = (fee.product, fee.supplier) grouped[key].append(fee) result = [] for key, fee_list in grouped.items(): ordered_fees = [f for f in fee_list if f.type == 'ordered'] if ordered_fees: result.extend(ordered_fees) else: budgeted_fees = [f for f in fee_list if f.type == 'budgeted'] result.extend(budgeted_fees) return result @classmethod def create_pnl_fee_from_line(cls, line): fee_lines = [] Date = Pool().get('ir.date') Currency = Pool().get('currency.currency') FeeLots = Pool().get('fee.lots') #if line is matched with sale_line we should add the open sale side sale_lines = line.get_matched_lines() or [] sale_open_lots = tuple(s.lot_s for s in sale_lines if s.lot_s) all_lots = (line.lots or ()) + sale_open_lots for lot in all_lots: fl = FeeLots.search([('lot', '=', lot.id)]) if not fl: continue fees = [e.fee for e in fl] for sf in cls.group_fees_by_type_supplier(line, fees): sign = -1 if sf.p_r == 'pay' else 1 qty = round(lot.get_current_quantity_converted(), 5) if sf.mode == 'ppack' or sf.mode == 'rate': price = sf.price amount = sf.amount * sign elif sf.mode == 'lumpsum': price = sf.price amount = sf.price * sign qty = 1 else: price = Decimal(sf.get_price_per_qt()) amount = round(price * lot.get_current_quantity_converted() * sign, 2) if sf.currency != line.purchase.currency: with Transaction().set_context(date=Date.today()): price = Currency.compute(sf.currency, price, line.purchase.currency) if line.mtm: for strat in line.mtm: fee_lines.append({ 'lot': lot.id, 'sale': lot.sale_line.sale.id if lot.sale_line else None, 'purchase': line.purchase.id, 'line': line.id, 'type': ( 'shipment fee' if sf.shipment_in else 'sale fee' if sf.sale_line else 'pur. fee' ), 'date': Date.today(), 'price': price, 'counterparty': sf.supplier.id, 'reference': f"{sf.product.name}/{'Physic' if lot.lot_type == 'physic' else 'Open'}", 'product': sf.product.id, 'state': sf.type, 'quantity': qty, 'amount': amount, 'mtm': strat.get_mtm(line,qty), 'strategy': strat, 'unit': sf.unit.id if sf.unit else line.unit.id, 'currency': sf.currency.id, }) else: fee_lines.append({ 'lot': lot.id, 'sale': lot.sale_line.sale.id if lot.sale_line else None, 'purchase': line.purchase.id, 'line': line.id, 'type': ( 'shipment fee' if sf.shipment_in else 'sale fee' if sf.sale_line else 'pur. fee' ), 'date': Date.today(), 'price': price, 'counterparty': sf.supplier.id, 'reference': f"{sf.product.name}/{'Physic' if lot.lot_type == 'physic' else 'Open'}", 'product': sf.product.id, 'state': sf.type, 'quantity': qty, 'amount': amount, 'mtm': Decimal(0), 'strategy': None, 'unit': sf.unit.id if sf.unit else line.unit.id, 'currency': sf.currency.id, }) return fee_lines @classmethod def create_pnl_der_from_line(cls, line): Date = Pool().get('ir.date') der_lines = [] for d in line.derivatives or []: price = Decimal(d.price_index.get_price_per_qt( d.price, line.unit, line.purchase.currency )) mtm = Decimal(d.price_index.get_price( Date.today(), line.unit, line.purchase.currency, True )) der_lines.append({ 'purchase': line.purchase.id, 'line': line.id, 'type': 'derivative', 'date': Date.today(), 'reference': d.price_index.price_index, 'price': round(price, 4), 'counterparty': d.party.id, 'product': d.product.id, 'state': 'fixed', 'quantity': round(d.quantity, 5), 'amount': round(price * d.quantity * Decimal(-1), 2), 'mtm': round((price * d.quantity * Decimal(-1)) - (mtm * d.quantity * Decimal(-1)), 2), 'unit': line.unit.id, 'currency': line.purchase.currency.id, }) return der_lines @classmethod def generate(cls, line, valuation_type='all'): selected_types = cls._get_generate_types(valuation_type) cls._delete_existing(line, selected_types=selected_types) values = [] values.extend(cls.create_pnl_fee_from_line(line)) values.extend(cls.create_pnl_price_from_line(line)) values.extend(cls.create_pnl_der_from_line(line)) values = cls._filter_values_by_types(values, selected_types) if values: Valuation = Pool().get('valuation.valuation') ValuationLine = Pool().get('valuation.valuation.line') Valuation.create(values) ValuationLine.create(values) class Valuation(ValuationBase, ModelView): "Valuation" __name__ = 'valuation.valuation' @classmethod def get_totals(cls): cursor = Transaction().connection.cursor() table = cls.__table__() sql = f""" WITH totals AS ( SELECT date, SUM(amount) AS total_amount FROM {table} WHERE line IS NOT NULL OR sale_line IS NOT NULL GROUP BY date ), ranked AS ( SELECT date, total_amount, LAG(total_amount) OVER (ORDER BY date) AS previous_total, ROW_NUMBER() OVER (ORDER BY date DESC) AS rn FROM totals ) SELECT total_amount AS last_total, total_amount - previous_total AS last_variation FROM ranked WHERE rn = 1; """ cursor.execute(sql) row = cursor.fetchone() if not row: return Decimal(0), Decimal(0) last_total, last_variation = row return last_total or Decimal(0), last_variation or Decimal(0) class ValuationLine(ValuationBase, ModelView): "Last Valuation" __name__ = 'valuation.valuation.line' class ValuationDyn(ModelSQL,ModelView): "Valuation" __name__ = 'valuation.valuation.dyn' r_purchase = fields.Many2One('purchase.purchase',"Purchase") r_line = fields.Many2One('purchase.line',"Line") r_date = fields.Date("Date") r_type = fields.Selection(VALTYPE, "Type") r_reference = fields.Char("Reference") r_counterparty = fields.Many2One('party.party',"Counterparty") r_product = fields.Many2One('product.product',"Product") r_state = fields.Char("State") r_price = fields.Numeric("Price",digits='r_unit') r_currency = fields.Many2One('currency.currency',"Cur") r_quantity = fields.Numeric("Quantity",digits='r_unit') r_unit = fields.Many2One('product.uom',"Unit") r_amount = fields.Numeric("Amount",digits='r_unit') r_base_amount = fields.Numeric("Base Amount",digits='r_unit') r_rate = fields.Numeric("Rate",digits=(16,6)) r_mtm = fields.Numeric("Mtm",digits='r_unit') r_strategy = fields.Many2One('mtm.strategy',"Strategy") r_lot = fields.Many2One('lot.lot',"Lot") @classmethod def table_query(cls): Valuation = Pool().get('valuation.valuation.line') val = Valuation.__table__() context = Transaction().context group_pnl = context.get('group_pnl') wh = (val.id > 0) query = val.select( Literal(0).as_('create_uid'), CurrentTimestamp().as_('create_date'), Literal(None).as_('write_uid'), Literal(None).as_('write_date'), Max(val.id).as_('id'), Max(val.purchase).as_('r_purchase'), Max(val.line).as_('r_line'), Max(val.date).as_('r_date'), val.type.as_('r_type'), Max(val.reference).as_('r_reference'), val.counterparty.as_('r_counterparty'), Max(val.product).as_('r_product'), val.state.as_('r_state'), Avg(val.price).as_('r_price'), Max(val.currency).as_('r_currency'), Sum(val.quantity).as_('r_quantity'), Max(val.unit).as_('r_unit'), Sum(val.amount).as_('r_amount'), Sum(val.base_amount).as_('r_base_amount'), Sum(val.rate).as_('r_rate'), Sum(val.mtm).as_('r_mtm'), Max(val.strategy).as_('r_strategy'), Max(val.lot).as_('r_lot'), where=wh, group_by=[val.type,val.counterparty,val.state]) return query class ValuationReport(ValuationBase, ModelView): "Valuation Report" __name__ = 'valuation.report' @classmethod def table_query(cls): Valuation = Pool().get('valuation.valuation') Date = Pool().get('ir.date') val = Valuation.__table__() context = Transaction().context valuation_date = context.get('valuation_date') strategy = context.get('strategy') if not valuation_date: valuation_date = Date.today() wh = (val.date == valuation_date) if strategy: wh &= (val.strategy == strategy) query = val.select( Literal(0).as_('create_uid'), CurrentTimestamp().as_('create_date'), Literal(None).as_('write_uid'), Literal(None).as_('write_date'), val.id.as_('id'), val.purchase.as_('purchase'), val.sale.as_('sale'), val.sale_line.as_('sale_line'), val.line.as_('line'), val.date.as_('date'), val.type.as_('type'), val.reference.as_('reference'), val.counterparty.as_('counterparty'), val.product.as_('product'), val.state.as_('state'), val.price.as_('price'), val.currency.as_('currency'), val.quantity.as_('quantity'), val.unit.as_('unit'), val.amount.as_('amount'), val.base_amount.as_('base_amount'), val.rate.as_('rate'), val.mtm.as_('mtm'), val.strategy.as_('strategy'), val.lot.as_('lot'), where=wh) return query class ValuationReportContext(ModelView): "Valuation Report Context" __name__ = 'valuation.report.context' valuation_date = fields.Date("Valuation date") supplier = fields.Many2One('party.party',"Supplier") client = fields.Many2One('party.party',"Client") product = fields.Many2One('product.product',"Product") purchase = fields.Many2One('purchase.purchase', "Purchase") sale = fields.Many2One('sale.sale',"Sale") state = fields.Selection([ ('all', 'All'), ('open', 'Open'), ('fixed', 'Fixed'), ('hedged', 'Hedged') ], 'State') strategy = fields.Many2One('mtm.strategy',"Strategy") @classmethod def default_valuation_date(cls): pool = Pool() Date = pool.get('ir.date') return Date.today() @classmethod def default_state(cls): return 'all' class ValuationProcessDimension(ModelView): "Valuation Process Dimension" __name__ = 'valuation.process.dimension' start = fields.Many2One('valuation.process.start', "Start") dimension = fields.Many2One( 'analytic.dimension', 'Dimension', required=True ) value = fields.Many2One( 'analytic.dimension.value', 'Value', required=True, domain=[ ('dimension', '=', Eval('dimension')), ], depends=['dimension'] ) class ValuationProcessStart(ModelView): "Valuation Process Start" __name__ = 'valuation.process.start' purchase_from_date = fields.Date("Purchase From Date") purchase_to_date = fields.Date("Purchase To Date") sale_from_date = fields.Date("Sale From Date") sale_to_date = fields.Date("Sale To Date") purchase_numbers = fields.Text("Purchase Numbers") sale_numbers = fields.Text("Sale Numbers") analytic_dimensions = fields.One2Many( 'valuation.process.dimension', 'start', 'Analytic Dimensions' ) valuation_type = fields.Selection( VALUATION_TYPE_GROUPS, "Type", required=True ) @classmethod def default_valuation_type(cls): return 'all' class ValuationProcessResult(ModelView): "Valuation Process Result" __name__ = 'valuation.process.result' message = fields.Text("Message", readonly=True) class ValuationProcess(Wizard): "Process Valuation" __name__ = 'valuation.process' start = StateView( 'valuation.process.start', 'purchase_trade.valuation_process_start_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Process', 'process', 'tryton-ok', default=True), ] ) process = StateTransition() result = StateView( 'valuation.process.result', 'purchase_trade.valuation_process_result_form', [ Button('Close', 'end', 'tryton-cancel'), Button('See Valuation', 'open_report', 'tryton-go-next', default=True), ] ) open_report = StateAction('purchase_trade.act_valuation_form') _result_message = None @staticmethod def _parse_numbers(text): if not text: return [] return [item for item in re.split(r'[\s,;]+', text) if item] @staticmethod def _matches_dimensions(record, dimension_filters): assignments = getattr(record, 'analytic_dimensions', []) or [] assignment_pairs = { (assignment.dimension.id, assignment.value.id) for assignment in assignments if assignment.dimension and assignment.value } return all( (dimension.id, value.id) in assignment_pairs for dimension, value in dimension_filters ) @classmethod def _get_dimension_filters(cls, start): return [ (line.dimension, line.value) for line in start.analytic_dimensions or [] if line.dimension and line.value ] @classmethod def _search_purchase_ids(cls, start, dimension_filters): Purchase = Pool().get('purchase.purchase') domain = [] numbers = cls._parse_numbers(start.purchase_numbers) if start.purchase_from_date: domain.append(('purchase_date', '>=', start.purchase_from_date)) if start.purchase_to_date: domain.append(('purchase_date', '<=', start.purchase_to_date)) if numbers: domain.append(('number', 'in', numbers)) purchases = Purchase.search(domain) if dimension_filters: purchases = [ purchase for purchase in purchases if cls._matches_dimensions(purchase, dimension_filters) ] return {purchase.id for purchase in purchases} @classmethod def _search_sale_ids(cls, start, dimension_filters): Sale = Pool().get('sale.sale') domain = [] numbers = cls._parse_numbers(start.sale_numbers) if start.sale_from_date: domain.append(('sale_date', '>=', start.sale_from_date)) if start.sale_to_date: domain.append(('sale_date', '<=', start.sale_to_date)) if numbers: domain.append(('number', 'in', numbers)) sales = Sale.search(domain) if dimension_filters: sales = [sale for sale in sales if cls._matches_dimensions(sale, dimension_filters)] return {sale.id for sale in sales} @classmethod def _purchase_line_ids_from_purchase_ids(cls, purchase_ids): if not purchase_ids: return set() PurchaseLine = Pool().get('purchase.line') return { line.id for line in PurchaseLine.search([('purchase', 'in', list(purchase_ids))]) } @classmethod def _purchase_line_ids_from_sale_ids(cls, sale_ids): if not sale_ids: return set() SaleLine = Pool().get('sale.line') purchase_line_ids = set() sale_lines = SaleLine.search([('sale', 'in', list(sale_ids))]) for sale_line in sale_lines: for matched_line in sale_line.get_matched_lines() or []: if matched_line.lot_p and matched_line.lot_p.line: purchase_line_ids.add(matched_line.lot_p.line.id) return purchase_line_ids @classmethod def _get_target_purchase_line_ids(cls, start): PurchaseLine = Pool().get('purchase.line') dimension_filters = cls._get_dimension_filters(start) has_purchase_filters = bool( start.purchase_from_date or start.purchase_to_date or cls._parse_numbers(start.purchase_numbers) ) has_sale_filters = bool( start.sale_from_date or start.sale_to_date or cls._parse_numbers(start.sale_numbers) ) purchase_side_ids = cls._purchase_line_ids_from_purchase_ids( cls._search_purchase_ids( start, dimension_filters if (dimension_filters and (has_purchase_filters or not has_sale_filters)) else [], ) ) if (has_purchase_filters or (dimension_filters and not has_sale_filters)) else set() sale_side_ids = cls._purchase_line_ids_from_sale_ids( cls._search_sale_ids( start, dimension_filters if (dimension_filters and (has_sale_filters or not has_purchase_filters)) else [], ) ) if (has_sale_filters or (dimension_filters and not has_purchase_filters)) else set() if has_purchase_filters and has_sale_filters: target_ids = purchase_side_ids & sale_side_ids elif has_purchase_filters: target_ids = purchase_side_ids elif has_sale_filters: target_ids = sale_side_ids elif dimension_filters: target_ids = purchase_side_ids | sale_side_ids else: target_ids = {line.id for line in PurchaseLine.search([])} return target_ids def transition_process(self): PurchaseLine = Pool().get('purchase.line') target_ids = self._get_target_purchase_line_ids(self.start) lines = PurchaseLine.browse(list(target_ids)) purchase_ids = {line.purchase.id for line in lines if line.purchase} sale_ids = set() for line in lines: for matched_line in line.get_matched_lines() or []: if matched_line.lot_s and matched_line.lot_s.sale_line: sale_ids.add(matched_line.lot_s.sale_line.sale.id) Valuation.generate(line, valuation_type=self.start.valuation_type) self._result_message = ( f"Processed {len(lines)} purchase line(s) " f"from {len(purchase_ids)} purchase(s) " f"and {len(sale_ids)} linked sale(s)." ) return 'result' def default_result(self, fields): return { 'message': self._result_message or 'No valuation was processed.', } def do_open_report(self, action): Date = Pool().get('ir.date') action['pyson_context'] = PYSONEncoder().encode({ 'valuation_date': Date.today(), }) return action, {}