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)

    def _get_per_quantity_amount_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)

    @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))
                return self._unsigned_amount(round(
                    self._get_per_quantity_amount_quantity()
                    * 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()
