# 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 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_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 trytond.exceptions import UserWarning, UserError from trytond.modules.purchase_trade.numbers_to_words import quantity_to_words, amount_to_currency_words, format_date_en 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") 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() 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: if self.sale_line.quantity: return round(self.sale_line.quantity_theorical / (self.nbdays if self.nbdays > 0 else 1),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): if self.sale_line: return round((self.fixed_qt * self.fixed_qt_price + self.unfixed_qt * self.unfixed_qt_price)/Decimal(self.sale_line.quantity),4) return Decimal(0) 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' btb = fields.Many2One('back.to.back',"Back to back") bank_accounts = fields.Function( fields.Many2Many('bank.account', None, None, "Bank Accounts"), 'on_change_with_bank_accounts') bank_account = fields.Many2One( 'bank.account', "Bank Account", domain=[('id', 'in', Eval('bank_accounts', []))], depends=['bank_accounts']) 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 +") 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') 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' ) trader = fields.Many2One('party.party',"Trader") operator = fields.Many2One('party.party',"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") @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 == 'MELYA') 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] @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', '_parent_party.bank_accounts') def on_change_party(self): super().on_change_party() self.bank_account = self._get_default_bank_account() @fields.depends('party', 'currency', '_parent_party.bank_accounts') def on_change_currency(self): self.bank_account = self._get_default_bank_account() @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 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): value = Decimal(str(value or 0)).quantize(Decimal(digits)) text = format(value, '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): return Decimal(str(line.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 '' @property def report_gross(self): lines = self._get_report_lines() if lines: total = Decimal(0) for line in lines: phys_lots = [l for l in line.lots if l.lot_type == 'physic'] if phys_lots: total += sum(Decimal(str(l.get_current_gross_quantity() or 0)) for l in phys_lots) else: total += Decimal(str(line.quantity or 0)) return total return '' @property def report_net(self): lines = self._get_report_lines() if lines: total = Decimal(0) for line in lines: phys_lots = [l for l in line.lots if l.lot_type == 'physic'] if phys_lots: total += sum(Decimal(str(l.get_current_quantity() or 0)) for l in phys_lots) else: total += Decimal(str(line.quantity or 0)) return total return '' @property def report_total_quantity(self): lines = self._get_report_lines() if lines: total = sum(Decimal(str(line.quantity or 0)) for line in lines) 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() if line and line.unit: return line.unit.rec_name.upper() return '' def _get_report_line_quantity(self, line): phys_lots = [l for l in line.lots if l.lot_type == 'physic'] if phys_lots: return sum(Decimal(str(l.get_current_quantity() or 0)) for l in phys_lots) return Decimal(str(line.quantity or 0)) @property def report_qt(self): lines = self._get_report_lines() if lines: total = sum(self._get_report_line_quantity(line) for line in lines) return quantity_to_words(total) 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) unit = line.unit.rec_name.upper() if line.unit and line.unit.rec_name else '' words = quantity_to_words(current_quantity) 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): if self.lines and self.lines[0].lots and len(self.lines[0].lots)>1: return self.lines[0].lots[1].line.purchase.number + ' ' + self.number else: '' @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_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) unit = line.unit.rec_name.upper() if line.unit and line.unit.rec_name else '' words = quantity_to_words(current_quantity) 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 @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 getLots(self): if self.lines: if self.lines.lots: return [l for l in self.lines.lots] @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]) class PriceComposition(metaclass=PoolMeta): __name__ = 'price.composition' sale_line = fields.Many2One('sale.line',"Sale line") class SaleLine(metaclass=PoolMeta): __name__ = 'sale.line' @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 '' del_period = fields.Many2One('product.month',"Delivery 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=True) premium = fields.Numeric("Premium/Discount",digits='unit') 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") 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') 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.Numeric("Premium/Discount",digits='unit') 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") @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') def on_change_with_attributes_name(self, name=None): 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)) @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): 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_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 get_progress(self,name): PS = Pool().get('sale.pricing.summary') ps = PS.search(['sale_line','=',self.id]) if ps: 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) if not source_unit and self.product: source_unit = self.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_price(self): premium = Decimal(self.premium or 0) if not premium: return Decimal(0) if self.enable_linked_currency and self.linked_currency: return self._linked_to_line_price(premium) return premium 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) 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('id','unit','quantity','unit_price','price_pricing','price_type','price_components','estimated_date','lots','fees','enable_linked_currency','linked_price','linked_currency','linked_unit') 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', 'unit_price', 'unit', 'product', 'sale', '_parent_sale.currency', 'premium', '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.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', 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_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: cumul_qt = Decimal(0) index = 0 for pr in pricings: cumul_qt += pr.quantity pr.fixed_qt = cumul_qt pr.fixed_qt_price = pr.get_fixed_price() pr.unfixed_qt = Decimal(pr.sale_line.quantity_theorical) - pr.fixed_qt pr.unfixed_qt_price = pr.fixed_qt_price pr.eod_price = pr.get_eod_price_sale() if index == len(pricings) - 1: pr.last = True index += 1 Pricing.save([pr]) if pc.triggers and pc.auto: prDate = [] prPrice = [] apDate = [] apPrice = [] for t in pc.triggers: prD, prP = t.getPricingListDates(pc.calendar) apD, apP = t.getApplicationListDates(pc.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 = float(0) i = 1 for p in lprice: if i > 1: p['avg_minus_1'] = cumulprice / (i-1) 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 generate_pricing(self,pc,dl,pl): Pricing = Pool().get('pricing.pricing') pricing = Pricing.search(['price_component','=',pc.id]) if pricing: Pricing.delete(pricing) 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(Decimal(self.quantity_theorical) - Decimal(cumul_qt),4) if p.unfixed_qt < 0.001: p.unfixed_qt = Decimal(0) p.fixed_qt = Decimal(self.quantity_theorical) if price > 0: logger.info("GENERATE_1:%s",price) p.unfixed_qt_price = price else: pr = Decimal(pc.price_index.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 write(cls, records, values): # if 'quantity' in values: # for record in records: # old_qt = record.quantity # new_qt = values['quantity'] # logger.info("WRITE_OLD_QT:%s",old_qt) # logger.info("WRITE_NEW_QT:%s",new_qt) # if old_qt != new_qt: # LotQt = Pool().get('lot.qt') # lqts = LotQt.search(['lot_s','=',record.lots[0]]) # if len(lqts)>1: # raise UserError("You cannot changed quantity with open quantities defined !") # return # elif len(lqts)==1: # if lqts[0].lot_p or lqts[0].lot_shipment_origin: # raise UserError("You cannot changed quantity with open quantities defined !") # return # lqts[0].lot_quantity = new_qt # LotQt.save(lqts) # super().write(records, values) @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 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 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() #no lot need to create one with line quantity logger.info("FROM_VALIDATE_LINE:%s",line.created_by_code) if not line.created_by_code: if not line.lots and line.product.type != 'service' and line.quantity != Decimal(0): Lot = Pool().get('lot.lot') lot = Lot() lot.sale_line = line.id lot.lot_qt = line.quantity lot.lot_unit_line = line.unit lot.lot_quantity = Decimal(str(line.quantity)).quantize(Decimal("0.00001"))#round(line.quantity,5) 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 line.quantity > 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]) #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)]) logger.info("VALIDATE_SL:%s",lqts) 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.base_amount).as_('r_base_amount'), Sum(val.rate).as_('r_rate'), Avg(val.mtm_price).as_('r_mtm_price'), 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'), where=wh, group_by=[val.purchase,val.sale]) return query class Fee(metaclass=PoolMeta): "Fee" __name__ = 'fee.fee' sale_line = fields.Many2One('sale.line',"Line") 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, {}