# This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. from trytond.model import fields, Workflow from trytond.pool import Pool, PoolMeta from trytond.pyson import Bool, Eval, Id, If, PYSONEncoder from trytond.model import (ModelSQL, ModelView) from trytond.i18n import gettext from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction from trytond.transaction import Transaction, inactive_records from decimal import getcontext, Decimal, ROUND_CEILING, ROUND_FLOOR, 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 import datetime import logging import json from collections import defaultdict from trytond.exceptions import UserWarning, UserError from trytond.modules.purchase_trade.numbers_to_words import quantity_to_words, amount_to_currency_words, format_date_en from trytond.modules.purchase_trade.company_defaults import ( add_itsa_analytic_dimension_defaults, default_itsa_analytic_dimension_assignments, default_itsa_currency, default_itsa_unit, is_itsa_company, record_id) logger = logging.getLogger(__name__) VALTYPE = [ ('priced', 'Price'), ('fee', 'Fee'), ('market', 'Market'), ('derivative', 'Derivative'), ] class ContractDocumentType(metaclass=PoolMeta): "Contract - Document Type" __name__ = 'contract.document.type' # lc_out = fields.Many2One('lc.letter.outgoing', 'LC out') # lc_in = fields.Many2One('lc.letter.incoming', 'LC in') sale = fields.Many2One('sale.sale', "Sale") class AnalyticDimensionAssignment(metaclass=PoolMeta): 'Analytic Dimension Assignment' __name__ = 'analytic.dimension.assignment' sale = fields.Many2One('sale.sale', "Sale") class Estimated(metaclass=PoolMeta): "Estimated date" __name__ = 'pricing.estimated' sale_line = fields.Many2One('sale.line',"Line") class FeeLots(metaclass=PoolMeta): "Fee lots" __name__ = 'fee.lots' sale_line = fields.Many2One('sale.line',"Line", ondelete='CASCADE') class Backtoback(metaclass=PoolMeta): 'Back To Back' __name__ = 'back.to.back' sale = fields.One2Many('sale.sale','btb', "Sale") class OpenPosition(metaclass=PoolMeta): "Open position" __name__ = 'open.position' sale = fields.Many2One('sale.sale',"Sale") sale_line = fields.Many2One('sale.line',"Sale Line") client = fields.Many2One('party.party',"Client") class SaleStrategy(ModelSQL): "Sale - Document Type" __name__ = 'sale.strategy' sale_line = fields.Many2One('sale.line', 'Sale Line') strategy = fields.Many2One('mtm.strategy', "Strategy") class Component(metaclass=PoolMeta): "Component" __name__ = 'pricing.component' sale_line = fields.Many2One('sale.line',"Line") quota_sale = fields.Function(fields.Numeric("Quota",digits='unit'),'get_quota_sale') unit_sale = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit_sale') def getDelMonthDateSale(self): PM = Pool().get('product.month') if self.sale_line and hasattr(self.sale_line, 'del_period') and self.sale_line.del_period: pm = PM(self.sale_line.del_period) if pm: logger.info("DELMONTHDATE:%s",pm.beg_date) return pm.beg_date def getEstimatedTriggerSale(self,t): logger.info("GETTRIGGER:%s",t) if t == 'delmonth': return self.getDelMonthDateSale() if self.sale_line and hasattr(self.sale_line, 'get_date'): trigger_date = self.sale_line.get_date(t) if trigger_date: return trigger_date PE = Pool().get('pricing.estimated') Date = Pool().get('ir.date') pe = PE.search([('sale_line','=',self.sale_line),('trigger','=',t)]) if pe: return pe[0].estimated_date else: return Date.today() def get_unit_sale(self, name): if self.sale_line: return self.sale_line.unit def get_quota_sale(self, name): if self.sale_line: quantity = getattr(self.sale_line, 'quantity_theorical', None) if quantity is None: quantity = getattr(self.sale_line, 'quantity', None) if quantity is not None: nbdays = self.nbdays if self.nbdays and self.nbdays > 0 else 1 return round(Decimal(quantity) / nbdays, 4) class Pricing(metaclass=PoolMeta): "Pricing" __name__ = 'pricing.pricing' sale_line = fields.Many2One('sale.line',"Lines") unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit_sale') def get_unit_sale(self,name): if self.sale_line: return self.sale_line.unit def get_eod_price_sale(self): return self._weighted_average_price( self.fixed_qt, self.fixed_qt_price, self.unfixed_qt, self.unfixed_qt_price, ) class Summary(ModelSQL,ModelView): "Pricing summary" __name__ = 'sale.pricing.summary' sale_line = fields.Many2One('sale.line',"Lines") price_component = fields.Many2One('pricing.component',"Component") quantity = fields.Numeric("Qt",digits=(1,4)) fixed_qt = fields.Numeric("Fixed qt",digits=(1,4)) unfixed_qt = fields.Numeric("Unfixed qt",digits=(1,4)) price = fields.Numeric("Price",digits=(1,4)) progress = fields.Float("Fix. progress") ratio = fields.Numeric("Ratio") def get_name(self): if self.price_component: return self.price_component.get_rec_name() return "" def get_last_price(self): Date = Pool().get('ir.date') if self.price_component: pc = Pool().get('pricing.component')(self.price_component) if pc.price_index: PI = Pool().get('price.price') pi = PI(pc.price_index) return pi.get_price(Date.today(),self.sale_line.unit,self.sale_line.sale.currency,True) @classmethod def table_query(cls): SalePricing = Pool().get('pricing.pricing') sp = SalePricing.__table__() SaleComponent = Pool().get('pricing.component') sc = SaleComponent.__table__() #wh = Literal(True) context = Transaction().context group_pnl = context.get('group_pnl') if group_pnl: return None query = sp.join(sc,'LEFT',condition=sp.price_component == sc.id).select( Literal(0).as_('create_uid'), CurrentTimestamp().as_('create_date'), Literal(None).as_('write_uid'), Literal(None).as_('write_date'), Max(sp.id).as_('id'), sp.sale_line.as_('sale_line'), sp.price_component.as_('price_component'), Max(sp.fixed_qt+sp.unfixed_qt).as_('quantity'), Max(sp.fixed_qt).as_('fixed_qt'), (Min(sp.unfixed_qt)).as_('unfixed_qt'), Max(Case((sp.last, sp.eod_price),else_=0)).as_('price'), (Max(sp.fixed_qt)/Max(sp.fixed_qt+sp.unfixed_qt)).as_('progress'), Max(sc.ratio).as_('ratio'), #where=wh, group_by=[sp.sale_line,sp.price_component]) return query class Lot(metaclass=PoolMeta): __name__ = 'lot.lot' sale_line = fields.Many2One('sale.line',"Sale",ondelete='CASCADE') lot_quantity_sale = fields.Function(fields.Numeric("Net weight",digits='lot_unit'),'get_qt') lot_gross_quantity_sale = fields.Function(fields.Numeric("Gross weight",digits='lot_unit'),'get_gross_qt') def get_qt(self, name): quantity = self.lot_quantity if self.lot_hist: for h in self.lot_hist: if h.quantity_type.id == 3: quantity = h.quantity return quantity def get_gross_qt(self, name): quantity = self.lot_quantity if self.lot_hist: for h in self.lot_hist: if h.quantity_type.id == 3: quantity = h.quantity return quantity def getClient(self): if self.sale_line: return Pool().get('sale.sale')(self.sale_line.sale).party.id def getSale(self): if self.sale_line: return self.sale_line.sale.id class Sale(metaclass=PoolMeta): __name__ = 'sale.sale' @classmethod def __setup__(cls): super().__setup__() cls._transitions.discard(('confirmed', 'processing')) cls.lines.states['readonly'] = ( (Eval('state') != 'draft') & ((Eval('state') != 'quotation') | ~Eval('allow_modification_after_validation'))) cls._buttons['process'] = { 'invisible': True, 'depends': ['state'], } @classmethod @ModelView.button @Workflow.transition('confirmed') def confirm(cls, sales): cls.set_sale_date(sales) cls.store_cache(sales) @classmethod @ModelView.button def process(cls, sales): sales = [s for s in sales if s.state != 'confirmed'] if sales: super().process(sales) @classmethod def create(cls, vlist): for values in vlist: add_itsa_analytic_dimension_defaults(values) return super().create(vlist) @classmethod def default_analytic_dimensions(cls): default = getattr(super(), 'default_analytic_dimensions', None) values = default() if default else [] return values or default_itsa_analytic_dimension_assignments() btb = fields.Many2One('back.to.back',"Back to back") charter_conditions = fields.One2Many( 'charter.condition', 'sale', "Charter Conditions") contract_template = fields.Many2One( 'contract.template', "Contract Template", domain=[ ('direction', 'in', ['both', 'sale']), ('active', '=', True), ]) contract_clauses = fields.One2Many( 'contract.clause.selection', 'sale', "Contract Clauses") bank_accounts = fields.Function( fields.Many2Many('bank.account', None, None, "Bank Accounts"), 'on_change_with_bank_accounts') bank_account = fields.Many2One( 'bank.account', "Client Bank Account", domain=[('id', 'in', Eval('bank_accounts', []))], depends=['bank_accounts']) our_bank_account = fields.Many2One( 'bank.account', "Our Bank Account") from_location = fields.Many2One('stock.location', 'From location', required=True,domain=[('type', "!=", 'customer')]) to_location = fields.Many2One('stock.location', 'To location', required=True,domain=[('type', "!=", 'supplier')]) shipment_out = fields.Many2One('stock.shipment.out','Sales') #pnl = fields.One2Many('valuation.valuation', 'sale', 'Pnl') pnl = fields.One2Many('valuation.valuation.dyn', 'r_sale', 'Pnl',states={'invisible': ~Eval('group_pnl'),}) pnl_ = fields.One2Many('valuation.valuation.line', 'sale', 'Pnl',states={'invisible': Eval('group_pnl'),}) group_pnl = fields.Boolean("Group Pnl") derivatives = fields.One2Many('derivative.derivative', 'sale', 'Derivative') #plans = fields.One2Many('workflow.plan','sale',"Execution plans") forex = fields.One2Many('forex.cover.physical.sale','contract',"Forex",readonly=True) broker = fields.Many2One('party.party',"Broker",domain=[('categories.parent', 'child_of', [4])]) tol_min = fields.Numeric("Tol - in %", required=True) tol_max = fields.Numeric("Tol + in %", required=True) tol_min_qt = fields.Numeric("Tol -") tol_max_qt = fields.Numeric("Tol +") tolerance_used = fields.Function( fields.Numeric("Tolerance used"), 'get_tolerance_used') tolerance_option = fields.Selection([ ('', ''), ('buyer_option', "Buyer's option"), ('seller_option', "Seller's option"), ], "Tolerance option") tolerance_min = fields.Function( fields.Numeric("Tolerance min"), 'get_tolerance_min') tolerance_max = fields.Function( fields.Numeric("Tolerance max"), 'get_tolerance_max') certif = fields.Many2One('purchase.certification',"Certification", required=True,states={'invisible': Eval('company_visible'),}) wb = fields.Many2One('purchase.weight.basis',"Weight basis", required=True) association = fields.Many2One('purchase.association',"Association", required=True,states={'invisible': Eval('company_visible'),}) crop = fields.Many2One('purchase.crop',"Crop",states={'invisible': Eval('company_visible'),}) viewer = fields.Function(fields.Text(""),'get_viewer') execution_summary_viewer = fields.Function( fields.Text(""), 'get_execution_summary_viewer') doc_template = fields.Many2One('doc.template',"Template") required_documents = fields.Many2Many( 'contract.document.type', 'sale', 'doc_type', 'Required Documents') analytic_dimensions = fields.One2Many( 'analytic.dimension.assignment', 'sale', 'Analytic Dimensions' ) allow_modification_after_validation = fields.Function( fields.Boolean("Autorise modification after validation"), 'on_change_with_allow_modification_after_validation') trader = fields.Many2One( 'party.party', "Trader", domain=[('categories.name', '=', 'TRADER')]) operator = fields.Many2One( 'party.party', "Operator", domain=[('categories.name', '=', 'OPERATOR')]) our_reference = fields.Char("Our Reference") company_visible = fields.Function( fields.Boolean("Visible"), 'on_change_with_company_visible') lc_date = fields.Date("LC date") product_origin = fields.Char( "Origin", states={'invisible': ~Eval('company_visible'),}) @fields.depends('company', '_parent_company.party') def on_change_with_company_visible(self, name=None): return bool( self.company and self.company.party and self.company.party.name in {'MELYA', 'ITSA'}) def on_change_with_allow_modification_after_validation(self, name=None): Configuration = Pool().get('sale.configuration') configurations = Configuration.search([], limit=1) return bool( configurations and configurations[0].allow_modification_after_validation) def _get_default_bank_account(self): if not self.party or not self.party.bank_accounts: return None party_bank_accounts = list(self.party.bank_accounts) if self.currency: for account in party_bank_accounts: if account.currency == self.currency: return account return party_bank_accounts[0] def _get_default_our_bank_account(self): if (not self.company or not self.company.party or not self.company.party.bank_accounts): return None company_bank_accounts = list(self.company.party.bank_accounts) if self.currency: for account in company_bank_accounts: if account.currency == self.currency: return account return company_bank_accounts[0] @fields.depends('party', '_parent_party.bank_accounts') def on_change_with_bank_accounts(self, name=None): if self.party and self.party.bank_accounts: return [account.id for account in self.party.bank_accounts] return [] @fields.depends( 'company', 'party', 'invoice_party', 'shipment_party', 'warehouse', 'payment_term', 'lines', 'bank_account', 'our_bank_account', '_parent_party.bank_accounts', '_parent_company.party') def on_change_party(self): super().on_change_party() self.bank_account = self._get_default_bank_account() self.our_bank_account = self._get_default_our_bank_account() @fields.depends( 'party', 'company', 'currency', '_parent_party.bank_accounts', '_parent_company.party') def on_change_currency(self): self.bank_account = self._get_default_bank_account() self.our_bank_account = self._get_default_our_bank_account() @fields.depends('company', 'currency', 'our_bank_account', '_parent_company.party') def on_change_company(self): previous_currency = self.currency super().on_change_company() if (is_itsa_company(self.company) and (not previous_currency or record_id(self.currency) == record_id(getattr(self.company, 'currency', None)) or record_id(previous_currency) == record_id(getattr(self.company, 'currency', None)))): currency = default_itsa_currency() if currency: self.currency = currency self.our_bank_account = self._get_default_our_bank_account() @classmethod def default_currency(cls, **pattern): company = pattern.get('company') if company and not hasattr(company, 'party'): company = Pool().get('company.company')(company) if is_itsa_company(company): currency = default_itsa_currency() if currency: return currency default = getattr(super(), 'default_currency', None) if default: return default(**pattern) @classmethod def default_wb(cls): WB = Pool().get('purchase.weight.basis') wb = WB.search(['id','>',0]) if wb: return wb[0].id @classmethod def default_certif(cls): Certification = Pool().get('purchase.certification') certification = Certification.search(['id','>',0]) if certification: return certification[0].id @classmethod def default_association(cls): Association = Pool().get('purchase.association') association = Association.search(['id','>',0]) if association: return association[0].id @classmethod def default_tol_min(cls): return 0 @classmethod def default_tol_max(cls): return 0 @classmethod def default_tolerance_option(cls): return '' def _line_quantity_in_unit(self, quantity, from_unit, to_unit): if not from_unit or not to_unit or from_unit == to_unit: return abs(Decimal(str(quantity or 0))) Uom = Pool().get('product.uom') factor = None rate = None if from_unit.category.id != to_unit.category.id: factor = 1 rate = 1 return abs(Decimal(str(Uom.compute_qty( from_unit, float(quantity or 0), to_unit, True, factor, rate)))) def _line_lot_quantity_for_tolerance(self, line): LotQt = Pool().get('lot.qt') quantity = Decimal(0) unit = getattr(line, 'unit', None) physical_lots = [ lot for lot in getattr(line, 'lots', None) or [] if getattr(lot, 'lot_type', None) == 'physic'] if physical_lots: return sum( abs(Decimal(str( lot.get_current_quantity_converted(unit=unit) or 0))) for lot in physical_lots) for lot in getattr(line, 'lots', None) or []: if getattr(lot, 'lot_type', None) != 'virtual': continue for lqt in LotQt.search([('lot_s', '=', lot.id)]): quantity += self._line_quantity_in_unit( getattr(lqt, 'lot_quantity', 0), getattr(lqt, 'lot_unit', None), unit) return quantity def _tolerance_totals(self): theoretical = Decimal(0) actual = Decimal(0) for line in self.lines or []: if getattr(line, 'type', None) != 'line': continue theoretical += abs(Decimal(str( getattr(line, 'quantity_theorical', None) or getattr(line, 'quantity', None) or 0))) if hasattr(line, '_tolerance_targeted_quantity'): actual += line._tolerance_targeted_quantity() else: actual += self._line_lot_quantity_for_tolerance(line) return theoretical, actual def get_tolerance_used(self, name): theoretical, actual = self._tolerance_totals() if not theoretical: return Decimal(0) return round( ((actual - theoretical) / theoretical) * Decimal(100), 5) def get_tolerance_min(self, name): return -Decimal(str(self.tol_min or 0)) def get_tolerance_max(self, name): return Decimal(str(self.tol_max or 0)) @staticmethod def _has_matched_physical_lots(sale): for line in sale.lines or []: for lot in line.lots or []: if ( getattr(lot, 'lot_type', None) == 'physic' and getattr(lot, 'line', None) and getattr(lot, 'sale_line', None)): return True return False @classmethod def delete(cls, sales): for sale in sales: if cls._has_matched_physical_lots(sale): raise UserError( "You cannot delete a sale matched to a purchase") super().delete(sales) def _get_report_lines(self): return [line for line in self.lines if getattr(line, 'type', None) == 'line'] def _get_report_first_line(self): lines = self._get_report_lines() if lines: return lines[0] @staticmethod def _format_report_number(value, digits='0.0000', keep_trailing_decimal=False, strip_trailing_zeros=True, use_grouping=False): value = Decimal(str(value or 0)).quantize(Decimal(digits)) text = format(value, ',f' if use_grouping else 'f') if strip_trailing_zeros: text = text.rstrip('0').rstrip('.') if keep_trailing_decimal and '.' not in text: text += '.0' return text or '0' def _format_report_price_words(self, line): value = self._get_report_display_price_value(line) currency = self._get_report_display_currency(line) if currency and (currency.rec_name or '').upper() == 'USC': return amount_to_currency_words(value, 'USC', 'USC') return amount_to_currency_words(value) def _get_report_display_currency(self, line): if getattr(line, 'price_type', None) == 'basis': if getattr(line, 'enable_linked_currency', False) and getattr(line, 'linked_currency', None): return line.linked_currency return self.currency return getattr(line, 'linked_currency', None) or self.currency def _get_report_display_unit(self, line): if getattr(line, 'price_type', None) == 'basis': if getattr(line, 'enable_linked_currency', False) and getattr(line, 'linked_unit', None): return line.linked_unit return getattr(line, 'unit', None) return getattr(line, 'linked_unit', None) or getattr(line, 'unit', None) def _get_report_display_price_value(self, line): if getattr(line, 'price_type', None) == 'basis': if getattr(line, 'enable_linked_currency', False) and getattr(line, 'linked_currency', None): premium = getattr(line, 'premium', None) if premium is None and hasattr(line, '_get_premium_price'): premium = line._get_premium_price() return Decimal(str(premium or 0)) return Decimal(str(line._get_premium_price() or 0)) if getattr(line, 'linked_price', None): return Decimal(str(line.linked_price or 0)) return Decimal(str(line.unit_price or 0)) def _format_report_price_line(self, line): currency = self._get_report_display_currency(line) unit = self._get_report_display_unit(line) pricing_text = getattr(line, 'get_pricing_text', '') or '' parts = [ (currency.rec_name.upper() if currency and currency.rec_name else '').strip(), self._format_report_number( self._get_report_display_price_value(line), strip_trailing_zeros=False), 'PER', (unit.rec_name.upper() if unit and unit.rec_name else '').strip(), f"({self._format_report_price_words(line)})", ] if pricing_text: parts.append(pricing_text) return ' '.join(part for part in parts if part) @property def report_terms(self): line = self._get_report_first_line() if line: return line.note return '' @staticmethod def _get_report_line_lots(line): return list(getattr(line, 'lots', []) or []) @classmethod def _get_report_preferred_lots(cls, line): lots = cls._get_report_line_lots(line) physicals = [ lot for lot in lots if getattr(lot, 'lot_type', None) == 'physic' ] if physicals: return physicals virtuals = [ lot for lot in lots if getattr(lot, 'lot_type', None) == 'virtual' ] if len(virtuals) == 1: return virtuals return [] @staticmethod def _get_report_lot_hist_weights(lot): if not lot: return None, None if hasattr(lot, 'get_hist_quantity'): net, gross = lot.get_hist_quantity() return ( Decimal(str(net or 0)), Decimal(str(gross if gross not in (None, '') else net or 0)), ) hist = list(getattr(lot, 'lot_hist', []) or []) state = getattr(lot, 'lot_state', None) state_id = getattr(state, 'id', None) if state_id is not None: for entry in hist: quantity_type = getattr(entry, 'quantity_type', None) if getattr(quantity_type, 'id', None) == state_id: net = Decimal(str(getattr(entry, 'quantity', 0) or 0)) gross = Decimal(str( getattr(entry, 'gross_quantity', None) if getattr(entry, 'gross_quantity', None) not in (None, '') else net)) return net, gross return None, None @classmethod def _get_report_line_weights(cls, line): lots = cls._get_report_preferred_lots(line) if lots: net_total = Decimal(0) gross_total = Decimal(0) for lot in lots: net, gross = cls._get_report_lot_hist_weights(lot) if net is None: continue net_total += net gross_total += gross if net_total or gross_total: return net_total, gross_total quantity = Decimal(str(getattr(line, 'quantity', 0) or 0)) return quantity, quantity @classmethod def _get_report_line_unit(cls, line): lots = cls._get_report_preferred_lots(line) if lots and getattr(lots[0], 'lot_unit_line', None): return lots[0].lot_unit_line return getattr(line, 'unit', None) def _get_report_total_unit(self): virtual_units = [] for line in self._get_report_lines(): for lot in self._get_report_line_lots(line): if ( getattr(lot, 'lot_type', None) == 'virtual' and getattr(lot, 'lot_unit_line', None)): virtual_units.append(lot.lot_unit_line) if len(virtual_units) == 1: return virtual_units[0] line = self._get_report_first_line() if line: return self._get_report_line_unit(line) return None @staticmethod def _get_report_unit_wording(unit): label = '' for attr in ('symbol', 'rec_name', 'name'): value = getattr(unit, attr, None) if value: label = str(value).strip().upper() break mapping = { 'MT': ('METRIC TON', 'METRIC TONS'), 'METRIC TON': ('METRIC TON', 'METRIC TONS'), 'METRIC TONS': ('METRIC TON', 'METRIC TONS'), 'KG': ('KILOGRAM', 'KILOGRAMS'), 'KGS': ('KILOGRAM', 'KILOGRAMS'), 'KILOGRAM': ('KILOGRAM', 'KILOGRAMS'), 'KILOGRAMS': ('KILOGRAM', 'KILOGRAMS'), 'LB': ('POUND', 'POUNDS'), 'LBS': ('POUND', 'POUNDS'), 'POUND': ('POUND', 'POUNDS'), 'POUNDS': ('POUND', 'POUNDS'), 'BALE': ('BALE', 'BALES'), 'BALES': ('BALE', 'BALES'), } if label in mapping: return mapping[label] if label.endswith('S') and len(label) > 1: return label[:-1], label return label, label @classmethod def _report_quantity_to_words(cls, quantity, unit): singular, plural = cls._get_report_unit_wording(unit) return quantity_to_words( quantity, unit_singular=singular, unit_plural=plural, ) @staticmethod def _convert_report_quantity(quantity, from_unit, to_unit): value = Decimal(str(quantity or 0)) if not from_unit or not to_unit: return value if getattr(from_unit, 'id', None) == getattr(to_unit, 'id', None): return value from_name = getattr(from_unit, 'rec_name', None) to_name = getattr(to_unit, 'rec_name', None) if from_name and to_name and from_name == to_name: return value Uom = Pool().get('product.uom') converted = Uom.compute_qty(from_unit, float(value), to_unit) or 0 return Decimal(str(converted)) def _get_report_total_weight(self, index): lines = self._get_report_lines() if not lines: return None total_unit = self._get_report_total_unit() total = Decimal(0) for line in lines: quantity = self._get_report_line_weights(line)[index] total += self._convert_report_quantity( quantity, self._get_report_line_unit(line), total_unit, ) return total @property def report_gross(self): total = self._get_report_total_weight(1) if total is not None: return total return '' @property def report_net(self): total = self._get_report_total_weight(0) if total is not None: return total return '' @property def report_total_quantity(self): total = self._get_report_total_weight(0) if total is not None: return self._format_report_number(total, keep_trailing_decimal=True) return '0.0' @property def report_quantity_unit_upper(self): line = self._get_report_first_line() unit = self._get_report_line_unit(line) if line else None if unit and unit.rec_name: return unit.rec_name.upper() return '' def _get_report_line_quantity(self, line): return self._get_report_line_weights(line)[0] @property def report_qt(self): total = self._get_report_total_weight(0) if total is not None: return self._report_quantity_to_words( total, self._get_report_total_unit()) return '' @property def report_quantity_lines(self): lines = self._get_report_lines() if not lines: return '' details = [] for line in lines: current_quantity = self._get_report_line_quantity(line) quantity = self._format_report_number( current_quantity, keep_trailing_decimal=True) line_unit = self._get_report_line_unit(line) unit = ( line_unit.rec_name.upper() if line_unit and line_unit.rec_name else '' ) words = self._report_quantity_to_words(current_quantity, line_unit) period = line.del_period.description if getattr(line, 'del_period', None) else '' detail = ' '.join( part for part in [ quantity, unit, f"({words})", f"- {period}" if period else '', ] if part) if detail: details.append(detail) return '\n'.join(details) @property def report_nb_bale(self): nb_bale = 0 lines = self._get_report_lines() if lines: for line in lines: if line.lots: nb_bale += sum([l.lot_qt for l in line.lots if l.lot_type == 'physic']) if nb_bale: return 'NB BALES: ' + str(int(nb_bale)) return '' @property def report_product_name(self): line = self._get_report_first_line() if line and line.product: return line.product.name or '' return '' @property def report_product_description(self): line = self._get_report_first_line() if line and line.product: return line.product.description or '' return '' @property def report_crop_name(self): if self.crop: return self.crop.name or '' return '' @property def report_deal(self): purchase = self._get_report_deal_purchase() purchase_number = self._format_report_deal_number( getattr(purchase, 'number', None), 'P') sale_number = self._format_report_deal_number( getattr(self, 'number', None), 'S') return ' '.join( number for number in [purchase_number, sale_number] if number) def _get_report_deal_purchase(self): lots = [] for line in getattr(self, 'lines', []) or []: lots.extend(getattr(line, 'lots', []) or []) for lot in lots: purchase = getattr(getattr(lot, 'line', None), 'purchase', None) if purchase: return purchase if not lots: return None LotQt = Pool().get('lot.qt') for lot in lots: lot_ids = [] lot_id = getattr(lot, 'id', None) if lot_id: lot_ids.append(lot_id) get_vlot_s = getattr(lot, 'getVlot_s', None) if get_vlot_s: virtual_lot = get_vlot_s() virtual_lot_id = getattr(virtual_lot, 'id', None) if virtual_lot_id: lot_ids.append(virtual_lot_id) for lot_id in lot_ids: lot_qts = LotQt.search([ ('lot_s', '=', lot_id), ('lot_p', '>', 0), ], limit=1) for lot_qt in lot_qts: purchase = getattr( getattr(getattr(lot_qt, 'lot_p', None), 'line', None), 'purchase', None) if purchase: return purchase return None @staticmethod def _format_report_deal_number(number, prefix): if not number: return '' number = str(number) expected_prefix = prefix + '-' if number.upper().startswith(expected_prefix): return number return expected_prefix + number @property def report_melya_proforma_number(self): return self.report_deal or self.full_number or '' @property def report_melya_buyer_address(self): if not self.invoice_address: return '' with Transaction().set_context(address_with_party=True): return self.invoice_address.full_address @property def report_melya_buyer_block(self): return '\n'.join(filter(None, [ 'Buyer:', self.report_melya_buyer_address, ])) @property def report_melya_incoterm(self): parts = [] if self.incoterm and self.incoterm.code: parts.append(self.incoterm.code) if self.incoterm_location: if self.incoterm_location.party_name: parts.append(self.incoterm_location.party_name) if self.incoterm_location.country: parts.append(self.incoterm_location.country.name) return ' '.join(parts) @staticmethod def _get_report_bank_account_number(account): if not account: return '' numbers = list(getattr(account, 'numbers', []) or []) for number in numbers: if getattr(number, 'type', None) == 'iban' and getattr(number, 'number', None): return number.number or '' for number in numbers: if getattr(number, 'number', None): return number.number or '' return '' @staticmethod def _get_report_bank_swift(account): bank = getattr(account, 'bank', None) if account else None return getattr(bank, 'bic', None) or '' @staticmethod def _get_report_bank_address(account): bank = getattr(account, 'bank', None) if account else None party = getattr(bank, 'party', None) if bank else None address_get = getattr(party, 'address_get', None) if party else None if callable(address_get): return address_get() return None @staticmethod def _get_report_bank_party_name(account): bank = getattr(account, 'bank', None) if account else None party = getattr(bank, 'party', None) if bank else None address = Sale._get_report_bank_address(account) return ( getattr(address, 'party_full_name', None) or getattr(address, 'party_name', None) or getattr(party, 'rec_name', None) or getattr(bank, 'rec_name', None) or '') @staticmethod def _get_report_bank_address_line(account): address = Sale._get_report_bank_address(account) if not address: return '' city_line = ' '.join(filter(None, [ getattr(address, 'postal_code', None), getattr(address, 'city', None), ])) lines = [ getattr(address, 'name', None), getattr(address, 'street', None), city_line, ] return ' '.join(line.strip() for line in lines if line and line.strip()) @property def report_melya_bank_name(self): return self._get_report_bank_party_name(self.our_bank_account) @property def report_melya_bank_address(self): return self._get_report_bank_address_line(self.our_bank_account) @property def report_melya_bank_iban(self): return self._get_report_bank_account_number(self.our_bank_account) @property def report_melya_bank_swift(self): return self._get_report_bank_swift(self.our_bank_account) @property def report_melya_bank_swift_line(self): if self.report_melya_bank_swift: return 'Swift Code: %s' % self.report_melya_bank_swift return '' @property def report_melya_bank_iban_line(self): if self.report_melya_bank_iban: return 'IBAN : %s' % self.report_melya_bank_iban return '' @property def report_melya_bank_account_line(self): parts = [] if self.report_melya_bank_iban: parts.append('IBAN : %s' % self.report_melya_bank_iban) if self.report_melya_bank_swift: parts.append('Swift Code: %s' % self.report_melya_bank_swift) return ', '.join(parts) @property def report_packing(self): nb_packing = 0 unit = '' lines = self._get_report_lines() if lines: for line in lines: if line.lots: nb_packing += sum([l.lot_qt for l in line.lots if l.lot_type == 'physic']) if len(line.lots)>1: unit = line.lots[1].lot_unit.name return str(int(nb_packing)) + unit @property def report_price(self): line = self._get_report_first_line() if line: return self._format_report_price_words(line) return '' @property def report_price_lines(self): lines = self._get_report_lines() if lines: return '\n'.join(self._format_report_price_line(line) for line in lines) return '' @property def report_commission_invoice_number(self): return self.reference or self.full_number or self.number or '' @property def report_commission_contract_number(self): return self.full_number or self.number or '' def _get_report_commission_fee(self): fallback = None for line in self._get_report_lines(): for fee in getattr(line, 'fees', []) or []: if fallback is None: fallback = fee product = getattr(fee, 'product', None) product_name = ( getattr(product, 'rec_name', None) or getattr(product, 'name', None) or '') if 'commission' in product_name.lower(): return fee return fallback def _get_report_commission_shipment(self): for line in self._get_report_lines(): for lot in getattr(line, 'lots', []) or []: shipment = ( getattr(lot, 'lot_shipment_out', None) or getattr(lot, 'lot_shipment_in', None) or getattr(lot, 'lot_shipment_internal', None)) if shipment: return shipment @property def report_commission_bl_number(self): shipment = self._get_report_commission_shipment() return getattr(shipment, 'bl_number', None) or '' @property def report_commission_bl_date(self): shipment = self._get_report_commission_shipment() return getattr(shipment, 'bl_date', None) @property def report_commission_vessel(self): shipment = self._get_report_commission_shipment() vessel = getattr(shipment, 'vessel', None) if shipment else None return ( getattr(vessel, 'vessel_name', None) or getattr(shipment, 'vessel_name', None) or '') @property def report_commission_quantity_display(self): return self.report_total_quantity @property def report_commission_quantity_unit_upper(self): return self.report_quantity_unit_upper or '' @property def report_commission_lbs_display(self): total = self._get_report_total_weight(0) if total is None: return '' unit = self._get_report_total_unit() lbs_unit = self._find_report_unit('LBS') if not lbs_unit: lbs_unit = self._find_report_unit('LB') if not lbs_unit: return '' return self._format_report_number( self._convert_report_quantity(total, unit, lbs_unit), digits='0.01') @staticmethod def _find_report_unit(name): Uom = Pool().get('product.uom') units = Uom.search([ 'OR', ('name', '=', name), ('symbol', '=', name), ], limit=1) return units[0] if units else None @property def report_commission_base_amount_display(self): return self._format_report_number( self.untaxed_amount or self.total_amount or 0, digits='0.01', strip_trailing_zeros=False) @property def report_commission_currency_name(self): return getattr(self.currency, 'name', None) or getattr( self.currency, 'code', None) or '' @property def report_commission_invoice_line(self): return ' '.join(part for part in [ 'Our invoice N.', self.report_commission_invoice_number, self.report_commission_currency_name, self.report_commission_base_amount_display, ] if part) @property def report_commission_rate_line(self): fee = self._get_report_commission_fee() base = self.report_commission_base_amount_display currency = self.report_commission_currency_name if fee: price = self._format_report_number( getattr(fee, 'price', 0) or 0, digits='0.0000') if getattr(fee, 'mode', None) in {'pprice', 'rate', 'pcost'}: return ' '.join(part for part in [ price, '% on', base, currency, ] if part) fee_currency = getattr(fee, 'currency', None) fee_unit = getattr(fee, 'unit', None) return ' '.join(part for part in [ price, getattr(fee_currency, 'name', None) or getattr( fee_currency, 'code', None) or '', '/', (getattr(fee_unit, 'rec_name', None) or '').upper(), ] if part) return ' '.join(part for part in ['0 % on', base, currency] if part) @property def report_commission_secondary_rate_line(self): fee = self._get_report_commission_fee() if not fee or not getattr(fee, 'enable_linked_currency', False): return '' currency = getattr(fee, 'linked_currency', None) unit = getattr(fee, 'linked_unit', None) return ' '.join(part for part in [ self._format_report_number( getattr(fee, 'linked_price', 0) or 0, digits='0.0000'), getattr(currency, 'name', None) or getattr(currency, 'code', None) or '', '/', (getattr(unit, 'rec_name', None) or '').upper(), ] if part) @property def report_commission_total_display(self): fee = self._get_report_commission_fee() if fee and hasattr(fee, 'get_amount'): amount = fee.get_amount() else: amount = self.total_amount or 0 return self._format_report_number( abs(Decimal(str(amount or 0))), digits='0.01', strip_trailing_zeros=False) @property def report_price_composition_lines(self): return '\n'.join( row['label'] for row in self.report_price_composition_rows) @property def report_price_composition_rows(self): lines = [] for line in self._get_report_lines(): currency = self._get_report_display_currency(line) currency_name = ( currency.rec_name.upper() if currency and currency.rec_name else '') compositions = list(getattr(line, 'price_composition', []) or []) missing = [ composition for composition in compositions if getattr(composition, 'price', None) in (None, '')] quantity = self._get_report_line_quantity(line) known_total = sum( Decimal(str(getattr(composition, 'price', 0) or 0)) * quantity for composition in compositions if getattr(composition, 'price', None) not in (None, '')) residual = None if len(missing) == 1: residual = ( self._get_report_price_composition_line_amount(line) - known_total) for composition in compositions: component = getattr(composition, 'component', '') or '' price = getattr(composition, 'price', None) if price in (None, ''): if composition is missing[0] and residual is not None: price = residual else: continue label = ' '.join( part for part in [ component, 'value in', currency_name, ] if part) if label: amount = Decimal(str(price or 0)) * quantity if missing and composition is missing[0] and residual is not None: amount = residual lines.append({ 'label': label, 'amount': amount, 'amount_text': self._format_report_number( amount, digits='0.01', strip_trailing_zeros=False, use_grouping=True), }) return lines @staticmethod def _get_report_price_composition_line_amount(line): amount = getattr(line, 'amount', None) if amount not in (None, ''): return Decimal(str(amount or 0)) quantity = Decimal(str(getattr(line, 'quantity', 0) or 0)) unit_price = Decimal(str(getattr(line, 'unit_price', 0) or 0)) premium_getter = getattr(line, '_get_premium_price', None) premium = premium_getter() if premium_getter else getattr( line, 'premium', 0) return quantity * (unit_price + Decimal(str(premium or 0))) @property def report_trade_blocks(self): lines = self._get_report_lines() blocks = [] for line in lines: current_quantity = self._get_report_line_quantity(line) quantity = self._format_report_number( current_quantity, keep_trailing_decimal=True) line_unit = self._get_report_line_unit(line) unit = ( line_unit.rec_name.upper() if line_unit and line_unit.rec_name else '' ) words = self._report_quantity_to_words(current_quantity, line_unit) period = line.del_period.description if getattr(line, 'del_period', None) else '' quantity_line = ' '.join( part for part in [ quantity, unit, f"({words})", f"- {period}" if period else '', ] if part) price_line = self._format_report_price_line(line) blocks.append((quantity_line, price_line)) return blocks @property def report_delivery(self): del_date = 'PROMPT' if self.lines: if self.lines[0].estimated_date: delivery_date = [dd.estimated_date for dd in self.lines[0].estimated_date if dd.trigger=='deldate'] if delivery_date: del_date = delivery_date[0] if del_date: del_date = format_date_en(del_date) return del_date @property def report_delivery_period_description(self): line = self._get_report_first_line() if line and line.del_period: return line.del_period.description or '' return '' @property def report_shipment_periods(self): periods = [] for line in self._get_report_lines(): period = line.del_period.description if line.del_period else '' if period and period not in periods: periods.append(period) if periods: return '\n'.join(periods) return '' @property def report_payment_date(self): line = self._get_report_first_line() if line: if self.lc_date: return format_date_en(self.lc_date) Date = Pool().get('ir.date') payment_date = line.sale.payment_term.lines[0].get_date(Date.today(), line) if payment_date: payment_date = format_date_en(payment_date) return payment_date def _get_report_bill_amount(self): invoices = [ invoice for invoice in (self.invoices or []) if getattr(invoice, 'state', None) != 'cancelled' ] if invoices: invoice = sorted( invoices, key=lambda i: ( getattr(i, 'invoice_date', None) or datetime.date.min, getattr(i, 'id', 0)))[0] return Decimal(str(getattr(invoice, 'total_amount', 0) or 0)) return Decimal(str(self.total_amount or 0)) @property def report_bill_amount(self): return self._get_report_bill_amount() @property def report_bill_amount_words(self): value = self._get_report_bill_amount() if self.currency and (self.currency.rec_name or '').upper() == 'USC': return amount_to_currency_words(value, 'USC', 'USC') return amount_to_currency_words(value) @property def report_bill_maturity_date(self): maturity_dates = [] for invoice in (self.invoices or []): if getattr(invoice, 'state', None) == 'cancelled': continue for line in (invoice.lines_to_pay or []): if getattr(line, 'maturity_date', None): maturity_dates.append(line.maturity_date) if maturity_dates: return min(maturity_dates) if self.lc_date: return self.lc_date line = self._get_report_first_line() if line and self.payment_term and self.payment_term.lines: Date = Pool().get('ir.date') return self.payment_term.lines[0].get_date(Date.today(), line) @property def report_shipment(self): if self.lines: if len(self.lines[0].lots)>1: shipment = self.lines[0].lots[1].lot_shipment_in lot = self.lines[0].lots[1].lot_name if shipment: info = '' if shipment.bl_number: info += ' B/L ' + shipment.bl_number if shipment.supplier: info += ' BY ' + shipment.supplier.name if shipment.vessel: info += ' (' + shipment.vessel.vessel_name + ')' if shipment.container and shipment.container[0].container_no: id = 1 for cont in shipment.container: if id == 1: info += ' Container(s)' if cont.container_no: info += ' ' + cont.container_no else: info += ' unnamed' id += 1 # info += ' (LOT ' + lot + ')' if shipment.note: info += ' ' + shipment.note return info else: return '' @classmethod def default_viewer(cls): country_start = "Zobiland" data = { "highlightedCountryName": country_start } return "d3:" + json.dumps(data) @fields.depends('doc_template','required_documents') def on_change_with_required_documents(self): if self.doc_template: return self.doc_template.type def get_viewer(self, name=None): country_start = '' dep_name = '' arr_name = '' departure = '' arrival = '' if self.party and self.party.addresses: if self.party.addresses[0].country: country_start = self.party.addresses[0].country.name if self.from_location: lat_from = self.from_location.lat lon_from = self.from_location.lon dep_name = self.from_location.name departure = { "name":dep_name,"lat": str(lat_from), "lon": str(lon_from) } if self.to_location: lat_to = self.to_location.lat lon_to = self.to_location.lon arr_name = self.to_location.name arrival = { "name":arr_name,"lat": str(lat_to), "lon": str(lon_to) } data = { "highlightedCountryNames": [{"name":country_start}], "routePoints": [ { "lon": -46.3, "lat": -23.9 }, { "lon": -30.0, "lat": -20.0 }, { "lon": -30.0, "lat": 0.0 }, { "lon": -6.0, "lat": 35.9 }, { "lon": 15.0, "lat": 38.0 }, { "lon": 29.0, "lat": 41.0 } ], "boats": [ # {"name": "CARIBBEAN 1", # "imo": "1234567", # "lon": -30.0, # "lat": 0.0, # "status": "En route", # "links": [ # { "text": "Voir sur VesselFinder", "url": "https://www.vesselfinder.com" }, # { "text": "Détails techniques", "url": "https://example.com/tech" } # ], # "actions": [ # { "type": "track", "id": "123", "label": "Suivre ce bateau" }, # { "type": "details", "id": "123", "label": "Voir détails" } # ]} ], "cottonStocks": [ # { "name":"Mali","lat": 12.65, "lon": -8.0, "amount": 300 }, # { "name":"Egypte","lat": 30.05, "lon": 31.25, "amount": 500 }, # { "name":"Irak","lat": 33.0, "lon": 44.0, "amount": 150 } ], "departures": [departure], "arrivals": [arrival] } return "d3:" + json.dumps(data) @fields.depends('party','from_location','to_location') def on_change_with_viewer(self): return self.get_viewer() def _execution_summary_quantity(self, value): if value is None: return 0.0 try: return float(value) except (TypeError, ValueError): return 0.0 def get_execution_summary_viewer(self, name=None): LotQt = Pool().get('lot.qt') rows = [] totals = defaultdict(float) for line in (self.lines or []): if getattr(line, 'type', 'line') != 'line': continue quantity = self._execution_summary_quantity( getattr(line, 'quantity_theorical', None) or getattr(line, 'quantity', None)) matched = 0.0 shipped = 0.0 lots_count = 0 shipped_routes = [] line_lots = list(getattr(line, 'lots', None) or []) lot_ids = [lot.id for lot in line_lots if getattr(lot, 'id', None)] lot_qts = LotQt.search([('lot_s', 'in', lot_ids)]) if lot_ids else [] for lqt in lot_qts: lots_count += 1 lot_quantity = self._execution_summary_quantity( getattr(lqt, 'lot_quantity', None) or getattr(lqt, 'lot_qt', None)) if getattr(lqt, 'lot_p', None): matched += lot_quantity shipment = ( getattr(lqt, 'lot_shipment_in', None) or getattr(lqt, 'lot_shipment_internal', None) or getattr(lqt, 'lot_shipment_out', None)) if shipment: shipped += lot_quantity route_from = getattr( getattr(shipment, 'from_location', None), 'rec_name', None) route_to = getattr( getattr(shipment, 'to_location', None), 'rec_name', None) route_from = route_from or getattr( getattr(self, 'from_location', None), 'rec_name', None) route_to = route_to or getattr( getattr(self, 'to_location', None), 'rec_name', None) if route_from or route_to: shipped_routes.append( '%s > %s' % (route_from or '-', route_to or '-')) if not lot_qts: for lot in line_lots: lots_count += 1 lot_quantity = self._execution_summary_quantity( getattr(lot, 'lot_qt', None)) if getattr(lot, 'lot_p', None) or getattr(lot, 'line', None): matched += lot_quantity shipment = ( getattr(lot, 'lot_shipment_in', None) or getattr(lot, 'lot_shipment_internal', None) or getattr(lot, 'lot_shipment_out', None)) if shipment: shipped += lot_quantity route_from = getattr( getattr(shipment, 'from_location', None), 'rec_name', None) route_to = getattr( getattr(shipment, 'to_location', None), 'rec_name', None) route_from = route_from or getattr( getattr(self, 'from_location', None), 'rec_name', None) route_to = route_to or getattr( getattr(self, 'to_location', None), 'rec_name', None) if route_from or route_to: shipped_routes.append( '%s > %s' % (route_from or '-', route_to or '-')) unit = getattr(getattr(line, 'unit', None), 'rec_name', '') or '' product = getattr(getattr(line, 'product', None), 'rec_name', '') or '' route = (shipped_routes[0] if shipped_routes else '%s > %s' % ( getattr(getattr(self, 'from_location', None), 'rec_name', None) or '-', getattr(getattr(self, 'to_location', None), 'rec_name', None) or '-')) rows.append({ 'product': product, 'quantity': quantity, 'matched': matched, 'shipped': shipped, 'open': max(quantity - shipped, 0.0), 'unit': unit, 'lots': lots_count, 'route': route, }) totals['quantity'] += quantity totals['matched'] += matched totals['shipped'] += shipped totals['open'] += max(quantity - shipped, 0.0) totals['lines'] += 1 data = { 'type': 'execution-summary', 'unit': rows[0]['unit'] if rows else '', 'totals': dict(totals), 'rows': rows[:4], } return "d3:" + json.dumps(data) def getLots(self): if self.lines: if self.lines.lots: return [l for l in self.lines.lots] @classmethod def _check_lines_delivery_period_values(cls, values): line_commands = values.get('lines') or [] if not line_commands: return Line = Pool().get('sale.line') pending_values = {} for command in line_commands: action = command[0] if action == 'create': for line_values in command[1]: Line._check_delivery_period_values([Line()], line_values) elif action == 'write': actions = iter(command[1:]) for line_ids, line_values in zip(actions, actions): for line_id in line_ids: pending_values.setdefault(line_id, {}).update( line_values) elif action == 'add': for line_id in command[1]: pending_values.setdefault(line_id, {}) for line_id, line_values in pending_values.items(): Line._check_delivery_period_values( Line.browse([line_id]), line_values) @classmethod def write(cls, *args): actions = iter(args) args = [] for sales, values in zip(actions, actions): cls._check_lines_delivery_period_values(values) args.extend((sales, values)) super().write(*args) @classmethod def validate(cls, sales): super(Sale, cls).validate(sales) Line = Pool().get('sale.line') Date = Pool().get('ir.date') for sale in sales: for line in sale.lines: if not line.quantity_theorical and line.quantity > 0: line.quantity_theorical = Decimal(str(line.quantity)).quantize(Decimal("0.00001")) Line.save([line]) if line.lots: line_p = line.get_matched_lines()#line.lots[0].line if line_p: for l in line_p: #compute pnl Pnl = Pool().get('valuation.valuation') Pnl.generate(l.lot_p.line) # if line.quantity_theorical: # OpenPosition = Pool().get('open.position') # OpenPosition.create_from_sale_line(line) if line.price_type == 'basis' and line.lots: #line.price_pricing and line.price_components and previous_linked_price = line.linked_price line.sync_linked_price_from_basis() unit_price = line.get_basis_price() if unit_price != line.unit_price or line.linked_price != previous_linked_price: Line = Pool().get('sale.line') line.unit_price = unit_price Line.save([line]) if line.price_type == 'efp': if line.derivatives: for d in line.derivatives: line.unit_price = d.price_index.get_price(Date.today(),line.unit,line.currency,True) Line.save([line]) def _contract_clause_values_from_template(self, direction): Selection = Pool().get('contract.clause.selection') values = [] template = getattr(self, 'contract_template', None) if not template: return values for template_line in sorted( template.lines or [], key=lambda line: line.sequence or 0): clause = template_line.clause if not clause or not clause.matches_contract(self, direction): continue values.append(Selection( sequence=template_line.sequence, section=template_line.section or clause.category, clause=clause, selected=not template_line.optional, optional=template_line.optional, )) return values @fields.depends('contract_template', 'contract_clauses', 'incoterm', 'payment_term', 'lines') def on_change_contract_template(self): if self.contract_template: self.contract_clauses = self._contract_clause_values_from_template( 'sale') class PriceComposition(metaclass=PoolMeta): __name__ = 'price.composition' sale_line = fields.Many2One('sale.line',"Sale line") class ContractClauseSelection(metaclass=PoolMeta): __name__ = 'contract.clause.selection' sale = fields.Many2One('sale.sale', "Sale", ondelete='CASCADE') class CharterCondition(metaclass=PoolMeta): __name__ = 'charter.condition' sale = fields.Many2One( 'sale.sale', "Sale", ondelete='CASCADE') sale_line = fields.Many2One( 'sale.line', "Sale Line", ondelete='CASCADE') class PremiumComposition(metaclass=PoolMeta): __name__ = 'premium.composition' sale_line = fields.Many2One('sale.line', "Sale line", ondelete='CASCADE') class SaleLine(metaclass=PoolMeta): __name__ = 'sale.line' @classmethod def __setup__(cls): super().__setup__() cls.quantity.readonly = True @classmethod def default_unit(cls): if is_itsa_company(): unit = default_itsa_unit() if unit: return unit default = getattr(super(), 'default_unit', None) if default: return default() @fields.depends('product', 'unit', 'sale', '_parent_sale.company') def on_change_product(self): previous_unit = self.unit parent_on_change = getattr(super(), 'on_change_product', None) if parent_on_change: parent_on_change() if (self.sale and is_itsa_company(self.sale.company) and (not previous_unit or record_id(previous_unit) == default_itsa_unit())): unit = default_itsa_unit() if unit: self.unit = unit @staticmethod def _is_empty_quantity(quantity): if quantity in (None, ''): return True return Decimal(str(quantity or 0)) == Decimal(0) @staticmethod def _has_physical_lot(line): return any( getattr(lot, 'lot_type', None) == 'physic' for lot in (getattr(line, 'lots', None) or [])) @classmethod def _sync_initial_quantity_from_theorical(cls, line): if cls._has_physical_lot(line): return False if not cls._is_empty_quantity(getattr(line, 'quantity', None)): return False quantity = getattr(line, 'quantity_theorical', None) if cls._is_empty_quantity(quantity): return False line.quantity = Decimal(str(quantity)).quantize(Decimal("0.00001")) return True @classmethod def _sync_quantity_counter_from_theorical(cls, line): if cls._has_physical_lot(line): return False quantity = getattr(line, 'quantity_theorical', None) if cls._is_empty_quantity(quantity): quantity = Decimal(0) else: quantity = Decimal(str(quantity)).quantize(Decimal("0.00001")) current = getattr(line, 'quantity', None) try: current = Decimal(str(current or 0)).quantize(Decimal("0.00001")) except Exception: current = Decimal(0) if current == quantity: return False line.quantity = quantity return True @classmethod def _set_initial_quantity_values(cls, values): if 'quantity_theorical' not in values: return quantity = values.get('quantity_theorical') if cls._is_empty_quantity(quantity): return quantity = Decimal(str(quantity)).quantize(Decimal("0.00001")) if cls._is_empty_quantity(values.get('quantity')): values['quantity'] = quantity if cls._is_empty_quantity(values.get('targeted_qt')): values['targeted_qt'] = quantity @classmethod def _set_quantity_counter_values_from_theorical(cls, records, values): if 'quantity_theorical' not in values: return if any(cls._has_physical_lot(record) for record in records): return quantity = values.get('quantity_theorical') if cls._is_empty_quantity(quantity): quantity = Decimal(0) else: quantity = Decimal(str(quantity)).quantize(Decimal("0.00001")) values['quantity'] = quantity if 'targeted_qt' not in values: values['targeted_qt'] = quantity @classmethod def _fresh_lines_for_quantity_consistency(cls, lines): try: return [cls(line.id) for line in lines if getattr(line, 'id', None)] except Exception: return lines def get_invoice_line(self, lots=None, action=None): lines = super().get_invoice_line(lots, action) if action != 'prov' or not lots: return lines padding_by_lot = { lot.id: Decimal(str(getattr(lot, 'sale_invoice_padding', 0) or 0)) for lot in lots} for invoice_line in lines: if (getattr(invoice_line, 'invoice_type', None) != 'out' or getattr(invoice_line, 'description', None) != 'Pro forma' or Decimal(str(getattr(invoice_line, 'quantity', 0) or 0)) <= 0): continue lot = getattr(invoice_line, 'lot', None) lot_id = getattr(lot, 'id', lot) padding = padding_by_lot.get(lot_id, Decimal(0)) if not padding: continue quantity = Decimal(str(invoice_line.quantity or 0)) + padding if invoice_line.unit: quantity = Decimal(str(invoice_line.unit.round(float(quantity)))) invoice_line.quantity = quantity return lines @classmethod def default_pricing_rule(cls): try: Configuration = Pool().get('purchase_trade.configuration') except KeyError: return '' configurations = Configuration.search([], limit=1) if configurations: return configurations[0].pricing_rule or '' return '' @staticmethod def _has_invalid_delivery_period(line): return ( bool(line.from_del) and bool(line.to_del) and line.from_del > line.to_del) @classmethod def _check_delivery_period_values(cls, lines, values=None): values = values or {} for line in lines: from_del = values.get('from_del', getattr(line, 'from_del', None)) to_del = values.get('to_del', getattr(line, 'to_del', None)) if from_del and to_del and from_del > to_del: raise UserError( "Shipment period From date must be before To date.") @classmethod def _estimated_bl_relation_field(cls): return 'sale_line' @classmethod def _has_estimated_bl_date(cls, estimated_dates): return any( getattr(estimated, 'trigger', None) == 'bldate' for estimated in (estimated_dates or [])) @classmethod def _values_have_estimated_bl_date(cls, values): for command in values.get('estimated_date') or []: action = command[0] if action == 'create': if any( estimated.get('trigger') == 'bldate' for estimated in command[1]): return True elif action == 'write': actions = iter(command[1:]) for _estimated_ids, estimated_values in zip(actions, actions): if estimated_values.get('trigger') == 'bldate': return True return False @classmethod def _delivery_period_from_values(cls, values): period = values.get('del_period') if period and not hasattr(period, 'beg_date'): period = Pool().get('product.month')(period) return period @classmethod def _default_estimated_bl_date(cls, values): period = cls._delivery_period_from_values(values) return getattr(period, 'beg_date', None) @classmethod def _set_default_estimated_bl_date_values(cls, values): if cls._values_have_estimated_bl_date(values): return estimated_date = cls._default_estimated_bl_date(values) if not estimated_date: return if values.get('estimated_date') is None: values['estimated_date'] = [] elif isinstance(values['estimated_date'], tuple): values['estimated_date'] = list(values['estimated_date']) values['estimated_date'].append(('create', [{ 'trigger': 'bldate', 'estimated_date': estimated_date, }])) @classmethod def _create_missing_estimated_bl_dates(cls, lines): Estimated = Pool().get('pricing.estimated') values = [] relation_field = cls._estimated_bl_relation_field() for line in lines: if cls._has_estimated_bl_date(getattr(line, 'estimated_date', None)): continue del_period = getattr(line, 'del_period', None) estimated_date = getattr(del_period, 'beg_date', None) if not estimated_date: continue values.append({ relation_field: line.id, 'trigger': 'bldate', 'estimated_date': estimated_date, }) if values: Estimated.create(values) @classmethod def create(cls, vlist): regenerate_valuation = any( cls._should_regenerate_valuation(values) for values in vlist) for values in vlist: cls._check_delivery_period_values([cls()], values) cls._set_default_estimated_bl_date_values(values) cls._set_initial_quantity_values(values) lines = super().create(vlist) if not Transaction().context.get('_purchase_trade_skip_fee_rules'): Pool().get('fee.rule').apply_to_lines( lines, 'sale_line', auto_only=True) if (regenerate_valuation and not Transaction().context.get( '_purchase_trade_skip_valuation_regeneration')): Valuation = Pool().get('valuation.valuation') with Transaction().set_context( _purchase_trade_skip_valuation_regeneration=True): Valuation.regenerate_for_sale_lines(lines) return lines @classmethod @ModelView.button def apply_default_fees(cls, lines): Pool().get('fee.rule').apply_to_lines(lines, 'sale_line') def _check_delivery_period(self): if self._has_invalid_delivery_period(self): raise UserError( "Shipment period From date must be before To date.") @fields.depends('from_del', 'to_del') def on_change_from_del(self): self._check_delivery_period() @fields.depends('from_del', 'to_del') def on_change_to_del(self): self._check_delivery_period() del_period = fields.Many2One('product.month',"Shipment Period") lots = fields.One2Many('lot.lot','sale_line',"Lots",readonly=True) fees = fields.One2Many('fee.fee', 'sale_line', 'Fees') quantity_theorical = fields.Numeric("Th. quantity", digits='unit', readonly=False) price_type = fields.Selection([ ('cash', 'Cash Price'), ('priced', 'Priced'), ('basis', 'Basis'), ('efp', 'EFP'), ], 'Price type') progress = fields.Function(fields.Float("Fix. progress", states={ 'invisible': Eval('price_type') != 'basis', }),'get_progress') from_del = fields.Date("From") to_del = fields.Date("To") period_at = fields.Selection([ (None, ''), ('laycan', 'Laycan'), ('loading', 'Loading'), ('discharge', 'Discharge'), ('crossing_border', 'Crossing Border'), ('title_transfer', 'Title transfer'), ('arrival', 'Arrival'), ],"Period at") concentration = fields.Numeric("Concentration") price_components = fields.One2Many('pricing.component','sale_line',"Components") mtm = fields.Many2Many('sale.strategy', 'sale_line', 'strategy', 'Mtm Strategy') derivatives = fields.One2Many('derivative.derivative','sale_line',"Derivatives") price_pricing = fields.One2Many('pricing.pricing','sale_line',"Pricing") price_summary = fields.One2Many('sale.pricing.summary','sale_line',"Summary") premium_decomposition = fields.One2Many( 'premium.composition', 'sale_line', "Premium decomposition") charter_conditions = fields.One2Many( 'charter.condition', 'sale_line', "Charter Conditions") estimated_date = fields.One2Many('pricing.estimated','sale_line',"Estimated date") tol_min = fields.Numeric("Tol - in %",states={ 'readonly': (Eval('inherit_tol')), }) tol_max = fields.Numeric("Tol + in %",states={ 'readonly': (Eval('inherit_tol')), }) tol_min_qt = fields.Numeric("Tol -",states={ 'readonly': (Eval('inherit_tol')), }) tol_max_qt = fields.Numeric("Tol +",states={ 'readonly': (Eval('inherit_tol')), }) inherit_tol = fields.Boolean("Inherit tolerance") tol_min_v = fields.Function(fields.Numeric("Qt min"),'get_tol_min') tol_max_v = fields.Function(fields.Numeric("Qt max"),'get_tol_max') tolerance_used = fields.Function( fields.Numeric("Tolerance used"), 'get_tolerance_used') tolerance_min = fields.Function( fields.Numeric("Tolerance min"), 'get_tolerance_min') tolerance_max = fields.Function( fields.Numeric("Tolerance max"), 'get_tolerance_max') targeted_qt = fields.Numeric("Targeted Qt", digits='unit') certif = fields.Many2One('purchase.certification',"Certification",states={'readonly': (Eval('inherit_cer')),}) # certification = fields.Selection([ # (None, ''), # ('bci', 'BCI'), # ],"Certification",states={'readonly': (Eval('inherit_cer')),}) inherit_cer = fields.Boolean("Inherit certification") enable_linked_currency = fields.Boolean("Linked currencies") linked_price = fields.Numeric("Price", digits='unit',states={ 'invisible': (~Eval('enable_linked_currency')), 'required': Eval('enable_linked_currency'), 'readonly': Eval('price_type') == 'basis', }, depends=['enable_linked_currency', 'price_type']) linked_currency = fields.Many2One('currency.linked',"Currency",states={ 'invisible': (~Eval('enable_linked_currency')), 'required': Eval('enable_linked_currency'), }, depends=['enable_linked_currency']) linked_unit = fields.Many2One('product.uom', 'Unit',states={ 'invisible': (~Eval('enable_linked_currency')), 'required': Eval('enable_linked_currency'), }, depends=['enable_linked_currency']) premium = fields.Function( fields.Numeric("Premium/Discount", digits='unit', readonly=True), 'get_premium', setter='set_premium') fee_ = fields.Many2One('fee.fee',"Fee") attributes = fields.Dict( 'product.attribute', 'Attributes', domain=[ ('sets', '=', Eval('attribute_set')), ], states={ 'readonly': ~Eval('attribute_set'), }, depends=['product', 'attribute_set'], help="Add attributes to the variant." ) attribute_set = fields.Function( fields.Many2One('product.attribute.set', "Attribute Set"), 'on_change_with_attribute_set' ) attributes_name = fields.Function( fields.Char("Attributes Name"), 'on_change_with_attributes_name' ) finished = fields.Boolean("Mark as finished") pricing_rule = fields.Text("Pricing description") price_composition = fields.One2Many('price.composition','sale_line',"Price composition") parent_trader = fields.Function( fields.Many2One('party.party', "Trader"), 'get_parent_field') parent_operator = fields.Function( fields.Many2One('party.party', "Operator"), 'get_parent_field') parent_broker = fields.Function( fields.Many2One('party.party', "Broker"), 'get_parent_field') parent_contract_template = fields.Function( fields.Many2One('contract.template', "Contract Template"), 'get_parent_field') parent_invoice_party = fields.Function( fields.Many2One('party.party', "Invoice Party"), 'get_parent_field') parent_shipment_party = fields.Function( fields.Many2One('party.party', "Shipment Party"), 'get_parent_field') parent_our_reference = fields.Function( fields.Char("Our Reference"), 'get_parent_field') parent_lc_date = fields.Function( fields.Date("LC date"), 'get_parent_field') parent_product_origin = fields.Function( fields.Char("Origin"), 'get_parent_field') price_component_count = fields.Function( fields.Integer("Components"), 'get_relation_count') pricing_count = fields.Function( fields.Integer("Pricing"), 'get_relation_count') price_summary_count = fields.Function( fields.Integer("Summary"), 'get_relation_count') price_composition_count = fields.Function( fields.Integer("Price composition"), 'get_relation_count') premium_decomposition_count = fields.Function( fields.Integer("Premium lines"), 'get_relation_count') mtm_count = fields.Function( fields.Integer("MTM"), 'get_relation_count') fee_count = fields.Function( fields.Integer("Fees"), 'get_relation_count') lot_count = fields.Function( fields.Integer("Lots"), 'get_relation_count') derivative_count = fields.Function( fields.Integer("Derivatives"), 'get_relation_count') estimated_date_count = fields.Function( fields.Integer("Estimated dates"), 'get_relation_count') charter_condition_count = fields.Function( fields.Integer("Charter conditions"), 'get_relation_count') _parent_field_map = { 'parent_trader': 'trader', 'parent_operator': 'operator', 'parent_broker': 'broker', 'parent_contract_template': 'contract_template', 'parent_invoice_party': 'invoice_party', 'parent_shipment_party': 'shipment_party', 'parent_our_reference': 'our_reference', 'parent_lc_date': 'lc_date', 'parent_product_origin': 'product_origin', } _relation_count_map = { 'price_component_count': 'price_components', 'pricing_count': 'price_pricing', 'price_summary_count': 'price_summary', 'price_composition_count': 'price_composition', 'premium_decomposition_count': 'premium_decomposition', 'mtm_count': 'mtm', 'fee_count': 'fees', 'lot_count': 'lots', 'derivative_count': 'derivatives', 'estimated_date_count': 'estimated_date', 'charter_condition_count': 'charter_conditions', } def get_parent_field(self, name=None): field_name = self._parent_field_map.get(name) if not field_name or not self.sale: return None value = getattr(self.sale, field_name, None) if hasattr(value, 'id'): return value.id return value def get_relation_count(self, name=None): field_name = self._relation_count_map.get(name) if not field_name: return 0 return len(getattr(self, field_name, None) or []) @classmethod def default_finished(cls): return False @property def report_fixing_rule(self): pricing_rule = '' if self.pricing_rule: pricing_rule = self.pricing_rule return pricing_rule @property def get_pricing_text(self): parts = [] if self.price_components: for pc in self.price_components: if pc.price_index: price_desc = pc.price_index.price_desc or '' period_desc = ( pc.price_index.price_period.description if pc.price_index.price_period else '') or '' part = ' '.join( piece for piece in ['ON', price_desc, period_desc] if piece) if part: parts.append(part) return ' '.join(parts) @fields.depends('product') def on_change_with_attribute_set(self, name=None): if self.product and self.product.template and self.product.template.attribute_set: return self.product.template.attribute_set.id @fields.depends( 'product', 'attributes', 'coffee_origin', 'coffee_type', 'coffee_process', 'coffee_variety', 'coffee_crop_year', 'coffee_screen_size', 'coffee_moisture_max', 'coffee_defect_max', 'coffee_cup_score_min') def on_change_with_attributes_name(self, name=None): coffee_attributes_name = getattr( self, '_coffee_attributes_name', lambda: None)() if coffee_attributes_name: return coffee_attributes_name if not self.product or not self.product.attribute_set or not self.attributes: return def key(attribute): return getattr(attribute, 'sequence', attribute.name) values = [] for attribute in sorted(self.product.attribute_set.attributes, key=key): if attribute.name in self.attributes: value = self.attributes[attribute.name] values.append(gettext( 'product_attribute.msg_label_value', label=attribute.string, value=attribute.format(value) )) return " | ".join(filter(None, values)) @fields.depends( 'quantity_theorical', 'quantity', 'lots', 'targeted_qt', methods=['_recompute_trade_price_fields']) def on_change_quantity_theorical(self): if self._sync_quantity_counter_from_theorical(self): self._recompute_trade_price_fields() self.targeted_qt = self._tolerance_theoretical_quantity() @classmethod def default_price_type(cls): return 'priced' @classmethod def default_inherit_tol(cls): return True @classmethod def default_enable_linked_currency(cls): return False @classmethod def default_inherit_cer(cls): return True def get_matched_lines(self): if self.lots: LotQt = Pool().get('lot.qt') return LotQt.search([('lot_s','=',self.lots[0].id),('lot_p','>',0)]) def get_date(self,trigger_event): if trigger_event == 'bldate': real_bl_date = self._get_real_bl_date() if real_bl_date: return real_bl_date trigger_date = None if self.estimated_date: logger.info("ESTIMATED_DATE:%s",self.estimated_date) trigger_date = [d.estimated_date for d in self.estimated_date if d.trigger == trigger_event] logger.info("TRIGGER_DATE:%s",trigger_date) logger.info("TRIGGER_EVENT:%s",trigger_event) trigger_date = trigger_date[0] if trigger_date else None return trigger_date def _get_real_bl_date(self): bl_dates = [] for lot in self.lots or []: shipment = getattr(lot, 'lot_shipment_in', None) bl_date = getattr(shipment, 'bl_date', None) if bl_date: bl_dates.append(bl_date) if not bl_dates: for match in self.get_matched_lines() or []: shipment = getattr(match, 'lot_shipment_in', None) bl_date = getattr(shipment, 'bl_date', None) if bl_date: bl_dates.append(bl_date) # Multiple BL dates on one line should not happen; use the first one # for now and revisit when partial shipments need split maturity/pricing. return bl_dates[0] if bl_dates else None def get_tol_min(self,name): if self.inherit_tol: if self.sale.tol_min and self.quantity_theorical: return round((1-(self.sale.tol_min/100))*Decimal(self.quantity_theorical),3) else: if self.tol_min and self.quantity_theorical: return round((1-(self.tol_min/100))*Decimal(self.quantity_theorical),3) def get_tol_max(self,name): if self.inherit_tol: if self.sale.tol_max and self.quantity_theorical: return round((1+(self.sale.tol_max/100))*Decimal(self.quantity_theorical),3) else: if self.tol_max and self.quantity_theorical: return round((1+(self.tol_max/100))*Decimal(self.quantity_theorical),3) def _line_quantity_in_unit(self, quantity, from_unit, to_unit): if not from_unit or not to_unit or from_unit == to_unit: return abs(Decimal(str(quantity or 0))) Uom = Pool().get('product.uom') factor = None rate = None if from_unit.category.id != to_unit.category.id: factor = 1 rate = 1 return abs(Decimal(str(Uom.compute_qty( from_unit, float(quantity or 0), to_unit, True, factor, rate)))) def _tolerance_theoretical_quantity(self): return abs(Decimal(str( getattr(self, 'quantity_theorical', None) or getattr(self, 'quantity', None) or 0))) def _tolerance_actual_quantity(self): LotQt = Pool().get('lot.qt') unit = getattr(self, 'unit', None) physical_lots = [ lot for lot in getattr(self, 'lots', None) or [] if getattr(lot, 'lot_type', None) == 'physic'] if physical_lots: return sum( abs(Decimal(str( lot.get_current_quantity_converted(unit=unit) or 0))) for lot in physical_lots) quantity = Decimal(0) for lot in getattr(self, 'lots', None) or []: if getattr(lot, 'lot_type', None) != 'virtual': continue for lqt in LotQt.search([('lot_s', '=', lot.id)]): quantity += self._line_quantity_in_unit( getattr(lqt, 'lot_quantity', 0), getattr(lqt, 'lot_unit', None), unit) return quantity def _tolerance_targeted_quantity(self): targeted = getattr(self, 'targeted_qt', None) if not self._is_empty_quantity(targeted): return abs(Decimal(str(targeted))) return self._tolerance_theoretical_quantity() @fields.depends('targeted_qt', 'quantity_theorical', 'quantity') def get_tolerance_used(self, name): theoretical = self._tolerance_theoretical_quantity() if not theoretical: return Decimal(0) return round( ((self._tolerance_targeted_quantity() - theoretical) / theoretical) * Decimal(100), 5) def get_tolerance_min(self, name): sale = getattr(self, 'sale', None) if self.inherit_tol and sale: return -Decimal(str(sale.tol_min or 0)) return -Decimal(str(self.tol_min or 0)) def get_tolerance_max(self, name): sale = getattr(self, 'sale', None) if self.inherit_tol and sale: return Decimal(str(sale.tol_max or 0)) return Decimal(str(self.tol_max or 0)) def check_targeted_qt_tolerance(self): theoretical = self._tolerance_theoretical_quantity() if not theoretical: return if self._is_empty_quantity(getattr(self, 'targeted_qt', None)): self.targeted_qt = theoretical targeted = self._tolerance_targeted_quantity() tol_min = -self.get_tolerance_min('tolerance_min') tol_max = self.get_tolerance_max('tolerance_max') min_qt = theoretical * (Decimal(1) - tol_min / Decimal(100)) max_qt = theoretical * (Decimal(1) + tol_max / Decimal(100)) if targeted < min_qt: raise UserError( "Targeted Qt is below the tolerance minimum.") if targeted > max_qt: raise UserError( "Targeted Qt exceeds the tolerance maximum.") def get_progress(self,name): PS = Pool().get('sale.pricing.summary') ps = PS.search(['sale_line','=',self.id]) if ps: if not self.price_components: manual = [e for e in ps if not e.price_component] if manual: return manual[0].progress or 0 return sum((e.progress if e.progress else 0) * (e.ratio if e.ratio else 0) / 100 for e in ps) def getVirtualLot(self): if self.lots: return [l for l in self.lots if l.lot_type=='virtual'][0] def _get_linked_unit_factor(self): if not (self.enable_linked_currency and self.linked_currency): return None factor = Decimal(self.linked_currency.factor or 0) if not factor: return None unit_factor = Decimal(1) if self.linked_unit: source_unit = getattr(self, 'unit', None) product = getattr(self, 'product', None) if not source_unit and product: source_unit = product.sale_uom if not source_unit: return factor Uom = Pool().get('product.uom') unit_factor = Decimal(str( Uom.compute_qty(source_unit, float(1), self.linked_unit) or 0)) return factor * unit_factor def _linked_to_line_price(self, price): factor = self._get_linked_unit_factor() price = Decimal(price or 0) if not factor: return price return round(price * factor, 4) def _line_to_linked_price(self, price): factor = self._get_linked_unit_factor() price = Decimal(price or 0) if not factor: return price return round(price / factor, 4) def get_premium(self, name=None): return round(sum( Decimal(str(getattr(premium, 'base_amount', None) if getattr(premium, 'base_amount', None) is not None else premium.get_base_amount('base_amount') or 0)) for premium in (self.premium_decomposition or [])), 4) def get_effective_charter_conditions(self): if self.charter_conditions: return list(self.charter_conditions) sale = getattr(self, 'sale', None) return list(getattr(sale, 'charter_conditions', None) or []) @classmethod def set_premium(cls, lines, name, value): Premium = Pool().get('premium.composition') value = Decimal(str(value or 0)) for line in lines: existing = Premium.search([('sale_line', '=', line.id)]) if existing: Premium.delete(existing) if value: Premium.create([{ 'sale_line': line.id, 'premium': value, 'currency': ( line.sale.currency.id if line.sale and line.sale.currency else None), }]) def _get_premium_price(self): premium = getattr(self, 'premium', None) if premium is None: premium = self.get_premium('premium') return Decimal(str(premium or 0)) def _get_amount_quantity(self): if getattr(self, 'finished', False): weight_basis_quantity = self._get_weight_basis_quantity() if weight_basis_quantity is not None: return weight_basis_quantity quantity = getattr(self, 'quantity', None) if quantity is None: quantity = getattr(self, 'quantity_theorical', None) return quantity quantity = getattr(self, 'quantity_theorical', None) if quantity is None: quantity = getattr(self, 'quantity', None) return quantity def _get_weight_basis_quantity(self): physical_lots = [ lot for lot in (getattr(self, 'lots', None) or []) if getattr(lot, 'lot_type', None) == 'physic'] if not physical_lots: return None wb = getattr(getattr(self, 'sale', None), 'wb', None) qt_type = getattr(wb, 'qt_type', None) if not qt_type: return None state_id = getattr(qt_type, 'id', qt_type) quantity = Decimal(0) for lot in physical_lots: hist_types = [ getattr(hist, 'quantity_type', None) for hist in (getattr(lot, 'lot_hist', None) or [])] hist_type_ids = [getattr(hist_type, 'id', hist_type) for hist_type in hist_types] if qt_type not in hist_types and state_id not in hist_type_ids: return None quantity += Decimal(str( lot.get_current_quantity_converted(state_id, self.unit) or 0)) return quantity def get_price(self,lot_premium=0): return round( Decimal(self.unit_price or 0) + Decimal(lot_premium or 0), 4) def _get_basis_component_price(self): price = Decimal(0) if not self.price_components: PP = Pool().get('sale.pricing.summary') pp = PP.search([ ('sale_line', '=', self.id), ('price_component', '=', None), ], limit=1) if pp: return round(Decimal(pp[0].price or 0), 4) for pc in self.price_components: PP = Pool().get('sale.pricing.summary') pp = PP.search([('price_component','=',pc.id),('sale_line','=',self.id)]) if pp: price += pp[0].price * (pc.ratio / 100) return round(price,4) def get_basis_price(self): return round(self._get_basis_component_price(), 4) def sync_linked_price_from_basis(self): if self.enable_linked_currency and self.linked_currency: self.linked_price = self._line_to_linked_price( self._get_basis_component_price()) def get_price_linked_currency(self,lot_premium=0): return round( self._linked_to_line_price( Decimal(self.linked_price or 0) + Decimal(lot_premium or 0)), 4) @fields.depends('product', 'quantity', 'unit_price', methods=['_get_context_sale_price']) def compute_unit_price(self): unit_price = super().compute_unit_price() if unit_price is None: unit_price = self.unit_price return unit_price @fields.depends('id','unit','quantity','unit_price','price_pricing','price_type','price_components','estimated_date','lots','fees','enable_linked_currency','linked_price','linked_currency','linked_unit','premium_decomposition') def on_change_with_unit_price(self, name=None): Date = Pool().get('ir.date') logger.info("ONCHANGEUNITPRICE:%s",self.unit_price) if self.price_type == 'basis': self.sync_linked_price_from_basis() logger.info("ONCHANGEUNITPRICE_IN:%s",self.get_basis_price()) return self.get_basis_price() if self.enable_linked_currency and self.linked_price and self.linked_currency and self.price_type == 'priced': return self.get_price_linked_currency() if self.price_type == 'efp': if hasattr(self, 'derivatives') and self.derivatives: for d in self.derivatives: return d.price_index.get_price(Date.today(),self.unit,self.sale.currency,True) return self.get_price() @fields.depends( 'type', 'quantity', 'quantity_theorical', 'finished', 'unit_price', 'unit', 'product', 'lots', 'sale', '_parent_sale.currency', '_parent_sale.wb', 'premium_decomposition', 'enable_linked_currency', 'linked_currency', 'linked_unit') def on_change_with_amount(self): if self.type == 'line': currency = self.sale.currency if self.sale else None amount = Decimal(str(self._get_amount_quantity() or 0)) * ( Decimal(self.unit_price or 0) + self._get_premium_price()) if currency: return currency.round(amount) return amount return Decimal(0) @fields.depends( 'unit', 'product', 'price_type', 'enable_linked_currency', 'linked_currency', 'linked_unit', 'linked_price', 'premium_decomposition', methods=['on_change_with_unit_price', 'on_change_with_amount']) def _recompute_trade_price_fields(self): self.unit_price = self.on_change_with_unit_price() self.amount = self.on_change_with_amount() @fields.depends(methods=['_recompute_trade_price_fields']) def on_change_premium(self): self._recompute_trade_price_fields() @fields.depends(methods=['_recompute_trade_price_fields']) def on_change_premium_decomposition(self): self._recompute_trade_price_fields() @fields.depends(methods=['_recompute_trade_price_fields']) def on_change_price_type(self): self._recompute_trade_price_fields() @fields.depends(methods=['_recompute_trade_price_fields']) def on_change_enable_linked_currency(self): self._recompute_trade_price_fields() @fields.depends(methods=['_recompute_trade_price_fields']) def on_change_linked_price(self): self._recompute_trade_price_fields() @fields.depends(methods=['_recompute_trade_price_fields']) def on_change_linked_currency(self): self._recompute_trade_price_fields() @fields.depends(methods=['_recompute_trade_price_fields']) def on_change_linked_unit(self): self._recompute_trade_price_fields() def check_from_to(self,tr): if tr.pricing_period: date_from,date_to, d, include, dates = tr.getDateWithEstTrigger(1) if date_from: tr.from_p = date_from.date() if date_to: tr.to_p = date_to.date() if tr.application_period: date_from,date_to, d, include, dates = tr.getDateWithEstTrigger(2) if date_from: tr.from_a = date_from.date() if date_to: tr.to_a = date_to.date() TR = Pool().get('pricing.trigger') TR.save([tr]) def check_pricing(self): if self.price_components: for pc in self.price_components: if not pc.auto: Pricing = Pool().get('pricing.pricing') pricings = Pricing.search(['price_component','=',pc.id],order=[('pricing_date', 'ASC')]) if pricings: Pricing._sync_manual_values(pricings) Pricing._sync_manual_last(pricings) Pricing._sync_eod_price(pricings) if pc.triggers and pc.auto: prDate = [] prPrice = [] apDate = [] apPrice = [] calendar = pc.get_calendar() for t in pc.triggers: prD, prP = t.getPricingListDates(calendar) apD, apP = t.getApplicationListDates(calendar) prDate.extend(prD) prPrice.extend(prP) apDate.extend(apD) apPrice.extend(apP) if pc.quota_sale: prPrice = self.get_avg(prPrice) self.generate_pricing(pc,apDate,prPrice) def get_avg(self,lprice): l = len(lprice) if l > 0 : cumulprice = Decimal(0) i = 1 for p in lprice: if i > 1: p['avg_minus_1'] = cumulprice / (i-1) p['price'] = Decimal(str(p['price'] or 0)) cumulprice += p['price'] p['avg'] = cumulprice / i i += 1 return lprice def getnearprice(self,pl,d,t,max_date=None): if pl: pl_sorted = sorted(pl, key=lambda x: x['date']) pminus = pl_sorted[0] if not max_date: max_date = d.date() for p in pl_sorted: if p['date'].date() == d.date(): if p['isAvg'] and t == 'avg': return p[t] if not p['isAvg'] and t == 'avg': return p['price'] elif p['date'].date() > d.date(): if pminus != p: return pminus[t] else: return Decimal(0) pminus = p return pl_sorted[len(pl)-1][t] return Decimal(0) def _get_pricing_base_quantity(self): quantity = self.quantity_theorical if quantity is None: quantity = self.quantity return Decimal(str(quantity or 0)) def generate_pricing(self,pc,dl,pl): Pricing = Pool().get('pricing.pricing') pricing = Pricing.search(['price_component','=',pc.id]) if pricing: Pricing.delete(pricing) base_quantity = self._get_pricing_base_quantity() cumul_qt = 0 index = 0 dl_sorted = sorted(dl) for d in dl_sorted: if pc.pricing_date and d.date() > pc.pricing_date: break p = Pricing() p.sale_line = self.id logger.info("GENEDATE:%s",d) logger.info("TYPEDATE:%s",type(d)) p.pricing_date = d.date() p.price_component = pc.id p.quantity = round(Decimal(pc.quota_sale),4) price = round(Decimal(self.getnearprice(pl,d,'price',pc.pricing_date)),4) p.settl_price = price if price > 0: cumul_qt += pc.quota_sale p.fixed_qt = round(Decimal(cumul_qt),4) p.fixed_qt_price = round(Decimal(self.getnearprice(pl,d,'avg',pc.pricing_date)),4) #p.fixed_qt_price = p.get_fixed_price() if p.fixed_qt_price == 0: p.fixed_qt_price = round(Decimal(self.getnearprice(pl,d,'avg_minus_1',pc.pricing_date)),4) p.unfixed_qt = round(base_quantity - Decimal(cumul_qt),4) if p.unfixed_qt < 0.001: p.unfixed_qt = Decimal(0) p.fixed_qt = base_quantity if price > 0: logger.info("GENERATE_1:%s",price) p.unfixed_qt_price = price else: pr = Decimal(pc.get_price( p.pricing_date, self.unit, self.sale.currency, True)) pr = round(pr,4) logger.info("GENERATE_2:%s",pr) p.unfixed_qt_price = pr p.eod_price = p.get_eod_price_sale() if (index == len(dl)-1) or (pc.pricing_date and (index < len(dl)-1 and dl_sorted[index+1].date() > pc.pricing_date)): p.last = True logger.info("GENERATE_3:%s",p.unfixed_qt_price) Pricing.save([p]) index += 1 @classmethod def _valuation_regeneration_fields(cls): return { 'quantity', 'quantity_theorical', 'unit', 'unit_price', 'price_type', 'premium', 'linked_price', 'linked_currency', 'linked_unit', 'price_pricing', 'price_components', 'derivatives', 'fees', 'lots', 'product', 'currency', } @classmethod def _should_regenerate_valuation(cls, values): return bool(cls._valuation_regeneration_fields() & set(values)) @classmethod def write(cls, *args): actions = iter(args) args = [] valuation_line_ids = set() for records, values in zip(actions, actions): cls._check_delivery_period_values(records, values) if cls._should_regenerate_valuation(values): valuation_line_ids.update(record.id for record in records) args.extend((records, values)) old_values = {} for records, values in zip(args[::2], args[1::2]): if 'quantity_theorical' in values: for record in records: old_values[record.id] = record.quantity_theorical cls._set_quantity_counter_values_from_theorical( records, values) super().write(*args) lines = sum(args[::2], []) for line in lines: if line.id not in old_values: continue if old_values[line.id] is None: continue old = Decimal(old_values[line.id] or 0) new = Decimal(line.quantity_theorical or 0) delta = new - old if delta == 0: continue virtual_lots = [ lot for lot in (line.lots or []) if lot.lot_type == 'virtual' ] if not virtual_lots: continue vlot = virtual_lots[0] physical_quantity = sum( Decimal(lot.get_current_quantity_converted() or 0) for lot in (line.lots or []) if lot.lot_type == 'physic') target_quantity = round(new - physical_quantity, 5) if target_quantity < 0: raise UserError("Please unlink or unmatch lot") cls._sync_open_lot_quantity(line, vlot, target_quantity) for fee in line.fees or []: fee.sync_quantity_from_lots() fee.adjust_purchase_values() Pool().get('lot.lot').assert_lines_quantity_consistency( cls._fresh_lines_for_quantity_consistency(lines)) cls._create_missing_estimated_bl_dates(lines) if not Transaction().context.get('_purchase_trade_skip_fee_rules'): Pool().get('fee.rule').apply_to_lines( lines, 'sale_line', auto_only=True) if (valuation_line_ids and not Transaction().context.get( '_purchase_trade_skip_valuation_regeneration')): Valuation = Pool().get('valuation.valuation') with Transaction().set_context( _purchase_trade_skip_valuation_regeneration=True): Valuation.regenerate_for_sale_lines( cls.browse(list(valuation_line_ids))) @classmethod def _sync_open_lot_quantity(cls, line, vlot, target_quantity): Lot = Pool().get('lot.lot') LotQt = Pool().get('lot.qt') free_lqts = LotQt.search([ ('lot_s', '=', vlot.id), ('lot_p', '=', None), ('lot_shipment_in', '=', None), ('lot_shipment_internal', '=', None), ('lot_shipment_out', '=', None), ]) allocated_lqts = LotQt.search([ ('lot_s', '=', vlot.id), [ 'OR', ('lot_p', '!=', None), ('lot_shipment_in', '!=', None), ('lot_shipment_internal', '!=', None), ('lot_shipment_out', '!=', None), ], ]) allocated_quantity = sum( Decimal(lqt.lot_quantity or 0) for lqt in allocated_lqts) free_quantity = round(target_quantity - allocated_quantity, 5) logger.info( "SALE_QTY_SYNC line=%s vlot=%s target=%s allocated=%s " "free=%s free_lqts=%s", getattr(line, 'id', None), getattr(vlot, 'id', None), target_quantity, allocated_quantity, free_quantity, [getattr(lqt, 'id', None) for lqt in free_lqts]) if free_quantity < 0: raise UserError("Please unlink or unmatch lot") if free_lqts: lqt = free_lqts[0] logger.info( "SALE_QTY_SYNC_FREE_BEFORE lqt=%s quantity=%s new=%s", getattr(lqt, 'id', None), getattr(lqt, 'lot_quantity', None), free_quantity) lqt.lot_quantity = free_quantity LotQt.save([lqt]) logger.info( "SALE_QTY_SYNC_FREE_AFTER lqt=%s quantity=%s", getattr(lqt, 'id', None), getattr(LotQt(getattr(lqt, 'id')), 'lot_quantity', None)) elif free_quantity > 0: lqt = LotQt() lqt.lot_p = None lqt.lot_s = vlot.id lqt.lot_quantity = free_quantity lqt.lot_unit = line.unit LotQt.save([lqt]) current_quantity = round( Decimal(vlot.get_current_quantity_converted() or 0), 5) if current_quantity != target_quantity: vlot.set_current_quantity(target_quantity, target_quantity, 1) Lot.save([vlot]) @classmethod def delete(cls, lines): pool = Pool() LotQt = pool.get('lot.qt') Valuation = pool.get('valuation.valuation') OpenPosition = pool.get('open.position') for line in lines: if line.lots: vlot_s = line.lots[0].getVlot_s() lqts = LotQt.search([('lot_s','=',vlot_s.id),('lot_p','!=',None),('lot_quantity','>',0)]) if lqts: raise UserError("You cannot delete matched sale") return lqts = LotQt.search([('lot_s','=',vlot_s.id)]) LotQt.delete(lqts) valuations = Valuation.search([('lot','in',line.lots)]) if valuations: Valuation.delete(valuations) # op = OpenPosition.search(['sale_line','=',line.id]) # if op: # OpenPosition.delete(op) super(SaleLine, cls).delete(lines) @classmethod def copy(cls, lines, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('lots', None) default.setdefault('quantity', Decimal(0)) default.setdefault('quantity_theorical', None) default.setdefault('price_pricing', None) return super().copy(lines, default=default) @classmethod def _sync_virtual_lot_packing(cls, line): Lot = Pool().get('lot.lot') if not getattr(line, 'lots', None): line = cls(line.id) virtual_lots = [ lot for lot in (line.lots or []) if getattr(lot, 'lot_type', None) == 'virtual'] if not virtual_lots: return lot = virtual_lots[0] packing_count = getattr(line, 'coffee_packing_count', None) packing_unit = getattr(line, 'coffee_packing_unit', None) lot_unit_id = getattr(lot.lot_unit, 'id', lot.lot_unit) packing_unit_id = getattr(packing_unit, 'id', packing_unit) if lot.lot_qt == packing_count and lot_unit_id == packing_unit_id: return lot.lot_qt = packing_count lot.lot_unit = packing_unit Lot.save([lot]) @classmethod def _auto_hedge_configuration(cls): Configuration = Pool().get('sale.configuration') configurations = Configuration.search([], limit=1) if configurations: return configurations[0] @classmethod def _auto_hedge_contract_count(cls, line, over_hedge=False): price_index = getattr(line, 'coffee_market_reference', None) if not price_index or not getattr(line, 'unit', None): return 0 quantity = Decimal(str( getattr(line, 'quantity_theorical', None) or getattr(line, 'quantity', None) or 0)) if quantity <= 0: return 0 contract_quantity = Decimal(str(price_index.get_qt(1, line.unit) or 0)) if contract_quantity <= 0: return 0 rounding = ROUND_CEILING if over_hedge else ROUND_FLOOR return int((quantity / contract_quantity).to_integral_value( rounding=rounding)) @classmethod def _ensure_auto_hedge_derivative(cls, line): config = cls._auto_hedge_configuration() if not config or not getattr(config, 'auto_hedging', False): return if getattr(line, 'derivatives', None): return price_index = getattr(line, 'coffee_market_reference', None) nb_ct = cls._auto_hedge_contract_count( line, over_hedge=getattr(config, 'auto_hedging_over', False)) if not price_index or nb_ct <= 0: return Derivative = Pool().get('derivative.derivative') Date = Pool().get('ir.date') quantity = price_index.get_qt(nb_ct, line.unit) Derivative.create([{ 'sale': line.sale.id if line.sale else None, 'sale_line': line.id, 'product': line.product.id if line.product else None, 'party': line.sale.party.id if line.sale and line.sale.party else None, 'price_index': price_index.id, 'nb_ct': nb_ct, 'price': getattr(line, 'coffee_market_price', None), 'direction': 'long', 'trade_date': Date.today(), 'open_qty': quantity, }]) @classmethod def validate(cls, salelines): LotQtHist = Pool().get('lot.qt.hist') LotQtType = Pool().get('lot.qt.type') Pnl = Pool().get('valuation.valuation') super(SaleLine, cls).validate(salelines) for line in salelines: if cls._has_invalid_delivery_period(line): raise UserError( "Shipment period From date must be before To date.") if line.price_components: for pc in line.price_components: if pc.triggers: for tr in pc.triggers: line.check_from_to(tr) line.check_pricing() line.check_targeted_qt_tolerance() #no lot need to create one with line quantity if not line.created_by_code: if cls._sync_initial_quantity_from_theorical(line): cls.save([line]) if (not line.lots and line.product.type != 'service' and line.quantity_theorical and Decimal(str(line.quantity_theorical or 0)) != Decimal(0)): FeeLots = Pool().get('fee.lots') LotQtHist = Pool().get('lot.qt.hist') LotQtType = Pool().get('lot.qt.type') Lot = Pool().get('lot.lot') lot = Lot() lot.sale_line = line.id lot.lot_qt = line.quantity_theorical lot.lot_unit_line = line.unit lot.lot_quantity = Decimal( str(line.quantity_theorical)).quantize( Decimal("0.00001")) lot.lot_status = 'forecast' lot.lot_type = 'virtual' lot.lot_product = line.product lqtt = LotQtType.search([('sequence','=',1)]) if lqtt: lqh = LotQtHist() lqh.quantity_type = lqtt[0] lqh.quantity = lot.lot_quantity lqh.gross_quantity = lot.lot_quantity lot.lot_hist = [lqh] if Decimal(str(line.quantity_theorical or 0)) > 0: Lot.save([lot]) #check if fees need to be updated if line.fees: for fee in line.fees: fl_check = FeeLots.search([('fee','=',fee.id),('lot','=',lot.id),('sale_line','=',line.id)]) if not fl_check: fl = FeeLots() fl.fee = fee.id fl.lot = lot.id fl.sale_line = line.id FeeLots.save([fl]) cls._sync_virtual_lot_packing(line) cls._ensure_auto_hedge_derivative(line) #generate valuation for purchase and sale LotQt = Pool().get('lot.qt') line = cls(line.id) generated_purchase_side = False if line.lots: for lot in line.lots: lqts = LotQt.search([('lot_s','=',lot.id),('lot_p','>',0)]) if lqts: generated_purchase_side = True purchase_lines = [e.lot_p.line for e in lqts] if purchase_lines: for pl in purchase_lines: Pnl.generate(pl) if line.lots and not generated_purchase_side: Pnl.generate_from_sale_line(line) class SaleCreatePurchase(Wizard): "Create mirror purchase" __name__ = "sale.create.mirror" start = StateTransition() purchase = StateView( 'sale.create.input', 'purchase_trade.create_purchase_view_form', [ Button("Cancel", 'end', 'tryton-cancel'), Button("Create", 'creating', 'tryton-ok', default=True), ]) creating = StateTransition() def transition_start(self): return 'purchase' def transition_creating(self): Purchase = Pool().get('purchase.purchase') PL = Pool().get('purchase.line') LotQt = Pool().get('lot.qt') p = None pl = None for r in self.records: if r.lines: p = Purchase() p.party = self.purchase.party p.incoterm = self.purchase.incoterm p.payment_term = self.purchase.payment_term p.from_location = self.purchase.from_location p.to_location = self.purchase.to_location Purchase.save([p]) pl = PL() pl.quantity = r.lines[0].quantity pl.unit = r.lines[0].unit pl.product = r.lines[0].product pl.unit_price = self.purchase.unit_price pl.currency = self.purchase.currency pl.purchase = p.id PL.save([pl]) #Match if requested if self.purchase.match: #Increase forecasted virtual part matched if pl: if pl.lots: qt = Decimal(pl.quantity) vlot_p = pl.getVirtualLot() vlot_s = self.records[0].lines[0].getVirtualLot() if not vlot_p.updateVirtualPart(None,qt,vlot_p,None,vlot_s): vlot_p.createVirtualPart(qt,pl.unit,vlot_p,None,vlot_s) #Decrease forecasted virtual part non matched lqts = LotQt.search([('lot_p','=',vlot_p)]) if lqts: vlot_p.updateVirtualPart(lqts[0],-qt) lqts = LotQt.search([('lot_s','=',vlot_s)]) if lqts: vlot_p.updateVirtualPart(lqts[0],-qt) return 'end' def end(self): return 'reload' class SaleCreatePurchaseInput(ModelView): "Create purchase mirror" __name__ = "sale.create.input" party = fields.Many2One('party.party',"Supplier") incoterm = fields.Many2One('incoterm.incoterm',"Incoterm",domain=[('location', '=', False)]) payment_term = fields.Many2One('account.invoice.payment_term', "Payment Term") from_location = fields.Many2One('stock.location',"From location") to_location = fields.Many2One('stock.location',"To location") unit_price = fields.Numeric("Price") currency = fields.Many2One('currency.currency',"Currency") match = fields.Boolean("Match open quantity") class Derivative(metaclass=PoolMeta): "Derivative" __name__ = 'derivative.derivative' sale = fields.Many2One('sale.sale',"Sale") sale_line = fields.Many2One('sale.line',"Line") class Valuation(metaclass=PoolMeta): "Valuation" __name__ = 'valuation.valuation' sale = fields.Many2One('sale.sale',"Sale") sale_line = fields.Many2One('sale.line',"Line") class ValuationLine(metaclass=PoolMeta): "Last Valuation" __name__ = 'valuation.valuation.line' sale = fields.Many2One('sale.sale',"Sale") sale_line = fields.Many2One('sale.line',"Line") class ValuationReport(metaclass=PoolMeta): "Valuation Report" __name__ = 'valuation.report' sale = fields.Many2One('sale.sale',"Sale") sale_line = fields.Many2One('sale.line',"Line") class ValuationDyn(metaclass=PoolMeta): "Valuation" __name__ = 'valuation.valuation.dyn' r_sale = fields.Many2One('sale.sale',"Sale") r_sale_line = fields.Many2One('sale.line',"Line") @classmethod def table_query(cls): Valuation = Pool().get('valuation.valuation') 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.sale).as_('r_sale'), Max(val.line).as_('r_line'), Max(val.date).as_('r_date'), Literal(None).as_('r_type'), Max(val.reference).as_('r_reference'), Literal(None).as_('r_counterparty'), Max(val.product).as_('r_product'), Literal(None).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.amount_prev).as_('r_amount_prev'), Sum(val.base_amount).as_('r_base_amount'), Sum(val.pnl).as_('r_pnl'), Max(val.base_currency).as_('r_base_currency'), Sum(val.rate).as_('r_rate'), Avg(val.mtm_price).as_('r_mtm_price'), Avg(val.mtm_price_prev).as_('r_mtm_price_prev'), val.mtm_curve.as_('r_mtm_curve'), Sum(val.mtm).as_('r_mtm'), Max(val.strategy).as_('r_strategy'), Max(val.lot).as_('r_lot'), Max(val.sale_line).as_('r_sale_line'), Max(val.shipment_in).as_('r_shipment_in'), where=wh, group_by=[val.purchase, val.sale, val.mtm_curve]) return query class Fee(metaclass=PoolMeta): "Fee" __name__ = 'fee.fee' sale_line = fields.Many2One('sale.line',"Line", ondelete='CASCADE') class SaleAllocationsWizard(Wizard): 'Open Allocations report from Sale without modal' __name__ = 'sale.allocations.wizard' start_state = 'open_report' open_report = StateAction('purchase_trade.act_lot_report_form') def do_open_report(self, action): sale_id = Transaction().context.get('active_id') if not sale_id: raise ValueError("No active sale ID in context") action['context_model'] = 'lot.context' action['pyson_context'] = PYSONEncoder().encode({ 'sale': sale_id, }) return action, {}