# This file is part of Tryton.  The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id, If
from trytond.model import (ModelSQL, ModelView, dualmethod)
from trytond.tools import (
    cursor_dict, is_full_text, lstrip_wildcard)
from trytond.transaction import Transaction, inactive_records
from decimal import getcontext, Decimal, ROUND_HALF_UP
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
from sql.conditionals import Case
from sql import Column, Literal, Union, Select
from sql.conditionals import Coalesce
from sql.functions import CurrentTimestamp, DateTrunc, Abs
from sql.operators import Exists
from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction
from itertools import chain, groupby
from operator import itemgetter
import datetime
import json
import logging
from trytond.exceptions import UserWarning, UserError
from trytond.modules.purchase_trade.service import ContractFactory
from trytond.modules.purchase_trade.company_defaults import (
    default_itsa_bulk_unit, default_itsa_unit, is_itsa_company)

logger = logging.getLogger(__name__)


def _fee_invoice_action(fee, invoice_action):
    invoice = fee.get_invoice('inv') if fee else None
    if invoice_action == 'final' and invoice:
        return 'dn_cn'
    if invoice:
        return 'already_invoiced'
    return 'standard'

class LotAccountingGraph(ModelSQL,ModelView):
    "Lot accounting graph"
    __name__ = 'lot.accounting'

    lot = fields.Many2One('lot.lot',"Lot")
    name = fields.Char("Name")
    graph = fields.Text("Graph")
    flow_graph = fields.Function(fields.Text("Flux comptable (graph)"),
                                 'get_flow_graph')

    def get_flow_graph(self, name=None):
        return self.lot.get_flow_graph()

class LotMove(ModelSQL,ModelView):
    "Lots moves"
    __name__ = 'lot.move'

    lot = fields.Many2One('lot.lot', "Lot", ondelete='CASCADE')
    move = fields.Many2One('stock.move', "Move", ondelete='CASCADE')
    sequence = fields.Integer("Sequence")

class Lot(metaclass=PoolMeta):
    __name__ = 'lot.lot'

    _quantity_consistency_epsilon = Decimal("0.00001")

    @classmethod
    def _round_consistency_quantity(cls, quantity):
        return Decimal(str(quantity or 0)).quantize(
            cls._quantity_consistency_epsilon,
            rounding=ROUND_HALF_UP)

    @staticmethod
    def _same_record(left, right):
        return (
            left == right
            or getattr(left, 'id', left) == getattr(right, 'id', right))

    @classmethod
    def _convert_consistency_quantity(cls, quantity, from_unit, to_unit):
        if not from_unit or not to_unit or cls._same_record(from_unit, to_unit):
            return Decimal(str(quantity or 0))
        Uom = Pool().get('product.uom')
        factor = None
        rate = None
        if from_unit.category.id != to_unit.category.id:
            factor = 1
            rate = 1
        return Decimal(Uom.compute_qty(
            from_unit, float(quantity or 0), to_unit, True, factor, rate))

    @classmethod
    def _get_line_lots_for_consistency(cls, line):
        lots = list(getattr(line, 'lots', None) or [])
        virtual_lots = [
            lot for lot in lots
            if getattr(lot, 'lot_type', None) == 'virtual']
        physical_lots = [
            lot for lot in lots
            if getattr(lot, 'lot_type', None) == 'physic']
        return virtual_lots, physical_lots

    @classmethod
    def _get_lot_current_quantity_for_consistency(cls, lot, unit):
        quantity = lot.get_current_quantity_converted(unit=unit)
        return Decimal(str(quantity or 0))

    @classmethod
    def _get_lot_qt_quantity_for_consistency(cls, lqt, unit):
        quantity = Decimal(str(getattr(lqt, 'lot_quantity', 0) or 0))
        return cls._convert_consistency_quantity(
            quantity, getattr(lqt, 'lot_unit', None), unit)

    @classmethod
    def _format_consistency_error(cls, line, message, values):
        line_name = (
            getattr(line, 'rec_name', None)
            or getattr(line, 'id', None)
            or line)
        detail = ", ".join(
            "%s=%s" % (key, cls._round_consistency_quantity(value))
            for key, value in values)
        return "Quantity consistency error on line %s: %s (%s)" % (
            line_name, message, detail)

    @classmethod
    def _assert_line_quantity_consistency(cls, line):
        if not line:
            return
        product = getattr(line, 'product', None)
        if getattr(product, 'type', None) == 'service':
            return
        theorical = getattr(line, 'quantity_theorical', None)
        if theorical is None:
            return

        unit = getattr(line, 'unit', None)
        virtual_lots, physical_lots = cls._get_line_lots_for_consistency(line)
        if not virtual_lots:
            return
        if len(virtual_lots) > 1:
            raise UserError(
                cls._format_consistency_error(
                    line,
                    "more than one virtual lot exists",
                    [('virtual_lots', len(virtual_lots))]))

        # Temporarily suspended: repesages successifs are no longer tied
        # reliably to the original lot.qt forecast split. We keep the helper
        # calls in place, but do not enforce the two historical invariants:
        # 1) physical lots + virtual lot must equal quantity_theorical
        # 2) sum(non-zero lot.qt) must cover the positive virtual lot quantity
        #
        # vlot = virtual_lots[0]
        # physical_quantity = sum(
        #     cls._get_lot_current_quantity_for_consistency(lot, unit)
        #     for lot in physical_lots)
        # virtual_quantity = cls._get_lot_current_quantity_for_consistency(
        #     vlot, unit)
        # theorical_quantity = Decimal(str(theorical or 0))
        # expected_diff = (
        #     cls._round_consistency_quantity(physical_quantity)
        #     + cls._round_consistency_quantity(virtual_quantity)
        #     - cls._round_consistency_quantity(theorical_quantity))
        # if expected_diff:
        #     raise UserError(
        #         cls._format_consistency_error(
        #             line,
        #             "physical lots + virtual lot must equal theoretical "
        #             "quantity",
        #             [
        #                 ('physical', physical_quantity),
        #                 ('virtual', virtual_quantity),
        #                 ('theoretical', theorical_quantity),
        #                 ('diff', expected_diff),
        #             ]))
        #
        # cls._assert_virtual_lot_open_quantity_consistency(vlot, line)

    @classmethod
    def _assert_virtual_lot_open_quantity_consistency(cls, vlot, line=None):
        LotQt = Pool().get('lot.qt')
        line = line or getattr(vlot, 'line', None) or getattr(vlot, 'sale_line', None)
        if not line:
            return
        unit = getattr(line, 'unit', None)
        if getattr(vlot, 'line', None):
            lotqts = LotQt.search([('lot_p', '=', vlot.id)])
        elif getattr(vlot, 'sale_line', None):
            lotqts = LotQt.search([('lot_s', '=', vlot.id)])
        else:
            return

        open_quantity = sum(
            cls._get_lot_qt_quantity_for_consistency(lqt, unit)
            for lqt in lotqts
            if Decimal(str(getattr(lqt, 'lot_quantity', 0) or 0)) != 0)
        virtual_quantity = cls._get_lot_current_quantity_for_consistency(
            vlot, unit)
        expected_open_quantity = max(virtual_quantity, Decimal(0))
        diff = (
            cls._round_consistency_quantity(open_quantity)
            - cls._round_consistency_quantity(expected_open_quantity))
        if diff < 0:
            raise UserError(
                cls._format_consistency_error(
                    line,
                    "lot.qt forecast quantity must not be lower than "
                    "virtual lot positive current quantity",
                    [
                        ('lot_qt', open_quantity),
                        ('virtual', virtual_quantity),
                        ('expected_open', expected_open_quantity),
                        ('diff', diff),
                    ]))

    @classmethod
    def _fresh_consistency_line(cls, line):
        if not line:
            return None
        line_id = getattr(line, 'id', None)
        if not line_id:
            return line
        if getattr(line, 'purchase', None):
            return Pool().get('purchase.line')(line_id)
        if getattr(line, 'sale', None):
            return Pool().get('sale.line')(line_id)
        return line

    @classmethod
    def assert_lines_quantity_consistency(cls, lines):
        seen = set()
        for line in lines:
            line = cls._fresh_consistency_line(line)
            line_id = (
                getattr(line, '__name__', None),
                getattr(line, 'id', id(line)))
            if line_id in seen:
                continue
            seen.add(line_id)
            cls._assert_line_quantity_consistency(line)

    @classmethod
    def assert_lots_quantity_consistency(cls, lots):
        lines = []
        for lot in lots:
            if getattr(lot, 'line', None):
                lines.append(lot.line)
            if getattr(lot, 'sale_line', None):
                lines.append(lot.sale_line)
        cls.assert_lines_quantity_consistency(lines)

    @staticmethod
    def skip_quantity_consistency():
        return Transaction().set_context(
            _purchase_trade_skip_quantity_consistency=True)

    line = fields.Many2One('purchase.line',"Purchase",ondelete='CASCADE')
    move = fields.Function(fields.Many2One('stock.move',"Move"),'get_current_move')
    lot_move = fields.One2Many('lot.move','lot',"Move")
    invoice_line = fields.Many2One('account.invoice.line',"Purch.Invoice line")
    invoice_line_prov = fields.Many2One('account.invoice.line',"Purch. Invoice line prov")
    sale_invoice_line = fields.Many2One('account.invoice.line',"Sale Invoice line")
    sale_invoice_line_prov = fields.Many2One('account.invoice.line',"Sale Invoice line prov")
    sale_invoice_padding = fields.Numeric(
        "Sale invoice padding", digits='lot_unit_line', readonly=True)
    delta_qt = fields.Numeric("Delta Qt")
    delta_pr = fields.Numeric("Delta Pr")
    delta_amt = fields.Numeric("Delta Amt")
    warrant_nb = fields.Char("Warrant Nb")
    lot_chunk_key = fields.Integer("Chunk key")
    planned_from_location = fields.Many2One(
        'stock.location', "Planned From")
    planned_to_location = fields.Many2One(
        'stock.location', "Planned To")
    planned_transport_type = fields.Selection([
        (None, ''),
        ('vessel', "Vessel"),
        ('truck', "Truck"),
        ('container', "Container"),
        ], "Planned Transport")
    planned_from_date = fields.Date("Planned From Date")
    planned_to_date = fields.Date("Planned To Date")
    planned_note = fields.Char("Planning Note")
    #fees = fields.Many2Many('fee.lots', 'lot', 'fee',"Fees")
    dashboard = fields.Many2One('purchase.dashboard',"Dashboard")
    pivot = fields.Function(
        fields.Text('Pivot'), 'get_pivot'
    )
    vis_data = fields.Function(
        fields.Text('Graphe JSON'), 'get_vis_data'
    )

    flow_graph = fields.Function(fields.Text("Flux comptable (graph)"),
                                 'get_flow_graph')

    def is_planned_transport(self):
        return bool(
            self.planned_from_location
            or self.planned_to_location
            or self.planned_transport_type
            or self.planned_from_date
            or self.planned_to_date
            or self.planned_note)

    @classmethod
    def default_lot_unit(cls):
        default = getattr(super(), 'default_lot_unit', None)
        if default:
            return default()

    @classmethod
    def default_lot_unit_line(cls):
        if is_itsa_company():
            return default_itsa_unit()
        default = getattr(super(), 'default_lot_unit_line', None)
        if default:
            return default()

    def get_pivot(self,name=None):
        AccountMoveLine = Pool().get('account.move.line')
        Account = Pool().get('account.account')
        Stock = Pool().get('stock.move')
        Invoice = Pool().get('account.invoice')
        ForexP = Pool().get('forex.cover.physical.contract')
        ForexS = Pool().get('forex.cover.physical.sale')
        InvoiceLine = Pool().get('account.invoice.line')
        Currency = Pool().get('currency.currency')
        aml = AccountMoveLine.search([('lot','=',self.id)])
        logger.info("GET_PIVOT:%s",aml)
        forexP = None
        forexS = None
        if self.line:
            forexP = ForexP.search(['contract','=',self.line.purchase.id])
            logger.info("GET_PIVOT:%s",forexP)
        if self.sale_line:
            forexS = ForexS.search(['contract','=',self.sale_line.sale.id])
        if forexP:
            forex = AccountMoveLine.search([('origin','=','forex.forex,'+str(forexP[0].forex.id))])
            if forex:
                aml += AccountMoveLine.search(['move','=',forex[0].move.id])
                aml_ = AccountMoveLine.search([('revaluate','=',forex[1].id)])
                if aml_:
                    aml += AccountMoveLine.search(['move','=',aml_[0].move.id])
                logger.info("GET_PIVOT:%s",aml_)
        pivot_data = {
            "data": [],
            "options": None
        }
        if aml:
            for l in aml:
                acc= Account(l.account)
                ct_type = 'Purchase'
                pr_type = 'Goods'
                acc_label = 'Account event'
                move_line = str(l.id)
                journal = l.move.journal.id
                product = self.lot_product.name
                ct_number = ''
                amount = l.debit if l.debit != 0 else -l.credit
                second_amount = ''
                rate = 1
                if l.amount_second_currency:
                    second_amount = float(abs(l.amount_second_currency))
                    rate = round(abs(amount / abs(l.amount_second_currency)),4)
                second_curr = None
                if l.second_currency:
                    second_curr = Currency(l.second_currency.id).symbol
                else:
                    second_curr = Currency(l.move.company.currency.id).symbol
                
                # if l.account.type.payable and l.account.type.statement == 'income':
                #     amount = -amount
                ori = None #l.origin
                if not ori:
                    ori = l.move.origin
                if ori:
                    origin, id = str(ori).split(',')
                    if origin == 'stock.move':
                        acc_label = '1-Reception'
                        sm = Stock(int(id))
                        if sm.to_location.type == 'customer':
                            ct_type = 'Sale'
                            if self.sale_line:
                                ct_number = self.sale_line.sale.number
                        else:
                            ct_type = 'Purchase'
                            if self.line:
                                ct_number = self.line.purchase.number
                    elif origin == 'account.invoice':
                        inv = Invoice(int(id))
                        product = None
                        if inv.lines[0].product:
                            product = inv.lines[0].product.name
                        if inv.type == 'out':
                            ct_type = 'Sale'
                            if self.sale_line:
                                ct_number = self.sale_line.sale.number
                        else:
                            ct_type = 'Purchase'
                            if self.line:
                                ct_number = self.line.purchase.number
                        if (journal == 1 and (l.account.id == 824 or l.account.id == 766)):
                            acc_label = 'COG Goods'
                        elif journal == 3:
                            acc_label = '4-Payment'
                        else:
                            if "Payment of invoice" in (l.move.description if l.move.description else ""):
                                acc_label = '4-Payment'
                            else:
                                if 'Prepayment' in (inv.description if inv.description else ""):
                                    acc_label = '0-Prepayment'
                                else:
                                    if inv.reference == 'Provisional':
                                        acc_label = '2-Prov'
                                        if l.move.description == 'Adjustment':
                                            acc_label = '2-Adjustment'
                                    else:
                                        acc_label = '3-Final'
                else:
                    if journal == 6:
                        # rate = 1
                        if l.move.IsBankMove():
                            acc_label = '4-Forex reval'
                        else:    
                            acc_label = '4-Inv reval'
                    elif journal == 8:
                        acc_label = '4-Payment Fx'

                pivot_data["data"].append(
                    {
                        "lot": self.lot_name,
                        "ct type": ct_type,
                        "pr type": pr_type,
                        "ct number": ct_number, 
                        "event": acc_label,
                        "product": product,
                        "amount": str(amount),
                        "account": str(acc.code),
                        "move": l.move.number,
                        "move line": move_line,
                        "journal": journal,
                        "event_date": str(l.move.date),
                        "rate": str(rate),
                        "tr_amount": str(second_amount),
                        "Curr": second_curr
                    }
                )
        
            pivot_data['options'] = {
                "rows": ["lot","ct type","event_date","event","move","Curr","rate"],
                "cols": ["account"],                
                "aggregatorName": "Sum",
                "vals": ["amount"]
            }
            return "pivotjson:" + json.dumps(pivot_data)

    def get_flow_graph(self, name=None):
        AccountMove = Pool().get('account.move')
        colorBox = '#AABDAF'
        colorBoxB = '#5C665E'
        colorBoxH = '#C2D7C7'
        colorBoxHB = '#7A877D'
        colorCircle = '#C4D9C9'
        colorCircleB = '#617052'
        colorCircleH = '#DFF7E5'
        colorCircleHB = '#8EA67A'
        data = {
            'nodes': [
                {
                    'id': 'lot.lot ' + str(self.id),
                    'label': 'Lot\n' + self.lot_name,
                    'data_model': 'lot.lot',
                    'data_id': self.id,
                    'x': -300,
                    'y': 0,
                    'color': {
                        'background': colorBox,
                        'border': colorBoxB,
                        'highlight': {
                            'background':colorBoxH,
                            'border': colorBoxHB
                        }
                    },
                    'fixed': {
                        'x': False,
                        'y': True
                    }
                }
            ],
            'edges': []
        }

        data['nodes'].append({
            'id': 'purchase.purchase ' + str(self.line.purchase.id),
            'label': 'Purchase\n' + self.line.purchase.number + '\n' + self.line.purchase.party.name,
            'data_model': 'purchase.purchase',
            'data_id': self.line.purchase.id,
            'x': -900,
            'y': 0,
            'color': {
                'background': colorBox,
                'border': colorBoxB,
                'highlight': {
                    'background':colorBoxH,
                    'border': colorBoxHB
                }
            },
            'fixed': {
                'x': False,
                'y': True
            }
        })

        data['nodes'].append({
            'id': 'purchase.line ' + str(self.line.id),
            'label': 'Line\n' + self.line.product.name + '\n' + str(self.line.quantity),
            'data_model': 'purchase.line',
            'data_id': self.line.id,
            'x': -600,
            'y': 0,
            'color': {
                'background': colorBox,
                'border': colorBoxB,
                'highlight': {
                    'background':colorBoxH,
                    'border': colorBoxHB
                }
            },
            'fixed': {
                'x': False,
                'y': True
            }
        })

        data['edges'].append({
            'from': 'purchase.purchase ' + str(self.line.purchase.id),
            'to': 'purchase.line ' + str(self.line.id),
            'label': '',
            'length': 300,
            'color': {
                'color': '#267F82',
                'highlight': '#1D5F62',
                'hover': '#1D5F62'
            }
        })

        data['edges'].append({
            'from': 'purchase.line ' + str(self.line.id),
            'to': 'lot.lot ' + str(self.id),
            'label': '',
            'length': 300,
            'color': {
                'color': '#267F82',
                'highlight': '#1D5F62',
                'hover': '#1D5F62'
            }
        })

        labLines = ''
        if self.lot_move:
            stock_node = {
                "id": "stock.move " + str(self.lot_move[0].move.id),
                "label": "Move\n"
                + self.lot_move[0].move.from_location.name
                + "\n"
                + self.lot_move[0].move.to_location.name,
                "data_model": "stock.move",
                "data_id": self.lot_move[0].move.id,
                "shape": "circle",
                "color": {
                    "background": colorCircle, 
                    "border": colorCircleB,
                    'highlight': {
                        'background':colorCircleH,
                        'border': colorCircleHB
                    }
                },
            }

            data['nodes'].append(stock_node)
            origin = 'stock.move,' + str(self.lot_move[0].move.id)
            am = AccountMove.search([('origin','=',origin)])
            if am:
                for l in am[0].lines:
                    labLines += 'Debit ' if l.debit != 0 else 'Credit '
                    labLines += str(l.account.code) + "\n"
                data["edges"].append(
                    {
                        "from": "lot.lot " + str(self.id),
                        "to": "stock.move " + str(self.lot_move[0].move.id),
                        "label": labLines,
                        "data_model": "account.move",
                        "data_id": am[0].id,
                        "length": 300,
                        'color': {
                            'color': '#267F82',
                            'highlight': '#1D5F62',
                            'hover': '#1D5F62'
                        }
                    }
                )

            
        if self.lot_shipment_in:
            shipment_node = {
                "id": "stock.shipment.in " + str(self.lot_shipment_in.id),
                "label": "Shipment\n" + self.lot_shipment_in.reference,
                "data_model": "stock.shipment.in",
                "data_id": self.lot_shipment_in.id,
                "title": "Vessel in transit",
                "x": 0,
                "y": 0,
                'color': {
                    'background': colorBox,
                    'border': colorBoxB,
                    'highlight': {
                        'background':colorBoxH,
                        'border': colorBoxHB
                    }
                },
                "fixed": {"x": False, "y": True},
            }

            data['nodes'].append(shipment_node)
            if self.lot_move:
                data["edges"].append(
                    {
                        "from": "lot.lot " + str(self.id),
                        "to": "stock.move " + str(self.lot_move[0].move.id),
                        "label": labLines,
                        "length": 300,
                        'color': {
                            'color': '#267F82',
                            'highlight': '#1D5F62',
                            'hover': '#1D5F62'
                        }
                    }
                )
                data["edges"].append(
                    {
                        "from": "stock.move " + str(self.lot_move[0].move.id),
                        "to": "stock.shipment.in " + str(self.lot_shipment_in.id),
                        "length": 300,
                        'color': {
                            'color': '#267F82',
                            'highlight': '#1D5F62',
                            'hover': '#1D5F62'
                        }
                    }
                )

            else:
                data["edges"].append(
                    {
                        "from": "lot.lot " + str(self.id),
                        "to": "stock.shipment.in " + str(self.lot_shipment_in.id),
                        "length": 300,
                        'color': {
                            'color': '#267F82',
                            'highlight': '#1D5F62',
                            'hover': '#1D5F62'
                        }
                    }
                )


        if self.invoice_line_prov:
            prov_node = {
                "id": "account.invoice " + str(self.invoice_line_prov.invoice.id),
                "label": "Prov.\n" + self.invoice_line_prov.invoice.number,
                "data_model": "account.invoice",
                "data_id": self.invoice_line_prov.invoice.id,
                "length": 300,
                "shape": "circle",
                "color": {
                    "background": colorCircle, 
                    "border": colorCircleB,
                    'highlight': {
                        'background':colorCircleH,
                        'border': colorCircleHB
                    }
                }
            }

            data['nodes'].append(prov_node)
            origin = 'account.invoice,' + str(self.invoice_line_prov.invoice.id)
            am = AccountMove.search([('origin','=',origin)])
            if am:
                labLines = ''
                for l in am[0].lines:
                    labLines += 'Debit ' if l.debit != 0 else 'Credit '
                    labLines += str(l.account.code) + "\n"
                data["edges"].append(
                    {
                        "from": "account.invoice " + str(self.invoice_line_prov.invoice.id),
                        "to": "lot.lot " + str(self.id),
                        "label": labLines,
                        "data_model": "account.move",
                        "data_id": am[0].id,
                        "length": 300,
                        'color': {
                            'color': '#267F82',
                            'highlight': '#1D5F62',
                            'hover': '#1D5F62'
                        }
                    }
                )
        
        return "visjson:" + json.dumps(data)

    def get_vis_data(self, name):
        data = {
            "nodes": [{"id": self.id, "label": self.lot_name}],
            "edges": [{"from": self.id, "to": self.id}]
        }
        return 'visjson:' + json.dumps(data)

    def get_forex_rate(self, amount):
        """
        Calcule le taux de change moyen applicable à un montant donné,
        en fonction des contrats de couverture Forex liés à l'achat (lot).
        
        Retourne :
            rate (float): taux moyen pondéré appliqué
            left_amount (float): montant restant à couvrir (si non totalement couvert)
        """
        rate = 0.0
        left_amount = amount

        Forex = Pool().get('forex.forex')
        ForexP = Pool().get('forex.cover.physical.contract')

        # On récupère les contrats forex physiques liés au contrat d'achat
        forex_contracts = ForexP.search([
            ('contract', '=', self.line.purchase.id)
        ])

        if not forex_contracts:
            return None, amount  # Aucun contrat forex trouvé

        total_covered = sum([fc.amount for fc in forex_contracts])
        weighted_sum = Decimal(0)

        # Cas 1 : Montant totalement couvert ou surcouvert
        if total_covered >= amount:
            remaining_to_cover = amount
            for fc in forex_contracts:
                if remaining_to_cover <= 0:
                    break

                covered_part = min(fc.amount, remaining_to_cover)
                forex = Forex(fc.forex.id)
                rate_part = forex.rate

                weighted_sum += covered_part * rate_part
                remaining_to_cover -= covered_part

            rate = weighted_sum / amount
            left_amount = Decimal(0)

        # Cas 2 : Montant partiellement couvert
        else:
            for fc in forex_contracts:
                forex = Forex(fc.forex)
                weighted_sum += fc.amount * forex.rate

            rate = weighted_sum / total_covered if total_covered > 0 else None
            left_amount = amount - total_covered
        logger.info("GET_FOREX:%s",rate)
        logger.info("GET_FOREX2:%s",left_amount)
        return rate, left_amount

    def get_cog(self):
        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', '=', self.id),
            ('fee', '=', None),
            ('account', '=', self.lot_product.account_stock_in_used.id),
            ('origin', 'ilike', '%stock.move%'),
        ])

        logger.info("GET_COG:%s",ml)
        if ml:
            return round(Decimal(sum([e.credit-e.debit for e in ml if e.description != 'Delivery'])),2)
        else:
            if self.lot_type == 'physic':
                sup_mov = self.get_last_supplier_move()
                if sup_mov:
                    logger.info("GET_COG_MOV:%s",sup_mov)
                    unit_price = sup_mov.unit_price
                    unit_price = Uom.compute_price(
                        self.lot_product.default_uom, unit_price, sup_mov.unit)
                    amount = sup_mov.company.currency.round(
                            Decimal(str(sup_mov.quantity)) * unit_price)
                    if account_configuration.stock_fx_forex == 'forex':
                        frate,amt = self.get_forex_rate(amount)
                        if frate:
                            amount = round(frate * amount,2)
                            with Transaction().set_context(date=Date.today()):
                                amount += Currency.compute(sup_mov.currency,
                                    amt, sup_mov.company.currency)
                            amount_converted = round(amount,2)
                        else:
                            with Transaction().set_context(date=sup_mov.effective_date):
                                amount_converted = Currency.compute(sup_mov.currency,
                                amount, sup_mov.company.currency)
                    else:
                        with Transaction().set_context(date=sup_mov.effective_date):
                            amount_converted = Currency.compute(sup_mov.currency,
                            amount, sup_mov.company.currency)
                    return round(amount_converted,2)
            return Decimal(0)

    def IsDelivered(self):
        if self.lot_move:
            lm = sorted(self.lot_move, key=lambda x: x.sequence, reverse=True)
            for m in lm:
                if m.move.to_location.type == 'customer' and m.move.state == 'done':
                    return True
        return False
    
    def get_received_move(self):
        if self.lot_move:
            lm = sorted(self.lot_move, key=lambda x: x.sequence, reverse=True)
            for m in lm:
                if m.move.from_location.type == 'supplier' and m.move.state == 'done':
                    return m.move
        return None
    
    def GetShipment(self,type):
        if type == 'in':
            m = self.get_current_supplier_move()
            if m and m.shipment:
                origin,id = str(m.shipment).split(',')
                if origin == 'stock.shipment.in':
                    return id 

    def get_current_customer_move(self):
        if self.lot_move:
            lm = sorted(self.lot_move, key=lambda x: x.sequence, reverse=True)
            for m in lm:
                if m.move.to_location.type == 'customer' and m.move.state != 'done' and m.move.from_location.type != 'supplier':
                    return m.move
    
    def get_current_supplier_move(self):
        if self.lot_move:
            lm = sorted(self.lot_move, key=lambda x: x.sequence, reverse=True)
            for m in lm:
                if m.move.from_location.type == 'supplier' and m.move.state != 'done':
                    return m.move
                
    def get_last_supplier_move(self):
        if self.lot_move:
            lm = sorted(self.lot_move, key=lambda x: x.sequence, reverse=True)
            for m in lm:
                if m.move.from_location.type == 'supplier':
                    return m.move

    def get_current_move(self,name):
        return self.get_current_move_by_seq()

    def get_current_move_by_seq(self,seq=-1):
        if self.lot_move:
            if seq == -1:
                lm = sorted(self.lot_move, key=lambda x: x.sequence, reverse=True)
            if seq == 0:
                lm = sorted(self.lot_move, key=lambda x: x.sequence, reverse=False)
            if lm:
                return lm[0].move

    def getSupplier(self):
        if self.line:
            return Pool().get('purchase.purchase')(self.line.purchase).party.id

    def getPurchase(self):
        if self.line:
            return self.line.purchase.id

    def getVlot_p(self):
        if self.line:
            return [l for l in self.line.lots if l.lot_type=='virtual'][0]

    def getVlot_s(self):
        if self.sale_line:
            return [l for l in self.sale_line.lots if l.lot_type=='virtual'][0]

    def getLotQt(self):
        LotQt = Pool().get('lot.qt')
        if self.sale_line and not self.line:
            return LotQt.search(['lot_s','=',self.id])
        else:
            return LotQt.search(['lot_p','=',self.id])

    @classmethod
    def default_dashboard(cls):
        return 1

    @classmethod
    def _recompute_virtual_lot(cls, line):
        LotHist = Pool().get('lot.qt.hist')

        if not line:
            return

        lots = list(line.lots or [])
        physical_lots = [l for l in lots if l.lot_type == 'physic']
        virtual_lots = [l for l in lots if l.lot_type == 'virtual']

        total_physic = sum(l.get_current_quantity_converted() for l in physical_lots)
        theorical = line.quantity_theorical if hasattr(line, 'quantity_theorical') else line.quantity
        virtual_qty = round(theorical - total_physic, 5)
        if virtual_lots:
            vlot = virtual_lots[0]
            lh = vlot.lot_hist[0]
            lh.quantity = virtual_qty
            lh.gross_quantity = virtual_qty
            LotHist.save([lh])
        
    @classmethod
    def validate(cls, lots):
        super(Lot, cls).validate(lots)
        LotQt = Pool().get('lot.qt')
        StockMove = Pool().get('stock.move')
        virtual_lots_to_check = []
        for lot in lots:
            if lot.lot_type == 'virtual':
                if lot.line:
                    lqt = LotQt.search([('lot_p','=',lot.id)])
                    if len(lqt)==0 and not lot.line.created_by_code:
                        lot.createVirtualPart(lot.lot_quantity,None,None)    
                    # Wizard-created matched lines attach/split lot.qt after
                    # the virtual lot save; the final workflow check covers
                    # the complete state.
                    if not lot.line.created_by_code:
                        virtual_lots_to_check.append(lot)
                    
                if lot.sale_line:
                    lqt = LotQt.search([('lot_s','=',lot.id)])
                    if len(lqt)==0 and not lot.sale_line.created_by_code:
                        lot.createVirtualPart(lot.lot_quantity,None,lot.id,'only sale')
                    # Same rule as purchase side: do not check the transient
                    # save of a virtual lot created by matching code.
                    if not lot.sale_line.created_by_code:
                        virtual_lots_to_check.append(lot)
                
            #Recalculate total line quantity
            if lot.lot_type == 'physic':
                if lot.line:
                    cls._recompute_virtual_lot(Pool().get('purchase.line')(lot.line))
                    cls._recalc_line_quantity(lot.line,True)
                    sup_mov = lot.get_current_supplier_move()
                    if sup_mov:
                        sup_mov.quantity = lot.get_current_quantity_converted()
                        sup_mov.unit_price = lot.get_lot_price()
                        StockMove.save([sup_mov])
                    
                if lot.sale_line:
                    cls._recompute_virtual_lot(Pool().get('sale.line')(lot.sale_line))
                    cls._recalc_line_quantity(lot.sale_line,False)
                    cus_mov = lot.get_current_customer_move()
                    if cus_mov:                        
                        cus_mov.quantity = lot.get_current_quantity_converted()
                        cus_mov.unit_price = lot.get_lot_sale_price()
                        StockMove.save([cus_mov])
                    #if matched check that a move exist sale side
                    if lot.line:
                        if not lot.IsDelivered() and not lot.get_current_customer_move():
                            from_ = lot.sale_line.sale.from_location
                            to_ = lot.sale_line.sale.to_location
                            if from_ and to_ and not (from_.type=='supplier' and to_.type=='customer'):
                                Move = Pool().get('stock.move')
                                nm = Move()
                                nm.from_location = from_
                                nm.to_location = to_
                                nm.product = lot.lot_product
                                nm.unit = lot.sale_line.unit
                                nm.quantity = lot.get_current_quantity_converted()
                                nm.origin = lot.sale_line
                                nm.lot = lot
                                nm.shipment = None
                                nm.currency = lot.sale_line.currency
                                nm.unit_price = lot.get_lot_sale_price()
                                Move.save([nm])
            #check that lot with shipment are on this shipment fees
            if lot.lot_shipment_in:
                if lot.lot_shipment_in.fees:
                    for f in lot.lot_shipment_in.fees:
                        FeeLots = Pool().get('fee.lots')
                        fl = FeeLots.search(['fee','=',f.id])
                        if not fl:
                            fl = FeeLots()
                            fl.fee = f.id
                            fl.lot = lot.id
                            FeeLots.save([fl])

        if not Transaction().context.get(
                '_purchase_trade_skip_quantity_consistency'):
            cls.assert_lots_quantity_consistency(virtual_lots_to_check)
            
    def createVirtualPart(self,qt=0,sh=None,lot_s=None,mode='both'):
        lqt = LotQt()
        if self.lot_type == 'physic':
            lqt.lot_p = self.getVlot_p()
            lqt.lot_s = lot_s
        else:
            if mode =='both':
                lqt.lot_p = self.id
                lqt.lot_s = lot_s
            else:
                lqt.lot_s = lot_s
                lqt.lot_p = None
        if sh:
            str_sh = str(sh)
            index = str_sh.rfind(",")
            if index != -1:
                id = int(str_sh[index+1:].strip())
            if 'stock.shipment.in' in str_sh:
                lqt.lot_shipment_in = id
            elif 'stock.shipment.out' in str_sh:
                lqt.lot_shipment_out = id
            elif 'stock.shipment.internal' in str_sh:
                lqt.lot_shipment_internal = id
        lqt.lot_move = None
        lqt.lot_av = 'available'
        lqt.lot_status = 'forecast'
        lqt.lot_quantity = qt
        if self.line:
            lqt.lot_unit = self.line.unit
        if self.sale_line:
            lqt.lot_unit = self.sale_line.unit
        LotQt.save([lqt])

    def updateVirtualPart(self,qt=0,sh=None,lot_s=None,mode='both'):
        LotQt = Pool().get('lot.qt')
        vlot = self
        if self.lot_type == 'physic':
            vlot = self.getVlot_p()

        if  mode == 'only sale':
            vlot = None
        
        lqts = None
        if sh:
            str_sh = str(sh)
            index = str_sh.rfind(",")
            if index != -1:
                id = int(str_sh[index+1:].strip())
            if 'stock.shipment.in' in str_sh:
                lqts = LotQt.search([('lot_p','=',vlot),('lot_shipment_in','=',id),('lot_s','=',lot_s)])
            elif 'stock.shipment.internal' in str_sh:
                lqts = LotQt.search([('lot_p','=',vlot),('lot_shipment_internal','=',id),('lot_s','=',lot_s)])
            elif 'stock.shipment.out' in str_sh:
                lqts = LotQt.search([('lot_p','=',vlot),('lot_shipment_out','=',id),('lot_s','=',lot_s)])
        else:
            lqts = LotQt.search([('lot_p','=',vlot),('lot_s','=',lot_s),('lot_shipment_in','=',None),('lot_shipment_internal','=',None),('lot_shipment_out','=',None)])
        if lqts:
            lqts[0].lot_quantity += qt
            if lqts[0].lot_quantity < 0:
                lqts[0].lot_quantity = 0
            LotQt.save(lqts)
            return True
        return False

    def createMove(self,r,qt,sh):
        Move = Pool().get('stock.move')
        nm = Move()
        nm.from_location = r.lot_p.line.purchase.from_location
        nm.to_location = r.lot_p.line.purchase.to_location
        nm.product = r.lot_p.lot_product
        nm.unit = r.lot_unit
        nm.quantity = qt
        nm.origin = r.lot_p.line
        nm.shipment = sh
        nm.currency = r.lot_p.line.currency
        nm.unit_price = r.lot_p.line.unit_price
        Move.save([nm])
        return nm.id

    def create_shipment_move(self, shipment_origin):
        Move = Pool().get('stock.move')
        line = self.line
        if not line or not getattr(line, 'purchase', None):
            return
        unit = self.lot_unit or self.lot_unit_line or line.unit
        quantity = self.get_current_quantity_converted(0, unit)
        move = Move()
        move.from_location = line.purchase.from_location
        move.to_location = line.purchase.to_location
        move.product = self.lot_product
        move.unit = unit
        move.quantity = quantity
        move.origin = line
        move.shipment = shipment_origin
        move.currency = line.currency
        move.unit_price = line.unit_price
        move.lot = self
        Move.save([move])
        return move

    @classmethod
    def create(cls, vlist):
        vlist = [x.copy() for x in vlist]
        L = Pool().get('lot.lot')
        Uom = Pool().get('product.uom')
        for values in vlist:
            parent = values.get('lot_parent')
            if parent:
                #no split for virtual lot
                if values['lot_type']=='virtual':
                    return
                l = L(parent)
                balenw = round(Decimal((l.lot_quantity if l.lot_quantity else 0)/Decimal(l.lot_qt)),2)
                balegw = round(Decimal((l.lot_gross_quantity if l.lot_gross_quantity else 0)/Decimal(l.lot_qt)),2)
                nw = Decimal(int(values.get('lot_qt')) * balenw)
                gw = Decimal(int(values.get('lot_qt')) * balegw)
                if l.lot_quantity:
                    l.lot_quantity -= nw
                if l.lot_gross_quantity:
                    l.lot_gross_quantity -= gw
                if l.lot_qt:
                    l.lot_qt -= values.get('lot_qt')
                values['lot_quantity'] = nw
                values['lot_type'] = 'physic'
                values['lot_gross_quantity'] = gw
                values['line'] = l.line
                L.save([l])
            
        records = super().create(vlist)
        for record in records:
            if not record.lot_name:
                record.lot_name = str(record.id)
        cls.save(records)
        return records

    @classmethod
    def delete(cls, lots):
        pool = Pool()
        LL = pool.get('lot.lot')
        FeeLots = pool.get('fee.lots')
        lines_to_update = set()
        sale_lines_to_update = set()
        fees_to_sync = set()
        logger.info("DELETE_FROM_LOTS:%s",lots)
        for lot in lots:
            if lot.lot_type == 'physic':
                if lot.line:
                    lines_to_update.add(lot.line.id)
                if lot.sale_line:
                    sale_lines_to_update.add(lot.sale_line.id)
            if lot.lot_parent:
                pa = LL(lot.lot_parent)
                if pa:
                    pa.lot_qt += lot.lot_qt
                    pa.lot_quantity += lot.lot_quantity
                    pa.lot_gross_quantity += lot.lot_gross_quantity
                    LL.save([pa])
            logger.info("DELETE_FROM_LOTS2:%s",lot)
            fls = FeeLots.search(['lot','=',lot.id])
            if fls:
                fees_to_sync.update(
                    fl.fee.id for fl in fls if getattr(fl, 'fee', None))
                FeeLots.delete(fls)

        super(Lot, cls).delete(lots)

        for line_id in lines_to_update:
            cls._recompute_virtual_lot(Pool().get('purchase.line')(line_id))
            cls._recalc_line_quantity(line_id,True)

        for line_id in sale_lines_to_update:
            cls._recompute_virtual_lot(Pool().get('sale.line')(line_id))
            cls._recalc_line_quantity(line_id,False)

        cls.assert_lines_quantity_consistency(
            [Pool().get('purchase.line')(line_id) for line_id in lines_to_update])
        cls.assert_lines_quantity_consistency(
            [Pool().get('sale.line')(line_id) for line_id in sale_lines_to_update])

        Fee = Pool().get('fee.fee')
        for fee in Fee.browse(list(fees_to_sync)):
            fee.sync_quantity_from_lots()
            fee.adjust_purchase_values()

    @classmethod
    def _recalc_line_quantity(cls, line_id,purchase=True):
        PL = Pool().get('purchase.line')
        SL = Pool().get('sale.line')
        if purchase:
            line = PL(line_id)
        else:
            line = SL(line_id)
        qt = Decimal(0)
        lots = list(line.lots or [])
        for lot in lots:
            if len(lots) > 1:
                if lot.lot_type == 'physic':
                    qt += lot.get_current_quantity_converted()
            elif len(lots) == 1:
                qt = lot.get_current_quantity_converted()

        line.quantity = round(float(qt), 5)
        if purchase:
            PL.save([line])
        else:
            SL.save([line])
    
    # @classmethod
    # def write(cls, *args):
    #     Lot = Pool().get('lot.lot')
    #     super().write(*args)
    #     lots = sum(args[::2], [])
    
class LotQt(
        ModelSQL, ModelView):
    "Lots Qt"
    __name__ = 'lot.qt'

    lot_p = fields.Many2One('lot.lot',"Pur. lot")
    lot_s = fields.Many2One('lot.lot',"Sale lot")
    lot_quantity = fields.Numeric("Qt",digits=(1,5))
    lot_unit = fields.Many2One('product.uom', "Unit")
    lot_shipment_in = fields.Many2One('stock.shipment.in',"Shipment In")
    lot_shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out")
    lot_shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal")
    planned_from_location = fields.Many2One(
        'stock.location', "Planned From")
    planned_to_location = fields.Many2One(
        'stock.location', "Planned To")
    planned_transport_type = fields.Selection([
        (None, ''),
        ('vessel', 'Vessel'),
        ('truck', 'Truck'),
    ], "Planned Transport")
    planned_from_date = fields.Date("Planned From Date")
    planned_to_date = fields.Date("Planned To Date")
    planned_note = fields.Char("Planning Note")
    lot_move = fields.Many2One('stock.move',"Move")
    lot_av = fields.Selection([
        ('available', 'Available'),
        ('reserved', 'Reserved'),
        ('locked', 'Locked'),
        ('prov', 'Prov. inv'),
        ('invoiced', 'Invoiced')
    ], 'State')
    lot_status = fields.Selection([
        ('forecast', 'Forecast'),
        ('loading', 'Loading'),
        ('transit', 'Transit'),
        ('destination', 'Destination'),
        ('stock', 'Stock'),
        ('delivered', 'Delivered')
    ], 'Where')
    lot_visible = fields.Boolean("Visible")
    lot_shipment_origin = fields.Function(
        fields.Reference(
            selection=[
                ("stock.shipment.in", "In"),
                ("stock.shipment.out", "Out"),
                ("stock.shipment.internal", "Internal"),
            ],
            string="Shipment",
        ),
        "get_shipment_origin",
    )
    purchase = fields.Function(
        fields.Many2One('purchase.purchase', "Purchase"), 'get_purchase')
    sale = fields.Function(
        fields.Many2One('sale.sale', "Sale"), 'get_sale')
    purchase_reference = fields.Function(
        fields.Char("Purchase Ref."), 'get_purchase_reference')
    sale_reference = fields.Function(
        fields.Char("Sale Ref."), 'get_sale_reference')
    purchase_line_price = fields.Function(
        fields.Numeric("Purchase Price", digits=(16, 4)),
        'get_purchase_line_price')
    sale_line_price = fields.Function(
        fields.Numeric("Sale Price", digits=(16, 4)),
        'get_sale_line_price')

    def _purchase_line(self):
        return getattr(getattr(self, 'lot_p', None), 'line', None)

    def _sale_line(self):
        return getattr(getattr(self, 'lot_s', None), 'sale_line', None)

    @staticmethod
    def _contract_reference(contract):
        if not contract:
            return None
        return (
            getattr(contract, 'reference', None)
            or getattr(contract, 'our_reference', None))

    def get_purchase(self, name):
        line = self._purchase_line()
        purchase = getattr(line, 'purchase', None)
        return purchase.id if purchase else None

    def get_sale(self, name):
        line = self._sale_line()
        sale = getattr(line, 'sale', None)
        return sale.id if sale else None

    def get_purchase_reference(self, name):
        line = self._purchase_line()
        return self._contract_reference(getattr(line, 'purchase', None))

    def get_sale_reference(self, name):
        line = self._sale_line()
        return self._contract_reference(getattr(line, 'sale', None))

    def get_purchase_line_price(self, name):
        line = self._purchase_line()
        return getattr(line, 'unit_price', None)

    def get_sale_line_price(self, name):
        line = self._sale_line()
        return getattr(line, 'unit_price', None)

    def get_shipment_origin(self, name):
        if self.lot_shipment_in:
            return 'stock.shipment.in,' + str(self.lot_shipment_in.id)
        elif self.lot_shipment_out:
            return 'stock.shipment.out,' + str(self.lot_shipment_out.id)
        elif self.lot_shipment_internal:
            return 'stock.shipment.internal,' + str(self.lot_shipment_internal.id)
        return None

    @classmethod
    def default_lot_av(cls):
        return 'available'

    @classmethod
    def default_lot_visible(cls):
        return True

    @classmethod
    def default_lot_status(cls):
        return 'forecast'

    def get_rec_name(self, name):
        return self.id

    def is_planned_transport(self):
        return bool(
            self.planned_from_location
            or self.planned_to_location
            or self.planned_transport_type
            or self.planned_from_date
            or self.planned_to_date
            or self.planned_note)

    def has_shipment(self):
        return bool(
            self.lot_shipment_in
            or self.lot_shipment_out
            or self.lot_shipment_internal)

    @classmethod
    def link_to_transport(cls, source, quantity, shipment_origin):
        quantity = abs(Decimal(str(quantity or 0))).quantize(
            Decimal("0.00001"))
        available = abs(Decimal(str(source.lot_quantity or 0))).quantize(
            Decimal("0.00001"))
        if not quantity or quantity > available:
            raise UserError("Quantity to link exceeds available quantity.")

        shipment_model, shipment_id = str(shipment_origin).split(',', 1)
        shipment_field = {
            'stock.shipment.in': 'lot_shipment_in',
            'stock.shipment.out': 'lot_shipment_out',
            'stock.shipment.internal': 'lot_shipment_internal',
        }.get(shipment_model)
        if not shipment_field:
            raise UserError("Shipment type is not supported.")
        shipment_id = int(shipment_id)
        signed_quantity = quantity
        if Decimal(str(source.lot_quantity or 0)) < 0:
            signed_quantity = -quantity

        if source.is_planned_transport():
            values = cls._split_plan_values(
                source, signed_quantity, {
                    'planned_from_location': cls._record_id(
                        source.planned_from_location),
                    'planned_to_location': cls._record_id(
                        source.planned_to_location),
                    'planned_transport_type': source.planned_transport_type,
                    'planned_from_date': source.planned_from_date,
                    'planned_to_date': source.planned_to_date,
                    'planned_note': source.planned_note,
                })
            values[shipment_field] = shipment_id
            linked, = cls.create([values])
            source.lot_quantity -= signed_quantity
            if abs(source.lot_quantity) < Decimal("0.00001"):
                source.lot_quantity = 0
            cls.save([source])
            return linked

        plan_lot = source.lot_p or source.lot_s
        plan_mode = 'both' if source.lot_p else 'only sale'
        if not plan_lot.updateVirtualPart(
                signed_quantity, shipment_origin,
                source.lot_s, plan_mode):
            plan_lot.createVirtualPart(
                signed_quantity, shipment_origin,
                source.lot_s, plan_mode)
        plan_lot.updateVirtualPart(
            -signed_quantity, None, source.lot_s, plan_mode)
        domain = [
            ('lot_p', '=', source.lot_p),
            ('lot_s', '=', source.lot_s),
            (shipment_field, '=', shipment_id),
        ]
        linked = cls.search(domain)
        if not linked:
            raise UserError("The linked transport quantity was not created.")
        return linked[0]

    @staticmethod
    def _record_id(record):
        return record.id if record else None

    @classmethod
    def _split_plan_values(cls, source, quantity, values):
        return {
            'lot_p': source.lot_p.id if source.lot_p else None,
            'lot_s': source.lot_s.id if source.lot_s else None,
            'lot_quantity': quantity,
            'lot_unit': source.lot_unit.id if source.lot_unit else None,
            'lot_av': source.lot_av,
            'lot_status': source.lot_status,
            'lot_visible': source.lot_visible,
            'planned_from_location': values.get('planned_from_location'),
            'planned_to_location': values.get('planned_to_location'),
            'planned_transport_type': values.get('planned_transport_type'),
            'planned_from_date': values.get('planned_from_date'),
            'planned_to_date': values.get('planned_to_date'),
            'planned_note': values.get('planned_note'),
        }

    @classmethod
    def plan_to_transport(cls, source, quantity, values):
        quantity = Decimal(str(quantity or 0)).quantize(Decimal("0.00001"))
        if quantity <= 0:
            raise UserError("Quantity to plan must be positive.")
        if not any(values.values()):
            raise UserError("Please enter at least one planning value.")
        if source.has_shipment():
            raise UserError("Already linked quantities cannot be planned.")
        if source.is_planned_transport():
            raise UserError("This quantity is already planned.")

        available = Decimal(str(source.lot_quantity or 0)).quantize(
            Decimal("0.00001"))
        if quantity > abs(available):
            raise UserError("Quantity to plan exceeds available quantity.")

        signed_quantity = -quantity if available < 0 else quantity
        source.lot_quantity = available - signed_quantity
        planned_values = cls._split_plan_values(
            source, signed_quantity, values)
        cls.save([source])
        cls.create([planned_values])

    @classmethod
    def _cancel_plan_domain(cls, planned):
        return [
            ('lot_p', '=', planned.lot_p.id if planned.lot_p else None),
            ('lot_s', '=', planned.lot_s.id if planned.lot_s else None),
            ('lot_shipment_in', '=', None),
            ('lot_shipment_out', '=', None),
            ('lot_shipment_internal', '=', None),
            ('planned_from_location', '=', None),
            ('planned_to_location', '=', None),
            ('planned_transport_type', '=', None),
            ('planned_from_date', '=', None),
            ('planned_to_date', '=', None),
            ('planned_note', '=', None),
        ]

    @classmethod
    def _cancel_plan_values(cls, planned):
        return {
            'lot_p': planned.lot_p.id if planned.lot_p else None,
            'lot_s': planned.lot_s.id if planned.lot_s else None,
            'lot_quantity': planned.lot_quantity,
            'lot_unit': planned.lot_unit.id if planned.lot_unit else None,
            'lot_av': planned.lot_av,
            'lot_status': planned.lot_status,
            'lot_visible': planned.lot_visible,
        }

    @classmethod
    def cancel_transport_plan(cls, planned):
        if planned.has_shipment():
            raise UserError("Linked quantities cannot have their plan removed.")
        if not planned.is_planned_transport():
            raise UserError("This quantity is not planned.")

        targets = cls.search(cls._cancel_plan_domain(planned), limit=1)
        if targets:
            target = targets[0]
            target.lot_quantity = (
                Decimal(str(target.lot_quantity or 0))
                + Decimal(str(planned.lot_quantity or 0)))
            cls.save([target])
        else:
            cls.create([cls._cancel_plan_values(planned)])
        cls.delete([planned])

    @classmethod
    def _planned_open_domain(cls, planned):
        return [
            ('lot_p', '=', planned.lot_p.id if planned.lot_p else None),
            ('lot_s', '=', planned.lot_s.id if planned.lot_s else None),
            ('lot_shipment_in', '=', None),
            ('lot_shipment_out', '=', None),
            ('lot_shipment_internal', '=', None),
            ('planned_from_location', '=',
                planned.planned_from_location.id
                if planned.planned_from_location else None),
            ('planned_to_location', '=',
                planned.planned_to_location.id
                if planned.planned_to_location else None),
            ('planned_transport_type', '=',
                planned.planned_transport_type),
            ('planned_from_date', '=', planned.planned_from_date),
            ('planned_to_date', '=', planned.planned_to_date),
            ('planned_note', '=', planned.planned_note),
        ]

    @classmethod
    def _planned_open_values(cls, planned, quantity):
        return {
            'lot_p': planned.lot_p.id if planned.lot_p else None,
            'lot_s': planned.lot_s.id if planned.lot_s else None,
            'lot_quantity': quantity,
            'lot_unit': planned.lot_unit.id if planned.lot_unit else None,
            'lot_av': planned.lot_av,
            'lot_status': planned.lot_status,
            'lot_visible': planned.lot_visible,
            'planned_from_location': (
                planned.planned_from_location.id
                if planned.planned_from_location else None),
            'planned_to_location': (
                planned.planned_to_location.id
                if planned.planned_to_location else None),
            'planned_transport_type': planned.planned_transport_type,
            'planned_from_date': planned.planned_from_date,
            'planned_to_date': planned.planned_to_date,
            'planned_note': planned.planned_note,
        }

    @classmethod
    def restore_planned_open_quantity(cls, planned, quantity):
        targets = cls.search(cls._planned_open_domain(planned), limit=1)
        if targets:
            target = targets[0]
            target.lot_quantity = (
                Decimal(str(target.lot_quantity or 0))
                + Decimal(str(quantity or 0)))
            cls.save([target])
        else:
            cls.create([cls._planned_open_values(planned, quantity)])

    @classmethod
    def _shipment_domain_from_origin(cls, shipment_origin):
        values = {
            'lot_shipment_in': None,
            'lot_shipment_out': None,
            'lot_shipment_internal': None,
        }
        if not shipment_origin:
            return values
        str_shipment = str(shipment_origin)
        index = str_shipment.rfind(",")
        if index == -1:
            return values
        shipment_id = int(str_shipment[index + 1:].strip())
        if 'stock.shipment.in' in str_shipment:
            values['lot_shipment_in'] = shipment_id
        elif 'stock.shipment.out' in str_shipment:
            values['lot_shipment_out'] = shipment_id
        elif 'stock.shipment.internal' in str_shipment:
            values['lot_shipment_internal'] = shipment_id
        return values

    @classmethod
    def _planned_scheduled_domain(cls, physical_lot, quantity_lot_s,
            shipment_origin):
        shipment_values = cls._shipment_domain_from_origin(shipment_origin)
        return [
            ('lot_p', '=', physical_lot.getVlot_p().id),
            ('lot_s', '=',
                quantity_lot_s.id if quantity_lot_s else None),
            ('lot_shipment_in', '=',
                shipment_values['lot_shipment_in']),
            ('lot_shipment_out', '=',
                shipment_values['lot_shipment_out']),
            ('lot_shipment_internal', '=',
                shipment_values['lot_shipment_internal']),
            ('planned_from_location', '=',
                physical_lot.planned_from_location.id
                if physical_lot.planned_from_location else None),
            ('planned_to_location', '=',
                physical_lot.planned_to_location.id
                if physical_lot.planned_to_location else None),
            ('planned_transport_type', '=',
                physical_lot.planned_transport_type),
            ('planned_from_date', '=', physical_lot.planned_from_date),
            ('planned_to_date', '=', physical_lot.planned_to_date),
            ('planned_note', '=', physical_lot.planned_note),
        ]

    @classmethod
    def _planned_scheduled_values(cls, physical_lot, quantity, quantity_lot_s,
            shipment_origin):
        values = {
            'lot_p': physical_lot.getVlot_p().id,
            'lot_s': quantity_lot_s.id if quantity_lot_s else None,
            'lot_quantity': quantity,
            'lot_unit': (
                physical_lot.lot_unit_line.id
                if physical_lot.lot_unit_line else None),
            'lot_av': 'available',
            'lot_status': 'forecast',
            'planned_from_location': (
                physical_lot.planned_from_location.id
                if physical_lot.planned_from_location else None),
            'planned_to_location': (
                physical_lot.planned_to_location.id
                if physical_lot.planned_to_location else None),
            'planned_transport_type': physical_lot.planned_transport_type,
            'planned_from_date': physical_lot.planned_from_date,
            'planned_to_date': physical_lot.planned_to_date,
            'planned_note': physical_lot.planned_note,
        }
        values.update(cls._shipment_domain_from_origin(shipment_origin))
        return values

    @classmethod
    def restore_planned_scheduled_quantity(cls, physical_lot, quantity,
            quantity_lot_s, shipment_origin):
        targets = cls.search(cls._planned_scheduled_domain(
            physical_lot, quantity_lot_s, shipment_origin), limit=1)
        if targets:
            target = targets[0]
            target.lot_quantity = (
                Decimal(str(target.lot_quantity or 0))
                + Decimal(str(quantity or 0)))
            cls.save([target])
        else:
            cls.create([cls._planned_scheduled_values(
                physical_lot, quantity, quantity_lot_s, shipment_origin)])

    @classmethod
    def unlink_transport_remainder(cls, lqt):
        if not lqt or not lqt.has_shipment():
            return []
        quantity = Decimal(str(lqt.lot_quantity or 0))
        if quantity <= 0:
            return []
        mode = 'both'
        if not lqt.lot_p:
            lot = lqt.lot_s
            mode = 'only sale'
        else:
            lot = lqt.lot_p
        if not lot:
            return []

        lot.updateVirtualPart(
            -quantity, lqt.lot_shipment_origin, lqt.lot_s, mode)
        if lqt.is_planned_transport():
            cls.restore_planned_open_quantity(lqt, quantity)
        elif not lot.updateVirtualPart(quantity, None, lqt.lot_s, mode):
            lot.createVirtualPart(quantity, None, lqt.lot_s, mode)

        affected_lines = []
        if lqt.lot_p and lqt.lot_p.line:
            affected_lines.append(lqt.lot_p.line)
        if lqt.lot_s and lqt.lot_s.sale_line:
            affected_lines.append(lqt.lot_s.sale_line)
        return affected_lines

    @classmethod
    def unmatch_lotqt(cls, lqt):
        if not lqt or not lqt.lot_p or not lqt.lot_s:
            return []
        lot = lqt.lot_p
        qt = lqt.lot_quantity
        if not lot.updateVirtualPart(qt, lqt.lot_shipment_origin, None):
            lot.createVirtualPart(qt, lqt.lot_shipment_origin, None)
        if not lot.updateVirtualPart(
                qt, lqt.lot_shipment_origin, lqt.lot_s, 'only sale'):
            lot.createVirtualPart(
                qt, lqt.lot_shipment_origin, lqt.lot_s, 'only sale')
        lot.updateVirtualPart(-qt, lqt.lot_shipment_origin, lqt.lot_s)
        return cls._affected_lines_for_lqt(lqt)

    @classmethod
    def mark_lines_finished_from_lotqt(cls, lqt, mark_purchase=False,
            mark_sale=False, unmatch=False):
        if not mark_purchase and not mark_sale:
            return []

        affected_lines = []
        if mark_purchase and lqt.lot_p and lqt.lot_p.line:
            purchase_line = lqt.lot_p.line
            purchase_line.finished = True
            Pool().get('purchase.line').save([purchase_line])
            affected_lines.append(purchase_line)
        if mark_sale and lqt.lot_s and lqt.lot_s.sale_line:
            sale_line = lqt.lot_s.sale_line
            sale_line.finished = True
            Pool().get('sale.line').save([sale_line])
            affected_lines.append(sale_line)
        if unmatch:
            if not lqt.lot_p or not lqt.lot_s:
                raise UserError(
                    "Mark as finished is only available on matched virtual "
                    "lines.")
            affected_lines.extend(cls.unmatch_lotqt(lqt))
        return affected_lines

    def isVirtualP(self):
        if self.lot_p:
            Lot = Pool().get('lot.lot')
            l = Lot(self.lot_p)
            return  (l.lot_type == 'virtual')
        return False

    def isVirtualS(self):
        if self.lot_s:
            Lot = Pool().get('lot.lot')
            l = Lot(self.lot_s)
            return  (l.lot_type == 'virtual')
        return False

    @staticmethod
    def _matching_line(lqt, side):
        if side == 'purchase':
            lot = getattr(lqt, 'lot_p', None)
            return getattr(lot, 'line', None)
        lot = getattr(lqt, 'lot_s', None)
        return getattr(lot, 'sale_line', None)

    @classmethod
    def _matching_line_tolerance_percent(cls, line, field):
        contract = getattr(line, 'purchase', None) or getattr(line, 'sale', None)
        if getattr(line, 'inherit_tol', False) and contract:
            return Decimal(str(getattr(contract, field, None) or 0))
        return Decimal(str(getattr(line, field, None) or 0))

    @classmethod
    def _matching_line_tolerance_info(cls, line):
        if not line:
            return {}
        theoretical = Decimal(str(
            getattr(line, 'quantity_theorical', None)
            or getattr(line, 'quantity', None)
            or 0))
        targeted = getattr(line, 'targeted_qt', None)
        if targeted in (None, ''):
            targeted = theoretical
        tol_min = cls._matching_line_tolerance_percent(line, 'tol_min')
        tol_max = cls._matching_line_tolerance_percent(line, 'tol_max')
        return {
            'lot_theoretical_qt': theoretical,
            'lot_targeted_qt': Decimal(str(targeted or 0)),
            'lot_tol_min': tol_min,
            'lot_tol_max': tol_max,
            'lot_tolerance_min': -tol_min,
            'lot_tolerance_max': tol_max,
            'lot_qt_min': round(
                theoretical * (Decimal(1) - tol_min / Decimal(100)), 5),
            'lot_qt_max': round(
                theoretical * (Decimal(1) + tol_max / Decimal(100)), 5),
        }

    @classmethod
    def _matching_quantity_in_line_unit(cls, lqt, line):
        quantity = abs(Decimal(str(getattr(lqt, 'lot_quantity', 0) or 0)))
        lot_unit = getattr(lqt, 'lot_unit', None)
        line_unit = getattr(line, 'unit', None)
        if not lot_unit or not line_unit or lot_unit == line_unit:
            return quantity
        Uom = Pool().get('product.uom')
        factor = None
        rate = None
        if lot_unit.category.id != line_unit.category.id:
            factor = 1
            rate = 1
        return Decimal(str(Uom.compute_qty(
            lot_unit, float(quantity), line_unit, True, factor, rate)))

    @classmethod
    def _matching_tolerated_available(cls, lqt, side):
        available = abs(Decimal(str(getattr(lqt, 'lot_quantity', 0) or 0)))
        line = cls._matching_line(lqt, side)
        if not line:
            return available
        info = cls._matching_line_tolerance_info(line)
        qt_max = info.get('lot_qt_max')
        if qt_max is None:
            return available
        domain = [('lot_s', '>', 0)]
        if side == 'purchase':
            domain.append(('lot_p', '=', getattr(lqt.lot_p, 'id', lqt.lot_p)))
        else:
            domain = [('lot_p', '>', 0),
                ('lot_s', '=', getattr(lqt.lot_s, 'id', lqt.lot_s))]
        matched = sum(
            cls._matching_quantity_in_line_unit(matched_lqt, line)
            for matched_lqt in cls.search(domain))
        tolerated_available = Decimal(str(qt_max)) - matched
        return max(available, tolerated_available, Decimal(0))

    @classmethod
    def _matching_already_matched(cls, lqt, side):
        line = cls._matching_line(lqt, side)
        if not line:
            return Decimal(0)
        if side == 'purchase':
            domain = [('lot_p', '=', getattr(lqt.lot_p, 'id', lqt.lot_p)),
                ('lot_s', '>', 0)]
        else:
            domain = [('lot_p', '>', 0),
                ('lot_s', '=', getattr(lqt.lot_s, 'id', lqt.lot_s))]
        return sum(
            cls._matching_quantity_in_line_unit(matched_lqt, line)
            for matched_lqt in cls.search(domain))

    @classmethod
    def match_lots(cls,lot_p,lot_s):
        logger.info("MATCH_LOTS:%s",lot_p)
        logger.info("MATCH_LOTS:%s",lot_s)
        LotQt = Pool().get('lot.qt')
        Lot = Pool().get('lot.lot')
        for lp in lot_p:
            qt_p = Decimal(str(lp.lot_matched_qt or 0))
            if qt_p <= 0:
                continue
            lqt = LotQt(lp.lot_r_id) if lp.lot_r_id else None
            if lqt:
                available = cls._matching_tolerated_available(lqt, 'purchase')
            else:
                lot = Lot(lp.lot_id)
                available = Decimal(str(
                    lot.get_current_quantity_converted() or 0))
            if qt_p > available:
                raise UserError(
                    "Quantity to match exceeds the available purchase "
                    "quantity with tolerance.")
        for ls in lot_s:
            qt_s = Decimal(str(ls.lot_matched_qt or 0))
            if qt_s <= 0:
                continue
            lqt = LotQt(ls.lot_r_id)
            available = cls._matching_tolerated_available(lqt, 'sale')
            if qt_s > available:
                raise UserError(
                    "Quantity to match exceeds the available sale quantity "
                    "with tolerance.")
        for lp in lot_p:
            qt_p = lp.lot_matched_qt
            if qt_p == 0:
                continue
            lqt = LotQt(lp.lot_r_id) if lp.lot_r_id else None
            if lqt and (not lqt.lot_p or lqt.lot_s):
                raise UserError(
                    "Please select only unmatched purchase quantities.")
            for ls in lot_s:
                if not ls.lot_matched_qt:
                    continue
                l = Lot(lp.lot_id)
                qt = Decimal(0)
                if qt_p >= ls.lot_matched_qt:
                    qt = ls.lot_matched_qt
                    left_qt = qt_p - qt
                else:
                    qt = qt_p
                    left_qt = 0
                lqt2 =LotQt(ls.lot_r_id)
                if lqt2.lot_p or not lqt2.lot_s:
                    raise UserError(
                        "Please select only unmatched sale quantities.")
                if l.lot_type == 'physic':
                    ll = lqt2.lot_s
                    #Decrease forecasted virtual parts matched
                    vlot_p = l.getVlot_p()
                    vlot_s = ll.getVlot_s()
                    logger.info("SALEVIRTUALLOT:%s",vlot_p)
                    logger.info("SALEVIRTUALLOT2:%s",vlot_s)
                    #l.updateVirtualPart(-qt,l.lot_shipment_origin,l.getVlot_s())
                    #Increase forecasted virtual part non matched
                    #if not l.updateVirtualPart(qt,l.lot_shipment_origin,None):
                    #    l.createVirtualPart(qt,l.lot_shipment_origin,None)
                    l.updateVirtualPart(-qt,None,ll,'only sale')
                    if lqt:
                        lqt.lot_s = ll
                        lqt.lot_av = 'reserved'
                        LotQt.save([lqt])
                    else:
                        l.createVirtualPart(qt, None, ll)
                    l.sale_line = ll.sale_line
                    Lot.save([l])
                    #ll.lot_gross_quantity -= qt
                    ll.lot_quantity = (
                        Decimal(str(ll.lot_quantity or 0)) - qt)
                    if ll.lot_qt is not None:
                        ll.lot_qt -= float(qt)
                    Lot.save([ll])
                else:
                    #Increase forecasted virtual part matched
                    if not l.updateVirtualPart(qt,lqt.lot_shipment_origin,lqt2.lot_s):
                        l.createVirtualPart(qt,lqt.lot_shipment_origin,lqt2.lot_s)
                    #Decrease forecasted virtual part non matched
                    l.updateVirtualPart(-qt,lqt.lot_shipment_origin,None)
                    l.updateVirtualPart(-qt,None,lqt2.lot_s,'only sale')
                if left_qt == 0:
                    break
                qt_p = left_qt

    @classmethod
    def add_physical_lots(cls, lqt, vlots, unlink_remainder=False):
        Purchase = Pool().get('purchase.purchase')
        LotQt = Pool().get('lot.qt')
        Lot = Pool().get('lot.lot')
        Uom = Pool().get('product.uom')
        if not lqt.has_shipment():
            raise UserError(
                "Please link the quantity to transport before adding "
                "physical lots.")
        purchase = lqt.lot_p.line.purchase
        lots = []
        tot_qt = 0
        for l in vlots:
            tot_qt += Decimal(str(Uom.compute_qty(
                l.lot_unit_line, float(l.lot_quantity or 0),
                lqt.lot_p.line.unit)))
        tot_qt = round(tot_qt, 5)
        cls._warn_physical_lot_tolerance(lqt, vlots)
        with Lot.skip_quantity_consistency():
            # Consume the forecast first. Physical lot saves recompute the
            # virtual lot, so the matching lot.qt must already carry the
            # target open quantity during the rest of the transaction.
            lqt.lot_quantity -= tot_qt
            if lqt.lot_quantity < 0:
                lqt.lot_quantity = 0
            LotQt.save([lqt])

            for l in vlots:
                lot,qt = lqt.add_physical_lot(l)
                lots.append(lot)

            #create move for lots
            Purchase._process_shipment([purchase],lots)
        affected_lines = []
        if unlink_remainder:
            affected_lines.extend(cls.unlink_transport_remainder(lqt))
        if lqt.lot_p and lqt.lot_p.line:
            affected_lines.append(lqt.lot_p.line)
        if lqt.lot_s and lqt.lot_s.sale_line:
            affected_lines.append(lqt.lot_s.sale_line)
        Lot.assert_lines_quantity_consistency(affected_lines)
        cls._sync_contract_tolerances_after_physical_lots(affected_lines)
        
        if lots:
            return lots[0].id

    @classmethod
    def _line_contract(cls, line):
        return getattr(line, 'purchase', None) or getattr(line, 'sale', None)

    @classmethod
    def _line_model_name(cls, line):
        if getattr(line, 'purchase', None):
            return 'purchase.line'
        if getattr(line, 'sale', None):
            return 'sale.line'

    @classmethod
    def _line_theoretical_quantity(cls, line):
        return Decimal(str(getattr(line, 'quantity_theorical', None) or 0))

    @classmethod
    def _line_tolerance_percent(cls, line, field):
        contract = cls._line_contract(line)
        if getattr(line, 'inherit_tol', False) and contract:
            value = getattr(contract, field, None)
        else:
            value = getattr(line, field, None)
            if value is None and contract:
                value = getattr(contract, field, None)
        return Decimal(str(value or 0))

    @classmethod
    def _line_physical_quantity(cls, line):
        quantity = Decimal(0)
        unit = getattr(line, 'unit', None)
        for lot in getattr(line, 'lots', None) or []:
            if getattr(lot, 'lot_type', None) == 'physic':
                quantity += Decimal(str(
                    lot.get_current_quantity_converted(unit=unit) or 0))
        return quantity

    @classmethod
    def _affected_lines_for_lqt(cls, lqt):
        lines = []
        if lqt.lot_p and lqt.lot_p.line:
            lines.append(lqt.lot_p.line)
        if lqt.lot_s and lqt.lot_s.sale_line:
            lines.append(lqt.lot_s.sale_line)
        return lines

    @classmethod
    def _added_quantity_for_line(cls, line, vlots):
        Uom = Pool().get('product.uom')
        quantity = Decimal(0)
        for lot in vlots:
            quantity += Decimal(str(Uom.compute_qty(
                lot.lot_unit_line, float(lot.lot_quantity or 0), line.unit)))
        return round(quantity, 5)

    @classmethod
    def _warn_physical_lot_tolerance(cls, lqt, vlots):
        if Transaction().context.get(
                '_purchase_trade_skip_physical_lot_tolerance_warning'):
            return
        Warning = Pool().get('res.user.warning')
        for line in cls._affected_lines_for_lqt(lqt):
            theoretical = cls._line_theoretical_quantity(line)
            if theoretical <= 0:
                continue
            tolerance = cls._line_tolerance_percent(line, 'tol_max')
            max_quantity = theoretical * (Decimal(1) + tolerance / Decimal(100))
            projected = (
                cls._line_physical_quantity(line)
                + cls._added_quantity_for_line(line, vlots))
            if projected <= max_quantity:
                continue
            warning_name = Warning.format(
                "Physical lot tolerance exceeded",
                [line, projected, max_quantity])
            if Warning.check(warning_name):
                over_percent = (
                    (projected - theoretical) / theoretical * Decimal(100))
                side = 'purchase'
                contract = getattr(line, 'purchase', None)
                if getattr(line, 'sale', None):
                    side = 'sale'
                    contract = line.sale
                contract_number = getattr(contract, 'number', None) or getattr(
                    contract, 'id', None)
                raise LotToleranceWarning(
                    warning_name,
                    "The physical lot quantity exceeds the current tolerance "
                    "on the %s side (contract: %s, line: %s, projected "
                    "quantity: %s, maximum with current tolerance: %s, "
                    "projected tolerance: %s%%). "
                    "Do you want to continue and consume the contract "
                    "tolerance envelope?"
                    % (
                        side,
                        contract_number,
                        getattr(line, 'id', None),
                        round(projected, 5),
                        round(max_quantity, 5),
                        round(over_percent, 2),
                    ))

    @classmethod
    def _contract_lines(cls, contract):
        return [
            line for line in getattr(contract, 'lines', None) or []
            if getattr(line, 'type', 'line') == 'line']

    @classmethod
    def _sync_contract_tolerances_after_physical_lots(cls, lines):
        contracts = {}
        for line in lines:
            contract = cls._line_contract(line)
            model_name = cls._line_model_name(line)
            if contract and model_name:
                contracts[(model_name, contract.id)] = (model_name, contract)

        for model_name, contract in contracts.values():
            if model_name == 'purchase.line':
                contract = Pool().get('purchase.purchase')(contract.id)
            elif model_name == 'sale.line':
                contract = Pool().get('sale.sale')(contract.id)
            contract_lines = cls._contract_lines(contract)
            cls._sync_contract_tolerance_lines(contract, contract_lines)

    @classmethod
    def _sync_contract_tolerance_lines(cls, contract, lines):
        if not lines:
            return

        header_tol_max = Decimal(str(getattr(contract, 'tol_max', None) or 0))
        header_tol_min = Decimal(str(getattr(contract, 'tol_min', None) or 0))
        theoretical_by_line = {
            line.id: cls._line_theoretical_quantity(line) for line in lines}
        total_theoretical = sum(theoretical_by_line.values(), Decimal(0))
        if total_theoretical <= 0:
            return

        total_allowed_over = (
            total_theoretical * header_tol_max / Decimal(100))
        over_by_line = {}
        any_over_header = False
        for line in lines:
            theoretical = theoretical_by_line[line.id]
            if theoretical <= 0:
                over_by_line[line.id] = Decimal(0)
                continue
            physical = cls._line_physical_quantity(line)
            over = max(physical - theoretical, Decimal(0))
            over_by_line[line.id] = over
            header_line_over = theoretical * header_tol_max / Decimal(100)
            if over > header_line_over:
                any_over_header = True

        if not any_over_header:
            return

        line_models = {}
        for line in lines:
            model_name = cls._line_model_name(line)
            if not model_name:
                continue
            theoretical = theoretical_by_line[line.id]
            if theoretical <= 0:
                continue
            consumed_by_others = sum(
                over for line_id, over in over_by_line.items()
                if line_id != line.id)
            remaining_for_line = max(
                total_allowed_over - consumed_by_others, Decimal(0))
            actual_over = over_by_line[line.id]
            header_line_over = theoretical * header_tol_max / Decimal(100)
            if actual_over > header_line_over:
                allowed_over = actual_over
            else:
                allowed_over = remaining_for_line
            allowed_percent = allowed_over / theoretical * Decimal(100)
            line.inherit_tol = False
            line.tol_min = header_tol_min
            line.tol_max = round(allowed_percent, 5)
            line.tol_min_qt = round(
                theoretical * header_tol_min / Decimal(100), 5)
            line.tol_max_qt = round(allowed_over, 5)
            line_models.setdefault(model_name, []).append(line)

        for model_name, model_lines in line_models.items():
            if model_lines:
                Pool().get(model_name).save(model_lines)

    def add_physical_lot(self,l):
        Lot = Pool().get('lot.lot')
        FeeLots = Pool().get('fee.lots')
        Purchase = Pool().get('purchase.purchase')
        newlot = Lot()
        newlot.line = self.lot_p.line
        newlot.lot_shipment_in = self.lot_shipment_in
        newlot.lot_shipment_internal = self.lot_shipment_internal
        newlot.lot_shipment_out = self.lot_shipment_out
        newlot.lot_product = self.lot_p.line.product
        newlot.lot_chunk_key = getattr(l, 'lot_chunk_key', None)
        if self.lot_s:
            newlot.sale_line = self.lot_s.sale_line if self.lot_s.sale_line else None
        newlot.lot_type = 'physic'
        newlot.lot_qt = l.lot_qt
        newlot.lot_unit = l.lot_unit
        newlot.lot_unit_line = l.lot_unit_line
        newlot.lot_premium = getattr(l, 'lot_premium', None) or Decimal(0)
        newlot.planned_from_location = self.planned_from_location
        newlot.planned_to_location = self.planned_to_location
        newlot.planned_transport_type = self.planned_transport_type
        newlot.planned_from_date = self.planned_from_date
        newlot.planned_to_date = self.planned_to_date
        newlot.planned_note = self.planned_note
        if newlot.lot_shipment_in or newlot.lot_shipment_internal or newlot.lot_shipment_out:
            newlot.lot_status = 'transit'
        newlot.set_current_quantity(l.lot_quantity,l.lot_gross_quantity,1)
        if (newlot.lot_shipment_in or newlot.lot_shipment_internal or newlot.lot_shipment_out):
            newlot.set_current_quantity(l.lot_quantity,l.lot_gross_quantity,2)
        Lot.save([newlot])
        #add lot to services
        if newlot.line.fees:
            for f in newlot.line.fees:
                exist = FeeLots.search([('fee','=',f.id),('lot','=',newlot.id)])
                if not exist:
                    fl =FeeLots()
                    fl.fee = f
                    fl.lot = newlot
                    fl.line = newlot.line
                    FeeLots.save([fl])
                #update ordered fee with physical lot quantity
                f.sync_quantity_from_lots()
                f.adjust_purchase_values()

        if newlot.sale_line and newlot.sale_line.fees:
            for f in newlot.sale_line.fees:
                exist = FeeLots.search([('fee','=',f.id),('lot','=',newlot.id)])
                if not exist:
                    fl =FeeLots()
                    fl.fee = f
                    fl.lot = newlot
                    fl.sale_line = newlot.sale_line
                    FeeLots.save([fl])
                #update ordered fee with physical lot quantity
                f.sync_quantity_from_lots()
                f.adjust_purchase_values()

        if newlot.lot_shipment_in and newlot.lot_shipment_in.fees:
            for f in newlot.lot_shipment_in.fees:
                exist = FeeLots.search([('fee','=',f.id),('lot','=',newlot.id)])
                if not exist:
                    fl =FeeLots()
                    fl.fee = f
                    fl.lot = newlot
                    FeeLots.save([fl])
                #update ordered fee with physical lot quantity
                f.sync_quantity_from_lots()
                f.adjust_purchase_values()

        return newlot, newlot.get_current_quantity_converted()

    def getMove(self,sh):
        if sh:
            if self.lot_p:
                Lot = Pool().get('lot.lot')
                LotQt = Pool().get('lot.qt')
                lp = Lot(self.lot_p)
                vlot_p = lp.getVlot_p()
                if 'stock.shipment.in' in sh:
                    lqts = LotQt.search([('lot_p','=',vlot_p),('lot_shipment_in','=',sh),('lot_move','!=',None)])
                elif 'stock.shipment.internal' in sh:
                    lqts = LotQt.search([('lot_p','=',vlot_p),('lot_shipment_internal','=',sh),('lot_move','!=',None)])
                elif 'stock.shipment.out' in sh:
                    lqts = LotQt.search([('lot_p','=',vlot_p),('lot_shipment_out','=',sh),('lot_move','!=',None)])
                if lqts:
                    return lqts[0].lot_move
                else:
                    return lp.createMove(self,self.lot_quantity,sh)
    
    @classmethod
    def updateMove(cls,move):
        if move:
            Move = Pool().get('stock.move')
            move = Move(move)
            if move:
                move.quantity = round(sum([(e.lot_quantity if e.lot_quantity else 0) for e in move.lotqt]),5)
                Move.save([move])

    @classmethod
    def validate(cls, lotqts):
        super(LotQt, cls).validate(lotqts)
        Date = Pool().get('ir.date')
        #Update Move
        for lqt in lotqts:
            if (not lqt.lot_p and not lqt.lot_s
                    and Decimal(str(lqt.lot_quantity or 0)) != 0):
                raise UserError(
                    "Lot quantity forecast must be linked to a purchase "
                    "virtual lot or a sale virtual lot.")
            cls.updateMove(lqt.lot_move)
            #generate valuation for purchase and sale
            logger.info("VALIDATE_LQT_LS:%s",lqt.lot_s)
            logger.info("VALIDATE_LQT_LP:%s",lqt.lot_p)
            if lqt.lot_p and lqt.lot_quantity > 0:
                pl = lqt.lot_p.line
                logger.info("VALIDATE_LQT_PL:%s",pl)
                # Pnl = Pool().get('valuation.valuation')
                # pnl = Pnl.search([('line','=',pl.id),('date','=',Date.today())])
                # if pnl:
                #     Pnl.delete(pnl)
                # pnl_lines = []
                # pnl_lines.extend(pl.get_pnl_fee_lines())
                # pnl_lines.extend(pl.get_pnl_price_lines())
                # pnl_lines.extend(pl.get_pnl_der_lines())  
                # Pnl.save(pnl_lines)

                #Open position update
                # if pl.quantity_theorical:
                #     OpenPosition = Pool().get('open.position')
                #     OpenPosition.create_from_purchase_line(pl)

    @classmethod
    def getQuery(cls, purchase=None, sale=None, shipment=None, type=None,
            state=None, qttype=None, supplier=None, client=None, ps=None,
            lot_status=None, group=None, product=None, location=None,
            origin=None, finished=False, shipping_status=None, asof=None,
            todate=None, dimension=None, strategy=None, vessel=None,
            reference=None):
        pool = Pool()
        LotQt = pool.get('lot.qt')
        lqt = LotQt.__table__()
        LotQtHis = pool.get('lot.qt.hist')
        lqh = LotQtHis.__table__()
        LotP = pool.get('lot.lot')
        lp = LotP.__table__()
        LotS = pool.get('lot.lot')
        ls = LotS.__table__()
        Purchase = pool.get('purchase.purchase')
        pu = Purchase.__table__()
        PurchaseLine = pool.get('purchase.line')
        pl = PurchaseLine.__table__()
        Sale = pool.get('sale.sale')
        sa = Sale.__table__()
        SaleLine = pool.get('sale.line')
        sl = SaleLine.__table__()
        ShipmentIn = pool.get('stock.shipment.in')
        si = ShipmentIn.__table__()
        ShipmentInternal = pool.get('stock.shipment.internal')
        sin = ShipmentInternal.__table__()
        Prov = pool.get('account.invoice')
        prov = Prov.__table__()
        ProvLine = pool.get('account.invoice.line')
        prov_line = ProvLine.__table__()
        Final = pool.get('account.invoice')
        final = Final.__table__()
        FinalLine = pool.get('account.invoice.line')
        final_line = FinalLine.__table__()
        Prov_s = pool.get('account.invoice')
        prov_s = Prov_s.__table__()
        ProvLine_s = pool.get('account.invoice.line')
        prov_line_s = ProvLine_s.__table__()
        Final_s = pool.get('account.invoice')
        final_s = Final_s.__table__()
        FinalLine_s = pool.get('account.invoice.line')
        final_line_s = FinalLine_s.__table__()
        ProductUom = pool.get('product.uom')
        puom = ProductUom.__table__()
        ProductUomLine = pool.get('product.uom')
        puoml = ProductUomLine.__table__()
        PurchaseStrategy = pool.get('purchase.strategy')
        pstr = PurchaseStrategy.__table__()
        SaleStrategy = pool.get('sale.strategy')
        sstr = SaleStrategy.__table__()
        AnalyticDimensionAssignment = pool.get('analytic.dimension.assignment')
        ada = AnalyticDimensionAssignment.__table__()
        type = type or 'all'
        state = state or 'all'
        lot_status = lot_status or 'all'
        ps = ps or 'all'
        shipping_status = shipping_status or 'all'

        def date_filter(purchase_date, sale_date):
            if not asof and not todate:
                return None
            purchase_clause = Literal(True)
            sale_clause = Literal(True)
            if asof:
                purchase_clause &= (purchase_date >= asof)
                sale_clause &= (sale_date >= asof)
            if todate:
                purchase_clause &= (purchase_date <= todate)
                sale_clause &= (sale_date <= todate)
            if ps == 'P':
                return purchase_clause
            elif ps == 'S':
                return sale_clause
            return purchase_clause | sale_clause

        def dimension_filter(purchase_id, sale_id):
            if not dimension:
                return None
            purchase_dimension = Exists(ada.select(
                ada.id,
                where=((ada.purchase == purchase_id)
                    & (ada.value == dimension))))
            sale_dimension = Exists(ada.select(
                ada.id,
                where=((ada.sale == sale_id) & (ada.value == dimension))))
            if ps == 'P':
                return purchase_dimension
            elif ps == 'S':
                return sale_dimension
            return purchase_dimension | sale_dimension

        def reference_filter():
            if not reference:
                return None
            pattern = '%%%s%%' % reference
            purchase_reference = (
                pu.reference.ilike(pattern)
                | pu.our_reference.ilike(pattern))
            sale_reference = (
                sa.reference.ilike(pattern)
                | sa.our_reference.ilike(pattern))
            if ps == 'P':
                return purchase_reference
            elif ps == 'S':
                return sale_reference
            return purchase_reference | sale_reference
        
        wh = ((lqt.lot_quantity > 0) & ((lp.lot_type == 'virtual') | (ls.lot_type == 'virtual')))

        #wh &= (((lqt.create_date >= asof) & ((lqt.create_date-datetime.timedelta(1)) <= todate)))
        if ps == 'P':
            wh &= (lqt.lot_p != None)
        elif ps == 'S':
            wh &= (lqt.lot_s != None)
        if not finished:
            wh &= (
                ((lqt.lot_p == None) | (pl.finished == False))
                & ((lqt.lot_s == None) | (sl.finished == False)))
        if purchase:
            wh &= (pu.id == purchase)
        if sale:
            wh &= (sa.id == sale)
        if state != 'all':
            wh &= (lqt.lot_av == state)
        if shipment:
            wh &= (lqt.lot_shipment_in == shipment)
        if vessel:
            wh &= (si.vessel == vessel)
        if product:
            wh &= (lqt.lot_product == product)
        if location:
            wh &= (Case((lqt.lot_shipment_in > 0, si.to_location),else_=Case((lqt.lot_shipment_internal > 0, sin.to_location),else_=pu.to_location)) == location)
        date_clause = date_filter(pu.purchase_date, sa.sale_date)
        if date_clause is not None:
            wh &= date_clause
        dimension_clause = dimension_filter(pu.id, sa.id)
        if dimension_clause is not None:
            wh &= dimension_clause
        reference_clause = reference_filter()
        if reference_clause is not None:
            wh &= reference_clause
        if strategy:
            purchase_strategy = Exists(pstr.select(
                pstr.id,
                where=((pstr.line == pl.id) & (pstr.strategy == strategy))))
            sale_strategy = Exists(sstr.select(
                sstr.id,
                where=((sstr.sale_line == sl.id)
                    & (sstr.strategy == strategy))))
            if ps == 'P':
                wh &= purchase_strategy
            elif ps == 'S':
                wh &= sale_strategy
            else:
                wh &= (purchase_strategy | sale_strategy)
        if type=='matched':
            wh &= ((ls.sale_line > 0) & (lp.line > 0))
        elif type=='not matched':
            wh &= (((ls.sale_line > 0) & (lp.line == None)) | ((ls.sale_line == None) & (lp.line > 0)))
        PlannedTransport = (
            (lqt.planned_from_location != None)
            | (lqt.planned_to_location != None)
            | (lqt.planned_transport_type != None)
            | (lqt.planned_from_date != None)
            | (lqt.planned_to_date != None)
            | (lqt.planned_note != None))
        NotPlannedTransport = (
            (lqt.planned_from_location == None)
            & (lqt.planned_to_location == None)
            & (lqt.planned_transport_type == None)
            & (lqt.planned_from_date == None)
            & (lqt.planned_to_date == None)
            & (lqt.planned_note == None))
        if shipping_status == 'unshipped':
            wh &= ((lqt.lot_shipment_in == None) & NotPlannedTransport)
        elif shipping_status == 'planned':
            wh &= ((lqt.lot_shipment_in == None) & PlannedTransport)
        elif shipping_status == 'scheduled':
            wh &= ((lqt.lot_shipment_in != None) & (si.state == 'draft'))
        elif shipping_status == 'shipped':
            wh &= ((lqt.lot_shipment_in != None) & (si.state == 'started'))
        elif shipping_status == 'received':
            wh &= ((lqt.lot_shipment_in != None)
                & (si.state.in_(['received', 'done'])))
        if qttype=='virtual':
            wh &= ((lp.lot_type == 'virtual') | (ls.lot_type == 'virtual'))
        elif qttype=='physic':
            wh &= ((lp.lot_type == 'physic') | (ls.lot_type == 'physic'))
        if lot_status:
            if lot_status == 'forecast':
                wh &= (lqt.lot_status == 'forecast')
            elif lot_status == 'loading':
                wh &= (lqt.lot_status == 'loading')
            elif lot_status == 'destination':
                wh &= (lqt.lot_status == 'destination')
            elif lot_status == 'stock':
                wh &= (lqt.lot_status == 'stock')
            elif lot_status == 'delivered':
                wh &= (lqt.lot_status == 'delivered')
        if supplier and pu:
            wh &= (pu.party == supplier)
        if client and sa:
            wh &= (sa.party == client)
        
        AvQt = Case(((lqt.lot_p>0) & (lqt.lot_s>0), 0),else_=Case((lqt.lot_p>0,lqt.lot_quantity),else_=-lqt.lot_quantity))
        MaQt = Case(((lqt.lot_p>0) & (lqt.lot_s>0), lqt.lot_quantity),else_=0)
        ShippingStatus = Case(
            ((lqt.lot_shipment_in == None) & PlannedTransport, 'planned'),
            else_=Case(
                (lqt.lot_shipment_in == None, 'unshipped'),
                else_=Case(
                    (si.state == 'draft', 'scheduled'),
                    else_=Case(
                        (si.state == 'started', 'shipped'),
                        else_=Case(
                            (si.state.in_(['received', 'done']), 'received'),
                            else_=None)))))
        query = (
            lqt.join(lp,'LEFT',condition=lqt.lot_p == lp.id)
            .join(pl,'LEFT',condition=lp.line == pl.id)
            .join(pu,'LEFT', condition=pl.purchase == pu.id)
            .join(ls,'LEFT',condition=lqt.lot_s == ls.id)
            .join(sl,'LEFT',condition=ls.sale_line == sl.id)
            .join(sa,'LEFT', condition=sl.sale == sa.id)
            .join(si,'LEFT',condition=lqt.lot_shipment_in == si.id)
            .join(sin,'LEFT',condition=lqt.lot_shipment_internal == sin.id)
            .select(
                Literal(0).as_('create_uid'),
                CurrentTimestamp().as_('create_date'),
                Literal(None).as_('write_uid'),
                Literal(None).as_('write_date'),
                (lqt.id + 10000000).as_('id'),
                lqt.lot_p.as_('r_lot_p'),
                lqt.lot_s.as_('r_lot_s'),
                Case((lp.id>0, lp.lot_name),else_=ls.lot_name).as_('r_lot_name'),
                Case((lp.id>0, lp.number),else_=ls.number).as_('r_number'),
                Case((lp.id>0, lp.lot_qt),else_=ls.lot_qt).as_('r_lot_qt'),
                Case((lp.id>0, lp.lot_unit),else_=ls.lot_unit).as_('r_lot_unit'),
                AvQt.as_('r_lot_quantity'),
                MaQt.as_('r_lot_matched'),
                Case((lp.id>0, lp.lot_product),else_=ls.lot_product).as_('r_lot_product'),
                Case((lp.id>0, lp.lot_type),else_=ls.lot_type).as_('r_lot_type'),
                lqt.lot_shipment_in.as_('r_lot_shipment_in'),
                lqt.lot_shipment_out.as_('r_lot_shipment_out'),
                lqt.lot_shipment_internal.as_('r_lot_shipment_internal'),
                si.vessel.as_('r_vessel'),
                si.operator.as_('r_operator'),
                si.bl_date.as_('r_bl_date'),
                si.bl_number.as_('r_bl_number'),
                lqt.lot_move.as_('r_lot_move'),
                lqt.lot_status.as_('r_lot_status'),
                Case((lp.lot_type=='physic',lp.lot_av),else_=lqt.lot_av).as_('r_lot_av'),
                Case((lp.id>0, lp.lot_premium),else_=ls.lot_premium).as_('r_lot_premium'),
                Case((lp.id>0, lp.lot_premium_sale),else_=ls.lot_premium_sale).as_('r_lot_premium_sale'),
                Case((lp.id>0, lp.lot_parent),else_=ls.lot_parent).as_('r_lot_parent'),
                Case((lp.id>0, lp.lot_himself),else_=ls.lot_himself).as_('r_lot_himself'),
                Case((lp.id>0, lp.lot_container),else_=ls.lot_container).as_('r_lot_container'),
                Case((lp.id>0, lp.line),else_=None).as_('r_line'),
                Case((pl.id>0, pl.del_period), else_=None).as_('r_del_period'),
                Case((sl.id>0, sl.del_period), else_=None).as_('r_sale_del_period'),
                Case((pu.id>0, pu.id),else_=None).as_('r_purchase'),
                Case((sa.id>0, sa.id),else_=None).as_('r_sale'),
                Case((ls.id>0, ls.sale_line),else_=None).as_('r_sale_line'),
                (MaQt + AvQt).as_('r_tot'),
                pu.party.as_('r_supplier'),
                sa.party.as_('r_client'),
                Case((lqt.lot_shipment_in > 0, si.from_location),else_=Case((lqt.lot_shipment_internal > 0, sin.from_location),else_=pu.from_location)).as_('r_from_l'),
                Case((lqt.lot_shipment_in > 0, si.to_location),else_=Case((lqt.lot_shipment_internal > 0, sin.to_location),else_=pu.to_location)).as_('r_from_t'),
                lqt.planned_from_location.as_('r_planned_from_location'),
                lqt.planned_to_location.as_('r_planned_to_location'),
                lqt.planned_transport_type.as_('r_planned_transport_type'),
                lqt.planned_from_date.as_('r_planned_from_date'),
                lqt.planned_to_date.as_('r_planned_to_date'),
                lqt.planned_note.as_('r_planned_note'),
                Literal(None).as_('r_lot_pur_inv_state'),
                Literal(None).as_('r_lot_sale_inv_state'),
                Literal(None).as_('r_lot_pur_inv'),
                Literal(None).as_('r_lot_sal_inv'),
                Literal(None).as_('r_lot_state'),
                ShippingStatus.as_('r_shipping_status'),
                where=wh
            )
        )

        wh2 = (lp.lot_type == 'physic')
        if ps == 'P':
            wh2 &= (lp.line != None)
        elif ps == 'S':
            wh2 &= (lp.sale_line != None)
        if purchase:
            wh2 &= (pu.id == purchase)
        if sale:
            wh2 &= (sa.id == sale)
        if state != 'all':
            wh2 &= (lp.lot_av == state)
        if shipment:
            wh2 &= (lp.lot_shipment_in == shipment)
        if vessel:
            wh2 &= (si.vessel == vessel)
        if product:
            wh2 &= (lp.lot_product == product)
        if location:
            wh2 &= (Case((lp.lot_shipment_in > 0, si.to_location), else_=Case((lp.lot_shipment_internal > 0, sin.to_location), else_=pu.to_location)) == location)
        date_clause = date_filter(pu.purchase_date, sa.sale_date)
        if date_clause is not None:
            wh2 &= date_clause
        dimension_clause = dimension_filter(pu.id, sa.id)
        if dimension_clause is not None:
            wh2 &= dimension_clause
        reference_clause = reference_filter()
        if reference_clause is not None:
            wh2 &= reference_clause
        if strategy:
            purchase_strategy = Exists(pstr.select(
                pstr.id,
                where=((pstr.line == pl.id) & (pstr.strategy == strategy))))
            sale_strategy = Exists(sstr.select(
                sstr.id,
                where=((sstr.sale_line == sl.id)
                    & (sstr.strategy == strategy))))
            if ps == 'P':
                wh2 &= purchase_strategy
            elif ps == 'S':
                wh2 &= sale_strategy
            else:
                wh2 &= (purchase_strategy | sale_strategy)
        if type=='matched':
            wh2 &= (lp.sale_line != None)
        elif type=='not matched':
            wh2 &= (lp.sale_line == None)
        if shipping_status in {'unshipped', 'planned'}:
            wh2 &= (lp.lot_shipment_in == None)
            if shipping_status == 'planned':
                wh2 &= False
        elif shipping_status == 'scheduled':
            wh2 &= ((lp.lot_shipment_in != None) & (si.state == 'draft'))
        elif shipping_status == 'shipped':
            wh2 &= ((lp.lot_shipment_in != None) & (si.state == 'started'))
        elif shipping_status == 'received':
            wh2 &= ((lp.lot_shipment_in != None)
                & (si.state.in_(['received', 'done'])))
        if qttype=='virtual':
            wh2 &= False
        if lot_status:
            if lot_status == 'forecast':
                wh2 &= (lp.lot_status == 'forecast')
            elif lot_status == 'loading':
                wh2 &= (lp.lot_status == 'loading')
            elif lot_status == 'destination':
                wh2 &= (lp.lot_status == 'destination')
            elif lot_status == 'stock':
                wh2 &= (lp.lot_status == 'stock')
            elif lot_status == 'delivered':
                wh2 &= (lp.lot_status == 'delivered')
        if supplier and pu:
            wh2 &= (pu.party == supplier)
        if client and sa:
            wh2 &= (sa.party == client)

        AvQt2 = Case((lp.sale_line>0, 0),else_=lqh.quantity * (puom.factor / puoml.factor))
        MaQt2 = Case((lp.sale_line>0, lqh.quantity * (puom.factor / puoml.factor)),else_=0)
        ShippingStatus2 = Case(
            (lp.lot_shipment_in == None, 'unshipped'),
            else_=Case(
                (si.state == 'draft', 'scheduled'),
                else_=Case(
                    (si.state == 'started', 'shipped'),
                    else_=Case(
                        (si.state.in_(['received', 'done']), 'received'),
                        else_=None))))
        
        query2 = (
            lp.join(lqh, "LEFT", condition=(lp.id == lqh.lot) & (lp.lot_state == lqh.quantity_type))
            .join(pl, "INNER", condition=lp.line == pl.id)
            .join(pu, "INNER", condition=pl.purchase == pu.id)
            .join(sl, "LEFT", condition=lp.sale_line == sl.id)
            .join(sa, "LEFT", condition=sl.sale == sa.id)
            .join(prov_line, "LEFT", condition=lp.invoice_line_prov == prov_line.id)
            .join(prov, "LEFT", condition=prov_line.invoice == prov.id)
            .join(prov_line_s, "LEFT", condition=lp.sale_invoice_line_prov == prov_line_s.id)
            .join(prov_s, "LEFT", condition=prov_line_s.invoice == prov_s.id)
            .join(final_line, "LEFT", condition=lp.invoice_line == final_line.id)
            .join(final, "LEFT", condition=final_line.invoice == final.id)
            .join(final_line_s, "LEFT", condition=lp.sale_invoice_line == final_line_s.id)
            .join(final_s, "LEFT", condition=final_line_s.invoice == final_s.id)
            .join(si,'LEFT',condition=lp.lot_shipment_in == si.id)
            .join(sin,'LEFT',condition=lp.lot_shipment_internal == sin.id)
            .join(puom,'LEFT',condition=lp.lot_unit_line == puom.id)
            .join(puoml,'LEFT',condition=pl.unit == puoml.id)
            .select(
                Literal(0).as_("create_uid"),
                CurrentTimestamp().as_("create_date"),
                Literal(None).as_("write_uid"),
                Literal(None).as_("write_date"),
                (lp.id).as_("id"),
                lp.id.as_("r_lot_p"),
                Literal(None).as_("r_lot_s"),
                lp.lot_name.as_("r_lot_name"),
                lp.number.as_("r_number"),
                lp.lot_qt.as_("r_lot_qt"),
                lp.lot_unit.as_("r_lot_unit"),
                AvQt2.as_("r_lot_quantity"),
                MaQt2.as_("r_lot_matched"),
                lp.lot_product.as_("r_lot_product"),
                lp.lot_type.as_("r_lot_type"),
                lp.lot_shipment_in.as_("r_lot_shipment_in"),
                lp.lot_shipment_out.as_('r_lot_shipment_out'),
                lp.lot_shipment_internal.as_('r_lot_shipment_internal'),
                si.vessel.as_("r_vessel"),
                si.operator.as_("r_operator"),
                si.bl_date.as_("r_bl_date"),
                si.bl_number.as_("r_bl_number"),
                lp.move.as_("r_lot_move"),
                lp.lot_status.as_("r_lot_status"),
                lp.lot_av.as_("r_lot_av"),
                lp.lot_premium.as_("r_lot_premium"),
                lp.lot_premium_sale.as_("r_lot_premium_sale"),
                lp.lot_parent.as_("r_lot_parent"),
                lp.lot_himself.as_("r_lot_himself"),
                lp.lot_container.as_("r_lot_container"),
                lp.line.as_("r_line"),
                pl.del_period.as_("r_del_period"),
                sl.del_period.as_("r_sale_del_period"),
                Case((pu.id > 0, pu.id), else_=None).as_("r_purchase"),
                Case((sa.id > 0, sa.id), else_=None).as_("r_sale"),
                lp.sale_line.as_("r_sale_line"),
                (MaQt2 + Abs(AvQt2)).as_("r_tot"),
                pu.party.as_("r_supplier"),
                sa.party.as_("r_client"),
                Case((lp.lot_shipment_in > 0, si.from_location), else_=Case((lp.lot_shipment_internal > 0, sin.from_location), else_=pu.from_location)).as_("r_from_l"),
                Case((lp.lot_shipment_in > 0, si.to_location), else_=Case((lp.lot_shipment_internal > 0, sin.to_location), else_=pu.to_location)).as_("r_from_t"),
                Literal(None).as_('r_planned_from_location'),
                Literal(None).as_('r_planned_to_location'),
                Literal(None).as_('r_planned_transport_type'),
                Literal(None).as_('r_planned_from_date'),
                Literal(None).as_('r_planned_to_date'),
                Literal(None).as_('r_planned_note'),
                lp.lot_pur_inv_state.as_('r_lot_pur_inv_state'),
                lp.lot_sale_inv_state.as_('r_lot_sale_inv_state'),
                Case((final.id > 0, final.id), else_=prov.id).as_('r_lot_pur_inv'),
                Case((final_s.id > 0, final_s.id), else_=prov_s.id).as_('r_lot_sal_inv'),
                lp.lot_state.as_('r_lot_state'),
                ShippingStatus2.as_('r_shipping_status'),
                where=wh2,
            )
        )

        if group=='by_physic':
            ship_group = Case((lp.lot_shipment_in>0, lp.lot_shipment_in),else_=Case((lp.lot_shipment_internal>0, lp.lot_shipment_internal),else_=Case((lp.lot_shipment_out>0, lp.lot_shipment_out),else_=None)))
            query2 = (
            lp.join(lqh, "LEFT", condition=(lp.id == lqh.lot) & (lp.lot_state == lqh.quantity_type))
            .join(pl, "INNER", condition=lp.line == pl.id)
            .join(pu, "INNER", condition=pl.purchase == pu.id)
            .join(sl, "LEFT", condition=lp.sale_line == sl.id)
            .join(sa, "LEFT", condition=sl.sale == sa.id)
            .join(prov_line, "LEFT", condition=lp.invoice_line_prov == prov_line.id)
            .join(prov, "LEFT", condition=prov_line.invoice == prov.id)
            .join(prov_line_s, "LEFT", condition=lp.sale_invoice_line_prov == prov_line_s.id)
            .join(prov_s, "LEFT", condition=prov_line_s.invoice == prov_s.id)
            .join(final_line, "LEFT", condition=lp.invoice_line == final_line.id)
            .join(final, "LEFT", condition=final_line.invoice == final.id)
            .join(final_line_s, "LEFT", condition=lp.sale_invoice_line == final_line_s.id)
            .join(final_s, "LEFT", condition=final_line_s.invoice == final_s.id)
            .join(si,'LEFT',condition=lp.lot_shipment_in == si.id)
            .join(sin,'LEFT',condition=lp.lot_shipment_internal == sin.id)
            .join(puom,'LEFT',condition=lp.lot_unit_line == puom.id)
            .join(puoml,'LEFT',condition=pl.unit == puoml.id)
            .select(
                Literal(0).as_("create_uid"),
                CurrentTimestamp().as_("create_date"),
                Literal(None).as_("write_uid"),
                Literal(None).as_("write_date"),
                Max(lp.id).as_("id"),
                Max(lp.id).as_("r_lot_p"),
                Literal(None).as_("r_lot_s"),
                Max(lp.lot_name).as_("r_lot_name"),
                Max(lp.number).as_("r_number"),
                Sum(lp.lot_qt).as_("r_lot_qt"),
                Max(lp.lot_unit).as_("r_lot_unit"),
                Sum(AvQt2).as_("r_lot_quantity"),
                Sum(MaQt2).as_("r_lot_matched"),
                Max(lp.lot_product).as_("r_lot_product"),
                Max(lp.lot_type).as_("r_lot_type"),
                lp.lot_shipment_in.as_("r_lot_shipment_in"),
                lp.lot_shipment_out.as_('r_lot_shipment_out'),
                lp.lot_shipment_internal.as_('r_lot_shipment_internal'),
                Max(si.vessel).as_("r_vessel"),
                Max(si.operator).as_("r_operator"),
                Max(si.bl_date).as_("r_bl_date"),
                Max(si.bl_number).as_("r_bl_number"),
                Max(lp.move).as_("r_lot_move"),
                Max(lp.lot_status).as_("r_lot_status"),
                Max(lp.lot_av).as_("r_lot_av"),
                Avg(lp.lot_premium).as_("r_lot_premium"),
                Literal(None).as_("r_lot_premium_sale"),
                Literal(None).as_("r_lot_parent"),
                Literal(None).as_("r_lot_himself"),
                Max(lp.lot_container).as_("r_lot_container"),
                lp.line.as_("r_line"),
                Max(pl.del_period).as_("r_del_period"),
                Max(sl.del_period).as_("r_sale_del_period"),
                Max(Case((pu.id > 0, pu.id), else_=None)).as_("r_purchase"),
                Max(Case((sa.id > 0, sa.id), else_=None)).as_("r_sale"),
                lp.sale_line.as_("r_sale_line"),
                Sum(MaQt2 + Abs(AvQt2)).as_("r_tot"),
                Max(pu.party).as_("r_supplier"),
                Max(sa.party).as_("r_client"),
                Max(Case((lp.lot_shipment_in > 0, si.from_location), else_=Case((lp.lot_shipment_internal > 0, si.from_location), else_=pu.from_location))).as_("r_from_l"),
                Max(Case((lp.lot_shipment_in > 0, si.to_location), else_=Case((lp.lot_shipment_internal > 0, si.to_location), else_=pu.to_location))).as_("r_from_t"),
                Literal(None).as_('r_planned_from_location'),
                Literal(None).as_('r_planned_to_location'),
                Literal(None).as_('r_planned_transport_type'),
                Literal(None).as_('r_planned_from_date'),
                Literal(None).as_('r_planned_to_date'),
                Literal(None).as_('r_planned_note'),
                Max(lp.lot_pur_inv_state).as_('r_lot_pur_inv_state'),
                Max(lp.lot_sale_inv_state).as_('r_lot_sale_inv_state'),
                Max(Case((final.id > 0, final.id), else_=prov.id)).as_('r_lot_pur_inv'),
                Max(Case((final_s.id > 0, final_s.id), else_=prov_s.id)).as_('r_lot_sal_inv'),
                Max(lp.lot_state).as_('r_lot_state'),
                Max(ShippingStatus2).as_('r_shipping_status'),
                where=wh2,
                group_by=[lp.line,lp.lot_shipment_in,lp.lot_shipment_internal,lp.lot_shipment_out,lp.sale_line]
                )
            )

        if origin == 'physic':
            union = Union(query2,query2,all_=False)
        elif origin == 'open':
            union = Union(query,query,all_=False)
        else:
            union = Union(query,query2,all_=True) 
        
        ordered_union = union.select(
            Literal(0).as_("create_uid"),
            CurrentTimestamp().as_("create_date"),
            Literal(None).as_("write_uid"),
            Literal(None).as_("write_date"),
            union.id.as_("id"),
            union.r_lot_p.as_("r_lot_p"),
            Literal(None).as_("r_lot_s"),
            union.r_lot_name.as_("r_lot_name"),
            union.r_number.as_("r_number"),
            union.r_lot_qt.as_("r_lot_qt"),
            union.r_lot_unit.as_("r_lot_unit"),
            union.r_lot_quantity.as_("r_lot_quantity"),
            union.r_lot_matched.as_("r_lot_matched"),
            union.r_lot_product.as_("r_lot_product"),
            union.r_lot_type.as_("r_lot_type"),
            union.r_lot_shipment_in.as_("r_lot_shipment_in"),
            union.r_lot_shipment_out.as_('r_lot_shipment_out'),
            union.r_lot_shipment_internal.as_('r_lot_shipment_internal'),
            union.r_vessel.as_("r_vessel"),
            union.r_operator.as_("r_operator"),
            union.r_bl_date.as_("r_bl_date"),
            union.r_bl_number.as_("r_bl_number"),
            union.r_lot_move.as_("r_lot_move"),
            union.r_lot_status.as_("r_lot_status"),
            union.r_lot_av.as_("r_lot_av"),
            union.r_lot_premium.as_("r_lot_premium"),
            union.r_lot_premium_sale.as_("r_lot_premium_sale"),
            union.r_lot_parent.as_("r_lot_parent"),
            union.r_lot_himself.as_("r_lot_himself"),
            union.r_lot_container.as_("r_lot_container"),
            union.r_line.as_("r_line"),
            union.r_del_period.as_("r_del_period"),
            union.r_sale_del_period.as_("r_sale_del_period"),
            union.r_purchase.as_("r_purchase"),
            union.r_sale.as_("r_sale"),
            union.r_sale_line.as_("r_sale_line"),
            union.r_tot.as_("r_tot"),
            union.r_supplier.as_("r_supplier"),
            union.r_client.as_("r_client"),
            union.r_from_l.as_("r_from_l"),
            union.r_from_t.as_("r_from_t"),
            union.r_planned_from_location.as_('r_planned_from_location'),
            union.r_planned_to_location.as_('r_planned_to_location'),
            union.r_planned_transport_type.as_('r_planned_transport_type'),
            union.r_planned_from_date.as_('r_planned_from_date'),
            union.r_planned_to_date.as_('r_planned_to_date'),
            union.r_planned_note.as_('r_planned_note'),
            union.r_lot_pur_inv_state.as_('r_lot_pur_inv_state'),
            union.r_lot_sale_inv_state.as_('r_lot_sale_inv_state'),
            union.r_lot_pur_inv.as_('r_lot_pur_inv'),
            union.r_lot_sal_inv.as_('r_lot_sal_inv'),
            union.r_lot_state.as_('r_lot_state'),
            union.r_shipping_status.as_('r_shipping_status'),
            order_by=[union.r_line.desc])
        return ordered_union

class LotReport(
        ModelSQL, ModelView):
    "Lots management"
    __name__ = 'lot.report'
    r_line = fields.Many2One('purchase.line',"P. line")
    r_purchase = fields.Many2One('purchase.purchase', "Purchase")
    r_lot_p =fields.Many2One('lot.lot',"lot P")
    r_lot_s =fields.Many2One('lot.lot',"lot S")
    r_lot_name = fields.Char("Lot")
    r_number = fields.Char("Number")
    r_lot_qt = fields.Float("Quantity")
    r_lot_unit = fields.Many2One('product.uom', "Unit")
    r_lot_product = fields.Many2One('product.product', "Product")
    r_lot_type = fields.Selection([
        ('virtual', 'Open'),
        ('physic', 'Physic'),
        ('loss', 'Loss'),
    ], 'Qt type')
    r_lot_status = fields.Selection([
        (None, ''),
        ('forecast', 'Forecast'),
        ('loading', 'Loading'),
        ('transit', 'Transit'),
        ('destination', 'Destination'),
        ('stock', 'Stock'),
        ('delivered', 'Delivered')
    ], 'Where')
    r_lot_av = fields.Selection([
        ('available', 'Available'),
        ('reserved', 'Reserved'),
        ('locked', 'Locked'),
    ], 'State')
    r_lot_pur_inv_state = fields.Selection([
        (None, ''),
        ('prov', 'Prov'),
        ('invoiced', 'Final')
    ], 'Pur. inv')
    r_lot_pur_inv = fields.Many2One('account.invoice',"Pur. inv")
    r_lot_sal_inv = fields.Many2One('account.invoice',"Sale inv")
    r_lot_sale_inv_state = fields.Selection([
        (None, ''),
        ('prov', 'Prov'),
        ('invoiced', 'Final')
    ], 'Sale inv')
    r_lot_state = fields.Many2One('lot.qt.type',"Qt state")
    r_lot_quantity = fields.Numeric("Avail. Qt",digits='r_lot_unit_line')
    r_lot_matched = fields.Numeric("Matched Qt",digits='r_lot_unit_line')
    r_lot_premium = fields.Numeric("Prem/Disc", digits='r_lot_unit_line')
    r_lot_premium_sale = fields.Numeric("Prem/Disc", digits='r_lot_unit_line')
    r_lot_shipment_in = fields.Many2One('stock.shipment.in', "Shipment In")
    r_lot_shipment_out = fields.Many2One('stock.shipment.out', "Shipment Out")
    r_lot_shipment_internal = fields.Many2One('stock.shipment.internal', "Shipment Internal")
    r_vessel = fields.Many2One('trade.vessel', "Vessel")
    r_operator = fields.Many2One('party.party', "Operator")
    r_bl_date = fields.Date("BL date")
    r_bl_number = fields.Char("BL number")
    r_lot_move = fields.Many2One('stock.move', "Move")
    r_lot_parent = fields.Many2One('lot.lot',"Parent")
    r_lot_himself = fields.Many2One('lot.lot',"Lot")
    r_lot_container = fields.Char("Container")
    r_lot_unit_line = fields.Function(fields.Many2One('product.uom', "Unit"),'get_unit')
    r_lot_price = fields.Function(fields.Numeric("Price", digits='r_lot_unit_line'),'get_lot_price')
    r_lot_price_sale = fields.Function(fields.Numeric("Price", digits='r_lot_unit_line'),'get_lot_sale_price')
    r_purchase_reference = fields.Function(
        fields.Char("Purchase Ref."), 'get_purchase_reference')
    r_purchase_creator = fields.Function(
        fields.Many2One('res.user', "Purchase Created By"),
        'get_purchase_creator')
    r_purchase_date = fields.Function(
        fields.Date("Purchase Date"), 'get_purchase_date')
    r_sale_reference = fields.Function(
        fields.Char("Sale Ref."), 'get_sale_reference')
    r_purchase_line_price = fields.Function(
        fields.Numeric("Purchase Price", digits=(16, 4)),
        'get_purchase_line_price')
    r_sale_line_price = fields.Function(
        fields.Numeric("Sale Price", digits=(16, 4)),
        'get_sale_line_price')
    r_purchase_currency_symbol = fields.Function(
        fields.Char("Purchase Currency"), 'get_purchase_currency_symbol')
    r_sale_currency_symbol = fields.Function(
        fields.Char("Sale Currency"), 'get_sale_currency_symbol')
    r_del_period = fields.Many2One(
        'product.month', "Purchase Shipment Period")
    r_sale_del_period = fields.Many2One(
        'product.month', "Sale Shipment Period")
    r_sale_line = fields.Many2One('sale.line',"S. line")
    r_sale = fields.Many2One('sale.sale',"Sale")
    r_tot = fields.Numeric("Qt tot", digits='r_lot_unit_line')
    r_supplier = fields.Many2One('party.party',"Supplier")
    r_client = fields.Many2One('party.party',"Client")
    r_from_l = fields.Many2One('stock.location', 'From')
    r_from_t = fields.Many2One('stock.location', 'To')
    r_planned_from_location = fields.Many2One(
        'stock.location', "Planned From")
    r_planned_to_location = fields.Many2One(
        'stock.location', "Planned To")
    r_planned_transport_type = fields.Selection([
        (None, ''),
        ('vessel', 'Vessel'),
        ('truck', 'Truck'),
    ], "Planned Transport")
    r_planned_from_date = fields.Date("Planned From Date")
    r_planned_to_date = fields.Date("Planned To Date")
    r_planned_note = fields.Char("Planning Note")
    r_shipping_status = fields.Selection([
        (None, ''),
        ('unshipped', 'Unshipped'),
        ('planned', 'Planned'),
        ('scheduled', 'Scheduled'),
        ('shipped', 'Shipped'),
        ('received', 'Received'),
    ], "Shipping status")
    r_shipment_type = fields.Function(fields.Selection([
        (None, ''),
        ('dropship', 'Dropship'),
        ('inbound', 'Inbound'),
    ], "Shipment Type"), 'get_shipment_type')
    sh_icon = fields.Function(fields.Char("Shipment Icon"), 'on_change_with_sh_icon')
    qt_icon = fields.Function(fields.Char("Qt Icon"), 'on_change_with_qt_icon')
    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",
    )
    vis_data = fields.Function(
        fields.Text('Graphe JSON'), 'get_vis_data'
    )

    def get_vis_data(self, name):
        AccountMoveLine = Pool().get('account.move.line')
        Account = Pool().get('account.account')
        Stock = Pool().get('stock.move')
        Lot = Pool().get('lot.lot')
        Invoice = Pool().get('account.invoice')
        InvoiceLine = Pool().get('account.invoice.line')
        aml = AccountMoveLine.search([('lot','>',0)])
        pivot_data = {
            "data": [],
            "options": None
        }
        if aml:
            for l in aml:
                lot = Lot(l.lot)
                acc= Account(l.account)
                ct_type = 'Purchase'
                pr_type = 'Goods'
                acc_label = 'Account event'
                move_line = str(l.id)
                journal = l.move.journal.id
                product = lot.lot_product.name
                ct_number = ''
                amount = l.debit if l.debit != 0 else -l.credit
                # if l.account.type.payable and l.account.type.statement == 'income':
                #     amount = -amount
                ori = None #l.origin
                if not ori:
                    ori = l.move.origin
                if ori:
                    origin, id = str(ori).split(',')
                    if origin == 'stock.move':
                        acc_label = 'Reception'
                        sm = Stock(int(id))
                        if sm.to_location.type == 'customer':
                            ct_type = 'Sale'
                            if lot.sale_line:
                                ct_number = lot.sale_line.sale.number
                        else:
                            ct_type = 'Purchase'
                            if lot.line:
                                ct_number = lot.line.purchase.number
                    elif origin == 'account.invoice':
                        inv = Invoice(int(id))
                        product = None
                        if inv.lines[0].product:
                            product = inv.lines[0].product.name
                        if inv.type == 'out':
                            ct_type = 'Sale'
                            if lot.sale_line:
                                ct_number = lot.sale_line.sale.number
                        else:
                            ct_type = 'Purchase'
                            if lot.line:
                                ct_number = lot.line.purchase.number
                        if (journal == 1 and (l.account.id == 824 or l.account.id == 766)):
                            acc_label = 'COG Goods'
                        else:
                            if "Payment of invoice" in (l.move.description if l.move.description else ""):
                                acc_label = 'Payment'
                            else:
                                if 'Prepayment' in (inv.description if inv.description else ""):
                                    acc_label = 'Prepayment'
                                else:
                                    if inv.reference == 'Provisional':
                                        acc_label = 'Prov'
                                    else:
                                        acc_label = 'Final'

                pivot_data["data"].append(
                    {
                        "lot": lot.lot_name,
                        "ct type": ct_type,
                        "pr type": pr_type,
                        "ct number": ct_number, 
                        "event": acc_label,
                        "product": product,
                        "amount": str(amount),
                        "account": str(acc.code),
                        "move": str(l.move),
                        "move line": move_line,
                        "journal": journal
                    }
                )
        
            pivot_data['options'] = {
                "rows": ["lot","ct type","event"],
                "cols": ["account"],                
                "aggregatorName": "Sum",
                "vals": ["amount"]
            }
            return "pivotjson:" + json.dumps(pivot_data)

    def get_shipment_origin(self, name):
        if self.r_lot_shipment_in:
            return 'stock.shipment.in,' + str(self.r_lot_shipment_in.id)
        elif self.r_lot_shipment_out:
            return 'stock.shipment.out,' + str(self.r_lot_shipment_out.id)
        elif self.r_lot_shipment_internal:
            return 'stock.shipment.internal,' + str(self.r_lot_shipment_internal.id)
        return None

    @fields.depends('r_lot_shipment_in', 'r_from_l', 'r_from_t')
    def get_shipment_type(self, name):
        if self.r_lot_shipment_in:
            return self.r_lot_shipment_in.shipment_type
        if (self.r_from_l and self.r_from_t
                and self.r_from_l.type == 'supplier'
                and self.r_from_t.type == 'customer'):
            return 'dropship'
        return 'inbound'

    @classmethod
    def __setup__(cls):
        cls.r_line.search_unaccented = False
        super(LotReport, cls).__setup__()
        cls._order.insert(0, ('r_line', 'DESC'))
    
    @fields.depends('r_lot_type')
    def on_change_with_qt_icon(self, name=None):
        mapping = {
            'virtual': '\U0001F4D1',   # 📑
            'physic':   '\U0001F4E6',   # 📦 
        }
        return mapping.get(self.r_lot_type if self.r_lot_type else None, '')


    @fields.depends('r_lot_shipment_in')
    def on_change_with_sh_icon(self, name=None):
        mapping = {
            '': '\U0001F69A', #truck
            'vessel': '\U0001F6A2', 
        }
        return mapping.get(self.r_lot_shipment_in.transport_type if self.r_lot_shipment_in else None, '')

    @classmethod
    def _get_origin(cls):
        'Return list of Model names for origin Reference'
        return [cls.__name__]

    @classmethod
    def get_origin(cls):
        Model = Pool().get('ir.model')
        get_name = Model.get_name
        models = cls._get_origin()
        return [(None, '')] + [(m, get_name(m)) for m in models]

    def get_unit(self,name):
        if self.r_line:
            return self.r_line.unit
        if self.r_sale_line:
            return self.r_sale_line.unit

    def get_lot_price(self,name=None):
        price = Decimal(0)
        if self.r_line:
            l = Pool().get('purchase.line')(self.r_line)
            premium = Decimal(0)
            if self.r_lot_premium:
                premium = self.r_lot_premium
            price = premium + l.unit_price  
        return round(price,2)

    def get_lot_sale_price(self,name=None):
        price = Decimal(0)
        # if self.sale_line:
        #     l = Pool().get('sale.line')(self.sale_line)
        #     premium = Decimal(0)
        #     if self.lot_premium_sale:
        #         premium = self.lot_premium_sale
        #     price = premium + l.unit_price
        return round(price,2)

    @staticmethod
    def _contract_reference(contract):
        if not contract:
            return None
        return (
            getattr(contract, 'reference', None)
            or getattr(contract, 'our_reference', None))

    def get_purchase_reference(self, name=None):
        return self._contract_reference(self.r_purchase)

    def get_purchase_creator(self, name=None):
        creator = getattr(self.r_purchase, 'create_uid', None)
        return getattr(creator, 'id', creator)

    def get_purchase_date(self, name=None):
        return getattr(self.r_purchase, 'purchase_date', None)

    def get_sale_reference(self, name=None):
        return self._contract_reference(self.r_sale)

    def get_purchase_line_price(self, name=None):
        return getattr(self.r_line, 'unit_price', None)

    def get_sale_line_price(self, name=None):
        return getattr(self.r_sale_line, 'unit_price', None)

    def get_purchase_currency_symbol(self, name=None):
        currency = getattr(getattr(self.r_line, 'purchase', None), 'currency', None)
        return getattr(currency, 'symbol', None) or getattr(currency, 'code', None)

    def get_sale_currency_symbol(self, name=None):
        currency = getattr(getattr(self.r_sale_line, 'sale', None), 'currency', None)
        return getattr(currency, 'symbol', None) or getattr(currency, 'code', None)

    #@classmethod
    #def delete(cls, records):
        # Unmatch = Pool().get('lot.unmatch', type='wizard')
        # session_id, _, _ = Unmatch.create()
        # Unmatch.execute(session_id, {}, 'start')

    @classmethod
    def table_query(cls, context=None):
        if context is None:
            context = Transaction().context or {}
        LotQT = Pool().get('lot.qt')
        purchase = context.get('purchase')
        sale = context.get('sale')
        shipment = context.get('shipment')
        vessel = context.get('vessel')
        reference = context.get('reference')
        product = context.get('product')
        location = context.get('location')
        type = context.get('type')
        state = context.get('state')
        wh = context.get('wh')
        mode = context.get('mode')
        group = context.get('group')
        origin = context.get('origin')
        supplier = context.get('supplier')
        client = context.get('client')
        ps = context.get('ps')
        shipping_status = context.get('shipping_status')
        asof = context.get('asof')
        todate = context.get('todate')
        dimension = context.get('dimension')
        strategy = context.get('strategy')
        finished = context.get('finished')
        type = type or 'all'
        state = state or 'all'
        wh = wh or 'all'
        group = group or 'by_physic'
        origin = origin or 'all'
        ps = ps or 'all'
        shipping_status = shipping_status or 'all'
        query = LotQT.getQuery(
            purchase=purchase,
            sale=sale,
            shipment=shipment,
            type=type,
            state=state,
            supplier=supplier,
            client=client,
            ps=ps,
            lot_status=wh,
            group=group,
            product=product,
            location=location,
            origin=origin,
            finished=finished,
            shipping_status=shipping_status,
            asof=asof,
            todate=todate,
            dimension=dimension,
            strategy=strategy,
            vessel=vessel,
            reference=reference)
        return query

    @classmethod
    def search_count(cls, domain, active_test=True, context=None):
        type = 'all'
        if 'r_lot_matched' in str(domain):
            type = 'matched'
        context = Transaction().context
        context = {'purchase':None,'sale':None,'shipment':None,'type':type,'state':'all','wh':'all','group':'by_physic'}
        query = cls.table_query(context)
        cursor = Transaction().connection.cursor()
        cursor.execute(*query)
        return cursor.rowcount

    @classmethod
    def view_attributes(cls):
        return super().view_attributes() + [
            ('/tree/field[@name="r_lot_shipment_in"]', 'tree_invisible',
                (Eval('mode')=='pnl')),
            ('/tree/field[@name="r_lot_state"]', 'tree_invisible',
                (Eval('mode')=='pnl')),
            ]

    # @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_line', operator, operand, *extra),
    #         ('r_lot_name', operator, operand, *extra),
    #         ('r_lot_product', operator, operand, *extra),
    #         ]

class LotContext(ModelView):
    "Lot Context"
    __name__ = 'lot.context'

    asof = fields.Date("As of")
    todate = fields.Date("To")
    purchase =  fields.Many2One('purchase.purchase', "Purchase")
    supplier = fields.Many2One('party.party', "Supplier")
    client = fields.Many2One('party.party', "Client")
    reference = fields.Char("Reference")
    sale =  fields.Many2One('sale.sale', "Sale")
    shipment =  fields.Many2One('stock.shipment.in', "Shipment")
    vessel = fields.Many2One('trade.vessel', "Vessel")
    dimension = fields.Many2One('analytic.dimension.value', "Dimension")
    strategy = fields.Many2One('mtm.strategy', "Strategy")
    product = fields.Many2One('product.product',"Product")
    location = fields.Many2One('stock.location',"Location")
    type = fields.Selection([
        ('all', 'All'),
        ('matched', 'Matched'),
        ('not matched', 'Not Matched'),
    ], 'Matching status')
    shipping_status = fields.Selection([
        ('all', 'All'),
        ('unshipped', 'Unshipped'),
        ('planned', 'Planned'),
        ('scheduled', 'Scheduled'),
        ('shipped', 'Shipped'),
        ('received', 'Received'),
    ], "Shipping status")
    ps = fields.Selection([
        ('all', 'All'),
        ('P', 'Purchase'),
        ('S', 'Sale'),
    ], "Side")
    state = fields.Selection([
        ('all', 'All'),
        ('available', 'Available'),
        ('reserved', 'Reserved'),
        ('locked', 'Locked')
    ], 'State')

    wh = fields.Selection([
        ('all', 'All'),
        ('forecast', 'Forecast'),
        ('loading', 'Loading'),
        ('destination', 'Destination'),
        ('stock', 'Stock'),
        ('delivered', 'Delivered')
    ], 'Where')

    group = fields.Selection([
        ('all', 'All'),
        ('by_physic', 'By physic'),
    ], 'Group')

    origin = fields.Selection([
        ('all', 'All'),
        ('open', 'Open'),
        ('physic', 'Physic'),
    ], 'Open/Physic')

    mode = fields.Selection([
        ('normal', 'Normal'),
        ('pnl', 'Pnl'),
    ],'Mode')

    finished = fields.Boolean("Display finished")

    @classmethod
    def default_finished(cls):
        return False
    
    @classmethod
    def default_type(cls):
        return 'all'

    @classmethod
    def default_shipping_status(cls):
        return 'all'

    @classmethod
    def default_ps(cls):
        return 'all'

    @classmethod
    def default_group(cls):
        return 'all'

    @classmethod
    def default_wh(cls):
        return 'all'

    @classmethod
    def default_state(cls):
        return 'all'

    @classmethod
    def default_purchase(cls):
        return Transaction().context.get('purchase', None)
    
    @classmethod
    def default_sale(cls):
        return Transaction().context.get('sale', None)

    @classmethod
    def get_context(cls, fields_names=None):
        fields_names = fields_names.copy() if fields_names is not None else []
        for field_name in [
                'purchase', 'sale', 'shipment', 'supplier', 'client',
                'reference', 'vessel', 'dimension', 'strategy', 'product',
                'location', 'type',
                'shipping_status', 'ps', 'state', 'wh', 'mode', 'group',
                'origin', 'finished', 'asof', 'todate']:
            if field_name not in fields_names:
                fields_names.append(field_name)
        return super().get_context(fields_names=fields_names)

    
class LotShipping(Wizard):
    "Link lot to transport"
    __name__ = "lot.shipping"

    start = StateTransition()

    ship = StateView(
        'lot.shipping.start',
        'purchase_trade.shipping_view_form', [
        Button("Cancel", 'end', 'tryton-cancel'),
        Button("Link", 'shipping', 'tryton-ok', default=True),
        ])
    
    shipping = StateTransition()

    def transition_start(self):
        if self.model.__name__ == 'lot.report':
            return 'ship'
        return 'end'
    
    def default_ship(self, fields):
        values = {}
        planned_from_locations = {
            location.id
            for record in self.records
            for location in [
                record.r_planned_from_location
                or getattr(record.r_purchase, 'from_location', None)]
            if location
            }
        planned_to_locations = {
            location.id
            for record in self.records
            for location in [
                record.r_planned_to_location
                or getattr(record.r_purchase, 'to_location', None)]
            if location
            }
        if len(planned_from_locations) == 1:
            values['planned_from_location'] = next(iter(
                planned_from_locations))
        if len(planned_to_locations) == 1:
            values['planned_to_location'] = next(iter(planned_to_locations))
        if len(self.records) != 1:
            return values
        record = self.records[0]
        return values

    @staticmethod
    def _record_id(record):
        return record.id if record else None

    def _selected_shipment(self):
        if self.ship.shipment == 'in':
            return self.ship.shipment_in
        if self.ship.shipment == 'out':
            return self.ship.shipment_out
        if self.ship.shipment == 'int':
            return self.ship.shipment_internal

    def _shipment_in_create_values(self):
        if self.ship.shipment != 'in':
            raise UserError("Shipment creation is only available for Shipment In.")
        if not self.ship.shipment_supplier:
            raise UserError("Broker is required.")
        values = {
            'supplier': self._record_id(self.ship.shipment_supplier),
            }
        if self.ship.planned_from_location:
            values['from_location'] = self.ship.planned_from_location.id
        if self.ship.planned_to_location:
            values['to_location'] = self.ship.planned_to_location.id
        vessel = getattr(self.ship, 'shipment_vessel', None)
        if vessel:
            values['vessel'] = vessel.id
        bl_number = getattr(self.ship, 'shipment_bl_number', None)
        bl_date = getattr(self.ship, 'shipment_bl_date', None)
        if bl_number:
            values['bl_number'] = bl_number
        if bl_date:
            values['bl_date'] = bl_date
        return values

    def _create_shipment_in(self):
        ShipmentIn = Pool().get('stock.shipment.in')
        shipment, = ShipmentIn.create([self._shipment_in_create_values()])
        self.ship.shipment_in = shipment

    def transition_shipping(self):
        Lot = Pool().get('lot.lot')
        LotQt = Pool().get('lot.qt')
        Move = Pool().get('stock.move')
        affected_lines = []
        if not self.ship.shipment:
            pass
        if self.ship.create_new_shipment and self._selected_shipment():
            raise UserError(
                "Choose either an existing shipment or create a new shipment.")
        if self.ship.create_new_shipment:
            self._create_shipment_in()
        with Lot.skip_quantity_consistency():
            for r in self.records:
                if r.r_lot_shipment_in:
                    raise UserError("Please unlink before linking to a new shipment !")
                else:
                    shipped_quantity = Decimal(str(r.r_lot_quantity)).quantize(Decimal("0.00001"))
                    logger.info("LotShipping:%s",shipped_quantity)
                    shipment_origin = None
                    if self.ship.quantity:
                        shipped_quantity = self.ship.quantity
                    if shipped_quantity == 0:
                        shipped_quantity = Decimal(str(r.r_lot_matched)).quantize(Decimal("0.00001"))
                    if self.ship.shipment == 'in':
                        if not self.ship.shipment_in:
                            raise UserError("Shipment not known!")
                        shipment_origin = 'stock.shipment.in,'+str(self.ship.shipment_in.id)
                    elif self.ship.shipment == 'out':
                        if not self.ship.shipment_out:
                            raise UserError("Shipment not known!")
                        shipment_origin = 'stock.shipment.out,'+str(self.ship.shipment_out.id)
                    elif self.ship.shipment == 'int':
                        if not self.ship.shipment_internal:
                            raise UserError("Shipment not known!")
                        shipment_origin = 'stock.shipment.internal,'+str(self.ship.shipment_internal.id)
                    if r.id < 10000000 :
                        l = Lot(r.id)
                        logger.info("IN_SHIPPING1:%s",l)
                        if self.ship.shipment == 'in':
                            l.lot_shipment_in = self.ship.shipment_in
                        elif self.ship.shipment == 'out':
                            l.lot_shipment_out = self.ship.shipment_out
                        elif self.ship.shipment == 'int':
                            l.lot_shipment_internal = self.ship.shipment_internal
                        logger.info("IN_SHIPPING2:%s",l.move)
                        move = None
                        if not l.move:
                            move = l.create_shipment_move(shipment_origin)
                            if not move:
                                raise UserError(
                                    "Cannot create shipment move for lot %s."
                                    % l)
                        logger.info("IN_SHIPPING3:%s",r)
                        if not move:
                            move = Move(l.move)
                        move.shipment = shipment_origin
                        Move.save([move])
                        linked_transit_move = move.get_linked_transit_move()
                        if linked_transit_move:
                            linked_transit_move.shipment = shipment_origin
                            Move.save([linked_transit_move])
                        #Decrease forecasted virtual part shipped
                        vlot_p = l.getVlot_p()
                        l.updateVirtualPart(-l.get_current_quantity_converted(),shipment_origin,l.getVlot_s())
                        l.lot_av = 'reserved'
                        Lot.save([l])
                        l.set_current_quantity(l.lot_quantity,l.lot_gross_quantity,2)
                        Lot.save([l])
                        if l.line:
                            affected_lines.append(l.line)
                        if l.sale_line:
                            affected_lines.append(l.sale_line)
                    else:
                        lqt = LotQt(r.id - 10000000)
                        LotQt.link_to_transport(
                            lqt, shipped_quantity, shipment_origin)
                        if lqt.lot_p and lqt.lot_p.line:
                            affected_lines.append(lqt.lot_p.line)
                        if lqt.lot_s and lqt.lot_s.sale_line:
                            affected_lines.append(lqt.lot_s.sale_line)

        Lot.assert_lines_quantity_consistency(affected_lines)
        return 'end'

    def end(self):
        return 'reload'

class LotShippingStart(ModelView):
    "Link lot to transport"
    __name__ = "lot.shipping.start"
    shipment = fields.Selection([
        ('in', 'Shipment In'),
        ('out', 'Shipment Out'),
        ('int', 'Internal Shipment'),
    ], 'Shipment type', readonly=True)
    shipment_in = fields.Many2One('stock.shipment.in',"Shipment In", states={'invisible': (Eval('shipment') != 'in')})
    shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out", states={'invisible': (Eval('shipment') != 'out')})
    shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal", states={'invisible': (Eval('shipment') != 'int')})
    create_new_shipment = fields.Boolean(
        "Create a new shipment",
        states={'invisible': Eval('shipment') != 'in'})
    shipment_supplier = fields.Many2One(
        'party.party', "Broker",
        states={
            'invisible': ~Bool(Eval('create_new_shipment')),
            'required': Bool(Eval('create_new_shipment')),
            },
        depends=['create_new_shipment'])
    shipment_vessel = fields.Many2One(
        'trade.vessel', "Vessel",
        states={'invisible': ~Bool(Eval('create_new_shipment'))},
        depends=['create_new_shipment'])
    planned_from_location = fields.Many2One(
        'stock.location', "Planned From", readonly=True)
    planned_to_location = fields.Many2One(
        'stock.location', "Planned To", readonly=True)
    full = fields.Boolean("Link all the lot")
    quantity = fields.Numeric("Qt", states={'readonly': (Eval('full') == True)})

    @classmethod
    def default_full(cls):
        return True

    @classmethod
    def default_shipment(cls):
        return 'in'

    @classmethod
    def default_create_new_shipment(cls):
        return False


class LotPlanTransport(Wizard):
    "Plan lot to transport"
    __name__ = "lot.plan_transport"

    start = StateTransition()

    plan = StateView(
        'lot.plan_transport.start',
        'purchase_trade.plan_transport_view_form', [
        Button("Cancel", 'end', 'tryton-cancel'),
        Button("Apply", 'planning', 'tryton-ok', default=True),
        ])

    planning = StateTransition()

    def transition_start(self):
        if self.model.__name__ == 'lot.report':
            return 'plan'
        return 'end'

    @staticmethod
    def _append_lotqt_affected_lines(lotqt, affected_lines):
        if lotqt.lot_p and lotqt.lot_p.line:
            affected_lines.append(lotqt.lot_p.line)
        if lotqt.lot_s and lotqt.lot_s.sale_line:
            affected_lines.append(lotqt.lot_s.sale_line)

    def default_plan(self, fields):
        if not self.records:
            return {}
        lines = []
        default_lotqt = None
        default_unit = None
        for record in self.records:
            quantity = record.r_lot_quantity or record.r_lot_matched or 0
            if record.id < 10000000:
                raise UserError(
                    "Only open quantity lines can be planned.")
            if len(self.records) == 1:
                default_lotqt = record.id - 10000000
                default_unit = (
                    record.r_lot_unit_line.id
                    if record.r_lot_unit_line else None)
            lines.append({
                'lotqt': record.id - 10000000,
                'quantity': quantity,
                'unit': (
                    record.r_lot_unit_line.id
                    if record.r_lot_unit_line else None),
                'cancel_plan': bool(
                    record.r_planned_from_location
                    or record.r_planned_to_location
                    or record.r_planned_transport_type
                    or record.r_planned_from_date
                    or record.r_planned_to_date
                    or record.r_planned_note),
                'planned_from_location': (
                    record.r_planned_from_location.id
                    if record.r_planned_from_location else None),
                'planned_to_location': (
                    record.r_planned_to_location.id
                    if record.r_planned_to_location else None),
                'planned_transport_type': record.r_planned_transport_type,
                'planned_from_date': record.r_planned_from_date,
                'planned_to_date': record.r_planned_to_date,
                'planned_note': record.r_planned_note,
                })
        return {
            'default_lotqt': default_lotqt,
            'default_unit': default_unit,
            'lines': lines,
            }

    def transition_planning(self):
        Lot = Pool().get('lot.lot')
        LotQt = Pool().get('lot.qt')
        affected_lines = []
        if not self.plan.lines:
            raise UserError("Please select at least one quantity line to plan.")
        with Lot.skip_quantity_consistency():
            for line in self.plan.lines:
                lotqt = line.lotqt
                plan_values = {
                    'planned_from_location': (
                        line.planned_from_location.id
                        if line.planned_from_location else None),
                    'planned_to_location': (
                        line.planned_to_location.id
                        if line.planned_to_location else None),
                    'planned_transport_type': line.planned_transport_type,
                    'planned_from_date': line.planned_from_date,
                    'planned_to_date': line.planned_to_date,
                    'planned_note': line.planned_note,
                    }
                self._append_lotqt_affected_lines(lotqt, affected_lines)
                if line.cancel_plan:
                    LotQt.cancel_transport_plan(lotqt)
                else:
                    LotQt.plan_to_transport(
                        lotqt, line.quantity, plan_values)
        Lot.assert_lines_quantity_consistency(affected_lines)
        return 'end'

    def end(self):
        return 'reload'


class LotPlanTransportStart(ModelView):
    "Plan lot to transport"
    __name__ = "lot.plan_transport.start"

    default_lotqt = fields.Many2One(
        'lot.qt', "Default quantity line", readonly=True)
    default_unit = fields.Many2One(
        'product.uom', "Default unit", readonly=True)
    lines = fields.One2Many(
        'lot.plan_transport.line', 'plan', "Planning lines",
        context={
            'lot_plan_transport_default_lotqt': Eval('default_lotqt'),
            'lot_plan_transport_default_unit': Eval('default_unit'),
            },
        depends=['default_lotqt', 'default_unit'])


class LotPlanTransportLine(ModelView):
    "Plan lot to transport line"
    __name__ = "lot.plan_transport.line"

    plan = fields.Many2One('lot.plan_transport.start', "Plan")
    lotqt = fields.Many2One('lot.qt', "Quantity line", readonly=True)
    cancel_plan = fields.Boolean("Cancel existing plan")
    planned_from_location = fields.Many2One(
        'stock.location', "Planned From",
        states={'readonly': Eval('cancel_plan')})
    planned_to_location = fields.Many2One(
        'stock.location', "Planned To",
        states={'readonly': Eval('cancel_plan')})
    planned_transport_type = fields.Selection([
        (None, ''),
        ('vessel', 'Vessel'),
        ('truck', 'Truck'),
    ], "Planned Transport", states={'readonly': Eval('cancel_plan')})
    planned_from_date = fields.Date(
        "Planned From Date", states={'readonly': Eval('cancel_plan')})
    planned_to_date = fields.Date(
        "Planned To Date", states={'readonly': Eval('cancel_plan')})
    planned_note = fields.Char(
        "Planning Note", states={'readonly': Eval('cancel_plan')})
    quantity = fields.Numeric(
        "Quantity", digits='unit', states={'readonly': Eval('cancel_plan')})
    unit = fields.Many2One('product.uom', "Unit", readonly=True)

    @classmethod
    def default_cancel_plan(cls):
        return False

    @classmethod
    def default_lotqt(cls):
        return Transaction().context.get(
            'lot_plan_transport_default_lotqt')

    @classmethod
    def default_unit(cls):
        return Transaction().context.get(
            'lot_plan_transport_default_unit')

class QtWarning(UserWarning):
    pass


class LotRemoveWarning(UserWarning):
    pass


class LotToleranceWarning(UserWarning):
    pass


class LotMatching(Wizard):
    "Matching"
    __name__ = "lot.matching"

    start = StateTransition()

    match = StateView(
        'lot.matching.start',
        'purchase_trade.matching_view_form', [
        Button("Cancel", 'end', 'tryton-cancel'),
        Button("Match", 'matching', 'tryton-ok', default=True),
        ])
    
    matching = StateTransition()

    def transition_start(self):
        return 'match'

    @staticmethod
    def _record_id(record):
        return record.id if record else None

    @classmethod
    def _matching_lot_value(cls, lqt, side):
        lot = lqt.lot_p if side == 'purchase' else lqt.lot_s
        line = getattr(lot, 'line', None)
        sale_line = getattr(lot, 'sale_line', None)
        purchase = getattr(line, 'purchase', None)
        sale = getattr(sale_line, 'sale', None)
        party = (
            getattr(purchase, 'party', None) if side == 'purchase'
            else getattr(sale, 'party', None))
        quantity = Decimal(str(lqt.lot_quantity or 0))
        if side == 'sale':
            quantity = abs(quantity)
        values = {
            'lot_id': cls._record_id(lot),
            'lot_r_id': cls._record_id(lqt),
            'lot_purchase': cls._record_id(purchase),
            'lot_sale': cls._record_id(sale),
            'lot_purchase_reference': (
                getattr(purchase, 'reference', None)
                or getattr(purchase, 'our_reference', None)),
            'lot_sale_reference': (
                getattr(sale, 'reference', None)
                or getattr(sale, 'our_reference', None)),
            'lot_purchase_line_price': getattr(line, 'unit_price', None),
            'lot_sale_line_price': getattr(sale_line, 'unit_price', None),
            'lot_shipment_in': cls._record_id(lqt.lot_shipment_in),
            'lot_shipment_internal': cls._record_id(
                lqt.lot_shipment_internal),
            'lot_shipment_out': cls._record_id(lqt.lot_shipment_out),
            'lot_type': getattr(lot, 'lot_type', None),
            'lot_product': cls._record_id(getattr(lot, 'lot_product', None)),
            'lot_quantity': quantity,
            'lot_matched_qt': 0,
            'lot_already_matched_qt': LotQt._matching_already_matched(
                lqt, side),
            'lot_cp': cls._record_id(party),
        }
        trade_line = line if side == 'purchase' else sale_line
        values.update(LotQt._matching_line_tolerance_info(trade_line))
        return values

    @classmethod
    def _physical_purchase_matching_value(cls, lot):
        line = getattr(lot, 'line', None)
        purchase = getattr(line, 'purchase', None)
        quantity = Decimal(str(lot.get_current_quantity_converted() or 0))
        values = {
            'lot_id': cls._record_id(lot),
            'lot_r_id': None,
            'lot_purchase': cls._record_id(purchase),
            'lot_sale': None,
            'lot_purchase_reference': (
                getattr(purchase, 'reference', None)
                or getattr(purchase, 'our_reference', None)),
            'lot_purchase_line_price': getattr(line, 'unit_price', None),
            'lot_shipment_in': cls._record_id(
                getattr(lot, 'lot_shipment_in', None)),
            'lot_shipment_internal': cls._record_id(
                getattr(lot, 'lot_shipment_internal', None)),
            'lot_shipment_out': cls._record_id(
                getattr(lot, 'lot_shipment_out', None)),
            'lot_type': getattr(lot, 'lot_type', None),
            'lot_product': cls._record_id(getattr(lot, 'lot_product', None)),
            'lot_quantity': quantity,
            'lot_matched_qt': 0,
            'lot_already_matched_qt': 0,
            'lot_cp': cls._record_id(getattr(purchase, 'party', None)),
        }
        values.update(LotQt._matching_line_tolerance_info(line))
        return values

    @classmethod
    def _selected_matching_defaults(cls, active_ids):
        LotQt = Pool().get('lot.qt')
        Lot = Pool().get('lot.lot')
        lot_p = []
        lot_s = []
        for active_id in active_ids or []:
            if active_id <= 10000000:
                lot = Lot(active_id)
                if (getattr(lot, 'lot_type', None) == 'physic'
                        and getattr(lot, 'line', None)
                        and not getattr(lot, 'sale_line', None)):
                    lot_p.append(cls._physical_purchase_matching_value(lot))
                continue
            lqt = LotQt(active_id - 10000000)
            if lqt.lot_p and lqt.lot_s:
                continue
            if lqt.lot_p:
                lot_p.append(cls._matching_lot_value(lqt, 'purchase'))
            elif lqt.lot_s:
                lot_s.append(cls._matching_lot_value(lqt, 'sale'))
        return {
            'lot_p': lot_p,
            'lot_s': lot_s,
        }

    def transition_matching(self):
        Warning = Pool().get('res.user.warning')   
        LotQt = Pool().get('lot.qt')
        Lot = Pool().get('lot.lot')
        if self.match.tot_p != self.match.tot_s:
            warning_name = Warning.format("Quantity's issue", [])
            if Warning.check(warning_name):
                raise QtWarning(warning_name,
                            "Quantities not compatibles")
        with Lot.skip_quantity_consistency():
            LotQt.match_lots(self.match.lot_p,self.match.lot_s)
        affected_lines = []
        for matched_lot in list(self.match.lot_p or []) + list(self.match.lot_s or []):
            if matched_lot.lot_id:
                if matched_lot.lot_id.line:
                    affected_lines.append(matched_lot.lot_id.line)
                if matched_lot.lot_id.sale_line:
                    affected_lines.append(matched_lot.lot_id.sale_line)
            if matched_lot.lot_r_id:
                if matched_lot.lot_r_id.lot_p and matched_lot.lot_r_id.lot_p.line:
                    affected_lines.append(matched_lot.lot_r_id.lot_p.line)
                if matched_lot.lot_r_id.lot_s and matched_lot.lot_r_id.lot_s.sale_line:
                    affected_lines.append(matched_lot.lot_r_id.lot_s.sale_line)
        Lot.assert_lines_quantity_consistency(affected_lines)
        
        return 'end'
    
    def end(self):
        return 'reload'


class LotGoMatching(Wizard):
    "Go to matching"
    __name__ = "lot.go_matching"

    start = StateTransition()

    match = StateView(
        'lot.matching.start',
        'purchase_trade.go_matching_view_form', [
        Button("Cancel", 'end', 'tryton-cancel'),
        Button("Match", 'matching', 'tryton-ok', default=True),
        ])

    matching = StateTransition()

    def transition_start(self):
        return 'match'

    def default_match(self, fields):
        context = Transaction().context
        values = LotMatching._selected_matching_defaults(
            context.get('active_ids') or [])
        if not values.get('lot_p') or not values.get('lot_s'):
            raise UserError(
                "Please select at least one purchase line and one sale line "
                "before using Go to matching.")
        values['qt_type'] = 'all'
        return values

    def transition_matching(self):
        if not self.match.lot_p or not self.match.lot_s:
            raise UserError(
                "Please select at least one purchase line and one sale line "
                "before using Go to matching.")
        return LotMatching.transition_matching(self)

    def end(self):
        return 'reload'


class LotMatchingStart(ModelView):
    "Matching"
    __name__ = "lot.matching.start"
    lot_p = fields.One2Many('lot.matching.lot','lms',"Purchase")
    tot_p = fields.Numeric("Tot. from P")
    lot_s = fields.One2Many('lot.matching.lot','lms',"Sale")
    tot_s = fields.Numeric("Tot. from S")
    filter_p = fields.Boolean("Purchase filter")
    filter_s = fields.Boolean("Sale filter")
    product_s = fields.Many2Many('product.product',None,None,"Sale product")
    product_p = fields.Many2Many('product.product',None,None,"Purchase product")
    location_p = fields.Many2Many('stock.location',None,None,"Purchase destination")
    location_s = fields.Many2Many('stock.location',None,None,"Sale loading")
    cp_p = fields.Many2Many('party.party',None,None,"Supplier")
    cp_s = fields.Many2Many('party.party',None,None,"Client")
    inc_p = fields.Many2Many('incoterm.incoterm',None,None,"Supplier incoterm")
    inc_s = fields.Many2Many('incoterm.incoterm',None,None,"Client incoterm")
    qt_type = fields.Selection([
        ('virtual', 'Open'),
        ('physic', 'Physic'),
        ('all', 'All'),
    ], 'Qt type')
    purchase = fields.Many2One('purchase.purchase',"Purchase")
    sale = fields.Many2One('sale.sale',"Sale")

    @classmethod
    def __setup__(cls):
        super(LotMatchingStart, cls).__setup__()
        cls._buttons.update({
                'filters': {
                    },
                })
    
    @classmethod
    def default_qt_type(cls):
        return 'all'

    @dualmethod
    @ModelView.button
    def filters(cls, lotmatchingstarts):
        pool = Pool()

    # @fields.depends('filter_p')
    # def on_change_filter_p(self, name=None):
    #     if self.filter_p:
    #         ApplyMatching = Pool().get('lot.matching', type='wizard')
    #         session_id, _, _ = ApplyMatching.create()
    #         ApplyMatching.execute(session_id, {}, 'start')

    @fields.depends('lot_p')
    def on_change_with_tot_p(self, name=None):
        return sum([e.lot_matched_qt for e in self.lot_p])

    @fields.depends('lot_s')
    def on_change_with_tot_s(self, name=None):
        return sum([e.lot_matched_qt for e in self.lot_s])

    @fields.depends('product_s','qt_type','sale','cp_s')
    def on_change_with_lot_s(self, name=None):
        lp = []
        LotQt = Pool().get('lot.qt')
        cursor = Transaction().connection.cursor()
        query = LotQt.getQuery(None,self.sale.id if self.sale else None,None,'not matched','all',self.qt_type,None,self.cp_s.id if self.cp_s else None,'S',None,None,self.product_s,None,'open')
        cursor.execute(*query)

        for r in cursor_dict(cursor):
            val = {}
            val['lot_id'] = r['r_lot_s']
            val['lot_r_id'] = r['id'] - 10000000
            val['lot_sale'] = r['r_sale']
            val['lot_purchase'] = r['r_purchase']
            val['lot_shipment_in'] = r['r_lot_shipment_in']
            val['lot_shipment_internal'] = r['r_lot_shipment_internal']
            val['lot_shipment_out'] = r['r_lot_shipment_out']
            val['lot_type'] = r['r_lot_type']
            val['lot_product'] = r['r_lot_product']
            val['lot_quantity'] = -r['r_lot_quantity']
            if r['r_sale'] and r['r_purchase']:
                val['lot_quantity'] = r['r_lot_matched']
            val['lot_matched_qt'] = 0
            val['lot_cp'] = r['r_client']
            lqt = LotQt(r['id'] - 10000000)
            line = LotQt._matching_line(lqt, 'sale')
            sale = getattr(line, 'sale', None)
            val['lot_sale_line_price'] = getattr(line, 'unit_price', None)
            val['lot_sale_reference'] = (
                getattr(sale, 'reference', None)
                or getattr(sale, 'our_reference', None))
            val['lot_already_matched_qt'] = (
                LotQt._matching_already_matched(lqt, 'sale'))
            val.update(LotQt._matching_line_tolerance_info(line))
            lp.append(val)
        return lp

    @fields.depends('cp_p','qt_type','purchase','product_p')
    def on_change_with_lot_p(self, name=None):
        lp = []
        LotQt = Pool().get('lot.qt')
        cursor = Transaction().connection.cursor()
        query = LotQt.getQuery(self.purchase.id if self.purchase else None,None,None,'not matched','all',self.qt_type,self.cp_p.id if self.cp_p else None,None,'P',None,None,self.product_p)
        cursor.execute(*query)

        for r in cursor_dict(cursor):
            val = {}
            val['lot_id'] = r['r_lot_p']
            val['lot_r_id'] = r['id'] - 10000000
            val['lot_purchase'] = r['r_purchase']
            val['lot_sale'] = r['r_sale']
            val['lot_shipment_in'] = r['r_lot_shipment_in']
            val['lot_shipment_internal'] = r['r_lot_shipment_internal']
            val['lot_shipment_out'] = r['r_lot_shipment_out']
            val['lot_type'] = r['r_lot_type']
            val['lot_product'] = r['r_lot_product']
            val['lot_quantity'] = r['r_lot_quantity']
            val['lot_matched_qt'] = 0
            val['lot_cp'] = r['r_supplier']
            lqt = LotQt(r['id'] - 10000000)
            line = LotQt._matching_line(lqt, 'purchase')
            purchase = getattr(line, 'purchase', None)
            val['lot_purchase_line_price'] = getattr(line, 'unit_price', None)
            val['lot_purchase_reference'] = (
                getattr(purchase, 'reference', None)
                or getattr(purchase, 'our_reference', None))
            val['lot_already_matched_qt'] = (
                LotQt._matching_already_matched(lqt, 'purchase'))
            val.update(LotQt._matching_line_tolerance_info(line))
            lp.append(val)
        return lp

class LotMatchingLot(ModelView):
    "Lots"
    __name__ = "lot.matching.lot"

    lms = fields.Many2One('lot.matching.start',"Matching")
    lot_id = fields.Many2One('lot.lot',"Lot")
    lot_r_id = fields.Many2One('lot.qt',"Lot Qt")
    lot_purchase = fields.Many2One('purchase.purchase', "Purchase")
    lot_sale = fields.Many2One('sale.sale', "Sale")
    lot_purchase_reference = fields.Char("Purchase Ref.", readonly=True)
    lot_sale_reference = fields.Char("Sale Ref.", readonly=True)
    lot_purchase_line_price = fields.Numeric(
        "Purchase Price", digits=(16, 4), readonly=True)
    lot_sale_line_price = fields.Numeric(
        "Sale Price", digits=(16, 4), readonly=True)
    lot_name = fields.Char("Name",readonly=True)
    lot_type = fields.Selection([
        ('virtual', 'Open'),
        ('physic', 'Physic'),
        ('loss', 'Loss'),
    ], 'Qt type')
    lot_shipment_in = fields.Many2One('stock.shipment.in',"Shipment In")
    lot_shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal")
    lot_shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out")
    lot_product = fields.Many2One('product.product',"Product",readonly=True)
    lot_quantity = fields.Numeric("Qt",readonly=True)
    lot_theoretical_qt = fields.Numeric("Theoretical Qt", readonly=True)
    lot_targeted_qt = fields.Numeric("Targeted Qt", readonly=True)
    lot_already_matched_qt = fields.Numeric("Already matched", readonly=True)
    lot_qt_min = fields.Numeric("Qt min", readonly=True)
    lot_qt_max = fields.Numeric("Qt max", readonly=True)
    lot_tol_min = fields.Numeric("Tol -", readonly=True)
    lot_tol_max = fields.Numeric("Tol +", readonly=True)
    lot_tolerance_min = fields.Numeric("Tolerance min", readonly=True)
    lot_tolerance_max = fields.Numeric("Tolerance max", readonly=True)
    lot_tolerance_used = fields.Function(
        fields.Numeric("Tolerance used"), 'on_change_with_lot_tolerance_used')
    lot_unit = fields.Many2One('product.uom',"Unit",readonly=True)
    lot_matched_qt = fields.Numeric("Qt to match")
    lot_cp = fields.Many2One('party.party',"Counterparty")
    lot_shipment_origin = fields.Function(
        fields.Reference(
            selection=[
                ("stock.shipment.in", "In"),
                ("stock.shipment.out", "Out"),
                ("stock.shipment.internal", "Internal"),
            ],
            string="Shipment",
        ),
        "get_shipment_origin",
    )

    @fields.depends(
        'lot_matched_qt', 'lot_already_matched_qt', 'lot_theoretical_qt')
    def on_change_with_lot_tolerance_used(self, name=None):
        theoretical = Decimal(str(self.lot_theoretical_qt or 0))
        if not theoretical:
            return Decimal(0)
        projected = (
            Decimal(str(self.lot_already_matched_qt or 0))
            + Decimal(str(self.lot_matched_qt or 0)))
        return round(
            ((projected - theoretical) / theoretical) * Decimal(100), 5)

    def get_shipment_origin(self, name):
        if self.lot_shipment_in:
            return 'stock.shipment.in,' + str(self.lot_shipment_in.id)
        elif self.lot_shipment_out:
            return 'stock.shipment.out,' + str(self.lot_shipment_out.id)
        elif self.lot_shipment_internal:
            return 'stock.shipment.internal,' + str(self.lot_shipment_internal.id)
        return None


class LotUnmatch(Wizard):
    "Unmatch"
    __name__ = "lot.unmatch"

    start = StateTransition()

    def transition_start(self):
        Lot = Pool().get('lot.lot')
        LotQt = Pool().get('lot.qt')
        affected_lines = []
        with Lot.skip_quantity_consistency():
            for r in self.records:
                if r.id > 10000000 :
                    lqt = LotQt(r.id - 10000000)
                    if lqt.lot_p and lqt.lot_s:
                        affected_lines.extend(LotQt.unmatch_lotqt(lqt))
                else:
                    lot = Lot(r.id)
                    qt = lot.get_current_quantity_converted()
                    # #Increase forecasted virtual part matched
                    # if not lot.updateVirtualPart(qt,lot.lot_shipment_origin,lot.getVlot_s()):
                    #     lot.createVirtualPart(qt,lot.lot_shipment_origin,lot.getVlot_s())
                    # #Decrease forecasted virtual part non matched
                    # if not lot.updateVirtualPart(-qt,lot.lot_shipment_origin,None):
                    #     lot.createVirtualPart(-qt,lot.lot_shipment_origin,None)
                    ls = lot.getVlot_s()
                    ls.lot_quantity += qt
                    Lot.save([ls])
                    lot.sale_line = None
                    Lot.save([lot])
                    if lot.line:
                        affected_lines.append(lot.line)
                    if ls.sale_line:
                        affected_lines.append(ls.sale_line)
        Lot.assert_lines_quantity_consistency(affected_lines)
        return 'end'

    def end(self):
        return 'reload'

class LotMarkFinished(Wizard):
    "Mark as finished"
    __name__ = "lot.mark_finished"

    start = StateView(
        'lot.mark_finished.start',
        'purchase_trade.lot_mark_finished_start_view_form', [
        Button("Cancel", 'end', 'tryton-cancel'),
        Button("Apply", 'marking', 'tryton-ok', default=True),
        ])

    marking = StateTransition()

    def transition_marking(self):
        if not (self.start.mark_purchase_line_finished
                or self.start.mark_sale_line_finished):
            raise UserError("Select at least one line side to mark.")
        Lot = Pool().get('lot.lot')
        LotQt = Pool().get('lot.qt')
        affected_lines = []
        with Lot.skip_quantity_consistency():
            for record in self.records:
                if record.id <= 10000000:
                    raise UserError(
                        "Mark as finished is only available on matched "
                        "virtual lines.")
                lqt = LotQt(record.id - 10000000)
                if not lqt.lot_p or not lqt.lot_s:
                    raise UserError(
                        "Mark as finished is only available on matched "
                        "virtual lines.")
                affected_lines.extend(LotQt.mark_lines_finished_from_lotqt(
                    lqt,
                    self.start.mark_purchase_line_finished,
                    self.start.mark_sale_line_finished,
                    unmatch=True))
        Lot.assert_lines_quantity_consistency(affected_lines)
        return 'end'

    def end(self):
        return 'reload'


class LotMarkFinishedStart(ModelView):
    "Mark as finished"
    __name__ = "lot.mark_finished.start"

    mark_purchase_line_finished = fields.Boolean(
        "Mark purchase line as finished")
    mark_sale_line_finished = fields.Boolean(
        "Mark sale line as finished")


class LotUnship(Wizard):
    "Unlink transport"
    __name__ = "lot.unship"

    start = StateTransition()
    
    def transition_start(self):
        Lot = Pool().get('lot.lot')
        LotQt = Pool().get('lot.qt')
        Move = Pool().get('stock.move')
        affected_lines = []
        with Lot.skip_quantity_consistency():
            for r in self.records:
                lqt = LotQt(r.id - 10000000)
                lot = Lot(r.id)
                if lqt and r.r_lot_type == 'virtual':
                    shipment = None
                    qt = Decimal(str(lqt.lot_quantity or 0))
                    mode = 'both'
                    if not lqt.lot_p:
                        lot = lqt.lot_s
                        mode = 'only sale'
                    else:
                        lot = lqt.lot_p
                    if lqt.lot_shipment_in:
                        shipment = lqt.lot_shipment_in
                    elif lqt.lot_shipment_internal:
                        shipment = lqt.lot_shipment_internal
                    elif lqt.lot_shipment_out:
                        shipment = lqt.lot_shipment_out
                    if not shipment:
                        continue
                    if lqt.lot_p and lqt.lot_p.line:
                        affected_lines.append(lqt.lot_p.line)
                    if lqt.lot_s and lqt.lot_s.sale_line:
                        affected_lines.append(lqt.lot_s.sale_line)
                    # Restore the selected scheduled quantity only once, then
                    # remove its shipment-linked lot.qt so it cannot be
                    # unlinked again from the shipment planned tab.
                    if qt:
                        if lqt.is_planned_transport():
                            LotQt.restore_planned_open_quantity(lqt, qt)
                        elif not lot.updateVirtualPart(qt,None,lqt.lot_s,mode):
                            lot.createVirtualPart(qt,None,lqt.lot_s,mode)
                    LotQt.delete([lqt])
                if lot and r.r_lot_type == 'physic':
                    lot = Lot(r.r_lot_p)
                    if lot.lot_type == 'physic':
                        str_sh = str(lot.lot_shipment_origin)
                        if 'stock.shipment.in' in str_sh:
                            lot.lot_shipment_in = None
                        elif 'stock.shipment.out' in str_sh:
                            lot.lot_shipment_out = None
                        elif 'stock.shipment.internal' in str_sh:
                            lot.lot_shipment_internal = None

                        if lot.move:
                            move = Move(lot.move)
                            move.shipment = None
                            Move.save([move])
                            # lm = LotMove.search([('lot','=',lot.id),('move','=',lot.move)])
                            # if lm:
                            #     LotMove.delete(lm)
                        lot.lot_status = 'forecast'
                        Lot.save([lot])
                        if lot.line:
                            affected_lines.append(lot.line)
                        if lot.sale_line:
                            affected_lines.append(lot.sale_line)
        Lot.assert_lines_quantity_consistency(affected_lines)
        return 'end'

    def end(self):
        return 'reload'

class LotAdding(Wizard):
    "Adding physical lots"
    __name__ = "lot.add"

    start = StateTransition()

    add = StateView(
        'lot.add.lot',
        'purchase_trade.add_lot_view_form', [
        Button("Cancel", 'end', 'tryton-cancel'),
        Button("Add", 'adding', 'tryton-ok', default=True),
        ])
    
    adding = StateTransition()

    def transition_start(self):
        return 'add'

    def default_add(self, fields):
        unit = None
        quantity = None
        sh_in = None
        sh_int = None
        sh_out = None
        Lot = Pool().get('lot.lot')
        LotQt = Pool().get('lot.qt')
        context = Transaction().context
        ids = context.get('active_ids')
        for i in ids:
            val = {}
            if i > 10000000 :
                lqt = LotQt(i - 10000000)
                if not lqt.lot_p:
                    raise UserError("To add physical lot to sale, use action 'Apply matching'!")
                lot = lqt.lot_p
                purchase = lot.line.purchase
                unit = lot.lot_unit_line.id
                quantity = lqt.lot_quantity
                sh_in = lqt.lot_shipment_in.id if lqt.lot_shipment_in else None
                sh_int = lqt.lot_shipment_internal.id if lqt.lot_shipment_internal else None
                sh_out = lqt.lot_shipment_out.id if lqt.lot_shipment_out else None
            else:
                raise UserError("Trying to add physical lots to physical lot!")
        return {
            'unit': unit,
            'quantity': quantity,
            'unlink_remainder': True,
            'shipment_in':sh_in,
            'shipment_internal':sh_int,
            'shipment_out':sh_out,
            'transport_missing': not any((sh_in, sh_int, sh_out)),
            'shipment_supplier': None,
            'transport_from_location': (
                (lqt.planned_from_location or purchase.from_location).id
                if not any((sh_in, sh_int, sh_out))
                and (lqt.planned_from_location or purchase.from_location)
                else None),
            'transport_to_location': (
                (lqt.planned_to_location or purchase.to_location).id
                if not any((sh_in, sh_int, sh_out))
                and (lqt.planned_to_location or purchase.to_location)
                else None),
            }
        
    def transition_adding(self):
        if self.records:
            LotQt = Pool().get('lot.qt')
            lqt = LotQt(self.records[0].id - 10000000)
            if not lqt.has_shipment():
                shipment = self.add.shipment_in
                if self.add.create_new_shipment and shipment:
                    raise UserError(
                        "Choose either an existing shipment or create a new "
                        "shipment.")
                if self.add.create_new_shipment:
                    if not self.add.shipment_supplier:
                        raise UserError("Shipment broker is required.")
                    ShipmentIn = Pool().get('stock.shipment.in')
                    values = {
                        'supplier': self.add.shipment_supplier.id,
                    }
                    if self.add.transport_from_location:
                        values['from_location'] = (
                            self.add.transport_from_location.id)
                    if self.add.transport_to_location:
                        values['to_location'] = self.add.transport_to_location.id
                    if self.add.shipment_bl_number:
                        values['bl_number'] = self.add.shipment_bl_number
                    if self.add.shipment_bl_date:
                        values['bl_date'] = self.add.shipment_bl_date
                    shipment, = ShipmentIn.create([values])
                if not shipment:
                    raise UserError(
                        "Select a Shipment In or create a new shipment.")
                Lot = Pool().get('lot.lot')
                with Lot.skip_quantity_consistency():
                    lqt = LotQt.link_to_transport(
                        lqt, abs(Decimal(str(lqt.lot_quantity or 0))),
                        'stock.shipment.in,%s' % shipment.id)
            LotQt.add_physical_lots(
                lqt, self.add.lots, self.add.unlink_remainder)
            LotQt.mark_lines_finished_from_lotqt(
                lqt,
                getattr(self.add, 'mark_purchase_line_finished', False),
                getattr(self.add, 'mark_sale_line_finished', False))
        return 'end'
    
    def end(self):
        return 'reload'

class LotAddLot(ModelView):
    "Add physical lots"
    __name__ = "lot.add.lot"
    unit = fields.Many2One('product.uom',"Unit",readonly=True)
    quantity = fields.Numeric("Quantity available",digits='unit',readonly=True)
    unlink_remainder = fields.Boolean("Unlink remaining quantity")
    mark_purchase_line_finished = fields.Boolean(
        "Mark purchase line as finished")
    mark_sale_line_finished = fields.Boolean(
        "Mark sale line as finished")
    transport_missing = fields.Boolean(
        "Transport missing", readonly=True,
        states={'invisible': ~Eval('transport_missing')})
    create_new_shipment = fields.Boolean(
        "Create a new shipment",
        states={'invisible': ~Eval('transport_missing')})
    shipment_supplier = fields.Many2One(
        'party.party', "Broker",
        states={
            'invisible': ~(
                Eval('transport_missing') & Eval('create_new_shipment')),
            'required': (
                Eval('transport_missing') & Eval('create_new_shipment')),
        })
    transport_from_location = fields.Many2One(
        'stock.location', "From location", readonly=True,
        states={'invisible': ~Eval('transport_missing')})
    transport_to_location = fields.Many2One(
        'stock.location', "To location", readonly=True,
        states={'invisible': ~Eval('transport_missing')})
    shipment_bl_number = fields.Char(
        "BL number",
        states={'invisible': ~(
            Eval('transport_missing') & Eval('create_new_shipment'))})
    shipment_bl_date = fields.Date(
        "BL date",
        states={'invisible': ~(
            Eval('transport_missing') & Eval('create_new_shipment'))})
    shipment_in = fields.Many2One(
        'stock.shipment.in', "Shipment In",
        states={
            'invisible': ~Eval('transport_missing') & ~Eval('shipment_in'),
            'readonly': ~Eval('transport_missing'),
        })
    shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal",states={'invisible': ~Eval('shipment_internal')})
    shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out",states={'invisible': ~Eval('shipment_out')})
    lots = fields.One2Many('lot.add.line','lal',"Lots")

    @classmethod
    def default_unlink_remainder(cls):
        return True

    @classmethod
    def default_mark_purchase_line_finished(cls):
        return False

    @classmethod
    def default_mark_sale_line_finished(cls):
        return False

    @classmethod
    def default_create_new_shipment(cls):
        return False

class LotAddLine(ModelView):
    "Lots"
    __name__ = "lot.add.line"

    lal = fields.Many2One('lot.add.lot',"Lots")
    lot_qt = fields.Numeric("Packing detail")
    lot_unit = fields.Many2One('product.uom', "Unit",required=True)
    lot_quantity = fields.Numeric("Net weight")
    lot_gross_quantity = fields.Numeric("Gross weight")
    lot_unit_line = fields.Many2One('product.uom', "Unit",required=True)
    lot_premium = fields.Numeric("Premium")
    lot_chunk_key = fields.Integer("Chunk key")

    @classmethod
    def default_lot_qt(cls):
        if is_itsa_company():
            return Decimal('1')

    @classmethod
    def default_lot_unit(cls):
        if is_itsa_company():
            return default_itsa_bulk_unit()

    @classmethod
    def default_lot_unit_line(cls):
        if is_itsa_company():
            return default_itsa_unit()

    @fields.depends('lot_quantity', 'lot_gross_quantity')
    def on_change_lot_quantity(self):
        if (is_itsa_company()
                and self.lot_quantity
                and not self.lot_gross_quantity):
            self.lot_gross_quantity = self.lot_quantity

    # @fields.depends('lot_qt')
    # def on_change_with_lot_quantity(self):
    #     if not self.lot_quantity:
    #         return self.lot_qt

    # @fields.depends('lot_qt')
    # def on_change_with_lot_gross_quantity(self):
    #     if not self.lot_gross_quantity:
    #         return self.lot_qt

    # @fields.depends('lot_unit')
    # def on_change_with_lot_unit_line(self):
    #     if not self.lot_unit_line:
    #         return self.lot_unit
    

class LotImporting(Wizard):
    "Importing physical lots"
    __name__ = "lot.import"

    start = StateTransition()

    imp = StateView(
        'lot.import.lot',
        'purchase_trade.import_lot_view_form', [
        Button("Cancel", 'end', 'tryton-cancel'),
        Button("Import", 'importing', 'tryton-ok', default=True),
        ])
    
    importing = StateTransition()

    def transition_start(self):
        return 'imp'
        
    def transition_importing(self):
        if self.records:
            line = self.records[0].r_line
            Lot = Pool().get('lot.lot')
            LotQt = Pool().get('lot.qt')
            LotQtHist = Pool().get('lot.qt.hist')
            LotQtType = Pool().get('lot.qt.type')
            lqt = LotQt(self.records[0].id - 10000000)
            tot_qt = 0
            affected_lines = [line]
            if self.records[0].r_sale_line:
                affected_lines.append(self.records[0].r_sale_line)
            with Lot.skip_quantity_consistency():
                for l in self.imp.lots:
                    l.line = line
                    l.lot_shipment_in = self.records[0].r_lot_shipment_in
                    l.lot_shipment_internal = self.records[0].r_lot_shipment_internal
                    l.lot_shipment_out = self.records[0].r_lot_shipment_out
                    l.lot_product = self.records[0].r_lot_product
                    l.sale_line = self.records[0].r_sale_line
                    l.lot_type = 'physic'
                    premium = Decimal(0)
                    if l.lot_price:
                        premium = l.lot_price - l.line.unit_price
                        logger.info("IMPORTLOT:%s",l.line.linked_price)
                        if l.line.linked_price:
                            premium = l.lot_price - l.line.linked_price
                    l.lot_premium = premium
                    l.lot_premium_sup = premium
                    Uom = Pool().get('product.uom')
                    net_qt = round(Decimal(Uom.compute_qty(l.lot_unit_line, float(l.lot_quantity), line.unit)),5)
                    gross_qt = round(Decimal(Uom.compute_qty(l.lot_unit_line, float(l.lot_gross_quantity), line.unit)),5)
                    lqtt = LotQtType.search([('sequence','=',1)])
                    if lqtt:
                        lqh = LotQtHist()
                        lqh.quantity_type = lqtt[0]
                        lqh.quantity = l.lot_quantity
                        lqh.gross_quantity = l.lot_gross_quantity
                        l.lot_hist = [lqh]
                    Lot.save([l])
                    tot_qt += net_qt
                    logger.info("IMPORTING:%s",net_qt)
                    #need to decrease sale virtual lot if matching
                    if self.records[0].r_lot_s:
                        ls = Lot(self.records[0].r_lot_s)
                        ls.lot_quantity -= net_qt
                        ls.lot_gross_quantity -= net_qt
                        ls.lot_qt -= float(net_qt)
                        Lot.save([ls])
                lqt.lot_quantity -= tot_qt
                if lqt.lot_quantity < 0:
                    lqt.lot_quantity = 0
                LotQt.save([lqt])
            Lot.assert_lines_quantity_consistency(affected_lines)
        return 'end'
    
    def end(self):
        return 'reload'

class LotImportLot(ModelView):
    "Import physical lots"
    __name__ = "lot.import.lot"

    reference = fields.Char("Reference")
    lots = fields.One2Many('lot.lot','',"Lots")

    @fields.depends('reference')
    def on_change_with_lots(self, name=None):
        lp = []
        LotFCR = Pool().get('lot.fcr')
        if self.reference:
            lfcrs = LotFCR.search([('lot_offer','=',self.reference)])
            for r in lfcrs:
                val = {}
                if r.lot_product:
                    val['lot_product'] = r.lot_product.id
                val['lot_qt'] = r.lot_qt
                val['lot_quantity'] = r.lot_quantity
                if r.lot_unit:
                    val['lot_unit'] = r.lot_unit.id
                if r.lot_unit_line:
                    val['lot_unit_line'] = r.lot_unit_line.id
                val['lot_gross_quantity'] = r.lot_gross_quantity
                val['lot_premium'] = r.lot_premium
                val['lot_price'] = r.lot_price
                lp.append(val)
            return lp

class LotRemove(Wizard):
    "Remove lots"
    __name__ = "lot.remove"

    start = StateTransition()

    @staticmethod
    def _remove_warning_message(lot):
        messages = []
        if lot.sale_line:
            messages.append("matched")
        if (lot.lot_shipment_in or lot.lot_shipment_internal
                or lot.lot_shipment_out):
            messages.append("shipped")
        if messages:
            return (
                "This physical lot is %s. By clicking yes, the physical lot "
                "and its draft stock move will be deleted and the quantity "
                "will be restored as open quantity."
                % " and ".join(messages))
        return None

    def transition_start(self):
        Lot = Pool().get('lot.lot')
        LotQt = Pool().get('lot.qt')
        Move = Pool().get('stock.move')
        Warning = Pool().get('res.user.warning')
        for r in self.records:
            if r.id > 10000000 :
                raise UserError("Trying to remove open lots!")
            lot = Lot(r.id)
            if lot.move and lot.move.state != 'draft':
                raise UserError(
                    "Trying to remove a lot with a stock move that is not "
                    "draft!")
            warning_message = self._remove_warning_message(lot)
            if warning_message:
                warning_name = Warning.format("Remove physical lot", [lot])
                if Warning.check(warning_name):
                    raise LotRemoveWarning(warning_name, warning_message)
            quantity = lot.get_current_quantity_converted()
            shipment_origin = lot.lot_shipment_origin
            lot_s = lot.getVlot_s() if lot.sale_line else None
            #delete move first if exist
            if lot.move:
                Move.delete([lot.move])
            if lot.is_planned_transport():
                LotQt.restore_planned_scheduled_quantity(
                    lot, quantity, lot_s, shipment_origin)
            elif not lot.updateVirtualPart(quantity, shipment_origin, lot_s):
                lot.createVirtualPart(quantity, shipment_origin, lot_s)
            Lot.delete([lot])
        return 'end'

    def end(self):
        return 'reload'

class LotInvoice(Wizard):
    "Invoice lots"
    __name__ = "lot.invoice"

    start = StateTransition()

    inv = StateView(
        'lot.invoice.start',
        'purchase_trade.invoice_lot_view_form', [
        Button("Cancel", 'end', 'tryton-cancel'),
        Button("Invoice", 'invoicing', 'tryton-ok', default=True),
        ])
    
    invoicing = StateTransition()

    message = StateView(
        'purchase.create_prepayment.message',
        'purchase_trade.create_prepayment_message_form',
        [
            Button('OK', 'end', 'tryton-ok'),
            Button('See Invoice', 'see_invoice', 'tryton-go-next'),
        ]
    )

    see_invoice = StateAction('account_invoice.act_invoice_form')

    def transition_start(self):
        return 'inv'
    
    def default_inv(self, fields):
        lot_p = []
        lot_s = []
        fee_pur = []
        pp_pur = []
        fee_sale = []
        pp_sale = []
        act = 'prov'
        line = None
        sale_line = None
        purchase_lines = []
        sale_lines = []
        val = {}
        Lot = Pool().get('lot.lot')
        LotQt = Pool().get('lot.qt')
        context = Transaction().context
        ids = context.get('active_ids')
        unit = None
        for i in ids:
            val = {}
            lqts = LotQt.search(['id','=',i - 10000000])
            if lqts:
                lot = lqts[0].lot_p
            else:
                lot = Lot(i)
            line = lot.line
            if line and line not in purchase_lines:
                purchase_lines.append(line)
            if lot.line.purchase:
                if lot.line.purchase.wb:
                    if lot.line.purchase.wb.qt_type in [e.quantity_type for e in lot.lot_hist]:
                        act = 'final'
            val['lot'] = lot.id
            val['lot_name'] = lot.lot_name
            val['lot_state'] = lot.lot_state.id
            val['lot_pur_inv_state'] = lot.lot_pur_inv_state
            val['lot_sale_inv_state'] = lot.lot_sale_inv_state
            val['lot_diff_quantity'] = Decimal(0)
            val['lot_diff_price'] = Decimal(0)
            val['lot_diff_amount'] = Decimal(0)
            val['lot_quantity'] = lot.get_current_quantity_converted()
            val['sale_invoice_padding'] = lot.sale_invoice_padding
            val['lot_price'] = lot.lot_price
            val['lot_amount'] = lot.get_current_quantity_converted() * lot.lot_price if lot.lot_price else Decimal(0)
            if lot.invoice_line_prov:
                val['lot_diff_quantity'] = val['lot_quantity'] - Decimal(lot.invoice_line_prov.quantity)
                val['lot_diff_price'] = val['lot_price'] - Decimal(lot.invoice_line_prov.unit_price)
                val['lot_diff_amount'] = val['lot_amount'] - Decimal(lot.invoice_line_prov.amount)
            val['lot_unit'] = line.unit.id #lot.lot_unit_line.id
            unit = val['lot_unit']
            val['lot_currency'] = lot.lot_price_ct_symbol
            lot_p.append(val)
            sale_line = lot.sale_line
            if sale_line and sale_line not in sale_lines:
                sale_lines.append(sale_line)
            val_s = val.copy()  # ou utiliser deepcopy si certains champs sont des objets imbriqués
            val_s['lot_price'] = lot.lot_price_sale
            val_s['lot_amount'] = lot.get_current_quantity_converted() * lot.lot_price_sale if lot.lot_price_sale else Decimal(0)
            val_s['lot_diff_quantity'] = 0
            val_s['lot_diff_price'] = 0
            val_s['lot_diff_amount'] = 0
            if lot.sale_invoice_line_prov:
                provisional_quantity = (
                    Decimal(lot.sale_invoice_line_prov.quantity)
                    - Decimal(str(lot.sale_invoice_padding or 0)))
                val_s['lot_diff_quantity'] = (
                    val_s['lot_quantity'] - provisional_quantity)
                val_s['lot_diff_price'] = val_s['lot_price'] - Decimal(lot.sale_invoice_line_prov.unit_price)
                provisional_amount = (
                    provisional_quantity
                    * Decimal(lot.sale_invoice_line_prov.unit_price))
                val_s['lot_diff_amount'] = (
                    val_s['lot_amount'] - provisional_amount)
            val_s['lot_currency'] = lot.lot_price_ct_symbol_sale
            val_s['lot_unit'] = sale_line.unit.id if sale_line else None
            lot_s.append(val_s)
        seen_fees = set()
        for purchase_line in purchase_lines:
            if purchase_line.fees:
                for f in purchase_line.fees:
                    if f.type == 'ordered':
                        fee_key = getattr(f, 'id', None) or id(f)
                        if fee_key in seen_fees:
                            continue
                        seen_fees.add(fee_key)
                        val = {}
                        val['fee'] = f.id
                        val['fee_type'] = f.product.id
                        val['fee_quantity'] = f.quantity
                        val['fee_price'] = f.price
                        val['fee_unit'] = purchase_line.unit.id
                        val['fee_amount'] = self._fee_display_amount(f, act)
                        val['fee_landed_cost'] = f.fee_landed_cost
                        fee_pur.append(val)
        if line:
            if line.purchase:
                Prep = Pool().get('account.invoice')
                prep = Prep.search([('description','=','Prepayment'),('party','=',line.purchase.party.id),('state','!=','draft')])
                if prep:
                    for p in prep:
                        for li in p.lines:
                            val = {}
                            val['inv'] = li.id
                            val['inv_amount'] = li.amount
                            pp_pur.append(val)
        seen_fees = set()
        for current_sale_line in sale_lines:
            if current_sale_line.fees:
                for f in current_sale_line.fees:
                    if f.type == 'ordered':
                        fee_key = getattr(f, 'id', None) or id(f)
                        if fee_key in seen_fees:
                            continue
                        seen_fees.add(fee_key)
                        val = {}
                        val['fee'] = f.id
                        val['fee_type'] = f.product.id
                        val['fee_quantity'] = f.quantity
                        val['fee_price'] = f.price
                        val['fee_unit'] = current_sale_line.unit.id
                        val['fee_amount'] = self._fee_display_amount(f, act)
                        val['fee_landed_cost'] = f.fee_landed_cost
                        fee_sale.append(val)
        if sale_line:
            if sale_line.sale:
                Prep = Pool().get('account.invoice')
                prep = Prep.search([('description','=','Prepayment'),('party','=',sale_line.sale.party.id),('state','!=','draft')])
                if prep:
                    for p in prep:
                        for li in p.lines:
                            val = {}
                            val['inv'] = li.id
                            val['inv_amount'] = li.amount
                            pp_sale.append(val)
        return {
            'lot_p': lot_p,
            'lot_s': lot_s,
            'unit': unit,
            'fee_pur': fee_pur,
            'pp_pur': pp_pur,
            'fee_sale': fee_sale,
            'pp_sale': pp_sale,
            'action': act
            }

    @staticmethod
    def _fee_display_amount(fee, action):
        if (
                action == 'final'
                and getattr(fee, 'state', None) == 'invoiced'
                and getattr(fee, 'add_padding', False)):
            with Transaction().set_context(
                    _purchase_trade_ignore_fee_padding=True):
                return fee.amount
        return fee.amount

    def transition_invoicing(self):
        Lot = Pool().get('lot.lot')
        Purchase = Pool().get('purchase.purchase')
        Sale = Pool().get('sale.sale')
        lots = []
        purchases = []
        sales = []
        action = self.inv.action
        for r in self.records:
            purchase = r.r_line.purchase if r.r_line else None
            sale = r.r_sale_line.sale if r.r_sale_line else None
            if purchase and purchase not in purchases:
                purchases.append(purchase)
            if sale and sale not in sales:
                sales.append(sale)
            lot = Lot(r.r_lot_p)
            # if lot.move == None:
            #     Warning = Pool().get('res.user.warning')   
            #     warning_name = Warning.format("Lot not confirmed", [])
            #     if Warning.check(warning_name):
            #         raise QtWarning(warning_name,
            #                     "Lot not confirmed, click yes to confirm and invoice")
            #         continue
            if self.inv.type == 'purchase' and lot.invoice_line:
                continue
            if self.inv.type == 'sale' and lot.sale_invoice_line:
                continue
            lots.append(lot)

        invoice_line = None
        if self.inv.type == 'purchase':
            Purchase._process_invoice(purchases, lots, action, self.inv.pp_pur)
            for lot in lots:
                lot = Lot(lot.id)
                invoice_line = lot.invoice_line or lot.invoice_line_prov
                if invoice_line:
                    break
        else:
            if sales:
                self._apply_sale_invoice_padding(lots)
                Sale._process_invoice(sales, lots, action, self.inv.pp_sale)
                for lot in lots:
                    lot = Lot(lot.id)
                    invoice_line = lot.sale_invoice_line or lot.sale_invoice_line_prov
                    if invoice_line:
                        break
        if not invoice_line:
            raise UserError("No invoice line was generated from the selected lots.")
        self._invoiced_fee_count = self._invoice_selected_fees()
        self.message.invoice = invoice_line.invoice

        return 'message'

    def _invoice_selected_fees(self):
        Fee = Pool().get('fee.fee')
        fee_lines = (
            self.inv.fee_pur
            if self.inv.type == 'purchase' else self.inv.fee_sale)
        fees = []
        seen = set()
        for line in fee_lines or []:
            fee = getattr(line, 'fee', None)
            if not getattr(line, 'to_invoice', False) or not fee:
                continue
            fee_key = getattr(fee, 'id', None) or id(fee)
            if fee_key in seen:
                continue
            seen.add(fee_key)
            action = _fee_invoice_action(fee, self.inv.action)
            if action == 'already_invoiced':
                continue
            fees.append(fee)
        if fees:
            Fee.invoice(fees)
        return len(fees)

    @classmethod
    def _split_sale_padding(cls, padding, lots):
        padding = Decimal(str(padding or 0))
        if not padding or not lots:
            return {}
        share = padding / Decimal(len(lots))
        return {lot.id: share for lot in lots}

    def _apply_sale_invoice_padding(self, lots):
        Lot = Pool().get('lot.lot')
        if self.inv.type != 'sale' or self.inv.action != 'prov':
            return
        padding_by_lot = self._split_sale_padding(self.inv.sale_padding, lots)
        for lot in lots:
            lot.sale_invoice_padding = padding_by_lot.get(lot.id, Decimal(0))
        Lot.save(lots)

    def default_message(self, fields):
        fee_count = getattr(self, '_invoiced_fee_count', 0)
        message = 'Invoice created.'
        if fee_count:
            message += ' Fee invoices: %s.' % fee_count
        return {
            'message': message,
        }
    
    def do_see_invoice(self, action):
        action['views'].reverse()  # pour ouvrir en form directement
        logger.info("*************SEE_INVOICE******************:%s",self.message.invoice)
        return action, {'res_id':self.message.invoice.id}

    def end(self):
        return 'reload'

class LotInvoiceStart(ModelView):
    "Invoice physical lots"
    __name__ = "lot.invoice.start"

    invoice = fields.Many2One('account.invoice',"Invoice to add lines")
    lot_p = fields.One2Many('lot.invoicing.lot','lis',"Lots",states={'invisible': Eval('type')=='sale'})
    lot_s = fields.One2Many('lot.invoicing.lot','lis',"Lots",states={'invisible': Eval('type')=='purchase'})
    fee_sale = fields.One2Many('lot.invoicing.fee','lfs',"Fees",states={'invisible': Eval('type')=='purchase'})
    pp_sale = fields.One2Many('lot.invoicing.inv','lps',"Prepayments",states={'invisible': Eval('type')=='purchase'})
    fee_pur = fields.One2Many('lot.invoicing.fee','lfs',"Fees",states={'invisible': Eval('type')=='sale'})
    pp_pur = fields.One2Many('lot.invoicing.inv','lps',"Prepayments",states={'invisible': Eval('type')=='sale'})
    unit = fields.Many2One('product.uom',"Unit",readonly=True)
    action = fields.Selection([
        ('add', 'Add lines'),
        ('final', 'Final'),
        ('prov', 'Provisional'),
    ], 'Action', required=True)

    type = fields.Selection([
        ('purchase', 'Purchase'),
        ('sale', 'Sale'),
    ], 'Type', required=True)
    
    quantity = fields.Numeric("Quantity to invoice",digits='unit',readonly=True,states={'invisible': Eval('type')=='sale'})
    amount = fields.Numeric("Amount to invoice",digits='unit',readonly=True,states={'invisible': Eval('type')=='sale'})
    quantity_s = fields.Numeric("Quantity to invoice",digits='unit',readonly=True,states={'invisible': Eval('type')=='purchase'})
    amount_s = fields.Numeric("Amount to invoice",digits='unit',readonly=True,states={'invisible': Eval('type')=='purchase'})
    sale_padding = fields.Numeric("Global padding", digits='unit',
        states={
            'invisible': (Eval('type') == 'purchase') | (Eval('action') != 'prov'),
            })

    @fields.depends('type')
    def on_change_with_action(self, name=None):
        if self.lot_p and self.type == 'purchase':
            purchase = getattr(getattr(self.lot_p[0].lot, 'line', None), 'purchase', None)
            wb = getattr(purchase, 'wb', None)
            if wb and wb.qt_type in [e.quantity_type for e in self.lot_p[0].lot.lot_hist]:
                return 'final'
            else:
                return 'prov'
        if self.lot_s and self.type == 'sale':
            sale = getattr(getattr(self.lot_s[0].lot, 'sale_line', None), 'sale', None)
            wb = getattr(sale, 'wb', None)
            if wb and wb.qt_type in [e.quantity_type for e in self.lot_s[0].lot.lot_hist]:
                return 'final'
            else:
                return 'prov'
            
    @fields.depends('lot_p','type','quantity')
    def on_change_with_quantity(self, name=None):
        if self.lot_p and self.type == 'purchase':
            quantity = Decimal(0)
            for l in self.lot_p:
                if l.lot.invoice_line_prov:
                    quantity += l.lot_diff_quantity
                else:
                    quantity += l.lot_quantity
            return quantity

    @fields.depends('lot_p','type','amount')
    def on_change_with_amount(self, name=None):
        if self.lot_p and self.type == 'purchase':
            amount = Decimal(0)
            for l in self.lot_p:
                if l.lot.invoice_line_prov:
                    amount += l.lot_diff_amount
                else:
                    amount += l.lot_amount
            return amount

    @fields.depends('lot_s','type','quantity_s','sale_padding')
    def on_change_with_quantity_s(self, name=None):
        if self.lot_s and self.type == 'sale':
            quantity = Decimal(0)
            for l in self.lot_s:
                if l.lot.sale_invoice_line_prov:
                    quantity += l.lot_diff_quantity
                else:
                    quantity += l.lot_quantity
            quantity += Decimal(str(self.sale_padding or 0))
            return quantity

    @fields.depends('lot_s','type','amount_s','sale_padding')
    def on_change_with_amount_s(self, name=None):
        if self.lot_s and self.type == 'sale':
            amount = Decimal(0)
            padding = Decimal(str(self.sale_padding or 0))
            padding_share = (
                padding / Decimal(len(self.lot_s))
                if padding and self.lot_s else Decimal(0))
            for l in self.lot_s:
                if l.lot.sale_invoice_line_prov:
                    amount += l.lot_diff_amount
                else:
                    amount += l.lot_amount
                amount += padding_share * Decimal(str(l.lot_price or 0))
            return amount

    def _get_sale_padding_by_lot(self):
        lots = [
            line.lot for line in (self.lot_s or [])
            if getattr(line, 'lot', None)]
        return self._split_sale_padding(self.sale_padding, lots)

    @classmethod
    def _split_sale_padding(cls, padding, lots):
        padding = Decimal(str(padding or 0))
        if not padding or not lots:
            return {}
        share = padding / Decimal(len(lots))
        return {lot.id: share for lot in lots}

    @staticmethod
    def _record_key(record):
        return getattr(record, 'id', None) or id(record)

    def _fee_wizard_padding_quantity(self, fee, padding_by_lot):
        if self._fee_wizard_ignores_padding(fee):
            return Decimal(0)
        if not getattr(fee, 'add_padding', False):
            return Decimal(0)
        quantity = Decimal(0)
        for lot in fee._get_effective_fee_lots():
            padding = padding_by_lot.get(self._record_key(lot), Decimal(0))
            if not padding:
                continue
            source_line = (
                getattr(fee, 'sale_line', None)
                or getattr(lot, 'sale_line', None)
                or getattr(fee, '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)
            quantity += fee._convert_padding_quantity(padding, source_unit)
        return quantity

    def _fee_wizard_billing_quantity(self, fee, padding_by_lot):
        quantity = fee._get_effective_fee_lots_quantity()
        if quantity is None:
            quantity = getattr(fee, 'quantity', None)
        if quantity is None:
            quantity = fee.get_quantity()
        return (
            Decimal(quantity or 0)
            + self._fee_wizard_padding_quantity(fee, padding_by_lot))

    @staticmethod
    def _fee_wizard_unit_price(fee):
        line = getattr(fee, 'sale_line', None) or getattr(fee, 'line', None)
        if line:
            return Decimal(str(getattr(line, 'unit_price', 0) or 0))
        for lot in fee._get_effective_fee_lots():
            if getattr(lot, 'sale_line', None):
                return Decimal(str(
                    getattr(lot.sale_line, 'unit_price', 0) or 0))
            if getattr(lot, 'line', None):
                return Decimal(str(getattr(lot.line, 'unit_price', 0) or 0))
        return Decimal(0)

    def _fee_wizard_ignores_padding(self, fee):
        return (
            self.action == 'final'
            and getattr(fee, 'state', None) == 'invoiced')

    @fields.depends('type', 'action', 'sale_padding', 'lot_s', 'fee_sale')
    def on_change_sale_padding(self):
        if self.type != 'sale' or not self.fee_sale:
            return
        padding_by_lot = self._get_sale_padding_by_lot()
        for fee_line in self.fee_sale:
            fee = getattr(fee_line, 'fee', None)
            if not fee or not getattr(fee, 'add_padding', False):
                continue
            if fee.mode not in {'perqt', 'pprice'}:
                continue
            quantity = self._fee_wizard_billing_quantity(fee, padding_by_lot)
            fee_line.fee_quantity = quantity
            if fee.mode == 'perqt':
                fee_line.fee_amount = round(
                    quantity * Decimal(str(fee.price or 0)), 2)
            elif fee.mode == 'pprice':
                fee_line.fee_amount = round(
                    Decimal(str(fee.price or 0)) / Decimal(100)
                    * quantity * self._fee_wizard_unit_price(fee), 2)

    @classmethod
    def default_action(cls):
        return 'prov'

    @classmethod
    def default_type(cls):
        return 'purchase'

class LotInvoicingFee(ModelView):
    "Fees"
    __name__ = "lot.invoicing.fee"

    lfs = fields.Many2One('lot.invoice.start',"Invoicing")
    fee = fields.Many2One('fee.fee',"Fee")
    fee_type = fields.Many2One('product.product',"Fee type")
    invoice_action = fields.Function(fields.Selection([
        ('standard', 'Standard invoice'),
        ('dn_cn', 'DN/CN'),
        ('already_invoiced', 'Already invoiced'),
        ], "Planned action"), 'on_change_with_invoice_action')
    to_invoice = fields.Boolean("To invoice", states={
        'readonly': Eval('invoice_action') == 'already_invoiced',
        }, depends=['invoice_action'])
    fee_unit = fields.Many2One('product.uom',"Unit",readonly=True)
    fee_quantity = fields.Numeric("Qt",digits=(1,5),readonly=True)
    fee_price = fields.Numeric("Price",digits=(1,4),readonly=True)
    fee_amount = fields.Numeric("Amount",digits=(1,2),readonly=True)
    fee_landed_cost = fields.Boolean("To inventory",readonly=True)

    @fields.depends('fee', '_parent_lfs.action')
    def on_change_with_invoice_action(self, name=None):
        action = self.lfs.action if self.lfs else None
        return _fee_invoice_action(self.fee, action)

class LotInvoicingInv(ModelView):
    "Fees"
    __name__ = "lot.invoicing.inv"

    lps = fields.Many2One('lot.invoice.start',"Invoicing")
    inv = fields.Many2One('account.invoice.line',"Invoice lines")
    inv_amount = fields.Numeric("Av.amount",digits=(1,2),readonly=True)
    inv_amount_to_use = fields.Numeric("Amount to use",digits=(1,2))

class LotInvoicingLot(ModelView):
    "Lots"
    __name__ = "lot.invoicing.lot"

    lis = fields.Many2One('lot.invoice.start',"Invoicing")
    lot = fields.Many2One('lot.lot',"Lot")
    lot_name = fields.Char("Name",readonly=True)
    lot_state = fields.Many2One('lot.qt.type',"State")
    lot_quantity = fields.Numeric("Qt",digits='unit',readonly=True)
    sale_invoice_padding = fields.Numeric(
        "Padding", digits='lot_unit', readonly=True)
    lot_diff_quantity = fields.Numeric("Diff. qt",digits='unit',readonly=True)
    lot_unit = fields.Many2One('product.uom',"Unit",readonly=True)
    lot_price = fields.Numeric("Price",digits='unit',readonly=True)
    lot_diff_price = fields.Numeric("Diff. price",digits='unit',readonly=True)
    lot_currency = fields.Char("Curr")
    lot_amount = fields.Numeric("Amt",digits='unit',readonly=True)
    lot_diff_amount = fields.Numeric("Diff amt",digits='unit',readonly=True)
    lot_pur_inv_state = fields.Selection([
        (None, ''),
        ('prov', 'Prov'),
        ('invoiced', 'Final')
    ], 'Pur. inv')
    lot_sale_inv_state = fields.Selection([
        (None, ''),
        ('prov', 'Prov'),
        ('invoiced', 'Final')
    ], 'Sale inv')

    @classmethod
    def view_attributes(cls):
        return super().view_attributes() + [
            ('/tree/field[@name="lot_diff_price"]', 'visual',
                If(Eval('lot_diff_price') & (Eval('lot_diff_price', 0) < 0),'danger','success')),
            ('/tree/field[@name="lot_diff_quantity"]', 'visual',
                If(Eval('lot_diff_quantity') & (Eval('lot_diff_quantity', 0) < 0),'danger','success')),
            ]

class LotFCR(ModelSQL, ModelView):
    "Lot FCR"
    __name__ = 'lot.fcr'
    _rec_name = 'lot_name'

    lot_tare = fields.Numeric("Tare")
    lot_grade = fields.Char("Grade")
    lot_staple = fields.Char("Staple")
    lot_mic = fields.Char("Mic")
    lot_gpt = fields.Char("Gpt")
    lot_uhm = fields.Char("Uhm")
    lot_color = fields.Char("Color")
    lot_counterparty = fields.Many2One('party.party','Supplier')
    lot_offer_date = fields.Date("Offer date")
    lot_offer =fields.Char("Reference")
    lot_product = fields.Many2One('product.product','Product')
    lot_origin = fields.Many2One('country.country','Origin')
    lot_name = fields.Char("Lot")
    lot_qt = fields.Integer("Qt")
    lot_price = fields.Numeric("Price",digits=(1,4))
    lot_amount = fields.Numeric("Amount",digits=(1,2))
    lot_premium = fields.Numeric("Prem/Disc", digits=(1,2))
    lot_quantity = fields.Numeric("Net Qt",digits=(1,5),required=True)
    lot_unit = fields.Many2One('product.uom', "Unit",required=True)
    lot_unit_line = fields.Many2One('product.uom', "Base unit",required=True)
    lot_gross_quantity = fields.Numeric("Gross Qt",digits=(1,5))
    lot_linked = fields.Many2One('lot.lot', "Linked lot")
    lot_create = fields.Boolean("Create")

class LotWeighing(Wizard):
    "Weighing"
    __name__ = "lot.weighing"

    start = StateTransition()

    w = StateView(
        'lot.weighing.start',
        'purchase_trade.weighing_view_form', [
        Button("Cancel", 'end', 'tryton-cancel'),
        Button("Do", 'weighing', 'tryton-ok', default=True),
        ])
    
    weighing = StateTransition()

    def transition_start(self):
        return 'w'
        
    def default_w(self, fields):
        lot_p = []
        val = {}
        Lot = Pool().get('lot.lot')
        context = Transaction().context
        ids = context.get('active_ids')
        for i in ids:
            if i > 10000000:
                raise UserError("Trying to do weighing on open quantity!")
            val = {}
            lot = Lot(i)
            val['lot'] = lot.id
            val['lot_name'] = lot.lot_name
            if lot.lot_shipment_in:
                val['lot_shipment_in'] = lot.lot_shipment_in.id
            if lot.lot_shipment_internal:
                val['lot_shipment_internal'] = lot.lot_shipment_internal.id
            if lot.lot_shipment_out:
                val['lot_shipment_out'] = lot.lot_shipment_out.id
            val['lot_product'] = lot.lot_product.id
            val['lot_qt'] = lot.lot_qt
            val['lot_quantity'] = lot.lot_quantity
            val['lot_gross_quantity'] = lot.lot_gross_quantity
            val['sale_invoice_padding'] = lot.sale_invoice_padding
            val['lot_unit'] = lot.lot_unit.id
            val['lot_unit_line'] = lot.lot_unit_line.id
            lot_p.append(val)
        return {
            'lot_p': lot_p,
            }

    def transition_weighing(self):
        Warning = Pool().get('res.user.warning')  
        FeeLots = Pool().get('fee.lots') 
        Lot = Pool().get('lot.lot')
        LotHist = Pool().get('lot.qt.hist') 
        affected_lines = []
        with Lot.skip_quantity_consistency():
            for l in self.w.lot_p:
                quantity = l.lot.get_current_quantity_converted()
                lhs = LotHist.search([('lot',"=",l.lot.id),('quantity_type','=',self.w.lot_state.id)])
                if lhs:
                    lh = lhs[0]
                else:
                    lh = LotHist()
                lh.lot = l.lot
                lh.quantity_type = self.w.lot_state
                lh.quantity = round(l.lot_quantity_new,5)
                lh.gross_quantity = round(l.lot_gross_quantity_new,5)
                LotHist.save([lh])
                l.lot.lot_qt = l.lot_qt

                if self.w.lot_update_state :
                    l.lot.lot_state = self.w.lot_state
                Lot.save([l.lot])
                diff = round(Decimal(l.lot.get_current_quantity_converted() - quantity),5)
                if diff != 0 :
                    #need to update virtual part
                    l.lot.updateVirtualPart(-diff,l.lot.lot_shipment_origin,l.lot.getVlot_s())
                if l.lot.line:
                    affected_lines.append(l.lot.line)
                if l.lot.sale_line:
                    affected_lines.append(l.lot.sale_line)
                #adjuts fee ordered with new quantity
                fees = FeeLots.search(['lot','=',l.lot.id])
                for f in fees:
                    f.fee.sync_quantity_from_lots()
                    f.fee.adjust_purchase_values()

        Lot.assert_lines_quantity_consistency(affected_lines)
        return 'end'
    
    def end(self):
        return 'reload'

class LotWeighingStart(ModelView):
    "Add weighing"
    __name__ = "lot.weighing.start"
    lot_p = fields.One2Many('lot.weighing.lot','lws',"Lots")
    lot_state = fields.Many2One('lot.qt.type',"Weighing type",required=True)
    lot_update_state = fields.Boolean("Update lot state to this weighing")

    @classmethod
    def default_lot_update_state(cls):
        return True

class LotWeighingLot(ModelView):
    "Lots"
    __name__ = "lot.weighing.lot"

    lws = fields.Many2One('lot.weighing.start',"Weighing")
    lot = fields.Many2One('lot.lot',"Lot")
    lot_name = fields.Char("Name",readonly=True)
    lot_shipment_in = fields.Many2One('stock.shipment.in',"Shipment In")
    lot_shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal")
    lot_shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out")
    lot_product = fields.Many2One('product.product',"Product",readonly=True)
    lot_qt = fields.Integer("Qt")
    lot_quantity = fields.Numeric("Net weight",digits=(1,5),readonly=True)
    lot_gross_quantity = fields.Numeric("Gross weight",digits=(1,5),readonly=True)
    sale_invoice_padding = fields.Numeric(
        "Padding", digits='lot_unit_line', readonly=True)
    lot_unit = fields.Many2One('product.uom',"Unit",readonly=True)
    lot_unit_line = fields.Many2One('product.uom',"Unit",readonly=True)
    lot_quantity_new = fields.Numeric("New net weight",digits=(1,5))
    lot_gross_quantity_new = fields.Numeric("New gross weight",digits=(1,5))
    lot_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_shipment_origin(self, name):
        if self.lot_shipment_in:
            return 'stock.shipment.in,' + str(self.lot_shipment_in.id)
        elif self.lot_shipment_out:
            return 'stock.shipment.out,' + str(self.lot_shipment_out.id)
        elif self.lot_shipment_internal:
            return 'stock.shipment.internal,' + str(self.lot_shipment_internal.id)
        return None

class CreateContracts(Wizard):
    "Create Contracts"
    __name__ = "create.contracts"

    start = StateTransition()

    ct = StateView(
        'contracts.start',
        'purchase_trade.contracts_view_form', [
        Button("Cancel", 'end', 'tryton-cancel'),
        Button("Create", 'creating', 'tryton-ok', default=True),
        ])
    
    creating = StateTransition()

    #matching = StateAction('purchase_trade.matching_unit')

    def transition_start(self):
        return 'ct'
        
    def default_ct(self, fields):
        LotQt = Pool().get('lot.qt')
        Lot = Pool().get('lot.lot')
        context = Transaction().context
        ids = context.get('active_ids')
        unit = None
        product = None
        sh_in = None
        sh_int = None
        sh_out = None
        lot = None
        qt = Decimal(0)
        type = None
        shipment_in_values = set()
        shipment_internal_values = set()
        shipment_out_values = set()
        for i in ids:
            if i < 10000000:
                raise UserError("You must create contract from an open quantity !")
            l = LotQt(i - 10000000)
            ll = Lot(l.lot_p if l.lot_p else l.lot_s)
            current_type = "Sale" if l.lot_p else "Purchase"
            if type and current_type != type:
                raise UserError("You must select open quantities from the same side.")
            type = current_type
            if product and ll.lot_product.id != product:
                raise UserError("You must select open quantities with the same product.")
            if unit and l.lot_unit.id != unit:
                raise UserError("You must select open quantities with the same unit.")
            unit = l.lot_unit.id
            qt += abs(Decimal(str(l.lot_quantity or 0)))
            product = ll.lot_product.id
            shipment_in_values.add(l.lot_shipment_in.id if l.lot_shipment_in else None)
            shipment_internal_values.add(
                l.lot_shipment_internal.id if l.lot_shipment_internal else None)
            shipment_out_values.add(l.lot_shipment_out.id if l.lot_shipment_out else None)
            if lot is None:
                lot = ll.id

        if len(shipment_in_values) == 1:
            sh_in = next(iter(shipment_in_values))
        if len(shipment_internal_values) == 1:
            sh_int = next(iter(shipment_internal_values))
        if len(shipment_out_values) == 1:
            sh_out = next(iter(shipment_out_values))
            
        return {
            'quantity': qt,
            'unit': unit,
            'product': product,
            'shipment_in': sh_in,
            'shipment_internal': sh_int,
            'shipment_out': sh_out,
            'lot': lot,
            'type': type,
            }

    def transition_creating(self):
        ContractFactory.create_contracts(
            self.ct.contracts,
            type_=self.ct.type,
            ct=self.ct,
        )
        return 'end'
    
    def end(self):
        return 'reload'

class ContractsStart(ModelView):
    "Create Contracts"
    __name__ = "contracts.start"
    type = fields.Char("Type of contract to create",readonly=True)
    matched = fields.Boolean("Matched")
    product = fields.Many2One('product.product',"Product",readonly=True)
    unit = fields.Many2One('product.uom',"Unit",readonly=True)
    quantity = fields.Numeric("Current Qt",digits='unit',readonly=True)
    shipment_in = fields.Many2One('stock.shipment.in',"Shipment In",readonly=True,states={'invisible': ~Eval('shipment_in')})
    shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal",readonly=True,states={'invisible': ~Eval('shipment_internal')})
    shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out",readonly=True,states={'invisible': ~Eval('shipment_out')})
    # shipment_origin = fields.Reference(
    #         selection=[
    #             ("stock.shipment.in", "In"),
    #             ("stock.shipment.out", "Out"),
    #             ("stock.shipment.internal", "Internal"),
    #         ],
    #         string="Shipment",
    #     )
    lot = fields.Many2One('lot.lot',"Lot",readonly=True)#,states={'invisible': Eval('lqt')})
    contracts = fields.One2Many('contract.detail','cd',"Contracts")

    @classmethod
    def default_matched(cls):
        return True

class CreateMirror(Wizard):
    "Create Mirror Contract"
    __name__ = "create.mirror"

    start = StateTransition()

    mirror = StateView(
        'mirror.start',
        'purchase_trade.mirror_start_view_form', [
        Button("Cancel", 'end', 'tryton-cancel'),
        Button("Create", 'creating', 'tryton-ok', default=True),
        ])

    creating = StateTransition()

    def transition_start(self):
        return 'mirror'

    def default_mirror(self, fields):
        return MirrorStart.default_from_context()

    def transition_creating(self):
        ContractFactory.create_mirror_contract(
            self.mirror,
            self.mirror.lines,
            type_=self.mirror.type,
        )
        return 'end'

    def end(self):
        return 'reload'

class MirrorStart(ModelView):
    "Create Mirror Contract"
    __name__ = "mirror.start"

    type = fields.Char("Type of contract to create", readonly=True)
    category = fields.Integer("Category")
    party = fields.Many2One(
        'party.party', "Party", required=True,
        domain=[('categories.parent', 'child_of', Eval('category'))],
        depends=['category'])
    currency = fields.Many2One('currency.currency', "Currency", required=True)
    incoterm = fields.Many2One('incoterm.incoterm', "Incoterm", required=True)
    payment_term = fields.Many2One(
        'account.invoice.payment_term', "Payment Term", required=True)
    reference = fields.Char("Reference")
    tol_min = fields.Numeric("Tol - in %")
    tol_max = fields.Numeric("Tol + in %")
    crop = fields.Many2One(
        'purchase.crop', "Crop",
        states={'invisible': Eval('company_visible')})
    from_location = fields.Many2One(
        'stock.location', "From location",
        domain=[('type', 'in', ['storage', 'supplier'])])
    to_location = fields.Many2One(
        'stock.location', "To location",
        domain=[('type', 'in', ['storage', 'customer'])])
    company_visible = fields.Boolean("Visible")
    selected_source_count = fields.Integer("Selected sources", readonly=True)
    selected_source_ids = fields.Char("Selected source ids", readonly=True)
    lines = fields.One2Many('mirror.line', 'mirror', "Lines")

    @classmethod
    def default_company_visible(cls):
        return ContractDetail.default_company_visible()

    @classmethod
    def default_from_context(cls):
        sources, type_ = cls._selected_sources()
        base_contract = cls._base_contract(sources[0], type_)
        from_location, to_location = ContractFactory._get_mirror_locations(
            base_contract, type_)
        return {
            'type': type_,
            'category': 1 if type_ == 'Sale' else 2,
            'currency': getattr(getattr(base_contract, 'currency', None), 'id', None),
            'incoterm': getattr(getattr(base_contract, 'incoterm', None), 'id', None),
            'payment_term': getattr(
                getattr(base_contract, 'payment_term', None), 'id', None),
            'reference': getattr(base_contract, 'reference', None),
            'tol_min': getattr(base_contract, 'tol_min', None),
            'tol_max': getattr(base_contract, 'tol_max', None),
            'crop': getattr(getattr(base_contract, 'crop', None), 'id', None),
            'from_location': getattr(from_location, 'id', None),
            'to_location': getattr(to_location, 'id', None),
            'selected_source_count': len(sources),
            'selected_source_ids': ','.join(
                str(source['lqt'].id) for source in sources),
            'lines': [cls._line_defaults(source, type_) for source in sources],
            }

    @staticmethod
    def _base_contract(source, type_):
        lot = source['lot']
        if type_ == 'Purchase':
            return lot.sale_line.sale
        return lot.line.purchase

    @classmethod
    def _selected_sources(cls):
        LotQt = Pool().get('lot.qt')
        context = Transaction().context
        ids = context.get('active_ids') or []
        if not ids:
            raise UserError("You must select open quantities to mirror.")
        sources = []
        type_ = None
        for record_id in ids:
            if record_id < 10000000:
                raise UserError("You must create mirror from open quantities.")
            lqt = LotQt(record_id - 10000000)
            if bool(lqt.lot_p) == bool(lqt.lot_s):
                raise UserError(
                    "You must select unmatched open quantities from one side.")
            lot = lqt.lot_p or lqt.lot_s
            if getattr(lot, 'lot_type', None) != 'virtual':
                raise UserError("You must create mirror from open quantities.")
            current_type = "Sale" if lqt.lot_p else "Purchase"
            if type_ and current_type != type_:
                raise UserError(
                    "You must select open quantities from the same side.")
            type_ = current_type
            sources.append({
                'lqt': lqt,
                'lot': lot,
                'trade_line': lot.line if lqt.lot_p else lot.sale_line,
                })
        return sources, type_

    @staticmethod
    def _line_defaults(source, type_):
        lqt = source['lqt']
        line = source['trade_line']
        lot = source['lot']
        currency_unit = None
        if getattr(line, 'enable_linked_currency', False):
            currency_unit = "0_%s" % line.linked_unit.id
        else:
            contract = line.purchase if type_ == 'Sale' else line.sale
            currency = getattr(contract, 'currency', None)
            unit = getattr(line, 'unit', None)
            if currency and unit:
                currency_unit = "%s_%s" % (currency.id, unit.id)
        return {
            'source_lqt': lqt.id,
            'source_lot': lot.id,
            'product': line.product.id,
            'unit': line.unit.id,
            'quantity': abs(Decimal(str(lqt.lot_quantity or 0))),
            'price': getattr(line, 'linked_price', None)
                if getattr(line, 'enable_linked_currency', False)
                else getattr(line, 'unit_price', None),
            'price_type': getattr(line, 'price_type', None) or 'priced',
            'premium': getattr(line, 'premium', None) or Decimal(0),
            'currency_unit': currency_unit,
            'del_period': getattr(getattr(line, 'del_period', None), 'id', None),
            'from_del': getattr(line, 'from_del', None),
            'to_del': getattr(line, 'to_del', None),
            }

class MirrorLine(ModelView):
    "Mirror Contract Line"
    __name__ = "mirror.line"

    mirror = fields.Many2One('mirror.start', "Mirror")
    source_lqt = fields.Many2One('lot.qt', "Open quantity", readonly=True)
    source_lot = fields.Many2One('lot.lot', "Source lot", readonly=True)
    product = fields.Many2One('product.product', "Product", required=True)
    quantity = fields.Numeric("Quantity", digits=(1, 5), required=True)
    unit = fields.Many2One('product.uom', "Unit", required=True)
    price = fields.Numeric("Price", required=True, digits=(1, 4),
        states={'invisible': Eval('price_type') != 'priced'})
    price_type = fields.Selection([
        ('cash', 'Cash Price'),
        ('priced', 'Priced'),
        ('basis', 'Basis'),
    ], 'Price type', required=True)
    premium = fields.Numeric("Prem/Disc", digits=(1, 4))
    currency_unit = fields.Selection(
        'get_currency_unit', string="Curr/Unit")
    del_period = fields.Many2One('product.month', "Shipment Period")
    from_del = fields.Date("From")
    to_del = fields.Date("To")

    @staticmethod
    def get_currency_unit():
        return ContractDetail.get_currency_unit()
    
class ContractDetail(ModelView):

    "Contract Detail"
    __name__ = "contract.detail"

    category = fields.Integer("Category")
    cd = fields.Many2One('contracts.start',"Contracts")
    party = fields.Many2One('party.party',"Party", required=True,domain=[('categories.parent', 'child_of', Eval('category'))],depends=['category'])
    currency = fields.Many2One('currency.currency',"Currency", required=True)
    incoterm = fields.Many2One('incoterm.incoterm',"Incoterm", required=True)
    quantity = fields.Numeric("Quantity",digits=(1,5), required=True)
    unit = fields.Many2One('product.uom',"Unit", required=True)
    qt_unit = fields.Many2One('product.uom',"Unit")
    tol_min = fields.Numeric("Tol - in %")
    tol_max = fields.Numeric("Tol + in %")
    crop = fields.Many2One(
        'purchase.crop', "Crop",
        states={'invisible': Eval('company_visible')})
    del_period = fields.Many2One('product.month',"Shipment Period")
    from_del = fields.Date("From")
    to_del = fields.Date("To")
    price = fields.Numeric("Price", required=True,digits=(1,4),states={'invisible': Eval('price_type') != 'priced'})
    price_type = price_type = fields.Selection([
        ('cash', 'Cash Price'),
        ('priced', 'Priced'),
        ('basis', 'Basis'),
    ], 'Price type', required=True)
    currency_unit = fields.Selection('get_currency_unit',string="Curr/Unit")
    reference = fields.Char("Reference")
    from_location = fields.Many2One(
        'stock.location', "From location",
        domain=[('type', 'in', ['storage', 'supplier'])])
    to_location = fields.Many2One(
        'stock.location', "To location",
        domain=[('type', 'in', ['storage', 'customer'])])
    payment_term = fields.Many2One('account.invoice.payment_term',"Payment Term", required=True)
    company_visible = fields.Boolean("Visible")

    @classmethod
    def default_category(cls):
        lqt = cls._get_lqt_from_context()
        if lqt and lqt.lot_p:
            return 1
        else:
            return 2

    @staticmethod
    def get_currency_unit():
        Currency = Pool().get('currency.currency')
        Uom = Pool().get('product.uom')

        result = []
        currencies = Currency.search([('concatenate','=',True)])
        units = Uom.search([('concatenate','=',True)])
        result.append(("0_5","USC/lb"))
        result.append(("0_37","USC/mt"))
        for c in currencies:
            for u in units:
                key = "%s_%s" % (c.id, u.id)
                value = "%s / %s" % (c.name, u.symbol or u.name)
                result.append((key, value))
        return result
    
    @classmethod
    def default_price_type(cls):
        return 'priced'

    @classmethod
    def default_company_visible(cls):
        Company = Pool().get('company.company')
        company_id = Transaction().context.get('company')
        if not company_id:
            return False
        company = Company(company_id)
        return bool(company.party and company.party.name == 'MELYA')
    
    @classmethod
    def _get_lqt_from_context(cls):
        ctx = Transaction().context
        active_id = ctx.get('active_id') or next(iter(ctx.get('active_ids', [])), None)
        if not active_id:
            return None
        LotQt = Pool().get('lot.qt')
        try:
            return LotQt(active_id - 10000000)
        except Exception:
            return None
    
    @classmethod
    def default_reference(cls):
        lqt = cls._get_lqt_from_context()
        if lqt and lqt.lot_p and getattr(lqt.lot_p.line.purchase, 'reference', None):
            return lqt.lot_p.line.purchase.reference
        if lqt and lqt.lot_s and getattr(lqt.lot_s.sale_line.sale, 'reference', None):
            return lqt.lot_s.sale_line.sale.reference

    @classmethod
    def default_crop(cls):
        lqt = cls._get_lqt_from_context()
        if lqt and lqt.lot_p and getattr(lqt.lot_p.line.purchase, 'crop', None):
            return lqt.lot_p.line.purchase.crop.id
        if lqt and lqt.lot_s and getattr(lqt.lot_s.sale_line.sale, 'crop', None):
            return lqt.lot_s.sale_line.sale.crop.id

    @classmethod
    def default_currency(cls):
        lqt = cls._get_lqt_from_context()
        if not lqt:
            return
        line = lqt.lot_p.line if lqt.lot_p else lqt.lot_s.sale_line
        if getattr(line, 'enable_linked_currency', False):
            return line.linked_currency.id
        return getattr(line.purchase.currency if lqt.lot_p else line.sale.currency, 'id', None)

    @classmethod
    def default_unit(cls):
        lqt = cls._get_lqt_from_context()
        if not lqt:
            return
        line = lqt.lot_p.line if lqt.lot_p else lqt.lot_s.sale_line
        if getattr(line, 'enable_linked_currency', False):
            return line.linked_unit.id
        return getattr(line.unit, 'id', None)

    @classmethod
    def default_currency_unit(cls):
        lqt = cls._get_lqt_from_context()
        if not lqt:
            return
        line = lqt.lot_p.line if lqt.lot_p else lqt.lot_s.sale_line
        if getattr(line, 'enable_linked_currency', False):
            return "0_%s" % line.linked_unit.id
        contract = line.purchase if lqt.lot_p else line.sale
        currency = getattr(contract, 'currency', None)
        unit = getattr(line, 'unit', None)
        if currency and unit:
            return "%s_%s" % (currency.id, unit.id)

    @classmethod
    def default_tol_min(cls):
        lqt = cls._get_lqt_from_context()
        return getattr(lqt.lot_p.line.purchase if lqt.lot_p else lqt.lot_s.sale_line.sale, 'tol_min', None) if lqt else None

    @classmethod
    def default_tol_max(cls):
        lqt = cls._get_lqt_from_context()
        return getattr(lqt.lot_p.line.purchase if lqt.lot_p else lqt.lot_s.sale_line.sale, 'tol_max', None) if lqt else None

    @fields.depends('party', '_parent_party.tol_min', '_parent_party.tol_max')
    def on_change_party(self):
        if self.party:
            self.tol_min = self.party.tol_min
            self.tol_max = self.party.tol_max

    @classmethod
    def default_del_period(cls):
        lqt = cls._get_lqt_from_context()
        if lqt and getattr(lqt.lot_p.line.purchase if lqt.lot_p else lqt.lot_s.sale_line.sale, 'del_period', None):
            return lqt.lot_p.line.purchase.del_period.id if lqt.lot_p else lqt.lot_s.sale_line.sale.del_period.id

    @classmethod
    def _get_base_contract(cls):
        lqt = cls._get_lqt_from_context()
        if lqt and lqt.lot_p:
            return lqt.lot_p.line.purchase, 'Sale'
        if lqt and lqt.lot_s:
            return lqt.lot_s.sale_line.sale, 'Purchase'
        return None, None

    @classmethod
    def default_from_location(cls):
        base, type_ = cls._get_base_contract()
        from_location, _ = ContractFactory._get_mirror_locations(base, type_)
        return getattr(from_location, 'id', None)

    @classmethod
    def default_to_location(cls):
        base, type_ = cls._get_base_contract()
        _, to_location = ContractFactory._get_mirror_locations(base, type_)
        return getattr(to_location, 'id', None)

    @fields.depends('del_period')
    def on_change_del_period(self):
        if self.del_period:
            self.from_del = self.del_period.beg_date
            self.to_del = self.del_period.end_date
    
