from decimal import Decimal, ROUND_HALF_UP
from datetime import date as dt_date
import os
from pathlib import Path

from sql import Literal
from sql.conditionals import Case
from sql.functions import CurrentTimestamp

from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import Pool, PoolMeta
from trytond.modules.purchase_trade.numbers_to_words import amount_to_currency_words
from trytond.exceptions import UserError
from trytond.transaction import Transaction
from trytond.tools import file_open
from trytond.modules.account_invoice.invoice import (
    InvoiceReport as BaseInvoiceReport)
from trytond.modules.sale.sale import SaleReport as BaseSaleReport
from trytond.modules.purchase.purchase import (
    PurchaseReport as BasePurchaseReport)


class InvoiceLineLotWeight(ModelSQL, ModelView):
    "Invoice Line Lot Weight"
    __name__ = 'account.invoice.line.lot.weight'

    invoice_line = fields.Many2One(
        'account.invoice.line', "Invoice Line", required=True,
        ondelete='CASCADE')
    lot = fields.Many2One('lot.lot', "Lot", required=True, ondelete='CASCADE')
    lot_qt_hist = fields.Many2One('lot.qt.hist', "Quantity History")
    quantity_type = fields.Many2One('lot.qt.type', "Quantity Type")
    net_quantity = fields.Numeric("Net Weight", digits=(1, 5), required=True)
    gross_quantity = fields.Numeric(
        "Gross Weight", digits=(1, 5), required=True)
    unit = fields.Many2One('product.uom', "Unit")
    lot_qt = fields.Integer("Packing Quantity")
    lot_unit = fields.Many2One('product.uom', "Packing Unit")

    @classmethod
    def _get_lot_hist_entry(cls, lot):
        state = getattr(lot, 'lot_state', None)
        state_id = getattr(state, 'id', None)
        hist = list(getattr(lot, 'lot_hist', []) or [])
        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:
                    return entry
        if len(hist) == 1:
            return hist[0]
        return None

    @classmethod
    def _get_lot_weights(cls, lot):
        hist = cls._get_lot_hist_entry(lot)
        if hist:
            net = Decimal(str(getattr(hist, 'quantity', 0) or 0))
            gross = Decimal(str(
                getattr(hist, 'gross_quantity', None)
                if getattr(hist, 'gross_quantity', None) not in (None, '')
                else net))
            return hist, net, gross
        if hasattr(lot, 'get_hist_quantity'):
            net, gross = lot.get_hist_quantity()
            net = Decimal(str(net or 0))
            gross = Decimal(str(gross if gross not in (None, '') else net))
            return None, net, gross
        net = Decimal(str(getattr(lot, 'lot_quantity', 0) or 0))
        gross = Decimal(str(
            getattr(lot, 'lot_gross_quantity', None)
            if getattr(lot, 'lot_gross_quantity', None) not in (None, '')
            else net))
        return None, net, gross

    @classmethod
    def create_for_invoice_line(cls, line, lot):
        if not getattr(line, 'id', None) or not getattr(lot, 'id', None):
            return
        existing = cls.search([
                ('invoice_line', '=', line.id),
                ('lot', '=', lot.id),
                ])
        if existing:
            cls.delete(existing)
        hist, net, gross = cls._get_lot_weights(lot)
        snapshot = cls()
        snapshot.invoice_line = line
        snapshot.lot = lot
        snapshot.lot_qt_hist = hist
        snapshot.quantity_type = (
            getattr(hist, 'quantity_type', None)
            or getattr(lot, 'lot_state', None))
        snapshot.net_quantity = net
        snapshot.gross_quantity = gross
        snapshot.unit = (
            getattr(lot, 'lot_unit_line', None)
            or getattr(lot, 'lot_unit', None)
            or getattr(line, 'unit', None))
        snapshot.lot_qt = getattr(lot, 'lot_qt', None)
        snapshot.lot_unit = getattr(lot, 'lot_unit', None)
        cls.save([snapshot])


class Invoice(metaclass=PoolMeta):
    __name__ = 'account.invoice'

    @classmethod
    def _sale_padding_lots_to_clear_on_delete(cls, invoices):
        lots = {}
        for invoice in invoices:
            if (getattr(invoice, 'type', None) != 'out'
                    or getattr(invoice, 'reference', None) != 'Provisional'
                    or getattr(invoice, 'number', None)):
                continue
            invoice_id = getattr(invoice, 'id', None)
            for line in getattr(invoice, 'lines', None) or []:
                if (getattr(line, 'type', None) != 'line'
                        or getattr(line, 'description', None) != 'Pro forma'):
                    continue
                lot = getattr(line, 'lot', None)
                if not lot:
                    continue
                provisional_line = getattr(lot, 'sale_invoice_line_prov', None)
                provisional_invoice = getattr(provisional_line, 'invoice', None)
                if (invoice_id and provisional_invoice
                        and getattr(provisional_invoice, 'id', None) != invoice_id):
                    continue
                if Decimal(str(getattr(lot, 'sale_invoice_padding', 0) or 0)):
                    lots[getattr(lot, 'id', id(lot))] = lot
        return list(lots.values())

    @classmethod
    def _fee_updates_on_delete(cls, invoices):
        reset_fees = {}
        clear_dn_cn_fees = {}
        for invoice in invoices:
            if getattr(invoice, 'number', None):
                continue
            invoice_id = getattr(invoice, 'id', None)
            for line in getattr(invoice, 'lines', None) or []:
                fee = getattr(line, 'fee', None)
                if not fee:
                    continue
                fee_key = getattr(fee, 'id', None) or id(fee)
                dn_cn = getattr(fee, 'dn_cn', None)
                dn_cn_id = getattr(dn_cn, 'id', dn_cn)
                if invoice_id and dn_cn_id == invoice_id:
                    clear_dn_cn_fees[fee_key] = fee
                else:
                    reset_fees[fee_key] = fee
        return list(reset_fees.values()), list(clear_dn_cn_fees.values())

    @classmethod
    def delete(cls, invoices):
        Lot = Pool().get('lot.lot')
        Fee = Pool().get('fee.fee')
        padding_lots = cls._sale_padding_lots_to_clear_on_delete(invoices)
        reset_fees, clear_dn_cn_fees = cls._fee_updates_on_delete(invoices)
        super().delete(invoices)
        if padding_lots:
            Lot.write(padding_lots, {'sale_invoice_padding': Decimal(0)})
        if reset_fees:
            Fee.write(reset_fees, {
                    'state': 'not invoiced',
                    'dn_cn': None,
                    })
        if clear_dn_cn_fees:
            Fee.write(clear_dn_cn_fees, {'dn_cn': None})

    def do_lot_invoicing(self):
        super().do_lot_invoicing()
        self._create_lot_weight_snapshots()
        self._create_sale_padding_moves()

    def _create_lot_weight_snapshots(self):
        Snapshot = Pool().get('account.invoice.line.lot.weight')
        Lot = Pool().get('lot.lot')
        for line in self._get_report_invoice_lines():
            if (getattr(line, 'description', None) not in {'Pro forma', 'Final'}
                    or Decimal(str(getattr(line, 'quantity', 0) or 0)) <= 0
                    or not getattr(line, 'lot', None)):
                continue
            Snapshot.create_for_invoice_line(line, Lot(line.lot.id))

    @classmethod
    def _post(cls, invoices):
        pool = Pool()
        Move = pool.get('account.move')

        super()._post(invoices)

        padding_moves = []
        for invoice in invoices:
            padding_moves.extend(invoice._create_sale_padding_moves())
            padding_moves.extend([
                    move for move in (invoice.additional_moves or [])
                    if move.description in {
                        invoice._get_sale_padding_move_description(False),
                        invoice._get_sale_padding_move_description(True),
                        }
                    and move.state != 'posted'
                    ])
        if padding_moves:
            cls.save(invoices)
            Move.post([m for m in padding_moves if m.state != 'posted'])

    def _get_sale_padding_accounts(self):
        Configuration = Pool().get('account.configuration')
        config = Configuration(1)
        sale_account = config.get_multivalue(
            'default_sale_padding_account', company=self.company.id)
        accrual_account = config.get_multivalue(
            'default_accrual_padding_account', company=self.company.id)
        if not sale_account or not accrual_account:
            raise UserError(
                'Default Sale Padding and Default Accrual Padding '
                'accounts must be configured.')
        return sale_account, accrual_account

    def _has_sale_padding_move(self, reversal=False):
        description = self._get_sale_padding_move_description(reversal)
        return any(
            move.description == description
            for move in (self.additional_moves or []))

    @staticmethod
    def _get_sale_padding_move_description(reversal=False):
        if reversal:
            return 'Sale padding reversal'
        return 'Sale padding accrual'

    def _get_padding_company_amount(self, invoice_line, padding):
        Currency = Pool().get('currency.currency')
        invoice = invoice_line.invoice
        amount = Decimal(str(padding or 0)) * Decimal(
            str(invoice_line.unit_price or 0))
        amount = invoice.currency.round(amount)
        if invoice.currency == invoice.company.currency:
            return amount, amount
        if invoice.rate:
            company_amount = invoice.company.currency.round(
                amount / invoice.rate)
        else:
            with Transaction().set_context(date=invoice.currency_date):
                company_amount = Currency.compute(
                    invoice.currency, amount, invoice.company.currency)
        return amount, company_amount

    def _get_sale_padding_entries(self):
        if self.type != 'out':
            return []

        entries = []
        for line in self.lines or []:
            if getattr(line, 'type', None) != 'line':
                continue
            lot = getattr(line, 'lot', None)
            if not lot:
                continue
            padding = Decimal(str(
                    getattr(lot, 'sale_invoice_padding', 0) or 0))
            if padding <= 0:
                continue

            if self.reference == 'Provisional' and line.description == 'Pro forma':
                entries.append((lot, line, padding, False))
            elif self.reference == 'Final' and line.description == 'Final':
                provisional_line = getattr(lot, 'sale_invoice_line_prov', None)
                if provisional_line:
                    entries.append((lot, provisional_line, padding, True))
        return entries

    def _get_sale_padding_move_lines(self, entries):
        MoveLine = Pool().get('account.move.line')
        sale_account, accrual_account = self._get_sale_padding_accounts()

        move_lines = []
        for lot, invoice_line, padding, reversal in entries:
            padding_amount, company_amount = self._get_padding_company_amount(
                invoice_line, padding)
            if not company_amount:
                continue

            sale_line = MoveLine()
            accrual_line = MoveLine()
            for move_line in (sale_line, accrual_line):
                move_line.lot = lot
                move_line.origin = invoice_line
                move_line.description = 'Padding'

            if not reversal:
                sale_line.account = sale_account
                sale_line.debit = company_amount
                sale_line.credit = Decimal(0)
                accrual_line.account = accrual_account
                accrual_line.debit = Decimal(0)
                accrual_line.credit = company_amount
            else:
                accrual_line.account = accrual_account
                accrual_line.debit = company_amount
                accrual_line.credit = Decimal(0)
                sale_line.account = sale_account
                sale_line.debit = Decimal(0)
                sale_line.credit = company_amount

            if self.currency != self.company.currency:
                sale_line.second_currency = self.currency
                accrual_line.second_currency = self.currency
                sale_line.amount_second_currency = padding_amount.copy_sign(
                    sale_line.debit - sale_line.credit)
                accrual_line.amount_second_currency = padding_amount.copy_sign(
                    accrual_line.debit - accrual_line.credit)

            if sale_line.account.party_required:
                sale_line.party = self.party
            if accrual_line.account.party_required:
                accrual_line.party = self.party
            move_lines.extend([sale_line, accrual_line])
        return move_lines

    def _create_sale_padding_moves(self):
        pool = Pool()
        Move = pool.get('account.move')
        Period = pool.get('account.period')
        Date = pool.get('ir.date')

        entries = self._get_sale_padding_entries()
        if not entries:
            return []
        reversal = entries[0][3]
        if self._has_sale_padding_move(reversal):
            return []

        move_lines = self._get_sale_padding_move_lines(entries)
        if not move_lines:
            return []

        with Transaction().set_context(company=self.company.id):
            today = Date.today()
        accounting_date = self.accounting_date or self.invoice_date or today
        period = Period.find(self.company, date=accounting_date)
        move = Move()
        move.journal = self.journal
        move.period = period
        move.date = accounting_date
        move.origin = self
        move.company = self.company
        move.description = self._get_sale_padding_move_description(reversal)
        move.lines = move_lines
        Move.save([move])
        self.additional_moves = tuple(self.additional_moves or ()) + (move,)
        return [move]


    @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'

    @classmethod
    def _format_report_quantity_display(cls, value):
        return cls._format_report_number(
            value, digits='0.01', strip_trailing_zeros=False)

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

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

    @staticmethod
    def _report_record_name(record):
        if not record:
            return ''
        return (
            getattr(record, 'rec_name', None)
            or getattr(record, 'name', None)
            or getattr(record, 'code', None)
            or '')

    def _get_report_commission_fee(self):
        for line in self._get_report_invoice_lines():
            fee = getattr(line, 'fee', None)
            if fee:
                return fee
            origin = getattr(line, 'origin', None)
            fee = getattr(origin, 'fee_', None)
            if fee:
                return fee

    def _get_report_commission_lots(self):
        lots = []
        seen = set()

        def add_lot(lot):
            if not lot:
                return
            lot_id = getattr(lot, 'id', None)
            key = ('id', lot_id) if lot_id is not None else ('obj', id(lot))
            if key in seen:
                return
            seen.add(key)
            lots.append(lot)

        for line in self._get_report_invoice_lines():
            add_lot(getattr(line, 'lot', None))
        fee = self._get_report_commission_fee()
        if fee and hasattr(fee, '_get_effective_fee_lots'):
            for lot in fee._get_effective_fee_lots():
                add_lot(lot)
        for lot in getattr(fee, 'lots', []) or []:
            add_lot(lot)
        return lots

    def _get_report_commission_trade_for(self, kind):
        fee = self._get_report_commission_fee()
        if fee:
            if kind == 'sale' and getattr(fee, 'sale_line', None):
                return fee.sale_line.sale
            if kind == 'purchase' and getattr(fee, 'line', None):
                return fee.line.purchase
        for lot in self._get_report_commission_lots():
            if kind == 'sale' and getattr(lot, 'sale_line', None):
                return lot.sale_line.sale
            if kind == 'purchase' and getattr(lot, 'line', None):
                return lot.line.purchase
        records = getattr(self, kind + 's', None)
        if records:
            return records[0]

    def _get_report_commission_trade(self):
        for kind in ('sale', 'purchase'):
            trade = self._get_report_commission_trade_for(kind)
            if trade:
                return trade
        for line in self._get_report_invoice_lines():
            origin = getattr(line, 'origin', None)
            if not origin:
                continue
            trade = getattr(origin, 'sale', None) or getattr(origin, 'purchase', None)
            if trade:
                return trade

    def _get_report_commission_kind(self):
        fee = self._get_report_commission_fee()
        if fee:
            if getattr(fee, 'sale_line', None):
                return 'sale'
            if getattr(fee, 'line', None):
                return 'purchase'
        for lot in self._get_report_commission_lots():
            if getattr(lot, 'sale_line', None):
                return 'sale'
            if getattr(lot, 'line', None):
                return 'purchase'

    def _get_report_commission_adjustment_unit(self):
        fee = self._get_report_commission_fee()
        if fee:
            unit = (
                getattr(fee, 'linked_unit', None)
                if getattr(fee, 'enable_linked_currency', False) else None)
            if unit:
                return unit
            if getattr(fee, 'unit', None):
                return fee.unit
        lots = self._get_report_commission_lots()
        if lots:
            lot = lots[0]
            return (
                getattr(lot, 'lot_unit_line', None)
                or getattr(lot, 'lot_unit', None))
        line = self._get_report_invoice_line()
        return getattr(line, 'unit', None) if line else None

    def _get_report_commission_provisional_line(self, lot, kind):
        if kind == 'sale':
            return getattr(lot, 'sale_invoice_line_prov', None)
        return getattr(lot, 'invoice_line_prov', None)

    def _get_report_commission_lot_current_quantity(self, lot, unit):
        if hasattr(lot, 'get_current_quantity_converted'):
            try:
                return Decimal(str(
                    lot.get_current_quantity_converted(0, unit) or 0))
            except TypeError:
                return Decimal(str(
                    lot.get_current_quantity_converted() or 0))
        net, _ = self._get_report_lot_hist_weights(lot)
        if net is None:
            return Decimal(0)
        return self._convert_report_quantity(
            net, getattr(lot, 'lot_unit_line', None), unit)

    def _get_report_commission_lot_provisional_quantity(self, lot, kind, unit):
        line = self._get_report_commission_provisional_line(lot, kind)
        if not line:
            return None
        quantity = Decimal(str(getattr(line, 'quantity', 0) or 0))
        if kind == 'sale':
            quantity -= Decimal(str(
                getattr(lot, 'sale_invoice_padding', 0) or 0))
        return self._convert_report_quantity(
            quantity, getattr(line, 'unit', None), unit)

    def _get_report_commission_adjustment_totals(self):
        kind = self._get_report_commission_kind()
        unit = self._get_report_commission_adjustment_unit()
        if not kind or not unit:
            return None
        invoice_total = Decimal(0)
        landed_total = Decimal(0)
        matched = False
        for lot in self._get_report_commission_lots():
            provisional_quantity = (
                self._get_report_commission_lot_provisional_quantity(
                    lot, kind, unit))
            if provisional_quantity is None:
                continue
            invoice_total += abs(provisional_quantity)
            landed_total += abs(
                self._get_report_commission_lot_current_quantity(lot, unit))
            matched = True
        if not matched:
            return None
        return {
            'invoice': invoice_total,
            'landed': landed_total,
            'difference': abs(landed_total - invoice_total),
            'unit': unit,
        }

    @property
    def report_commission_adjustment_unit_upper(self):
        totals = self._get_report_commission_adjustment_totals()
        unit = totals['unit'] if totals else self._get_report_commission_adjustment_unit()
        if unit:
            return (
                getattr(unit, 'symbol', None)
                or getattr(unit, 'rec_name', None)
                or getattr(unit, 'name', None)
                or '').upper()
        return ''

    @property
    def report_commission_invoice_weight_display(self):
        totals = self._get_report_commission_adjustment_totals()
        if not totals:
            return self.report_net_display
        return self._format_report_quantity_display(totals['invoice'])

    @property
    def report_commission_landed_weight_display(self):
        totals = self._get_report_commission_adjustment_totals()
        if not totals:
            return self.report_gross_display
        return self._format_report_quantity_display(totals['landed'])

    @property
    def report_commission_landed_lbs_display(self):
        totals = self._get_report_commission_adjustment_totals()
        if not totals:
            return self.report_gross_lbs_display
        lbs = self._convert_report_quantity_to_lbs(
            totals['landed'], totals['unit'])
        return self._format_report_quantity_display(lbs)

    @property
    def report_commission_weight_difference_display(self):
        totals = self._get_report_commission_adjustment_totals()
        if not totals:
            return self.report_weight_difference_display
        return self._format_report_quantity_display(totals['difference'])

    @property
    def report_commission_weight_difference_lbs_display(self):
        totals = self._get_report_commission_adjustment_totals()
        if not totals:
            return self.report_weight_difference_lbs_display
        lbs = self._convert_report_quantity_to_lbs(
            totals['difference'], totals['unit'])
        return self._format_report_quantity_display(lbs)

    @property
    def report_commission_adjustment_rate_line(self):
        fee = self._get_report_commission_fee()
        if not fee:
            return self.report_commission_rate_line

        mode = getattr(fee, 'mode', None)
        price = getattr(fee, 'price', None) or 0
        if mode in {'pprice', 'rate', 'pcost'}:
            kind = self._get_report_commission_kind() or 'sale'
            return ' '.join(part for part in [
                self._format_report_number(price, digits='0.0000'),
                '% on',
                kind,
                'amount',
                self._get_report_commission_base_amount_display_for(kind),
                self._get_report_commission_currency_name_for(kind),
            ] if part)

        if (getattr(fee, 'enable_linked_currency', False)
                and getattr(fee, 'linked_price', None) is not None):
            price = fee.linked_price
            currency = getattr(fee, 'linked_currency', None)
            unit = getattr(fee, 'linked_unit', None)
        else:
            currency = getattr(fee, 'currency', None)
            unit = getattr(fee, 'unit', None)
        price_text = self._format_report_number(price, digits='0.0000')
        currency_text = self._report_record_name(currency)
        unit_text = self._report_record_name(unit)
        if currency_text and unit_text:
            return '%s %s / %s' % (price_text, currency_text, unit_text)
        return ' '.join(part for part in [
            price_text, currency_text or unit_text,
        ] if part)

    @property
    def report_commission_tax_rate_line(self):
        for tax in getattr(self, 'taxes', []) or []:
            description = getattr(tax, 'description', None) or '0%'
            return 'VAT %s RATE' % description
        return 'VAT 0% RATE'

    @property
    def report_commission_address(self):
        trade = self._get_report_commission_trade()
        return getattr(trade, 'report_address', '') or ''

    @property
    def report_commission_date(self):
        trade = self._get_report_commission_trade()
        return (
            getattr(trade, 'sale_date', None)
            or getattr(trade, 'purchase_date', None)
            or self.invoice_date)

    def _get_report_commission_value(self, name):
        trade = self._get_report_commission_trade()
        return getattr(trade, name, '') if trade else ''

    def _get_report_commission_value_for(self, kind, name):
        trade = self._get_report_commission_trade_for(kind)
        return getattr(trade, name, '') if trade else ''

    @property
    def report_commission_invoice_number(self):
        return self._get_report_commission_value(
            'report_commission_invoice_number')

    @property
    def report_commission_contract_number(self):
        return self._get_report_commission_value(
            'report_commission_contract_number')

    @property
    def report_commission_bl_number(self):
        return self._get_report_commission_value(
            'report_commission_bl_number')

    @property
    def report_commission_bl_date(self):
        return self._get_report_commission_value(
            'report_commission_bl_date')

    @property
    def report_commission_vessel(self):
        return self._get_report_commission_value(
            'report_commission_vessel')

    @property
    def report_commission_invoice_line(self):
        return self._get_report_commission_value(
            'report_commission_invoice_line')

    @property
    def report_commission_quantity_unit_upper(self):
        return self._get_report_commission_value(
            'report_commission_quantity_unit_upper')

    @property
    def report_commission_quantity_display(self):
        return self._get_report_commission_value(
            'report_commission_quantity_display')

    @property
    def report_commission_lbs_display(self):
        return self._get_report_commission_value(
            'report_commission_lbs_display')

    @property
    def report_commission_rate_line(self):
        return self._get_report_commission_value(
            'report_commission_rate_line')

    @property
    def report_commission_secondary_rate_line(self):
        return self._get_report_commission_value(
            'report_commission_secondary_rate_line')

    def _get_report_commission_base_amount_display_for(self, kind):
        trade = self._get_report_commission_trade_for(kind)
        if trade:
            base = getattr(trade, 'report_commission_base_amount_display', None)
            if base:
                return base
            amount = (
                getattr(trade, 'untaxed_amount', None)
                or getattr(trade, 'total_amount', None)
                or 0)
            return self._format_report_number(
                amount, digits='0.01', strip_trailing_zeros=False)
        return self.report_commission_total_display

    def _get_report_commission_currency_name_for(self, kind):
        trade = self._get_report_commission_trade_for(kind)
        currency = getattr(trade, 'currency', None) if trade else self.currency
        return self._report_record_name(currency)

    def _get_report_commission_rate_line_for(self, kind):
        fee = self._get_report_commission_fee()
        if not fee:
            return self._get_report_commission_value(
                'report_commission_rate_line')

        mode = getattr(fee, 'mode', None)
        price = getattr(fee, 'price', None) or 0
        if mode in {'pprice', 'rate', 'pcost'}:
            return ' '.join(part for part in [
                self._format_report_number(price, digits='0.0000'),
                '% on',
                kind,
                'amount',
                self._get_report_commission_base_amount_display_for(kind),
                self._get_report_commission_currency_name_for(kind),
            ] if part)

        if (getattr(fee, 'enable_linked_currency', False)
                and getattr(fee, 'linked_price', None) is not None):
            price = fee.linked_price
            currency = getattr(fee, 'linked_currency', None)
            unit = getattr(fee, 'linked_unit', None)
        else:
            currency = getattr(fee, 'currency', None)
            unit = getattr(fee, 'unit', None)
        price_text = self._format_report_number(price, digits='0.0000')
        currency_text = self._report_record_name(currency)
        unit_text = self._report_record_name(unit)
        if currency_text and unit_text:
            return '%s %s/%s' % (price_text, currency_text, unit_text)
        return ' '.join(part for part in [
            price_text, currency_text or unit_text,
        ] if part)

    @property
    def report_sale_commission_rate_line(self):
        return self._get_report_commission_rate_line_for('sale')

    @property
    def report_purchase_commission_rate_line(self):
        return self._get_report_commission_rate_line_for('purchase')

    @property
    def report_sale_commission_secondary_rate_line(self):
        return ''

    @property
    def report_purchase_commission_secondary_rate_line(self):
        return ''

    @property
    def report_commission_currency_name(self):
        return self._get_report_commission_value(
            'report_commission_currency_name')

    @property
    def report_commission_total_display(self):
        return self._get_report_commission_value(
            'report_commission_total_display')

    def _get_report_commission_amount(self):
        return self.total_amount or 0

    @property
    def report_commission_note_title(self):
        amount = Decimal(str(self._get_report_commission_amount() or 0))
        if amount < 0:
            return 'Credit Note'
        return 'Debit Note'

    @property
    def report_commission_due_label(self):
        amount = Decimal(str(self._get_report_commission_amount() or 0))
        if amount < 0:
            return 'TOTAL DUE TO US'
        return 'TOTAL DUE TO YOU'

    @property
    def report_commission_note_total_display(self):
        amount = self._get_report_commission_amount()
        return self._format_report_number(
            abs(Decimal(str(amount or 0))), digits='0.01',
            strip_trailing_zeros=False)

    def _get_report_commission_address_for(self, kind):
        trade = self._get_report_commission_trade_for(kind)
        return getattr(trade, 'report_address', '') or ''

    def _get_report_commission_date_for(self, kind):
        trade = self._get_report_commission_trade_for(kind)
        return (
            getattr(trade, 'sale_date', None)
            or getattr(trade, 'purchase_date', None)
            or self.invoice_date)

    def _get_report_commission_total_display_for(self, kind):
        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_sale_commission_address(self):
        return self._get_report_commission_address_for('sale')

    @property
    def report_purchase_commission_address(self):
        return self._get_report_commission_address_for('purchase')

    @property
    def report_sale_commission_date(self):
        return self._get_report_commission_date_for('sale')

    @property
    def report_purchase_commission_date(self):
        return self._get_report_commission_date_for('purchase')

    @property
    def report_sale_commission_invoice_number(self):
        return self._get_report_commission_value_for(
            'sale', 'report_commission_invoice_number')

    @property
    def report_purchase_commission_invoice_number(self):
        return self._get_report_commission_value_for(
            'purchase', 'report_commission_invoice_number')

    @property
    def report_sale_commission_contract_number(self):
        return self._get_report_commission_value_for(
            'sale', 'report_commission_contract_number')

    @property
    def report_purchase_commission_contract_number(self):
        return self._get_report_commission_value_for(
            'purchase', 'report_commission_contract_number')

    @property
    def report_sale_commission_bl_number(self):
        return self._get_report_commission_value_for(
            'sale', 'report_commission_bl_number')

    @property
    def report_purchase_commission_bl_number(self):
        return self._get_report_commission_value_for(
            'purchase', 'report_commission_bl_number')

    @property
    def report_sale_commission_bl_date(self):
        return self._get_report_commission_value_for(
            'sale', 'report_commission_bl_date')

    @property
    def report_purchase_commission_bl_date(self):
        return self._get_report_commission_value_for(
            'purchase', 'report_commission_bl_date')

    @property
    def report_sale_commission_vessel(self):
        return self._get_report_commission_value_for(
            'sale', 'report_commission_vessel')

    @property
    def report_purchase_commission_vessel(self):
        return self._get_report_commission_value_for(
            'purchase', 'report_commission_vessel')

    @property
    def report_sale_commission_invoice_line(self):
        return self._get_report_commission_value_for(
            'sale', 'report_commission_invoice_line')

    @property
    def report_purchase_commission_invoice_line(self):
        return self._get_report_commission_value_for(
            'purchase', 'report_commission_invoice_line')

    @property
    def report_sale_commission_quantity_unit_upper(self):
        return self._get_report_commission_value_for(
            'sale', 'report_commission_quantity_unit_upper')

    @property
    def report_purchase_commission_quantity_unit_upper(self):
        return self._get_report_commission_value_for(
            'purchase', 'report_commission_quantity_unit_upper')

    @property
    def report_sale_commission_quantity_display(self):
        return self._get_report_commission_value_for(
            'sale', 'report_commission_quantity_display')

    @property
    def report_purchase_commission_quantity_display(self):
        return self._get_report_commission_value_for(
            'purchase', 'report_commission_quantity_display')

    @property
    def report_sale_commission_lbs_display(self):
        return self._get_report_commission_value_for(
            'sale', 'report_commission_lbs_display')

    @property
    def report_purchase_commission_lbs_display(self):
        return self._get_report_commission_value_for(
            'purchase', 'report_commission_lbs_display')

    @property
    def report_sale_commission_currency_name(self):
        return self._report_record_name(self.currency)

    @property
    def report_purchase_commission_currency_name(self):
        return self._report_record_name(self.currency)

    @property
    def report_sale_commission_total_display(self):
        return self._get_report_commission_total_display_for('sale')

    @property
    def report_purchase_commission_total_display(self):
        return self._get_report_commission_total_display_for('purchase')

    @staticmethod
    def _same_report_invoice_line(left, right):
        if not left or not right:
            return False
        left_id = getattr(left, 'id', None)
        right_id = getattr(right, 'id', None)
        if left_id is not None and right_id is not None:
            return left_id == right_id
        return left is right

    def _get_report_invoice_line_lots(self, invoice_line):
        lots = []
        seen = set()

        def add_lot(lot):
            if not lot or getattr(lot, 'lot_type', None) == 'virtual':
                return
            lot_id = getattr(lot, 'id', None)
            key = lot_id if lot_id is not None else id(lot)
            if key in seen:
                return
            seen.add(key)
            lots.append(lot)

        add_lot(getattr(invoice_line, 'lot', None))
        trade = self._get_report_trade()
        trade_lines = getattr(trade, 'lines', []) if trade else []
        for line in trade_lines or []:
            for lot in getattr(line, 'lots', []) or []:
                if getattr(lot, 'lot_type', None) != 'physic':
                    continue
                refs = [
                    getattr(lot, 'sale_invoice_line', None),
                    getattr(lot, 'sale_invoice_line_prov', None),
                    getattr(lot, 'invoice_line', None),
                    getattr(lot, 'invoice_line_prov', None),
                ]
                if any(
                        self._same_report_invoice_line(ref, invoice_line)
                        for ref in refs):
                    add_lot(lot)
        return lots

    @staticmethod
    def _get_report_related_lots(line):
        lots = []
        seen = set()

        def add_lot(lot):
            if not lot:
                return
            lot_id = getattr(lot, 'id', None)
            key = ('id', lot_id) if lot_id is not None else ('obj', id(lot))
            if key in seen:
                return
            seen.add(key)
            lots.append(lot)

        add_lot(getattr(line, 'lot', None))
        origin = getattr(line, 'origin', None)
        for lot in getattr(origin, 'lots', []) or []:
            add_lot(lot)
        return lots

    @classmethod
    def _get_report_preferred_lots(cls, line):
        lots = cls._get_report_related_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_line_sign(line):
        quantity = Decimal(str(getattr(line, 'quantity', 0) or 0))
        return Decimal(-1) if quantity < 0 else Decimal(1)

    @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

    def _get_report_invoice_line_weights(self, line):
        snapshots = self._get_report_invoice_line_weight_snapshots(line)
        if snapshots:
            sign = self._get_report_line_sign(line)
            net_total = sum(
                Decimal(str(snapshot.net_quantity or 0))
                for snapshot in snapshots)
            gross_total = sum(
                Decimal(str(snapshot.gross_quantity or 0))
                for snapshot in snapshots)
            unit = self._get_report_invoice_line_unit(line)
            padding = self._get_report_invoice_line_padding(line, unit)
            net_total += padding
            gross_total += padding
            return net_total * sign, gross_total * sign
        lots = self._get_report_invoice_line_lots(line)
        if lots and self._report_invoice_line_reuses_lot(line):
            quantity = self._get_report_invoice_line_quantity_from_line(line)
            return quantity, quantity
        if lots:
            sign = self._get_report_line_sign(line)
            net_total = Decimal(0)
            gross_total = Decimal(0)
            for lot in lots:
                net, gross = self._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 * sign, gross_total * sign
        quantity = Decimal(str(getattr(line, 'quantity', 0) or 0))
        return quantity, quantity

    @staticmethod
    def _get_report_invoice_line_weight_snapshots(line):
        snapshots = getattr(line, 'lot_weight_snapshots', None)
        if snapshots and isinstance(snapshots, (list, tuple)):
            return list(snapshots)
        line_id = getattr(line, 'id', None)
        if not isinstance(line_id, int):
            return []
        Snapshot = Pool().get('account.invoice.line.lot.weight')
        return Snapshot.search([('invoice_line', '=', line_id)])

    @classmethod
    def _get_report_invoice_line_padding(cls, line, to_unit):
        if getattr(line, 'description', None) != 'Pro forma':
            return Decimal(0)
        invoice_type = getattr(line, 'invoice_type', None)
        invoice = getattr(line, 'invoice', None)
        if invoice_type != 'out' and getattr(invoice, 'type', None) != 'out':
            return Decimal(0)
        lot = getattr(line, 'lot', None)
        padding = Decimal(str(
            getattr(lot, 'sale_invoice_padding', 0) or 0))
        if not padding:
            return Decimal(0)
        return cls._convert_report_quantity(
            padding, getattr(line, 'unit', None), to_unit)

    @staticmethod
    def _get_report_line_lot_keys(line):
        keys = []
        invoice = getattr(line, 'invoice', None)
        lots = (
            invoice._get_report_invoice_line_lots(line)
            if invoice else Invoice._get_report_preferred_lots(line))
        for lot in lots:
            lot_id = getattr(lot, 'id', None)
            keys.append(lot_id if lot_id is not None else id(lot))
        return tuple(sorted(keys))

    def _report_invoice_line_reuses_lot(self, line):
        line_keys = self._get_report_line_lot_keys(line)
        if not line_keys:
            return False
        for other in self._get_report_invoice_lines():
            if other is line:
                continue
            if self._get_report_line_lot_keys(other) == line_keys:
                return True
        return False

    def _get_report_reused_lot_lines(self):
        groups = {}
        for line in self._get_report_invoice_lines():
            lots = self._get_report_invoice_line_lots(line)
            if not lots:
                continue
            for lot in lots:
                lot_id = getattr(lot, 'id', None)
                key = lot_id if lot_id is not None else id(lot)
                groups.setdefault(key, {'lot': lot, 'lines': []})
                groups[key]['lines'].append(line)
        return {
            key: value for key, value in groups.items()
            if len(value['lines']) > 1
        }

    @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
        converted = Pool().get('product.uom').compute_qty(
            from_unit, float(value), to_unit) or 0
        return Decimal(str(converted))

    @classmethod
    def _get_report_invoice_line_quantity_from_line(cls, line):
        quantity = Decimal(str(getattr(line, 'quantity', 0) or 0))
        return cls._convert_report_quantity(
            quantity,
            getattr(line, 'unit', None),
            cls._get_report_invoice_line_unit(line),
        )

    @classmethod
    def _find_report_hist_entry_for_quantity(cls, lot, quantity, exclude_state_id=None):
        target = Decimal(str(quantity or 0)).copy_abs()
        for entry in list(getattr(lot, 'lot_hist', []) or []):
            quantity_type = getattr(entry, 'quantity_type', None)
            if getattr(quantity_type, 'id', None) == exclude_state_id:
                continue
            entry_quantity = Decimal(str(getattr(entry, 'quantity', 0) or 0))
            if entry_quantity == target:
                return entry
        return None

    def _get_report_reused_lot_gross_total(self):
        reused_lots = self._get_report_reused_lot_lines()
        if not reused_lots:
            return None
        total = Decimal(0)
        for data in reused_lots.values():
            lot = data['lot']
            _, current_gross = self._get_report_lot_hist_weights(lot)
            if current_gross is None:
                continue
            current_state = getattr(getattr(lot, 'lot_state', None), 'id', None)
            negative_lines = [
                line for line in data['lines']
                if Decimal(str(getattr(line, 'quantity', 0) or 0)) < 0
            ]
            previous_gross = Decimal(0)
            matched = False
            for line in negative_lines:
                previous_entry = self._find_report_hist_entry_for_quantity(
                    lot,
                    self._get_report_invoice_line_quantity_from_line(line),
                    exclude_state_id=current_state,
                )
                if not previous_entry:
                    continue
                previous_gross += Decimal(str(
                    getattr(previous_entry, 'gross_quantity', 0) or 0))
                matched = True
            if matched:
                total += current_gross - previous_gross
        return total

    def _get_report_weight_adjustment_totals(self):
        reused_lots = self._get_report_reused_lot_lines()
        if not reused_lots:
            return None

        invoice_total = Decimal(0)
        landed_total = Decimal(0)
        matched = False
        for data in reused_lots.values():
            lines = data['lines']
            negative_lines = [
                line for line in lines
                if Decimal(str(getattr(line, 'quantity', 0) or 0)) < 0
            ]
            positive_lines = [
                line for line in lines
                if Decimal(str(getattr(line, 'quantity', 0) or 0)) > 0
            ]
            if not negative_lines or not positive_lines:
                continue

            invoice_values = [
                abs(self._get_report_invoice_line_weights(line)[0])
                for line in negative_lines]
            landed_values = [
                abs(self._get_report_invoice_line_quantity_from_line(line))
                for line in positive_lines]
            invoice_total += sum(invoice_values)
            landed_total += sum(landed_values)
            matched = True

        if not matched:
            return None
        return {
            'invoice': invoice_total,
            'landed': landed_total,
            'difference': abs(landed_total - invoice_total),
        }

    @staticmethod
    def _get_report_invoice_line_unit(line):
        snapshots = Invoice._get_report_invoice_line_weight_snapshots(line)
        if snapshots and getattr(snapshots[0], 'unit', None):
            return snapshots[0].unit
        invoice = getattr(line, 'invoice', None)
        lots = (
            invoice._get_report_invoice_line_lots(line)
            if invoice else Invoice._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)

    @staticmethod
    def _get_report_lbs_unit():
        Uom = Pool().get('product.uom')
        for domain in (
                [('symbol', '=', 'LBS')],
                [('rec_name', '=', 'LBS')],
                [('name', '=', 'LBS')],
                [('symbol', '=', 'LB')],
                [('rec_name', '=', 'LB')],
                [('name', '=', 'LB')]):
            units = Uom.search(domain, limit=1)
            if units:
                return units[0]
        return None

    @classmethod
    def _convert_report_quantity_to_lbs(cls, quantity, unit):
        value = Decimal(str(quantity or 0))
        if value == 0:
            return Decimal('0.00')
        if not unit:
            return (value * Decimal('2204.62')).quantize(Decimal('0.01'))
        label = (
            getattr(unit, 'symbol', None)
            or getattr(unit, 'rec_name', None)
            or getattr(unit, 'name', None)
            or ''
        ).strip().upper()
        if label in {'LBS', 'LB', 'POUND', 'POUNDS'}:
            return value.quantize(Decimal('0.01'))
        lbs_unit = cls._get_report_lbs_unit()
        if lbs_unit:
            converted = Pool().get('product.uom').compute_qty(
                unit, float(value), lbs_unit) or 0
            return Decimal(str(converted)).quantize(Decimal('0.01'))
        if label in {'KG', 'KGS', 'KILOGRAM', 'KILOGRAMS'}:
            return (value * Decimal('2.20462')).quantize(Decimal('0.01'))
        return (value * Decimal('2204.62')).quantize(Decimal('0.01'))

    @staticmethod
    def _clean_report_description(value):
        text = (value or '').strip()
        normalized = text.replace(' ', '').upper()
        if normalized == 'PROFORMA':
            return ''
        return text.upper() if text else ''

    def _get_report_purchase(self):
        purchases = list(getattr(self, 'purchases', None) or [])
        return purchases[0] if purchases else None

    def _get_report_sale(self):
        # Bridge invoice templates to the originating sale so FODT files can
        # reuse stable sale.report_* properties instead of complex expressions.
        sales = list(getattr(self, 'sales', None) or [])
        return sales[0] if sales else None

    def _get_report_trade(self):
        return self._get_report_sale() or self._get_report_purchase()

    def _get_report_deal_sale(self):
        sale = self._get_report_sale()
        if sale:
            return sale
        for lot in self._get_report_invoice_lots() or []:
            line = getattr(lot, 'sale_line', None)
            sale = getattr(line, 'sale', None) if line else None
            if sale:
                return sale

    def _get_report_purchase_line(self):
        purchase = self._get_report_purchase()
        if purchase and purchase.lines:
            return purchase.lines[0]

    def _get_report_sale_line(self):
        sale = self._get_report_sale()
        if sale and sale.lines:
            return sale.lines[0]

    def _get_report_trade_line(self):
        return self._get_report_sale_line() or self._get_report_purchase_line()

    def _get_report_lot(self):
        line = self._get_report_trade_line()
        if line and line.lots:
            for lot in line.lots:
                if lot.lot_type == 'physic':
                    return lot
            return line.lots[0]

    @staticmethod
    def _get_report_lot_shipment(lot):
        if not lot:
            return None
        return (
            getattr(lot, 'lot_shipment_in', None)
            or getattr(lot, 'lot_shipment_out', None)
            or getattr(lot, 'lot_shipment_internal', None)
        )

    def _get_report_invoice_shipments(self):
        shipments = []
        seen = set()
        for line in self._get_report_invoice_lines():
            for lot in self._get_report_invoice_line_lots(line):
                shipment = self._get_report_lot_shipment(lot)
                if not shipment:
                    continue
                shipment_id = getattr(shipment, 'id', None)
                key = (
                    getattr(shipment, '__name__', None),
                    shipment_id if shipment_id is not None else id(shipment),
                )
                if key in seen:
                    continue
                seen.add(key)
                shipments.append(shipment)
        return shipments

    def _get_report_invoice_lots(self):
        invoice_lines = self._get_report_invoice_lines()
        if not invoice_lines:
            return []

        trade = self._get_report_trade()
        trade_lines = getattr(trade, 'lines', []) if trade else []
        lots = []
        for line in trade_lines or []:
            for lot in getattr(line, 'lots', []) or []:
                if getattr(lot, 'lot_type', None) != 'physic':
                    continue
                refs = [
                    getattr(lot, 'sale_invoice_line', None),
                    getattr(lot, 'sale_invoice_line_prov', None),
                    getattr(lot, 'invoice_line', None),
                    getattr(lot, 'invoice_line_prov', None),
                ]
                if any(
                        self._same_report_invoice_line(ref, invoice_line)
                        for ref in refs for invoice_line in invoice_lines):
                    lots.append(lot)
        return lots

    @staticmethod
    def _format_report_package_label(unit):
        label = (
            getattr(unit, 'symbol', None)
            or getattr(unit, 'rec_name', None)
            or getattr(unit, 'name', None)
            or 'BALE'
        )
        label = label.upper()
        if not label.endswith('S'):
            label += 'S'
        return label

    def _get_report_freight_fee(self):
        pool = Pool()
        Fee = pool.get('fee.fee')
        shipment = self._get_report_shipment()
        if not shipment:
            return None
        fees = Fee.search([
            ('shipment_in', '=', shipment.id),
            ('product.name', '=', 'Maritime freight'),
        ], limit=1)
        return fees[0] if fees else None

    def _get_report_shipment(self):
        shipments = self._get_report_invoice_shipments()
        if len(shipments) == 1:
            return shipments[0]
        if len(shipments) > 1:
            return None
        lot = self._get_report_lot()
        return self._get_report_lot_shipment(lot)

    @staticmethod
    def _get_report_bank_account(party):
        accounts = list(getattr(party, 'bank_accounts', []) or [])
        return accounts[0] if accounts else None

    @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_name(account):
        bank = getattr(account, 'bank', None) if account else None
        party = getattr(bank, 'party', None) if bank else None
        return getattr(party, 'rec_name', None) or getattr(bank, 'rec_name', None) or ''

    @staticmethod
    def _get_report_bank_city(account):
        bank = getattr(account, 'bank', None) if account else None
        party = getattr(bank, 'party', None) if bank else None
        address = party.address_get() if party and hasattr(party, 'address_get') else None
        return getattr(address, 'city', None) or ''

    @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 = Invoice._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_lines(account):
        address = Invoice._get_report_bank_address(account)
        if not address:
            return ''
        city_line = ' '.join(filter(None, [
            getattr(address, 'postal_code', None),
            getattr(address, 'city', None),
        ]))
        full_address = '\n'.join(filter(None, [
            getattr(address, 'name', None),
            getattr(address, 'street', None),
            city_line,
        ]))
        return '\n'.join(
            line for line in (full_address or '').splitlines()
            if line.strip())

    @staticmethod
    def _format_report_payment_amount(value):
        amount = Decimal(str(value or 0)).quantize(Decimal('0.01'))
        return format(amount, 'f')

    def _get_report_melya_bank_account(self):
        sale = self._get_report_deal_sale()
        return getattr(sale, 'our_bank_account', None) if sale else None

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

    @property
    def report_melya_bank_address(self):
        return self._get_report_bank_address_lines(
            self._get_report_melya_bank_account())

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

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

    @property
    def report_melya_bank_details_line(self):
        name = self.report_melya_bank_name
        return 'BANK DETAILS:\t\t%s' % name if name else ''

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

    @property
    def report_melya_maturity_date(self):
        maturity_dates = [
            line.maturity_date for line in (self.lines_to_pay or [])
            if getattr(line, 'maturity_date', None)]
        if maturity_dates:
            return min(maturity_dates)

    @property
    def _report_payment_order_company_account(self):
        return self._get_report_bank_account(getattr(self.company, 'party', None))

    @property
    def _report_payment_order_beneficiary_account(self):
        return self._get_report_bank_account(self.party)

    @property
    def report_payment_order_short_name(self):
        company_party = getattr(self.company, 'party', None)
        return getattr(company_party, 'rec_name', '') or ''

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

    @property
    def report_payment_order_from_account_nb(self):
        return self._get_report_bank_account_number(
            self._report_payment_order_company_account)

    @property
    def report_payment_order_to_bank_name(self):
        return self._get_report_bank_name(self._report_payment_order_beneficiary_account)

    @property
    def report_payment_order_to_bank_city(self):
        return self._get_report_bank_city(self._report_payment_order_beneficiary_account)

    @property
    def report_payment_order_amount(self):
        return self._format_report_payment_amount(self.total_amount)

    @property
    def report_payment_order_currency_code(self):
        currency = self.currency
        code = getattr(currency, 'code', None) or ''
        rec_name = getattr(currency, 'rec_name', None) or ''
        symbol = getattr(currency, 'symbol', None) or ''
        if code and any(ch.isalpha() for ch in code):
            return code
        if rec_name and any(ch.isalpha() for ch in rec_name):
            return rec_name
        if symbol and any(ch.isalpha() for ch in symbol):
            return symbol
        return code or rec_name or symbol or ''

    @property
    def report_payment_order_amount_text(self):
        return amount_to_currency_words(self.total_amount)

    @property
    def report_payment_order_value_date(self):
        value_date = self.payment_term_date or self.invoice_date
        if isinstance(value_date, dt_date):
            return value_date.strftime('%d-%m-%Y')
        return ''

    @property
    def report_payment_order_company_address(self):
        if self.invoice_address and getattr(self.invoice_address, 'full_address', None):
            return self.invoice_address.full_address
        return self.report_address

    @property
    def report_payment_order_beneficiary_account_nb(self):
        return self._get_report_bank_account_number(
            self._report_payment_order_beneficiary_account)

    @property
    def report_payment_order_beneficiary_bank_name(self):
        return self._get_report_bank_name(self._report_payment_order_beneficiary_account)

    @property
    def report_payment_order_beneficiary_bank_city(self):
        return self._get_report_bank_city(self._report_payment_order_beneficiary_account)

    @property
    def report_payment_order_swift_code(self):
        return self._get_report_bank_swift(self._report_payment_order_beneficiary_account)

    @property
    def report_payment_order_other_instructions(self):
        return self.description or ''

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

    @staticmethod
    def _get_report_current_user():
        user_id = Transaction().user
        if not user_id:
            return None
        User = Pool().get('res.user')
        return User(user_id)

    @property
    def report_payment_order_current_user(self):
        user = self._get_report_current_user()
        return getattr(user, 'rec_name', None) or ''

    @property
    def report_payment_order_current_user_email(self):
        user = self._get_report_current_user()
        party = getattr(user, 'party', None) if user else None
        if party and hasattr(party, 'contact_mechanism_get'):
            return party.contact_mechanism_get('email') or ''
        return getattr(user, 'email', None) or ''

    @property
    def report_address(self):
        trade = self._get_report_trade()
        if trade and trade.report_address:
            return trade.report_address
        if self.invoice_address and self.invoice_address.full_address:
            return self.invoice_address.full_address
        return ''

    @property
    def report_contract_number(self):
        sale = self._get_report_deal_sale()
        deal = getattr(sale, 'report_deal', None) if sale else None
        if deal:
            return deal.replace(' ', '/')
        trade = self._get_report_trade()
        if trade and trade.full_number:
            return trade.full_number
        return self.origins or ''

    @property
    def report_shipment(self):
        trade = self._get_report_trade()
        if trade and trade.report_shipment:
            return trade.report_shipment
        return self.description or ''

    @property
    def report_trader_initial(self):
        trade = self._get_report_trade()
        if trade and getattr(trade, 'trader', None):
            return trade.trader.initial or ''
        return ''
    
    @property
    def report_origin(self):
        trade = self._get_report_trade()
        if trade and getattr(trade, 'product_origin', None):
            return trade.product_origin or ''
        return ''

    @property
    def report_operator_initial(self):
        trade = self._get_report_trade()
        if trade and getattr(trade, 'operator', None):
            return trade.operator.initial or ''
        return ''

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

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

    @property
    def report_description_upper(self):
        if self.lines:
            return self._clean_report_description(self.lines[0].description)
        return ''

    @property
    def report_crop_name(self):
        trade = self._get_report_trade()
        if trade and getattr(trade, 'crop', None):
            return trade.crop.name or ''
        return ''

    @property
    def report_attributes_name(self):
        line = self._get_report_trade_line()
        if line:
            return getattr(line, 'attributes_name', '') or ''
        return ''

    @property
    def report_price(self):
        trade = self._get_report_trade()
        if trade and trade.report_price:
            return trade.report_price
        return ''

    @property
    def report_quantity_lines(self):
        details = []
        for line in self._get_report_invoice_lines():
            quantity, _ = self._get_report_invoice_line_weights(line)
            if quantity == '':
                continue
            quantity_text = self._format_report_quantity_display(quantity)
            unit = self._get_report_invoice_line_unit(line)
            unit_name = unit.rec_name.upper() if unit and unit.rec_name else ''
            lbs = self._convert_report_quantity_to_lbs(quantity, unit)
            parts = [quantity_text, unit_name]
            if lbs != '':
                parts.append(
                    f"({self._format_report_quantity_display(lbs)} LBS)")
            detail = ' '.join(part for part in parts if part)
            if detail:
                details.append(detail)
        return '\n'.join(details)

    @property
    def report_trade_blocks(self):
        blocks = []
        quantity_lines = self.report_quantity_lines.splitlines()
        rate_lines = self.report_rate_lines.splitlines()
        for index, quantity_line in enumerate(quantity_lines):
            price_line = rate_lines[index] if index < len(rate_lines) else ''
            blocks.append((quantity_line, price_line))
        return blocks

    @property
    def report_rate_currency_upper(self):
        line = self._get_report_invoice_line()
        if line:
            return line.report_rate_currency_upper
        return ''

    @property
    def report_rate_value(self):
        line = self._get_report_invoice_line()
        if line:
            return line.report_rate_value
        return ''

    @property
    def report_rate_unit_upper(self):
        line = self._get_report_invoice_line()
        if line:
            return line.report_rate_unit_upper
        return ''

    @property
    def report_rate_price_words(self):
        line = self._get_report_invoice_line()
        if line:
            return line.report_rate_price_words
        return self.report_price or ''

    @property
    def report_rate_pricing_text(self):
        line = self._get_report_invoice_line()
        if line:
            return line.report_rate_pricing_text
        return ''

    @property
    def report_rate_lines(self):
        rows = self.report_rate_rows
        if rows:
            return '\n'.join(row['label'] for row in rows)
        return ''

    @property
    def report_rate_rows(self):
        price_composition_rows = self._get_report_price_composition_rows()
        if price_composition_rows:
            return price_composition_rows
        return [{
            'label': line,
            'amount': None,
        } for line in self._get_report_rate_line_details()]

    @property
    def report_melya_rate_rows(self):
        return self._get_report_price_composition_rows()

    def _get_report_rate_line_details(self):
        details = []
        for line in self._get_report_invoice_lines():
            currency = getattr(line, 'report_rate_currency_upper', '') or ''
            value = getattr(line, 'report_rate_value', '')
            value_text = ''
            if value != '':
                value_text = self._format_report_number(
                    value, strip_trailing_zeros=False)
            unit = getattr(line, 'report_rate_unit_upper', '') or ''
            words = getattr(line, 'report_rate_price_words', '') or ''
            pricing_text = getattr(line, 'report_rate_pricing_text', '') or ''
            detail = ' '.join(
                part for part in [
                    currency,
                    value_text,
                    'PER' if unit else '',
                    unit,
                    f"({words})" if words else '',
                    pricing_text,
                ] if part)
            if detail:
                details.append(detail)
        return details

    def _get_report_price_composition_rows(self):
        rows = self._get_report_invoice_price_composition_rows()
        if rows:
            return rows
        sale = self._get_report_sale()
        if sale and getattr(sale, 'report_price_composition_rows', None):
            return sale.report_price_composition_rows
        for line in self._get_report_invoice_lines():
            origin = getattr(line, 'origin', None)
            sale = getattr(origin, 'sale', None)
            if sale and getattr(sale, 'report_price_composition_rows', None):
                return sale.report_price_composition_rows
        return []

    def _get_report_invoice_price_composition_rows(self):
        rows = []
        for invoice_line in self._get_report_invoice_lines():
            sale_line = getattr(invoice_line, 'origin', None)
            compositions = list(
                getattr(sale_line, 'price_composition', []) or [])
            if not compositions:
                continue
            quantity = self._get_report_price_composition_quantity(
                invoice_line, sale_line)
            currency = getattr(invoice_line, 'currency', None)
            sale = getattr(sale_line, 'sale', None)
            currency = currency or getattr(sale, 'currency', None)
            currency_name = (
                currency.rec_name.upper()
                if currency and getattr(currency, 'rec_name', None) else '')
            missing = [
                composition for composition in compositions
                if getattr(composition, 'price', None) in (None, '')]
            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_invoice_line_amount(
                        invoice_line, sale_line, quantity)
                    - 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:
                        amount = residual
                    else:
                        continue
                else:
                    amount = Decimal(str(price or 0)) * quantity
                label = ' '.join(
                    part for part in [
                        component,
                        'value in',
                        currency_name,
                    ] if part)
                if label:
                    rows.append({
                        'label': label,
                        'amount': amount,
                        'amount_text': self._format_report_number(
                            amount, digits='0.01',
                            strip_trailing_zeros=False,
                            use_grouping=True),
                    })
        return rows

    def _get_report_price_composition_quantity(self, invoice_line, sale_line):
        quantity = self._get_report_invoice_line_weights(invoice_line)[0]
        return self._convert_report_quantity(
            quantity,
            self._get_report_invoice_line_unit(invoice_line),
            getattr(sale_line, 'unit', None),
        )

    def _get_report_price_composition_invoice_line_amount(
            self, invoice_line, sale_line, quantity):
        amount = getattr(invoice_line, 'amount', None)
        if amount not in (None, ''):
            return Decimal(str(amount or 0))
        unit_price = getattr(invoice_line, 'unit_price', None)
        if unit_price not in (None, ''):
            return Decimal(str(unit_price or 0)) * quantity
        unit_price = Decimal(str(getattr(sale_line, 'unit_price', 0) or 0))
        premium_getter = getattr(sale_line, '_get_premium_price', None)
        premium = premium_getter() if premium_getter else getattr(
            sale_line, 'premium', 0)
        return quantity * (unit_price + Decimal(str(premium or 0)))

    @property
    def report_positive_rate_lines(self):
        sale = self._get_report_sale()
        if sale and getattr(sale, 'report_price_lines', None):
            return sale.report_price_lines
        details = []
        for line in self._get_report_invoice_lines():
            quantity = getattr(line, 'report_net', '')
            if quantity == '':
                quantity = getattr(line, 'quantity', '')
            if Decimal(str(quantity or 0)) <= 0:
                continue
            currency = getattr(line, 'report_rate_currency_upper', '') or ''
            value = getattr(line, 'report_rate_value', '')
            value_text = ''
            if value != '':
                value_text = self._format_report_number(
                    value, strip_trailing_zeros=False)
            unit = getattr(line, 'report_rate_unit_upper', '') or ''
            words = getattr(line, 'report_rate_price_words', '') or ''
            pricing_text = getattr(line, 'report_rate_pricing_text', '') or ''
            detail = ' '.join(
                part for part in [
                    currency,
                    value_text,
                    'PER' if unit else '',
                    unit,
                    f"({words})" if words else '',
                    pricing_text,
                ] if part)
            if detail:
                details.append(detail)
        return '\n'.join(details)

    @property
    def report_payment_date(self):
        trade = self._get_report_trade()
        if trade and trade.report_payment_date:
            return trade.report_payment_date
        return ''

    @property
    def report_delivery_period_description(self):
        trade = self._get_report_trade()
        if trade and getattr(trade, 'report_delivery_period_description', None):
            return trade.report_delivery_period_description
        line = self._get_report_trade_line()
        if line and getattr(line, 'del_period', None):
            return line.del_period.description or ''
        return ''

    @property
    def report_payment_description(self):
        trade = self._get_report_trade()
        if trade and trade.payment_term:
            return trade.payment_term.description or ''
        if self.payment_term:
            return self.payment_term.description or ''
        return ''

    @property
    def report_nb_bale(self):
        total_packages = Decimal(0)
        package_unit = None
        has_invoice_line_packages = False
        for line in self._get_report_invoice_lines():
            lot = getattr(line, 'lot', None)
            if not lot or getattr(lot, 'lot_qt', None) in (None, ''):
                continue
            has_invoice_line_packages = True
            if not package_unit and getattr(lot, 'lot_unit', None):
                package_unit = lot.lot_unit
            sign = Decimal(1)
            if Decimal(str(getattr(line, 'quantity', 0) or 0)) < 0:
                sign = Decimal(-1)
            total_packages += (
                Decimal(str(lot.lot_qt or 0)).quantize(
                    Decimal('1'), rounding=ROUND_HALF_UP) * sign)
        if has_invoice_line_packages:
            label = self._format_report_package_label(package_unit)
            return f"NB {label}: {int(total_packages)}"

        lots = self._get_report_invoice_lots()
        if lots:
            total_packages = Decimal(0)
            package_unit = None
            for lot in lots:
                if getattr(lot, 'lot_qt', None):
                    total_packages += Decimal(str(lot.lot_qt or 0))
                if not package_unit and getattr(lot, 'lot_unit', None):
                    package_unit = lot.lot_unit
            package_qty = total_packages.quantize(
                Decimal('1'), rounding=ROUND_HALF_UP)
            label = self._format_report_package_label(package_unit)
            return f"NB {label}: {int(package_qty)}"
        sale = self._get_report_sale()
        if sale and sale.report_nb_bale:
            return sale.report_nb_bale
        line = self._get_report_trade_line()
        if line and line.lots:
            nb_bale = sum(
                lot.lot_qt for lot in line.lots if lot.lot_type == 'physic'
            )
            return 'NB BALES: ' + str(int(nb_bale))
        return ''

    @property
    def report_cndn_nb_bale(self):
        nb_bale = self.report_nb_bale
        if nb_bale == 'NB BALES: 0':
            return 'Unchanged'
        return nb_bale

    @property
    def report_packaging(self):
        nb_bale = self.report_nb_bale
        if nb_bale.startswith('NB ') and ': ' in nb_bale:
            label, quantity = nb_bale[3:].split(': ', 1)
            return f"{quantity} {label}"
        return nb_bale

    @property
    def report_net_display(self):
        net = self.report_net
        if net == '':
            return ''
        return self._format_report_quantity_display(net)

    @property
    def report_gross_display(self):
        gross = self.report_gross
        if gross == '':
            return ''
        return self._format_report_quantity_display(gross)

    @property
    def report_lbs_display(self):
        lbs = self.report_lbs
        if lbs == '':
            return ''
        return self._format_report_quantity_display(lbs)

    @property
    def report_gross_lbs_display(self):
        lbs = self.report_gross_lbs
        if lbs == '':
            return ''
        return self._format_report_quantity_display(lbs)

    @property
    def report_weight_difference_display(self):
        difference = self.report_weight_difference
        if difference == '':
            return ''
        return self._format_report_quantity_display(difference)

    @property
    def report_weight_difference_lbs_display(self):
        lbs = self.report_weight_difference_lbs
        if lbs == '':
            return ''
        return self._format_report_quantity_display(lbs)

    @property
    def report_gross(self):
        if self.lines:
            adjustment = self._get_report_weight_adjustment_totals()
            if adjustment:
                return adjustment['landed']
            reused_gross = self._get_report_reused_lot_gross_total()
            if reused_gross is not None:
                non_reused_total = sum(
                    self._get_report_invoice_line_weights(line)[1]
                    for line in self._get_report_invoice_lines()
                    if not self._report_invoice_line_reuses_lot(line))
                return non_reused_total + reused_gross
            return sum(
                self._get_report_invoice_line_weights(line)[1]
                for line in self._get_report_invoice_lines())
        line = self._get_report_trade_line()
        if line and line.lots:
            return sum(
                lot.get_current_gross_quantity()
                for lot in line.lots if lot.lot_type == 'physic'
            )
        return ''

    @property
    def report_net(self):
        if self.lines:
            adjustment = self._get_report_weight_adjustment_totals()
            if adjustment:
                return adjustment['invoice']
            return sum(
                self._get_report_invoice_line_weights(line)[0]
                for line in self._get_report_invoice_lines())
        line = self._get_report_trade_line()
        if line and line.lots:
            return sum(
                lot.get_current_quantity()
                for lot in line.lots if lot.lot_type == 'physic'
            )
        if self.lines:
            return self.lines[0].quantity
        return ''

    @property
    def report_lbs(self):
        net = self.report_net
        if net == '':
            return ''
        invoice_line = self._get_report_invoice_line()
        unit = self._get_report_invoice_line_unit(invoice_line) if invoice_line else None
        return self._convert_report_quantity_to_lbs(net, unit)

    @property
    def report_gross_lbs(self):
        gross = self.report_gross
        if gross == '':
            return ''
        invoice_line = self._get_report_invoice_line()
        unit = self._get_report_invoice_line_unit(invoice_line) if invoice_line else None
        return self._convert_report_quantity_to_lbs(gross, unit)

    @property
    def report_weight_difference(self):
        adjustment = self._get_report_weight_adjustment_totals()
        if adjustment:
            return adjustment['difference']
        gross = self.report_gross
        net = self.report_net
        if gross == '' or net == '':
            return ''
        return gross - net

    @property
    def report_weight_difference_lbs(self):
        difference = self.report_weight_difference
        if difference == '':
            return ''
        invoice_line = self._get_report_invoice_line()
        unit = self._get_report_invoice_line_unit(invoice_line) if invoice_line else None
        return self._convert_report_quantity_to_lbs(difference, unit)

    @property
    def report_weight_unit_upper(self):
        invoice_line = self._get_report_invoice_line()
        unit = self._get_report_invoice_line_unit(invoice_line) if invoice_line else None
        if not unit:
            line = self._get_report_trade_line()
            lot = self._get_report_lot()
            unit = (
                getattr(lot, 'lot_unit_line', None)
                or getattr(line, 'unit', None) if line else None
            )
        if unit and unit.rec_name:
            return unit.rec_name.upper()
        return 'KGS'

    @property
    def report_note_title(self):
        total = Decimal(str(self.total_amount or 0))
        invoice_type = getattr(self, 'type', None)
        if not invoice_type:
            if self.sales:
                invoice_type = 'out'
            elif self.purchases:
                invoice_type = 'in'
        if invoice_type == 'out':
            if total < 0:
                return 'Credit Note'
            return 'Debit Note'
        if total < 0:
            return 'Debit Note'
        return 'Credit Note'

    @property
    def report_transportation(self):
        shipment = self._get_report_shipment()
        if not shipment:
            return ''
        if getattr(shipment, 'transport_type', None) == 'truck':
            return 'By Truck'
        vessel = getattr(shipment, 'vessel', None)
        vessel_name = getattr(vessel, 'vessel_name', None) or ''
        return vessel_name

    @property
    def report_bl_date(self):
        shipment = self._get_report_shipment()
        if shipment:
            return shipment.bl_date

    @property
    def report_bl_nb(self):
        shipment = self._get_report_shipment()
        if shipment:
            return shipment.bl_number
    
    @property
    def report_vessel(self):
        shipment = self._get_report_shipment()
        if shipment and shipment.vessel:
            return shipment.vessel.vessel_name
        
    @property
    def report_loading_port(self):
        shipment = self._get_report_shipment()
        if shipment and shipment.from_location:
            return shipment.from_location.rec_name
        return ''

    @property
    def report_discharge_port(self):
        shipment = self._get_report_shipment()
        if shipment and shipment.to_location:
            return shipment.to_location.rec_name
        return ''

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

    @property
    def report_proforma_invoice_number(self):
        lot = self._get_report_lot()
        if lot:
            line = (
                getattr(lot, 'sale_invoice_line_prov', None)
                or getattr(lot, 'invoice_line_prov', None)
            )
            if line and line.invoice:
                return line.invoice.number or ''
        return ''

    @property
    def report_proforma_invoice_date(self):
        lot = self._get_report_lot()
        if lot:
            line = (
                getattr(lot, 'sale_invoice_line_prov', None)
                or getattr(lot, 'invoice_line_prov', None)
            )
            if line and line.invoice:
                return line.invoice.invoice_date

    @property
    def report_controller_name(self):
        shipment = self._get_report_shipment()
        if shipment and shipment.controller:
            return shipment.controller.rec_name
        return ''

    @property
    def report_si_number(self):
        shipment = self._get_report_shipment()
        if shipment:
            return shipment.number or ''
        return ''

    @property
    def report_si_reference(self):
        shipment = self._get_report_shipment()
        if shipment:
            return getattr(shipment, 'reference', None) or ''
        return ''

    @property
    def report_freight_amount(self):
        fee = self._get_report_freight_fee()
        if fee:
            return fee.get_amount()
        return ''

    @property
    def report_freight_currency_symbol(self):
        fee = self._get_report_freight_fee()
        if fee and fee.currency:
            return fee.currency.symbol or ''
        if self.currency:
            return self.currency.symbol or ''
        return 'USD'


class InvoicePaddingReport(ModelSQL, ModelView):
    "Invoice with padding"
    __name__ = 'invoice.padding.report'

    party = fields.Many2One('party.party', "Party")
    sale = fields.Many2One('sale.sale', "Sale")
    sale_line = fields.Many2One('sale.line', "Sale Line")
    lot = fields.Many2One('lot.lot', "Lot")
    product = fields.Many2One('product.product', "Product")
    currency = fields.Many2One('currency.currency', "Currency")
    unit = fields.Many2One('product.uom', "Unit")
    padding_quantity = fields.Numeric("Padding Quantity", digits='unit')
    unit_price = fields.Numeric("Unit Price", digits=(16, 6))
    padding_amount = fields.Numeric("Padding Amount", digits=(16, 2))
    provisional_invoice = fields.Many2One(
        'account.invoice', "Provisional Invoice")
    provisional_date = fields.Date("Provisional Date")
    provisional_state = fields.Selection([
            ('draft', "Draft"),
            ('validated', "Validated"),
            ('posted', "Posted"),
            ('paid', "Paid"),
            ('cancelled', "Cancelled"),
            ], "Provisional State")
    final_invoice = fields.Many2One('account.invoice', "Final Invoice")
    final_date = fields.Date("Final Date")
    final_state = fields.Selection([
            (None, ''),
            ('draft', "Draft"),
            ('validated', "Validated"),
            ('posted', "Posted"),
            ('paid', "Paid"),
            ('cancelled', "Cancelled"),
            ], "Final State")
    padding_status = fields.Selection([
            ('active', "Active"),
            ('reversed', "Reversed"),
            ], "Padding Status")

    @classmethod
    def table_query(cls):
        pool = Pool()
        Lot = pool.get('lot.lot')
        SaleLine = pool.get('sale.line')
        Sale = pool.get('sale.sale')
        InvoiceLine = pool.get('account.invoice.line')
        Invoice = pool.get('account.invoice')

        lot = Lot.__table__()
        sale_line = SaleLine.__table__()
        sale = Sale.__table__()
        prov_line = InvoiceLine.__table__()
        prov_invoice = Invoice.__table__()
        final_line = InvoiceLine.__table__()
        final_invoice = Invoice.__table__()

        context = Transaction().context
        party = context.get('party')
        currency = context.get('currency')
        status_filter = context.get('status')
        from_date = context.get('from_date')
        to_date = context.get('to_date')
        sale_filter = context.get('sale')
        lot_filter = context.get('lot')

        reversed_condition = (
            (final_invoice.id > 0)
            & ~final_invoice.state.in_(['draft', 'cancelled']))
        padding_status = Case(
            (reversed_condition, 'reversed'),
            else_='active')

        where = Literal(True)
        where &= lot.sale_invoice_padding > 0
        where &= prov_invoice.id > 0
        if party:
            where &= prov_invoice.party == party
        if currency:
            where &= prov_invoice.currency == currency
        if status_filter and status_filter != 'all':
            where &= padding_status == status_filter
        if from_date:
            where &= prov_invoice.invoice_date >= from_date
        if to_date:
            where &= prov_invoice.invoice_date <= to_date
        if sale_filter:
            where &= sale.id == sale_filter
        if lot_filter:
            where &= lot.id == lot_filter

        return (
            lot
            .join(sale_line, 'LEFT', condition=lot.sale_line == sale_line.id)
            .join(sale, 'LEFT', condition=sale_line.sale == sale.id)
            .join(prov_line, 'LEFT',
                condition=lot.sale_invoice_line_prov == prov_line.id)
            .join(prov_invoice, 'LEFT',
                condition=prov_line.invoice == prov_invoice.id)
            .join(final_line, 'LEFT',
                condition=lot.sale_invoice_line == final_line.id)
            .join(final_invoice, 'LEFT',
                condition=final_line.invoice == final_invoice.id)
            .select(
                Literal(0).as_('create_uid'),
                CurrentTimestamp().as_('create_date'),
                Literal(0).as_('write_uid'),
                Literal(None).as_('write_date'),
                lot.id.as_('id'),
                prov_invoice.party.as_('party'),
                sale.id.as_('sale'),
                sale_line.id.as_('sale_line'),
                lot.id.as_('lot'),
                prov_line.product.as_('product'),
                prov_invoice.currency.as_('currency'),
                prov_line.unit.as_('unit'),
                lot.sale_invoice_padding.as_('padding_quantity'),
                prov_line.unit_price.as_('unit_price'),
                (lot.sale_invoice_padding * prov_line.unit_price).as_(
                    'padding_amount'),
                prov_invoice.id.as_('provisional_invoice'),
                prov_invoice.invoice_date.as_('provisional_date'),
                prov_invoice.state.as_('provisional_state'),
                final_invoice.id.as_('final_invoice'),
                final_invoice.invoice_date.as_('final_date'),
                final_invoice.state.as_('final_state'),
                padding_status.as_('padding_status'),
                where=where,
            ))


class InvoicePaddingContext(ModelView):
    "Invoice with padding context"
    __name__ = 'invoice.padding.context'

    from_date = fields.Date("From")
    to_date = fields.Date("To")
    party = fields.Many2One('party.party', "Party")
    currency = fields.Many2One('currency.currency', "Currency")
    status = fields.Selection([
            ('all', "All"),
            ('active', "Active"),
            ('reversed', "Reversed"),
            ], "Status")
    sale = fields.Many2One('sale.sale', "Sale")
    lot = fields.Many2One('lot.lot', "Lot")

    @classmethod
    def default_from_date(cls):
        Date = Pool().get('ir.date')
        return Date.today().replace(day=1, month=1, year=1999)

    @classmethod
    def default_to_date(cls):
        Date = Pool().get('ir.date')
        return Date.today()

    @classmethod
    def default_status(cls):
        return 'active'


class InvoiceLine(metaclass=PoolMeta):
    __name__ = 'account.invoice.line'

    def _get_report_trade(self):
        origin = getattr(self, 'origin', None)
        if not origin:
            return None
        return getattr(origin, 'sale', None) or getattr(origin, 'purchase', None)

    def _get_report_trade_line(self):
        return getattr(self, 'origin', None)

    @property
    def report_product_description(self):
        if self.product:
            return self.product.description or ''
        origin = getattr(self, 'origin', None)
        if origin and getattr(origin, 'product', None):
            return origin.product.description or ''
        return ''

    @property
    def report_product_name(self):
        if self.product:
            return self.product.name or ''
        origin = getattr(self, 'origin', None)
        if origin and getattr(origin, 'product', None):
            return origin.product.name or ''
        return ''

    @property
    def report_description_upper(self):
        return Invoice._clean_report_description(self.description)

    @property
    def report_rate_currency_upper(self):
        origin = self._get_report_trade_line()
        currency = getattr(origin, 'linked_currency', None) or self.currency
        if currency and currency.rec_name:
            return currency.rec_name.upper()
        return ''

    @property
    def report_rate_value(self):
        origin = self._get_report_trade_line()
        if origin and getattr(origin, 'price_type', None) == 'basis':
            if getattr(origin, 'enable_linked_currency', False) and getattr(origin, 'linked_currency', None):
                return Decimal(str(origin.premium or 0))
            return Decimal(str(origin._get_premium_price() or 0))
        return self.unit_price if self.unit_price is not None else ''

    @property
    def report_rate_unit_upper(self):
        origin = self._get_report_trade_line()
        unit = getattr(origin, 'linked_unit', None) or self.unit
        if unit and unit.rec_name:
            return unit.rec_name.upper()
        return ''

    @property
    def report_rate_price_words(self):
        origin = self._get_report_trade_line()
        if origin and getattr(origin, 'price_type', None) == 'basis':
            value = self.report_rate_value
            if self.report_rate_currency_upper == 'USC':
                return amount_to_currency_words(value, 'USC', 'USC')
            return amount_to_currency_words(value)
        trade = self._get_report_trade()
        if trade and getattr(trade, 'report_price', None):
            return trade.report_price
        return ''

    @property
    def report_rate_pricing_text(self):
        origin = self._get_report_trade_line()
        return getattr(origin, 'get_pricing_text', '') or ''

    @property
    def report_crop_name(self):
        trade = self._get_report_trade()
        if trade and getattr(trade, 'crop', None):
            return trade.crop.name or ''
        return ''

    @property
    def report_attributes_name(self):
        origin = getattr(self, 'origin', None)
        if origin:
            return getattr(origin, 'attributes_name', '') or ''
        return ''

    @property
    def report_net(self):
        if self.type == 'line':
            invoice = getattr(self, 'invoice', None)
            snapshots = Invoice._get_report_invoice_line_weight_snapshots(self)
            if snapshots:
                sign = Invoice._get_report_line_sign(self)
                net = sum(
                    Decimal(str(snapshot.net_quantity or 0))
                    for snapshot in snapshots)
                unit = Invoice._get_report_invoice_line_unit(self)
                net += Invoice._get_report_invoice_line_padding(self, unit)
                return net * sign
            if invoice and invoice._report_invoice_line_reuses_lot(self):
                return Invoice._get_report_invoice_line_quantity_from_line(self)
            lot = getattr(self, 'lot', None)
            if lot:
                net, _ = Invoice._get_report_lot_hist_weights(lot)
                if net is None:
                    net = 0
                sign = Invoice._get_report_line_sign(self)
                return Decimal(str(net or 0)) * sign
            return self.quantity
        return ''

    @property
    def report_lbs(self):
        net = self.report_net
        if net == '':
            return ''
        unit = Invoice._get_report_invoice_line_unit(self)
        return Invoice._convert_report_quantity_to_lbs(net, unit)


class ReportTemplateMixin:
    @classmethod
    def _get_purchase_trade_configuration(cls):
        Configuration = Pool().get('purchase_trade.configuration')
        configurations = Configuration.search([], limit=1)
        return configurations[0] if configurations else None

    @classmethod
    def _get_action_name(cls, action):
        if isinstance(action, dict):
            return action.get('name') or ''
        return getattr(action, 'name', '') or ''

    @classmethod
    def _get_action_report_path(cls, action):
        if isinstance(action, dict):
            return action.get('report') or ''
        return getattr(action, 'report', '') or ''

    @classmethod
    def _resolve_template_path(cls, action, field_name, default_prefix):
        config = cls._get_purchase_trade_configuration()
        template = getattr(config, field_name, '') if config else ''
        template = (template or '').strip()
        if not template:
            raise UserError('No template found')
        if '/' not in template:
            return f'{default_prefix}/{template}'
        return template

    @classmethod
    def _load_report_content(cls, report_path):
        file_name = report_path.replace('/', os.sep)
        try:
            with file_open(file_name, mode='rb') as fp:
                return fp.read()
        except FileNotFoundError:
            modules_dir = Path(__file__).resolve().parent.parent
            path = modules_dir.joinpath(*report_path.split('/'))
            try:
                return path.read_bytes()
            except FileNotFoundError:
                raise UserError('No template found')

    @classmethod
    def _get_resolved_action(cls, action):
        report_path = cls._resolve_configured_report_path(action)
        report_content = cls._load_report_content(report_path)
        if isinstance(action, dict):
            resolved = dict(action)
            resolved['report'] = report_path
            resolved['report_content'] = report_content
            return resolved
        setattr(action, 'report', report_path)
        setattr(action, 'report_content', report_content)
        action._template_cache.clear()
        return action

    @classmethod
    def _execute(cls, records, header, data, action):
        resolved_action = cls._get_resolved_action(action)
        return super()._execute(records, header, data, resolved_action)


class InvoiceReport(ReportTemplateMixin, BaseInvoiceReport):
    __name__ = 'account.invoice'

    @classmethod
    def _resolve_configured_report_path(cls, action):
        report_path = cls._get_action_report_path(action) or ''
        action_name = cls._get_action_name(action)
        legacy_prefix = None

        if (report_path.endswith('/prepayment.fodt')
                or action_name == 'Prepayment'):
            field_name = 'invoice_prepayment_report_template'
        elif report_path.endswith('/packing_list.fodt'):
            field_name = 'invoice_packing_list_report_template'
        elif (report_path.endswith('/payment_order.fodt')
                or action_name == 'Payment Order'):
            field_name = 'invoice_payment_order_report_template'
        elif (report_path.endswith('/invoice_ict_final.fodt')
                or action_name == 'CN/DN'):
            field_name = 'invoice_cndn_report_template'
        elif (report_path.endswith('/commission__ict_final.fodt')
                or action_name == 'CN/DN Commission'):
            field_name = 'invoice_commission_cndn_report_template'
        elif (report_path.endswith('/sale_commission_ict.fodt')
                or action_name == 'Commission invoice Sale'):
            field_name = 'sale_commission_report_template'
            legacy_prefix = 'sale/'
        elif (report_path.endswith('/purchase_commission_ict.fodt')
                or action_name == 'Commission invoice Purchase'):
            field_name = 'purchase_commission_report_template'
            legacy_prefix = 'purchase/'
        else:
            field_name = 'invoice_report_template'
        resolved_path = cls._resolve_template_path(
            action, field_name, 'account_invoice')
        if legacy_prefix and resolved_path.startswith(legacy_prefix):
            return 'account_invoice/%s' % resolved_path.rsplit('/', 1)[-1]
        return resolved_path


class SaleReport(ReportTemplateMixin, BaseSaleReport):
    __name__ = 'sale.sale'

    @classmethod
    def _resolve_configured_report_path(cls, action):
        report_path = cls._get_action_report_path(action)
        action_name = cls._get_action_name(action)
        if report_path.endswith('/bill.fodt') or action_name == 'Bill':
            field_name = 'sale_bill_report_template'
        elif report_path.endswith('/sale_final.fodt') or action_name == 'Sale (final)':
            field_name = 'sale_final_report_template'
        else:
            field_name = 'sale_report_template'
        return cls._resolve_template_path(action, field_name, 'sale')


class PurchaseReport(ReportTemplateMixin, BasePurchaseReport):
    __name__ = 'purchase.purchase'

    @classmethod
    def _resolve_configured_report_path(cls, action):
        report_path = cls._get_action_report_path(action)
        action_name = cls._get_action_name(action)
        return cls._resolve_template_path(
            action, 'purchase_report_template', 'purchase')
