# 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, Workflow
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id, If, PYSONEncoder
from trytond.model import (ModelSQL, ModelView)
from trytond.i18n import gettext
from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction
from trytond.transaction import Transaction, inactive_records
from decimal import getcontext, Decimal, ROUND_CEILING, ROUND_FLOOR, ROUND_HALF_UP
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
from sql.conditionals import Case
from sql import Column, Literal
from sql.functions import CurrentTimestamp, DateTrunc
import datetime
import logging
import json
from collections import defaultdict
from trytond.exceptions import UserWarning, UserError
from trytond.modules.purchase_trade.numbers_to_words import quantity_to_words, amount_to_currency_words, format_date_en
from trytond.modules.purchase_trade.company_defaults import (
    add_itsa_analytic_dimension_defaults,
    default_itsa_analytic_dimension_assignments, default_itsa_currency,
    default_itsa_unit, is_itsa_company, record_id)

logger = logging.getLogger(__name__)

VALTYPE = [
    ('priced', 'Price'),
    ('fee', 'Fee'),
    ('market', 'Market'),
    ('derivative', 'Derivative'),
]

class ContractDocumentType(metaclass=PoolMeta):
    "Contract - Document Type"
    __name__ = 'contract.document.type'
    # lc_out = fields.Many2One('lc.letter.outgoing', 'LC out')
    # lc_in = fields.Many2One('lc.letter.incoming', 'LC in')
    sale = fields.Many2One('sale.sale', "Sale")

class AnalyticDimensionAssignment(metaclass=PoolMeta):
    'Analytic Dimension Assignment'
    __name__ = 'analytic.dimension.assignment'
    sale = fields.Many2One('sale.sale', "Sale")

class Estimated(metaclass=PoolMeta):
    "Estimated date"
    __name__ = 'pricing.estimated'

    sale_line = fields.Many2One('sale.line',"Line")

class FeeLots(metaclass=PoolMeta):

    "Fee lots"
    __name__ = 'fee.lots'

    sale_line = fields.Many2One('sale.line',"Line", ondelete='CASCADE')

class Backtoback(metaclass=PoolMeta):
    'Back To Back'
    __name__ = 'back.to.back'

    sale = fields.One2Many('sale.sale','btb', "Sale")

class OpenPosition(metaclass=PoolMeta):
    "Open position"
    __name__ = 'open.position'
    sale = fields.Many2One('sale.sale',"Sale")
    sale_line = fields.Many2One('sale.line',"Sale Line")
    client = fields.Many2One('party.party',"Client")

class SaleStrategy(ModelSQL):
    "Sale - Document Type"
    __name__ = 'sale.strategy'
    sale_line = fields.Many2One('sale.line', 'Sale Line')
    strategy = fields.Many2One('mtm.strategy', "Strategy")

class Component(metaclass=PoolMeta):
    "Component"
    __name__ = 'pricing.component'

    sale_line = fields.Many2One('sale.line',"Line")
    quota_sale = fields.Function(fields.Numeric("Quota",digits='unit'),'get_quota_sale')
    unit_sale = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit_sale')

    def getDelMonthDateSale(self):
        PM = Pool().get('product.month')
        if self.sale_line and hasattr(self.sale_line, 'del_period') and self.sale_line.del_period:
            pm = PM(self.sale_line.del_period)
            if pm:
                logger.info("DELMONTHDATE:%s",pm.beg_date)
                return pm.beg_date

    def getEstimatedTriggerSale(self,t):
        logger.info("GETTRIGGER:%s",t)
        if t == 'delmonth':
            return self.getDelMonthDateSale()
        if self.sale_line and hasattr(self.sale_line, 'get_date'):
            trigger_date = self.sale_line.get_date(t)
            if trigger_date:
                return trigger_date
        PE = Pool().get('pricing.estimated')
        Date = Pool().get('ir.date')
        pe = PE.search([('sale_line','=',self.sale_line),('trigger','=',t)])
        if pe:
            return pe[0].estimated_date
        else:
            return Date.today()

    def get_unit_sale(self, name):
        if self.sale_line:
            return self.sale_line.unit
        
    def get_quota_sale(self, name):
        if self.sale_line:
            quantity = getattr(self.sale_line, 'quantity_theorical', None)
            if quantity is None:
                quantity = getattr(self.sale_line, 'quantity', None)
            if quantity is not None:
                nbdays = self.nbdays if self.nbdays and self.nbdays > 0 else 1
                return round(Decimal(quantity) / nbdays, 4)

class Pricing(metaclass=PoolMeta):
    "Pricing"
    __name__ = 'pricing.pricing'

    sale_line = fields.Many2One('sale.line',"Lines")
    unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit_sale')

    def get_unit_sale(self,name):
        if self.sale_line:
            return self.sale_line.unit

    def get_eod_price_sale(self):
        return self._weighted_average_price(
            self.fixed_qt,
            self.fixed_qt_price,
            self.unfixed_qt,
            self.unfixed_qt_price,
        )

class Summary(ModelSQL,ModelView):
    "Pricing summary"
    __name__ = 'sale.pricing.summary'

    sale_line = fields.Many2One('sale.line',"Lines")
    price_component = fields.Many2One('pricing.component',"Component")
    quantity = fields.Numeric("Qt",digits=(1,4))
    fixed_qt = fields.Numeric("Fixed qt",digits=(1,4))
    unfixed_qt = fields.Numeric("Unfixed qt",digits=(1,4))
    price = fields.Numeric("Price",digits=(1,4))
    progress = fields.Float("Fix. progress")
    ratio = fields.Numeric("Ratio")

    def get_name(self):
        if self.price_component:
            return self.price_component.get_rec_name()
        return ""

    def get_last_price(self):
        Date = Pool().get('ir.date')
        if self.price_component:
            pc = Pool().get('pricing.component')(self.price_component)
            if pc.price_index:
                PI = Pool().get('price.price')
                pi = PI(pc.price_index)
                return pi.get_price(Date.today(),self.sale_line.unit,self.sale_line.sale.currency,True)
            
    @classmethod
    def table_query(cls):
        SalePricing = Pool().get('pricing.pricing')
        sp = SalePricing.__table__()
        SaleComponent = Pool().get('pricing.component')
        sc = SaleComponent.__table__()
        #wh = Literal(True)
        context = Transaction().context
        group_pnl = context.get('group_pnl')

        if group_pnl:
            return None
        
        query = sp.join(sc,'LEFT',condition=sp.price_component == sc.id).select(
            Literal(0).as_('create_uid'),
            CurrentTimestamp().as_('create_date'),
            Literal(None).as_('write_uid'),
            Literal(None).as_('write_date'),
            Max(sp.id).as_('id'),
            sp.sale_line.as_('sale_line'),
            sp.price_component.as_('price_component'),
            Max(sp.fixed_qt+sp.unfixed_qt).as_('quantity'),
            Max(sp.fixed_qt).as_('fixed_qt'),
            (Min(sp.unfixed_qt)).as_('unfixed_qt'),
            Max(Case((sp.last, sp.eod_price),else_=0)).as_('price'),
            (Max(sp.fixed_qt)/Max(sp.fixed_qt+sp.unfixed_qt)).as_('progress'),
            Max(sc.ratio).as_('ratio'),
            #where=wh,
            group_by=[sp.sale_line,sp.price_component])

        return query    

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

    sale_line = fields.Many2One('sale.line',"Sale",ondelete='CASCADE')
    lot_quantity_sale = fields.Function(fields.Numeric("Net weight",digits='lot_unit'),'get_qt')
    lot_gross_quantity_sale = fields.Function(fields.Numeric("Gross weight",digits='lot_unit'),'get_gross_qt')

    def get_qt(self, name):
        quantity = self.lot_quantity
        if self.lot_hist:
            for h in self.lot_hist:
                if h.quantity_type.id == 3:
                    quantity = h.quantity
        return quantity

    def get_gross_qt(self, name):
        quantity = self.lot_quantity
        if self.lot_hist:
            for h in self.lot_hist:
                if h.quantity_type.id == 3:
                    quantity = h.quantity
        return quantity
    
    def getClient(self):
        if self.sale_line:
            return Pool().get('sale.sale')(self.sale_line.sale).party.id

    def getSale(self):
        if self.sale_line:
            return self.sale_line.sale.id

class Sale(metaclass=PoolMeta):
    __name__ = 'sale.sale'

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._transitions.discard(('confirmed', 'processing'))
        cls.lines.states['readonly'] = (
            (Eval('state') != 'draft')
            & ((Eval('state') != 'quotation')
                | ~Eval('allow_modification_after_validation')))
        cls._buttons['process'] = {
            'invisible': True,
            'depends': ['state'],
            }

    @classmethod
    @ModelView.button
    @Workflow.transition('confirmed')
    def confirm(cls, sales):
        cls.set_sale_date(sales)
        cls.store_cache(sales)

    @classmethod
    @ModelView.button
    def process(cls, sales):
        sales = [s for s in sales if s.state != 'confirmed']
        if sales:
            super().process(sales)

    @classmethod
    def create(cls, vlist):
        for values in vlist:
            add_itsa_analytic_dimension_defaults(values)
        return super().create(vlist)

    @classmethod
    def default_analytic_dimensions(cls):
        default = getattr(super(), 'default_analytic_dimensions', None)
        values = default() if default else []
        return values or default_itsa_analytic_dimension_assignments()

    btb = fields.Many2One('back.to.back',"Back to back")
    charter_conditions = fields.One2Many(
        'charter.condition', 'sale', "Charter Conditions")
    contract_template = fields.Many2One(
        'contract.template', "Contract Template",
        domain=[
            ('direction', 'in', ['both', 'sale']),
            ('active', '=', True),
        ])
    contract_clauses = fields.One2Many(
        'contract.clause.selection', 'sale', "Contract Clauses")
    bank_accounts = fields.Function(
        fields.Many2Many('bank.account', None, None, "Bank Accounts"),
        'on_change_with_bank_accounts')
    bank_account = fields.Many2One(
        'bank.account', "Client Bank Account",
        domain=[('id', 'in', Eval('bank_accounts', []))],
        depends=['bank_accounts'])
    our_bank_account = fields.Many2One(
        'bank.account', "Our Bank Account")
    from_location = fields.Many2One('stock.location', 'From location', required=True,domain=[('type', "!=", 'customer')])
    to_location = fields.Many2One('stock.location', 'To location', required=True,domain=[('type', "!=", 'supplier')])
    shipment_out = fields.Many2One('stock.shipment.out','Sales')
    #pnl = fields.One2Many('valuation.valuation', 'sale', 'Pnl')
    pnl = fields.One2Many('valuation.valuation.dyn', 'r_sale', 'Pnl',states={'invisible': ~Eval('group_pnl'),})
    pnl_ = fields.One2Many('valuation.valuation.line', 'sale', 'Pnl',states={'invisible': Eval('group_pnl'),}) 
    group_pnl = fields.Boolean("Group Pnl")
    derivatives = fields.One2Many('derivative.derivative', 'sale', 'Derivative')
    #plans = fields.One2Many('workflow.plan','sale',"Execution plans")
    forex = fields.One2Many('forex.cover.physical.sale','contract',"Forex",readonly=True)
    broker = fields.Many2One('party.party',"Broker",domain=[('categories.parent', 'child_of', [4])])
    tol_min = fields.Numeric("Tol - in %", required=True)
    tol_max = fields.Numeric("Tol + in %", required=True)
    tol_min_qt = fields.Numeric("Tol -")
    tol_max_qt = fields.Numeric("Tol +")
    tolerance_used = fields.Function(
        fields.Numeric("Tolerance used"), 'get_tolerance_used')
    tolerance_option = fields.Selection([
        ('', ''),
        ('buyer_option', "Buyer's option"),
        ('seller_option', "Seller's option"),
        ], "Tolerance option")
    tolerance_min = fields.Function(
        fields.Numeric("Tolerance min"), 'get_tolerance_min')
    tolerance_max = fields.Function(
        fields.Numeric("Tolerance max"), 'get_tolerance_max')
    certif = fields.Many2One('purchase.certification',"Certification", required=True,states={'invisible': Eval('company_visible'),})
    wb = fields.Many2One('purchase.weight.basis',"Weight basis", required=True)
    association = fields.Many2One('purchase.association',"Association", required=True,states={'invisible': Eval('company_visible'),})
    crop = fields.Many2One('purchase.crop',"Crop",states={'invisible': Eval('company_visible'),})
    viewer = fields.Function(fields.Text(""),'get_viewer')
    execution_summary_viewer = fields.Function(
        fields.Text(""), 'get_execution_summary_viewer')
    sale_summary_data = fields.Function(
        fields.Text("Sale Summary"), 'get_sale_summary_data')
    doc_template = fields.Many2One('doc.template',"Template")
    required_documents = fields.Many2Many(
        'contract.document.type', 'sale', 'doc_type', 'Required Documents')
    analytic_dimensions = fields.One2Many(
        'analytic.dimension.assignment',
        'sale',
        'Analytic Dimensions'
    )
    allow_modification_after_validation = fields.Function(
        fields.Boolean("Autorise modification after validation"),
        'on_change_with_allow_modification_after_validation')
    trader = fields.Many2One(
        'party.party', "Trader",
        domain=[('categories.name', '=', 'TRADER')])
    operator = fields.Many2One(
        'party.party', "Operator",
        domain=[('categories.name', '=', 'OPERATOR')])
    our_reference = fields.Char("Our Reference")
    company_visible = fields.Function(
        fields.Boolean("Visible"), 'on_change_with_company_visible')
    lc_date = fields.Date("LC date")
    product_origin = fields.Char(
        "Origin", states={'invisible': ~Eval('company_visible'),})

    @fields.depends('company', '_parent_company.party')
    def on_change_with_company_visible(self, name=None):
        return bool(
            self.company and self.company.party
            and self.company.party.name in {'MELYA', 'ITSA'})

    def on_change_with_allow_modification_after_validation(self, name=None):
        Configuration = Pool().get('sale.configuration')
        configurations = Configuration.search([], limit=1)
        return bool(
            configurations
            and configurations[0].allow_modification_after_validation)

    def _get_default_bank_account(self):
        if not self.party or not self.party.bank_accounts:
            return None
        party_bank_accounts = list(self.party.bank_accounts)
        if self.currency:
            for account in party_bank_accounts:
                if account.currency == self.currency:
                    return account
        return party_bank_accounts[0]

    def _get_default_our_bank_account(self):
        if (not self.company or not self.company.party
                or not self.company.party.bank_accounts):
            return None
        company_bank_accounts = list(self.company.party.bank_accounts)
        if self.currency:
            for account in company_bank_accounts:
                if account.currency == self.currency:
                    return account
        return company_bank_accounts[0]

    @fields.depends('party', '_parent_party.bank_accounts')
    def on_change_with_bank_accounts(self, name=None):
        if self.party and self.party.bank_accounts:
            return [account.id for account in self.party.bank_accounts]
        return []

    @fields.depends(
        'company', 'party', 'invoice_party', 'shipment_party', 'warehouse',
        'payment_term', 'lines', 'bank_account', 'our_bank_account',
        '_parent_party.bank_accounts', '_parent_company.party')
    def on_change_party(self):
        super().on_change_party()
        self.bank_account = self._get_default_bank_account()
        self.our_bank_account = self._get_default_our_bank_account()

    @fields.depends(
        'party', 'company', 'currency', '_parent_party.bank_accounts',
        '_parent_company.party')
    def on_change_currency(self):
        self.bank_account = self._get_default_bank_account()
        self.our_bank_account = self._get_default_our_bank_account()

    @fields.depends('company', 'currency', 'our_bank_account',
        '_parent_company.party')
    def on_change_company(self):
        previous_currency = self.currency
        super().on_change_company()
        if (is_itsa_company(self.company)
                and (not previous_currency
                    or record_id(self.currency)
                    == record_id(getattr(self.company, 'currency', None))
                    or record_id(previous_currency)
                    == record_id(getattr(self.company, 'currency', None)))):
            currency = default_itsa_currency()
            if currency:
                self.currency = currency
        self.our_bank_account = self._get_default_our_bank_account()

    @classmethod
    def default_currency(cls, **pattern):
        company = pattern.get('company')
        if company and not hasattr(company, 'party'):
            company = Pool().get('company.company')(company)
        if is_itsa_company(company):
            currency = default_itsa_currency()
            if currency:
                return currency
        default = getattr(super(), 'default_currency', None)
        if default:
            return default(**pattern)
    
    @classmethod
    def default_wb(cls):
        WB = Pool().get('purchase.weight.basis')
        wb = WB.search(['id','>',0])
        if wb:
            return wb[0].id
        
    @classmethod
    def default_certif(cls):
        Certification = Pool().get('purchase.certification')
        certification = Certification.search(['id','>',0])
        if certification:
            return certification[0].id
        
    @classmethod
    def default_association(cls):
        Association = Pool().get('purchase.association')
        association = Association.search(['id','>',0])
        if association:
            return association[0].id
    
    @classmethod
    def default_tol_min(cls):
        return 0
    
    @classmethod
    def default_tol_max(cls):
        return 0

    @classmethod
    def default_tolerance_option(cls):
        return ''

    def _line_quantity_in_unit(self, quantity, from_unit, to_unit):
        if not from_unit or not to_unit or from_unit == to_unit:
            return abs(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 abs(Decimal(str(Uom.compute_qty(
            from_unit, float(quantity or 0), to_unit, True, factor, rate))))

    def _line_lot_quantity_for_tolerance(self, line):
        LotQt = Pool().get('lot.qt')
        quantity = Decimal(0)
        unit = getattr(line, 'unit', None)
        physical_lots = [
            lot for lot in getattr(line, 'lots', None) or []
            if getattr(lot, 'lot_type', None) == 'physic']
        if physical_lots:
            return sum(
                abs(Decimal(str(
                    lot.get_current_quantity_converted(unit=unit) or 0)))
                for lot in physical_lots)
        for lot in getattr(line, 'lots', None) or []:
            if getattr(lot, 'lot_type', None) != 'virtual':
                continue
            for lqt in LotQt.search([('lot_s', '=', lot.id)]):
                quantity += self._line_quantity_in_unit(
                    getattr(lqt, 'lot_quantity', 0),
                    getattr(lqt, 'lot_unit', None),
                    unit)
        return quantity

    def _tolerance_totals(self):
        theoretical = Decimal(0)
        actual = Decimal(0)
        for line in self.lines or []:
            if getattr(line, 'type', None) != 'line':
                continue
            theoretical += abs(Decimal(str(
                getattr(line, 'quantity_theorical', None)
                or getattr(line, 'quantity', None)
                or 0)))
            if hasattr(line, '_tolerance_targeted_quantity'):
                actual += line._tolerance_targeted_quantity()
            else:
                actual += self._line_lot_quantity_for_tolerance(line)
        return theoretical, actual

    def get_tolerance_used(self, name):
        theoretical, actual = self._tolerance_totals()
        if not theoretical:
            return Decimal(0)
        return round(
            ((actual - theoretical) / theoretical) * Decimal(100), 5)

    def get_tolerance_min(self, name):
        return -Decimal(str(self.tol_min or 0))

    def get_tolerance_max(self, name):
        return Decimal(str(self.tol_max or 0))

    @staticmethod
    def _has_matched_physical_lots(sale):
        for line in sale.lines or []:
            for lot in line.lots or []:
                if (
                        getattr(lot, 'lot_type', None) == 'physic'
                        and getattr(lot, 'line', None)
                        and getattr(lot, 'sale_line', None)):
                    return True
        return False

    @classmethod
    def delete(cls, sales):
        for sale in sales:
            if cls._has_matched_physical_lots(sale):
                raise UserError(
                    "You cannot delete a sale matched to a purchase")
        super().delete(sales)

    def _get_report_lines(self):
        return [line for line in self.lines if getattr(line, 'type', None) == 'line']

    def _get_report_first_line(self):
        lines = self._get_report_lines()
        if lines:
            return lines[0]

    @staticmethod
    def _format_report_number(value, digits='0.0000',
            keep_trailing_decimal=False, strip_trailing_zeros=True,
            use_grouping=False):
        value = Decimal(str(value or 0)).quantize(Decimal(digits))
        text = format(value, ',f' if use_grouping else 'f')
        if strip_trailing_zeros:
            text = text.rstrip('0').rstrip('.')
        if keep_trailing_decimal and '.' not in text:
            text += '.0'
        return text or '0'

    def _format_report_price_words(self, line):
        value = self._get_report_display_price_value(line)
        currency = self._get_report_display_currency(line)
        if currency and (currency.rec_name or '').upper() == 'USC':
            return amount_to_currency_words(value, 'USC', 'USC')
        return amount_to_currency_words(value)

    def _get_report_display_currency(self, line):
        if getattr(line, 'price_type', None) == 'basis':
            if getattr(line, 'enable_linked_currency', False) and getattr(line, 'linked_currency', None):
                return line.linked_currency
            return self.currency
        return getattr(line, 'linked_currency', None) or self.currency

    def _get_report_display_unit(self, line):
        if getattr(line, 'price_type', None) == 'basis':
            if getattr(line, 'enable_linked_currency', False) and getattr(line, 'linked_unit', None):
                return line.linked_unit
            return getattr(line, 'unit', None)
        return getattr(line, 'linked_unit', None) or getattr(line, 'unit', None)

    def _get_report_display_price_value(self, line):
        if getattr(line, 'price_type', None) == 'basis':
            if getattr(line, 'enable_linked_currency', False) and getattr(line, 'linked_currency', None):
                premium = getattr(line, 'premium', None)
                if premium is None and hasattr(line, '_get_premium_price'):
                    premium = line._get_premium_price()
                return Decimal(str(premium or 0))
            return Decimal(str(line._get_premium_price() or 0))
        if getattr(line, 'linked_price', None):
            return Decimal(str(line.linked_price or 0))
        return Decimal(str(line.unit_price or 0))

    def _format_report_price_line(self, line):
        currency = self._get_report_display_currency(line)
        unit = self._get_report_display_unit(line)
        pricing_text = getattr(line, 'get_pricing_text', '') or ''
        parts = [
            (currency.rec_name.upper() if currency and currency.rec_name else '').strip(),
            self._format_report_number(
                self._get_report_display_price_value(line),
                strip_trailing_zeros=False),
            'PER',
            (unit.rec_name.upper() if unit and unit.rec_name else '').strip(),
            f"({self._format_report_price_words(line)})",
        ]
        if pricing_text:
            parts.append(pricing_text)
        return ' '.join(part for part in parts if part)
    
    @property
    def report_terms(self):
        line = self._get_report_first_line()
        if line:
            return line.note
        return ''

    @staticmethod
    def _get_report_line_lots(line):
        return list(getattr(line, 'lots', []) or [])

    @classmethod
    def _get_report_preferred_lots(cls, line):
        lots = cls._get_report_line_lots(line)
        physicals = [
            lot for lot in lots
            if getattr(lot, 'lot_type', None) == 'physic'
        ]
        if physicals:
            return physicals
        virtuals = [
            lot for lot in lots
            if getattr(lot, 'lot_type', None) == 'virtual'
        ]
        if len(virtuals) == 1:
            return virtuals
        return []

    @staticmethod
    def _get_report_lot_hist_weights(lot):
        if not lot:
            return None, None
        if hasattr(lot, 'get_hist_quantity'):
            net, gross = lot.get_hist_quantity()
            return (
                Decimal(str(net or 0)),
                Decimal(str(gross if gross not in (None, '') else net or 0)),
            )
        hist = list(getattr(lot, 'lot_hist', []) or [])
        state = getattr(lot, 'lot_state', None)
        state_id = getattr(state, 'id', None)
        if state_id is not None:
            for entry in hist:
                quantity_type = getattr(entry, 'quantity_type', None)
                if getattr(quantity_type, 'id', None) == state_id:
                    net = Decimal(str(getattr(entry, 'quantity', 0) or 0))
                    gross = Decimal(str(
                        getattr(entry, 'gross_quantity', None)
                        if getattr(entry, 'gross_quantity', None) not in (None, '')
                        else net))
                    return net, gross
        return None, None

    @classmethod
    def _get_report_line_weights(cls, line):
        lots = cls._get_report_preferred_lots(line)
        if lots:
            net_total = Decimal(0)
            gross_total = Decimal(0)
            for lot in lots:
                net, gross = cls._get_report_lot_hist_weights(lot)
                if net is None:
                    continue
                net_total += net
                gross_total += gross
            if net_total or gross_total:
                return net_total, gross_total
        quantity = Decimal(str(getattr(line, 'quantity', 0) or 0))
        return quantity, quantity

    @classmethod
    def _get_report_line_unit(cls, line):
        lots = cls._get_report_preferred_lots(line)
        if lots and getattr(lots[0], 'lot_unit_line', None):
            return lots[0].lot_unit_line
        return getattr(line, 'unit', None)

    def _get_report_total_unit(self):
        virtual_units = []
        for line in self._get_report_lines():
            for lot in self._get_report_line_lots(line):
                if (
                        getattr(lot, 'lot_type', None) == 'virtual'
                        and getattr(lot, 'lot_unit_line', None)):
                    virtual_units.append(lot.lot_unit_line)
        if len(virtual_units) == 1:
            return virtual_units[0]
        line = self._get_report_first_line()
        if line:
            return self._get_report_line_unit(line)
        return None

    @staticmethod
    def _get_report_unit_wording(unit):
        label = ''
        for attr in ('symbol', 'rec_name', 'name'):
            value = getattr(unit, attr, None)
            if value:
                label = str(value).strip().upper()
                break
        mapping = {
            'MT': ('METRIC TON', 'METRIC TONS'),
            'METRIC TON': ('METRIC TON', 'METRIC TONS'),
            'METRIC TONS': ('METRIC TON', 'METRIC TONS'),
            'KG': ('KILOGRAM', 'KILOGRAMS'),
            'KGS': ('KILOGRAM', 'KILOGRAMS'),
            'KILOGRAM': ('KILOGRAM', 'KILOGRAMS'),
            'KILOGRAMS': ('KILOGRAM', 'KILOGRAMS'),
            'LB': ('POUND', 'POUNDS'),
            'LBS': ('POUND', 'POUNDS'),
            'POUND': ('POUND', 'POUNDS'),
            'POUNDS': ('POUND', 'POUNDS'),
            'BALE': ('BALE', 'BALES'),
            'BALES': ('BALE', 'BALES'),
        }
        if label in mapping:
            return mapping[label]
        if label.endswith('S') and len(label) > 1:
            return label[:-1], label
        return label, label

    @classmethod
    def _report_quantity_to_words(cls, quantity, unit):
        singular, plural = cls._get_report_unit_wording(unit)
        return quantity_to_words(
            quantity,
            unit_singular=singular,
            unit_plural=plural,
        )

    @staticmethod
    def _convert_report_quantity(quantity, from_unit, to_unit):
        value = Decimal(str(quantity or 0))
        if not from_unit or not to_unit:
            return value
        if getattr(from_unit, 'id', None) == getattr(to_unit, 'id', None):
            return value
        from_name = getattr(from_unit, 'rec_name', None)
        to_name = getattr(to_unit, 'rec_name', None)
        if from_name and to_name and from_name == to_name:
            return value
        Uom = Pool().get('product.uom')
        converted = Uom.compute_qty(from_unit, float(value), to_unit) or 0
        return Decimal(str(converted))

    def _get_report_total_weight(self, index):
        lines = self._get_report_lines()
        if not lines:
            return None
        total_unit = self._get_report_total_unit()
        total = Decimal(0)
        for line in lines:
            quantity = self._get_report_line_weights(line)[index]
            total += self._convert_report_quantity(
                quantity,
                self._get_report_line_unit(line),
                total_unit,
            )
        return total
        
    @property
    def report_gross(self):
        total = self._get_report_total_weight(1)
        if total is not None:
            return total
        return ''
        
    @property
    def report_net(self):
        total = self._get_report_total_weight(0)
        if total is not None:
            return total
        return ''

    @property
    def report_total_quantity(self):
        total = self._get_report_total_weight(0)
        if total is not None:
            return self._format_report_number(total, keep_trailing_decimal=True)
        return '0.0'

    @property
    def report_quantity_unit_upper(self):
        line = self._get_report_first_line()
        unit = self._get_report_line_unit(line) if line else None
        if unit and unit.rec_name:
            return unit.rec_name.upper()
        return ''

    def _get_report_line_quantity(self, line):
        return self._get_report_line_weights(line)[0]
    
    @property
    def report_qt(self):
        total = self._get_report_total_weight(0)
        if total is not None:
            return self._report_quantity_to_words(
                total, self._get_report_total_unit())
        return ''

    @property
    def report_quantity_lines(self):
        lines = self._get_report_lines()
        if not lines:
            return ''
        details = []
        for line in lines:
            current_quantity = self._get_report_line_quantity(line)
            quantity = self._format_report_number(
                current_quantity, keep_trailing_decimal=True)
            line_unit = self._get_report_line_unit(line)
            unit = (
                line_unit.rec_name.upper()
                if line_unit and line_unit.rec_name else ''
            )
            words = self._report_quantity_to_words(current_quantity, line_unit)
            period = line.del_period.description if getattr(line, 'del_period', None) else ''
            detail = ' '.join(
                part for part in [
                    quantity,
                    unit,
                    f"({words})",
                    f"- {period}" if period else '',
                ] if part)
            if detail:
                details.append(detail)
        return '\n'.join(details)
        
    @property
    def report_nb_bale(self):
        nb_bale = 0
        lines = self._get_report_lines()
        if lines:
            for line in lines:
                if line.lots:
                    nb_bale += sum([l.lot_qt for l in line.lots if l.lot_type == 'physic'])
        if nb_bale:
            return 'NB BALES: ' + str(int(nb_bale))
        return ''

    @property
    def report_product_name(self):
        line = self._get_report_first_line()
        if line and line.product:
            return line.product.name or ''
        return ''

    @property
    def report_product_description(self):
        line = self._get_report_first_line()
        if line and line.product:
            return line.product.description or ''
        return ''

    @property
    def report_crop_name(self):
        if self.crop:
            return self.crop.name or ''
        return ''
    
    @property
    def report_deal(self):
        purchase = self._get_report_deal_purchase()
        purchase_number = self._format_report_deal_number(
            getattr(purchase, 'number', None), 'P')
        sale_number = self._format_report_deal_number(
            getattr(self, 'number', None), 'S')
        return ' '.join(
            number for number in [purchase_number, sale_number] if number)

    def _get_report_deal_purchase(self):
        lots = []
        for line in getattr(self, 'lines', []) or []:
            lots.extend(getattr(line, 'lots', []) or [])

        for lot in lots:
            purchase = getattr(getattr(lot, 'line', None), 'purchase', None)
            if purchase:
                return purchase

        if not lots:
            return None

        LotQt = Pool().get('lot.qt')
        for lot in lots:
            lot_ids = []
            lot_id = getattr(lot, 'id', None)
            if lot_id:
                lot_ids.append(lot_id)
            get_vlot_s = getattr(lot, 'getVlot_s', None)
            if get_vlot_s:
                virtual_lot = get_vlot_s()
                virtual_lot_id = getattr(virtual_lot, 'id', None)
                if virtual_lot_id:
                    lot_ids.append(virtual_lot_id)
            for lot_id in lot_ids:
                lot_qts = LotQt.search([
                    ('lot_s', '=', lot_id),
                    ('lot_p', '>', 0),
                ], limit=1)
                for lot_qt in lot_qts:
                    purchase = getattr(
                        getattr(getattr(lot_qt, 'lot_p', None), 'line', None),
                        'purchase', None)
                    if purchase:
                        return purchase
        return None

    @staticmethod
    def _format_report_deal_number(number, prefix):
        if not number:
            return ''
        number = str(number)
        expected_prefix = prefix + '-'
        if number.upper().startswith(expected_prefix):
            return number
        return expected_prefix + number

    @property
    def report_melya_proforma_number(self):
        return self.report_deal or self.full_number or ''

    @property
    def report_melya_buyer_address(self):
        if not self.invoice_address:
            return ''
        with Transaction().set_context(address_with_party=True):
            return self.invoice_address.full_address

    @property
    def report_melya_buyer_block(self):
        return '\n'.join(filter(None, [
            'Buyer:',
            self.report_melya_buyer_address,
            ]))

    @property
    def report_melya_incoterm(self):
        if not self.incoterm or not self.incoterm.code:
            return ''
        code = self.incoterm.code
        parts = [code]
        if code.upper() not in {'CIF', 'CFR', 'CIP'}:
            return code
        if self.to_location:
            location = (
                getattr(self.to_location, 'party_name', None)
                or getattr(self.to_location, 'rec_name', None)
                or getattr(self.to_location, 'name', None)
            )
            if location:
                parts.append(location)
            country = getattr(self.to_location, 'country', None)
            country_name = getattr(country, 'name', None)
            if country_name:
                parts.append(country_name)
        return ' '.join(parts)

    @staticmethod
    def _get_report_bank_account_number(account):
        if not account:
            return ''
        numbers = list(getattr(account, 'numbers', []) or [])
        for number in numbers:
            if getattr(number, 'type', None) == 'iban' and getattr(number, 'number', None):
                return number.number or ''
        for number in numbers:
            if getattr(number, 'number', None):
                return number.number or ''
        return ''

    @staticmethod
    def _get_report_bank_swift(account):
        bank = getattr(account, 'bank', None) if account else None
        return getattr(bank, 'bic', None) or ''

    @staticmethod
    def _get_report_bank_address(account):
        bank = getattr(account, 'bank', None) if account else None
        party = getattr(bank, 'party', None) if bank else None
        address_get = getattr(party, 'address_get', None) if party else None
        if callable(address_get):
            return address_get()
        return None

    @staticmethod
    def _get_report_bank_party_name(account):
        bank = getattr(account, 'bank', None) if account else None
        party = getattr(bank, 'party', None) if bank else None
        address = Sale._get_report_bank_address(account)
        return (
            getattr(address, 'party_full_name', None)
            or getattr(address, 'party_name', None)
            or getattr(party, 'rec_name', None)
            or getattr(bank, 'rec_name', None)
            or '')

    @staticmethod
    def _get_report_bank_address_line(account):
        address = Sale._get_report_bank_address(account)
        if not address:
            return ''
        city_line = ' '.join(filter(None, [
            getattr(address, 'postal_code', None),
            getattr(address, 'city', None),
        ]))
        lines = [
            getattr(address, 'name', None),
            getattr(address, 'street', None),
            city_line,
        ]
        return ' '.join(line.strip() for line in lines if line and line.strip())

    @property
    def report_melya_bank_name(self):
        return self._get_report_bank_party_name(self.our_bank_account)

    @property
    def report_melya_bank_address(self):
        return self._get_report_bank_address_line(self.our_bank_account)

    @property
    def report_melya_bank_iban(self):
        return self._get_report_bank_account_number(self.our_bank_account)

    @property
    def report_melya_bank_swift(self):
        return self._get_report_bank_swift(self.our_bank_account)

    @property
    def report_melya_bank_swift_line(self):
        if self.report_melya_bank_swift:
            return 'Swift Code: %s' % self.report_melya_bank_swift
        return ''

    @property
    def report_melya_bank_iban_line(self):
        if self.report_melya_bank_iban:
            return 'IBAN : %s' % self.report_melya_bank_iban
        return ''

    @property
    def report_melya_bank_account_line(self):
        parts = []
        if self.report_melya_bank_iban:
            parts.append('IBAN : %s' % self.report_melya_bank_iban)
        if self.report_melya_bank_swift:
            parts.append('Swift Code: %s' % self.report_melya_bank_swift)
        return ', '.join(parts)

    @property
    def report_packing(self):
        nb_packing = 0
        unit = ''
        lines = self._get_report_lines()
        if lines:
            for line in lines:
                if line.lots:
                    nb_packing += sum([l.lot_qt for l in line.lots if l.lot_type == 'physic'])
                    if len(line.lots)>1:
                        unit = line.lots[1].lot_unit.name
        return str(int(nb_packing)) + unit
    
    @property
    def report_price(self):
        line = self._get_report_first_line()
        if line:
            return self._format_report_price_words(line)
        return ''

    @property
    def report_price_lines(self):
        lines = self._get_report_lines()
        if lines:
            return '\n'.join(self._format_report_price_line(line) for line in lines)
        return ''

    @property
    def report_commission_invoice_number(self):
        return self.reference or self.full_number or self.number or ''

    @property
    def report_commission_contract_number(self):
        return self.full_number or self.number or ''

    def _get_report_commission_fee(self):
        fallback = None
        for line in self._get_report_lines():
            for fee in getattr(line, 'fees', []) or []:
                if fallback is None:
                    fallback = fee
                product = getattr(fee, 'product', None)
                product_name = (
                    getattr(product, 'rec_name', None)
                    or getattr(product, 'name', None) or '')
                if 'commission' in product_name.lower():
                    return fee
        return fallback

    def _get_report_commission_shipment(self):
        for line in self._get_report_lines():
            for lot in getattr(line, 'lots', []) or []:
                shipment = (
                    getattr(lot, 'lot_shipment_out', None)
                    or getattr(lot, 'lot_shipment_in', None)
                    or getattr(lot, 'lot_shipment_internal', None))
                if shipment:
                    return shipment

    @property
    def report_commission_bl_number(self):
        shipment = self._get_report_commission_shipment()
        return getattr(shipment, 'bl_number', None) or ''

    @property
    def report_commission_bl_date(self):
        shipment = self._get_report_commission_shipment()
        return getattr(shipment, 'bl_date', None)

    @property
    def report_commission_vessel(self):
        shipment = self._get_report_commission_shipment()
        vessel = getattr(shipment, 'vessel', None) if shipment else None
        return (
            getattr(vessel, 'vessel_name', None)
            or getattr(shipment, 'vessel_name', None)
            or '')

    @property
    def report_commission_quantity_display(self):
        return self.report_total_quantity

    @property
    def report_commission_quantity_unit_upper(self):
        return self.report_quantity_unit_upper or ''

    @property
    def report_commission_lbs_display(self):
        total = self._get_report_total_weight(0)
        if total is None:
            return ''
        unit = self._get_report_total_unit()
        lbs_unit = self._find_report_unit('LBS')
        if not lbs_unit:
            lbs_unit = self._find_report_unit('LB')
        if not lbs_unit:
            return ''
        return self._format_report_number(
            self._convert_report_quantity(total, unit, lbs_unit),
            digits='0.01')

    @staticmethod
    def _find_report_unit(name):
        Uom = Pool().get('product.uom')
        units = Uom.search([
            'OR',
            ('name', '=', name),
            ('symbol', '=', name),
        ], limit=1)
        return units[0] if units else None

    @property
    def report_commission_base_amount_display(self):
        return self._format_report_number(
            self.untaxed_amount or self.total_amount or 0,
            digits='0.01', strip_trailing_zeros=False)

    @property
    def report_commission_currency_name(self):
        return getattr(self.currency, 'name', None) or getattr(
            self.currency, 'code', None) or ''

    @property
    def report_commission_invoice_line(self):
        return ' '.join(part for part in [
            'Our invoice N.',
            self.report_commission_invoice_number,
            self.report_commission_currency_name,
            self.report_commission_base_amount_display,
        ] if part)

    @property
    def report_commission_rate_line(self):
        fee = self._get_report_commission_fee()
        base = self.report_commission_base_amount_display
        currency = self.report_commission_currency_name
        if fee:
            price = self._format_report_number(
                getattr(fee, 'price', 0) or 0, digits='0.0000')
            if getattr(fee, 'mode', None) in {'pprice', 'rate', 'pcost'}:
                return ' '.join(part for part in [
                    price, '% on', base, currency,
                ] if part)
            fee_currency = getattr(fee, 'currency', None)
            fee_unit = getattr(fee, 'unit', None)
            return ' '.join(part for part in [
                price,
                getattr(fee_currency, 'name', None) or getattr(
                    fee_currency, 'code', None) or '',
                '/',
                (getattr(fee_unit, 'rec_name', None) or '').upper(),
            ] if part)
        return ' '.join(part for part in ['0 % on', base, currency] if part)

    @property
    def report_commission_secondary_rate_line(self):
        fee = self._get_report_commission_fee()
        if not fee or not getattr(fee, 'enable_linked_currency', False):
            return ''
        currency = getattr(fee, 'linked_currency', None)
        unit = getattr(fee, 'linked_unit', None)
        return ' '.join(part for part in [
            self._format_report_number(
                getattr(fee, 'linked_price', 0) or 0, digits='0.0000'),
            getattr(currency, 'name', None) or getattr(currency, 'code', None) or '',
            '/',
            (getattr(unit, 'rec_name', None) or '').upper(),
        ] if part)

    @property
    def report_commission_total_display(self):
        fee = self._get_report_commission_fee()
        if fee and hasattr(fee, 'get_amount'):
            amount = fee.get_amount()
        else:
            amount = self.total_amount or 0
        return self._format_report_number(
            abs(Decimal(str(amount or 0))), digits='0.01',
            strip_trailing_zeros=False)

    @property
    def report_price_composition_lines(self):
        return '\n'.join(
            row['label'] for row in self.report_price_composition_rows)

    @property
    def report_price_composition_rows(self):
        lines = []
        for line in self._get_report_lines():
            currency = self._get_report_display_currency(line)
            currency_name = (
                currency.rec_name.upper()
                if currency and currency.rec_name else '')
            compositions = list(getattr(line, 'price_composition', []) or [])
            missing = [
                composition for composition in compositions
                if getattr(composition, 'price', None) in (None, '')]
            quantity = self._get_report_line_quantity(line)
            known_total = sum(
                Decimal(str(getattr(composition, 'price', 0) or 0)) * quantity
                for composition in compositions
                if getattr(composition, 'price', None) not in (None, ''))
            residual = None
            if len(missing) == 1:
                residual = (
                    self._get_report_price_composition_line_amount(line)
                    - known_total)
            for composition in compositions:
                component = getattr(composition, 'component', '') or ''
                price = getattr(composition, 'price', None)
                if price in (None, ''):
                    if composition is missing[0] and residual is not None:
                        price = residual
                    else:
                        continue
                label = ' '.join(
                    part for part in [
                        component,
                        'value in',
                        currency_name,
                    ] if part)
                if label:
                    amount = Decimal(str(price or 0)) * quantity
                    if missing and composition is missing[0] and residual is not None:
                        amount = residual
                    lines.append({
                        'label': label,
                        'amount': amount,
                        'amount_text': self._format_report_number(
                            amount, digits='0.01',
                            strip_trailing_zeros=False,
                            use_grouping=True),
                    })
        return lines

    @staticmethod
    def _get_report_price_composition_line_amount(line):
        amount = getattr(line, 'amount', None)
        if amount not in (None, ''):
            return Decimal(str(amount or 0))
        quantity = Decimal(str(getattr(line, 'quantity', 0) or 0))
        unit_price = Decimal(str(getattr(line, 'unit_price', 0) or 0))
        premium_getter = getattr(line, '_get_premium_price', None)
        premium = premium_getter() if premium_getter else getattr(
            line, 'premium', 0)
        return quantity * (unit_price + Decimal(str(premium or 0)))

    @property
    def report_trade_blocks(self):
        lines = self._get_report_lines()
        blocks = []
        for line in lines:
            current_quantity = self._get_report_line_quantity(line)
            quantity = self._format_report_number(
                current_quantity, keep_trailing_decimal=True)
            line_unit = self._get_report_line_unit(line)
            unit = (
                line_unit.rec_name.upper()
                if line_unit and line_unit.rec_name else ''
            )
            words = self._report_quantity_to_words(current_quantity, line_unit)
            period = line.del_period.description if getattr(line, 'del_period', None) else ''
            quantity_line = ' '.join(
                part for part in [
                    quantity,
                    unit,
                    f"({words})",
                    f"- {period}" if period else '',
                ] if part)
            price_line = self._format_report_price_line(line)
            blocks.append((quantity_line, price_line))
        return blocks
    
    @property
    def report_delivery(self):
        del_date = 'PROMPT'
        if self.lines:
            if self.lines[0].estimated_date:
                delivery_date = [dd.estimated_date for dd in self.lines[0].estimated_date if dd.trigger=='deldate']
                if delivery_date:
                    del_date = delivery_date[0]
                    if del_date:
                        del_date = format_date_en(del_date)
        return del_date

    @property
    def report_delivery_period_description(self):
        line = self._get_report_first_line()
        if line and line.del_period:
            return line.del_period.description or ''
        return ''

    @property
    def report_shipment_periods(self):
        periods = []
        for line in self._get_report_lines():
            period = line.del_period.description if line.del_period else ''
            if period and period not in periods:
                periods.append(period)
        if periods:
            return '\n'.join(periods)
        return ''
    
    @property
    def report_payment_date(self):
        line = self._get_report_first_line()
        if line:
            if self.lc_date:
                return format_date_en(self.lc_date)
            Date = Pool().get('ir.date')
            payment_date = line.sale.payment_term.lines[0].get_date(Date.today(), line)
            if payment_date:
                payment_date = format_date_en(payment_date)
                return payment_date

    def _get_report_bill_amount(self):
        invoices = [
            invoice for invoice in (self.invoices or [])
            if getattr(invoice, 'state', None) != 'cancelled'
        ]
        if invoices:
            invoice = sorted(
                invoices,
                key=lambda i: (
                    getattr(i, 'invoice_date', None) or datetime.date.min,
                    getattr(i, 'id', 0)))[0]
            return Decimal(str(getattr(invoice, 'total_amount', 0) or 0))
        return Decimal(str(self.total_amount or 0))

    @property
    def report_bill_amount(self):
        return self._get_report_bill_amount()

    @property
    def report_bill_amount_words(self):
        value = self._get_report_bill_amount()
        if self.currency and (self.currency.rec_name or '').upper() == 'USC':
            return amount_to_currency_words(value, 'USC', 'USC')
        return amount_to_currency_words(value)

    @property
    def report_bill_maturity_date(self):
        maturity_dates = []
        for invoice in (self.invoices or []):
            if getattr(invoice, 'state', None) == 'cancelled':
                continue
            for line in (invoice.lines_to_pay or []):
                if getattr(line, 'maturity_date', None):
                    maturity_dates.append(line.maturity_date)
        if maturity_dates:
            return min(maturity_dates)

        if self.lc_date:
            return self.lc_date

        line = self._get_report_first_line()
        if line and self.payment_term and self.payment_term.lines:
            Date = Pool().get('ir.date')
            return self.payment_term.lines[0].get_date(Date.today(), line)
        
    @property
    def report_shipment(self):
        if self.lines:
            if len(self.lines[0].lots)>1:
                shipment = self.lines[0].lots[1].lot_shipment_in
                lot = self.lines[0].lots[1].lot_name
                if shipment:
                    info = ''
                    if shipment.bl_number:
                        info += ' B/L ' + shipment.bl_number
                    if shipment.supplier:
                        info += ' BY ' + shipment.supplier.name
                    if shipment.vessel:
                        info += ' (' + shipment.vessel.vessel_name + ')'
                    if shipment.container and shipment.container[0].container_no:
                        id = 1
                        for cont in shipment.container:
                            if id == 1:
                                info += ' Container(s)'
                            if cont.container_no:
                                info += ' ' + cont.container_no
                            else:
                                info += ' unnamed'
                            id += 1
                    # info += ' (LOT ' + lot + ')'
                    if shipment.note:
                        info += ' ' + shipment.note
                    return info
        else:
            return ''
        
    @classmethod
    def default_viewer(cls):
        country_start = "Zobiland"
        data = {
            "highlightedCountryName": country_start
        }
        return "d3:" + json.dumps(data)

    @fields.depends('doc_template','required_documents')
    def on_change_with_required_documents(self):
            if self.doc_template:
                return self.doc_template.type

    def get_viewer(self, name=None):
        country_start = ''
        dep_name = ''
        arr_name = ''
        departure = ''
        arrival = ''
        if self.party and self.party.addresses:
            if self.party.addresses[0].country:
                country_start = self.party.addresses[0].country.name
        if self.from_location:
            lat_from = self.from_location.lat
            lon_from = self.from_location.lon
            dep_name = self.from_location.name
            departure = { "name":dep_name,"lat": str(lat_from), "lon": str(lon_from) }
        if self.to_location:
            lat_to = self.to_location.lat
            lon_to = self.to_location.lon
            arr_name = self.to_location.name
            arrival = { "name":arr_name,"lat": str(lat_to), "lon": str(lon_to) }
        data = {
            "highlightedCountryNames": [{"name":country_start}],
            "routePoints": [
                { "lon": -46.3, "lat": -23.9 },
                { "lon": -30.0, "lat": -20.0 },
                { "lon": -30.0, "lat": 0.0 },
                { "lon": -6.0,  "lat": 35.9 },
                { "lon": 15.0,  "lat": 38.0 },
                { "lon": 29.0,  "lat": 41.0 }
                ],
            "boats": [
                # {"name": "CARIBBEAN 1",
                # "imo": "1234567",
                # "lon": -30.0,
                # "lat": 0.0,
                # "status": "En route",
                # "links": [
                #     { "text": "Voir sur VesselFinder", "url": "https://www.vesselfinder.com" },
                #     { "text": "Détails techniques", "url": "https://example.com/tech" }
                # ],
                # "actions": [
                #     { "type": "track", "id": "123", "label": "Suivre ce bateau" },
                #     { "type": "details", "id": "123", "label": "Voir détails" }
                # ]}
                ],
            "cottonStocks": [
                # { "name":"Mali","lat": 12.65, "lon": -8.0, "amount": 300 },
                # { "name":"Egypte","lat": 30.05, "lon": 31.25, "amount": 500 },
                # { "name":"Irak","lat": 33.0,  "lon": 44.0,  "amount": 150 }
            ],
            "departures": [departure],
            "arrivals": [arrival]
        }

        return "d3:" + json.dumps(data)

    @fields.depends('party','from_location','to_location')
    def on_change_with_viewer(self):
            return self.get_viewer()

    def _execution_summary_quantity(self, value):
        if value is None:
            return 0.0
        try:
            return float(value)
        except (TypeError, ValueError):
            return 0.0

    @staticmethod
    def _summary_decimal(value):
        if value in (None, ''):
            return Decimal('0')
        try:
            return Decimal(str(value))
        except Exception:
            return Decimal('0')

    @classmethod
    def _summary_percent(cls, part, total):
        total = cls._summary_decimal(total)
        if not total:
            return 0.0
        part = cls._summary_decimal(part)
        percent = (part / total) * Decimal('100')
        percent = max(Decimal('0'), min(Decimal('100'), percent))
        return float(round(percent, 2))

    @staticmethod
    def _summary_number(value):
        if value is None:
            return None
        try:
            return float(value)
        except (TypeError, ValueError):
            return None

    @staticmethod
    def _summary_link(model, records):
        unique = []
        seen = set()
        for record in records:
            record_id = getattr(record, 'id', None)
            if not record_id:
                continue
            key = (model, record_id)
            if key in seen:
                continue
            seen.add(key)
            unique.append(record)
        link = {
            "model": model,
            "count": len(unique),
        }
        if len(unique) == 1:
            link["res_id"] = unique[0].id
        return link

    @staticmethod
    def _summary_mixed_link(records, default_model=None):
        unique = []
        seen = set()
        for model, record in records:
            record_id = getattr(record, 'id', None)
            if not model or not record_id:
                continue
            key = (model, record_id)
            if key in seen:
                continue
            seen.add(key)
            unique.append((model, record))
        link = {"count": len(unique)}
        if default_model:
            link["model"] = default_model
        if len(unique) == 1:
            model, record = unique[0]
            link.update({
                "model": model,
                "res_id": record.id,
            })
        elif unique:
            link["model"] = unique[0][0]
        return link

    @staticmethod
    def _summary_shipment_record(record):
        if getattr(record, 'lot_shipment_out', None):
            return ('stock.shipment.out', record.lot_shipment_out)
        if getattr(record, 'lot_shipment_in', None):
            return ('stock.shipment.in', record.lot_shipment_in)
        if getattr(record, 'lot_shipment_internal', None):
            return ('stock.shipment.internal', record.lot_shipment_internal)
        return (None, None)

    @classmethod
    def _line_priced_percent(cls, line):
        price_type = getattr(line, 'price_type', None)
        if price_type in {'cash', 'priced', 'efp'}:
            return 100.0
        progress = getattr(line, 'progress', None)
        if progress is None and hasattr(line, 'get_progress'):
            progress = line.get_progress('progress')
        return cls._summary_percent(progress or 0, 1)

    @classmethod
    def _line_hedged_percent(cls, line, quantity):
        hedged_quantity = Decimal('0')
        for derivative in (getattr(line, 'derivatives', None) or []):
            der_quantity = getattr(derivative, 'quantity', None)
            if der_quantity is None and hasattr(derivative, 'get_qt'):
                der_quantity = derivative.get_qt('quantity')
            hedged_quantity += abs(cls._summary_decimal(der_quantity))
        return cls._summary_percent(hedged_quantity, quantity)

    @classmethod
    def _line_invoice_quantity_percent(cls, line, quantity, states=None):
        invoiced_quantity = Decimal('0')
        Uom = Pool().get('product.uom')
        for invoice_line in (getattr(line, 'invoice_lines', None) or []):
            invoice = getattr(invoice_line, 'invoice', None)
            invoice_state = getattr(invoice, 'state', None)
            if (not invoice or invoice_state == 'cancelled'
                    or (states and invoice_state not in states)
                    or getattr(invoice_line, 'type', 'line') != 'line'):
                continue
            invoice_quantity = cls._summary_decimal(
                getattr(invoice_line, 'quantity', None))
            invoice_unit = getattr(invoice_line, 'unit', None) or getattr(
                line, 'unit', None)
            line_unit = getattr(line, 'unit', None)
            if invoice_unit and line_unit and invoice_unit != line_unit:
                invoice_quantity = cls._summary_decimal(Uom.compute_qty(
                    invoice_unit, float(invoice_quantity), line_unit))
            invoiced_quantity += abs(invoice_quantity)
        return cls._summary_percent(invoiced_quantity, quantity)

    @classmethod
    def _line_period_name(cls, line):
        period = getattr(line, 'del_period', None)
        return getattr(period, 'rec_name', None) or getattr(period, 'name', '') or ''

    @classmethod
    def _line_amount_value(cls, line):
        amount = getattr(line, 'amount', None)
        if amount is None and hasattr(line, 'on_change_with_amount'):
            amount = line.on_change_with_amount()
        return cls._summary_decimal(amount)

    @classmethod
    def _line_pnl_values(cls, sale, line):
        line_id = getattr(line, 'id', None)
        pnl = Decimal('0')
        mtm = Decimal('0')
        previous_pnl = Decimal('0')
        has_previous = False
        records = (
            getattr(sale, 'pnl', None)
            if getattr(sale, 'group_pnl', False)
            else getattr(sale, 'pnl_', None))
        for record in (records or []):
            record_line = getattr(record, 'r_sale_line', None) or getattr(
                record, 'sale_line', None)
            if line_id and getattr(record_line, 'id', None) != line_id:
                continue
            pnl += cls._summary_decimal(
                getattr(record, 'r_pnl', None)
                if hasattr(record, 'r_pnl') else getattr(record, 'pnl', None))
            mtm += cls._summary_decimal(
                getattr(record, 'r_mtm', None)
                if hasattr(record, 'r_mtm') else getattr(record, 'mtm', None))
            amount_prev = (
                getattr(record, 'r_amount_prev', None)
                if hasattr(record, 'r_amount_prev')
                else getattr(record, 'amount_prev', None))
            if amount_prev in (None, ''):
                continue
            base_amount = (
                getattr(record, 'r_base_amount', None)
                if hasattr(record, 'r_base_amount')
                else getattr(record, 'base_amount', None))
            previous_pnl += (
                cls._summary_decimal(base_amount)
                - cls._summary_decimal(amount_prev))
            has_previous = True
        return pnl, mtm, previous_pnl if has_previous else None

    @classmethod
    def _pnl_change_percent(cls, current, previous):
        if previous in (None, ''):
            return None
        previous = cls._summary_decimal(previous)
        if not previous:
            return None
        current = cls._summary_decimal(current)
        return float(round(((current - previous) / abs(previous)) * 100, 2))

    def _sale_pnl_change_percent(self):
        changes = []
        for line in (self.lines or []):
            if getattr(line, 'type', 'line') != 'line':
                continue
            line_pnl, _line_mtm, previous_pnl = self._line_pnl_values(
                self, line)
            change = self._pnl_change_percent(line_pnl, previous_pnl)
            if change is not None:
                changes.append(Decimal(str(change)))
        if not changes:
            return None
        return float(round(sum(changes) / Decimal(len(changes)), 2))

    def _get_sale_map_summary_data(self):
        dep_name = ''
        arr_name = ''
        departure = None
        arrival = None
        if self.from_location:
            dep_name = self.from_location.name or self.from_location.rec_name
            departure = {
                "name": dep_name,
                "lat": self._summary_number(self.from_location.lat),
                "lon": self._summary_number(self.from_location.lon),
            }
        if self.to_location:
            arr_name = self.to_location.name or self.to_location.rec_name
            arrival = {
                "name": arr_name,
                "lat": self._summary_number(self.to_location.lat),
                "lon": self._summary_number(self.to_location.lon),
            }
        return {
            "from": dep_name,
            "to": arr_name,
            "route": '%s > %s' % (dep_name or '-', arr_name or '-'),
            "departures": [departure] if departure else [],
            "arrivals": [arrival] if arrival else [],
        }

    def _get_sale_execution_summary_data(self):
        LotQt = Pool().get('lot.qt')
        rows = []
        totals = defaultdict(float)

        for index, line in enumerate((
                l for l in (self.lines or [])
                if getattr(l, 'type', 'line') == 'line'), 1):
            quantity = self._execution_summary_quantity(
                getattr(line, 'quantity_theorical', None)
                or getattr(line, 'quantity', None))
            matched = 0.0
            shipped = 0.0
            lots_count = 0
            shipped_routes = []
            purchases = []
            shipments = []
            line_lots = list(getattr(line, 'lots', None) or [])
            lot_ids = [lot.id for lot in line_lots if getattr(lot, 'id', None)]
            lot_qts = LotQt.search([('lot_s', 'in', lot_ids)]) if lot_ids else []

            for lqt in lot_qts:
                lots_count += 1
                lot_quantity = self._execution_summary_quantity(
                    getattr(lqt, 'lot_quantity', None)
                    or getattr(lqt, 'lot_qt', None))
                if getattr(lqt, 'lot_p', None):
                    matched += lot_quantity
                    purchase_line = getattr(lqt.lot_p, 'line', None)
                    purchase = getattr(purchase_line, 'purchase', None)
                    if purchase:
                        purchases.append(purchase)
                shipment_model, shipment = self._summary_shipment_record(lqt)
                if shipment:
                    shipments.append((shipment_model, shipment))
                    shipped += lot_quantity
                    route_from = getattr(
                        getattr(shipment, 'from_location', None),
                        'rec_name', None)
                    route_to = getattr(
                        getattr(shipment, 'to_location', None),
                        'rec_name', None)
                    route_from = route_from or getattr(
                        getattr(self, 'from_location', None), 'rec_name', None)
                    route_to = route_to or getattr(
                        getattr(self, 'to_location', None), 'rec_name', None)
                    if route_from or route_to:
                        shipped_routes.append(
                            '%s > %s' % (route_from or '-', route_to or '-'))

            if not lot_qts:
                for lot in line_lots:
                    lots_count += 1
                    lot_quantity = self._execution_summary_quantity(
                        getattr(lot, 'lot_qt', None))
                    if getattr(lot, 'lot_p', None) or getattr(lot, 'line', None):
                        matched += lot_quantity
                    purchase_line = getattr(lot, 'line', None)
                    if getattr(lot, 'lot_p', None):
                        purchase_line = getattr(lot.lot_p, 'line', None)
                    purchase = getattr(purchase_line, 'purchase', None)
                    if purchase:
                        purchases.append(purchase)
                    shipment_model, shipment = self._summary_shipment_record(lot)
                    if shipment:
                        shipments.append((shipment_model, shipment))
                        shipped += lot_quantity
                        route_from = getattr(
                            getattr(shipment, 'from_location', None),
                            'rec_name', None)
                        route_to = getattr(
                            getattr(shipment, 'to_location', None),
                            'rec_name', None)
                        route_from = route_from or getattr(
                            getattr(self, 'from_location', None),
                            'rec_name', None)
                        route_to = route_to or getattr(
                            getattr(self, 'to_location', None),
                            'rec_name', None)
                        if route_from or route_to:
                            shipped_routes.append(
                                '%s > %s' % (route_from or '-', route_to or '-'))

            unit = getattr(getattr(line, 'unit', None), 'rec_name', '') or ''
            product = getattr(getattr(line, 'product', None), 'rec_name', '') or ''
            invoices = [
                invoice_line.invoice
                for invoice_line in (getattr(line, 'invoice_lines', None) or [])
                if (getattr(invoice_line, 'invoice', None)
                    and getattr(invoice_line.invoice, 'state', None) != 'cancelled')
            ]
            amount = self._line_amount_value(line)
            line_pnl, line_mtm, previous_pnl = self._line_pnl_values(
                self, line)
            line_pnl_change = self._pnl_change_percent(
                line_pnl, previous_pnl)
            route = (shipped_routes[0] if shipped_routes else '%s > %s' % (
                getattr(getattr(self, 'from_location', None), 'rec_name', None)
                or '-',
                getattr(getattr(self, 'to_location', None), 'rec_name', None)
                or '-'))

            rows.append({
                'index': index,
                'product': product,
                'physical': shipped,
                'quantity': quantity,
                'period': self._line_period_name(line),
                'matched': matched,
                'shipped': shipped,
                'priced': self._line_priced_percent(line),
                'hedged': self._line_hedged_percent(line, quantity),
                'invoiced': self._line_invoice_quantity_percent(line, quantity),
                'paid': self._line_invoice_quantity_percent(
                    line, quantity, states={'paid'}),
                'pnl': self._json_decimal(line_pnl),
                'pnl_change_d1': line_pnl_change,
                'amount': self._json_decimal(amount),
                'mtm': self._json_decimal(line_mtm),
                'open': max(quantity - shipped, 0.0),
                'unit': unit,
                'lots': lots_count,
                'route': route,
                'sales': self._summary_link('sale.sale', [self]),
                'purchases': self._summary_link('purchase.purchase', purchases),
                'shipments': self._summary_mixed_link(
                    shipments, default_model='stock.shipment.out'),
                'invoices': self._summary_link('account.invoice', invoices),
            })
            totals['quantity'] += quantity
            totals['matched'] += matched
            totals['shipped'] += shipped
            totals['open'] += max(quantity - shipped, 0.0)
            totals['lines'] += 1
            totals['amount'] += float(amount)
            totals['pnl'] += float(line_pnl)
            totals['mtm'] += float(line_mtm)

        return {
            'unit': rows[0]['unit'] if rows else '',
            'totals': dict(totals),
            'rows': rows[:3],
        }

    @staticmethod
    def _json_decimal(value):
        if value in (None, ''):
            return None
        return float(value)

    def _get_sale_pnl_summary_data(self):
        currency = getattr(getattr(self, 'currency', None), 'code', None) or ''
        pnl_value = Decimal('0')
        mtm_value = Decimal('0')
        records = (
            getattr(self, 'pnl', None)
            if getattr(self, 'group_pnl', False)
            else getattr(self, 'pnl_', None))
        for line in (records or []):
            pnl_value += self._summary_decimal(
                getattr(line, 'r_pnl', None)
                if hasattr(line, 'r_pnl') else getattr(line, 'pnl', None))
            mtm_value += self._summary_decimal(
                getattr(line, 'r_mtm', None)
                if hasattr(line, 'r_mtm') else getattr(line, 'mtm', None))
        data = {
            "pnl": {
                "value": self._json_decimal(pnl_value),
                "currency": currency,
                "change_d1": self._sale_pnl_change_percent(),
            },
            "mtm": {
                "value": self._json_decimal(mtm_value),
                "currency": currency,
                "change": None,
            },
        }
        return data

    def _get_sale_activity_summary_data(self):
        if not getattr(self, 'id', None):
            return []
        Log = Pool().get('ir.model.log')
        logs = Log.search([
            ('resource', '=', 'sale.sale,%s' % self.id),
            ('event', '!=', 'create'),
            ], limit=8)
        event_types = {
            'write': 'info',
            'transition': 'approved',
            'button': 'success',
            'wizard': 'info',
            'delete': 'warning',
        }
        activity = []
        for log in logs:
            if len(activity) >= 4:
                break
            date = getattr(log, 'create_date', None)
            user = getattr(getattr(log, 'user', None), 'rec_name', None) or ''
            text = getattr(log, 'change_summary', None)
            text = text or getattr(log, 'action', None) or getattr(
                log, 'target', None)
            if not text:
                text = getattr(log, 'event_string', None) or log.event
            if ' create' in text or 'create,' in text or text.startswith('Create'):
                continue
            activity.append({
                "type": event_types.get(log.event, 'info'),
                "text": text,
                "date": date.strftime('%d/%m/%Y') if date else '',
                "user": user,
            })
        return activity

    def _get_summary_total_chips(self):
        chips = []
        for assignment in (self.analytic_dimensions or [])[:2]:
            dimension = getattr(assignment, 'dimension', None)
            value = getattr(assignment, 'value', None)
            dimension_name = (
                getattr(dimension, 'name', None)
                or getattr(dimension, 'rec_name', None))
            value_name = (
                getattr(value, 'name', None)
                or getattr(value, 'rec_name', None))
            if not dimension_name or not value_name:
                continue
            chips.append({
                "dimension": dimension_name,
                "value": value_name,
            })
        return chips

    def get_execution_summary_viewer(self, name=None):
        data = self._get_sale_execution_summary_data()
        data = dict(data, type='execution-summary')
        return "d3:" + json.dumps(data)

    def get_sale_summary_data(self, name=None):
        execution = self._get_sale_execution_summary_data()
        currency = getattr(self, 'currency', None)
        currency_badge = (
            getattr(currency, 'name', None)
            or getattr(currency, 'code', None)
            or '')
        data = {
            "currency": getattr(currency, 'code', None) or '',
            "currency_badge": currency_badge,
            "map": self._get_sale_map_summary_data(),
            "execution": execution,
            "amount": {
                "value": execution.get('totals', {}).get('amount', 0),
            },
            "total_chips": self._get_summary_total_chips(),
            "activity": self._get_sale_activity_summary_data(),
            "approval": {
                "status": "need_approval",
            },
        }
        data.update(self._get_sale_pnl_summary_data())
        return json.dumps(data)

    @fields.depends(
        'currency', 'from_location', 'to_location', 'lines', 'pnl', 'pnl_',
        'group_pnl', 'analytic_dimensions')
    def on_change_with_sale_summary_data(self, name=None):
            return self.get_sale_summary_data()

    def getLots(self):
        if self.lines:
            if self.lines.lots:
                return [l for l in self.lines.lots]

    @classmethod
    def _check_lines_delivery_period_values(cls, values):
        line_commands = values.get('lines') or []
        if not line_commands:
            return
        Line = Pool().get('sale.line')
        pending_values = {}
        for command in line_commands:
            action = command[0]
            if action == 'create':
                for line_values in command[1]:
                    Line._check_delivery_period_values([Line()], line_values)
            elif action == 'write':
                actions = iter(command[1:])
                for line_ids, line_values in zip(actions, actions):
                    for line_id in line_ids:
                        pending_values.setdefault(line_id, {}).update(
                            line_values)
            elif action == 'add':
                for line_id in command[1]:
                    pending_values.setdefault(line_id, {})
        for line_id, line_values in pending_values.items():
            Line._check_delivery_period_values(
                Line.browse([line_id]), line_values)

    @classmethod
    def write(cls, *args):
        actions = iter(args)
        args = []
        for sales, values in zip(actions, actions):
            cls._check_lines_delivery_period_values(values)
            args.extend((sales, values))
        super().write(*args)

    @classmethod
    def validate(cls, sales):
        super(Sale, cls).validate(sales)
        Line = Pool().get('sale.line')
        Date = Pool().get('ir.date')
        for sale in sales:
            for line in sale.lines:
                if not line.quantity_theorical and line.quantity > 0:
                    line.quantity_theorical = Decimal(str(line.quantity)).quantize(Decimal("0.00001"))
                    Line.save([line])
                    
                if line.lots:
                    line_p = line.get_matched_lines()#line.lots[0].line
                    if line_p:
                        for l in line_p:
                            #compute pnl
                            Pnl = Pool().get('valuation.valuation')
                            Pnl.generate(l.lot_p.line)

                    # if line.quantity_theorical:
                    #     OpenPosition = Pool().get('open.position')
                    #     OpenPosition.create_from_sale_line(line)
                
                if line.price_type == 'basis' and line.lots: #line.price_pricing and line.price_components and 
                    previous_linked_price = line.linked_price
                    line.sync_linked_price_from_basis()
                    unit_price = line.get_basis_price()
                    if unit_price != line.unit_price or line.linked_price != previous_linked_price:
                        Line = Pool().get('sale.line')
                        line.unit_price = unit_price
                        Line.save([line])
                if line.price_type == 'efp':
                    if line.derivatives:
                        for d in line.derivatives:
                            line.unit_price = d.price_index.get_price(Date.today(),line.unit,line.currency,True)
                            Line.save([line])

    def _contract_clause_values_from_template(self, direction):
        Selection = Pool().get('contract.clause.selection')
        values = []
        template = getattr(self, 'contract_template', None)
        if not template:
            return values
        for template_line in sorted(
                template.lines or [], key=lambda line: line.sequence or 0):
            clause = template_line.clause
            if not clause or not clause.matches_contract(self, direction):
                continue
            values.append(Selection(
                sequence=template_line.sequence,
                section=template_line.section or clause.category,
                clause=clause,
                selected=not template_line.optional,
                optional=template_line.optional,
            ))
        return values

    @fields.depends('contract_template', 'contract_clauses', 'incoterm',
        'payment_term', 'lines')
    def on_change_contract_template(self):
        if self.contract_template:
            self.contract_clauses = self._contract_clause_values_from_template(
                'sale')

class PriceComposition(metaclass=PoolMeta):
    __name__ = 'price.composition'

    sale_line = fields.Many2One('sale.line',"Sale line")


class ContractClauseSelection(metaclass=PoolMeta):
    __name__ = 'contract.clause.selection'

    sale = fields.Many2One('sale.sale', "Sale", ondelete='CASCADE')


class CharterCondition(metaclass=PoolMeta):
    __name__ = 'charter.condition'

    sale = fields.Many2One(
        'sale.sale', "Sale", ondelete='CASCADE')
    sale_line = fields.Many2One(
        'sale.line', "Sale Line", ondelete='CASCADE')


class PremiumComposition(metaclass=PoolMeta):
    __name__ = 'premium.composition'

    sale_line = fields.Many2One('sale.line', "Sale line", ondelete='CASCADE')
    
class SaleLine(metaclass=PoolMeta):
    __name__ = 'sale.line'

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.quantity.readonly = True

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

    @fields.depends('product', 'unit', 'sale', '_parent_sale.company')
    def on_change_product(self):
        previous_unit = self.unit
        parent_on_change = getattr(super(), 'on_change_product', None)
        if parent_on_change:
            parent_on_change()
        if (self.sale and is_itsa_company(self.sale.company)
                and (not previous_unit
                    or record_id(previous_unit) == default_itsa_unit())):
            unit = default_itsa_unit()
            if unit:
                self.unit = unit

    @staticmethod
    def _is_empty_quantity(quantity):
        if quantity in (None, ''):
            return True
        return Decimal(str(quantity or 0)) == Decimal(0)

    @staticmethod
    def _has_physical_lot(line):
        return any(
            getattr(lot, 'lot_type', None) == 'physic'
            for lot in (getattr(line, 'lots', None) or []))

    @classmethod
    def _sync_initial_quantity_from_theorical(cls, line):
        if cls._has_physical_lot(line):
            return False
        if not cls._is_empty_quantity(getattr(line, 'quantity', None)):
            return False
        quantity = getattr(line, 'quantity_theorical', None)
        if cls._is_empty_quantity(quantity):
            return False
        line.quantity = Decimal(str(quantity)).quantize(Decimal("0.00001"))
        return True

    @classmethod
    def _sync_quantity_counter_from_theorical(cls, line):
        if cls._has_physical_lot(line):
            return False
        quantity = getattr(line, 'quantity_theorical', None)
        if cls._is_empty_quantity(quantity):
            quantity = Decimal(0)
        else:
            quantity = Decimal(str(quantity)).quantize(Decimal("0.00001"))
        current = getattr(line, 'quantity', None)
        try:
            current = Decimal(str(current or 0)).quantize(Decimal("0.00001"))
        except Exception:
            current = Decimal(0)
        if current == quantity:
            return False
        line.quantity = quantity
        return True

    @classmethod
    def _set_initial_quantity_values(cls, values):
        if 'quantity_theorical' not in values:
            return
        quantity = values.get('quantity_theorical')
        if cls._is_empty_quantity(quantity):
            return
        quantity = Decimal(str(quantity)).quantize(Decimal("0.00001"))
        if cls._is_empty_quantity(values.get('quantity')):
            values['quantity'] = quantity
        if cls._is_empty_quantity(values.get('targeted_qt')):
            values['targeted_qt'] = quantity

    @classmethod
    def _set_quantity_counter_values_from_theorical(cls, records, values):
        if 'quantity_theorical' not in values:
            return
        if any(cls._has_physical_lot(record) for record in records):
            return
        quantity = values.get('quantity_theorical')
        if cls._is_empty_quantity(quantity):
            quantity = Decimal(0)
        else:
            quantity = Decimal(str(quantity)).quantize(Decimal("0.00001"))
        values['quantity'] = quantity
        if 'targeted_qt' not in values:
            values['targeted_qt'] = quantity

    @classmethod
    def _fresh_lines_for_quantity_consistency(cls, lines):
        try:
            return [cls(line.id) for line in lines if getattr(line, 'id', None)]
        except Exception:
            return lines

    def get_invoice_line(self, lots=None, action=None):
        lines = super().get_invoice_line(lots, action)
        if action != 'prov' or not lots:
            return lines

        padding_by_lot = {
            lot.id: Decimal(str(getattr(lot, 'sale_invoice_padding', 0) or 0))
            for lot in lots}
        for invoice_line in lines:
            if (getattr(invoice_line, 'invoice_type', None) != 'out'
                    or getattr(invoice_line, 'description', None) != 'Pro forma'
                    or Decimal(str(getattr(invoice_line, 'quantity', 0) or 0)) <= 0):
                continue
            lot = getattr(invoice_line, 'lot', None)
            lot_id = getattr(lot, 'id', lot)
            padding = padding_by_lot.get(lot_id, Decimal(0))
            if not padding:
                continue
            quantity = Decimal(str(invoice_line.quantity or 0)) + padding
            if invoice_line.unit:
                quantity = Decimal(str(invoice_line.unit.round(float(quantity))))
            invoice_line.quantity = quantity
        return lines

    @classmethod
    def default_pricing_rule(cls):
        try:
            Configuration = Pool().get('purchase_trade.configuration')
        except KeyError:
            return ''
        configurations = Configuration.search([], limit=1)
        if configurations:
            return configurations[0].pricing_rule or ''
        return ''

    @staticmethod
    def _has_invalid_delivery_period(line):
        return (
            bool(line.from_del)
            and bool(line.to_del)
            and line.from_del > line.to_del)

    @classmethod
    def _check_delivery_period_values(cls, lines, values=None):
        values = values or {}
        for line in lines:
            from_del = values.get('from_del', getattr(line, 'from_del', None))
            to_del = values.get('to_del', getattr(line, 'to_del', None))
            if from_del and to_del and from_del > to_del:
                raise UserError(
                    "Shipment period From date must be before To date.")

    @classmethod
    def _estimated_bl_relation_field(cls):
        return 'sale_line'

    @classmethod
    def _has_estimated_bl_date(cls, estimated_dates):
        return any(
            getattr(estimated, 'trigger', None) == 'bldate'
            for estimated in (estimated_dates or []))

    @classmethod
    def _values_have_estimated_bl_date(cls, values):
        for command in values.get('estimated_date') or []:
            action = command[0]
            if action == 'create':
                if any(
                        estimated.get('trigger') == 'bldate'
                        for estimated in command[1]):
                    return True
            elif action == 'write':
                actions = iter(command[1:])
                for _estimated_ids, estimated_values in zip(actions, actions):
                    if estimated_values.get('trigger') == 'bldate':
                        return True
        return False

    @classmethod
    def _delivery_period_from_values(cls, values):
        period = values.get('del_period')
        if period and not hasattr(period, 'beg_date'):
            period = Pool().get('product.month')(period)
        return period

    @classmethod
    def _default_estimated_bl_date(cls, values):
        period = cls._delivery_period_from_values(values)
        return getattr(period, 'beg_date', None)

    @classmethod
    def _set_default_estimated_bl_date_values(cls, values):
        if cls._values_have_estimated_bl_date(values):
            return
        estimated_date = cls._default_estimated_bl_date(values)
        if not estimated_date:
            return
        if values.get('estimated_date') is None:
            values['estimated_date'] = []
        elif isinstance(values['estimated_date'], tuple):
            values['estimated_date'] = list(values['estimated_date'])
        values['estimated_date'].append(('create', [{
            'trigger': 'bldate',
            'estimated_date': estimated_date,
        }]))

    @classmethod
    def _create_missing_estimated_bl_dates(cls, lines):
        Estimated = Pool().get('pricing.estimated')
        values = []
        relation_field = cls._estimated_bl_relation_field()
        for line in lines:
            if cls._has_estimated_bl_date(getattr(line, 'estimated_date', None)):
                continue
            del_period = getattr(line, 'del_period', None)
            estimated_date = getattr(del_period, 'beg_date', None)
            if not estimated_date:
                continue
            values.append({
                relation_field: line.id,
                'trigger': 'bldate',
                'estimated_date': estimated_date,
            })
        if values:
            Estimated.create(values)

    @classmethod
    def create(cls, vlist):
        regenerate_valuation = any(
            cls._should_regenerate_valuation(values) for values in vlist)
        for values in vlist:
            cls._check_delivery_period_values([cls()], values)
            cls._set_default_estimated_bl_date_values(values)
            cls._set_initial_quantity_values(values)
        lines = super().create(vlist)
        if not Transaction().context.get('_purchase_trade_skip_fee_rules'):
            Pool().get('fee.rule').apply_to_lines(
                lines, 'sale_line', auto_only=True)
        if (regenerate_valuation
                and not Transaction().context.get(
                    '_purchase_trade_skip_valuation_regeneration')):
            Valuation = Pool().get('valuation.valuation')
            with Transaction().set_context(
                    _purchase_trade_skip_valuation_regeneration=True):
                Valuation.regenerate_for_sale_lines(lines)
        return lines

    @classmethod
    @ModelView.button
    def apply_default_fees(cls, lines):
        Pool().get('fee.rule').apply_to_lines(lines, 'sale_line')

    def _check_delivery_period(self):
        if self._has_invalid_delivery_period(self):
            raise UserError(
                "Shipment period From date must be before To date.")

    @fields.depends('from_del', 'to_del')
    def on_change_from_del(self):
        self._check_delivery_period()

    @fields.depends('from_del', 'to_del')
    def on_change_to_del(self):
        self._check_delivery_period()

    del_period = fields.Many2One('product.month',"Shipment Period")
    lots = fields.One2Many('lot.lot','sale_line',"Lots",readonly=True)
    fees = fields.One2Many('fee.fee', 'sale_line', 'Fees')
    quantity_theorical = fields.Numeric("Th. quantity", digits='unit', readonly=False)
    price_type = fields.Selection([
        ('cash', 'Cash Price'),
        ('priced', 'Priced'),
        ('basis', 'Basis'),
        ('efp', 'EFP'),
    ], 'Price type')
    progress = fields.Function(fields.Float("Fix. progress", 
        states={
            'invisible': Eval('price_type') != 'basis',
            }),'get_progress')
    from_del = fields.Date("From")
    to_del = fields.Date("To")
    period_at = fields.Selection([
        (None, ''),
        ('laycan', 'Laycan'),
        ('loading', 'Loading'),
        ('discharge', 'Discharge'),
        ('crossing_border', 'Crossing Border'),
        ('title_transfer', 'Title transfer'),
        ('arrival', 'Arrival'),
    ],"Period at")
    concentration = fields.Numeric("Concentration")
    price_components = fields.One2Many('pricing.component','sale_line',"Components")
    mtm = fields.Many2Many('sale.strategy', 'sale_line', 'strategy', 'Mtm Strategy')
    derivatives = fields.One2Many('derivative.derivative','sale_line',"Derivatives")
    price_pricing = fields.One2Many('pricing.pricing','sale_line',"Pricing")
    price_summary = fields.One2Many('sale.pricing.summary','sale_line',"Summary")
    premium_decomposition = fields.One2Many(
        'premium.composition', 'sale_line', "Premium decomposition")
    charter_conditions = fields.One2Many(
        'charter.condition', 'sale_line', "Charter Conditions")
    estimated_date = fields.One2Many('pricing.estimated','sale_line',"Estimated date")
    tol_min = fields.Numeric("Tol - in %",states={
            'readonly': (Eval('inherit_tol')),
            })
    tol_max = fields.Numeric("Tol + in %",states={
            'readonly': (Eval('inherit_tol')),
            })
    tol_min_qt = fields.Numeric("Tol -",states={
            'readonly': (Eval('inherit_tol')),
            })
    tol_max_qt = fields.Numeric("Tol +",states={
            'readonly': (Eval('inherit_tol')),
            })
    inherit_tol = fields.Boolean("Inh. tol.")
    tol_min_v = fields.Function(fields.Numeric("Qt min"),'get_tol_min')
    tol_max_v = fields.Function(fields.Numeric("Qt max"),'get_tol_max')
    tolerance_used = fields.Function(
        fields.Numeric("Tolerance used"), 'get_tolerance_used')
    tolerance_min = fields.Function(
        fields.Numeric("Tolerance min"), 'get_tolerance_min')
    tolerance_max = fields.Function(
        fields.Numeric("Tolerance max"), 'get_tolerance_max')
    targeted_qt = fields.Numeric("Targeted Qt", digits='unit')
    certif = fields.Many2One('purchase.certification',"Certification",states={'readonly': (Eval('inherit_cer')),})
    # certification = fields.Selection([
    #     (None, ''),
    #     ('bci', 'BCI'),
    # ],"Certification",states={'readonly': (Eval('inherit_cer')),})
    inherit_cer = fields.Boolean("Inh. cert.")
    enable_linked_currency = fields.Boolean("Linked currencies")
    linked_price = fields.Numeric("Price", digits='unit',states={
            'invisible': (~Eval('enable_linked_currency')),
            'required': Eval('enable_linked_currency'),
            'readonly': Eval('price_type') == 'basis',
            }, depends=['enable_linked_currency', 'price_type'])
    linked_currency = fields.Many2One('currency.linked',"Currency",states={
            'invisible': (~Eval('enable_linked_currency')),
            'required': Eval('enable_linked_currency'),
            }, depends=['enable_linked_currency'])
    linked_unit = fields.Many2One('product.uom', 'Unit',states={
            'invisible': (~Eval('enable_linked_currency')),
            'required': Eval('enable_linked_currency'),
            }, depends=['enable_linked_currency'])
    premium = fields.Function(
        fields.Numeric("Premium/Discount", digits='unit', readonly=True),
        'get_premium', setter='set_premium')
    fee_ = fields.Many2One('fee.fee',"Fee")

    attributes = fields.Dict(
        'product.attribute', 'Attributes',
        domain=[
            ('sets', '=', Eval('attribute_set')),
        ],
        states={
            'readonly': ~Eval('attribute_set'),
        },
        depends=['product', 'attribute_set'],
        help="Add attributes to the variant."
    )

    attribute_set = fields.Function(
        fields.Many2One('product.attribute.set', "Attribute Set"),
        'on_change_with_attribute_set'
    )

    attributes_name = fields.Function(
        fields.Char("Attributes Name"),
        'on_change_with_attributes_name'
    )
    specification = fields.Function(
        fields.Char("Specification"), 'on_change_with_specification')

    finished = fields.Boolean("Finished")
    pricing_rule = fields.Text("Pricing description")
    price_composition = fields.One2Many('price.composition','sale_line',"Price composition")
    parent_trader = fields.Function(
        fields.Many2One('party.party', "Trader"), 'get_parent_field')
    parent_operator = fields.Function(
        fields.Many2One('party.party', "Operator"), 'get_parent_field')
    parent_broker = fields.Function(
        fields.Many2One('party.party', "Broker"), 'get_parent_field')
    parent_contract_template = fields.Function(
        fields.Many2One('contract.template', "Contract Template"),
        'get_parent_field')
    parent_invoice_party = fields.Function(
        fields.Many2One('party.party', "Invoice Party"), 'get_parent_field')
    parent_shipment_party = fields.Function(
        fields.Many2One('party.party', "Shipment Party"), 'get_parent_field')
    parent_our_reference = fields.Function(
        fields.Char("Our Reference"), 'get_parent_field')
    parent_lc_date = fields.Function(
        fields.Date("LC date"), 'get_parent_field')
    parent_product_origin = fields.Function(
        fields.Char("Origin"), 'get_parent_field')
    price_component_count = fields.Function(
        fields.Integer("Components"), 'get_relation_count')
    pricing_count = fields.Function(
        fields.Integer("Pricing"), 'get_relation_count')
    price_summary_count = fields.Function(
        fields.Integer("Summary"), 'get_relation_count')
    price_composition_count = fields.Function(
        fields.Integer("Price composition"), 'get_relation_count')
    premium_decomposition_count = fields.Function(
        fields.Integer("Premium lines"), 'get_relation_count')
    mtm_count = fields.Function(
        fields.Integer("MTM"), 'get_relation_count')
    fee_count = fields.Function(
        fields.Integer("Fees"), 'get_relation_count')
    lot_count = fields.Function(
        fields.Integer("Lots"), 'get_relation_count')
    derivative_count = fields.Function(
        fields.Integer("Derivatives"), 'get_relation_count')
    estimated_date_count = fields.Function(
        fields.Integer("Estimated dates"), 'get_relation_count')
    charter_condition_count = fields.Function(
        fields.Integer("Charter conditions"), 'get_relation_count')

    _parent_field_map = {
        'parent_trader': 'trader',
        'parent_operator': 'operator',
        'parent_broker': 'broker',
        'parent_contract_template': 'contract_template',
        'parent_invoice_party': 'invoice_party',
        'parent_shipment_party': 'shipment_party',
        'parent_our_reference': 'our_reference',
        'parent_lc_date': 'lc_date',
        'parent_product_origin': 'product_origin',
    }
    _relation_count_map = {
        'price_component_count': 'price_components',
        'pricing_count': 'price_pricing',
        'price_summary_count': 'price_summary',
        'price_composition_count': 'price_composition',
        'premium_decomposition_count': 'premium_decomposition',
        'mtm_count': 'mtm',
        'fee_count': 'fees',
        'lot_count': 'lots',
        'derivative_count': 'derivatives',
        'estimated_date_count': 'estimated_date',
        'charter_condition_count': 'charter_conditions',
    }

    def get_parent_field(self, name=None):
        field_name = self._parent_field_map.get(name)
        if not field_name or not self.sale:
            return None
        value = getattr(self.sale, field_name, None)
        if hasattr(value, 'id'):
            return value.id
        return value

    def get_relation_count(self, name=None):
        field_name = self._relation_count_map.get(name)
        if not field_name:
            return 0
        return len(getattr(self, field_name, None) or [])

    @classmethod
    def default_finished(cls):
        return False
    
    @property
    def report_fixing_rule(self):
        pricing_rule = ''
        if self.pricing_rule:
            pricing_rule = self.pricing_rule
        return pricing_rule

    @property
    def get_pricing_text(self):
        parts = []
        if self.price_components:
            for pc in self.price_components:
                if pc.price_index:
                    price_desc = pc.price_index.price_desc or ''
                    period_desc = (
                        pc.price_index.price_period.description
                        if pc.price_index.price_period else '') or ''
                    part = ' '.join(
                        piece for piece in ['ON', price_desc, period_desc]
                        if piece)
                    if part:
                        parts.append(part)
        return ' '.join(parts)
    
    @fields.depends('product')
    def on_change_with_attribute_set(self, name=None):
        if self.product and self.product.template and self.product.template.attribute_set:
            return self.product.template.attribute_set.id

    @fields.depends(
        'product', 'attributes',
        'coffee_origin', 'coffee_type', 'coffee_process', 'coffee_variety',
        'coffee_crop_year', 'coffee_screen_size', 'coffee_moisture_max',
        'coffee_defect_max', 'coffee_cup_score_min')
    def on_change_with_attributes_name(self, name=None):
        coffee_attributes_name = getattr(
            self, '_coffee_attributes_name', lambda: None)()
        if coffee_attributes_name:
            return coffee_attributes_name
        if not self.product or not self.product.attribute_set or not self.attributes:
            return

        def key(attribute):
            return getattr(attribute, 'sequence', attribute.name)

        values = []
        for attribute in sorted(self.product.attribute_set.attributes, key=key):
            if attribute.name in self.attributes:
                value = self.attributes[attribute.name]
                values.append(gettext(
                    'product_attribute.msg_label_value',
                    label=attribute.string,
                    value=attribute.format(value)
                ))
        return " | ".join(filter(None, values))

    @fields.depends(
        'product', 'attributes',
        'coffee_origin', 'coffee_type', 'coffee_process', 'coffee_variety',
        'coffee_crop_year', 'coffee_screen_size', 'coffee_moisture_max',
        'coffee_defect_max', 'coffee_cup_score_min')
    def on_change_with_specification(self, name=None):
        return self.on_change_with_attributes_name(name)

    @fields.depends(
        'quantity_theorical', 'quantity', 'lots', 'targeted_qt',
        methods=['_recompute_trade_price_fields'])
    def on_change_quantity_theorical(self):
        if self._sync_quantity_counter_from_theorical(self):
            self._recompute_trade_price_fields()
        self.targeted_qt = self._tolerance_theoretical_quantity()
    
    @classmethod
    def default_price_type(cls):
        return 'priced'

    @classmethod
    def default_inherit_tol(cls):
        return True

    @classmethod
    def default_enable_linked_currency(cls):
        return False

    @classmethod
    def default_inherit_cer(cls):
        return True
 
    def get_matched_lines(self):
        if self.lots:
            LotQt = Pool().get('lot.qt')
            return LotQt.search([('lot_s','=',self.lots[0].id),('lot_p','>',0)])

    def get_date(self,trigger_event):
        if trigger_event == 'bldate':
            real_bl_date = self._get_real_bl_date()
            if real_bl_date:
                return real_bl_date
        trigger_date = None
        if self.estimated_date:
            logger.info("ESTIMATED_DATE:%s",self.estimated_date)
            trigger_date = [d.estimated_date for d in self.estimated_date if d.trigger == trigger_event]
            logger.info("TRIGGER_DATE:%s",trigger_date)
            logger.info("TRIGGER_EVENT:%s",trigger_event)
            trigger_date =  trigger_date[0] if trigger_date else None    
        return trigger_date

    def _get_real_bl_date(self):
        bl_dates = []
        for lot in self.lots or []:
            shipment = getattr(lot, 'lot_shipment_in', None)
            bl_date = getattr(shipment, 'bl_date', None)
            if bl_date:
                bl_dates.append(bl_date)
        if not bl_dates:
            for match in self.get_matched_lines() or []:
                shipment = getattr(match, 'lot_shipment_in', None)
                bl_date = getattr(shipment, 'bl_date', None)
                if bl_date:
                    bl_dates.append(bl_date)
        # Multiple BL dates on one line should not happen; use the first one
        # for now and revisit when partial shipments need split maturity/pricing.
        return bl_dates[0] if bl_dates else None

    def get_tol_min(self,name):
        if self.inherit_tol:
            if self.sale.tol_min and self.quantity_theorical:
                return round((1-(self.sale.tol_min/100))*Decimal(self.quantity_theorical),3)
        else:
            if self.tol_min and self.quantity_theorical:
                return round((1-(self.tol_min/100))*Decimal(self.quantity_theorical),3)

    def get_tol_max(self,name):
        if self.inherit_tol:
            if self.sale.tol_max and self.quantity_theorical:
                return round((1+(self.sale.tol_max/100))*Decimal(self.quantity_theorical),3)
        else:
            if self.tol_max and self.quantity_theorical:
                return round((1+(self.tol_max/100))*Decimal(self.quantity_theorical),3)

    def _line_quantity_in_unit(self, quantity, from_unit, to_unit):
        if not from_unit or not to_unit or from_unit == to_unit:
            return abs(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 abs(Decimal(str(Uom.compute_qty(
            from_unit, float(quantity or 0), to_unit, True, factor, rate))))

    def _tolerance_theoretical_quantity(self):
        return abs(Decimal(str(
            getattr(self, 'quantity_theorical', None)
            or getattr(self, 'quantity', None)
            or 0)))

    def _tolerance_actual_quantity(self):
        LotQt = Pool().get('lot.qt')
        unit = getattr(self, 'unit', None)
        physical_lots = [
            lot for lot in getattr(self, 'lots', None) or []
            if getattr(lot, 'lot_type', None) == 'physic']
        if physical_lots:
            return sum(
                abs(Decimal(str(
                    lot.get_current_quantity_converted(unit=unit) or 0)))
                for lot in physical_lots)
        quantity = Decimal(0)
        for lot in getattr(self, 'lots', None) or []:
            if getattr(lot, 'lot_type', None) != 'virtual':
                continue
            for lqt in LotQt.search([('lot_s', '=', lot.id)]):
                quantity += self._line_quantity_in_unit(
                    getattr(lqt, 'lot_quantity', 0),
                    getattr(lqt, 'lot_unit', None),
                    unit)
        return quantity

    def _tolerance_targeted_quantity(self):
        targeted = getattr(self, 'targeted_qt', None)
        if not self._is_empty_quantity(targeted):
            return abs(Decimal(str(targeted)))
        return self._tolerance_theoretical_quantity()

    @fields.depends('targeted_qt', 'quantity_theorical', 'quantity')
    def get_tolerance_used(self, name):
        theoretical = self._tolerance_theoretical_quantity()
        if not theoretical:
            return Decimal(0)
        return round(
            ((self._tolerance_targeted_quantity() - theoretical)
                / theoretical) * Decimal(100), 5)

    def get_tolerance_min(self, name):
        sale = getattr(self, 'sale', None)
        if self.inherit_tol and sale:
            return -Decimal(str(sale.tol_min or 0))
        return -Decimal(str(self.tol_min or 0))

    def get_tolerance_max(self, name):
        sale = getattr(self, 'sale', None)
        if self.inherit_tol and sale:
            return Decimal(str(sale.tol_max or 0))
        return Decimal(str(self.tol_max or 0))

    def check_targeted_qt_tolerance(self):
        theoretical = self._tolerance_theoretical_quantity()
        if not theoretical:
            return
        if self._is_empty_quantity(getattr(self, 'targeted_qt', None)):
            self.targeted_qt = theoretical
        targeted = self._tolerance_targeted_quantity()
        tol_min = -self.get_tolerance_min('tolerance_min')
        tol_max = self.get_tolerance_max('tolerance_max')
        min_qt = theoretical * (Decimal(1) - tol_min / Decimal(100))
        max_qt = theoretical * (Decimal(1) + tol_max / Decimal(100))
        if targeted < min_qt:
            raise UserError(
                "Targeted Qt is below the tolerance minimum.")
        if targeted > max_qt:
            raise UserError(
                "Targeted Qt exceeds the tolerance maximum.")

    def get_progress(self,name):
        PS = Pool().get('sale.pricing.summary')
        ps = PS.search(['sale_line','=',self.id])
        if ps:
            if not self.price_components:
                manual = [e for e in ps if not e.price_component]
                if manual:
                    return manual[0].progress or 0
            return sum((e.progress if e.progress else 0) * (e.ratio if e.ratio else 0) / 100 for e in ps)

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

    def _get_linked_unit_factor(self):
        if not (self.enable_linked_currency and self.linked_currency):
            return None
        factor = Decimal(self.linked_currency.factor or 0)
        if not factor:
            return None
        unit_factor = Decimal(1)
        if self.linked_unit:
            source_unit = getattr(self, 'unit', None)
            product = getattr(self, 'product', None)
            if not source_unit and product:
                source_unit = product.sale_uom
            if not source_unit:
                return factor
            Uom = Pool().get('product.uom')
            unit_factor = Decimal(str(
                Uom.compute_qty(source_unit, float(1), self.linked_unit) or 0))
        return factor * unit_factor

    def _linked_to_line_price(self, price):
        factor = self._get_linked_unit_factor()
        price = Decimal(price or 0)
        if not factor:
            return price
        return round(price * factor, 4)

    def _line_to_linked_price(self, price):
        factor = self._get_linked_unit_factor()
        price = Decimal(price or 0)
        if not factor:
            return price
        return round(price / factor, 4)

    def get_premium(self, name=None):
        return round(sum(
            Decimal(str(getattr(premium, 'base_amount', None)
                if getattr(premium, 'base_amount', None) is not None
                else premium.get_base_amount('base_amount') or 0))
            for premium in (self.premium_decomposition or [])), 4)

    def get_effective_charter_conditions(self):
        if self.charter_conditions:
            return list(self.charter_conditions)
        sale = getattr(self, 'sale', None)
        return list(getattr(sale, 'charter_conditions', None) or [])

    @classmethod
    def set_premium(cls, lines, name, value):
        Premium = Pool().get('premium.composition')
        value = Decimal(str(value or 0))
        for line in lines:
            existing = Premium.search([('sale_line', '=', line.id)])
            if existing:
                Premium.delete(existing)
            if value:
                Premium.create([{
                    'sale_line': line.id,
                    'premium': value,
                    'currency': (
                        line.sale.currency.id
                        if line.sale and line.sale.currency else None),
                }])

    def _get_premium_price(self):
        premium = getattr(self, 'premium', None)
        if premium is None:
            premium = self.get_premium('premium')
        return Decimal(str(premium or 0))

    def _get_amount_quantity(self):
        if getattr(self, 'finished', False):
            weight_basis_quantity = self._get_weight_basis_quantity()
            if weight_basis_quantity is not None:
                return weight_basis_quantity
            quantity = getattr(self, 'quantity', None)
            if quantity is None:
                quantity = getattr(self, 'quantity_theorical', None)
            return quantity
        quantity = getattr(self, 'quantity_theorical', None)
        if quantity is None:
            quantity = getattr(self, 'quantity', None)
        return quantity

    def _get_weight_basis_quantity(self):
        physical_lots = [
            lot for lot in (getattr(self, 'lots', None) or [])
            if getattr(lot, 'lot_type', None) == 'physic']
        if not physical_lots:
            return None
        wb = getattr(getattr(self, 'sale', None), 'wb', None)
        qt_type = getattr(wb, 'qt_type', None)
        if not qt_type:
            return None
        state_id = getattr(qt_type, 'id', qt_type)
        quantity = Decimal(0)
        for lot in physical_lots:
            hist_types = [
                getattr(hist, 'quantity_type', None)
                for hist in (getattr(lot, 'lot_hist', None) or [])]
            hist_type_ids = [getattr(hist_type, 'id', hist_type)
                for hist_type in hist_types]
            if qt_type not in hist_types and state_id not in hist_type_ids:
                return None
            quantity += Decimal(str(
                lot.get_current_quantity_converted(state_id, self.unit) or 0))
        return quantity

    def get_price(self,lot_premium=0):
        return round(
            Decimal(self.unit_price or 0)
            + Decimal(lot_premium or 0),
            4)
    
    def _get_basis_component_price(self):
        price = Decimal(0)
        if not self.price_components:
            PP = Pool().get('sale.pricing.summary')
            pp = PP.search([
                ('sale_line', '=', self.id),
                ('price_component', '=', None),
            ], limit=1)
            if pp:
                return round(Decimal(pp[0].price or 0), 4)
        for pc in self.price_components:
            PP = Pool().get('sale.pricing.summary')
            pp = PP.search([('price_component','=',pc.id),('sale_line','=',self.id)])
            if pp:
                price += pp[0].price * (pc.ratio / 100)
        return round(price,4)

    def get_basis_price(self):
        return round(self._get_basis_component_price(), 4)

    def sync_linked_price_from_basis(self):
        if self.enable_linked_currency and self.linked_currency:
            self.linked_price = self._line_to_linked_price(
                self._get_basis_component_price())

    def get_price_linked_currency(self,lot_premium=0):
        return round(
            self._linked_to_line_price(
                Decimal(self.linked_price or 0)
                + Decimal(lot_premium or 0)),
            4)

    @fields.depends('product', 'quantity', 'unit_price',
        methods=['_get_context_sale_price'])
    def compute_unit_price(self):
        unit_price = super().compute_unit_price()
        if unit_price is None:
            unit_price = self.unit_price
        return unit_price

    @fields.depends('id','unit','quantity','unit_price','price_pricing','price_type','price_components','estimated_date','lots','fees','enable_linked_currency','linked_price','linked_currency','linked_unit','premium_decomposition')
    def on_change_with_unit_price(self, name=None):
        Date = Pool().get('ir.date')
        logger.info("ONCHANGEUNITPRICE:%s",self.unit_price)
        if self.price_type == 'basis':
            self.sync_linked_price_from_basis()
            logger.info("ONCHANGEUNITPRICE_IN:%s",self.get_basis_price())
            return self.get_basis_price()
        if self.enable_linked_currency and self.linked_price and self.linked_currency and self.price_type == 'priced':
            return self.get_price_linked_currency()
        if self.price_type == 'efp':
            if hasattr(self, 'derivatives') and self.derivatives:
                for d in self.derivatives:
                    return d.price_index.get_price(Date.today(),self.unit,self.sale.currency,True)
        return self.get_price()

    @fields.depends(
        'type', 'quantity', 'quantity_theorical', 'finished',
        'unit_price', 'unit', 'product', 'lots',
        'sale', '_parent_sale.currency', '_parent_sale.wb',
        'premium_decomposition', 'enable_linked_currency',
        'linked_currency', 'linked_unit')
    def on_change_with_amount(self):
        if self.type == 'line':
            currency = self.sale.currency if self.sale else None
            amount = Decimal(str(self._get_amount_quantity() or 0)) * (
                Decimal(self.unit_price or 0) + self._get_premium_price())
            if currency:
                return currency.round(amount)
            return amount
        return Decimal(0)

    @fields.depends(
        'unit', 'product', 'price_type', 'enable_linked_currency',
        'linked_currency', 'linked_unit', 'linked_price',
        'premium_decomposition',
        methods=['on_change_with_unit_price', 'on_change_with_amount'])
    def _recompute_trade_price_fields(self):
        self.unit_price = self.on_change_with_unit_price()
        self.amount = self.on_change_with_amount()

    @fields.depends(methods=['_recompute_trade_price_fields'])
    def on_change_premium(self):
        self._recompute_trade_price_fields()

    @fields.depends(methods=['_recompute_trade_price_fields'])
    def on_change_premium_decomposition(self):
        self._recompute_trade_price_fields()

    @fields.depends(methods=['_recompute_trade_price_fields'])
    def on_change_price_type(self):
        self._recompute_trade_price_fields()

    @fields.depends(methods=['_recompute_trade_price_fields'])
    def on_change_enable_linked_currency(self):
        self._recompute_trade_price_fields()

    @fields.depends(methods=['_recompute_trade_price_fields'])
    def on_change_linked_price(self):
        self._recompute_trade_price_fields()

    @fields.depends(methods=['_recompute_trade_price_fields'])
    def on_change_linked_currency(self):
        self._recompute_trade_price_fields()

    @fields.depends(methods=['_recompute_trade_price_fields'])
    def on_change_linked_unit(self):
        self._recompute_trade_price_fields()

    def check_from_to(self,tr):
        if tr.pricing_period:
            date_from,date_to, d, include, dates = tr.getDateWithEstTrigger(1)
            if date_from:
                tr.from_p = date_from.date()
            if date_to:
                tr.to_p = date_to.date()
        if tr.application_period:
            date_from,date_to, d, include, dates = tr.getDateWithEstTrigger(2)
            if date_from:
                tr.from_a = date_from.date()
            if date_to:
                tr.to_a = date_to.date()
        TR = Pool().get('pricing.trigger')
        TR.save([tr])

    def check_pricing(self):
        if self.price_components:
            for pc in self.price_components:
                if not pc.auto:
                    Pricing = Pool().get('pricing.pricing')
                    pricings = Pricing.search(['price_component','=',pc.id],order=[('pricing_date', 'ASC')])
                    if pricings:
                        Pricing._sync_manual_values(pricings)
                        Pricing._sync_manual_last(pricings)
                        Pricing._sync_eod_price(pricings)
                            
                if pc.triggers and pc.auto:
                    prDate = []
                    prPrice = [] 
                    apDate = [] 
                    apPrice = []
                    calendar = pc.get_calendar()
                    for t in pc.triggers:
                        prD, prP = t.getPricingListDates(calendar)
                        apD, apP = t.getApplicationListDates(calendar)
                        prDate.extend(prD)
                        prPrice.extend(prP)
                        apDate.extend(apD)
                        apPrice.extend(apP)
                    if pc.quota_sale:
                        prPrice = self.get_avg(prPrice)
                        self.generate_pricing(pc,apDate,prPrice)

    def get_avg(self,lprice):
        l = len(lprice)
        if l > 0 :
            cumulprice = Decimal(0)
            i = 1
            for p in lprice:
                if i > 1:
                    p['avg_minus_1'] = cumulprice / (i-1)
                p['price'] = Decimal(str(p['price'] or 0))
                cumulprice += p['price']
                p['avg'] = cumulprice / i
                i += 1
            return lprice

    def getnearprice(self,pl,d,t,max_date=None):
        if pl:
            pl_sorted = sorted(pl, key=lambda x: x['date'])
            pminus = pl_sorted[0]
            if not max_date:
                max_date = d.date()
            for p in pl_sorted:
                if p['date'].date() == d.date():
                    if p['isAvg'] and t == 'avg':
                        return p[t]
                    if not p['isAvg'] and t == 'avg':
                        return p['price']
                elif p['date'].date() > d.date():
                    if pminus != p:
                        return pminus[t]
                    else:
                        return Decimal(0)
                pminus = p
            return pl_sorted[len(pl)-1][t]
        return Decimal(0)

    def _get_pricing_base_quantity(self):
        quantity = self.quantity_theorical
        if quantity is None:
            quantity = self.quantity
        return Decimal(str(quantity or 0))

    def generate_pricing(self,pc,dl,pl):
        Pricing = Pool().get('pricing.pricing')
        pricing = Pricing.search(['price_component','=',pc.id])
        if pricing:
            Pricing.delete(pricing)
        base_quantity = self._get_pricing_base_quantity()
        cumul_qt = 0
        index = 0
        dl_sorted = sorted(dl)
        for d in dl_sorted:
            if pc.pricing_date and d.date() > pc.pricing_date:
                break
            p = Pricing()
            p.sale_line = self.id
            logger.info("GENEDATE:%s",d)
            logger.info("TYPEDATE:%s",type(d))     
            p.pricing_date = d.date()
            p.price_component = pc.id
            p.quantity = round(Decimal(pc.quota_sale),4)
            price = round(Decimal(self.getnearprice(pl,d,'price',pc.pricing_date)),4)
            p.settl_price = price
            if price > 0:
                cumul_qt += pc.quota_sale
            p.fixed_qt = round(Decimal(cumul_qt),4)
            p.fixed_qt_price = round(Decimal(self.getnearprice(pl,d,'avg',pc.pricing_date)),4)
            #p.fixed_qt_price = p.get_fixed_price()
            if p.fixed_qt_price == 0:
                p.fixed_qt_price = round(Decimal(self.getnearprice(pl,d,'avg_minus_1',pc.pricing_date)),4)
            p.unfixed_qt = round(base_quantity - Decimal(cumul_qt),4)
            if p.unfixed_qt < 0.001:
                p.unfixed_qt = Decimal(0)
                p.fixed_qt = base_quantity
            if price > 0:
                logger.info("GENERATE_1:%s",price)
                p.unfixed_qt_price = price
            else:
                pr = Decimal(pc.get_price(
                    p.pricing_date, self.unit, self.sale.currency, True))
                pr = round(pr,4)
                logger.info("GENERATE_2:%s",pr)
                p.unfixed_qt_price = pr
            p.eod_price = p.get_eod_price_sale()
            if (index == len(dl)-1) or (pc.pricing_date and (index < len(dl)-1 and dl_sorted[index+1].date() > pc.pricing_date)):
                p.last = True
            logger.info("GENERATE_3:%s",p.unfixed_qt_price)
            Pricing.save([p])
            index += 1

    @classmethod
    def _valuation_regeneration_fields(cls):
        return {
            'quantity', 'quantity_theorical', 'unit', 'unit_price',
            'price_type', 'premium', 'linked_price', 'linked_currency',
            'linked_unit', 'price_pricing', 'price_components', 'derivatives',
            'fees', 'lots', 'product', 'currency',
        }

    @classmethod
    def _should_regenerate_valuation(cls, values):
        return bool(cls._valuation_regeneration_fields() & set(values))

    @classmethod
    def write(cls, *args):
        actions = iter(args)
        args = []
        valuation_line_ids = set()
        for records, values in zip(actions, actions):
            cls._check_delivery_period_values(records, values)
            if cls._should_regenerate_valuation(values):
                valuation_line_ids.update(record.id for record in records)
            args.extend((records, values))

        old_values = {}

        for records, values in zip(args[::2], args[1::2]):
            if 'quantity_theorical' in values:
                for record in records:
                    old_values[record.id] = record.quantity_theorical
                cls._set_quantity_counter_values_from_theorical(
                    records, values)

        super().write(*args)

        lines = sum(args[::2], [])
        for line in lines:
            if line.id not in old_values:
                continue
            if old_values[line.id] is None:
                continue
            old = Decimal(old_values[line.id] or 0)
            new = Decimal(line.quantity_theorical or 0)
            delta = new - old
            if delta == 0:
                continue

            virtual_lots = [
                lot for lot in (line.lots or [])
                if lot.lot_type == 'virtual'
            ]
            if not virtual_lots:
                continue

            vlot = virtual_lots[0]
            physical_quantity = sum(
                Decimal(lot.get_current_quantity_converted() or 0)
                for lot in (line.lots or [])
                if lot.lot_type == 'physic')
            target_quantity = round(new - physical_quantity, 5)
            if target_quantity < 0:
                raise UserError("Please unlink or unmatch lot")
            cls._sync_open_lot_quantity(line, vlot, target_quantity)
            for fee in line.fees or []:
                fee.sync_quantity_from_lots()
                fee.adjust_purchase_values()

        Pool().get('lot.lot').assert_lines_quantity_consistency(
            cls._fresh_lines_for_quantity_consistency(lines))
        cls._create_missing_estimated_bl_dates(lines)
        if not Transaction().context.get('_purchase_trade_skip_fee_rules'):
            Pool().get('fee.rule').apply_to_lines(
                lines, 'sale_line', auto_only=True)
        if (valuation_line_ids
                and not Transaction().context.get(
                    '_purchase_trade_skip_valuation_regeneration')):
            Valuation = Pool().get('valuation.valuation')
            with Transaction().set_context(
                    _purchase_trade_skip_valuation_regeneration=True):
                Valuation.regenerate_for_sale_lines(
                    cls.browse(list(valuation_line_ids)))

    @classmethod
    def _sync_open_lot_quantity(cls, line, vlot, target_quantity):
        Lot = Pool().get('lot.lot')
        LotQt = Pool().get('lot.qt')

        free_lqts = LotQt.search([
            ('lot_s', '=', vlot.id),
            ('lot_p', '=', None),
            ('lot_shipment_in', '=', None),
            ('lot_shipment_internal', '=', None),
            ('lot_shipment_out', '=', None),
        ])
        allocated_lqts = LotQt.search([
            ('lot_s', '=', vlot.id),
            [
                'OR',
                ('lot_p', '!=', None),
                ('lot_shipment_in', '!=', None),
                ('lot_shipment_internal', '!=', None),
                ('lot_shipment_out', '!=', None),
            ],
        ])
        allocated_quantity = sum(
            Decimal(lqt.lot_quantity or 0) for lqt in allocated_lqts)
        free_quantity = round(target_quantity - allocated_quantity, 5)
        logger.info(
            "SALE_QTY_SYNC line=%s vlot=%s target=%s allocated=%s "
            "free=%s free_lqts=%s",
            getattr(line, 'id', None),
            getattr(vlot, 'id', None),
            target_quantity,
            allocated_quantity,
            free_quantity,
            [getattr(lqt, 'id', None) for lqt in free_lqts])
        if free_quantity < 0:
            raise UserError("Please unlink or unmatch lot")

        if free_lqts:
            lqt = free_lqts[0]
            logger.info(
                "SALE_QTY_SYNC_FREE_BEFORE lqt=%s quantity=%s new=%s",
                getattr(lqt, 'id', None),
                getattr(lqt, 'lot_quantity', None),
                free_quantity)
            lqt.lot_quantity = free_quantity
            LotQt.save([lqt])
            logger.info(
                "SALE_QTY_SYNC_FREE_AFTER lqt=%s quantity=%s",
                getattr(lqt, 'id', None),
                getattr(LotQt(getattr(lqt, 'id')), 'lot_quantity', None))
        elif free_quantity > 0:
            lqt = LotQt()
            lqt.lot_p = None
            lqt.lot_s = vlot.id
            lqt.lot_quantity = free_quantity
            lqt.lot_unit = line.unit
            LotQt.save([lqt])

        current_quantity = round(
            Decimal(vlot.get_current_quantity_converted() or 0), 5)
        if current_quantity != target_quantity:
            vlot.set_current_quantity(target_quantity, target_quantity, 1)
            Lot.save([vlot])

    @classmethod
    def delete(cls, lines):
        pool = Pool()
        LotQt = pool.get('lot.qt')
        Valuation = pool.get('valuation.valuation')
        OpenPosition = pool.get('open.position')
        for line in lines:
            if line.lots:
                vlot_s = line.lots[0].getVlot_s()
                lqts = LotQt.search([('lot_s','=',vlot_s.id),('lot_p','!=',None),('lot_quantity','>',0)])
                if lqts:
                    raise UserError("You cannot delete matched sale")
                    return
                lqts = LotQt.search([('lot_s','=',vlot_s.id)])
                LotQt.delete(lqts)
                valuations = Valuation.search([('lot','in',line.lots)])
                if valuations:
                    Valuation.delete(valuations)
            # op = OpenPosition.search(['sale_line','=',line.id])
            # if op:
            #     OpenPosition.delete(op)

        super(SaleLine, cls).delete(lines)
    
    @classmethod
    def copy(cls, lines, default=None):
        if default is None:
            default = {}
        else:
            default = default.copy()
        default.setdefault('lots', None)
        default.setdefault('quantity', Decimal(0))
        default.setdefault('quantity_theorical', None)
        default.setdefault('price_pricing', None)
        return super().copy(lines, default=default)

    @classmethod
    def _sync_virtual_lot_packing(cls, line):
        Lot = Pool().get('lot.lot')
        if not getattr(line, 'lots', None):
            line = cls(line.id)
        virtual_lots = [
            lot for lot in (line.lots or [])
            if getattr(lot, 'lot_type', None) == 'virtual']
        if not virtual_lots:
            return
        lot = virtual_lots[0]
        packing_count = getattr(line, 'coffee_packing_count', None)
        packing_unit = getattr(line, 'coffee_packing_unit', None)
        lot_unit_id = getattr(lot.lot_unit, 'id', lot.lot_unit)
        packing_unit_id = getattr(packing_unit, 'id', packing_unit)
        if lot.lot_qt == packing_count and lot_unit_id == packing_unit_id:
            return
        lot.lot_qt = packing_count
        lot.lot_unit = packing_unit
        Lot.save([lot])

    @classmethod
    def _auto_hedge_configuration(cls):
        Configuration = Pool().get('sale.configuration')
        configurations = Configuration.search([], limit=1)
        if configurations:
            return configurations[0]

    @classmethod
    def _auto_hedge_contract_count(cls, line, over_hedge=False):
        price_index = getattr(line, 'coffee_market_reference', None)
        if not price_index or not getattr(line, 'unit', None):
            return 0
        quantity = Decimal(str(
            getattr(line, 'quantity_theorical', None)
            or getattr(line, 'quantity', None)
            or 0))
        if quantity <= 0:
            return 0
        contract_quantity = Decimal(str(price_index.get_qt(1, line.unit) or 0))
        if contract_quantity <= 0:
            return 0
        rounding = ROUND_CEILING if over_hedge else ROUND_FLOOR
        return int((quantity / contract_quantity).to_integral_value(
            rounding=rounding))

    @classmethod
    def _ensure_auto_hedge_derivative(cls, line):
        config = cls._auto_hedge_configuration()
        if not config or not getattr(config, 'auto_hedging', False):
            return
        if getattr(line, 'derivatives', None):
            return
        price_index = getattr(line, 'coffee_market_reference', None)
        nb_ct = cls._auto_hedge_contract_count(
            line, over_hedge=getattr(config, 'auto_hedging_over', False))
        if not price_index or nb_ct <= 0:
            return
        Derivative = Pool().get('derivative.derivative')
        Date = Pool().get('ir.date')
        quantity = price_index.get_qt(nb_ct, line.unit)
        Derivative.create([{
            'sale': line.sale.id if line.sale else None,
            'sale_line': line.id,
            'product': line.product.id if line.product else None,
            'party': line.sale.party.id
                if line.sale and line.sale.party else None,
            'price_index': price_index.id,
            'nb_ct': nb_ct,
            'price': getattr(line, 'coffee_market_price', None),
            'direction': 'long',
            'trade_date': Date.today(),
            'open_qty': quantity,
            }])
        
    @classmethod
    def validate(cls, salelines):
        LotQtHist = Pool().get('lot.qt.hist')
        LotQtType = Pool().get('lot.qt.type')            
        Pnl = Pool().get('valuation.valuation')
        super(SaleLine, cls).validate(salelines)
        for line in salelines:
            if cls._has_invalid_delivery_period(line):
                raise UserError(
                    "Shipment period From date must be before To date.")
            if line.price_components:
                for pc in line.price_components:
                    if pc.triggers:
                        for tr in pc.triggers:
                            line.check_from_to(tr)
            line.check_pricing()
            line.check_targeted_qt_tolerance()
            #no lot need to create one with line quantity
            if not line.created_by_code:
                if cls._sync_initial_quantity_from_theorical(line):
                    cls.save([line])
                if (not line.lots and line.product.type != 'service'
                        and line.quantity_theorical
                        and Decimal(str(line.quantity_theorical or 0)) != Decimal(0)):
                    FeeLots = Pool().get('fee.lots')
                    LotQtHist = Pool().get('lot.qt.hist')
                    LotQtType = Pool().get('lot.qt.type')
                    Lot = Pool().get('lot.lot')
                    lot = Lot()
                    lot.sale_line = line.id
                    lot.lot_qt = line.quantity_theorical
                    lot.lot_unit_line = line.unit
                    lot.lot_quantity = Decimal(
                        str(line.quantity_theorical)).quantize(
                            Decimal("0.00001"))
                    lot.lot_status = 'forecast'
                    lot.lot_type = 'virtual'
                    lot.lot_product = line.product
                    lqtt = LotQtType.search([('sequence','=',1)])
                    if lqtt:
                        lqh = LotQtHist()
                        lqh.quantity_type = lqtt[0]
                        lqh.quantity = lot.lot_quantity
                        lqh.gross_quantity = lot.lot_quantity
                        lot.lot_hist = [lqh]
                    if Decimal(str(line.quantity_theorical or 0)) > 0:
                        Lot.save([lot])
                    #check if fees need to be updated
                    if line.fees:
                        for fee in line.fees:
                            fl_check = FeeLots.search([('fee','=',fee.id),('lot','=',lot.id),('sale_line','=',line.id)])
                            if not fl_check:
                                fl = FeeLots()
                                fl.fee = fee.id  
                                fl.lot = lot.id
                                fl.sale_line = line.id
                                FeeLots.save([fl])
                cls._sync_virtual_lot_packing(line)
                cls._ensure_auto_hedge_derivative(line)
                
            #generate valuation for purchase and sale
            LotQt = Pool().get('lot.qt')
            line = cls(line.id)
            generated_purchase_side = False
            if line.lots:
                for lot in line.lots:
                    lqts = LotQt.search([('lot_s','=',lot.id),('lot_p','>',0)])
                    if lqts:
                        generated_purchase_side = True
                        purchase_lines = [e.lot_p.line for e in lqts]
                        if purchase_lines:
                            for pl in purchase_lines:
                                Pnl.generate(pl)
            if line.lots and not generated_purchase_side:
                Pnl.generate_from_sale_line(line)

class SaleCreatePurchase(Wizard):
    "Create mirror purchase"
    __name__ = "sale.create.mirror"

    start = StateTransition()

    purchase = StateView(
        'sale.create.input',
        'purchase_trade.create_purchase_view_form', [
        Button("Cancel", 'end', 'tryton-cancel'),
        Button("Create", 'creating', 'tryton-ok', default=True),
        ])

    creating = StateTransition()
    
    def transition_start(self):
        return 'purchase'

    def transition_creating(self):
        Purchase = Pool().get('purchase.purchase')
        PL = Pool().get('purchase.line')
        LotQt = Pool().get('lot.qt')
        p = None
        pl = None
        for r in self.records:
            if r.lines:
                p = Purchase()
                p.party = self.purchase.party
                p.incoterm = self.purchase.incoterm
                p.payment_term = self.purchase.payment_term
                p.from_location = self.purchase.from_location
                p.to_location = self.purchase.to_location
                Purchase.save([p])
                pl = PL()
                pl.quantity = r.lines[0].quantity
                pl.unit = r.lines[0].unit
                pl.product = r.lines[0].product
                pl.unit_price = self.purchase.unit_price
                pl.currency = self.purchase.currency
                pl.purchase = p.id
                PL.save([pl])
        #Match if requested
        if self.purchase.match:
            #Increase forecasted virtual part matched
            if pl:
                if pl.lots:
                    qt = Decimal(pl.quantity)
                    vlot_p = pl.getVirtualLot()
                    vlot_s = self.records[0].lines[0].getVirtualLot()
                    if not vlot_p.updateVirtualPart(None,qt,vlot_p,None,vlot_s):
                        vlot_p.createVirtualPart(qt,pl.unit,vlot_p,None,vlot_s)
                    #Decrease forecasted virtual part non matched
                    lqts = LotQt.search([('lot_p','=',vlot_p)])
                    if lqts:
                        vlot_p.updateVirtualPart(lqts[0],-qt)
                    lqts = LotQt.search([('lot_s','=',vlot_s)])
                    if lqts:
                        vlot_p.updateVirtualPart(lqts[0],-qt)
        return 'end'

    def end(self):
        return 'reload'

class SaleCreatePurchaseInput(ModelView):
    "Create purchase mirror"
    __name__ = "sale.create.input"
    party = fields.Many2One('party.party',"Supplier")
    incoterm = fields.Many2One('incoterm.incoterm',"Incoterm",domain=[('location', '=', False)])
    payment_term = fields.Many2One('account.invoice.payment_term', "Payment Term")
    from_location = fields.Many2One('stock.location',"From location")
    to_location = fields.Many2One('stock.location',"To location")
    unit_price = fields.Numeric("Price")
    currency = fields.Many2One('currency.currency',"Currency")
    match = fields.Boolean("Match open quantity")

class Derivative(metaclass=PoolMeta):
    "Derivative"
    __name__ = 'derivative.derivative'

    sale = fields.Many2One('sale.sale',"Sale")
    sale_line = fields.Many2One('sale.line',"Line")

class Valuation(metaclass=PoolMeta):
    "Valuation"
    __name__ = 'valuation.valuation'

    sale = fields.Many2One('sale.sale',"Sale")
    sale_line = fields.Many2One('sale.line',"Line")

class ValuationLine(metaclass=PoolMeta):
    "Last Valuation"
    __name__ = 'valuation.valuation.line'

    sale = fields.Many2One('sale.sale',"Sale")
    sale_line = fields.Many2One('sale.line',"Line")

class ValuationReport(metaclass=PoolMeta):
    "Valuation Report"
    __name__ = 'valuation.report'

    sale = fields.Many2One('sale.sale',"Sale")
    sale_line = fields.Many2One('sale.line',"Line")

class ValuationDyn(metaclass=PoolMeta):
    "Valuation"
    __name__ = 'valuation.valuation.dyn'

    r_sale = fields.Many2One('sale.sale',"Sale")
    r_sale_line = fields.Many2One('sale.line',"Line")

    @classmethod
    def table_query(cls):
        Valuation = Pool().get('valuation.valuation')
        val = Valuation.__table__()
        context = Transaction().context
        group_pnl = context.get('group_pnl')
        wh = (val.id > 0)
        query = val.select(
        Literal(0).as_('create_uid'),
        CurrentTimestamp().as_('create_date'),
        Literal(None).as_('write_uid'),
        Literal(None).as_('write_date'),
        Max(val.id).as_('id'),
        Max(val.purchase).as_('r_purchase'),
        Max(val.sale).as_('r_sale'),
        Max(val.line).as_('r_line'),
        Max(val.date).as_('r_date'),
        Literal(None).as_('r_type'),
        Max(val.reference).as_('r_reference'),
        Literal(None).as_('r_counterparty'),
        Max(val.product).as_('r_product'),
        Literal(None).as_('r_state'),
        Avg(val.price).as_('r_price'),
        Max(val.currency).as_('r_currency'),
        Sum(val.quantity).as_('r_quantity'),
        Max(val.unit).as_('r_unit'),
        Sum(val.amount).as_('r_amount'),
        Sum(val.amount_prev).as_('r_amount_prev'),
        Sum(val.base_amount).as_('r_base_amount'),
        Sum(val.pnl).as_('r_pnl'),
        Max(val.base_currency).as_('r_base_currency'),
        Sum(val.rate).as_('r_rate'),
        Avg(val.mtm_price).as_('r_mtm_price'),
        Avg(val.mtm_price_prev).as_('r_mtm_price_prev'),
        val.mtm_curve.as_('r_mtm_curve'),
        Sum(val.mtm).as_('r_mtm'),
        Max(val.strategy).as_('r_strategy'),
        Max(val.lot).as_('r_lot'),
        Max(val.sale_line).as_('r_sale_line'),
        Max(val.shipment_in).as_('r_shipment_in'),
        where=wh,
        group_by=[val.purchase, val.sale, val.mtm_curve])
            
        return query    

    
class Fee(metaclass=PoolMeta):
    "Fee"
    __name__ = 'fee.fee'

    sale_line = fields.Many2One('sale.line',"Line", ondelete='CASCADE')

class SaleAllocationsWizard(Wizard):
    'Open Allocations report from Sale without modal'
    __name__ = 'sale.allocations.wizard'

    start_state = 'open_report'

    open_report = StateAction('purchase_trade.act_lot_report_form')

    def do_open_report(self, action):
        sale_id = Transaction().context.get('active_id')
        if not sale_id:
            raise ValueError("No active sale ID in context")
        action['context_model'] = 'lot.context'
        action['pyson_context'] = PYSONEncoder().encode({
                'sale': sale_id,
                })
        return action, {}
