2050 lines
78 KiB
Python
Executable File
2050 lines
78 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, 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
|
||
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 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
|
||
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")
|
||
|
||
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()
|
||
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)) * Max(pc.ratio / 100)).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'
|
||
|
||
btb = fields.Many2One('back.to.back',"Back to back")
|
||
bank_accounts = fields.Function(
|
||
fields.Many2Many('bank.account', None, None, "Bank Accounts"),
|
||
'on_change_with_bank_accounts')
|
||
bank_account = fields.Many2One(
|
||
'bank.account', "Bank Account",
|
||
domain=[('id', 'in', Eval('bank_accounts', []))],
|
||
depends=['bank_accounts'])
|
||
from_location = fields.Many2One('stock.location', 'From location', required=True,domain=[('type', "!=", 'customer')])
|
||
to_location = fields.Many2One('stock.location', 'To location', required=True,domain=[('type', "!=", 'supplier')])
|
||
shipment_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 +")
|
||
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'),})
|
||
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')
|
||
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'
|
||
)
|
||
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")
|
||
|
||
@fields.depends('company', '_parent_company.party')
|
||
def on_change_with_company_visible(self, name=None):
|
||
return bool(
|
||
self.company and self.company.party
|
||
and self.company.party.name == 'MELYA')
|
||
|
||
def _get_default_bank_account(self):
|
||
if not self.party or not self.party.bank_accounts:
|
||
return None
|
||
party_bank_accounts = list(self.party.bank_accounts)
|
||
if self.currency:
|
||
for account in party_bank_accounts:
|
||
if account.currency == self.currency:
|
||
return account
|
||
return party_bank_accounts[0]
|
||
|
||
@fields.depends('party', '_parent_party.bank_accounts')
|
||
def on_change_with_bank_accounts(self, name=None):
|
||
if self.party and self.party.bank_accounts:
|
||
return [account.id for account in self.party.bank_accounts]
|
||
return []
|
||
|
||
@fields.depends(
|
||
'company', 'party', 'invoice_party', 'payment_term', 'lines',
|
||
'bank_account', '_parent_party.bank_accounts')
|
||
def on_change_party(self):
|
||
super().on_change_party()
|
||
self.bank_account = self._get_default_bank_account()
|
||
|
||
@fields.depends('party', 'currency', '_parent_party.bank_accounts')
|
||
def on_change_currency(self):
|
||
self.bank_account = self._get_default_bank_account()
|
||
|
||
@classmethod
|
||
def default_wb(cls):
|
||
WB = Pool().get('purchase.weight.basis')
|
||
wb = WB.search(['id','>',0])
|
||
if wb:
|
||
return wb[0].id
|
||
|
||
@classmethod
|
||
def default_certif(cls):
|
||
Certification = Pool().get('purchase.certification')
|
||
certification = Certification.search(['id','>',0])
|
||
if certification:
|
||
return certification[0].id
|
||
|
||
@classmethod
|
||
def default_association(cls):
|
||
Association = Pool().get('purchase.association')
|
||
association = Association.search(['id','>',0])
|
||
if association:
|
||
return association[0].id
|
||
|
||
@classmethod
|
||
def default_tol_min(cls):
|
||
return 0
|
||
|
||
@classmethod
|
||
def default_tol_max(cls):
|
||
return 0
|
||
|
||
@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 ''
|
||
|
||
@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 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 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])
|
||
|
||
class PriceComposition(ModelSQL,ModelView):
|
||
"Price Composition"
|
||
__name__ = 'price.composition'
|
||
|
||
line = fields.Many2One('purchase.line',"Purchase line")
|
||
component = fields.Char("Component")
|
||
price = fields.Numeric("Price")
|
||
|
||
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 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 ''
|
||
|
||
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',"Delivery 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")
|
||
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')
|
||
# 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.Numeric("Premium/Discount",digits='unit')
|
||
fee_ = fields.Many2One('fee.fee',"Fee")
|
||
pricing_rule = fields.Text("Pricing description")
|
||
|
||
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')
|
||
def on_change_with_attributes_name(self, name=None):
|
||
if not self.product or not self.product.attribute_set or not self.attributes:
|
||
return
|
||
|
||
def key(attribute):
|
||
return getattr(attribute, 'sequence', attribute.name)
|
||
|
||
values = []
|
||
for attribute in sorted(self.product.attribute_set.attributes, key=key):
|
||
if attribute.name in self.attributes:
|
||
value = self.attributes[attribute.name]
|
||
values.append(gettext(
|
||
'product_attribute.msg_label_value',
|
||
label=attribute.string,
|
||
value=attribute.format(value)
|
||
))
|
||
return " | ".join(filter(None, values))
|
||
|
||
@classmethod
|
||
def default_price_type(cls):
|
||
return 'priced'
|
||
|
||
@classmethod
|
||
def default_inherit_tol(cls):
|
||
return True
|
||
|
||
@classmethod
|
||
def default_enable_linked_currency(cls):
|
||
return False
|
||
|
||
@classmethod
|
||
def default_inherit_cer(cls):
|
||
return True
|
||
|
||
def get_matched_lines(self):
|
||
if self.lots:
|
||
LotQt = Pool().get('lot.qt')
|
||
return LotQt.search([('lot_p','=',self.lots[0].id),('lot_s','>',0)])
|
||
|
||
def get_date(self,trigger_event):
|
||
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_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 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_price(self):
|
||
premium = Decimal(self.premium or 0)
|
||
if not premium:
|
||
return Decimal(0)
|
||
if self.enable_linked_currency and self.linked_currency:
|
||
return self._linked_to_line_price(premium)
|
||
return premium
|
||
|
||
def _get_basis_component_price(self):
|
||
price = Decimal(0)
|
||
if self.terms:
|
||
for t in self.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')
|
||
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', 'unit_price', 'unit', 'product',
|
||
'purchase', '_parent_purchase.currency',
|
||
'premium', 'enable_linked_currency', 'linked_currency', 'linked_unit')
|
||
def on_change_with_amount(self):
|
||
if (self.type == 'line'
|
||
and self.quantity is not None
|
||
and self.unit_price is not None):
|
||
currency = self.purchase.currency if self.purchase else None
|
||
amount = Decimal(str(self.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',
|
||
methods=['on_change_with_unit_price', 'on_change_with_amount'])
|
||
def _recompute_trade_price_fields(self):
|
||
self.unit_price = self.on_change_with_unit_price()
|
||
self.amount = self.on_change_with_amount()
|
||
|
||
@fields.depends(methods=['_recompute_trade_price_fields'])
|
||
def on_change_premium(self):
|
||
self._recompute_trade_price_fields()
|
||
|
||
@fields.depends(methods=['_recompute_trade_price_fields'])
|
||
def on_change_price_type(self):
|
||
self._recompute_trade_price_fields()
|
||
|
||
@fields.depends(methods=['_recompute_trade_price_fields'])
|
||
def on_change_enable_linked_currency(self):
|
||
self._recompute_trade_price_fields()
|
||
|
||
@fields.depends(methods=['_recompute_trade_price_fields'])
|
||
def on_change_linked_price(self):
|
||
self._recompute_trade_price_fields()
|
||
|
||
@fields.depends(methods=['_recompute_trade_price_fields'])
|
||
def on_change_linked_currency(self):
|
||
self._recompute_trade_price_fields()
|
||
|
||
@fields.depends(methods=['_recompute_trade_price_fields'])
|
||
def on_change_linked_unit(self):
|
||
self._recompute_trade_price_fields()
|
||
|
||
@classmethod
|
||
def write(cls, *args):
|
||
# 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'
|
||
Lot = Pool().get('lot.lot')
|
||
LotQt = Pool().get('lot.qt')
|
||
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
|
||
|
||
super().write(*args)
|
||
|
||
lines = sum(args[::2], [])
|
||
for line in lines:
|
||
if line.id in old_values:
|
||
old = Decimal(old_values[line.id] or 0)
|
||
new = Decimal(line.quantity_theorical or 0)
|
||
delta = new - old
|
||
if delta > 0:
|
||
virtual_lots = [lot for lot in (line.lots or []) if lot.lot_type == 'virtual']
|
||
if not virtual_lots:
|
||
continue
|
||
vlot = virtual_lots[0]
|
||
new_qty = round(Decimal(vlot.get_current_quantity_converted() or 0) + delta, 5)
|
||
vlot.set_current_quantity(new_qty, new_qty, 1)
|
||
Lot.save([vlot])
|
||
lqts = LotQt.search([
|
||
('lot_p', '=', vlot.id),
|
||
('lot_s', '=', None),
|
||
('lot_shipment_in', '=', None),
|
||
('lot_shipment_internal', '=', None),
|
||
('lot_shipment_out', '=', None),
|
||
])
|
||
if lqts:
|
||
lqt = lqts[0]
|
||
lqt.lot_quantity = round(Decimal(lqt.lot_quantity or 0) + delta, 5)
|
||
LotQt.save([lqt])
|
||
else:
|
||
lqt = LotQt()
|
||
lqt.lot_p = vlot.id
|
||
lqt.lot_s = None
|
||
lqt.lot_quantity = round(delta, 5)
|
||
lqt.lot_unit = line.unit
|
||
LotQt.save([lqt])
|
||
elif delta < 0:
|
||
virtual_lots = [lot for lot in (line.lots or []) if lot.lot_type == 'virtual']
|
||
if not virtual_lots:
|
||
continue
|
||
vlot = virtual_lots[0]
|
||
decrease = abs(delta)
|
||
lqts = LotQt.search([
|
||
('lot_p', '=', vlot.id),
|
||
('lot_s', '=', None),
|
||
('lot_shipment_in', '=', None),
|
||
('lot_shipment_internal', '=', None),
|
||
('lot_shipment_out', '=', None),
|
||
])
|
||
if (not lqts
|
||
or Decimal(lqts[0].lot_quantity or 0) < decrease):
|
||
raise UserError("Please unlink or unmatch lot")
|
||
new_qty = round(Decimal(vlot.get_current_quantity_converted() or 0) - decrease, 5)
|
||
vlot.set_current_quantity(new_qty, new_qty, 1)
|
||
Lot.save([vlot])
|
||
lqt = lqts[0]
|
||
lqt.lot_quantity = round(Decimal(lqt.lot_quantity or 0) - decrease, 5)
|
||
LotQt.save([lqt])
|
||
|
||
@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 line.price_components:
|
||
for pc in line.price_components:
|
||
if pc.triggers:
|
||
for tr in pc.triggers:
|
||
line.check_from_to(tr)
|
||
line.check_pricing()
|
||
#no lot need to create one with line quantity
|
||
if not line.created_by_code:
|
||
if not line.lots and line.product.type != 'service' and line.quantity != 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(line.quantity),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 line.quantity > 0:
|
||
Lot.save([lot])
|
||
#check if fees need to be updated
|
||
if line.fees:
|
||
for fee in line.fees:
|
||
fl_check = FeeLots.search([('fee','=',fee.id),('lot','=',lot.id),('line','=',line.id)])
|
||
if not fl_check:
|
||
fl = FeeLots()
|
||
fl.fee = fee.id
|
||
fl.lot = lot.id
|
||
fl.line = line.id
|
||
FeeLots.save([fl])
|
||
|
||
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:
|
||
cumul_qt = Decimal(0)
|
||
base_quantity = self._get_pricing_base_quantity()
|
||
index = 0
|
||
for pr in pricings:
|
||
cumul_qt += pr.quantity
|
||
pr.fixed_qt = cumul_qt
|
||
pr.fixed_qt_price = pr.get_fixed_price()
|
||
pr.unfixed_qt = base_quantity - pr.fixed_qt
|
||
pr.unfixed_qt_price = pr.fixed_qt_price
|
||
pr.eod_price = pr.get_eod_price_purchase()
|
||
if index == len(pricings) - 1:
|
||
pr.last = True
|
||
index += 1
|
||
Pricing.save([pr])
|
||
|
||
if pc.triggers and pc.auto:
|
||
prDate = []
|
||
prPrice = []
|
||
apDate = []
|
||
apPrice = []
|
||
for t in pc.triggers:
|
||
prD, prP = t.getPricingListDates(pc.calendar)
|
||
apD, apP = t.getApplicationListDates(pc.calendar)
|
||
prDate.extend(prD)
|
||
prPrice.extend(prP)
|
||
apDate.extend(apD)
|
||
apPrice.extend(apP)
|
||
if pc.quota:
|
||
prPrice = self.get_avg(prPrice)
|
||
self.generate_pricing(pc,apDate,prPrice)
|
||
|
||
def get_avg(self,lprice):
|
||
l = len(lprice)
|
||
if l > 0 :
|
||
cumulprice = float(0)
|
||
i = 1
|
||
for p in lprice:
|
||
if i > 1:
|
||
p['avg_minus_1'] = cumulprice / (i-1)
|
||
cumulprice += p['price']
|
||
p['avg'] = cumulprice / i
|
||
i += 1
|
||
return lprice
|
||
|
||
def getnearprice(self,pl,d,t,max_date=None):
|
||
if pl:
|
||
pl_sorted = sorted(pl, key=lambda x: x['date'])
|
||
pminus = pl_sorted[0]
|
||
if not max_date:
|
||
max_date = d.date()
|
||
for p in pl_sorted:
|
||
if p['date'].date() == d.date():
|
||
if p['isAvg'] and t == 'avg':
|
||
return p[t]
|
||
if not p['isAvg'] and t == 'avg':
|
||
return p['price']
|
||
elif p['date'].date() > d.date():
|
||
if pminus != p:
|
||
return pminus[t]
|
||
else:
|
||
return Decimal(0)
|
||
pminus = p
|
||
return pl_sorted[len(pl)-1][t]
|
||
return Decimal(0)
|
||
|
||
def _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.price_index.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 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
|