1966 lines
79 KiB
Python
Executable File
1966 lines
79 KiB
Python
Executable File
from functools import wraps
|
|
from trytond.model import fields
|
|
from trytond.report import Report
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.pyson import Bool, Eval, Id, If
|
|
from trytond.model import (ModelSQL, ModelView)
|
|
from trytond.tools import is_full_text, lstrip_wildcard
|
|
from trytond.transaction import Transaction, inactive_records
|
|
from decimal import getcontext, Decimal, ROUND_UP, ROUND_HALF_UP
|
|
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
|
|
from sql.conditionals import Case, Coalesce
|
|
from sql import Column, Literal, Null
|
|
from sql.functions import CurrentTimestamp, DateTrunc
|
|
from trytond.wizard import Button, StateTransition, StateView, Wizard
|
|
from itertools import chain, groupby
|
|
from operator import itemgetter
|
|
import datetime
|
|
import logging
|
|
from collections import defaultdict
|
|
from trytond.exceptions import UserWarning, UserError
|
|
from trytond.modules.account.exceptions import PeriodNotFoundError
|
|
from trytond.modules.purchase_trade.company_defaults import (
|
|
default_itsa_currency, default_itsa_unit, is_itsa_company)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
def filter_state(state):
|
|
def filter(func):
|
|
@wraps(func)
|
|
def wrapper(cls, fees):
|
|
fees = [f for f in fees if f.state == state]
|
|
return func(cls, fees)
|
|
return wrapper
|
|
return filter
|
|
|
|
class Fee(ModelSQL,ModelView):
|
|
"Fee"
|
|
__name__ = 'fee.fee'
|
|
|
|
line = fields.Many2One('purchase.line',"Line", ondelete='CASCADE')
|
|
source_rule = fields.Many2One('fee.rule', "Source Rule", readonly=True)
|
|
generated_by_rule = fields.Boolean("Generated by Rule", readonly=True)
|
|
shipment_in = fields.Many2One('stock.shipment.in', ondelete='CASCADE')
|
|
shipment_out = fields.Many2One('stock.shipment.out', ondelete='CASCADE')
|
|
shipment_internal = fields.Many2One(
|
|
'stock.shipment.internal', ondelete='CASCADE')
|
|
currency = fields.Many2One('currency.currency',"Currency", states={
|
|
'required': ~Eval('enable_linked_currency'),
|
|
}, depends=['enable_linked_currency'])
|
|
supplier = fields.Many2One('party.party',"Supplier", required=True)
|
|
type = fields.Selection([
|
|
('budgeted', 'Budgeted'),
|
|
('ordered', 'Ordered'),
|
|
('actual', 'Actual'),
|
|
], "Type", required=True)
|
|
p_r = fields.Selection([
|
|
('pay', 'PAY'),
|
|
('rec', 'REC'),
|
|
], "P/R", required=True)
|
|
product = fields.Many2One('product.product',"Product", required=True, domain=[('type', '=', 'service')])
|
|
price = fields.Numeric("Price",digits=(1,4))
|
|
enable_linked_currency = fields.Boolean("Linked curr")
|
|
linked_price = fields.Numeric("Linked Price", digits='unit', states={
|
|
'invisible': ~Eval('enable_linked_currency'),
|
|
}, depends=['enable_linked_currency'])
|
|
linked_currency = fields.Many2One('currency.linked', "Linked curr",
|
|
states={
|
|
'invisible': ~Eval('enable_linked_currency'),
|
|
}, depends=['enable_linked_currency'])
|
|
linked_unit = fields.Many2One('product.uom', "Linked Unit",
|
|
states={
|
|
'invisible': ~Eval('enable_linked_currency'),
|
|
}, depends=['enable_linked_currency'])
|
|
mode = fields.Selection([
|
|
('lumpsum', 'Lump sum'),
|
|
('perqt', 'Per qt'),
|
|
('pprice', '% price'),
|
|
('rate', '% rate'),
|
|
('pcost', '% cost price'),
|
|
('ppack', 'Per packing'),
|
|
], 'Mode', required=True)
|
|
auto_calculation = fields.Boolean("Auto",states={'readonly': (Eval('mode') != 'ppack')})
|
|
inherit_qt = fields.Boolean("Inh Qt",states={'readonly': Eval('mode') != 'ppack'})
|
|
quantity = fields.Numeric("Qt",digits='unit',states={'readonly': (Eval('mode') != 'ppack') | Bool(Eval('auto_calculation'))})
|
|
add_padding = fields.Boolean("Add padding")
|
|
unit = fields.Many2One('product.uom',"Unit",domain=[
|
|
If(Eval('mode') == 'ppack',
|
|
('category', '=', Eval('packing_category')),
|
|
()),
|
|
],
|
|
states={
|
|
'readonly': (Bool(Eval('mode') != 'ppack') & Bool(Eval('mode') != 'perqt')),
|
|
},
|
|
depends=['mode', 'packing_category'])
|
|
packing_category = fields.Function(fields.Many2One('product.uom.category',"Packing Category"),'on_change_with_packing_category')
|
|
inherit_shipment = fields.Boolean("Inh Sh",states={
|
|
'invisible': (Eval('shipment_in')),
|
|
})
|
|
purchase = fields.Many2One('purchase.purchase',"Purchase", ondelete='CASCADE')
|
|
qt_state = fields.Many2One('lot.qt.type',"Qt State")
|
|
amount = fields.Function(fields.Numeric("Amount", digits='currency'),'get_amount')
|
|
fee_lots = fields.Function(fields.Many2Many('lot.lot', None, None, "Lots"),'get_lots')#, searcher='search_lots')
|
|
lots = fields.Many2Many('fee.lots', 'fee', 'lot',"Lots",domain=[('id', 'in', Eval('fee_lots',-1))] )
|
|
lots_cp = fields.Integer("Lots number")
|
|
|
|
state = fields.Selection([
|
|
('not invoiced', 'Not invoiced'),
|
|
('invoiced', 'Invoiced'),
|
|
], string='State', readonly=True)
|
|
|
|
fee_landed_cost = fields.Function(fields.Boolean("Inventory"),'get_landed_status')
|
|
inv = fields.Function(fields.Many2One('account.invoice',"Invoice"),'get_invoice')
|
|
dn_cn = fields.Many2One('account.invoice', "DN/CN", readonly=True)
|
|
|
|
weight_type = fields.Selection([
|
|
('net', 'Net'),
|
|
('brut', 'Gross'),
|
|
], string='W. type')
|
|
|
|
fee_date = fields.Date("Date")
|
|
|
|
@classmethod
|
|
def default_fee_date(cls):
|
|
Date = Pool().get('ir.date')
|
|
return Date.today()
|
|
|
|
@classmethod
|
|
def default_currency(cls):
|
|
if is_itsa_company():
|
|
currency = default_itsa_currency()
|
|
if currency:
|
|
return currency
|
|
|
|
@classmethod
|
|
def default_unit(cls):
|
|
if is_itsa_company():
|
|
unit = default_itsa_unit()
|
|
if unit:
|
|
return unit
|
|
|
|
@classmethod
|
|
def default_generated_by_rule(cls):
|
|
return False
|
|
|
|
@classmethod
|
|
def default_add_padding(cls):
|
|
return False
|
|
|
|
@classmethod
|
|
def default_qt_state(cls):
|
|
return None
|
|
|
|
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:
|
|
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_fee_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 _fee_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 _sync_price_from_linked(self):
|
|
if (self.enable_linked_currency and self.linked_currency
|
|
and self.linked_price is not None):
|
|
self.price = self._linked_to_fee_price(self.linked_price)
|
|
|
|
@fields.depends(
|
|
'enable_linked_currency', 'linked_currency', 'linked_unit',
|
|
'linked_price', 'price', 'unit')
|
|
def on_change_enable_linked_currency(self):
|
|
if self.enable_linked_currency and self.price is not None:
|
|
self.linked_price = self._fee_to_linked_price(self.price)
|
|
self._sync_price_from_linked()
|
|
|
|
@fields.depends(
|
|
'enable_linked_currency', 'linked_currency', 'linked_unit',
|
|
'linked_price', 'unit')
|
|
def on_change_linked_price(self):
|
|
self._sync_price_from_linked()
|
|
|
|
@fields.depends(
|
|
'enable_linked_currency', 'linked_currency', 'linked_unit',
|
|
'linked_price', 'unit')
|
|
def on_change_linked_currency(self):
|
|
self._sync_price_from_linked()
|
|
|
|
@fields.depends(
|
|
'enable_linked_currency', 'linked_currency', 'linked_unit',
|
|
'linked_price', 'unit')
|
|
def on_change_linked_unit(self):
|
|
self._sync_price_from_linked()
|
|
|
|
@fields.depends(
|
|
'enable_linked_currency', 'linked_currency', 'linked_unit',
|
|
'price', 'unit')
|
|
def on_change_price(self):
|
|
if (self.enable_linked_currency and self.linked_currency
|
|
and self.price is not None):
|
|
self.linked_price = self._fee_to_linked_price(self.price)
|
|
|
|
@fields.depends('mode','unit')
|
|
def on_change_with_packing_category(self, name=None):
|
|
UnitCategory = Pool().get('product.uom.category')
|
|
packing = UnitCategory.search(['name','=','Packing'])
|
|
if packing:
|
|
return packing[0]
|
|
|
|
@fields.depends('line','sale_line','shipment_in','lots','price','unit','auto_calculation','mode','_parent_line.unit','_parent_line.lots','_parent_sale_line.unit','_parent_sale_line.lots','_parent_shipment_in.id')
|
|
def on_change_with_quantity(self, name=None):
|
|
qt = None
|
|
unit = None
|
|
line = self.line
|
|
logger.info("ON_CHANGE_WITH_LINE:%s",line)
|
|
if not line:
|
|
line = self.sale_line
|
|
if line:
|
|
if line.lots:
|
|
qt = sum([e.get_current_quantity_converted(0,self.unit) for e in line.lots])
|
|
qt_ = sum([e.get_current_quantity_converted(0) for e in line.lots])
|
|
unit = line.lots[0].lot_unit or line.lots[0].lot_unit_line
|
|
logger.info("ON_CHANGE_WITH_QT0:%s",qt)
|
|
logger.info("ON_CHANGE_WITH_SI:%s",self.shipment_in)
|
|
if self.shipment_in:
|
|
Lot = Pool().get('lot.lot')
|
|
lots = Lot.search([('lot_shipment_in','=',self.shipment_in.id)])
|
|
logger.info("ON_CHANGE_WITH_LOTS:%s",lots)
|
|
if lots:
|
|
qt = sum([e.get_current_quantity_converted(0,self.unit) for e in lots])
|
|
qt_ = sum([e.get_current_quantity_converted(0) for e in lots])
|
|
unit = lots[0].lot_unit or lots[0].lot_unit_line
|
|
if not qt:
|
|
logger.info("ON_CHANGE_WITH_QT1:%s",qt)
|
|
LotQt = Pool().get('lot.qt')
|
|
if self.shipment_in:
|
|
lqts = LotQt.search(['lot_shipment_in','=',self.shipment_in.id])
|
|
if lqts:
|
|
qt = Decimal(lqts[0].lot_quantity)
|
|
qt_ = qt
|
|
unit = lqts[0].lot_unit
|
|
logger.info("ON_CHANGE_WITH_QT2:%s",qt)
|
|
if self.mode != 'ppack':
|
|
return qt
|
|
else:
|
|
if self.auto_calculation:
|
|
logger.info("AUTOCALCULATION:%s",qt)
|
|
logger.info("AUTOCALCULATION2:%s",qt_)
|
|
logger.info("AUTOCALCULATION3:%s",Decimal(unit.factor))
|
|
logger.info("AUTOCALCULATION4:%s",Decimal(self.unit.factor))
|
|
return (qt_ * Decimal(unit.factor) / Decimal(self.unit.factor)).to_integral_value(rounding=ROUND_UP)
|
|
|
|
@fields.depends('price','mode','_parent_line.lots','_parent_sale_line.lots','shipment_in')
|
|
def on_change_with_unit(self, name=None):
|
|
if self.mode != 'ppack' and self.mode != 'perqt':
|
|
line = self.line
|
|
if not line:
|
|
line = self.sale_line
|
|
if line:
|
|
if line.lots:
|
|
if len(line.lots) == 1:
|
|
return line.lots[0].lot_unit_line
|
|
else:
|
|
return line.lots[1].lot_unit_line
|
|
if self.shipment_in:
|
|
Lot = Pool().get('lot.lot')
|
|
lots = Lot.search([('lot_shipment_in','=',self.shipment_in.id)])
|
|
logger.info("ON_CHANGE_WITH_UNIT:%s",lots)
|
|
if lots:
|
|
return lots[0].lot_unit_line
|
|
else:
|
|
return self.unit
|
|
|
|
def get_lots(self, name):
|
|
logger.info("GET_LOTS_LINE:%s",self.line)
|
|
logger.info("GET_LOTS_SHIPMENT_IN:%s",self.shipment_in)
|
|
Lot = Pool().get('lot.lot')
|
|
if self.line:
|
|
return self.line.lots
|
|
if self.shipment_in:
|
|
LotQt = Pool().get('lot.qt')
|
|
lots = Lot.search([('lot_shipment_in','=',self.shipment_in.id)])
|
|
logger.info("LOTSDOMAIN:%s",lots)
|
|
domain_lots = []
|
|
seen = set()
|
|
for lot in lots:
|
|
lot_id = getattr(lot, 'id', None)
|
|
if lot_id not in seen:
|
|
domain_lots.append(lot)
|
|
seen.add(lot_id)
|
|
for lqt in LotQt.search(['lot_shipment_in','=',self.shipment_in.id]):
|
|
lot = getattr(lqt, 'lot_p', None)
|
|
lot_id = getattr(lot, 'id', None)
|
|
if lot and lot_id not in seen:
|
|
domain_lots.append(lot)
|
|
seen.add(lot_id)
|
|
if domain_lots:
|
|
return domain_lots
|
|
if self.shipment_internal:
|
|
return Lot.search([('lot_shipment_internal','=',self.shipment_internal.id)])
|
|
if self.shipment_out:
|
|
return Lot.search([('lot_shipment_out','=',self.shipment_out.id)])
|
|
|
|
return Lot.search(['id','>',0])
|
|
|
|
def get_cog(self,lot):
|
|
MoveLine = Pool().get('account.move.line')
|
|
Currency = Pool().get('currency.currency')
|
|
Date = Pool().get('ir.date')
|
|
AccountConfiguration = Pool().get('account.configuration')
|
|
account_configuration = AccountConfiguration(1)
|
|
Uom = Pool().get('product.uom')
|
|
ml = MoveLine.search([
|
|
('lot', '=', lot.id),
|
|
('fee', '=', self.id),
|
|
('account', '=', self.product.account_stock_in_used.id),
|
|
('origin', 'ilike', '%stock.move%'),
|
|
])
|
|
|
|
logger.info("GET_COG_FEE:%s",ml)
|
|
if ml:
|
|
return round(Decimal(sum([e.credit-e.debit for e in ml if e.description != 'Delivery fee'])),2)
|
|
return Decimal(0)
|
|
|
|
def get_non_cog(self,lot):
|
|
MoveLine = Pool().get('account.move.line')
|
|
Currency = Pool().get('currency.currency')
|
|
Date = Pool().get('ir.date')
|
|
AccountConfiguration = Pool().get('account.configuration')
|
|
account_configuration = AccountConfiguration(1)
|
|
Uom = Pool().get('product.uom')
|
|
ml = MoveLine.search([
|
|
('lot', '=', lot.id),
|
|
('fee', '=', self.id),
|
|
('account', '=', self.product.account_stock_in_used.id),
|
|
])
|
|
|
|
logger.info("GET_NON_COG_FEE:%s",ml)
|
|
if ml:
|
|
return round(Decimal(sum([e.credit-e.debit for e in ml])),2)
|
|
return Decimal(0)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._buttons.update({
|
|
'invoice': {},
|
|
})
|
|
|
|
@classmethod
|
|
def default_state(cls):
|
|
return 'not invoiced'
|
|
|
|
@classmethod
|
|
def default_p_r(cls):
|
|
return 'pay'
|
|
|
|
def get_unit(self, name=None):
|
|
FeeLots = Pool().get('fee.lots')
|
|
fl = FeeLots.search(['fee','=',self.id])
|
|
if fl:
|
|
if fl[0].lot.line:
|
|
return fl[0].lot.line.unit
|
|
if fl[0].lot.sale_line:
|
|
return fl[0].lot.sale_line.unit
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def invoice(cls, fees):
|
|
Purchase = Pool().get('purchase.purchase')
|
|
FeeLots = Pool().get('fee.lots')
|
|
Warning = Pool().get('res.user.warning')
|
|
fees_to_invoice = []
|
|
for fee in fees:
|
|
was_invoiced = fee.state == 'invoiced'
|
|
if fee.state == 'invoiced':
|
|
warning_name = Warning.format(
|
|
"Fee already invoiced", [fee])
|
|
if Warning.check(warning_name):
|
|
raise UserWarning(
|
|
warning_name,
|
|
"This fee has already been invoiced. Continuing will "
|
|
"create a debit note or credit note by reversing the "
|
|
"previous fee invoice line and adding a new line with "
|
|
"the current fee values. Do you want to continue?")
|
|
fee.warn_percent_price_partial_lots()
|
|
fee.ensure_ordered_purchase()
|
|
if fee.purchase:
|
|
fl = FeeLots.search([('fee','=',fee.id)])
|
|
logger.info("PROCESS_FROM_FEE:%s",fl)
|
|
if was_invoiced:
|
|
with Transaction().set_context(
|
|
_purchase_trade_ignore_fee_padding=True):
|
|
fee.adjust_purchase_values()
|
|
invoices = Purchase._process_invoice(
|
|
[fee.purchase], [e.lot for e in fl], 'service')
|
|
else:
|
|
fee.adjust_purchase_values()
|
|
invoices = Purchase._process_invoice(
|
|
[fee.purchase], [e.lot for e in fl], 'service')
|
|
invoice = invoices.get(fee.purchase) if invoices else None
|
|
logger.info(
|
|
"PROCESS_FROM_FEE_RESULT: fee=%s purchase=%s invoice=%s "
|
|
"was_invoiced=%s",
|
|
getattr(fee, 'id', None), getattr(fee.purchase, 'id', None),
|
|
getattr(invoice, 'id', None), was_invoiced)
|
|
if not invoice:
|
|
if was_invoiced:
|
|
raise UserError(
|
|
"No debit note or credit note was created for "
|
|
"fee %s. The current fee values may be identical "
|
|
"to the last invoiced values." % getattr(
|
|
fee, 'id', None))
|
|
raise UserError(
|
|
"No invoice was created for fee %s. Please check "
|
|
"that the fee has linked lots and invoiceable values."
|
|
% getattr(fee, 'id', None))
|
|
if was_invoiced:
|
|
cls.write([fee], {'dn_cn': invoice.id})
|
|
fees_to_invoice.append(fee)
|
|
else:
|
|
raise UserError(
|
|
"Cannot invoice fee %s: no generated service purchase "
|
|
"is linked to this fee." % getattr(fee, 'id', None))
|
|
if fees_to_invoice:
|
|
cls.write(fees_to_invoice, {'state': 'invoiced',})
|
|
|
|
@classmethod
|
|
def default_type(cls):
|
|
return 'budgeted'
|
|
|
|
@classmethod
|
|
def default_weight_type(cls):
|
|
return 'brut'
|
|
|
|
def get_price_per_qt(self):
|
|
price = Decimal(0)
|
|
if self.mode == 'lumpsum':
|
|
if self.quantity:
|
|
if self.quantity > 0:
|
|
return round(self.price / self.quantity,4)
|
|
elif self.mode == 'perqt':
|
|
return self.price
|
|
elif self.mode == 'ppack':
|
|
unit = self.get_unit()
|
|
if unit and self.unit:
|
|
return round(self.price / Decimal(self.unit.factor) * Decimal(unit.factor),4)
|
|
elif self.mode == 'pprice' or self.mode == 'pcost':
|
|
if self.line and self.price:
|
|
return round(self.price * Decimal(self.line.unit_price) / 100,4)
|
|
if self.sale_line and self.price:
|
|
return round(self.price * Decimal(self.sale_line.unit_price) / 100,4)
|
|
if self.shipment_in:
|
|
StockMove = Pool().get('stock.move')
|
|
sm = StockMove.search(['shipment','=','stock.shipment.in,'+str(self.shipment_in.id)])
|
|
if sm:
|
|
if sm[0].lot:
|
|
return round(self.price * Decimal(sm[0].lot.get_lot_price()) / 100,4)
|
|
return price
|
|
|
|
def get_invoice(self,name):
|
|
InvoiceLine = Pool().get('account.invoice.line')
|
|
invoice_lines = InvoiceLine.search([
|
|
('fee', '=', self.id),
|
|
('quantity', '>', 0),
|
|
('invoice.state', '!=', 'cancelled'),
|
|
], order=[
|
|
('invoice.invoice_date', 'ASC'),
|
|
('invoice.id', 'ASC'),
|
|
('id', 'ASC'),
|
|
], limit=1)
|
|
if invoice_lines:
|
|
return invoice_lines[0].invoice
|
|
if self.purchase:
|
|
if self.purchase.invoices:
|
|
invoices = [
|
|
invoice for invoice in self.purchase.invoices
|
|
if getattr(invoice, 'state', None) != 'cancelled']
|
|
if invoices:
|
|
return sorted(
|
|
invoices,
|
|
key=lambda invoice: (
|
|
invoice.invoice_date or datetime.date.min,
|
|
invoice.id or 0))[0]
|
|
|
|
def get_landed_status(self,name):
|
|
if self.product:
|
|
return self.product.template.landed_cost
|
|
|
|
def get_quantity(self,name=None):
|
|
qt = self.get_fee_lots_qt()
|
|
if qt:
|
|
return qt
|
|
line = self.line or self.sale_line
|
|
if line and line.lots:
|
|
return sum(
|
|
Decimal(lot.get_current_quantity_converted(0, self.unit))
|
|
for lot in line.lots)
|
|
LotQt = Pool().get('lot.qt')
|
|
if self.shipment_in:
|
|
lqts = LotQt.search(['lot_shipment_in','=',self.shipment_in.id])
|
|
if lqts:
|
|
return Decimal(lqts[0].lot_quantity)
|
|
|
|
def _get_effective_fee_lots(self):
|
|
FeeLots = Pool().get('fee.lots')
|
|
fee_lots = FeeLots.search([('fee', '=', self.id)])
|
|
lots = [fee_lot.lot for fee_lot in fee_lots if fee_lot.lot]
|
|
return self._select_effective_lots(lots)
|
|
|
|
@staticmethod
|
|
def _select_effective_lots(lots):
|
|
physical_lots = [
|
|
lot for lot in lots
|
|
if getattr(lot, 'lot_type', None) == 'physic'
|
|
]
|
|
if physical_lots:
|
|
return physical_lots
|
|
return [
|
|
lot for lot in lots
|
|
if getattr(lot, 'lot_type', None) == 'virtual'
|
|
]
|
|
|
|
@staticmethod
|
|
def _record_key(record):
|
|
return getattr(record, 'id', None) or id(record)
|
|
|
|
def _get_effective_line_lots(self):
|
|
line = self.line or self.sale_line
|
|
return self._select_effective_lots(
|
|
list(getattr(line, 'lots', []) or []))
|
|
|
|
def percent_price_lots_cover_line(self):
|
|
if self.mode != 'pprice':
|
|
return True
|
|
if getattr(self, 'add_padding', False):
|
|
return True
|
|
line_lots = self._get_effective_line_lots()
|
|
if not line_lots:
|
|
return True
|
|
fee_lots = self._get_effective_fee_lots()
|
|
fee_lot_keys = {self._record_key(lot) for lot in fee_lots}
|
|
return all(
|
|
self._record_key(lot) in fee_lot_keys
|
|
for lot in line_lots)
|
|
|
|
def warn_percent_price_partial_lots(self):
|
|
if self.percent_price_lots_cover_line():
|
|
return
|
|
Warning = Pool().get('res.user.warning')
|
|
warning_name = Warning.format(
|
|
"Partial % price fee lots", [self])
|
|
if Warning.check(warning_name):
|
|
raise UserWarning(
|
|
warning_name,
|
|
"This % price fee is linked to only part of the line lots. "
|
|
"The commission cannot be safely computed from a partial "
|
|
"quantity because lot quantities may change through BL, WR "
|
|
"or LR weighing states. Please link all lots to the fee or "
|
|
"review the fee amount manually before continuing.")
|
|
|
|
def is_effective_fee_lot(self, lot):
|
|
return bool(lot and lot in self._get_effective_fee_lots())
|
|
|
|
def _get_weight_basis_qt_type(self, lot=None):
|
|
if self.line and self.line.purchase:
|
|
return getattr(self.line.purchase.wb, 'qt_type', None)
|
|
if self.sale_line and self.sale_line.sale:
|
|
return getattr(self.sale_line.sale.wb, 'qt_type', None)
|
|
if lot and lot.line and lot.line.purchase:
|
|
return getattr(lot.line.purchase.wb, 'qt_type', None)
|
|
if lot and lot.sale_line and lot.sale_line.sale:
|
|
return getattr(lot.sale_line.sale.wb, 'qt_type', None)
|
|
|
|
@staticmethod
|
|
def _qt_type_sequence(qt_type):
|
|
sequence = getattr(qt_type, 'sequence', None)
|
|
if sequence is None:
|
|
return None
|
|
return Decimal(str(sequence))
|
|
|
|
@staticmethod
|
|
def _same_qt_type(left, right):
|
|
return (
|
|
getattr(left, 'id', left)
|
|
and getattr(left, 'id', left) == getattr(right, 'id', right))
|
|
|
|
def _target_qt_type_for_lot(self, lot):
|
|
weight_basis_qt_type = self._get_weight_basis_qt_type(lot)
|
|
target_qt_type = self.qt_state or weight_basis_qt_type
|
|
if not target_qt_type or not weight_basis_qt_type:
|
|
return target_qt_type
|
|
|
|
target_sequence = self._qt_type_sequence(target_qt_type)
|
|
weight_basis_sequence = self._qt_type_sequence(weight_basis_qt_type)
|
|
if (
|
|
target_sequence is not None
|
|
and weight_basis_sequence is not None
|
|
and target_sequence > weight_basis_sequence):
|
|
return weight_basis_qt_type
|
|
return target_qt_type
|
|
|
|
def _select_lot_qt_type(self, lot):
|
|
target_qt_type = self._target_qt_type_for_lot(lot)
|
|
if not target_qt_type:
|
|
return None
|
|
|
|
lot_hist = list(getattr(lot, 'lot_hist', []) or [])
|
|
for hist in lot_hist:
|
|
if self._same_qt_type(hist.quantity_type, target_qt_type):
|
|
return hist.quantity_type
|
|
|
|
target_sequence = self._qt_type_sequence(target_qt_type)
|
|
if target_sequence is None:
|
|
return None
|
|
|
|
previous = []
|
|
for hist in lot_hist:
|
|
hist_sequence = self._qt_type_sequence(hist.quantity_type)
|
|
if hist_sequence is not None and hist_sequence <= target_sequence:
|
|
previous.append((hist_sequence, hist.quantity_type))
|
|
if previous:
|
|
previous.sort(key=lambda item: item[0])
|
|
return previous[-1][1]
|
|
return None
|
|
|
|
def _get_lot_fee_quantity(self, lot):
|
|
state = self._select_lot_qt_type(lot)
|
|
target_qt_type = self._target_qt_type_for_lot(lot)
|
|
if target_qt_type and not state:
|
|
return None
|
|
state_id = getattr(state, 'id', 0) if state else 0
|
|
if self.weight_type == 'brut':
|
|
return Decimal(
|
|
lot.get_current_gross_quantity_converted(state_id, self.unit)
|
|
or 0)
|
|
return Decimal(
|
|
lot.get_current_quantity_converted(state_id, self.unit) or 0)
|
|
|
|
def _get_effective_fee_lots_quantity(self):
|
|
lots = self._get_effective_fee_lots()
|
|
if not lots:
|
|
return None
|
|
if self.mode == 'ppack':
|
|
packing_qty = sum(
|
|
Decimal(str(getattr(lot, 'lot_qt', 0) or 0))
|
|
for lot in lots)
|
|
if packing_qty:
|
|
return packing_qty
|
|
return self.quantity
|
|
quantities = [self._get_lot_fee_quantity(lot) for lot in lots]
|
|
if any(quantity is None for quantity in quantities):
|
|
return None
|
|
return sum(quantities)
|
|
|
|
def _convert_padding_quantity(self, quantity, from_unit):
|
|
quantity = Decimal(str(quantity or 0))
|
|
if not quantity:
|
|
return Decimal(0)
|
|
to_unit = getattr(self, 'unit', None)
|
|
if not from_unit or not to_unit or from_unit == to_unit:
|
|
return quantity
|
|
Uom = Pool().get('product.uom')
|
|
return Decimal(str(
|
|
Uom.compute_qty(from_unit, float(quantity), to_unit) or 0))
|
|
|
|
def _get_lot_padding_quantity(self, lot):
|
|
padding = Decimal(str(
|
|
getattr(lot, 'sale_invoice_padding', 0) or 0))
|
|
if not padding:
|
|
return Decimal(0)
|
|
source_line = (
|
|
getattr(self, 'sale_line', None)
|
|
or getattr(lot, 'sale_line', None)
|
|
or getattr(self, 'line', None)
|
|
or getattr(lot, 'line', None))
|
|
source_unit = getattr(source_line, 'unit', None)
|
|
if not source_unit:
|
|
source_unit = getattr(lot, 'lot_unit_line', None)
|
|
return self._convert_padding_quantity(padding, source_unit)
|
|
|
|
def get_padding_quantity(self):
|
|
if Transaction().context.get('_purchase_trade_ignore_fee_padding'):
|
|
return Decimal(0)
|
|
if getattr(self, 'state', None) == 'invoiced':
|
|
return Decimal(0)
|
|
if not getattr(self, 'add_padding', False):
|
|
return Decimal(0)
|
|
return sum(
|
|
self._get_lot_padding_quantity(lot)
|
|
for lot in self._get_effective_fee_lots())
|
|
|
|
def get_billing_quantity(self):
|
|
quantity = self._get_effective_fee_lots_quantity()
|
|
if quantity is None:
|
|
quantity = self.quantity
|
|
if quantity is None:
|
|
quantity = self.get_quantity()
|
|
return Decimal(quantity or 0) + self.get_padding_quantity()
|
|
|
|
def _get_percent_price_unit_price(self):
|
|
line = self.line or self.sale_line
|
|
if line:
|
|
return Decimal(str(getattr(line, 'unit_price', 0) or 0))
|
|
lots = self._get_effective_fee_lots()
|
|
lot = lots[0] if lots else None
|
|
if lot:
|
|
if getattr(lot, 'line', None):
|
|
return Decimal(str(getattr(lot.line, 'unit_price', 0) or 0))
|
|
if getattr(lot, 'sale_line', None):
|
|
return Decimal(str(
|
|
getattr(lot.sale_line, 'unit_price', 0) or 0))
|
|
return Decimal(0)
|
|
|
|
def _get_padding_percent_price_amount(self):
|
|
return (
|
|
self.get_billing_quantity()
|
|
* self._get_percent_price_unit_price())
|
|
|
|
def sync_quantity_from_lots(self):
|
|
if self.mode == 'ppack' and self.auto_calculation:
|
|
self.assert_quantity_consistency()
|
|
return
|
|
quantity = self._get_effective_fee_lots_quantity()
|
|
if quantity is None:
|
|
self.assert_quantity_consistency()
|
|
return
|
|
quantity = round(Decimal(quantity), 5)
|
|
if self.mode == 'ppack':
|
|
quantity = quantity.to_integral_value(rounding=ROUND_UP)
|
|
if Decimal(self.quantity or 0) != quantity:
|
|
self.quantity = quantity
|
|
with Transaction().set_context(_purchase_trade_skip_fee_sync=True):
|
|
self.__class__.save([self])
|
|
self.assert_quantity_consistency()
|
|
|
|
def assert_quantity_consistency(self):
|
|
if self.mode == 'ppack':
|
|
return
|
|
expected = self._get_effective_fee_lots_quantity()
|
|
if expected is None:
|
|
if self._get_effective_fee_lots():
|
|
raise UserError(
|
|
"Fee quantity consistency error on fee %s: no usable "
|
|
"quantity state found on linked lots" % (
|
|
getattr(self, 'id', None),))
|
|
return
|
|
expected = round(Decimal(expected), 5)
|
|
observed = round(Decimal(self.quantity or 0), 5)
|
|
diff = observed - expected
|
|
if diff:
|
|
raise UserError(
|
|
"Fee quantity consistency error on fee %s: fee quantity "
|
|
"must equal linked lots quantity (observed=%s, expected=%s, "
|
|
"diff=%s)" % (
|
|
getattr(self, 'id', None), observed, expected, diff))
|
|
|
|
@classmethod
|
|
def assert_quantities_consistency(cls, fees):
|
|
for fee in fees:
|
|
fee.assert_quantity_consistency()
|
|
|
|
def _get_amount_quantity(self):
|
|
quantity = self.quantity
|
|
if quantity is None:
|
|
quantity = self.get_quantity()
|
|
return Decimal(quantity or 0)
|
|
|
|
@staticmethod
|
|
def _unsigned_amount(amount):
|
|
if amount is None:
|
|
return None
|
|
return abs(Decimal(amount))
|
|
|
|
def get_amount(self,name=None):
|
|
sign = Decimal(1)
|
|
if self.price:
|
|
# if self.p_r:
|
|
# if self.p_r == 'pay':
|
|
# sign = -1
|
|
if self.mode == 'lumpsum':
|
|
return self._unsigned_amount(self.price * sign)
|
|
elif self.mode == 'ppack':
|
|
return self._unsigned_amount(
|
|
round(self.price * self.quantity,2))
|
|
elif self.mode == 'rate':
|
|
if self.line:
|
|
if self.line.estimated_date:
|
|
est_lines = [dd for dd in self.line.estimated_date if dd.trigger == 'bldate']
|
|
est_line = est_lines[0] if est_lines else None
|
|
if est_line and est_line.fin_int_delta:
|
|
factor = (
|
|
Decimal(self.price) / Decimal(100)
|
|
* Decimal(est_line.fin_int_delta) / Decimal(360)
|
|
)
|
|
return self._unsigned_amount(round(
|
|
factor * self.line.unit_price
|
|
* self._get_amount_quantity() * sign,2))
|
|
if self.sale_line:
|
|
if self.sale_line.estimated_date:
|
|
est_lines = [
|
|
dd for dd in self.sale_line.estimated_date
|
|
if dd.trigger == 'bldate'
|
|
]
|
|
est_line = est_lines[0] if est_lines else None
|
|
if est_line and est_line.fin_int_delta:
|
|
factor = (
|
|
Decimal(self.price) / Decimal(100)
|
|
* Decimal(est_line.fin_int_delta) / Decimal(360)
|
|
)
|
|
return self._unsigned_amount(round(
|
|
factor * self.sale_line.unit_price
|
|
* self._get_amount_quantity() * sign, 2))
|
|
|
|
elif self.mode == 'perqt':
|
|
if self.add_padding:
|
|
return self._unsigned_amount(round(
|
|
self.get_billing_quantity() * self.price * sign, 2))
|
|
if self.shipment_in:
|
|
StockMove = Pool().get('stock.move')
|
|
sm = StockMove.search(['shipment','=','stock.shipment.in,'+str(self.shipment_in.id)])
|
|
if sm:
|
|
unique_lots = {e.lot for e in sm if e.lot}
|
|
return self._unsigned_amount(round(self.price * Decimal(sum([e.get_current_quantity_converted(0,self.unit) for e in unique_lots])) * sign,2))
|
|
LotQt = Pool().get('lot.qt')
|
|
lqts = LotQt.search(['lot_shipment_in','=',self.shipment_in.id])
|
|
if lqts:
|
|
return self._unsigned_amount(round(self.price * Decimal(lqts[0].lot_quantity) * sign,2))
|
|
|
|
return self._unsigned_amount(round((self.quantity if self.quantity else 0) * self.price * sign,2))
|
|
elif self.mode == 'pprice':
|
|
if self.add_padding:
|
|
return self._unsigned_amount(round(
|
|
self.price / 100
|
|
* self._get_padding_percent_price_amount()
|
|
* sign, 2))
|
|
if self.line:
|
|
if self.percent_price_lots_cover_line():
|
|
return self._unsigned_amount(round(
|
|
self.price / 100
|
|
* Decimal(self.line.amount or 0) * sign, 2))
|
|
return self._unsigned_amount(round(self.price / 100 * self.line.unit_price * (self.quantity if self.quantity else 0) * sign,2))
|
|
if self.sale_line:
|
|
if self.percent_price_lots_cover_line():
|
|
return self._unsigned_amount(round(
|
|
self.price / 100
|
|
* Decimal(self.sale_line.amount or 0) * sign, 2))
|
|
return self._unsigned_amount(round(self.price / 100 * self.sale_line.unit_price * (self.quantity if self.quantity else 0) * sign,2))
|
|
if self.shipment_in:
|
|
StockMove = Pool().get('stock.move')
|
|
sm = StockMove.search(['shipment','=','stock.shipment.in,'+str(self.shipment_in.id)])
|
|
if sm:
|
|
if sm[0].lot:
|
|
return self._unsigned_amount(round(self.price * Decimal(sum([e.lot.get_lot_price() for e in sm if e.lot])) / 100 * self.quantity * sign,2))
|
|
LotQt = Pool().get('lot.qt')
|
|
lqts = LotQt.search(['lot_shipment_in','=',self.shipment_in.id])
|
|
if lqts:
|
|
return self._unsigned_amount(round(self.price * Decimal(lqts[0].lot_p.get_lot_price()) / 100 * lqts[0].lot_quantity * sign,2))
|
|
|
|
@classmethod
|
|
def copy(cls, fees, default=None):
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
|
|
# Important : on vide le champ 'lots'
|
|
default.setdefault('lots', [])
|
|
|
|
return super().copy(fees, default=default)
|
|
|
|
def get_fee_lots_qt(self,state_id=0):
|
|
qt = Decimal(0)
|
|
lots = self._get_effective_fee_lots()
|
|
if lots:
|
|
if state_id:
|
|
qt = sum([
|
|
e.get_current_quantity_converted(state_id,self.unit)
|
|
for e in lots])
|
|
else:
|
|
qt = self._get_effective_fee_lots_quantity() or Decimal(0)
|
|
logger.info("GET_FEE_LOTS_QT:%s",qt)
|
|
return qt
|
|
|
|
def adjust_purchase_values(self):
|
|
Purchase = Pool().get('purchase.purchase')
|
|
PurchaseLine = Pool().get('purchase.line')
|
|
logger.info("ADJUST_PURCHASE_VALUES:%s",self)
|
|
if self.type == 'ordered' and self.purchase:
|
|
logger.info("ADJUST_PURCHASE_VALUES_QT:%s",self.purchase.lines[0].quantity)
|
|
if self.mode == 'lumpsum':
|
|
if self.amount != self.purchase.lines[0].unit_price:
|
|
self.purchase.lines[0].unit_price = self.amount
|
|
elif self.mode == 'ppack':
|
|
if self.amount != self.purchase.lines[0].amount:
|
|
self.purchase.lines[0].unit_price = self.price
|
|
self.purchase.lines[0].quantity = self.quantity
|
|
elif self.mode == 'pprice':
|
|
if self.amount != self.purchase.lines[0].unit_price:
|
|
self.purchase.lines[0].unit_price = self.amount
|
|
if self.purchase.lines[0].quantity != 1:
|
|
self.purchase.lines[0].quantity = 1
|
|
elif self.mode == 'perqt' and self.add_padding:
|
|
quantity = self.get_billing_quantity()
|
|
if self.price != self.purchase.lines[0].unit_price:
|
|
self.purchase.lines[0].unit_price = self.price
|
|
if quantity != self.purchase.lines[0].quantity:
|
|
self.purchase.lines[0].quantity = quantity
|
|
else:
|
|
if self.get_price_per_qt() != self.purchase.lines[0].unit_price:
|
|
self.purchase.lines[0].unit_price = self.get_price_per_qt()
|
|
if self.quantity != self.purchase.lines[0].quantity:
|
|
self.purchase.lines[0].quantity = self.quantity
|
|
if self.product != self.purchase.lines[0].product:
|
|
self.purchase.lines[0].product = self.product
|
|
PurchaseLine.save([self.purchase.lines[0]])
|
|
if self.supplier != self.purchase.party:
|
|
self.purchase.party = self.supplier
|
|
if self.currency != self.purchase.currency:
|
|
self.purchase.currency = self.currency
|
|
Purchase.save([self.purchase])
|
|
|
|
@classmethod
|
|
def _add_lot_pnl_lines(cls, lot, purchase_lines, sale_lines):
|
|
if not lot:
|
|
return
|
|
if getattr(lot, 'line', None):
|
|
purchase_lines[lot.line.id] = lot.line
|
|
if getattr(lot, 'sale_line', None):
|
|
sale_lines[lot.sale_line.id] = lot.sale_line
|
|
|
|
@classmethod
|
|
def _collect_pnl_lines(cls, fees=None, lots=None):
|
|
purchase_lines = {}
|
|
sale_lines = {}
|
|
|
|
for lot in lots or []:
|
|
cls._add_lot_pnl_lines(lot, purchase_lines, sale_lines)
|
|
|
|
for fee in fees or []:
|
|
if getattr(fee, 'line', None):
|
|
purchase_lines[fee.line.id] = fee.line
|
|
if getattr(fee, 'sale_line', None):
|
|
sale_lines[fee.sale_line.id] = fee.sale_line
|
|
|
|
for lot in getattr(fee, 'lots', None) or []:
|
|
cls._add_lot_pnl_lines(lot, purchase_lines, sale_lines)
|
|
|
|
shipment = getattr(fee, 'shipment_in', None)
|
|
if not shipment:
|
|
continue
|
|
for move in getattr(shipment, 'incoming_moves', None) or []:
|
|
cls._add_lot_pnl_lines(
|
|
getattr(move, 'lot', None), purchase_lines, sale_lines)
|
|
for lqt in getattr(shipment, 'lotqt', None) or []:
|
|
cls._add_lot_pnl_lines(
|
|
getattr(lqt, 'lot_p', None), purchase_lines, sale_lines)
|
|
cls._add_lot_pnl_lines(
|
|
getattr(lqt, 'lot_s', None), purchase_lines, sale_lines)
|
|
|
|
return purchase_lines.values(), sale_lines.values()
|
|
|
|
@classmethod
|
|
def _regenerate_fee_pnl(cls, fees=None, lots=None):
|
|
purchase_lines, sale_lines = cls._collect_pnl_lines(fees, lots)
|
|
cls._regenerate_fee_pnl_for_lines(purchase_lines, sale_lines)
|
|
|
|
@classmethod
|
|
def _regenerate_fee_pnl_for_lines(cls, purchase_lines, sale_lines):
|
|
purchase_lines = cls._unique_records(purchase_lines)
|
|
sale_lines = cls._unique_records(sale_lines)
|
|
if not purchase_lines and not sale_lines:
|
|
return
|
|
Valuation = Pool().get('valuation.valuation')
|
|
for line in purchase_lines:
|
|
Valuation.generate(line, valuation_type='fees')
|
|
for sale_line in sale_lines:
|
|
Valuation.generate_from_sale_line(
|
|
sale_line, valuation_type='fees')
|
|
|
|
@staticmethod
|
|
def _unique_records(records):
|
|
unique = {}
|
|
for record in records:
|
|
if not record:
|
|
continue
|
|
unique[getattr(record, 'id', id(record))] = record
|
|
return list(unique.values())
|
|
|
|
# @classmethod
|
|
# def validate(cls, fees):
|
|
# super(Fee, cls).validate(fees)
|
|
|
|
@classmethod
|
|
def _save_fee_accrual_or_warn(cls, account_move, fee, lot):
|
|
AccountMove = Pool().get('account.move')
|
|
if account_move:
|
|
AccountMove.save([account_move])
|
|
return
|
|
|
|
Warning = Pool().get('res.user.warning')
|
|
warning_name = Warning.format(
|
|
"Fee accrual not generated", [fee.product, lot])
|
|
if Warning.check(warning_name):
|
|
raise UserWarning(
|
|
warning_name,
|
|
"The fee can be saved, but the accrual entry could not be "
|
|
"generated because the stock accounting configuration or "
|
|
"accounting period is missing or incomplete. Do you want to "
|
|
"continue without creating the accrual entry?")
|
|
|
|
def _can_create_ordered_purchase(self):
|
|
return (
|
|
getattr(self, 'type', None) == 'ordered'
|
|
and not getattr(self, 'purchase', None)
|
|
and getattr(self, 'currency', None)
|
|
and getattr(self, 'price', None) is not None)
|
|
|
|
def _get_ordered_purchase_quantity_unit(self):
|
|
quantity = Decimal(0)
|
|
unit = None
|
|
lots = self._get_effective_fee_lots()
|
|
if lots:
|
|
quantity = sum(
|
|
Decimal(lot.get_current_quantity_converted() or 0)
|
|
for lot in lots)
|
|
first_lot = lots[0]
|
|
if getattr(first_lot, 'line', None):
|
|
unit = first_lot.line.unit
|
|
elif getattr(first_lot, 'sale_line', None):
|
|
unit = first_lot.sale_line.unit
|
|
elif getattr(self, 'quantity', None) is not None:
|
|
quantity = Decimal(self.quantity or 0)
|
|
unit = getattr(self, 'unit', None)
|
|
if self.mode == 'perqt' and getattr(self, 'add_padding', False):
|
|
quantity = self.get_billing_quantity()
|
|
return quantity, unit
|
|
|
|
def _get_ordered_purchase_locations(self):
|
|
if getattr(self, 'shipment_in', None):
|
|
return self.shipment_in.from_location, self.shipment_in.to_location
|
|
if getattr(self, 'line', None):
|
|
return self.line.purchase.from_location, self.line.purchase.to_location
|
|
return self.sale_line.sale.from_location, self.sale_line.sale.to_location
|
|
|
|
def _get_ordered_purchase_payment_term(self):
|
|
if getattr(self, 'shipment_in', None) and self.shipment_in.lotqt:
|
|
return self.shipment_in.lotqt[0].lot_p.line.purchase.payment_term
|
|
if getattr(self, 'line', None):
|
|
return self.line.purchase.payment_term
|
|
return self.sale_line.sale.payment_term
|
|
|
|
def _set_ordered_purchase_line_values(self, line, quantity, unit):
|
|
line.product = self.product
|
|
line.quantity = round(quantity, 5)
|
|
line.unit = unit
|
|
line.fee_ = self.id
|
|
if self.mode == 'pprice':
|
|
line.quantity = 1
|
|
line.unit_price = round(Decimal(self.amount or 0), 4)
|
|
return
|
|
if getattr(self, 'price', None) is not None:
|
|
fee_price = self.get_price_per_qt()
|
|
logger.info("GET_FEE_PRICE_PER_QT:%s", fee_price)
|
|
line.unit_price = round(Decimal(fee_price), 4)
|
|
if self.mode == 'lumpsum':
|
|
line.quantity = 1
|
|
line.unit_price = round(Decimal(self.amount or 0), 4)
|
|
elif self.mode == 'ppack':
|
|
line.unit_price = self.price
|
|
|
|
def _create_fee_accruals(self):
|
|
if self.sale_line:
|
|
return
|
|
FeeLots = Pool().get('fee.lots')
|
|
feelots = FeeLots.search(['fee','=',self.id])
|
|
for fl in feelots:
|
|
if self.product.template.landed_cost:
|
|
move = fl.lot.get_received_move()
|
|
if move:
|
|
Warning = Pool().get('res.user.warning')
|
|
warning_name = Warning.format("Lot ever received", [])
|
|
if Warning.check(warning_name):
|
|
raise UserWarning(warning_name,
|
|
"By clicking yes, an accrual for this fee will be created")
|
|
account_move = move._get_account_stock_move_fee(self)
|
|
self.__class__._save_fee_accrual_or_warn(
|
|
account_move, self, fl.lot)
|
|
else:
|
|
account_move = self._get_account_move_fee(fl.lot)
|
|
self.__class__._save_fee_accrual_or_warn(
|
|
account_move, self, fl.lot)
|
|
|
|
def ensure_ordered_purchase(self):
|
|
if not self._can_create_ordered_purchase():
|
|
return False
|
|
Purchase = Pool().get('purchase.purchase')
|
|
PurchaseLine = Pool().get('purchase.line')
|
|
quantity, unit = self._get_ordered_purchase_quantity_unit()
|
|
line = PurchaseLine()
|
|
self._set_ordered_purchase_line_values(line, quantity, unit)
|
|
purchase = Purchase()
|
|
purchase.lines = [line]
|
|
purchase.party = self.supplier
|
|
if purchase.party.addresses:
|
|
purchase.invoice_address = purchase.party.addresses[0]
|
|
purchase.currency = self.currency
|
|
purchase.line_type = 'service'
|
|
purchase.from_location, purchase.to_location = (
|
|
self._get_ordered_purchase_locations())
|
|
purchase.payment_term = self._get_ordered_purchase_payment_term()
|
|
Purchase.save([purchase])
|
|
self.purchase = purchase
|
|
with Transaction().set_context(_purchase_trade_skip_fee_sync=True):
|
|
self.__class__.save([self])
|
|
self._create_fee_accruals()
|
|
return True
|
|
|
|
@classmethod
|
|
def create(cls, vlist):
|
|
vlist = [x.copy() for x in vlist]
|
|
fees = super(Fee, cls).create(vlist)
|
|
for fee in fees:
|
|
qt_sh = Decimal(0)
|
|
qt_line = Decimal(0)
|
|
FeeLots = Pool().get('fee.lots')
|
|
Lots = Pool().get('lot.lot')
|
|
LotQt = Pool().get('lot.qt')
|
|
if fee.line:
|
|
for l in fee.line.lots:
|
|
if (l.lot_type == 'virtual' and len(fee.line.lots)==1) or (l.lot_type == 'physic' and len(fee.line.lots)>1):
|
|
fl = FeeLots()
|
|
fl.fee = fee.id
|
|
fl.lot = l.id
|
|
fl.line = l.line.id
|
|
FeeLots.save([fl])
|
|
qt_line += l.get_current_quantity_converted()
|
|
unit = l.line.unit
|
|
if fee.sale_line:
|
|
for l in fee.sale_line.lots:
|
|
if (l.lot_type == 'virtual' and len(fee.sale_line.lots)==1) or (l.lot_type == 'physic' and len(fee.sale_line.lots)>1):
|
|
fl = FeeLots()
|
|
fl.fee = fee.id
|
|
fl.lot = l.id
|
|
fl.sale_line = l.sale_line.id
|
|
FeeLots.save([fl])
|
|
qt_line += l.get_current_quantity_converted()
|
|
unit = l.sale_line.unit
|
|
if fee.shipment_in:
|
|
if fee.shipment_in.state == 'draft'or fee.shipment_in.state == 'started':
|
|
lots = Lots.search(['lot_shipment_in','=',fee.shipment_in.id])
|
|
if lots:
|
|
for l in lots:
|
|
fl = FeeLots()
|
|
fl.fee = fee.id
|
|
fl.lot = l.id
|
|
FeeLots.save([fl])
|
|
qt_sh += l.get_current_quantity_converted()
|
|
unit = l.line.unit
|
|
else:
|
|
lqts = LotQt.search(['lot_shipment_in','=',fee.shipment_in.id])
|
|
if lqts:
|
|
for l in lqts:
|
|
if l.lot_p:
|
|
exist = FeeLots.search([
|
|
('fee','=',fee.id),
|
|
('lot','=',l.lot_p.id),
|
|
])
|
|
if not exist:
|
|
fl = FeeLots()
|
|
fl.fee = fee.id
|
|
fl.lot = l.lot_p.id
|
|
FeeLots.save([fl])
|
|
qt_sh += l.lot_p.get_current_quantity_converted()
|
|
unit = l.lot_p.line.unit
|
|
else:
|
|
raise UserError("You cannot add fee on received shipment!")
|
|
|
|
logger.info("CREATE_PURHCASE_FOR_FEE_QT:%s", qt_line or qt_sh)
|
|
fee.ensure_ordered_purchase()
|
|
|
|
for fee in fees:
|
|
fee.sync_quantity_from_lots()
|
|
cls.assert_quantities_consistency(fees)
|
|
cls._regenerate_fee_pnl(fees=fees)
|
|
return fees
|
|
|
|
@classmethod
|
|
def write(cls, *args):
|
|
old_purchase_lines, old_sale_lines = cls._collect_pnl_lines(
|
|
fees=sum(args[::2], []))
|
|
old_purchase_lines = list(old_purchase_lines)
|
|
old_sale_lines = list(old_sale_lines)
|
|
super().write(*args)
|
|
if Transaction().context.get('_purchase_trade_skip_fee_sync'):
|
|
return
|
|
fees = sum(args[::2], [])
|
|
for fee in fees:
|
|
fee.sync_quantity_from_lots()
|
|
fee.ensure_ordered_purchase()
|
|
fee.adjust_purchase_values()
|
|
cls.assert_quantities_consistency(fees)
|
|
purchase_lines, sale_lines = cls._collect_pnl_lines(fees=fees)
|
|
cls._regenerate_fee_pnl_for_lines(
|
|
old_purchase_lines + list(purchase_lines),
|
|
old_sale_lines + list(sale_lines))
|
|
|
|
@classmethod
|
|
def delete(cls, fees):
|
|
purchase_lines, sale_lines = cls._collect_pnl_lines(fees=fees)
|
|
purchases = cls._get_generated_purchases_to_delete(fees)
|
|
super().delete(fees)
|
|
if purchases:
|
|
Purchase = Pool().get('purchase.purchase')
|
|
Purchase.delete(purchases)
|
|
cls._regenerate_fee_pnl_for_lines(purchase_lines, sale_lines)
|
|
|
|
@classmethod
|
|
def _get_generated_purchases_to_delete(cls, fees):
|
|
purchases = []
|
|
seen = set()
|
|
for fee in fees:
|
|
purchase = getattr(fee, 'purchase', None)
|
|
if not cls._is_generated_purchase_for_fee(fee, purchase):
|
|
continue
|
|
purchase_id = getattr(purchase, 'id', id(purchase))
|
|
if purchase_id in seen:
|
|
continue
|
|
seen.add(purchase_id)
|
|
purchases.append(purchase)
|
|
return purchases
|
|
|
|
@staticmethod
|
|
def _is_generated_purchase_for_fee(fee, purchase):
|
|
if getattr(fee, 'type', None) != 'ordered' or not purchase:
|
|
return False
|
|
if getattr(purchase, 'line_type', 'service') != 'service':
|
|
return False
|
|
lines = list(getattr(purchase, 'lines', []) or [])
|
|
if not lines:
|
|
return False
|
|
fee_id = getattr(fee, 'id', fee)
|
|
for line in lines:
|
|
line_fee = getattr(line, 'fee_', None)
|
|
if getattr(line_fee, 'id', line_fee) != fee_id:
|
|
return False
|
|
return True
|
|
|
|
def _get_account_move_fee(self,lot,in_out='in',amt = None):
|
|
pool = Pool()
|
|
AccountMove = pool.get('account.move')
|
|
Date = pool.get('ir.date')
|
|
Period = pool.get('account.period')
|
|
AccountConfiguration = pool.get('account.configuration')
|
|
|
|
if self.product.type != 'service':
|
|
return
|
|
|
|
today = Date.today()
|
|
company = lot.line.purchase.company if lot.line else lot.sale_line.sale.company
|
|
for date in [today]:
|
|
try:
|
|
period = Period.find(company, date=date, test_state=False)
|
|
except PeriodNotFoundError:
|
|
if date < today:
|
|
return
|
|
continue
|
|
break
|
|
else:
|
|
return
|
|
|
|
if period.state != 'open':
|
|
date = today
|
|
period = Period.find(company, date=date)
|
|
|
|
AccountMoveLine = pool.get('account.move.line')
|
|
Currency = pool.get('currency.currency')
|
|
move_line = AccountMoveLine()
|
|
move_line.lot = lot
|
|
move_line.fee = self
|
|
move_line.origin = None
|
|
move_line_ = AccountMoveLine()
|
|
move_line_.lot = lot
|
|
move_line_.fee = self
|
|
move_line_.origin = None
|
|
amount = amt if amt else self.amount
|
|
|
|
if self.currency != company.currency:
|
|
with Transaction().set_context(date=today):
|
|
amount_converted = amount
|
|
amount = Currency.compute(self.currency,
|
|
amount, company.currency)
|
|
move_line.second_currency = self.currency
|
|
|
|
if self.p_r == 'pay':
|
|
move_line.debit = amount
|
|
move_line.credit = Decimal(0)
|
|
move_line.account = self.product.account_stock_used if in_out == 'in' else self.product.account_cogs_used
|
|
if hasattr(move_line, 'second_currency') and move_line.second_currency:
|
|
move_line.amount_second_currency = amount_converted
|
|
move_line_.debit = Decimal(0)
|
|
move_line_.credit = amount
|
|
move_line_.account = self.product.account_stock_in_used if in_out == 'in' else self.product.account_stock_out_used
|
|
if hasattr(move_line_, 'second_currency') and move_line_.second_currency:
|
|
move_line_.amount_second_currency = -amount_converted
|
|
else:
|
|
move_line.debit = Decimal(0)
|
|
move_line.credit = amount
|
|
move_line.account = self.product.account_stock_used if in_out == 'in' else self.product.account_cogs_used
|
|
if hasattr(move_line, 'second_currency') and move_line.second_currency:
|
|
move_line.amount_second_currency = -amount_converted
|
|
move_line_.debit = amount
|
|
move_line_.credit = Decimal(0)
|
|
move_line_.account = self.product.account_stock_in_used if in_out == 'in' else self.product.account_stock_out_used
|
|
if hasattr(move_line_, 'second_currency') and move_line_.second_currency:
|
|
move_line_.amount_second_currency = amount_converted
|
|
|
|
logger.info("FEE_MOVELINES_1:%s",move_line)
|
|
logger.info("FEE_MOVELINES_2:%s",move_line_)
|
|
|
|
AccountJournal = Pool().get('account.journal')
|
|
journal = AccountJournal.search(['type','=','expense'])
|
|
if journal:
|
|
journal = journal[0]
|
|
|
|
description = None
|
|
description = 'Fee'
|
|
return AccountMove(
|
|
journal=journal,
|
|
period=period,
|
|
date=date,
|
|
origin=None,
|
|
description=description,
|
|
lines=[move_line,move_line_],
|
|
)
|
|
|
|
class FeeRule(ModelSQL, ModelView):
|
|
"Fee Rule"
|
|
__name__ = 'fee.rule'
|
|
|
|
name = fields.Char("Name", required=True)
|
|
active = fields.Boolean("Active")
|
|
sequence = fields.Integer("Sequence")
|
|
apply_on = fields.Selection([
|
|
('purchase_line', "Purchase Line"),
|
|
('sale_line', "Sale Line"),
|
|
], "Apply On", required=True)
|
|
auto_apply = fields.Boolean("Auto Apply")
|
|
|
|
party = fields.Many2One('party.party', "Counterparty")
|
|
party_category = fields.Many2One(
|
|
'party.category', "Counterparty Category")
|
|
line_product = fields.Many2One('product.product', "Line Product")
|
|
incoterm_code = fields.Char("Incoterm Code")
|
|
from_location = fields.Many2One('stock.location', "From Location")
|
|
to_location = fields.Many2One('stock.location', "To Location")
|
|
execution_plan = fields.Many2One('workflow.plan', "Execution Plan")
|
|
|
|
fee_product = fields.Many2One(
|
|
'product.product', "Fee Product", required=True,
|
|
domain=[('type', '=', 'service')])
|
|
supplier = fields.Many2One('party.party', "Supplier")
|
|
p_r = fields.Selection([
|
|
('pay', 'PAY'),
|
|
('rec', 'REC'),
|
|
], "P/R", required=True)
|
|
mode = fields.Selection([
|
|
('lumpsum', 'Lump sum'),
|
|
('perqt', 'Per qt'),
|
|
('pprice', '% price'),
|
|
('rate', '% rate'),
|
|
('pcost', '% cost price'),
|
|
('ppack', 'Per packing'),
|
|
], "Mode", required=True)
|
|
price = fields.Numeric("Price", digits=(16, 4))
|
|
quantity = fields.Numeric("Qt", digits='unit')
|
|
currency = fields.Many2One('currency.currency', "Currency")
|
|
unit = fields.Many2One('product.uom', "Unit")
|
|
qt_state = fields.Many2One('lot.qt.type', "Qt State")
|
|
weight_type = fields.Selection([
|
|
('net', 'Net'),
|
|
('brut', 'Gross'),
|
|
], "W. type")
|
|
|
|
@classmethod
|
|
def default_active(cls):
|
|
return True
|
|
|
|
@classmethod
|
|
def default_sequence(cls):
|
|
return 10
|
|
|
|
@classmethod
|
|
def default_auto_apply(cls):
|
|
return False
|
|
|
|
@classmethod
|
|
def default_p_r(cls):
|
|
return 'pay'
|
|
|
|
@classmethod
|
|
def default_mode(cls):
|
|
return 'lumpsum'
|
|
|
|
@classmethod
|
|
def default_weight_type(cls):
|
|
return 'brut'
|
|
|
|
@classmethod
|
|
def default_currency(cls):
|
|
if is_itsa_company():
|
|
currency = default_itsa_currency()
|
|
if currency:
|
|
return currency
|
|
|
|
@classmethod
|
|
def default_unit(cls):
|
|
if is_itsa_company():
|
|
unit = default_itsa_unit()
|
|
if unit:
|
|
return unit
|
|
|
|
@staticmethod
|
|
def _same_record(left, right):
|
|
return bool(
|
|
left and right
|
|
and getattr(left, 'id', left) == getattr(right, 'id', right))
|
|
|
|
@classmethod
|
|
def _category_matches(cls, category, target):
|
|
while target:
|
|
if cls._same_record(category, target):
|
|
return True
|
|
target = getattr(target, 'parent', None)
|
|
return False
|
|
|
|
@classmethod
|
|
def _party_has_category(cls, party, category):
|
|
if not category:
|
|
return True
|
|
if not party:
|
|
return False
|
|
return any(
|
|
cls._category_matches(category, party_category)
|
|
for party_category in (getattr(party, 'categories', []) or []))
|
|
|
|
@classmethod
|
|
def _rules_for_lines(cls, apply_on, auto_only=False):
|
|
domain = [
|
|
('active', '=', True),
|
|
('apply_on', '=', apply_on),
|
|
]
|
|
if auto_only:
|
|
domain.append(('auto_apply', '=', True))
|
|
return cls.search(domain, order=[('sequence', 'ASC'), ('id', 'ASC')])
|
|
|
|
def _matches_line(self, line, apply_on):
|
|
contract = getattr(line, 'purchase', None) or getattr(line, 'sale', None)
|
|
if not contract or self.apply_on != apply_on:
|
|
return False
|
|
checks = [
|
|
(self.party, getattr(contract, 'party', None)),
|
|
(self.line_product, getattr(line, 'product', None)),
|
|
(self.from_location, getattr(contract, 'from_location', None)),
|
|
(self.to_location, getattr(contract, 'to_location', None)),
|
|
]
|
|
if any(rule_value and not self._same_record(rule_value, actual)
|
|
for rule_value, actual in checks):
|
|
return False
|
|
if not self._party_has_category(
|
|
getattr(contract, 'party', None), self.party_category):
|
|
return False
|
|
if self.incoterm_code:
|
|
incoterm = getattr(contract, 'incoterm', None)
|
|
if (getattr(incoterm, 'code', '') or '').upper() != (
|
|
self.incoterm_code or '').upper():
|
|
return False
|
|
if self.execution_plan and not self._same_record(
|
|
self.execution_plan, getattr(contract, 'plan', None)):
|
|
return False
|
|
return True
|
|
|
|
def _existing_fee(self, line, apply_on):
|
|
Fee = Pool().get('fee.fee')
|
|
field_name = 'line' if apply_on == 'purchase_line' else 'sale_line'
|
|
existing = Fee.search([
|
|
('source_rule', '=', self.id),
|
|
(field_name, '=', line.id),
|
|
], limit=1)
|
|
return existing[0] if existing else None
|
|
|
|
def _fee_values(self, line, apply_on):
|
|
contract = getattr(line, 'purchase', None) or getattr(line, 'sale', None)
|
|
values = {
|
|
'source_rule': self.id,
|
|
'generated_by_rule': True,
|
|
'type': 'budgeted',
|
|
'p_r': self.p_r,
|
|
'product': self.fee_product.id,
|
|
'supplier': (
|
|
getattr(self.supplier, 'id', None)
|
|
or getattr(getattr(contract, 'party', None), 'id', None)),
|
|
'mode': self.mode,
|
|
'price': self.price if self.price is not None else Decimal(0),
|
|
'currency': (
|
|
getattr(self.currency, 'id', None)
|
|
or getattr(getattr(contract, 'currency', None), 'id', None)),
|
|
'unit': (
|
|
getattr(self.unit, 'id', None)
|
|
or getattr(getattr(line, 'unit', None), 'id', None)),
|
|
'quantity': (
|
|
self.quantity if self.quantity is not None
|
|
else getattr(line, 'quantity_theorical', None)
|
|
or getattr(line, 'quantity', None)),
|
|
'qt_state': getattr(self.qt_state, 'id', None),
|
|
'weight_type': self.weight_type,
|
|
}
|
|
values['line' if apply_on == 'purchase_line' else 'sale_line'] = line.id
|
|
return values
|
|
|
|
@classmethod
|
|
def apply_to_lines(cls, lines, apply_on, auto_only=False):
|
|
Fee = Pool().get('fee.fee')
|
|
rules = cls._rules_for_lines(apply_on, auto_only=auto_only)
|
|
created = []
|
|
for line in lines:
|
|
if not getattr(line, 'id', None):
|
|
continue
|
|
for rule in rules:
|
|
if not rule._matches_line(line, apply_on):
|
|
continue
|
|
if rule._existing_fee(line, apply_on):
|
|
continue
|
|
values = rule._fee_values(line, apply_on)
|
|
if not (
|
|
values.get('supplier') and values.get('currency')
|
|
and values.get('unit')):
|
|
logger.info(
|
|
"FEE_RULE_SKIPPED_MISSING_VALUES rule=%s line=%s",
|
|
rule.id, line.id)
|
|
continue
|
|
with Transaction().set_context(
|
|
_purchase_trade_skip_fee_rules=True):
|
|
created.extend(Fee.create([values]))
|
|
return created
|
|
|
|
|
|
class FeeLots(ModelSQL,ModelView):
|
|
|
|
"Fee lots"
|
|
__name__ = 'fee.lots'
|
|
|
|
fee = fields.Many2One('fee.fee',"Fee",required=True, ondelete='CASCADE')
|
|
lot = fields.Many2One('lot.lot',"Lot",required=True, ondelete='CASCADE')
|
|
|
|
@classmethod
|
|
def create(cls, vlist):
|
|
records = super().create(vlist)
|
|
Fee = Pool().get('fee.fee')
|
|
fee_ids = {
|
|
record.fee.id for record in records if getattr(record, 'fee', None)}
|
|
for fee in Fee.browse(list(fee_ids)):
|
|
fee.sync_quantity_from_lots()
|
|
fee.adjust_purchase_values()
|
|
Fee.assert_quantities_consistency(Fee.browse(list(fee_ids)))
|
|
Fee._regenerate_fee_pnl(
|
|
fees=Fee.browse(list(fee_ids)),
|
|
lots=[record.lot for record in records if getattr(record, 'lot', None)])
|
|
return records
|
|
|
|
@classmethod
|
|
def write(cls, *args):
|
|
old_lots = [
|
|
record.lot for record in sum(args[::2], [])
|
|
if getattr(record, 'lot', None)]
|
|
super().write(*args)
|
|
records = sum(args[::2], [])
|
|
Fee = Pool().get('fee.fee')
|
|
fee_ids = {
|
|
record.fee.id for record in records if getattr(record, 'fee', None)}
|
|
for fee in Fee.browse(list(fee_ids)):
|
|
fee.sync_quantity_from_lots()
|
|
fee.adjust_purchase_values()
|
|
Fee.assert_quantities_consistency(Fee.browse(list(fee_ids)))
|
|
Fee._regenerate_fee_pnl(
|
|
fees=Fee.browse(list(fee_ids)),
|
|
lots=old_lots + [
|
|
record.lot for record in records
|
|
if getattr(record, 'lot', None)])
|
|
|
|
@classmethod
|
|
def delete(cls, records):
|
|
Fee = Pool().get('fee.fee')
|
|
fee_ids = {
|
|
record.fee.id for record in records if getattr(record, 'fee', None)}
|
|
lots = [record.lot for record in records if getattr(record, 'lot', None)]
|
|
super().delete(records)
|
|
for fee in Fee.browse(list(fee_ids)):
|
|
fee.sync_quantity_from_lots()
|
|
fee.adjust_purchase_values()
|
|
Fee.assert_quantities_consistency(Fee.browse(list(fee_ids)))
|
|
Fee._regenerate_fee_pnl(
|
|
fees=Fee.browse(list(fee_ids)), lots=lots)
|
|
|
|
class FeeReport(
|
|
ModelSQL, ModelView):
|
|
"Fee Report"
|
|
__name__ = 'fee.report'
|
|
r_purchase_line = fields.Many2One('purchase.line', "Purchase line")
|
|
r_sale_line = fields.Many2One('sale.line', "Sale line")
|
|
r_shipment_in = fields.Many2One('stock.shipment.in', "Shipment in")
|
|
r_shipment_out = fields.Many2One('stock.shipment.out', "Shipment out")
|
|
r_shipment_internal = fields.Many2One('stock.shipment.internal', "Shipment internal")
|
|
r_fee_type = fields.Many2One('product.product', 'Fee type')
|
|
r_fee_counterparty = fields.Many2One('party.party', "Counterparty", required=True)
|
|
r_type = fields.Selection([
|
|
('ordered', 'Ordered'),
|
|
('budgeted', 'Budgeted'),
|
|
('actual', 'Actual')
|
|
], 'Type')
|
|
r_fee_paystatus = fields.Selection([
|
|
('pay', 'PAY'),
|
|
('rec', 'REC')
|
|
], 'Pay status')
|
|
r_mode = fields.Selection([
|
|
('lumpsum', 'Lump sum'),
|
|
('perqt', 'Per qt'),
|
|
('pprice', '% price'),
|
|
('pcost', '% cost price'),
|
|
], 'Mode', required=True)
|
|
|
|
r_fee_quantity = fields.Function(fields.Numeric("Qt",digits=(1,4)),'get_quantity')
|
|
r_fee_unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit')
|
|
r_purchase = fields.Many2One('purchase.purchase',"Purchase", ondelete='CASCADE')
|
|
r_sale = fields.Many2One('sale.sale',"Sale", ondelete='CASCADE')
|
|
r_fee_amount = fields.Function(fields.Numeric("Amount", digits=(1,4)),'get_amount')
|
|
r_inv = fields.Function(fields.Many2One('account.invoice',"Invoice"),'get_invoice')
|
|
r_dn_cn = fields.Function(fields.Many2One('account.invoice',"DN/CN"),
|
|
'get_dn_cn')
|
|
r_lot_invoice_status = fields.Function(fields.Selection([
|
|
('not', 'Not invoiced'),
|
|
('partial', 'Partially invoiced'),
|
|
('invoiced', 'Invoiced'),
|
|
], "Lot invoicing"), 'get_lot_invoice_status')
|
|
r_lot_payment_status = fields.Function(fields.Selection([
|
|
('not', 'Not paid'),
|
|
('partial', 'Partially paid'),
|
|
('paid', 'Paid'),
|
|
], "Lot payment"), 'get_lot_payment_status')
|
|
r_fee_payment_status = fields.Function(fields.Selection([
|
|
('not_paid', 'Not paid'),
|
|
('invoice_paid', 'Invoice paid'),
|
|
('dn_cn_paid', 'DN/CN paid'),
|
|
('all_paid', 'All paid'),
|
|
], "Fee payment"), 'get_fee_payment_status')
|
|
r_state = fields.Selection([
|
|
('not invoiced', 'Not invoiced'),
|
|
('invoiced', 'Invoiced'),
|
|
], string='State', readonly=True)
|
|
|
|
#r_fee_lots = fields.Function(fields.Many2Many('lot.lot', None, None, "Lots"),'get_lots')#, searcher='search_lots')
|
|
#r_lots = fields.Many2Many('fee.lots', 'fee', 'lot',"Lots",domain=[('id', 'in', Eval('r_fee_lots',[]))] )
|
|
r_fee_currency = fields.Many2One('currency.currency',"Currency")
|
|
r_fee_price = fields.Numeric("Price",digits=(1,4))
|
|
r_shipment_origin = fields.Function(
|
|
fields.Reference(
|
|
selection=[
|
|
("stock.shipment.in", "In"),
|
|
("stock.shipment.out", "Out"),
|
|
("stock.shipment.internal", "Internal"),
|
|
],
|
|
string="Shipment",
|
|
),
|
|
"get_shipment_origin",
|
|
)
|
|
|
|
def get_invoice(self,name):
|
|
Fee = Pool().get('fee.fee')
|
|
fee = Fee(self.id)
|
|
invoice = fee.get_invoice(name)
|
|
return getattr(invoice, 'id', invoice)
|
|
|
|
def get_dn_cn(self, name):
|
|
Fee = Pool().get('fee.fee')
|
|
fee = Fee(self.id)
|
|
dn_cn = getattr(fee, 'dn_cn', None)
|
|
return getattr(dn_cn, 'id', dn_cn)
|
|
|
|
@staticmethod
|
|
def _unique_invoices(invoices):
|
|
unique = []
|
|
keys = set()
|
|
for invoice in invoices:
|
|
if not invoice or getattr(invoice, 'state', None) == 'cancelled':
|
|
continue
|
|
key = getattr(invoice, 'id', None) or id(invoice)
|
|
if key not in keys:
|
|
keys.add(key)
|
|
unique.append(invoice)
|
|
return unique
|
|
|
|
@classmethod
|
|
def _lot_invoice_data(cls, fee):
|
|
lots = fee._get_effective_fee_lots()
|
|
invoices = []
|
|
invoiced_lots = 0
|
|
for lot in lots:
|
|
lot_invoices = []
|
|
for name in ('invoice_line', 'invoice_line_prov'):
|
|
line = getattr(lot, name, None)
|
|
if line:
|
|
lot_invoices.append(getattr(line, 'invoice', None))
|
|
lot_invoices = cls._unique_invoices(lot_invoices)
|
|
if lot_invoices:
|
|
invoiced_lots += 1
|
|
invoices.extend(lot_invoices)
|
|
return lots, invoiced_lots, cls._unique_invoices(invoices)
|
|
|
|
@staticmethod
|
|
def _invoice_payment_status(invoices):
|
|
if not invoices:
|
|
return 'not'
|
|
statuses = []
|
|
for invoice in invoices:
|
|
payment_lines = list(
|
|
getattr(invoice, 'payment_lines', None) or [])
|
|
if not payment_lines:
|
|
statuses.append('not')
|
|
elif (getattr(invoice, 'state', None) == 'paid'
|
|
or getattr(invoice, 'amount_to_pay', None) == 0):
|
|
statuses.append('paid')
|
|
else:
|
|
statuses.append('partial')
|
|
if all(status == 'paid' for status in statuses):
|
|
return 'paid'
|
|
if any(status != 'not' for status in statuses):
|
|
return 'partial'
|
|
return 'not'
|
|
|
|
def get_lot_invoice_status(self, name):
|
|
Fee = Pool().get('fee.fee')
|
|
lots, invoiced_lots, _ = self._lot_invoice_data(Fee(self.id))
|
|
if not lots or not invoiced_lots:
|
|
return 'not'
|
|
if invoiced_lots == len(lots):
|
|
return 'invoiced'
|
|
return 'partial'
|
|
|
|
def get_lot_payment_status(self, name):
|
|
Fee = Pool().get('fee.fee')
|
|
_, _, invoices = self._lot_invoice_data(Fee(self.id))
|
|
return self._invoice_payment_status(invoices)
|
|
|
|
@classmethod
|
|
def _fee_payment_status(cls, invoice, dn_cn):
|
|
invoice_paid = (
|
|
cls._invoice_payment_status(cls._unique_invoices([invoice]))
|
|
== 'paid')
|
|
dn_cn_paid = (
|
|
cls._invoice_payment_status(cls._unique_invoices([dn_cn]))
|
|
== 'paid')
|
|
if invoice_paid and dn_cn_paid:
|
|
return 'all_paid'
|
|
if invoice_paid:
|
|
return 'invoice_paid'
|
|
if dn_cn_paid:
|
|
return 'dn_cn_paid'
|
|
return 'not_paid'
|
|
|
|
def get_fee_payment_status(self, name):
|
|
Fee = Pool().get('fee.fee')
|
|
fee = Fee(self.id)
|
|
return self._fee_payment_status(
|
|
fee.get_invoice('inv'), getattr(fee, 'dn_cn', None))
|
|
|
|
def get_shipment_origin(self, name):
|
|
if self.r_shipment_in:
|
|
return 'stock.shipment.in,' + str(self.r_shipment_in.id)
|
|
elif self.r_shipment_out:
|
|
return 'stock.shipment.out,' + str(self.r_shipment_out.id)
|
|
elif self.r_shipment_internal:
|
|
return 'stock.shipment.internal,' + str(self.r_shipment_internal.id)
|
|
return None
|
|
|
|
|
|
|
|
# def get_lots(self, name):
|
|
# if self.r_purchase_line:
|
|
# return self.r_purchase_line.lots
|
|
|
|
def get_unit(self, name):
|
|
if self.r_purchase_line:
|
|
return self.r_purchase_line.unit
|
|
|
|
def get_quantity(self,name=None):
|
|
Fee = Pool().get('fee.fee')
|
|
fee = Fee(self.id)
|
|
return fee.get_quantity()
|
|
|
|
def get_amount(self,name=None):
|
|
Fee = Pool().get('fee.fee')
|
|
fee = Fee(self.id)
|
|
return fee.get_amount()
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def invoice(cls, reports):
|
|
Fee = Pool().get('fee.fee')
|
|
Fee.invoice(Fee.browse([report.id for report in reports]))
|
|
|
|
@classmethod
|
|
def table_query(cls):
|
|
FeeReport = Pool().get('fee.fee')
|
|
fr = FeeReport.__table__()
|
|
FeeLots = Pool().get('fee.lots')
|
|
fl = FeeLots.__table__()
|
|
PurchaseLine = Pool().get('purchase.line')
|
|
pl = PurchaseLine.__table__()
|
|
SaleLine = Pool().get('sale.line')
|
|
sl = SaleLine.__table__()
|
|
Lot = Pool().get('lot.lot')
|
|
lot = Lot.__table__()
|
|
|
|
fee_lot_lines = (
|
|
fl
|
|
.join(lot, 'LEFT', condition=fl.lot == lot.id)
|
|
.join(pl, 'LEFT', condition=lot.line == pl.id)
|
|
.join(sl, 'LEFT', condition=lot.sale_line == sl.id)
|
|
.select(
|
|
fl.fee.as_('fee'),
|
|
Min(lot.line).as_('purchase_line'),
|
|
Min(pl.purchase).as_('purchase'),
|
|
Min(lot.sale_line).as_('sale_line'),
|
|
Min(sl.sale).as_('sale'),
|
|
group_by=[fl.fee]))
|
|
|
|
context = Transaction().context
|
|
party = context.get('party')
|
|
fee_type = context.get('fee_type')
|
|
purchase = context.get('purchase')
|
|
sale = context.get('sale')
|
|
shipment_in = context.get('shipment_in')
|
|
shipment_out = context.get('shipment_out')
|
|
shipment_internal = context.get('shipment_internal')
|
|
invoice_status = context.get('invoice_status')
|
|
asof = context.get('asof')
|
|
todate = context.get('todate')
|
|
wh = ((fr.create_date >= asof) & ((fr.create_date-datetime.timedelta(1)) <= todate))
|
|
if party:
|
|
wh &= (fr.fee_counterparty == party)
|
|
if fee_type:
|
|
wh &= (fr.fee_type == fee_type)
|
|
purchase_expr = fee_lot_lines.purchase
|
|
purchase_line_expr = fee_lot_lines.purchase_line
|
|
sale_expr = fee_lot_lines.sale
|
|
if purchase:
|
|
wh &= (purchase_expr == purchase)
|
|
if sale:
|
|
wh &= (sale_expr == sale)
|
|
if shipment_in:
|
|
wh &= (fr.shipment_in == shipment_in)
|
|
if invoice_status == 'not_invoiced':
|
|
wh &= (fr.state == 'not invoiced')
|
|
elif invoice_status == 'invoiced_without_dn_cn':
|
|
wh &= ((fr.state == 'invoiced') & (fr.dn_cn == Null))
|
|
elif invoice_status == 'invoiced_with_dn_cn':
|
|
wh &= ((fr.state == 'invoiced') & (fr.dn_cn != Null))
|
|
# if shipment_out:
|
|
# wh &= (fr.shipment_out == shipment_out)
|
|
|
|
query = fr.join(
|
|
fee_lot_lines, 'LEFT', condition=fr.id == fee_lot_lines.fee
|
|
).select(
|
|
Literal(0).as_('create_uid'),
|
|
CurrentTimestamp().as_('create_date'),
|
|
Literal(None).as_('write_uid'),
|
|
Literal(None).as_('write_date'),
|
|
fr.id.as_('id'),
|
|
purchase_line_expr.as_('r_purchase_line'),
|
|
fee_lot_lines.sale_line.as_('r_sale_line'),
|
|
fr.shipment_in.as_('r_shipment_in'),
|
|
Literal(None).as_('r_shipment_out'),
|
|
fr.shipment_internal.as_('r_shipment_internal'),
|
|
fr.product.as_('r_fee_type'),
|
|
fr.supplier.as_('r_fee_counterparty'),
|
|
fr.type.as_('r_type'),
|
|
fr.p_r.as_('r_fee_paystatus'),
|
|
fr.mode.as_('r_mode'),
|
|
fr.state.as_('r_state'),
|
|
purchase_expr.as_('r_purchase'),
|
|
sale_expr.as_('r_sale'),
|
|
#fr.amount.as_('r_fee_amount'),
|
|
fr.price.as_('r_fee_price'),
|
|
fr.currency.as_('r_fee_currency'),
|
|
#fr.fee_lots.as_('r_fee_lots'),
|
|
#fr.lots.as_('r_lots'),
|
|
where=wh)
|
|
|
|
return query
|
|
|
|
@classmethod
|
|
def search_rec_name(cls, name, clause):
|
|
_, operator, operand, *extra = clause
|
|
if operator.startswith('!') or operator.startswith('not '):
|
|
bool_op = 'AND'
|
|
else:
|
|
bool_op = 'OR'
|
|
code_value = operand
|
|
if operator.endswith('like') and is_full_text(operand):
|
|
code_value = lstrip_wildcard(operand)
|
|
return [bool_op,
|
|
('r_fee_type', operator, operand, *extra),
|
|
('r_fee_counterparty', operator, operand, *extra),
|
|
]
|
|
|
|
class FeeContext(ModelView):
|
|
"Fee Context"
|
|
__name__ = 'fee.context'
|
|
|
|
asof = fields.Date("As of")
|
|
todate = fields.Date("To")
|
|
party = fields.Many2One('party.party', "Counterparty")
|
|
fee_type = fields.Many2One('product.product', 'Fee type')
|
|
invoice_status = fields.Selection([
|
|
(None, ''),
|
|
('not_invoiced', 'Not invoiced'),
|
|
('invoiced_without_dn_cn', 'Invoiced without DN/CN'),
|
|
('invoiced_with_dn_cn', 'Invoiced with DN/CN'),
|
|
], "Invoice status")
|
|
purchase = fields.Many2One('purchase.purchase', "Purchase")
|
|
sale = fields.Many2One('sale.sale', "Sale")
|
|
shipment_in = fields.Many2One('stock.shipment.in',"Shipment In")
|
|
shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out")
|
|
shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal")
|
|
|
|
@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()
|