3656 lines
139 KiB
Python
Executable File
3656 lines
139 KiB
Python
Executable File
# 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 functools import wraps
|
||
from trytond.model import ModelSingleton, ModelSQL, ModelView, Workflow, fields
|
||
from trytond.i18n import gettext
|
||
from trytond.report import Report
|
||
from trytond.pool import Pool, PoolMeta
|
||
from trytond.pyson import Bool, Eval, Id, If, PYSONEncoder
|
||
from trytond.model import (ModelSQL, ModelView)
|
||
from trytond.tools import (cursor_dict, is_full_text, lstrip_wildcard)
|
||
from trytond.transaction import Transaction, inactive_records
|
||
from decimal import getcontext, Decimal, ROUND_HALF_UP
|
||
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
|
||
from sql.conditionals import Case, Coalesce
|
||
from sql import Column, Literal
|
||
from sql.functions import CurrentTimestamp, DateTrunc, Abs
|
||
from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction
|
||
from itertools import chain, groupby
|
||
from operator import itemgetter
|
||
import datetime
|
||
import logging
|
||
import json
|
||
import re
|
||
import jwt
|
||
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)
|
||
import requests
|
||
import io
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
TRIGGERS = [
|
||
('bldate', 'BL date'),
|
||
('invdate', 'Invoice date'),
|
||
('ctdate', 'Ct. date'),
|
||
('prdate', 'Pur. date'),
|
||
('cod', 'COD date'),
|
||
('border', 'Border crossing date'),
|
||
('pump', 'Pump date'),
|
||
('discharge', 'Discharge NOR'),
|
||
('arrival', 'Arrival date'),
|
||
('delmonth', 'Delivery month'),
|
||
('deldate', 'Delivery date'),
|
||
]
|
||
|
||
class DocType(ModelSQL,ModelView):
|
||
"Document Type"
|
||
__name__ = 'document.type'
|
||
name = fields.Char('Name')
|
||
|
||
class ContractDocumentType(ModelSQL):
|
||
"Contract - Document Type"
|
||
__name__ = 'contract.document.type'
|
||
doc_type = fields.Many2One('document.type', 'Document Type')
|
||
purchase = fields.Many2One('purchase.purchase', "Purchase")
|
||
|
||
class DocTemplate(ModelSQL,ModelView):
|
||
"Documents Template"
|
||
__name__ = 'doc.template'
|
||
name = fields.Char('Name')
|
||
type = fields.Many2Many('doc.type.template','template','type',"Document Type")
|
||
|
||
class DocTypeTemplate(ModelSQL):
|
||
"Template - Document Type"
|
||
__name__ = 'doc.type.template'
|
||
template = fields.Many2One('doc.template')
|
||
type = fields.Many2One('document.type')
|
||
|
||
class PurchaseStrategy(ModelSQL):
|
||
"Purchase - Document Type"
|
||
__name__ = 'purchase.strategy'
|
||
line = fields.Many2One('purchase.line', 'Purchase Line')
|
||
strategy = fields.Many2One('mtm.strategy', "Strategy")
|
||
|
||
class Estimated(metaclass=PoolMeta):
|
||
"Estimated date"
|
||
__name__ = 'pricing.estimated'
|
||
|
||
shipment_in = fields.Many2One('stock.shipment.in')
|
||
shipment_out = fields.Many2One('stock.shipment.out')
|
||
shipment_internal = fields.Many2One('stock.shipment.internal')
|
||
purchase = fields.Many2One('purchase.purchase',"Purchase")
|
||
line = fields.Many2One('purchase.line',"Line")
|
||
|
||
class Currency(metaclass=PoolMeta):
|
||
"Currency"
|
||
__name__ = 'currency.currency'
|
||
|
||
concatenate = fields.Boolean("Concatenate")
|
||
|
||
@classmethod
|
||
def default_concatenate(cls):
|
||
return False
|
||
|
||
class Unit(metaclass=PoolMeta):
|
||
"Unit"
|
||
__name__ = 'product.uom'
|
||
|
||
concatenate = fields.Boolean("Concatenate")
|
||
|
||
@classmethod
|
||
def default_concatenate(cls):
|
||
return False
|
||
|
||
class FeeLots(metaclass=PoolMeta):
|
||
|
||
"Fee lots"
|
||
__name__ = 'fee.lots'
|
||
|
||
line = fields.Many2One('purchase.line',"Line", ondelete='CASCADE')
|
||
|
||
class Component(metaclass=PoolMeta):
|
||
"Component"
|
||
__name__ = 'pricing.component'
|
||
|
||
line = fields.Many2One('purchase.line',"Line")
|
||
quota = fields.Function(fields.Numeric("Quota",digits='unit'),'get_quota_purchase')
|
||
unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit_purchase')
|
||
|
||
def getDelMonthDatePurchase(self):
|
||
PM = Pool().get('product.month')
|
||
if self.line.del_period:
|
||
pm = PM(self.line.del_period)
|
||
if pm:
|
||
return pm.beg_date
|
||
|
||
def getEstimatedTriggerPurchase(self,t):
|
||
if t == 'delmonth':
|
||
return self.getDelMonthDatePurchase()
|
||
if self.line and hasattr(self.line, 'get_date'):
|
||
trigger_date = self.line.get_date(t)
|
||
if trigger_date:
|
||
return trigger_date
|
||
PE = Pool().get('pricing.estimated')
|
||
Date = Pool().get('ir.date')
|
||
pe = PE.search([('line','=',self.line),('trigger','=',t)])
|
||
if pe:
|
||
return pe[0].estimated_date
|
||
else:
|
||
return Date.today()
|
||
|
||
def get_unit_purchase(self, name):
|
||
if self.line:
|
||
return self.line.unit
|
||
|
||
def get_quota_purchase(self, name):
|
||
if self.line:
|
||
quantity = getattr(self.line, 'quantity_theorical', None)
|
||
if quantity is None:
|
||
quantity = getattr(self.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, 5)
|
||
|
||
class Pricing(metaclass=PoolMeta):
|
||
"Pricing"
|
||
__name__ = 'pricing.pricing'
|
||
|
||
line = fields.Many2One('purchase.line',"Lines")
|
||
unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit_purchase')
|
||
|
||
def get_unit_purchase(self,name):
|
||
if self.line:
|
||
return self.line.unit
|
||
|
||
def get_eod_price_purchase(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__ = 'purchase.pricing.summary'
|
||
|
||
line = fields.Many2One('purchase.line',"Lines")
|
||
price_component = fields.Many2One('pricing.component',"Component")
|
||
quantity = fields.Numeric("Qt",digits=(1,5))
|
||
fixed_qt = fields.Numeric("Fixed qt",digits=(1,5))
|
||
unfixed_qt = fields.Numeric("Unfixed qt",digits=(1,5))
|
||
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.line.unit,self.line.purchase.currency,True)
|
||
|
||
@classmethod
|
||
def table_query(cls):
|
||
PurchasePricing = Pool().get('pricing.pricing')
|
||
pp = PurchasePricing.__table__()
|
||
PurchaseComponent = Pool().get('pricing.component')
|
||
pc = PurchaseComponent.__table__()
|
||
#wh = Literal(True)
|
||
context = Transaction().context
|
||
group_pnl = context.get('group_pnl')
|
||
|
||
if group_pnl:
|
||
return None
|
||
|
||
query = pp.join(pc,'LEFT',condition=pp.price_component == pc.id).select(
|
||
Literal(0).as_('create_uid'),
|
||
CurrentTimestamp().as_('create_date'),
|
||
Literal(None).as_('write_uid'),
|
||
Literal(None).as_('write_date'),
|
||
Literal(None).as_('sale_line'),
|
||
Max(pp.id).as_('id'),
|
||
pp.line.as_('line'),
|
||
pp.price_component.as_('price_component'),
|
||
Max(pp.fixed_qt+pp.unfixed_qt).as_('quantity'),
|
||
Max(pp.fixed_qt).as_('fixed_qt'),
|
||
(Min(pp.unfixed_qt)).as_('unfixed_qt'),
|
||
Max(Case((pp.last, pp.eod_price),else_=0)).as_('price'),
|
||
(Max(pp.fixed_qt)/Max(pp.fixed_qt+pp.unfixed_qt)).as_('progress'),
|
||
Max(pc.ratio).as_('ratio'),
|
||
#where=wh,
|
||
group_by=[pp.line,pp.price_component])
|
||
|
||
return query
|
||
|
||
class StockLocation(metaclass=PoolMeta):
|
||
__name__ = 'stock.location'
|
||
lat = fields.Numeric("Latitude")
|
||
lon = fields.Numeric("Longitude")
|
||
|
||
class PurchaseCertification(ModelSQL,ModelView):
|
||
"Certification"
|
||
__name__ = 'purchase.certification'
|
||
name = fields.Char("Name")
|
||
|
||
class PurchaseCertificationWeightBasis(ModelSQL,ModelView):
|
||
"Weight basis"
|
||
__name__ = 'purchase.weight.basis'
|
||
name = fields.Char("Name")
|
||
qt_type = fields.Many2One('lot.qt.type',"Associated type to final invoice")
|
||
description = fields.Char("Description")
|
||
|
||
class PurchaseAssociation(ModelSQL,ModelView):
|
||
"Association"
|
||
__name__ = 'purchase.association'
|
||
name = fields.Char("Name")
|
||
party = fields.Many2One('party.party',"Party")
|
||
rule = fields.Text("Description")
|
||
|
||
class PurchaseCrop(ModelSQL,ModelView):
|
||
"Crop"
|
||
__name__ = 'purchase.crop'
|
||
name = fields.Char("Name")
|
||
|
||
class Purchase(metaclass=PoolMeta):
|
||
__name__ = 'purchase.purchase'
|
||
|
||
@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, purchases):
|
||
pool = Pool()
|
||
Line = pool.get('purchase.line')
|
||
cls.set_purchase_date(purchases)
|
||
lines = list(sum((p.lines for p in purchases), ()))
|
||
Line._validate(lines, ['unit_price'])
|
||
cls.store_cache(purchases)
|
||
|
||
@classmethod
|
||
@ModelView.button
|
||
def process(cls, purchases):
|
||
purchases = [p for p in purchases if p.state != 'confirmed']
|
||
if purchases:
|
||
super().process(purchases)
|
||
|
||
@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', 'purchase', "Charter Conditions")
|
||
contract_template = fields.Many2One(
|
||
'contract.template', "Contract Template",
|
||
domain=[
|
||
('direction', 'in', ['both', 'purchase']),
|
||
('active', '=', True),
|
||
])
|
||
contract_clauses = fields.One2Many(
|
||
'contract.clause.selection', 'purchase', "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', "Supplier 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')])
|
||
first_line_product = fields.Function(
|
||
fields.Many2One('product.product', "Product"),
|
||
'get_first_line_product')
|
||
first_line_price_type = fields.Function(fields.Selection([
|
||
('cash', 'Cash Price'),
|
||
('priced', 'Priced'),
|
||
('basis', 'Basis'),
|
||
('efp', 'EFP'),
|
||
], "Price Type"), 'get_first_line_price_type')
|
||
first_line_del_period = fields.Function(
|
||
fields.Many2One('product.month', "Shipment Period"),
|
||
'get_first_line_del_period')
|
||
first_line_from_del = fields.Function(
|
||
fields.Date("Delivery From"), 'get_first_line_from_del')
|
||
first_line_to_del = fields.Function(
|
||
fields.Date("Delivery To"), 'get_first_line_to_del')
|
||
shipment_in = fields.Many2One('stock.shipment.in','Purchases')
|
||
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'),})
|
||
pnl = fields.One2Many('valuation.valuation.dyn', 'r_purchase', 'Pnl',states={'invisible': ~Eval('group_pnl'),})
|
||
pnl_ = fields.One2Many('valuation.valuation.line', 'purchase', 'Pnl',states={'invisible': Eval('group_pnl'),})
|
||
pnl_graph = fields.One2Many(
|
||
'purchase.pnl.graph', 'purchase', 'Pnl graph')
|
||
derivatives = fields.One2Many('derivative.derivative', 'purchase', 'Derivative')
|
||
plans = fields.One2Many('workflow.plan','purchase',"Execution plans")
|
||
forex = fields.One2Many('forex.cover.physical.contract','contract',"Forex",readonly=True)
|
||
plan = fields.Many2One('workflow.plan',"Name")
|
||
estimated_date = fields.One2Many('pricing.estimated','purchase',"Estimated date")
|
||
group_pnl = fields.Boolean("Group Pnl")
|
||
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', 'purchase', 'doc_type', 'Required Documents')
|
||
analytic_dimensions = fields.One2Many(
|
||
'analytic.dimension.assignment',
|
||
'purchase',
|
||
'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('purchase.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', '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 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 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(
|
||
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_p', '=', 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 += 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(purchase):
|
||
for line in purchase.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
|
||
|
||
def _get_first_trade_line(self):
|
||
for line in self.lines or []:
|
||
if getattr(line, 'type', None) == 'line':
|
||
return line
|
||
|
||
@staticmethod
|
||
def _record_id(record):
|
||
return getattr(record, 'id', record) if record else None
|
||
|
||
def get_first_line_product(self, name):
|
||
line = self._get_first_trade_line()
|
||
return self._record_id(getattr(line, 'product', None))
|
||
|
||
def get_first_line_price_type(self, name):
|
||
line = self._get_first_trade_line()
|
||
return getattr(line, 'price_type', None) if line else None
|
||
|
||
def get_first_line_del_period(self, name):
|
||
line = self._get_first_trade_line()
|
||
return self._record_id(getattr(line, 'del_period', None))
|
||
|
||
def get_first_line_from_del(self, name):
|
||
line = self._get_first_trade_line()
|
||
return getattr(line, 'from_del', None) if line else None
|
||
|
||
def get_first_line_to_del(self, name):
|
||
line = self._get_first_trade_line()
|
||
return getattr(line, 'to_del', None) if line else None
|
||
|
||
@classmethod
|
||
def delete(cls, purchases):
|
||
for purchase in purchases:
|
||
if cls._has_matched_physical_lots(purchase):
|
||
raise UserError(
|
||
"You cannot delete a purchase matched to a sale")
|
||
super().delete(purchases)
|
||
|
||
@property
|
||
def report_terms(self):
|
||
if self.lines:
|
||
return self.lines[0].note
|
||
else:
|
||
return ''
|
||
|
||
@property
|
||
def report_qt(self):
|
||
if self.lines:
|
||
return quantity_to_words(self.lines[0].quantity)
|
||
else:
|
||
return ''
|
||
|
||
@property
|
||
def report_price(self):
|
||
if self.lines:
|
||
if self.lines[0].price_type == 'priced':
|
||
return amount_to_currency_words(self.lines[0].unit_price)
|
||
elif self.lines[0].price_type == 'basis':
|
||
return amount_to_currency_words(self.lines[0].unit_price)
|
||
else:
|
||
return ''
|
||
|
||
@staticmethod
|
||
def _format_report_number(value, digits='0.0000',
|
||
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('.')
|
||
return text or '0'
|
||
|
||
def _get_report_lines(self):
|
||
return [
|
||
line for line in self.lines or []
|
||
if getattr(line, 'type', None) == 'line']
|
||
|
||
def _get_report_first_line(self):
|
||
lines = self._get_report_lines()
|
||
return lines[0] if lines else None
|
||
|
||
@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_in', None)
|
||
or getattr(lot, 'lot_shipment_out', 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(self):
|
||
return sum(
|
||
Decimal(str(getattr(line, 'quantity', 0) or 0))
|
||
for line in self._get_report_lines())
|
||
|
||
@property
|
||
def report_commission_quantity_display(self):
|
||
return self._format_report_number(
|
||
self.report_commission_quantity, digits='0.01',
|
||
strip_trailing_zeros=False)
|
||
|
||
@property
|
||
def report_commission_quantity_unit_upper(self):
|
||
line = self._get_report_first_line()
|
||
unit = getattr(line, 'unit', None) if line else None
|
||
return (getattr(unit, 'rec_name', None) or '').upper()
|
||
|
||
@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_lbs_display(self):
|
||
line = self._get_report_first_line()
|
||
unit = getattr(line, 'unit', None) if line else None
|
||
lbs_unit = self._find_report_unit('LBS') or self._find_report_unit('LB')
|
||
if not unit or not lbs_unit:
|
||
return ''
|
||
Uom = Pool().get('product.uom')
|
||
converted = Uom.compute_qty(
|
||
unit, float(self.report_commission_quantity), lbs_unit) or 0
|
||
return self._format_report_number(
|
||
Decimal(str(converted)), digits='0.01')
|
||
|
||
@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_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):
|
||
if self.lines and self.lines[0].del_period:
|
||
return self.lines[0].del_period.description or ''
|
||
return ''
|
||
|
||
@property
|
||
def report_payment_date(self):
|
||
if self.lines:
|
||
if self.lc_date:
|
||
return format_date_en(self.lc_date)
|
||
Date = Pool().get('ir.date')
|
||
payment_date = self.lines[0].sale.payment_term.lines[0].get_date(Date.today(),self.lines[0])
|
||
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 = 'B/L ' + shipment.bl_number
|
||
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)
|
||
|
||
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)
|
||
|
||
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_p', '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_s', 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_s', None) or getattr(lot, 'sale_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]
|
||
|
||
|
||
@fields.depends('party','from_location','to_location')
|
||
def on_change_with_viewer(self):
|
||
return self.get_viewer()
|
||
|
||
@fields.depends('doc_template','required_documents')
|
||
def on_change_with_required_documents(self):
|
||
if self.doc_template:
|
||
return self.doc_template.type
|
||
|
||
@classmethod
|
||
def _check_lines_delivery_period_values(cls, values):
|
||
line_commands = values.get('lines') or []
|
||
if not line_commands:
|
||
return
|
||
Line = Pool().get('purchase.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 purchases, values in zip(actions, actions):
|
||
cls._check_lines_delivery_period_values(values)
|
||
args.extend((purchases, values))
|
||
super().write(*args)
|
||
|
||
@classmethod
|
||
def copy(cls, purchases, default=None):
|
||
if default is None:
|
||
default = {}
|
||
else:
|
||
default = default.copy()
|
||
default.setdefault('pnl', None)
|
||
default.setdefault('derivatives', None)
|
||
default.setdefault('pnl_', None)
|
||
default.setdefault('plans', None)
|
||
return super().copy(purchases, default=default)
|
||
|
||
@classmethod
|
||
def validate(cls, purchases):
|
||
super(Purchase, cls).validate(purchases)
|
||
Line = Pool().get('purchase.line')
|
||
Date = Pool().get('ir.date')
|
||
for purchase in purchases:
|
||
for line in purchase.lines:
|
||
if not line.quantity_theorical and line.quantity > 0:
|
||
line.quantity_theorical = (
|
||
Decimal(str(line.quantity))
|
||
.quantize(Decimal("0.00001"))
|
||
)
|
||
Line.save([line])
|
||
|
||
#compute pnl
|
||
Pnl = Pool().get('valuation.valuation')
|
||
Pnl.generate(line)
|
||
|
||
if line.quantity_theorical:
|
||
OpenPosition = Pool().get('open.position')
|
||
# OpenPosition.create_from_purchase_line(line)
|
||
|
||
#line unit_price calculation
|
||
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()
|
||
logger.info("VALIDATEPURCHASE:%s",unit_price)
|
||
if unit_price != line.unit_price or line.linked_price != previous_linked_price:
|
||
line.unit_price = unit_price
|
||
logger.info("VALIDATEPURCHASE2:%s",line.unit_price)
|
||
Line.save([line])
|
||
if line.price_type == 'efp':
|
||
if line.derivatives:
|
||
for d in line.derivatives:
|
||
line.unit_price = round(Decimal(d.price_index.get_price(Date.today(),line.unit,line.currency,True)),4)
|
||
logger.info("EFP_PRICE:%s",line.unit_price)
|
||
Line.save([line])
|
||
|
||
def _contract_clause_values_from_template(self, direction):
|
||
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(ContractClauseSelection(
|
||
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(
|
||
'purchase')
|
||
|
||
class PriceComposition(ModelSQL,ModelView):
|
||
"Price Composition"
|
||
__name__ = 'price.composition'
|
||
|
||
line = fields.Many2One('purchase.line',"Purchase line")
|
||
component = fields.Char("Component")
|
||
price = fields.Numeric("Unit Price")
|
||
|
||
class PremiumComposition(ModelSQL, ModelView):
|
||
"Premium Composition"
|
||
__name__ = 'premium.composition'
|
||
|
||
line = fields.Many2One('purchase.line', "Purchase line", ondelete='CASCADE')
|
||
type = fields.Many2One('price.fixtype', "Type")
|
||
premium = fields.Numeric("Premium/Discount", digits=(16, 6))
|
||
currency = fields.Many2One('currency.currency', "Curr.")
|
||
reference = fields.Text("Reference")
|
||
base_amount = fields.Function(
|
||
fields.Numeric("Base Amt", digits=(16, 6)), 'get_base_amount')
|
||
|
||
def _get_owner_line(self):
|
||
return getattr(self, 'line', None) or getattr(self, 'sale_line', None)
|
||
|
||
def _get_contract(self):
|
||
line = self._get_owner_line()
|
||
if line:
|
||
return getattr(line, 'purchase', None) or getattr(line, 'sale', None)
|
||
|
||
def _get_contract_currency(self):
|
||
contract = self._get_contract()
|
||
return getattr(contract, 'currency', None)
|
||
|
||
def _get_contract_date(self):
|
||
contract = self._get_contract()
|
||
return (
|
||
getattr(contract, 'purchase_date', None)
|
||
or getattr(contract, 'sale_date', None))
|
||
|
||
def get_base_amount(self, name=None):
|
||
amount = Decimal(str(self.premium or 0))
|
||
currency = getattr(self, 'currency', None)
|
||
contract_currency = self._get_contract_currency()
|
||
if not currency or not contract_currency or currency == contract_currency:
|
||
return round(amount, 6)
|
||
Currency = Pool().get('currency.currency')
|
||
contract_date = self._get_contract_date()
|
||
if contract_date:
|
||
with Transaction().set_context(date=contract_date):
|
||
return round(Currency.compute(
|
||
currency, amount, contract_currency), 6)
|
||
return round(Currency.compute(currency, amount, contract_currency), 6)
|
||
|
||
|
||
CONTRACT_CLAUSE_CATEGORIES = [
|
||
(None, ''),
|
||
('payment', 'Payment'),
|
||
('delivery', 'Delivery'),
|
||
('transport', 'Transport'),
|
||
('documents', 'Documents'),
|
||
('quality', 'Quality'),
|
||
('quantity', 'Quantity'),
|
||
('price', 'Price'),
|
||
('insurance', 'Insurance'),
|
||
('title_transfer', 'Title Transfer'),
|
||
('charter', 'Charter / Demurrage'),
|
||
('tax', 'Tax'),
|
||
('sanctions', 'Sanctions'),
|
||
('force_majeure', 'Force Majeure'),
|
||
('law', 'Law / Arbitration'),
|
||
('other', 'Other'),
|
||
]
|
||
|
||
|
||
class ContractClause(ModelSQL, ModelView):
|
||
"Contract Clause"
|
||
__name__ = 'contract.clause'
|
||
|
||
name = fields.Char("Name", required=True)
|
||
category = fields.Selection(CONTRACT_CLAUSE_CATEGORIES, "Category")
|
||
direction = fields.Selection([
|
||
('both', 'Purchase & Sale'),
|
||
('purchase', 'Purchase'),
|
||
('sale', 'Sale'),
|
||
], "Direction", required=True)
|
||
active = fields.Boolean("Active")
|
||
version = fields.Char("Version")
|
||
language = fields.Char("Language")
|
||
source = fields.Char("Source")
|
||
text = fields.Text("Text", required=True)
|
||
incoterm_code = fields.Char("Incoterm")
|
||
product = fields.Many2One('product.product', "Product")
|
||
payment_term = fields.Many2One(
|
||
'account.invoice.payment_term', "Payment Term")
|
||
transport_type = fields.Selection([
|
||
(None, ''),
|
||
('vessel', 'Vessel'),
|
||
('truck', 'Truck'),
|
||
('other', 'Other'),
|
||
], "Transport Type")
|
||
cargo_mode = fields.Selection([
|
||
(None, ''),
|
||
('bulk', 'Bulk'),
|
||
('container', 'Container'),
|
||
('tanker', 'Tanker'),
|
||
], "Cargo Mode")
|
||
variables = fields.Function(
|
||
fields.Char("Variables"), 'get_variables')
|
||
|
||
@staticmethod
|
||
def default_active():
|
||
return True
|
||
|
||
@staticmethod
|
||
def default_direction():
|
||
return 'both'
|
||
|
||
def get_variables(self, name=None):
|
||
variables = sorted(set(re.findall(
|
||
r'\[([A-Za-z0-9_]+)\]', self.text or '')))
|
||
return ', '.join(variables)
|
||
|
||
def matches_contract(self, contract, direction):
|
||
if self.direction not in ('both', direction):
|
||
return False
|
||
if (self.incoterm_code
|
||
and getattr(getattr(contract, 'incoterm', None), 'code', None)
|
||
!= self.incoterm_code):
|
||
return False
|
||
if (self.payment_term
|
||
and getattr(getattr(contract, 'payment_term', None), 'id', None)
|
||
!= self.payment_term.id):
|
||
return False
|
||
if self.product:
|
||
products = {
|
||
getattr(getattr(line, 'product', None), 'id', None)
|
||
for line in getattr(contract, 'lines', []) or []
|
||
if getattr(line, 'type', 'line') == 'line'
|
||
}
|
||
if self.product.id not in products:
|
||
return False
|
||
if (self.transport_type
|
||
and getattr(contract, 'transport_type', None)
|
||
!= self.transport_type):
|
||
return False
|
||
if self.cargo_mode and getattr(contract, 'cargo_mode', None) != self.cargo_mode:
|
||
return False
|
||
return True
|
||
|
||
|
||
class ContractTemplate(ModelSQL, ModelView):
|
||
"Contract Template"
|
||
__name__ = 'contract.template'
|
||
|
||
name = fields.Char("Name", required=True)
|
||
direction = fields.Selection([
|
||
('both', 'Purchase & Sale'),
|
||
('purchase', 'Purchase'),
|
||
('sale', 'Sale'),
|
||
], "Direction", required=True)
|
||
active = fields.Boolean("Active")
|
||
incoterm_code = fields.Char("Incoterm")
|
||
product = fields.Many2One('product.product', "Product")
|
||
payment_term = fields.Many2One(
|
||
'account.invoice.payment_term', "Payment Term")
|
||
transport_type = fields.Selection([
|
||
(None, ''),
|
||
('vessel', 'Vessel'),
|
||
('truck', 'Truck'),
|
||
('other', 'Other'),
|
||
], "Transport Type")
|
||
cargo_mode = fields.Selection([
|
||
(None, ''),
|
||
('bulk', 'Bulk'),
|
||
('container', 'Container'),
|
||
('tanker', 'Tanker'),
|
||
], "Cargo Mode")
|
||
lines = fields.One2Many(
|
||
'contract.template.line', 'template', "Clauses")
|
||
notes = fields.Text("Notes")
|
||
|
||
@staticmethod
|
||
def default_active():
|
||
return True
|
||
|
||
@staticmethod
|
||
def default_direction():
|
||
return 'both'
|
||
|
||
def matches_contract(self, contract, direction):
|
||
if self.direction not in ('both', direction):
|
||
return False
|
||
if (self.incoterm_code
|
||
and getattr(getattr(contract, 'incoterm', None), 'code', None)
|
||
!= self.incoterm_code):
|
||
return False
|
||
if (self.payment_term
|
||
and getattr(getattr(contract, 'payment_term', None), 'id', None)
|
||
!= self.payment_term.id):
|
||
return False
|
||
if self.product:
|
||
products = {
|
||
getattr(getattr(line, 'product', None), 'id', None)
|
||
for line in getattr(contract, 'lines', []) or []
|
||
if getattr(line, 'type', 'line') == 'line'
|
||
}
|
||
if self.product.id not in products:
|
||
return False
|
||
if (self.transport_type
|
||
and getattr(contract, 'transport_type', None)
|
||
!= self.transport_type):
|
||
return False
|
||
if self.cargo_mode and getattr(contract, 'cargo_mode', None) != self.cargo_mode:
|
||
return False
|
||
return True
|
||
|
||
|
||
class ContractTemplateLine(ModelSQL, ModelView):
|
||
"Contract Template Line"
|
||
__name__ = 'contract.template.line'
|
||
|
||
template = fields.Many2One(
|
||
'contract.template', "Template", required=True, ondelete='CASCADE')
|
||
sequence = fields.Integer("Sequence")
|
||
section = fields.Selection(CONTRACT_CLAUSE_CATEGORIES, "Section")
|
||
clause = fields.Many2One('contract.clause', "Clause", required=True)
|
||
optional = fields.Boolean("Optional")
|
||
|
||
@staticmethod
|
||
def default_sequence():
|
||
return 10
|
||
|
||
|
||
class ContractClauseSelection(ModelSQL, ModelView):
|
||
"Contract Clause Selection"
|
||
__name__ = 'contract.clause.selection'
|
||
|
||
purchase = fields.Many2One(
|
||
'purchase.purchase', "Purchase", ondelete='CASCADE')
|
||
sequence = fields.Integer("Sequence")
|
||
section = fields.Selection(CONTRACT_CLAUSE_CATEGORIES, "Section")
|
||
clause = fields.Many2One('contract.clause', "Clause", required=True)
|
||
selected = fields.Boolean("Selected")
|
||
optional = fields.Boolean("Optional")
|
||
text_override = fields.Text("Override")
|
||
rendered_text = fields.Function(
|
||
fields.Text("Rendered Text"), 'get_rendered_text')
|
||
|
||
@staticmethod
|
||
def default_sequence():
|
||
return 10
|
||
|
||
@staticmethod
|
||
def default_selected():
|
||
return True
|
||
|
||
def _get_contract(self):
|
||
return getattr(self, 'purchase', None) or getattr(self, 'sale', None)
|
||
|
||
def _get_render_context(self):
|
||
contract = self._get_contract()
|
||
if not contract:
|
||
return {}
|
||
lines = [
|
||
line for line in getattr(contract, 'lines', []) or []
|
||
if getattr(line, 'type', 'line') == 'line']
|
||
products = ', '.join(filter(None, [
|
||
getattr(getattr(line, 'product', None), 'rec_name', None)
|
||
for line in lines]))
|
||
quantities = ', '.join(filter(None, [
|
||
str(getattr(line, 'quantity', '') or '')
|
||
for line in lines]))
|
||
units = ', '.join(filter(None, [
|
||
getattr(getattr(line, 'unit', None), 'rec_name', None)
|
||
for line in lines]))
|
||
return {
|
||
'Contract_Number': (
|
||
getattr(contract, 'reference', None)
|
||
or getattr(contract, 'number', None) or ''),
|
||
'reference': getattr(contract, 'reference', None) or '',
|
||
'party': getattr(getattr(contract, 'party', None), 'rec_name', None) or '',
|
||
'company': getattr(getattr(contract, 'company', None), 'rec_name', None) or '',
|
||
'currency': getattr(getattr(contract, 'currency', None), 'rec_name', None) or '',
|
||
'incoterm': getattr(getattr(contract, 'incoterm', None), 'code', None) or '',
|
||
'incoterm_location': getattr(getattr(contract, 'incoterm_location', None), 'rec_name', None) or '',
|
||
'payment_term': getattr(getattr(contract, 'payment_term', None), 'rec_name', None) or '',
|
||
'product': products,
|
||
'quantity': quantities,
|
||
'unit': units,
|
||
'product_origin': getattr(contract, 'product_origin', None) or '',
|
||
'del_from': str(getattr(contract, 'del_from', '') or ''),
|
||
'del_to': str(getattr(contract, 'del_to', '') or ''),
|
||
}
|
||
|
||
def get_rendered_text(self, name=None):
|
||
text = self.text_override or getattr(self.clause, 'text', '') or ''
|
||
context = self._get_render_context()
|
||
|
||
def replace(match):
|
||
key = match.group(1)
|
||
return str(context.get(key, match.group(0)))
|
||
|
||
return re.sub(r'\[([A-Za-z0-9_]+)\]', replace, text)
|
||
|
||
class AssayImporter:
|
||
|
||
def __init__(self):
|
||
pool = Pool()
|
||
self.AssayLine = pool.get('assay.line')
|
||
self.Element = pool.get('assay.element')
|
||
self.Unit = pool.get('assay.unit')
|
||
|
||
# -----------------------------
|
||
# PUBLIC ENTRYPOINT
|
||
# -----------------------------
|
||
def import_from_json(self, data: dict, assay):
|
||
self._update_assay(data,assay)
|
||
lines = self._create_lines(data, assay)
|
||
|
||
self.AssayLine.save(lines)
|
||
return assay
|
||
|
||
# -----------------------------
|
||
# HEADER
|
||
# -----------------------------
|
||
def _update_assay(self, data, assay):
|
||
Party = Pool().get('party.party')
|
||
metadata = data.get('document_metadata', {})
|
||
|
||
assay.reference = metadata.get('report_reference')
|
||
assay.date = self._parse_date(metadata.get('issue_date'))
|
||
assay.type = self._map_type(metadata.get('status'))
|
||
assay.status = 'draft'
|
||
assay.lab = Party.getPartyByName(metadata.get('lab_name'))
|
||
|
||
assay.save()
|
||
return assay
|
||
|
||
# -----------------------------
|
||
# LINES
|
||
# -----------------------------
|
||
def _create_lines(self, data, assay):
|
||
lines = []
|
||
|
||
# assays
|
||
for item in data.get('assays', []):
|
||
lines.append(self._build_line(item, assay, category='assay'))
|
||
|
||
# penalties
|
||
for item in data.get('penalties', []):
|
||
lines.append(self._build_line(item, assay, category='penalty'))
|
||
|
||
# moisture
|
||
moisture = data.get('weights_and_moisture', {}).get('moisture')
|
||
if moisture and moisture.get('value') is not None:
|
||
lines.append(self._build_line({
|
||
"element": "H2O",
|
||
"value": moisture.get('value'),
|
||
"unit": moisture.get('unit')
|
||
}, assay, category='moisture'))
|
||
|
||
return lines
|
||
|
||
# -----------------------------
|
||
# LINE BUILDER
|
||
# -----------------------------
|
||
def _build_line(self, item, assay, category):
|
||
line = self.AssayLine()
|
||
|
||
line.assay = assay
|
||
line.element = self._get_or_create_element(item.get('element'))
|
||
line.value = self._safe_float(item.get('value'))
|
||
line.unit = self._get_unit(item.get('unit'))
|
||
line.category = category
|
||
|
||
line.method = item.get('method')
|
||
line.is_payable = item.get('is_payable', False)
|
||
|
||
return line
|
||
|
||
# -----------------------------
|
||
# HELPERS
|
||
# -----------------------------
|
||
def _get_or_create_element(self, code):
|
||
if not code:
|
||
return None
|
||
|
||
elements = self.Element.search([('name', '=', code)])
|
||
if elements:
|
||
return elements[0]
|
||
|
||
# auto-create (optionnel mais pratique)
|
||
element = self.Element()
|
||
element.name = code
|
||
element.save()
|
||
return element
|
||
|
||
def _get_unit(self, unit_name):
|
||
if not unit_name:
|
||
return None
|
||
|
||
units = self.Unit.search([('symbol', '=', unit_name)])
|
||
if units:
|
||
return units[0]
|
||
|
||
return None # ou lever une erreur selon ton besoin
|
||
|
||
def _parse_date(self, date_str):
|
||
if not date_str:
|
||
return None
|
||
|
||
formats = [
|
||
"%Y-%m-%d", # 2025-02-28
|
||
"%d-%b-%Y", # 28-Feb-2025
|
||
"%d-%B-%Y", # 28-February-2025
|
||
"%d/%m/%Y", # 28/02/2025
|
||
]
|
||
|
||
for fmt in formats:
|
||
try:
|
||
return datetime.datetime.strptime(date_str, fmt).date()
|
||
except Exception:
|
||
continue
|
||
|
||
return None
|
||
|
||
def _parse_date_(self, date_str):
|
||
if not date_str:
|
||
return None
|
||
try:
|
||
return datetime.datetime.strptime(date_str, "%Y-%m-%d").date()
|
||
except Exception:
|
||
return None
|
||
|
||
def _safe_float(self, value):
|
||
try:
|
||
return float(value)
|
||
except Exception:
|
||
return None
|
||
|
||
def _map_type(self, status):
|
||
if not status:
|
||
return 'provisional'
|
||
|
||
status = status.lower()
|
||
|
||
if 'final' in status:
|
||
return 'final'
|
||
if 'umpire' in status:
|
||
return 'umpire'
|
||
|
||
return 'provisional'
|
||
|
||
class AssayUnit(ModelSQL, ModelView):
|
||
'Assay Unit'
|
||
__name__ = 'assay.unit'
|
||
_rec_name = 'symbol'
|
||
|
||
name = fields.Char('Name') # Percent, g/t, ppm
|
||
symbol = fields.Char('Symbol') # %, g/t, ppm
|
||
dimension = fields.Selection([
|
||
('mass_fraction', 'Mass Fraction'),
|
||
('grade', 'Grade'),
|
||
('trace', 'Trace'),
|
||
])
|
||
|
||
class Assay(ModelSQL, ModelView):
|
||
"Assay"
|
||
__name__ = 'assay.assay'
|
||
|
||
line = fields.Many2One('purchase.line',"Purchase Line")
|
||
|
||
reference = fields.Char("Reference")
|
||
date = fields.Date("Analysis Date")
|
||
|
||
type = fields.Selection([
|
||
(None, ''),
|
||
('provisional', 'Provisional'),
|
||
('final', 'Final'),
|
||
('umpire', 'Umpire'),
|
||
], "Type")
|
||
|
||
status = fields.Selection([
|
||
(None, ''),
|
||
('draft', 'Draft'),
|
||
('validated', 'Validated'),
|
||
], "Status")
|
||
|
||
lab = fields.Many2One('party.party',"Laboratory")
|
||
|
||
lines = fields.One2Many(
|
||
'assay.line', 'assay', "Assay Lines"
|
||
)
|
||
|
||
analysis = fields.Many2One('document.incoming',"Analysis")
|
||
|
||
class AssayLine(ModelSQL, ModelView):
|
||
"Assay Line"
|
||
__name__ = 'assay.line'
|
||
|
||
assay = fields.Many2One('assay.assay', "Assay")
|
||
|
||
element = fields.Many2One('assay.element', "Element")
|
||
|
||
value = fields.Numeric("Value")
|
||
|
||
unit = fields.Many2One('assay.unit', "Unit")
|
||
|
||
category = fields.Selection([
|
||
('assay', 'Assay'),
|
||
('penalty', 'Penalty'),
|
||
('moisture', 'Moisture'),
|
||
], "Category")
|
||
|
||
method = fields.Char("Method")
|
||
|
||
is_payable = fields.Boolean("Payable")
|
||
|
||
class AssayElement(ModelSQL, ModelView):
|
||
"Assay Element"
|
||
__name__ = 'assay.element'
|
||
|
||
name = fields.Char("Code") # Cu, Au, As
|
||
description = fields.Char("Description")
|
||
|
||
default_unit = fields.Many2One('product.uom', "Default Unit")
|
||
|
||
type = fields.Selection([
|
||
(None, ''),
|
||
('metal', 'Metal'),
|
||
('impurity', 'Impurity'),
|
||
], "Type")
|
||
|
||
class PayableRule(ModelSQL, ModelView):
|
||
"Payable Rule"
|
||
__name__ = 'payable.rule'
|
||
_rec_name = 'name'
|
||
|
||
name = fields.Char("Name")
|
||
element = fields.Many2One('assay.element', "Element")
|
||
|
||
payable_percent = fields.Numeric("Payable %")
|
||
|
||
deduction_value = fields.Numeric("Deduction Value")
|
||
deduction_unit = fields.Many2One('assay.unit',"Unit")
|
||
|
||
payable_method = fields.Selection([
|
||
('percent', 'Fixed %'),
|
||
('grade_minus', 'Grade minus deduction'),
|
||
('min_of_both', 'Min(% of grade, grade - deduction)'),
|
||
], "Method")
|
||
min_payable = fields.Numeric("Floor (min payable)")
|
||
|
||
def compute_payable_quantity(self, grade):
|
||
"""
|
||
Retourne la quantité payable dans l'unité du grade.
|
||
grade : Decimal (ex: Decimal('26.862'))
|
||
"""
|
||
grade = Decimal(str(grade))
|
||
|
||
if self.payable_method == 'percent':
|
||
result = grade * self.payable_percent / Decimal(100)
|
||
|
||
elif self.payable_method == 'grade_minus':
|
||
result = grade - self.deduction_value
|
||
|
||
elif self.payable_method == 'min_of_both':
|
||
by_percent = grade * self.payable_percent / Decimal(100)
|
||
by_deduction = grade - self.deduction_value
|
||
if self.deduction_unit.symbol == 'g/t':
|
||
result = min(by_percent/Decimal(10000), by_deduction/Decimal(10000))
|
||
else:
|
||
result = min(by_percent, by_deduction)
|
||
|
||
if self.min_payable is not None:
|
||
result = max(result, self.min_payable)
|
||
|
||
return result
|
||
|
||
class PenaltyRuleTier(ModelSQL, ModelView):
|
||
"Penalty Rule Tier"
|
||
__name__ = 'penalty.rule.tier'
|
||
rule = fields.Many2One('penalty.rule', "Rule", ondelete='CASCADE')
|
||
threshold_from = fields.Numeric("From")
|
||
threshold_to = fields.Numeric("To") # None = pas de plafond
|
||
threshold_unit = fields.Many2One('assay.unit', "Unit")
|
||
deduction_per_unit = fields.Numeric("Deduction / unit")
|
||
penalty_value = fields.Numeric("Penalty Value (USD/DMT)")
|
||
mode = fields.Selection([
|
||
('excess', 'Excess above threshold'),
|
||
('full', 'Full grade in tier'),
|
||
('min_or_both', 'Min(grade, cap) — full tier amount'),
|
||
], "Mode")
|
||
|
||
@classmethod
|
||
def default_mode(cls):
|
||
return 'excess'
|
||
|
||
def compute_tier_penalty(self, grade):
|
||
grade = Decimal(str(grade))
|
||
|
||
if grade <= self.threshold_from:
|
||
return Decimal(0)
|
||
|
||
if self.mode == 'excess':
|
||
# Ton comportement actuel : on paye seulement l'excès au-dessus du seuil bas
|
||
# Ex Codelco : As=0,7% → palier 0,2-0,5% donne 0,3% d'excès
|
||
excess_top = grade if self.threshold_to is None else min(grade, self.threshold_to)
|
||
taxable = excess_top - self.threshold_from
|
||
|
||
elif self.mode == 'full':
|
||
# Pénalité sur toute la teneur dans ce palier dès déclenchement
|
||
# Ex : As=0,7% → palier 0,5-1,0% donne 0,7% entier (pas 0,2%)
|
||
taxable = grade if self.threshold_to is None else min(grade, self.threshold_to)
|
||
|
||
elif self.mode == 'min_or_both':
|
||
# Pénalité sur min(grade, plafond) — utile quand le contrat dit
|
||
# "si As dépasse X%, pénalité sur la tranche entière jusqu'à Y%"
|
||
# Ex vente fichier : palier 0,3-0,9%, As=0,6% → taxable = min(0,6%, 0,9%) = 0,6%
|
||
cap = self.threshold_to if self.threshold_to is not None else grade
|
||
taxable = min(grade, cap)
|
||
|
||
return (taxable * self.penalty_value).quantize(Decimal('0.01'))
|
||
|
||
class PenaltyRule(ModelSQL, ModelView):
|
||
"Penalty Rule"
|
||
__name__ = 'penalty.rule'
|
||
name = fields.Char("Name")
|
||
element = fields.Many2One('assay.element', "Element")
|
||
tiers = fields.One2Many('penalty.rule.tier', 'rule', "Tiers")
|
||
|
||
def compute_penalty(self, grade):
|
||
"""
|
||
Retourne la pénalité totale USD en cumulant tous les paliers traversés.
|
||
grade : Decimal – teneur brute de l'élément
|
||
dry_weight_dmt: Decimal – poids sec en DMT
|
||
"""
|
||
grade = Decimal(str(grade))
|
||
|
||
total = Decimal(0)
|
||
for tier in self.tiers:
|
||
total += tier.compute_tier_penalty(grade)
|
||
|
||
return total.quantize(Decimal('0.01'))
|
||
|
||
class ConcentrateTerm(ModelSQL, ModelView):
|
||
"Concentrate Term"
|
||
__name__ = 'concentrate.term'
|
||
|
||
line = fields.Many2One(
|
||
'purchase.line', "Line",
|
||
ondelete='CASCADE'
|
||
)
|
||
|
||
element = fields.Many2One('assay.element',"Element")
|
||
|
||
component = fields.Many2One(
|
||
'pricing.component',
|
||
"Price Component",
|
||
domain=[
|
||
('id', 'in', Eval('line_component')),
|
||
],
|
||
depends=['line_component']
|
||
)
|
||
|
||
line_component = fields.Function(fields.One2Many('pricing.component','',"Component"),'on_change_with_line_component')
|
||
manual_price = fields.Numeric(
|
||
"Price",
|
||
digits=(16, 2)
|
||
)
|
||
|
||
currency = fields.Many2One('currency.currency',"Curr")
|
||
unit = fields.Many2One('product.uom',"Unit")
|
||
|
||
payable_rule = fields.Many2One(
|
||
'payable.rule',"Payable Rule"
|
||
)
|
||
|
||
penalty_rules = fields.Many2One(
|
||
'penalty.rule',
|
||
"Penalties"
|
||
)
|
||
|
||
valid_from = fields.Date("Valid From")
|
||
valid_to = fields.Date("Valid To")
|
||
|
||
@fields.depends('component')
|
||
def on_change_with_line_component(self, name):
|
||
PC = Pool().get('pricing.component')
|
||
return PC.search(['line','=',self.line])
|
||
|
||
class QualityAnalysis(ModelSQL,ModelView):
|
||
"Quality Analysis"
|
||
__name__ = 'quality.analysis'
|
||
|
||
line = fields.Many2One('purchase.line',"Purchase Line")
|
||
reference = fields.Char("Reference")
|
||
date = fields.Date("Analysis date")
|
||
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."
|
||
)
|
||
|
||
product = fields.Function(
|
||
fields.Many2One('product.product', "Product"),
|
||
'on_change_with_product'
|
||
)
|
||
|
||
attribute_set = fields.Function(
|
||
fields.Many2One('product.attribute.set', "Attribute Set"),
|
||
'on_change_with_attribute_set'
|
||
)
|
||
|
||
attributes_name = fields.Function(
|
||
fields.Char("Details"),
|
||
'on_change_with_attributes_name'
|
||
)
|
||
|
||
last_analysis_pricing = fields.Boolean("Used for pricing")
|
||
|
||
@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('line')
|
||
def on_change_with_product(self, name=None):
|
||
if self.line:
|
||
return self.line.product
|
||
|
||
@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))
|
||
|
||
class Line(metaclass=PoolMeta):
|
||
__name__ = 'purchase.line'
|
||
|
||
@classmethod
|
||
def __setup__(cls):
|
||
super().__setup__()
|
||
cls.quantity.readonly = True
|
||
|
||
def _get_service_fee_invoice_lines(self, invoice_line, lot):
|
||
Fee = Pool().get('fee.fee')
|
||
fee = Fee.search(['purchase', '=', self.purchase.id])
|
||
if not fee:
|
||
return [invoice_line]
|
||
|
||
fee = fee[0]
|
||
invoice_line.fee = fee
|
||
fee.warn_percent_price_partial_lots()
|
||
if fee.mode == 'lumpsum':
|
||
invoice_line.quantity = 1
|
||
elif fee.mode == 'pprice':
|
||
invoice_line.quantity = 1
|
||
elif fee.mode == 'ppack':
|
||
invoice_line.quantity = fee.quantity
|
||
elif (
|
||
fee.mode == 'perqt'
|
||
and getattr(fee, 'add_padding', False)
|
||
and not Transaction().context.get(
|
||
'_purchase_trade_ignore_fee_padding')):
|
||
invoice_line.quantity = fee.get_billing_quantity()
|
||
else:
|
||
invoice_line.quantity = fee.get_fee_lots_qt()
|
||
|
||
if getattr(fee, 'state', None) != 'invoiced':
|
||
return [invoice_line]
|
||
|
||
previous_line = self._get_last_fee_invoice_line(fee, lot)
|
||
if not self._fee_invoice_line_changed(previous_line, invoice_line):
|
||
return []
|
||
reversal_line = self._get_fee_reversal_invoice_line(
|
||
previous_line, self)
|
||
if reversal_line:
|
||
return [reversal_line, invoice_line]
|
||
return [invoice_line]
|
||
|
||
@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
|
||
|
||
@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 '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, 'purchase_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_purchase_lines(lines)
|
||
return lines
|
||
|
||
@classmethod
|
||
@ModelView.button
|
||
def apply_default_fees(cls, lines):
|
||
Pool().get('fee.rule').apply_to_lines(lines, 'purchase_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()
|
||
|
||
quantity_theorical = fields.Numeric("Contractual Qt", 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')
|
||
del_period = fields.Many2One('product.month',"Shipment Period")
|
||
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','line',"Components")
|
||
price_pricing = fields.One2Many('pricing.pricing','line',"Pricing")
|
||
price_summary = fields.One2Many('purchase.pricing.summary','line',"Summary")
|
||
premium_decomposition = fields.One2Many(
|
||
'premium.composition', 'line', "Premium decomposition")
|
||
charter_conditions = fields.One2Many(
|
||
'charter.condition', 'purchase_line', "Charter Conditions")
|
||
estimated_date = fields.One2Many('pricing.estimated','line',"Estimated date")
|
||
optional = fields.One2Many('optional.scenario','line',"Optionals Scenarios")
|
||
lots = fields.One2Many('lot.lot','line',"Lots",readonly=True)
|
||
purchase_line = fields.Many2One('purchase.line',"Lines")
|
||
fees = fields.One2Many('fee.fee', 'line', 'Fees')#, filter=[('product.type', '=', 'service')])
|
||
derivatives = fields.One2Many('derivative.derivative','line',"Derivatives")
|
||
mtm = fields.Many2Many('purchase.strategy', 'line', 'strategy', 'Mtm Strategy')
|
||
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')
|
||
# certification = fields.Selection([
|
||
# (None, ''),
|
||
# ('bci', 'BCI'),
|
||
# ],"Certification",states={'readonly': (Eval('inherit_cer')),})
|
||
certif = fields.Many2One('purchase.certification',"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")
|
||
pricing_rule = fields.Text("Pricing description")
|
||
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_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')
|
||
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_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',
|
||
'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.purchase:
|
||
return None
|
||
value = getattr(self.purchase, 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_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', 'purchase',
|
||
'_parent_purchase.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.purchase and is_itsa_company(self.purchase.company)
|
||
and (not previous_unit
|
||
or record_id(previous_unit) == default_itsa_unit())):
|
||
unit = default_itsa_unit()
|
||
if unit:
|
||
self.unit = unit
|
||
|
||
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")
|
||
|
||
quality_analysis = fields.One2Many('quality.analysis','line',"Quality analysis")
|
||
assays = fields.One2Many('assay.assay','line',"Assays")
|
||
terms = fields.One2Many('concentrate.term','line',"Terms")
|
||
term = fields.Many2One('document.incoming',"Contract")
|
||
update_pricing = fields.Boolean("Update pricing")
|
||
assay_state = fields.Selection([
|
||
(None, ''),
|
||
('provisional', 'Provisional'),
|
||
('final', 'Final'),
|
||
('umpire', 'Umpire'),
|
||
], "Type")
|
||
|
||
@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
|
||
|
||
|
||
@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_p','=',self.lots[0].id),('lot_s','>',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:
|
||
trigger_date = [d.estimated_date for d in self.estimated_date if d.trigger == 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.purchase.tol_min and self.quantity_theorical:
|
||
return round((1-(self.purchase.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.purchase.tol_max and self.quantity_theorical:
|
||
return round((1+(self.purchase.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 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 Decimal(str(Uom.compute_qty(
|
||
from_unit, float(quantity or 0), to_unit, True, factor, rate)))
|
||
|
||
def _tolerance_theoretical_quantity(self):
|
||
return 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(
|
||
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_p', '=', 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 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):
|
||
purchase = getattr(self, 'purchase', None)
|
||
if self.inherit_tol and purchase:
|
||
return -Decimal(str(purchase.tol_min or 0))
|
||
return -Decimal(str(self.tol_min or 0))
|
||
|
||
def get_tolerance_max(self, name):
|
||
purchase = getattr(self, 'purchase', None)
|
||
if self.inherit_tol and purchase:
|
||
return Decimal(str(purchase.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('purchase.pricing.summary')
|
||
ps = PS.search(['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)
|
||
if not source_unit and self.product:
|
||
source_unit = self.product.purchase_uom or self.product.default_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)
|
||
purchase = getattr(self, 'purchase', None)
|
||
return list(getattr(purchase, '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([('line', '=', line.id)])
|
||
if existing:
|
||
Premium.delete(existing)
|
||
if value:
|
||
Premium.create([{
|
||
'line': line.id,
|
||
'premium': value,
|
||
'currency': (
|
||
line.purchase.currency.id
|
||
if line.purchase and line.purchase.currency else None),
|
||
}])
|
||
|
||
def _get_premium_price(self):
|
||
return Decimal(self.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, 'purchase', 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_basis_component_price(self):
|
||
price = Decimal(0)
|
||
terms = getattr(self, 'terms', None)
|
||
if terms:
|
||
for t in terms:
|
||
price += (t.manual_price if t.manual_price else Decimal(0))
|
||
else:
|
||
if not self.price_components:
|
||
PP = Pool().get('purchase.pricing.summary')
|
||
pp = PP.search([
|
||
('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('purchase.pricing.summary')
|
||
pp = PP.search([('price_component','=',pc.id),('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(self,lot_premium=0):
|
||
return round(
|
||
Decimal(self.unit_price or 0)
|
||
+ Decimal(lot_premium or 0),
|
||
4)
|
||
|
||
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','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()
|
||
price = self.get_basis_price()
|
||
logger.info("ONCHANGEUNITPRICE_IN:%s",price)
|
||
return 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 round(d.price_index.get_price(Date.today(),self.unit,self.purchase.currency,True),4)
|
||
return self.get_price()
|
||
|
||
@fields.depends(
|
||
'type', 'quantity', 'quantity_theorical', 'finished',
|
||
'unit_price', 'unit', 'product', 'lots',
|
||
'purchase', '_parent_purchase.currency', '_parent_purchase.wb',
|
||
'premium_decomposition', 'enable_linked_currency',
|
||
'linked_currency', 'linked_unit')
|
||
def on_change_with_amount(self):
|
||
if self.type == 'line' and self.unit_price is not None:
|
||
quantity = self._get_amount_quantity()
|
||
if quantity is None:
|
||
return Decimal(0)
|
||
currency = self.purchase.currency if self.purchase else None
|
||
amount = Decimal(str(quantity)) * (
|
||
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()
|
||
|
||
@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 _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 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))
|
||
|
||
# Agents:
|
||
# Ici on gère la variation éventuelle de la théorical quantity après création du contrat
|
||
# Si delta > 0 on met à jour le lot virtual qui est toujours unique pour une purchase line
|
||
# mais aussi la table lot_qt dont le lot_p est ce lot virtuel (ajuster lot_quantity)
|
||
# si il n'existe aucun lot_qt non shippé (tous les lot_shipments à None) et aucun non matché (lot_s à None)
|
||
# alors il faut créer un nouveau lot_qt non shippé et non matché avec le delta
|
||
# Si delta négatif alors on decrease si c'est possible le lot_qt non shippé non matché et s'il n'y en a pas on envoie un
|
||
# message d'erreur 'Please unlink or unmatch lot'
|
||
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 in old_values:
|
||
new = Decimal(line.quantity_theorical or 0)
|
||
virtual_lots = [lot for lot in (line.lots or []) if lot.lot_type == 'virtual']
|
||
if not virtual_lots:
|
||
continue
|
||
vlot = virtual_lots[0]
|
||
# Initializing quantity_theorical after virtual-lot creation
|
||
# must not add the line quantity a second time.
|
||
old = old_values[line.id]
|
||
baseline = (
|
||
Decimal(old)
|
||
if old is not None
|
||
else Decimal(vlot.get_current_quantity_converted() or 0)
|
||
)
|
||
delta = new - baseline
|
||
if delta:
|
||
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, 'purchase_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_purchase_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_domain = [
|
||
('lot_p', '=', vlot.id),
|
||
('lot_s', '=', None),
|
||
('lot_shipment_in', '=', None),
|
||
('lot_shipment_internal', '=', None),
|
||
('lot_shipment_out', '=', None),
|
||
]
|
||
free_lqts = LotQt.search(free_domain)
|
||
allocated_lqts = LotQt.search([
|
||
('lot_p', '=', vlot.id),
|
||
[
|
||
'OR',
|
||
('lot_s', '!=', 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(
|
||
"PURCHASE_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(
|
||
"PURCHASE_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(
|
||
"PURCHASE_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 = vlot.id
|
||
lqt.lot_s = None
|
||
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 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 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_p = line.lots[0].getVlot_p()
|
||
lqts = LotQt.search([('lot_p','=',vlot_p.id),('lot_s','!=',None),('lot_quantity','>',0)])
|
||
if lqts:
|
||
raise UserError("You cannot delete matched sale")
|
||
return
|
||
lqts = LotQt.search([('lot_p','=',vlot_p.id)])
|
||
LotQt.delete(lqts)
|
||
valuations = Valuation.search([('lot','in',line.lots)])
|
||
if valuations:
|
||
Valuation.delete(valuations)
|
||
# op = OpenPosition.search(['line','=',line.id])
|
||
# if op:
|
||
# OpenPosition.delete(op)
|
||
super(Line, cls).delete(lines)
|
||
|
||
@classmethod
|
||
def validate(cls, lines):
|
||
super(Line, cls).validate(lines)
|
||
for line in lines:
|
||
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.line = line.id
|
||
lot.lot_qt = None
|
||
lot.lot_unit = None
|
||
lot.lot_unit_line = line.unit
|
||
lot.lot_quantity = round(
|
||
Decimal(str(line.quantity_theorical)), 5)
|
||
lot.lot_gross_quantity = None
|
||
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
|
||
logger.info("PURCHASE_VALIDATE:%s",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),('line','=',line.id)])
|
||
if not fl_check:
|
||
fl = FeeLots()
|
||
fl.fee = fee.id
|
||
fl.lot = lot.id
|
||
fl.line = line.id
|
||
FeeLots.save([fl])
|
||
cls._sync_virtual_lot_packing(line)
|
||
|
||
if line.fee_:
|
||
if not line.fee_.purchase:
|
||
Fee = Pool().get('fee.fee')
|
||
f = Fee(line.fee_)
|
||
f.purchase = line.purchase
|
||
Fee.save([f])
|
||
|
||
if line.assays:
|
||
for assay in line.assays:
|
||
if not assay.lines and assay.analysis:
|
||
file_data = assay.analysis.data or b""
|
||
logger.info(f"File size: {len(file_data)} bytes")
|
||
file_name = assay.analysis.name or "document"
|
||
|
||
response = requests.post(
|
||
"http://62.72.36.116:8006/ocr-parse-assay",
|
||
files={"file": (file_name, io.BytesIO(file_data))}
|
||
)
|
||
response.raise_for_status()
|
||
f = response.json()
|
||
logger.info("RUN_OCR_RESPONSE:%s", f)
|
||
|
||
parsed_data_str = f.get("parsed_data") # string JSON venant de ton endpoint
|
||
if parsed_data_str:
|
||
if isinstance(parsed_data_str, str):
|
||
data = json.loads(parsed_data_str)
|
||
else:
|
||
data = parsed_data_str or {}
|
||
else:
|
||
data = {} # fallback si aucune donnée
|
||
|
||
importer = AssayImporter()
|
||
importer.import_from_json(data, assay)
|
||
logger.info("Updated assay:%s", assay.id)
|
||
|
||
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 get_element_grade(self, state, element):
|
||
if self.assays:
|
||
for assay in self.assays:
|
||
if assay.type == state:
|
||
for line in assay.lines:
|
||
if line.element == element:
|
||
return line.value
|
||
|
||
def check_pricing(self):
|
||
if self.terms and self.update_pricing:
|
||
Concentrate = Pool().get('concentrate.term')
|
||
for t in self.terms:
|
||
grade = self.get_element_grade(self.assay_state,t.element)
|
||
logger.info("GRADE:%s",grade)
|
||
if grade != None:
|
||
payable_price = Decimal(0)
|
||
penalty_price = Decimal(0)
|
||
if t.penalty_rules:
|
||
penalty_price = t.penalty_rules.compute_penalty(grade)
|
||
if t.component:
|
||
cp = [c for c in self.price_summary if c.price_component == t.component]
|
||
if cp:
|
||
cp = cp[0]
|
||
price = Decimal(cp.get_last_price())
|
||
logger.info("PRICE:%s",price)
|
||
if t.payable_rule:
|
||
payable_price = t.payable_rule.compute_payable_quantity(grade) * price / Decimal(100)
|
||
|
||
t.manual_price = round(payable_price - penalty_price,2)
|
||
t.currency = self.purchase.currency
|
||
t.unit = self.unit
|
||
Concentrate.save([t])
|
||
|
||
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:
|
||
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.line = self.id
|
||
p.pricing_date = d.date()
|
||
p.price_component = pc.id
|
||
p.quantity = round(Decimal(pc.quota),5)
|
||
price = round(Decimal(self.getnearprice(pl,d,'price')),4)
|
||
p.settl_price = price
|
||
if price > 0:
|
||
cumul_qt += pc.quota
|
||
p.fixed_qt = round(Decimal(cumul_qt),5)
|
||
p.fixed_qt_price = round(Decimal(self.getnearprice(pl,d,'avg')),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')),4)
|
||
p.unfixed_qt = round(base_quantity - Decimal(cumul_qt),5)
|
||
if p.unfixed_qt < 0.001:
|
||
p.unfixed_qt = Decimal(0)
|
||
p.fixed_qt = base_quantity
|
||
if price > 0:
|
||
p.unfixed_qt_price = price
|
||
else:
|
||
pr = Decimal(pc.get_price(
|
||
p.pricing_date, self.unit, self.purchase.currency, True))
|
||
pr = round(pr,4)
|
||
p.unfixed_qt_price = pr
|
||
p.eod_price = p.get_eod_price_purchase()
|
||
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
|
||
Pricing.save([p])
|
||
index += 1
|
||
|
||
# @classmethod
|
||
# def view_attributes(cls):
|
||
# return super().view_attributes() + [
|
||
# ('/tree/field[@name="quantity"]', 'visual',
|
||
# If(Eval('quantity') & (Eval('quantity', 0) > 0),'success','danger')),
|
||
# ]
|
||
|
||
class GoToBi(Wizard):
|
||
__name__ = 'purchase.bi'
|
||
start_state = 'bi'
|
||
bi = StateAction('purchase_trade.url_bi')
|
||
|
||
def do_bi(self, action):
|
||
Configuration = Pool().get('gr.configuration')
|
||
config = Configuration.search(['id','>',0])[0]
|
||
|
||
ct_number = self.records[0].number
|
||
action['url'] = config.bi + '/dashboard/6-pnl?lot=&product=&purchase='+ ct_number + '&sale='
|
||
return action, {}
|
||
|
||
class PurchaseAllocationsWizard(Wizard):
|
||
'Open Allocations report from Purchase without modal'
|
||
__name__ = 'purchase.allocations.wizard'
|
||
|
||
start_state = 'open_report'
|
||
|
||
open_report = StateAction('purchase_trade.act_lot_report_form')
|
||
|
||
def do_open_report(self, action):
|
||
purchase_id = Transaction().context.get('active_id')
|
||
if not purchase_id:
|
||
raise ValueError("No active purchase ID in context")
|
||
action['context_model'] = 'lot.context'
|
||
action['pyson_context'] = PYSONEncoder().encode({
|
||
'purchase': purchase_id,
|
||
})
|
||
return action, {}
|
||
|
||
|
||
class PurchasePnlGraph(ModelSQL, ModelView):
|
||
"Purchase Pnl graph"
|
||
__name__ = 'purchase.pnl.graph'
|
||
|
||
purchase = fields.Many2One('purchase.purchase', "Purchase")
|
||
valuation_date = fields.Date("Valuation Date")
|
||
pnl = fields.Numeric("Pnl", digits=(16, 2))
|
||
mtm = fields.Numeric("Mtm", digits=(16, 2))
|
||
|
||
@classmethod
|
||
def __setup__(cls):
|
||
super().__setup__()
|
||
cls._order.insert(0, ('valuation_date', 'ASC'))
|
||
|
||
@classmethod
|
||
def table_query(cls):
|
||
Valuation = Pool().get('valuation.valuation')
|
||
PurchaseLine = Pool().get('purchase.line')
|
||
val = Valuation.__table__()
|
||
line = PurchaseLine.__table__()
|
||
purchase = Coalesce(val.purchase, line.purchase)
|
||
return val.join(
|
||
line, 'LEFT', condition=val.line == line.id).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'),
|
||
purchase.as_('purchase'),
|
||
val.date.as_('valuation_date'),
|
||
Sum(Coalesce(val.pnl, 0)).as_('pnl'),
|
||
Sum(Coalesce(val.mtm, 0)).as_('mtm'),
|
||
where=(purchase != None),
|
||
group_by=[purchase, val.date],
|
||
order_by=[val.date.asc])
|
||
|
||
|
||
class PurchaseInvoiceReport(
|
||
ModelSQL, ModelView):
|
||
"Purchase invoices"
|
||
__name__ = 'purchase.invoice.report'
|
||
r_supplier = fields.Many2One('party.party',"Supplier")
|
||
r_purchase = fields.Many2One('purchase.purchase', "Purchase")
|
||
r_line = fields.Many2One('purchase.line',"Line")
|
||
r_lot = fields.Many2One('lot.lot',"Lot")
|
||
r_product = fields.Many2One('product.product', "Product")
|
||
r_pur_invoice = fields.Many2One('account.invoice',"Invoice")
|
||
r_inv_date = fields.Date("Inv. date")
|
||
r_pur_payment = fields.Many2Many('account.invoice-account.move.line','invoice', 'line', string='Payment')
|
||
r_invoice_amount = fields.Numeric("Amount",digits=(1,2))
|
||
r_payment_amount = fields.Numeric("Paid",digits=(1,2))
|
||
r_left_amount = fields.Numeric("Left",digits=(1,2))
|
||
r_curr = fields.Many2One('currency.currency',"Curr")
|
||
r_reconciliation = fields.Integer("Reconciliation")
|
||
r_move = fields.Many2One('account.move',"Move")
|
||
r_status = fields.Selection([
|
||
('not', 'Not'),
|
||
('paid', 'Paid'),
|
||
('partial', 'Partially'),
|
||
], 'Status')
|
||
|
||
@classmethod
|
||
def table_query(cls):
|
||
pool = Pool()
|
||
Invoice = pool.get('account.invoice')
|
||
InvoiceLine = pool.get('account.invoice.line')
|
||
PurchaseLine = pool.get('purchase.line')
|
||
Purchase = pool.get('purchase.purchase')
|
||
Party = pool.get('party.party')
|
||
InvoicePayment = pool.get('account.invoice-account.move.line')
|
||
MoveLine = pool.get('account.move.line')
|
||
Move = pool.get('account.move')
|
||
Currency = pool.get('currency.currency')
|
||
Lot = pool.get('lot.lot')
|
||
|
||
ai = Invoice.__table__()
|
||
ail = InvoiceLine.__table__()
|
||
pl = PurchaseLine.__table__()
|
||
pu = Purchase.__table__()
|
||
pa = Party.__table__()
|
||
aiaml = InvoicePayment.__table__()
|
||
aml = MoveLine.__table__()
|
||
lot = Lot.__table__()
|
||
cu = Currency.__table__()
|
||
mo = Move.__table__()
|
||
|
||
context = Transaction().context
|
||
supplier = context.get('supplier')
|
||
purchase = context.get('purchase')
|
||
asof = context.get('asof')
|
||
todate = context.get('todate')
|
||
state = context.get('state')
|
||
|
||
wh = Literal(True)
|
||
wh &= lot.lot_type == 'physic'
|
||
wh &= ai.type == 'in'
|
||
if supplier:
|
||
wh &= (ai.party == supplier)
|
||
if purchase:
|
||
wh &= (pu.id == purchase)
|
||
if asof and todate:
|
||
wh &= (ai.invoice_date >= asof) & (ai.invoice_date <= todate)
|
||
|
||
query = (
|
||
lot
|
||
.join(ail, 'LEFT', condition=ail.lot == lot.id)
|
||
.join(ai, 'LEFT', condition=ai.id == ail.invoice)
|
||
.join(pl, 'LEFT', condition=pl.id == lot.line)
|
||
.join(pu, 'LEFT', condition=pl.purchase == pu.id)
|
||
.join(pa, 'LEFT', condition=ai.party == pa.id)
|
||
.join(cu, 'LEFT', condition=cu.id == ail.currency)
|
||
.select(
|
||
Literal(0).as_('create_uid'),
|
||
CurrentTimestamp().as_('create_date'),
|
||
Literal(0).as_('write_uid'),
|
||
Literal(0).as_('write_date'),
|
||
Max(ail.id).as_('id'),
|
||
Max(pa.id).as_('r_supplier'),
|
||
pu.id.as_('r_purchase'),
|
||
pl.id.as_('r_line'),
|
||
Max(ail.product).as_('r_product'),
|
||
Max(lot.id).as_('r_lot'),
|
||
ai.id.as_('r_pur_invoice'),
|
||
Max(ai.invoice_date).as_('r_inv_date'),
|
||
Sum(ail.quantity*ail.unit_price).as_('r_invoice_amount'),
|
||
Max(cu.id).as_('r_curr'),
|
||
where=wh,
|
||
group_by=[pu.id,pl.id,ai.id]
|
||
)
|
||
)
|
||
query_alias = query
|
||
left = Case((Abs(Sum(aml.amount_second_currency))>0,(Max(query_alias.r_invoice_amount)-Sum(aml.amount_second_currency))),else_=(Max(query_alias.r_invoice_amount)-(Sum(aml.debit)-Sum(aml.credit))))
|
||
status = Case((left==0, 'paid'),else_=Case((left<Max(query_alias.r_invoice_amount),'partial'),else_='not'))
|
||
wh = Literal(True)
|
||
if state != 'all':
|
||
wh &= status == state
|
||
|
||
query2 = (
|
||
query_alias
|
||
.join(aiaml, 'LEFT', condition=(aiaml.invoice == query_alias.r_pur_invoice))
|
||
.join(aml, 'LEFT', condition=aml.id == aiaml.line)
|
||
.join(mo, 'LEFT', condition=mo.id == aml.move)
|
||
.select(
|
||
Literal(0).as_('create_uid'),
|
||
CurrentTimestamp().as_('create_date'),
|
||
Literal(0).as_('write_uid'),
|
||
Literal(0).as_('write_date'),
|
||
Max(query_alias.id).as_('id'),
|
||
Max(query_alias.r_supplier).as_('r_supplier'),
|
||
query_alias.r_purchase.as_('r_purchase'),
|
||
query_alias.r_line.as_('r_line'),
|
||
Max(query_alias.r_product).as_('r_product'),
|
||
Max(query_alias.r_lot).as_('r_lot'),
|
||
query_alias.r_pur_invoice.as_('r_pur_invoice'),
|
||
Max(query_alias.r_inv_date).as_('r_inv_date'),
|
||
Max(query_alias.r_invoice_amount).as_('r_invoice_amount'),
|
||
Sum(aml.amount_second_currency).as_('r_payment_amount'),
|
||
left.as_('r_left_amount'),
|
||
status.as_('r_status'),
|
||
Max(query_alias.r_curr).as_('r_curr'),
|
||
Max(aml.reconciliation).as_('r_reconciliation'),
|
||
Max(mo.id).as_('r_move'),
|
||
Max(aml.id).as_('r_pur_payment'),
|
||
where=wh,
|
||
group_by=[query_alias.r_purchase,query_alias.r_line,query_alias.r_pur_invoice]
|
||
))
|
||
|
||
return query2
|
||
|
||
class PurchaseInvoiceContext(ModelView):
|
||
"Purchase Invoice Context"
|
||
__name__ = 'purchase.invoice.context'
|
||
|
||
asof = fields.Date("As of")
|
||
todate = fields.Date("To")
|
||
supplier = fields.Many2One('party.party',"Supplier")
|
||
purchase = fields.Many2One('purchase.purchase', "Purchase")
|
||
state = fields.Selection([
|
||
('all', 'All'),
|
||
('not', 'Not'),
|
||
('paid', 'Paid'),
|
||
('partial', 'Partially')
|
||
], 'State')
|
||
|
||
@classmethod
|
||
def default_asof(cls):
|
||
pool = Pool()
|
||
Date = pool.get('ir.date')
|
||
return Date.today().replace(day=1,month=1,year=1999)
|
||
|
||
@classmethod
|
||
def default_todate(cls):
|
||
pool = Pool()
|
||
Date = pool.get('ir.date')
|
||
return Date.today()
|
||
|
||
@classmethod
|
||
def default_state(cls):
|
||
return 'all'
|
||
|
||
class InvoicePayment(Wizard):
|
||
'Payments'
|
||
__name__ = 'purchase.invoice.payment'
|
||
start_state = 'open_'
|
||
open_ = StateAction('account.act_move_line_form')
|
||
|
||
def do_open_(self, action):
|
||
if self.record.r_pur_invoice:
|
||
Payment = Pool().get('account.invoice-account.move.line')
|
||
payments = Payment.search(['invoice','=',self.record.r_pur_invoice.id])
|
||
if payments:
|
||
lines = [e.line.id for e in payments]
|
||
return action, {'res_id': lines}
|
||
|
||
class PnlReport(Wizard):
|
||
'Pnl report'
|
||
__name__ = 'pnl.report'
|
||
|
||
start = StateAction('purchase_trade.act_pnl_bi')
|
||
|
||
def do_start(self, action):
|
||
pool = Pool()
|
||
# action['views'].reverse()
|
||
return action, {'res_id': [1]}
|
||
|
||
class PnlBI(ModelSingleton,ModelSQL, ModelView):
|
||
'Pnl BI'
|
||
__name__ = 'pnl.bi'
|
||
|
||
input = fields.Text("BI")
|
||
metabase = fields.Function(fields.Text(""),'get_bi')
|
||
|
||
def get_bi(self,name=None):
|
||
Configuration = Pool().get('gr.configuration')
|
||
config = Configuration.search(['id','>',0])[0]
|
||
|
||
payload = {
|
||
"resource": {"dashboard": config.pnl_id},
|
||
"params": {},
|
||
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30),
|
||
}
|
||
token = jwt.encode(payload, config.payload, algorithm="HS256")
|
||
logger.info("TOKEN:%s",token)
|
||
if config.dark:
|
||
url = f"metabase:{config.bi}/embed/dashboard/{token}#theme=night&bordered=true&titled=true"
|
||
else:
|
||
url = f"metabase:{config.bi}/embed/dashboard/{token}#bordered=true&titled=true"
|
||
return url
|
||
|
||
class PositionReport(Wizard):
|
||
'Position report'
|
||
__name__ = 'position.report'
|
||
|
||
start = StateAction('purchase_trade.act_position_bi')
|
||
|
||
def do_start(self, action):
|
||
pool = Pool()
|
||
# action['views'].reverse()
|
||
return action, {'res_id': [1]}
|
||
|
||
class PositionBI(ModelSingleton,ModelSQL, ModelView):
|
||
'Position BI'
|
||
__name__ = 'position.bi'
|
||
|
||
input = fields.Text("BI")
|
||
metabase = fields.Function(fields.Text(""),'get_bi')
|
||
|
||
def get_bi(self,name=None):
|
||
Configuration = Pool().get('gr.configuration')
|
||
config = Configuration.search(['id','>',0])[0]
|
||
|
||
payload = {
|
||
"resource": {"dashboard": config.position_id},
|
||
"params": {},
|
||
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30),
|
||
}
|
||
token = jwt.encode(payload, config.payload, algorithm="HS256")
|
||
logger.info("TOKEN:%s",token)
|
||
if config.dark:
|
||
url = f"metabase:{config.bi}/embed/dashboard/{token}#theme=night&bordered=true&titled=true"
|
||
else:
|
||
url = f"metabase:{config.bi}/embed/dashboard/{token}#bordered=true&titled=true"
|
||
return url
|