# This file is part of Tryton.  The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import datetime as dt
import re
from collections import defaultdict
from decimal import Decimal
from itertools import chain, groupby

import stdnum.exceptions
from genshi.template.text import TextTemplate
from sql import Null
from sql.aggregate import Sum
from sql.conditionals import Coalesce
from sql.functions import CharLength, Round
from sql.operators import Exists
from stdnum import iso7064, iso11649

from trytond import backend, config
from trytond.i18n import gettext
from trytond.model import (
    ChatMixin, DeactivableMixin, Index, ModelSQL, ModelView, Unique, Workflow,
    dualmethod, fields, sequence_ordered)
from trytond.model.exceptions import AccessError
from trytond.modules.account.exceptions import AccountMissing
from trytond.modules.account.tax import TaxableMixin
from trytond.modules.company.model import (
    employee_field, reset_employee, set_employee)
from trytond.modules.currency.fields import Monetary
from trytond.modules.product import price_digits
from trytond.pool import Pool
from trytond.pyson import Bool, Eval, Id, If
from trytond.report import Report
from trytond.rpc import RPC
from trytond.tools import (
    cached_property, firstline, grouped_slice, reduce_ids, slugify,
    sqlite_apply_types)
from trytond.transaction import Transaction
from trytond.wizard import (
    Button, StateAction, StateReport, StateTransition, StateView, Wizard)

from .exceptions import (
    InvoiceFutureWarning, InvoiceNumberError, InvoicePaymentTermDateWarning,
    InvoiceSimilarWarning, InvoiceTaxesWarning, InvoiceTaxValidationError,
    InvoiceValidationError, PayInvoiceError)

if config.getboolean('account_invoice', 'filestore', default=False):
    file_id = 'invoice_report_cache_id'
    store_prefix = config.get('account_invoice', 'store_prefix', default=None)
else:
    file_id = None
    store_prefix = None


class InvoiceReportMixin:
    __slots__ = ()

    invoice_report_cache = fields.Binary(
        "Invoice Report", readonly=True,
        file_id=file_id, store_prefix=store_prefix)
    invoice_report_cache_id = fields.Char("Invoice Report ID", readonly=True)
    invoice_report_format = fields.Char("Invoice Report Format", readonly=True)


class Invoice(
        Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin,
        ChatMixin):
    __name__ = 'account.invoice'
    _rec_name = 'number'
    _order_name = 'number'

    _states = {
        'readonly': Eval('state') != 'draft',
    }

    company = fields.Many2One(
        'company.company', "Company", required=True,
        states={
            'readonly': (
                _states['readonly']
                | Eval('party', True)
                | Eval('lines', [0])),
            },
        context={
            'party_contact_mechanism_usage': 'invoice',
            })
    company_party = fields.Function(
        fields.Many2One(
            'party.party', "Company Party",
            context={
                'company': Eval('company', -1),
                'party_contact_mechanism_usage': 'invoice',
                },
            depends={'company'}),
        'on_change_with_company_party')
    tax_identifier = fields.Many2One(
        'party.identifier', "Tax Identifier", ondelete='RESTRICT',
        states=_states)
    type = fields.Selection([
            ('out', "Customer"),
            ('in', "Supplier"),
            ], "Type", required=True,
        states={
            'readonly': ((Eval('state') != 'draft')
                | Eval('context', {}).get('type')
                | (Eval('lines', [0]) & Eval('type'))),
            })
    type_name = fields.Function(fields.Char('Type'), 'get_type_name')
    number = fields.Char("Number", readonly=True)
    number_alnum = fields.Char("Number Alphanumeric", readonly=True)
    number_digit = fields.Integer("Number Digit", readonly=True)
    number_digit._sql_type = 'BIGINT'
    reference = fields.Char(
        "Reference",
        states={
            'readonly': (
                Eval('has_report_cache', False)
                & ~Id('account', 'group_account_admin').in_(
                    Eval('context', {}).get('groups', []))),
            })
    description = fields.Char("Description", size=None,
        states={
            'readonly': (
                (Eval('state') != 'draft')
                & ~Id('account', 'group_account_admin').in_(
                    Eval('context', {}).get('groups', []))),
            })
    validated_by = employee_field(
        "Validated By",
        states=['validated', 'posted', 'paid', 'cancelled'])
    posted_by = employee_field(
        "Posted By",
        states=['posted', 'paid', 'cancelled'])
    state = fields.Selection([
            ('draft', "Draft"),
            ('validated', "Validated"),
            ('posted', "Posted"),
            ('paid', "Paid"),
            ('cancelled', "Cancelled"),
            ], "State", readonly=True, sort=False)
    invoice_date = fields.Date('Invoice Date',
        states={
            'readonly': Eval('state').in_(
                If(Eval('type') == 'in',
                    ['validated', 'posted', 'paid'],
                    ['posted', 'paid'])),
            'required': Eval('state').in_(
                If(Eval('type') == 'in',
                    ['validated', 'posted', 'paid'],
                    ['posted', 'paid'])),
            })
    accounting_date = fields.Date('Accounting Date', states=_states)
    payment_term_date = fields.Date(
        "Payment Term Date", states=_states,
        help="The date from which the payment term is calculated.\n"
        "Leave empty to use the invoice date.")
    supplier_payment_reference_type = fields.Selection([
            (None, ""),
            ('creditor_reference', "Creditor Reference"),
            ], "Payment Reference Type",
        states={
            'invisible': Eval('type', 'out') != 'in',
            })
    supplier_payment_reference_type_string = (
        supplier_payment_reference_type.translated(
            'supplier_payment_reference_type'))
    supplier_payment_reference = fields.Char(
        "Payment Reference",
        states={
            'invisible': Eval('type', 'out') != 'in',
            })
    customer_payment_reference = fields.Function(fields.Char(
            "Customer Payment Reference",
            states={
                'invisible': Eval('type', 'out') != 'out',
                }),
        'get_customer_payment_reference',
        searcher='search_customer_payment_reference')
    sequence = fields.Integer("Sequence", readonly=True)
    sequence_type_cache = fields.Selection([
            (None, ""),
            ('invoice', "Invoice"),
            ('credit_note', "Credit Note"),
            ], "Sequence Type Cache", readonly=True)
    party = fields.Many2One(
        'party.party', 'Party', required=True, states=_states,
        context={
            'company': Eval('company', -1),
            'party_contact_mechanism_usage': 'invoice',
            },
        depends={'company'})
    party_tax_identifier = fields.Many2One(
        'party.identifier', "Party Tax Identifier", ondelete='RESTRICT',
        states=_states)
    party_lang = fields.Function(fields.Char('Party Language'),
        'on_change_with_party_lang')
    invoice_address = fields.Many2One('party.address', 'Invoice Address',
        required=True, states=_states,
        domain=[('party', '=', Eval('party', -1))])
    currency = fields.Many2One('currency.currency', 'Currency', required=True,
        states={
            'readonly': (
                _states['readonly']
                | (Eval('lines', [0]) & Eval('currency'))),
            })
    currency_date = fields.Function(fields.Date('Currency Date'),
        'on_change_with_currency_date')
    journal = fields.Many2One(
        'account.journal', 'Journal',
        states={
            'readonly': _states['readonly'],
            'required': Eval('state') != 'draft',
            },
        context={
            'company': Eval('company', -1),
            }, depends={'company'})
    move = fields.Many2One('account.move', 'Move', readonly=True,
        domain=[
            ('company', '=', Eval('company', -1)),
            ])
    additional_moves = fields.Many2Many(
        'account.invoice-additional-account.move', 'invoice', 'move',
        "Additional Moves", readonly=True,
        domain=[
            ('company', '=', Eval('company', -1)),
            ],
        states={
            'invisible': ~Eval('additional_moves'),
            })
    cancel_move = fields.Many2One('account.move', 'Cancel Move', readonly=True,
        domain=[
            ('company', '=', Eval('company', -1)),
            ],
        states={
            'invisible': ~Eval('cancel_move'),
            })
    account = fields.Many2One('account.account', 'Account', required=True,
        states=_states,
        domain=[
            ('closed', '!=', True),
            ('company', '=', Eval('company', -1)),
            If(Eval('type') == 'out',
                ('type.receivable', '=', True),
                ('type.payable', '=', True)),
            ],
        context={
            'date': If(Eval('accounting_date'),
                Eval('accounting_date'),
                Eval('invoice_date')),
            })
    payment_term = fields.Many2One(
        'account.invoice.payment_term', "Payment Term",
        ondelete='RESTRICT', states=_states)
    alternative_payees = fields.Many2Many(
        'account.invoice.alternative_payee', 'invoice', 'party',
        "Alternative Payee", states=_states,
        size=If(~Eval('move'), 1, None),
        context={
            'company': Eval('company', -1),
            },
        depends=['company'])
    lines = fields.One2Many('account.invoice.line', 'invoice', 'Lines',
        domain=[
            ('company', '=', Eval('company', -1)),
            ('currency', '=', Eval('currency', -1)),
            ['OR',
                ('account', '=', None),
                ('account', '!=', Eval('account', -1)),
                ],
            ['OR',
                ('invoice_type', '=', Eval('type')),
                ('invoice_type', '=', None),
                ],
            ['OR',
                ('party', '=', Eval('party', -1)),
                ('party', '=', None),
                ],
            ],
        states={
            'readonly': (
                (Eval('state') != 'draft')
                | ~Eval('company')
                | ~Eval('currency')
                | ~Eval('account')),
            })
    line_lines = fields.One2Many(
        'account.invoice.line', 'invoice', "Line - Lines", readonly=True,
        filter=[
            ('type', '=', 'line'),
            ])
    taxes = fields.One2Many(
        'account.invoice.tax', 'invoice', 'Tax Lines',
        domain=[
            ('account', '!=', Eval('account', -1)),
            ],
        states={
            'readonly': (
                (Eval('state') != 'draft')
                | ~Eval('account')),
            })
    comment = fields.Text("Comment",
        states={
            'readonly': (
                (Eval('state') != 'draft')
                & ~Id('account', 'group_account_admin').in_(
                    Eval('context', {}).get('groups', []))),
            })
    origins = fields.Function(fields.Char('Origins'), 'get_origins')
    untaxed_amount = fields.Function(Monetary(
            "Untaxed", currency='currency', digits='currency'),
        'get_amount', searcher='search_untaxed_amount')
    untaxed_amount_cache = fields.Numeric(
        "Untaxed Cache", digits='currency', readonly=True)
    tax_amount = fields.Function(Monetary(
            "Tax", currency='currency', digits='currency'),
        'get_amount', searcher='search_tax_amount')
    tax_amount_cache = fields.Numeric(
        "Tax Cache", digits='currency', readonly=True)
    total_amount = fields.Function(Monetary(
            "Total", currency='currency', digits='currency'),
        'get_amount', searcher='search_total_amount')
    total_amount_cache = fields.Numeric(
        "Total Cache", digits='currency', readonly=True)
    reconciled = fields.Function(fields.Date('Reconciled',
            states={
                'invisible': ~Eval('reconciled'),
                }),
            'get_reconciled')
    lines_to_pay = fields.Function(fields.Many2Many(
            'account.move.line', None, None, 'Lines to Pay'),
        'get_lines_to_pay')
    payment_lines = fields.Many2Many('account.invoice-account.move.line',
        'invoice', 'line', string='Payment Lines',
        domain=[
            ('account', '=', Eval('account', -1)),
            ['OR',
                ('currency', '=', Eval('currency', -1)),
                ('second_currency', '=', Eval('currency', -1)),
                ],
            ['OR',
                ('party', 'in', [None, Eval('party', -1)]),
                ('party', 'in', Eval('alternative_payees', [])),
                ],
            ['OR',
                ('invoice_payment', '=', None),
                ('invoice_payment', '=', Eval('id', -1)),
                ],
            If(Eval('type') == 'out',
                If(Eval('total_amount', 0) >= 0,
                    ('debit', '=', 0),
                    ('credit', '=', 0)),
                If(Eval('total_amount', 0) >= 0,
                    ('credit', '=', 0),
                    ('debit', '=', 0))),
            ],
        states={
            'invisible': Eval('state') == 'paid',
            'readonly': Eval('state') != 'posted',
            })
    reconciliation_lines = fields.Function(fields.Many2Many(
            'account.move.line', None, None, "Payment Lines",
            states={
                'invisible': (
                    ~Eval('state').in_(['paid', 'cancelled'])
                    | ~Eval('reconciliation_lines')),
                }),
        'get_reconciliation_lines')
    amount_to_pay_today = fields.Function(Monetary(
            "Amount to Pay Today", currency='currency', digits='currency'),
        'get_amount_to_pay')
    amount_to_pay = fields.Function(Monetary(
            "Amount to Pay", currency='currency', digits='currency'),
        'get_amount_to_pay')
    invoice_report_revisions = fields.One2Many(
        'account.invoice.report.revision', 'invoice',
        "Invoice Report Revisions", readonly=True,
        states={
            'invisible': ~Eval('invoice_report_revisions'),
            })
    allow_cancel = fields.Function(
        fields.Boolean("Allow Cancel Invoice"), 'get_allow_cancel')
    has_payment_method = fields.Function(
        fields.Boolean("Has Payment Method"), 'get_has_payment_method')
    has_report_cache = fields.Function(
        fields.Boolean("Has Report Cached"), 'get_has_report_cache')
    has_account_move = fields.Function(
        fields.Boolean("Has Account Move"), 'on_change_with_has_account_move')

    del _states

    @classmethod
    def __setup__(cls):
        pool = Pool()
        Party = pool.get('party.party')
        cls.number.search_unaccented = False
        cls.reference.search_unaccented = False
        super().__setup__()
        t = cls.__table__()
        cls._sql_indexes.update({
                Index(t, (t.reference, Index.Similarity())),
                Index(
                    t,
                    (t.state, Index.Equality(cardinality='low')),
                    where=t.state.in_(['draft', 'validated', 'posted'])),
                Index(t, (t.total_amount_cache, Index.Range())),
                Index(
                    t,
                    (t.total_amount_cache, Index.Equality(cardinality='low')),
                    include=[t.id],
                    where=t.total_amount_cache == Null),
                Index(t, (t.untaxed_amount_cache, Index.Range())),
                Index(
                    t,
                    (t.untaxed_amount_cache,
                        Index.Equality(cardinality='low')),
                    include=[t.id],
                    where=t.untaxed_amount_cache == Null),
                Index(t, (t.tax_amount_cache, Index.Range())),
                Index(
                    t,
                    (t.tax_amount_cache, Index.Equality(cardinality='low')),
                    include=[t.id],
                    where=t.tax_amount_cache == Null),
                })
        cls._check_modify_exclude = {
            'state', 'alternative_payees', 'payment_lines',
            'move', 'cancel_move', 'additional_moves', 'description',
            'invoice_report_cache', 'invoice_report_format', 'comment',
            'total_amount_cache', 'tax_amount_cache', 'untaxed_amount_cache',
            'lines', 'reference', 'invoice_report_cache_id',
            'invoice_report_revisions'}
        cls._order = [
            ('number', 'DESC NULLS FIRST'),
            ('id', 'DESC'),
            ]
        cls.journal.domain = [
            If(Eval('type') == 'out',
                ('type', 'in', cls._journal_types('out')),
                ('type', 'in', cls._journal_types('in'))),
            ]
        tax_identifier_types = Party.tax_identifier_types()
        cls.tax_identifier.domain = [
            ('party', '=', Eval('company_party', -1)),
            ('type', 'in', tax_identifier_types),
            ]
        cls.party_tax_identifier.domain = [
            ('party', '=', Eval('party', -1)),
            ('type', 'in', tax_identifier_types),
            ]
        cls._transitions |= set((
                ('draft', 'validated'),
                ('validated', 'posted'),
                ('draft', 'posted'),
                ('posted', 'posted'),
                ('posted', 'paid'),
                ('validated', 'draft'),
                ('paid', 'posted'),
                ('draft', 'cancelled'),
                ('validated', 'cancelled'),
                ('posted', 'cancelled'),
                ('cancelled', 'draft'),
                ))
        cls._buttons.update({
                'cancel': {
                    'invisible': ~Eval('allow_cancel', False),
                    'depends': ['allow_cancel'],
                    },
                'draft': {
                    'invisible': (
                        ~Eval('state').in_(['cancelled', 'validated'])
                        | ((Eval('state') == 'cancelled')
                            & Eval('cancel_move', -1))),
                    'icon': If(Eval('state') == 'cancelled', 'tryton-undo',
                        'tryton-back'),
                    'depends': ['state'],
                    },
                'validate_invoice': {
                    'pre_validate':
                        ['OR',
                            ('invoice_date', '!=', None),
                            ('type', '!=', 'in'),
                        ],
                    'invisible': Eval('state') != 'draft',
                    'depends': ['state'],
                    },
                'post': {
                    'pre_validate':
                        ['OR',
                            ('invoice_date', '!=', None),
                            ('type', '!=', 'in'),
                        ],
                    'invisible': (~Eval('state').in_(['draft', 'validated'])
                        | ((Eval('state') == 'posted')
                           & ~Eval('has_account_move', False))),
                    'depends': ['state', 'has_account_move'],
                    },
                'pay': {
                    'invisible': (
                        (Eval('state') != 'posted')
                        | ~Eval('has_payment_method', False)),
                    'depends': ['state', 'has_payment_method'],
                    },
                'reschedule_lines_to_pay': {
                    'invisible': (
                        ~Eval('lines_to_pay') | Eval('reconciled', False)),
                    'depends': ['lines_to_pay', 'reconciled'],
                    },
                'delegate_lines_to_pay': {
                    'invisible': (
                        ~Eval('lines_to_pay') | Eval('reconciled', False)),
                    'depends': ['lines_to_pay', 'reconciled'],
                    },
                'process': {
                    'invisible': ~Eval('state').in_(
                        ['posted', 'paid']),
                    'depends': ['state'],
                    },
                })
        cls.__rpc__.update({
                'post': RPC(
                    readonly=False, instantiate=0, fresh_session=True),
                })

    @classmethod
    def __register__(cls, module_name):
        super().__register__(module_name)
        table = cls.__table_handler__(module_name)

        # Migration from 6.6: drop not null on journal
        table.not_null_action('journal', 'remove')

    @classmethod
    def order_number(cls, tables):
        table, _ = tables[None]
        return [
            ~((table.state == 'cancelled') & (table.number == Null)),
            CharLength(table.number), table.number]

    @staticmethod
    def default_type():
        return Transaction().context.get('type', 'out')

    @staticmethod
    def default_state():
        return 'draft'

    @staticmethod
    def default_currency():
        Company = Pool().get('company.company')
        if Transaction().context.get('company'):
            company = Company(Transaction().context['company'])
            return company.currency.id

    @staticmethod
    def default_company():
        return Transaction().context.get('company')

    @fields.depends(
        'company', 'tax_identifier', methods=['on_change_with_company_party'])
    def on_change_company(self):
        company_party = self.on_change_with_company_party()
        if self.company:
            if self.tax_identifier:
                if self.tax_identifier.party != company_party:
                    self.tax_identifier = None
        else:
            self.tax_identifier = None

    @fields.depends('company')
    def on_change_with_company_party(self, name=None):
        return self.company.party if self.company else None

    @fields.depends(methods=['set_journal', 'on_change_party'])
    def on_change_type(self):
        self.set_journal()
        self.on_change_party()

    @classmethod
    def _journal_types(cls, invoice_type):
        if invoice_type == 'out':
            return ['revenue']
        else:
            return ['expense']

    @fields.depends('type')
    def set_journal(self, pattern=None):
        pool = Pool()
        Journal = pool.get('account.journal')
        pattern = pattern.copy() if pattern is not None else {}
        pattern.setdefault('type', {
                'out': 'revenue',
                'in': 'expense',
                }.get(self.type))
        self.journal = Journal.find(pattern)

    @classmethod
    def order_accounting_date(cls, tables):
        table, _ = tables[None]
        return [Coalesce(table.accounting_date, table.invoice_date)]

    @fields.depends('party', 'type', methods=['_update_account'])
    def on_change_party(self):
        if self.party:
            self.invoice_address = self.party.address_get(type='invoice')
            self.party_tax_identifier = self.party.tax_identifier
            if self.type == 'out':
                self.account = self.party.account_receivable_used
                self.payment_term = self.party.customer_payment_term
            elif self.type == 'in':
                self.account = self.party.account_payable_used
                self.payment_term = self.party.supplier_payment_term
        else:
            self.invoice_address = None
            self.account = None
            self.payment_term = None
            self.party_tax_identifier = None
        self._update_account()

    @fields.depends(methods=['_update_account'])
    def on_change_accounting_date(self):
        self._update_account()

    @fields.depends(methods=['_update_account'])
    def on_change_invoice_date(self):
        self._update_account()

    @fields.depends('account', 'accounting_date', 'invoice_date')
    def _update_account(self):
        "Update account to current account"
        if self.account:
            account = self.account.current(
                date=self.accounting_date or self.invoice_date)
            if account != self.account:
                self.account = account

    @fields.depends('invoice_date', 'company')
    def on_change_with_currency_date(self, name=None):
        Date = Pool().get('ir.date')
        if self.company:
            company_id = self.company.id
        else:
            company_id = Transaction().context.get('company')
        with Transaction().set_context(company=company_id):
            return self.invoice_date or Date.today()

    @fields.depends(
        'supplier_payment_reference_type',
        'supplier_payment_reference')
    def on_change_with_supplier_payment_reference(self):
        reference = self.supplier_payment_reference
        if reference and (type := self.supplier_payment_reference_type):
            reference = getattr(
                self, f'_format_supplier_payment_reference_{type}')(reference)
        return reference

    @classmethod
    def _format_supplier_payment_reference_creditor_reference(cls, reference):
        try:
            return iso11649.format(reference)
        except stdnum.exceptions.ValidationError:
            return reference

    def get_customer_payment_reference(self, name):
        return self.format_customer_payment_reference(
            self._customer_payment_reference_type())

    @classmethod
    def search_customer_payment_reference(cls, name, clause):
        _, operator, value = clause[:3]
        if operator == '=':
            return ['OR', *cls._search_customer_payment_reference(value)]
        else:
            return []

    @classmethod
    def _search_customer_payment_reference(cls, value):
        if iso11649.is_valid(value):
            yield ('number_alnum', '=', value[4:])

    def _customer_payment_reference_type(self):
        return 'creditor_reference'

    def _customer_payment_reference_number_alnum(self, source):
        if source == 'invoice':
            return self.number_alnum
        elif source == 'party':
            return self.party.code_alnum

    def _customer_payment_reference_number_digit(self, source):
        if source == 'invoice':
            return self.number_digit
        elif source == 'party':
            return self.party.code_digit

    def format_customer_payment_reference(self, type):
        pool = Pool()
        Confifugaration = pool.get('account.configuration')
        configuration = Confifugaration(1)
        source = configuration.get_multivalue(
            'customer_payment_reference_number',
            company=self.company.id)
        return getattr(
            self, f'_format_customer_payment_reference_{type}')(source)

    def _format_customer_payment_reference_creditor_reference(self, source):
        number = self._customer_payment_reference_number_alnum(source)
        if number:
            number = number[-25:].upper()
            check_digits = iso7064.mod_97_10.calc_check_digits(f'{number}RF')
            return iso11649.format(f'RF{check_digits}{number}')

    @fields.depends('party')
    def on_change_with_party_lang(self, name=None):
        Config = Pool().get('ir.configuration')
        if self.party and self.party.lang:
            return self.party.lang.code
        return Config.get_language()

    @fields.depends('move')
    def on_change_with_has_account_move(self, name=None):
        return bool(self.move)

    @classmethod
    def get_type_name(cls, invoices, name):
        type_names = {}
        type2name = {}
        for type, name in cls.fields_get(fields_names=['type']
                )['type']['selection']:
            type2name[type] = name
        for invoice in invoices:
            type_names[invoice.id] = type2name[invoice.type]
        return type_names

    @fields.depends(methods=['_on_change_lines_taxes'])
    def on_change_lines(self):
        self._on_change_lines_taxes()

    @fields.depends(methods=['_on_change_lines_taxes'])
    def on_change_taxes(self):
        self._on_change_lines_taxes()

    @fields.depends(
        'lines', 'taxes', 'currency', methods=['_get_taxes', 'tax_date'])
    def _on_change_lines_taxes(self):
        pool = Pool()
        InvoiceTax = pool.get('account.invoice.tax')

        self.untaxed_amount = Decimal(0)
        self.tax_amount = Decimal(0)
        self.total_amount = Decimal(0)

        if self.lines:
            for line in self.lines:
                if getattr(line, 'type', '') == 'line':
                    self.untaxed_amount += getattr(line, 'amount', 0) or 0
            computed_taxes = self._get_taxes()
        else:
            computed_taxes = {}

        def is_zero(amount):
            if self.currency:
                return self.currency.is_zero(amount)
            else:
                return amount == Decimal(0)

        tax_keys = set()
        taxes = list(self.taxes or [])
        for tax in (self.taxes or []):
            if tax.manual:
                self.tax_amount += tax.amount or Decimal(0)
                continue
            key = tax._key
            if (key not in computed_taxes) or (key in tax_keys):
                taxes.remove(tax)
                continue
            tax_keys.add(key)
            if not is_zero(
                    computed_taxes[key].base - (tax.base or Decimal(0))):
                self.tax_amount += computed_taxes[key].amount
                tax.amount = computed_taxes[key].amount
                tax.base = computed_taxes[key].base
            else:
                self.tax_amount += tax.amount or Decimal(0)
        for key in computed_taxes:
            if key not in tax_keys:
                tax = computed_taxes[key].tax
                self.tax_amount += computed_taxes[key].amount
                value = InvoiceTax.default_get(
                    list(InvoiceTax._fields.keys()), with_rec_name=False)
                value.update(computed_taxes[key].values())
                value['manual'] = False
                value['description'] = tax.description
                value['legal_notice'] = tax.legal_notice
                value['currency'] = self.currency
                invoice_tax = InvoiceTax(**value)
                if invoice_tax.tax:
                    invoice_tax.sequence = invoice_tax.tax.sequence
                taxes.append(invoice_tax)
        self.taxes = taxes
        if self.currency:
            self.untaxed_amount = self.currency.round(self.untaxed_amount)
            self.tax_amount = self.currency.round(self.tax_amount)
        self.total_amount = self.untaxed_amount + self.tax_amount
        if self.currency:
            self.total_amount = self.currency.round(self.total_amount)

    @classmethod
    def get_amount(cls, invoices, names):
        pool = Pool()
        InvoiceTax = pool.get('account.invoice.tax')
        cursor = Transaction().connection.cursor()

        untaxed_amount = {i.id: i.currency.round(Decimal(0)) for i in invoices}
        tax_amount = untaxed_amount.copy()
        total_amount = untaxed_amount.copy()

        invoices_no_cache = []
        for invoice in invoices:
            if (invoice.total_amount_cache is not None
                    and invoice.untaxed_amount_cache is not None
                    and invoice.tax_amount_cache is not None):
                total_amount[invoice.id] = invoice.total_amount_cache
                untaxed_amount[invoice.id] = invoice.untaxed_amount_cache
                tax_amount[invoice.id] = invoice.tax_amount_cache
            else:
                invoices_no_cache.append(invoice.id)
        invoices_no_cache = cls.browse(invoices_no_cache)

        type_name = cls.tax_amount._field.sql_type().base
        tax = InvoiceTax.__table__()
        for sub_ids in grouped_slice(invoices_no_cache):
            red_sql = reduce_ids(tax.invoice, sub_ids)
            query = (tax.select(tax.invoice,
                    Coalesce(Sum(tax.amount), 0).as_(type_name),
                    where=red_sql,
                    group_by=tax.invoice))
            if backend.name == 'sqlite':
                sqlite_apply_types(query, [None, 'NUMERIC'])
            cursor.execute(*query)
            tax_amount.update(cursor)
        # Float amount must be rounded to get the right precision
        if backend.name == 'sqlite':
            for invoice in invoices:
                tax_amount[invoice.id] = invoice.currency.round(
                    tax_amount[invoice.id])

        for invoice in invoices_no_cache:
            zero = invoice.currency.round(Decimal(0))
            untaxed_amount[invoice.id] = sum(
                (line.amount for line in invoice.line_lines), zero)
            total_amount[invoice.id] = (
                untaxed_amount[invoice.id] + tax_amount[invoice.id])

        result = {
            'untaxed_amount': untaxed_amount,
            'tax_amount': tax_amount,
            'total_amount': total_amount,
            }
        for key in list(result.keys()):
            if key not in names:
                del result[key]
        return result

    def get_reconciled(self, name):
        def get_reconciliation(line):
            if line.reconciliation and line.reconciliation.delegate_to:
                return get_reconciliation(line.reconciliation.delegate_to)
            else:
                return line.reconciliation
        reconciliations = list(map(get_reconciliation, self.lines_to_pay))
        if not reconciliations:
            return None
        elif not all(reconciliations):
            return None
        else:
            return max(r.date for r in reconciliations)

    @classmethod
    def _query_lines_to_pay(cls, invoices):
        pool = Pool()
        MoveLine = pool.get('account.move.line')
        AdditionalMove = pool.get('account.invoice-additional-account.move')
        line = MoveLine.__table__()
        invoice = cls.__table__()
        additional_move = AdditionalMove.__table__()

        red_sql = reduce_ids(invoice.id, invoices)
        query = (invoice
            .join(line,
                condition=((invoice.move == line.move)
                    & (invoice.account == line.account)))
            .select(
                invoice.id.as_('invoice'),
                line.id.as_('line'),
                line.maturity_date.as_('maturity_date'),
                line.reconciliation.as_('reconciliation'),
                where=red_sql))
        query |= (invoice
            .join(additional_move,
                condition=additional_move.invoice == invoice.id)
            .join(line,
                condition=((additional_move.move == line.move)
                    & (invoice.account == line.account)))
            .select(
                invoice.id.as_('invoice'),
                line.id.as_('line'),
                line.maturity_date.as_('maturity_date'),
                line.reconciliation.as_('reconciliation'),
                where=red_sql))
        return query

    @classmethod
    def get_lines_to_pay(cls, invoices, name):
        cursor = Transaction().connection.cursor()
        lines = defaultdict(list)
        for sub_invoices in grouped_slice(invoices):
            query = cls._query_lines_to_pay(sub_invoices)
            query = query.select(
                query.invoice, query.line,
                order_by=query.maturity_date.nulls_last)
            cursor.execute(*query)
            for invoice_id, line_id in cursor:
                lines[invoice_id].append(line_id)
        return lines

    @classmethod
    def get_reconciliation_lines(cls, invoices, name):
        pool = Pool()
        Line = pool.get('account.move.line')
        Move = pool.get('account.move')
        line = Line.__table__()
        move = Move.__table__()
        cursor = Transaction().connection.cursor()
        lines = defaultdict(list)
        for sub_invoices in grouped_slice(invoices):
            sub_invoices = list(sub_invoices)
            lines_to_pay = cls._query_lines_to_pay(sub_invoices)
            query = cls._query_lines_to_pay(sub_invoices)
            query = (
                query
                .join(line,
                    condition=query.reconciliation == line.reconciliation)
                .join(move, condition=line.move == move.id)
                .select(
                    query.invoice, line.id,
                    where=~Exists(
                        lines_to_pay.select(
                            lines_to_pay.line,
                            where=(lines_to_pay.invoice == query.invoice)
                            & (lines_to_pay.line == line.id))),
                    group_by=[query.invoice, line.id, move.date],
                    order_by=move.date.asc))
            cursor.execute(*query)
            for invoice_id, line_id in cursor:
                lines[invoice_id].append(line_id)
        return lines

    @classmethod
    def get_amount_to_pay(cls, invoices, name):
        pool = Pool()
        Currency = pool.get('currency.currency')
        Date = pool.get('ir.date')

        amounts = defaultdict(Decimal)
        for company, grouped_invoices in groupby(
                invoices, key=lambda i: i.company):
            with Transaction().set_context(company=company.id):
                today = Date.today()
            for invoice in grouped_invoices:
                if invoice.state != 'posted':
                    continue
                amount = Decimal(0)
                amount_currency = Decimal(0)
                for line in invoice.lines_to_pay:
                    if line.reconciliation:
                        continue
                    if (name == 'amount_to_pay_today'
                            and (not line.maturity_date
                                or line.maturity_date > today)):
                        continue
                    if (line.second_currency
                            and line.second_currency == invoice.currency):
                        amount_currency += line.amount_second_currency
                    else:
                        amount += line.debit - line.credit
                for line in invoice.payment_lines:
                    if line.reconciliation:
                        continue
                    if (line.second_currency
                            and line.second_currency == invoice.currency):
                        amount_currency += line.amount_second_currency
                    else:
                        amount += line.debit - line.credit
                if amount:
                    with Transaction().set_context(date=invoice.currency_date):
                        amount_currency += Currency.compute(
                            invoice.company.currency, amount, invoice.currency)
                if invoice.type == 'in' and amount_currency:
                    amount_currency *= -1
                amounts[invoice.id] = amount_currency
        return amounts

    @classmethod
    def search_total_amount(cls, name, clause):
        pool = Pool()
        Line = pool.get('account.invoice.line')
        Tax = pool.get('account.invoice.tax')
        Invoice = pool.get('account.invoice')
        Currency = pool.get('currency.currency')
        type_name = cls.total_amount._field.sql_type().base
        line = Line.__table__()
        invoice = Invoice.__table__()
        currency = Currency.__table__()
        tax = Tax.__table__()

        _, operator, value = clause
        Operator = fields.SQL_OPERATORS[operator]
        # SQLite uses float for sum
        if value is not None and backend.name == 'sqlite':
            value = float(value)

        union = (
            line
            .join(invoice, condition=(invoice.id == line.invoice))
            .join(currency, condition=(currency.id == invoice.currency))
            .select(
                line.invoice.as_('invoice'),
                Coalesce(Round(
                        (line.quantity * line.unit_price).cast(type_name),
                        currency.digits), 0).as_('total_amount'),
                where=(invoice.total_amount_cache == Null))
            | tax.select(
                tax.invoice.as_('invoice'),
                tax.amount.as_('total_amount'),
                where=Exists(
                    invoice.select(
                        invoice.id,
                        where=(invoice.total_amount_cache == Null)
                        & (invoice.id == tax.invoice)))))
        union |= invoice.select(
            invoice.id.as_('invoice'),
            invoice.total_amount_cache.as_('total_amount'),
            where=(
                (invoice.total_amount_cache != Null)
                & Operator(invoice.total_amount_cache.cast(type_name), value)))
        query = union.select(union.invoice, group_by=union.invoice,
            having=Operator(Sum(union.total_amount).cast(type_name),
                value))
        return [('id', 'in', query)]

    @classmethod
    def search_untaxed_amount(cls, name, clause):
        pool = Pool()
        Line = pool.get('account.invoice.line')
        Invoice = pool.get('account.invoice')
        Currency = pool.get('currency.currency')
        type_name = cls.untaxed_amount._field.sql_type().base
        line = Line.__table__()
        invoice = Invoice.__table__()
        currency = Currency.__table__()

        _, operator, value = clause
        Operator = fields.SQL_OPERATORS[operator]
        # SQLite uses float for sum
        if value is not None and backend.name == 'sqlite':
            value = float(value)

        query = (
            line
            .join(invoice, condition=(invoice.id == line.invoice))
            .join(currency, condition=(currency.id == invoice.currency))
            .select(
                line.invoice,
                where=(invoice.untaxed_amount_cache == Null),
                group_by=line.invoice,
                having=Operator(
                    Coalesce(Sum(Round(
                                (line.quantity * line.unit_price).cast(
                                    type_name),
                                currency.digits)), 0).cast(type_name),
                    value)))
        query |= invoice.select(invoice.id,
            where=(
                (invoice.untaxed_amount_cache != Null)
                & Operator(
                    invoice.untaxed_amount_cache.cast(type_name), value)))
        return [('id', 'in', query)]

    @classmethod
    def search_tax_amount(cls, name, clause):
        pool = Pool()
        Tax = pool.get('account.invoice.tax')
        Invoice = pool.get('account.invoice')
        type_name = cls.tax_amount._field.sql_type().base
        tax = Tax.__table__()
        invoice = Invoice.__table__()

        _, operator, value = clause
        Operator = fields.SQL_OPERATORS[operator]
        # SQLite uses float for sum
        if value is not None and backend.name == 'sqlite':
            value = float(value)

        query = tax.select(tax.invoice,
            where=Exists(
                invoice.select(
                    invoice.id,
                    where=(invoice.tax_amount_cache == Null)
                    & (invoice.id == tax.invoice))),
            group_by=tax.invoice,
            having=Operator(
                Coalesce(Sum(tax.amount), 0).cast(type_name), value))
        query |= invoice.select(invoice.id,
            where=(
                (invoice.tax_amount_cache != Null)
                & Operator(invoice.tax_amount_cache.cast(type_name), value)))
        return [('id', 'in', query)]

    def get_allow_cancel(self, name):
        if self.state in {'draft', 'validated'}:
            return True
        if self.state == 'posted':
            return self.type == 'in' or self.company.cancel_invoice_out
        return False

    @classmethod
    def get_has_payment_method(cls, invoices, name):
        pool = Pool()
        Method = pool.get('account.invoice.payment.method')
        methods = {}
        for (company, account), sub_invoices in groupby(
                invoices, key=lambda i: (i.company, i.account)):
            sub_invoice_ids = [i.id for i in sub_invoices]
            value = bool(Method.search([
                        ('company', '=', company.id),
                        ('debit_account', '!=', account.id),
                        ('credit_account', '!=', account.id),
                        ], limit=1))
            methods.update(dict.fromkeys(sub_invoice_ids, value))
        return methods

    @classmethod
    def get_has_report_cache(cls, invoices, name):
        table = cls.__table__()
        cursor = Transaction().connection.cursor()

        result = {}
        has_cache = (
            (table.invoice_report_cache_id != Null)
            | (table.invoice_report_cache != Null))
        for sub_invoices in grouped_slice(invoices):
            sub_ids = map(int, sub_invoices)
            cursor.execute(*table.select(table.id, has_cache,
                    where=reduce_ids(table.id, sub_ids)))
            result.update(cursor)
        return result

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

    @property
    @fields.depends('accounting_date', 'invoice_date', 'company')
    def tax_date(self):
        pool = Pool()
        Date = pool.get('ir.date')
        context = Transaction().context
        with Transaction().set_context(
                company=self.company.id if self.company
                else context.get('company')):
            today = Date.today()
        return self.accounting_date or self.invoice_date or today

    @fields.depends('party', 'company')
    def _get_tax_context(self):
        context = {}
        if self.party and self.party.lang:
            context['language'] = self.party.lang.code
        if self.company:
            context['company'] = self.company.id
        return context

    def _compute_taxes(self):
        for key, tax_line in self._get_taxes().items():
            value = dict(tax_line.values())
            value['invoice'] = self.id
            value['manual'] = False
            value['description'] = tax_line.tax.description
            value['legal_notice'] = tax_line.tax.legal_notice
            yield key, value

    @dualmethod
    def update_taxes(cls, invoices, exception=False):
        Tax = Pool().get('account.invoice.tax')
        to_create = []
        to_delete = []
        to_write = []
        for invoice in invoices:
            if invoice.state in ('posted', 'paid', 'cancelled'):
                continue
            computed_taxes = dict(invoice._compute_taxes())
            if not invoice.taxes:
                to_create.extend(computed_taxes.values())
            else:
                tax_keys = set()
                for tax in invoice.taxes:
                    if tax.manual:
                        continue
                    key = tax._key
                    if (key not in computed_taxes) or (key in tax_keys):
                        to_delete.append(tax)
                        continue
                    tax_keys.add(key)
                    if not invoice.currency.is_zero(
                            computed_taxes[key]['base'] - tax.base):
                        to_write.extend(([tax], computed_taxes[key]))
                for key in computed_taxes:
                    if key not in tax_keys:
                        to_create.append(computed_taxes[key])
            if exception and (to_create or to_delete or to_write):
                raise InvoiceTaxValidationError(
                    gettext('account_invoice.msg_invoice_tax_invalid',
                        invoice=invoice.rec_name))
        if to_create:
            Tax.create(to_create)
        if to_delete:
            Tax.delete(to_delete)
        if to_write:
            Tax.write(*to_write)

    def _get_move_line(self, date, amount):
        '''
        Return move line
        '''
        pool = Pool()
        Currency = pool.get('currency.currency')
        MoveLine = pool.get('account.move.line')
        line = MoveLine()
        if self.currency != self.company.currency:
            line.amount_second_currency = amount
            line.second_currency = self.currency
            with Transaction().set_context(date=self.currency_date):
                amount = Currency.compute(
                    self.currency, amount, self.company.currency)
        else:
            line.amount_second_currency = None
            line.second_currency = None
        if amount >= 0:
            if self.type == 'out':
                line.debit, line.credit = amount, 0
            else:
                line.debit, line.credit = 0, amount
        else:
            if self.type == 'out':
                line.debit, line.credit = 0, -amount
            else:
                line.debit, line.credit = -amount, 0
        if line.amount_second_currency:
            line.amount_second_currency = (
                line.amount_second_currency.copy_sign(
                    line.debit - line.credit))
        line.account = self.account
        if self.account.party_required:
            if self.alternative_payees:
                line.party, = self.alternative_payees
            else:
                line.party = self.party
        line.maturity_date = date
        line.description = self.description
        return line

    def _get_exchange_move_line(self, amount):
        pool = Pool()
        Configuration = pool.get('account.configuration')
        MoveLine = pool.get('account.move.line')
        configuration = Configuration(1)
        line = MoveLine()
        line.debit = -amount if amount < 0 else 0
        line.credit = amount if amount > 0 else 0
        if line.credit:
            line.account = configuration.get_multivalue(
                'currency_exchange_credit_account', company=self.company.id)
            if not line.account:
                raise AccountMissing(gettext(
                        'account_invoice.'
                        'msg_invoice_currency_exchange_credit_account_missing',
                        invoice=self.rec_name,
                        company=self.company.rec_name))
        else:
            line.account = configuration.get_multivalue(
                'currency_exchange_debit_account', company=self.company.id)
            if not line.account:
                raise AccountMissing(gettext(
                        'account_invoice.'
                        'msg_invoice_currency_exchange_debit_account_missing',
                        invoice=self.rec_name,
                        company=self.company.rec_name))
        line.amount_second_currency = None
        line.second_currency = None
        return line

    def get_move(self):
        '''
        Compute account move for the invoice and return the created move
        '''
        pool = Pool()
        Move = pool.get('account.move')
        Period = pool.get('account.period')
        Date = pool.get('ir.date')
        Warning = pool.get('res.user.warning')
        Lang = pool.get('ir.lang')

        if self.move:
            return self.move
        with Transaction().set_context(company=self.company.id):
            today = Date.today()
        self.update_taxes(exception=True)
        move_lines = []
        for line in self.line_lines:
            move_lines += line.get_move_lines()
        for tax in self.taxes:
            move_lines += tax.get_move_lines()

        remainder = sum(l.debit - l.credit for l in move_lines)
        if self.payment_term:
            payment_date = self.payment_term_date or self.invoice_date or today
            term_lines = self.payment_term.compute(
                self.total_amount, self.currency, payment_date)
        else:
            term_lines = [(self.payment_term_date or today, self.total_amount)]
        past_payment_term_dates = []
        for date, amount in term_lines:
            line = self._get_move_line(date, amount)
            move_lines.append(line)
            remainder += line.debit - line.credit
            if self.type == 'out' and date < today:
                past_payment_term_dates.append(date)
        if self.currency != self.company.currency and remainder:
            line = self._get_exchange_move_line(remainder)
            move_lines.append(line)
        if any(past_payment_term_dates):
            lang = Lang.get()
            warning_key = Warning.format('invoice_payment_term', [self])
            if Warning.check(warning_key):
                raise InvoicePaymentTermDateWarning(warning_key,
                    gettext('account_invoice'
                        '.msg_invoice_payment_term_date_past',
                        invoice=self.rec_name,
                        date=lang.strftime(min(past_payment_term_dates))))

        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.lines = move_lines
        return move

    @classmethod
    def set_number(cls, invoices):
        '''
        Set number to the invoice
        '''
        pool = Pool()
        Date = pool.get('ir.date')
        Lang = pool.get('ir.lang')

        sequences = set()

        for company, grouped_invoices in groupby(
                invoices, key=lambda i: i.company):
            with Transaction().set_context(company=company.id):
                today = Date.today()

            def invoice_date(invoice):
                return invoice.invoice_date or today

            to_number = defaultdict(list)
            grouped_invoices = sorted(grouped_invoices, key=invoice_date)

            for invoice in grouped_invoices:
                # Posted, paid and cancelled invoices are tested by
                # check_modify so we can not modify tax_identifier nor number
                if invoice.state in {'posted', 'paid', 'cancelled'}:
                    continue
                if not invoice.tax_identifier:
                    invoice.tax_identifier = invoice.get_tax_identifier()
                # Generated invoice may not fill the party tax identifier
                if not invoice.party_tax_identifier:
                    invoice.party_tax_identifier = invoice.party.tax_identifier

                if invoice.number:
                    continue

                if not invoice.invoice_date and invoice.type == 'out':
                    invoice.invoice_date = today
                invoice.sequence_type_cache = invoice._sequence_type
                sequence, sequence_date = invoice._number_sequence()
                to_number[(sequence, sequence_date)].append(invoice)
                if invoice.type == 'out' and sequence not in sequences:
                    date = invoice_date(invoice)
                    # Do not need to lock the table
                    # because sequence.get_many is sequential
                    after_invoices = cls.search([
                            ('sequence', '=', sequence),
                            ('invoice_date', '>', date),
                            ],
                        limit=1, order=[('invoice_date', 'DESC')])
                    if after_invoices:
                        after_invoice, = after_invoices
                        raise InvoiceNumberError(
                            gettext('account_invoice.msg_invoice_number_after',
                                invoice=invoice.rec_name,
                                sequence=sequence.rec_name,
                                date=Lang.get().strftime(date),
                                after_invoice=after_invoice.rec_name))
                    sequences.add(sequence)
            for (sequence, date), n_invoices in to_number.items():
                with Transaction().set_context(
                        date=date, company=company.id):
                    for invoice, number in zip(
                            n_invoices, sequence.get_many(len(n_invoices))):
                        invoice.sequence = sequence
                        invoice.number = number
        cls.save(invoices)

    def _number_sequence(self, pattern=None):
        "Returns the sequence and date to use for numbering"
        pool = Pool()
        Period = pool.get('account.period')

        if pattern is None:
            pattern = {}
        else:
            pattern = pattern.copy()

        accounting_date = self.accounting_date or self.invoice_date
        period = Period.find(
            self.company, date=accounting_date,
            test_state=self.type != 'in')

        fiscalyear = period.fiscalyear
        pattern.setdefault('company', self.company.id)
        pattern.setdefault('fiscalyear', fiscalyear.id)
        pattern.setdefault('period', period.id)

        for invoice_sequence in fiscalyear.invoice_sequences:
            if invoice_sequence.match(pattern):
                return getattr(
                    invoice_sequence, self._sequence_field), accounting_date
        else:
            raise InvoiceNumberError(
                gettext('account_invoice.msg_invoice_no_sequence',
                    invoice=self.rec_name,
                    fiscalyear=fiscalyear.rec_name))

    @property
    def _sequence_type(self):
        if (all(l.amount <= 0 for l in self.line_lines)
                and self.total_amount < 0):
            return 'credit_note'
        else:
            return 'invoice'

    @property
    def sequence_type(self):
        return self.sequence_type_cache or self._sequence_type

    @property
    def _sequence_field(self):
        "Returns the field name of invoice_sequence to use"
        return f'{self.type}_{self.sequence_type}_sequence'

    def get_tax_identifier(self, pattern=None):
        "Return the default computed tax identifier"
        pattern = pattern.copy() if pattern is not None else {}
        if self.invoice_address and self.invoice_address.country:
            pattern.setdefault('country', self.invoice_address.country.id)
        return self.company.get_tax_identifier(pattern)

    @property
    def invoice_report_versioned(self):
        return self.state in {'posted', 'paid'} and self.type == 'out'

    def create_invoice_report_revision(self):
        pool = Pool()
        InvoiceReportRevision = pool.get('account.invoice.report.revision')
        if not self.invoice_report_versioned:
            return
        invoice_report_revision = InvoiceReportRevision(
            invoice=self,
            invoice_report_cache=self.invoice_report_cache,
            invoice_report_cache_id=self.invoice_report_cache_id,
            invoice_report_format=self.invoice_report_format)
        self.invoice_report_revisions += (invoice_report_revision,)
        self.invoice_report_cache = None
        self.invoice_report_cache_id = None
        self.invoice_report_format = None
        return invoice_report_revision

    @property
    def is_modifiable(self):
        return not (self.state in {'posted', 'paid'}
            or (self.state == 'cancelled'
                and (self.move or self.cancel_move or self.number)))

    def get_rec_name(self, name):
        items = []
        if self.number:
            items.append(self.number)
        if self.reference:
            items.append('[%s]' % self.reference)
        if not items:
            items.append('(%s)' % self.id)
        return ' '.join(items)

    @classmethod
    def search_rec_name(cls, name, clause):
        _, operator, value = clause
        if operator.startswith('!') or operator.startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
            ('number', *clause[1:]),
            ('reference', *clause[1:]),
            ]

    def get_origins(self, name):
        return ', '.join(set(filter(None,
                    (l.origin_name for l in self.line_lines))))

    def chat_language(self, audience='internal'):
        language = super().chat_language(audience=audience)
        if audience == 'public':
            language = self.party.lang.code if self.party.lang else None
        return language

    @classmethod
    def view_attributes(cls):
        return super().view_attributes() + [
            ('/form//field[@name="comment"]', 'spell', Eval('party_lang')),
            ('/tree', 'visual',
                If((
                        (Eval('type') == 'out')
                        & (Eval('amount_to_pay_today', 0) > 0))
                    | ((Eval('type') == 'in')
                        & (Eval('amount_to_pay_today', 0) < 0)),
                    'danger',
                    If(Eval('state') == 'cancelled', 'muted', ''))),
            ]

    @classmethod
    def check_modification(cls, mode, invoices, values=None, external=False):
        super().check_modification(
            mode, invoices, values=values, external=external)
        if (mode == 'delete'
                or (mode == 'write'
                    and values.keys() - cls._check_modify_exclude)):
            for invoice in invoices:
                if not invoice.is_modifiable:
                    raise AccessError(gettext(
                            'account_invoice.msg_invoice_modify',
                            invoice=invoice.rec_name))
        if mode == 'delete':
            for invoice in invoices:
                if invoice.state not in {'cancelled', 'draft'}:
                    raise AccessError(gettext(
                            'account_invoice.msg_invoice_delete_cancel',
                            invoice=invoice.rec_name))
                if invoice.number:
                    raise AccessError(gettext(
                            'account_invoice.msg_invoice_delete_numbered',
                            invoice=invoice.rec_name))

    @classmethod
    def on_modification(cls, mode, invoices, field_names=None):
        super().on_modification(mode, invoices, field_names=field_names)
        if mode != 'delete':
            if draft_invoices := [i for i in invoices if i.state == 'draft']:
                cls.update_taxes(draft_invoices)

    def compute_fields(self, field_names=None):
        values = super().compute_fields(field_names=field_names)
        if (field_names is None or 'number' in field_names):
            values['number_alnum'] = (
                re.sub(r'[\W_]', '', self.number)
                if self.number is not None else None)
            try:
                values['number_digit'] = int(
                    re.sub(r'\D', '', self.number or '')[-18:])
            except ValueError:
                values['number_digit'] = None
        return values

    @classmethod
    def copy(cls, invoices, default=None):
        if default is None:
            default = {}
        else:
            default = default.copy()

        alternative_payees2copy = set()
        for invoice in invoices:
            if len(invoice.alternative_payees) == 1:
                parties = {l.party for l in invoice.lines_to_pay}
                if parties <= set(invoice.alternative_payees):
                    alternative_payees2copy.add(invoice.id)

        def copy_alternative_payees(data):
            if data['id'] in alternative_payees2copy:
                return data.get('alternative_payees', [])
            else:
                return []

        default.setdefault('number', None)
        default.setdefault('number_alnum', None)
        default.setdefault('number_digit', None)
        default.setdefault('reference')
        default.setdefault('supplier_payment_reference')
        default.setdefault('supplier_payment_reference_type')
        default.setdefault('sequence')
        default.setdefault('move', None)
        default.setdefault('additional_moves', None)
        default.setdefault('cancel_move', None)
        default.setdefault('invoice_report_cache', None)
        default.setdefault('invoice_report_cache_id', None)
        default.setdefault('invoice_report_format', None)
        default.setdefault('alternative_payees', copy_alternative_payees)
        default.setdefault('payment_lines', None)
        default.setdefault('invoice_date', None)
        default.setdefault('accounting_date', None)
        default.setdefault('payment_term_date', None)
        default.setdefault('total_amount_cache', None)
        default.setdefault('untaxed_amount_cache', None)
        default.setdefault('tax_amount_cache', None)
        default.setdefault('validated_by')
        default.setdefault('posted_by')
        default.setdefault('invoice_report_revisions', None)
        return super().copy(invoices, default=default)

    @classmethod
    def validate(cls, invoices):
        super().validate(invoices)
        for invoice in invoices:
            invoice.check_payment_lines()

    def check_payment_lines(self):
        def balance(line):
            if self.currency == line.second_currency:
                return line.amount_second_currency
            elif self.currency == self.company.currency:
                return line.debit - line.credit
            else:
                return 0
        amount = sum(map(balance, self.lines_to_pay))
        payment_amount = sum(map(balance, self.payment_lines))
        if abs(amount) < abs(payment_amount):
            raise InvoiceValidationError(
                gettext('account_invoice'
                    '.msg_invoice_payment_lines_greater_amount',
                    invoice=self.rec_name))

    @classmethod
    def validate_fields(cls, invoices, field_names):
        super().validate_fields(invoices, field_names)
        cls.check_supplier_payment_reference(invoices, field_names)

    @classmethod
    def check_supplier_payment_reference(cls, invoices, field_names):
        if field_names and not (
                field_names & {
                    'supplier_payment_reference',
                    'supplier_payment_reference_type',
                    }):
            return

        for invoice in invoices:
            if type := invoice.supplier_payment_reference_type:
                method = getattr(
                    cls, f'_check_supplier_payment_reference_{type}')
                if not method(invoice):
                    reference = invoice.supplier_payment_reference
                    type = invoice.supplier_payment_reference_type_string
                    raise InvoiceValidationError(
                        gettext(
                            'account_invoice.'
                            'msg_invoice_supplier_payment_reference_invalid',
                            type=type,
                            reference=reference,
                            invoice=invoice.rec_name))

    def _check_supplier_payment_reference_creditor_reference(self):
        return iso11649.is_valid(self.supplier_payment_reference)

    def get_reconcile_lines_for_amount(self, amount, currency, party=None):
        '''
        Return list of lines and the remainder to make reconciliation.
        '''
        pool = Pool()
        Line = pool.get('account.move.line')

        assert currency in {self.currency, self.company.currency}

        if party is None:
            party = self.party
        lines = [
            l for l in self.payment_lines + self.lines_to_pay
            if not l.reconciliation
            and (not self.account.party_required or l.party == party)]

        return Line.find_best_reconciliation(
            lines, currency, amount=amount)

    def pay_invoice(
            self, amount, payment_method, date, description=None,
            overpayment=0, party=None):
        '''
        Adds a payment of amount to an invoice using the journal, date and
        description.
        If overpayment is set, then only the amount minus the overpayment is
        used to pay off the invoice.
        Returns the payment lines.
        '''
        pool = Pool()
        Currency = pool.get('currency.currency')
        Move = pool.get('account.move')
        Line = pool.get('account.move.line')
        Period = pool.get('account.period')

        if party is None:
            party = self.party

        pay_line = Line(account=self.account)
        counterpart_line = Line()
        lines = [pay_line, counterpart_line]

        pay_amount = amount - overpayment
        if self.currency != self.company.currency:
            amount_second_currency = pay_amount
            second_currency = self.currency
            overpayment_second_currency = overpayment
            with Transaction().set_context(date=date):
                amount = Currency.compute(
                    self.currency, amount, self.company.currency)
                overpayment = Currency.compute(
                    self.currency, overpayment, self.company.currency)
                pay_amount = amount - overpayment
        else:
            amount_second_currency = None
            second_currency = None
            overpayment_second_currency = None
        if pay_amount >= 0:
            if self.type == 'out':
                pay_line.debit, pay_line.credit = 0, pay_amount
            else:
                pay_line.debit, pay_line.credit = pay_amount, 0
        else:
            if self.type == 'out':
                pay_line.debit, pay_line.credit = -pay_amount, 0
            else:
                pay_line.debit, pay_line.credit = 0, -pay_amount
        if amount_second_currency is not None:
            pay_line.amount_second_currency = (
                amount_second_currency.copy_sign(
                    pay_line.debit - pay_line.credit))
            pay_line.second_currency = second_currency

        if overpayment:
            overpayment_line = Line(account=self.account)
            lines.insert(1, overpayment_line)
            overpayment_line.debit = (
                abs(overpayment) if pay_line.debit else 0)
            overpayment_line.credit = (
                abs(overpayment) if pay_line.credit else 0)
            if overpayment_second_currency is not None:
                overpayment_line.amount_second_currency = (
                    overpayment_second_currency.copy_sign(
                        overpayment_line.debit - overpayment_line.credit))
                overpayment_line.second_currency = second_currency

        counterpart_line.debit = abs(amount) if pay_line.credit else 0
        counterpart_line.credit = abs(amount) if pay_line.debit else 0
        if counterpart_line.debit:
            payment_acccount = 'debit_account'
        else:
            payment_acccount = 'credit_account'
        counterpart_line.account = getattr(
            payment_method, payment_acccount).current(date=date)
        if amount_second_currency is not None:
            counterpart_line.amount_second_currency = (
                amount_second_currency.copy_sign(
                    counterpart_line.debit - counterpart_line.credit))
            counterpart_line.second_currency = second_currency

        for line in lines:
            if line.account.party_required:
                line.party = party

        period = Period.find(self.company, date=date)

        move = Move(
            journal=payment_method.journal, period=period, date=date,
            origin=self, description=description,
            company=self.company, lines=lines)
        move.save()
        Move.post([move])

        payment_lines = [l for l in move.lines if l.account == self.account]
        payment_line = [l for l in payment_lines
            if (l.debit, l.credit) == (pay_line.debit, pay_line.credit)][0]
        self.add_payment_lines({self: [payment_line]})
        return payment_lines

    @classmethod
    def add_payment_lines(cls, payments):
        "Add value lines to the key invoice from the payment dictionary."
        to_write = []
        for invoice, lines in payments.items():
            if invoice.state == 'paid':
                raise AccessError(
                    gettext('account_invoice'
                        '.msg_invoice_payment_lines_add_remove_paid',
                        invoice=invoice.rec_name))
            to_write.append([invoice])
            to_write.append({'payment_lines': [('add', lines)]})
        if to_write:
            cls.write(*to_write)

    @classmethod
    def remove_payment_lines(cls, lines):
        "Remove payment lines from their invoices."
        pool = Pool()
        PaymentLine = pool.get('account.invoice-account.move.line')

        payments = defaultdict(list)
        ids = list(map(int, lines))
        for sub_ids in grouped_slice(ids):
            payment_lines = PaymentLine.search([
                    ('line', 'in', list(sub_ids)),
                    ])
            for payment_line in payment_lines:
                payments[payment_line.invoice].append(payment_line.line)

        to_write = []
        for invoice, lines in payments.items():
            if invoice.state == 'paid':
                raise AccessError(
                    gettext('account_invoice'
                        '.msg_invoice_payment_lines_add_remove_paid',
                        invoice=invoice.rec_name))
            to_write.append([invoice])
            to_write.append({'payment_lines': [('remove', lines)]})
        if to_write:
            cls.write(*to_write)

    @dualmethod
    def print_invoice(cls, invoices):
        '''
        Generate invoice report and store it in invoice_report field.
        '''
        InvoiceReport = Pool().get('account.invoice', type='report')
        for invoice in invoices:
            if not invoice.invoice_report_cache:
                InvoiceReport.execute([invoice.id], {})

    def _credit(self, **values):
        '''
        Return values to credit invoice.
        '''
        credit = self.__class__(**values)

        for field in [
                'company', 'tax_identifier', 'party', 'party_tax_identifier',
                'invoice_address', 'currency', 'journal', 'account',
                'payment_term', 'description', 'comment', 'type']:
            setattr(credit, field, getattr(self, field))

        credit.lines = [line._credit() for line in self.lines]
        credit.taxes = [tax._credit() for tax in self.taxes if tax.manual]
        return credit

    @classmethod
    def credit(cls, invoices, refund=False, **values):
        '''
        Credit invoices and return ids of new invoices.
        Return the list of new invoice
        '''
        new_invoices = [i._credit(**values) for i in invoices]
        cls.save(new_invoices)
        if refund:
            cls.post(new_invoices)
            for invoice, new_invoice in zip(invoices, new_invoices):
                if invoice.state != 'posted':
                    raise AccessError(
                        gettext('account_invoice'
                            '.msg_invoice_credit_refund_not_posted',
                            invoice=invoice.rec_name))
                invoice.cancel_move = new_invoice.move
            cls.save(invoices)
            cls.cancel(invoices)
        return new_invoices

    @classmethod
    def _store_cache(cls, invoices):
        invoices = list(invoices)
        cls.write(invoices, {
                'untaxed_amount_cache': None,
                'tax_amount_cache': None,
                'total_amount_cache': None,
                })
        for invoice in invoices:
            invoice.untaxed_amount_cache = invoice.untaxed_amount
            invoice.tax_amount_cache = invoice.tax_amount
            invoice.total_amount_cache = invoice.total_amount
        cls.save(invoices)

    @classmethod
    @ModelView.button
    @Workflow.transition('draft')
    @reset_employee('validated_by', 'posted_by')
    def draft(cls, invoices):
        Move = Pool().get('account.move')

        cls.write(invoices, {
                'tax_amount_cache': None,
                'untaxed_amount_cache': None,
                'total_amount_cache': None,
                })
        moves = []
        for invoice in invoices:
            if invoice.move:
                moves.append(invoice.move)
            if invoice.additional_moves:
                moves.extend(invoice.additional_moves)
            if len(invoice.alternative_payees) > 1:
                invoice.alternative_payees = []
        cls.save(invoices)
        if moves:
            Move.delete(moves)

    @classmethod
    @ModelView.button
    @Workflow.transition('validated')
    @set_employee('validated_by')
    def validate_invoice(cls, invoices):
        pool = Pool()
        Move = pool.get('account.move')

        cls._check_taxes(invoices)
        cls._check_similar(invoices)

        invoices_in = cls.browse([i for i in invoices if i.type == 'in'])
        cls.set_number(invoices_in)
        cls._store_cache(invoices)
        moves = []
        for invoice in invoices_in:
            move = invoice.get_move()
            if move != invoice.move:
                invoice.move = move
                moves.append(move)
        if moves:
            Move.save(moves)
        cls.save(invoices_in)

    @classmethod
    @Workflow.transition('posted')
    def post_batch(cls, invoices):
        pool = Pool()
        Date = pool.get('ir.date')
        transaction = Transaction()
        context = transaction.context
        cls.set_number(invoices)
        for company, grouped_invoices in groupby(
                invoices, key=lambda i: i.company):
            with Transaction().set_context(company=company.id):
                today = Date.today()
            for invoice in grouped_invoices:
                if not invoice.payment_term_date:
                    invoice.payment_term_date = today
        cls.save(invoices)
        with transaction.set_context(
                _skip_warnings=True,
                queue_batch=context.get('queue_batch', True)):
            cls.__queue__._post(invoices)

    @classmethod
    @ModelView.button
    @Workflow.transition('posted')
    @set_employee('posted_by', when='before')
    def post(cls, invoices):
        pool = Pool()
        Date = pool.get('ir.date')
        Warning = pool.get('res.user.warning')
        for company, grouped_invoices in groupby(
                invoices, key=lambda i: i.company):
            with Transaction().set_context(company=company.id):
                today = Date.today()
            future_invoices = [
                i for i in grouped_invoices
                if i.type == 'out'
                and i.invoice_date and i.invoice_date > today]
            if future_invoices:
                names = ', '.join(m.rec_name for m in future_invoices[:5])
                if len(future_invoices) > 5:
                    names += '...'
                warning_key = Warning.format(
                    'invoice_date_future', future_invoices)
                if Warning.check(warning_key):
                    raise InvoiceFutureWarning(warning_key,
                        gettext('account_invoice.msg_invoice_date_future',
                            invoices=names))
        to_check = [i for i in invoices if i.state != 'validated']
        cls._check_taxes(to_check)
        cls._check_similar(to_check)
        cls._post(invoices)

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

        cls.set_number(invoices)
        cls._store_cache(invoices)
        moves = []
        for invoice in invoices:
            move = invoice.get_move()
            if move != invoice.move:
                invoice.move = move
                moves.append(move)
            if invoice.state != 'posted':
                invoice.state = 'posted'
        if moves:
            Move.save(moves)
        cls.save(invoices)
        Move.post([i.move for i in invoices if i.move.state != 'posted'])
        reconciled = []
        to_print = []
        for invoice in invoices:
            if invoice.type == 'out':
                to_print.append(invoice)
            if invoice.reconciled:
                reconciled.append(invoice)
        if to_print:
            cls.__queue__.print_invoice(to_print)
        if reconciled:
            with transaction.set_context(
                    queue_batch=context.get('queue_batch', True)):
                cls.__queue__.process(reconciled)

    @classmethod
    def _check_taxes(cls, invoices):
        pool = Pool()
        Line = pool.get('account.invoice.line')
        Warning = pool.get('res.user.warning')
        for invoice in invoices:
            different_lines = []
            for line in invoice.line_lines:
                test_line = Line(line.id)
                test_line.on_change_product()
                if (set(test_line.taxes) != set(line.taxes)
                        or test_line.taxes_deductible_rate
                        != line.taxes_deductible_rate):
                    different_lines.append(line)
            if different_lines:
                warning_key = Warning.format(
                    'invoice_taxes', [invoice])
                if Warning.check(warning_key):
                    lines = ', '.join(l.rec_name for l in different_lines[:5])
                    if len(different_lines) > 5:
                        lines += '...'
                    raise InvoiceTaxesWarning(warning_key,
                        gettext('account_invoice.msg_invoice_default_taxes',
                            invoice=invoice.rec_name,
                            lines=lines))

    @classmethod
    def _check_similar(cls, invoices, type='in'):
        pool = Pool()
        Warning = pool.get('res.user.warning')
        for sub_invoices in grouped_slice(invoices):
            sub_invoices = list(sub_invoices)
            domain = list(filter(None,
                        (i._similar_domain() for i in sub_invoices
                        if i.type == type)))
            if not domain:
                continue
            if cls.search(['OR'] + domain, order=[]):
                for invoice in sub_invoices:
                    domain = invoice._similar_domain()
                    if not domain:
                        continue
                    try:
                        similar, = cls.search(domain, limit=1)
                    except ValueError:
                        continue
                    warning_key = Warning.format(
                        'invoice_similar', [invoice])
                    if Warning.check(warning_key):
                        raise InvoiceSimilarWarning(warning_key,
                            gettext('account_invoice.msg_invoice_similar',
                                similar=similar.rec_name,
                                invoice=invoice.rec_name))

    def _similar_domain(self, delay=None):
        pool = Pool()
        Date = pool.get('ir.date')
        if not self.reference:
            return
        with Transaction().set_context(company=self.company.id):
            invoice_date = self.invoice_date or Date.today()
        if delay is None:
            delay = dt.timedelta(days=60)
        return [
            ('company', '=', self.company.id),
            ('type', '=', self.type),
            ('party', '=', self.party.id),
            ('reference', '=', self.reference),
            ('id', '!=', self.id),
            ['OR',
                ('invoice_date', '=', None),
                [
                    ('invoice_date', '>=', invoice_date - delay),
                    ('invoice_date', '<=', invoice_date + delay),
                    ],
                ],
            ]

    @classmethod
    @ModelView.button_action('account_invoice.wizard_pay')
    def pay(cls, invoices):
        pass

    @classmethod
    @ModelView.button_action(
        'account_invoice.act_reschedule_lines_to_pay_wizard')
    def reschedule_lines_to_pay(cls, invoices):
        pass

    @classmethod
    @ModelView.button_action(
        'account_invoice.act_delegate_lines_to_pay_wizard')
    def delegate_lines_to_pay(cls, invoices):
        pass

    @classmethod
    @ModelView.button
    def process(cls, invoices):
        to_save = []
        paid = []
        posted = []
        for invoice in invoices:
            if invoice.state in {'posted', 'paid'}:
                if invoice.reconciled:
                    paid.append(invoice)
                else:
                    posted.append(invoice)
            elif invoice.state == 'cancelled' and invoice.move:
                if not invoice.reconciled:
                    if invoice.cancel_move:
                        invoice.cancel_move = None
                        invoice.save()
                        to_save.append(invoice)
                    posted.append(invoice)
        cls.save(to_save)
        cls.paid(paid)
        cls._post(posted)

    @classmethod
    @Workflow.transition('paid')
    def paid(cls, invoices):
        # Remove links to lines which actually do not pay the invoice
        cls._clean_payments(invoices)

    @classmethod
    @ModelView.button
    @Workflow.transition('cancelled')
    def cancel(cls, invoices):
        pool = Pool()
        Move = pool.get('account.move')
        Line = pool.get('account.move.line')

        cancel_moves = []
        delete_moves = []
        to_save = []
        for invoice in invoices:
            if invoice.move or invoice.number:
                if invoice.move and invoice.move.state == 'draft':
                    delete_moves.append(invoice.move)
                    delete_moves.extend(invoice.additional_moves)
                elif not invoice.cancel_move:
                    if (invoice.type == 'out'
                            and not invoice.company.cancel_invoice_out):
                        raise AccessError(
                            gettext('account_invoice'
                                '.msg_invoice_customer_cancel_move',
                                invoice=invoice.rec_name))
                    if invoice.move:
                        invoice.cancel_move = invoice.move.cancel()
                        additional_cancel_moves = [
                            m.cancel() for m in invoice.additional_moves]
                        invoice.additional_moves += tuple(
                            additional_cancel_moves)
                        to_save.append(invoice)
                        cancel_moves.append(invoice.cancel_move)
                        cancel_moves.extend(additional_cancel_moves)
        if cancel_moves:
            Move.save(cancel_moves)
        cls._store_cache(invoices)
        cls.save(to_save)
        if delete_moves:
            Move.delete(delete_moves)
        if cancel_moves:
            Move.post(cancel_moves)
        # Write state before reconcile to prevent invoice to go to paid state
        cls.write(invoices, {
                'state': 'cancelled',
                })

        for invoice in invoices:
            if not invoice.move or not invoice.cancel_move:
                continue
            to_reconcile = []
            for move in chain(
                    [invoice.move, invoice.cancel_move],
                    invoice.additional_moves):
                for line in move.lines:
                    if (not line.reconciliation
                            and line.account == invoice.account):
                        to_reconcile.append(line)
            Line.reconcile(to_reconcile)

        cls._clean_payments(invoices)

    @classmethod
    def _clean_payments(cls, invoices):
        to_write = []
        for invoice in invoices:
            to_remove = []
            reconciliations = [l.reconciliation for l in invoice.lines_to_pay]
            for payment_line in invoice.payment_lines:
                if payment_line.reconciliation not in reconciliations:
                    to_remove.append(payment_line.id)
            if to_remove:
                to_write.append([invoice])
                to_write.append({
                        'payment_lines': [('remove', to_remove)],
                        })
        if to_write:
            cls.write(*to_write)


class InvoiceAdditionalMove(ModelSQL):
    __name__ = 'account.invoice-additional-account.move'
    invoice = fields.Many2One(
        'account.invoice', "Invoice", ondelete='CASCADE', required=True)
    move = fields.Many2One(
        'account.move', "Additional Move", ondelete='CASCADE')


class AlternativePayee(ModelSQL):
    __name__ = 'account.invoice.alternative_payee'

    invoice = fields.Many2One(
        'account.invoice', "Invoice", ondelete='CASCADE', required=True)
    party = fields.Many2One(
        'party.party', "Payee", ondelete='RESTRICT', required=True)


class InvoicePaymentLine(ModelSQL):
    __name__ = 'account.invoice-account.move.line'
    invoice = fields.Many2One(
        'account.invoice', "Invoice", ondelete='CASCADE', required=True)
    invoice_account = fields.Function(
        fields.Many2One('account.account', "Invoice Account"),
        'get_invoice')
    invoice_party = fields.Function(
        fields.Many2One('party.party', "Invoice Party"), 'get_invoice')
    invoice_alternative_payees = fields.Function(
        fields.Many2Many(
            'party.party', None, None, "Invoice Alternative Payees"),
        'get_invoice')
    line = fields.Many2One(
        'account.move.line', "Payment Line", ondelete='CASCADE', required=True,
        domain=[
            ('account', '=', Eval('invoice_account', -1)),
            ['OR',
                ('party', '=', Eval('invoice_party', -1)),
                ('party', 'in', Eval('invoice_alternative_payees', [])),
                ],
            ])

    @classmethod
    def __setup__(cls):
        super().__setup__()
        t = cls.__table__()
        cls._sql_constraints = [
            ('line_unique', Unique(t, t.line),
                'account_invoice.msg_invoice_payment_line_unique'),
            ]

    @classmethod
    def get_invoice(cls, records, names):
        result = {}
        for name in names:
            result[name] = {}
        invoice_account = 'invoice_account' in result
        invoice_party = 'invoice_party' in result
        invoice_alternative_payees = 'invoice_alternative_payees' in result
        for record in records:
            if invoice_account:
                result['invoice_account'][record.id] = (
                    record.invoice.account.id)
            if invoice_party:
                if record.invoice.account.party_required:
                    party = record.invoice.party.id
                else:
                    party = None
                result['invoice_party'][record.id] = party
            if invoice_alternative_payees:
                result['invoice_alternative_payees'][record.id] = [
                    p.id for p in record.invoice.alternative_payees]
        return result


class InvoiceLine(sequence_ordered(), ModelSQL, ModelView, TaxableMixin):
    __name__ = 'account.invoice.line'
    _states = {
        'readonly': Eval('invoice_state') != 'draft',
        }

    invoice = fields.Many2One(
        'account.invoice', "Invoice", ondelete='CASCADE',
        states={
            'required': (~Eval('invoice_type') & Eval('party')
                & Eval('currency') & Eval('company')),
            'invisible': Bool(Eval('context', {}).get('standalone')),
            'readonly': _states['readonly'] & Bool(Eval('invoice')),
            })
    invoice_party = fields.Function(
        fields.Many2One(
            'party.party', "Party",
            context={
                'company': Eval('company', -1),
                'party_contact_mechanism_usage': 'invoice',
                },
            depends=['company']),
        'on_change_with_invoice_party', searcher='search_invoice_party')
    invoice_description = fields.Function(
        fields.Char("Invoice Description"),
        'on_change_with_invoice_description',
        searcher='search_invoice_description')
    invoice_state = fields.Function(
        fields.Selection('get_invoice_states', "Invoice State"),
        'on_change_with_invoice_state')
    invoice_type = fields.Selection(
        'get_invoice_types', "Invoice Type",
        states={
            'readonly': Eval('context', {}).get('type') | Eval('type'),
            'required': ~Eval('invoice'),
            })
    party = fields.Many2One(
        'party.party', "Party",
        states={
            'required': ~Eval('invoice'),
            'readonly': _states['readonly'],
            },
        context={
            'company': Eval('company', -1),
            'party_contact_mechanism_usage': 'invoice',
            },
        depends={'company'})
    party_lang = fields.Function(fields.Char('Party Language'),
        'on_change_with_party_lang')
    currency = fields.Many2One(
        'currency.currency', "Currency", required=True,
        states=_states)
    company = fields.Many2One(
        'company.company', "Company", required=True,
        states=_states,
        context={
            'party_contact_mechanism_usage': 'invoice',
            })
    type = fields.Selection([
        ('line', 'Line'),
        ('subtotal', 'Subtotal'),
        ('title', 'Title'),
        ('comment', 'Comment'),
        ], "Type", required=True,
        states={
            'invisible': Bool(Eval('context', {}).get('standalone')),
            'readonly': _states['readonly'],
            })
    quantity = fields.Float(
        "Quantity", digits='unit',
        states={
            'invisible': Eval('type') != 'line',
            'required': Eval('type') == 'line',
            'readonly': _states['readonly'],
            })
    unit = fields.Many2One('product.uom', 'Unit', ondelete='RESTRICT',
        states={
            'required': Bool(Eval('product')),
            'invisible': Eval('type') != 'line',
            'readonly': _states['readonly'],
            },
        domain=[
            If(Bool(Eval('product_uom_category')),
                ('category', '=', Eval('product_uom_category')),
                ('category', '!=', -1)),
            ])
    product = fields.Many2One('product.product', 'Product',
        ondelete='RESTRICT',
        domain=[
            If(Bool(Eval('product_uom_category')),
                ('default_uom_category', '=', Eval('product_uom_category')),
                ()),
            ],
        states={
            'invisible': Eval('type') != 'line',
            'readonly': _states['readonly'],
            },
        context={
            'company': Eval('company', None),
            },
        depends={'company'})
    product_uom_category = fields.Function(
        fields.Many2One(
            'product.uom.category', "Product UoM Category",
            help="The category of Unit of Measure for the product."),
        'on_change_with_product_uom_category')
    account = fields.Many2One('account.account', 'Account',
        ondelete='RESTRICT',
        states={
            'invisible': Eval('type') != 'line',
            'required': Eval('type') == 'line',
            'readonly': _states['readonly'],
            },
        context={
            'date': If(Eval('_parent_invoice', {}).get('accounting_date'),
                Eval('_parent_invoice', {}).get('accounting_date'),
                Eval('_parent_invoice', {}).get('invoice_date')),
            },
        depends={'invoice'})
    unit_price = Monetary(
        "Unit Price", currency='currency', digits=price_digits,
        states={
            'invisible': Eval('type') != 'line',
            'required': Eval('type') == 'line',
            'readonly': _states['readonly'],
            })
    amount = fields.Function(Monetary(
            "Amount", currency='currency', digits='currency',
            states={
                'invisible': ~Eval('type').in_(['line', 'subtotal']),
                }),
        'get_amount')
    description = fields.Text(
        "Description",
        states={
            'readonly': (_states['readonly']
                & ~Id('account', 'group_account_admin').in_(
                    Eval('context', {}).get('groups', []))),
                })
    summary = fields.Function(
        fields.Char('Summary'), 'on_change_with_summary',
        searcher='search_summary')
    note = fields.Text('Note')
    taxes = fields.Many2Many('account.invoice.line-account.tax',
        'line', 'tax', 'Taxes',
        order=[('tax.sequence', 'ASC'), ('tax.id', 'ASC')],
        domain=[('parent', '=', None), ['OR',
                ('group', '=', None),
                ('group.kind', 'in',
                    If(Bool(Eval('_parent_invoice')),
                        If(Eval('_parent_invoice', {}).get('type') == 'out',
                            ['sale', 'both'],
                            ['purchase', 'both']),
                        If(Eval('invoice_type') == 'out',
                            ['sale', 'both'],
                            ['purchase', 'both']))
                    )],
            ('company', '=', Eval('company', -1)),
            ],
        states={
            'invisible': Eval('type') != 'line',
            'readonly': _states['readonly'] | ~Bool(Eval('account')),
            },
        depends={'invoice'})
    taxes_deductible_rate = fields.Numeric(
        "Taxes Deductible Rate", digits=(None, 10),
        domain=[
            ('taxes_deductible_rate', '>=', 0),
            ('taxes_deductible_rate', '<=', 1),
            ],
        states={
            'invisible': (
                (Eval('invoice_type') != 'in')
                | (Eval('type') != 'line')),
            })
    taxes_date = fields.Date(
        "Taxes Date",
        states={
            'invisible': Eval('type') != 'line',
            'readonly': _states['readonly'],
            },
        help="The date at which the taxes are computed.\n"
        "Leave empty for the accounting date.")
    invoice_taxes = fields.Function(fields.Many2Many('account.invoice.tax',
        None, None, 'Invoice Taxes'), 'get_invoice_taxes')
    origin = fields.Reference("Origin", selection='get_origin', states=_states)

    del _states

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._check_modify_exclude = {'note', 'origin', 'description'}

        # Set account domain dynamically for kind
        cls.account.domain = [
            ('closed', '!=', True),
            ('company', '=', Eval('company', -1)),
            ('id', '!=', Eval('_parent_invoice', {}).get('account', -1)),
            If(Bool(Eval('_parent_invoice')),
                If(Eval('_parent_invoice', {}).get('type') == 'out',
                    cls._account_domain('out'),
                    If(Eval('_parent_invoice', {}).get('type') == 'in',
                        cls._account_domain('in'),
                        ['OR',
                            cls._account_domain('out'),
                            cls._account_domain('in')])),
                If(Eval('invoice_type') == 'out',
                    cls._account_domain('out'),
                    If(Eval('invoice_type') == 'in',
                        cls._account_domain('in'),
                        ['OR',
                            cls._account_domain('out'),
                            cls._account_domain('in')]))),
            ]
        cls.sequence.states.update({
                'invisible': Bool(Eval('context', {}).get('standalone')),
                })

    @staticmethod
    def _account_domain(type_):
        if type_ == 'out':
            return ['OR', ('type.revenue', '=', True)]
        elif type_ == 'in':
            return ['OR',
                ('type.expense', '=', True),
                ('type.debt', '=', True),
                ]

    @classmethod
    def get_invoice_types(cls):
        pool = Pool()
        Invoice = pool.get('account.invoice')
        return Invoice.fields_get(['type'])['type']['selection'] + [(None, '')]

    @fields.depends(
        'invoice', '_parent_invoice.currency', '_parent_invoice.company',
        '_parent_invoice.type',
        methods=['on_change_company'])
    def on_change_invoice(self):
        if self.invoice:
            self.currency = self.invoice.currency
            self.company = self.invoice.company
            self.on_change_company()
            self.invoice_type = self.invoice.type

    @fields.depends('company', 'invoice',
        '_parent_invoice.type', 'invoice_type')
    def on_change_company(self):
        invoice_type = self.invoice.type if self.invoice else self.invoice_type
        if (invoice_type == 'in'
                and self.company
                and self.company.purchase_taxes_expense):
            self.taxes_deductible_rate = 0

    @staticmethod
    def default_currency():
        Company = Pool().get('company.company')
        if Transaction().context.get('company'):
            company = Company(Transaction().context['company'])
            return company.currency.id

    @staticmethod
    def default_company():
        return Transaction().context.get('company')

    @staticmethod
    def default_type():
        return 'line'

    @fields.depends('party', 'invoice', '_parent_invoice.party')
    def on_change_with_invoice_party(self, name=None):
        if self.invoice and self.invoice.party:
            return self.invoice.party
        elif self.party:
            return self.party

    @classmethod
    def search_invoice_party(cls, name, clause):
        nested = clause[0][len(name):]
        return ['OR',
            ('invoice.party' + nested, *clause[1:]),
            ('party' + nested, *clause[1:]),
            ]

    @fields.depends('invoice', '_parent_invoice.description')
    def on_change_with_invoice_description(self, name=None):
        if self.invoice:
            return self.invoice.description

    @classmethod
    def search_invoice_description(cls, name, clause):
        return [('invoice.description', *clause[1:])]

    @classmethod
    def default_invoice_state(cls):
        return 'draft'

    @classmethod
    def get_invoice_states(cls):
        pool = Pool()
        Invoice = pool.get('account.invoice')
        return Invoice.fields_get(['state'])['state']['selection']

    @fields.depends('invoice', '_parent_invoice.state')
    def on_change_with_invoice_state(self, name=None):
        if self.invoice:
            state = self.invoice.state
            if state == 'cancelled' and self.invoice.cancel_move:
                state = 'paid'
        else:
            state = 'draft'
        return state

    @fields.depends('invoice', '_parent_invoice.party', 'party')
    def on_change_with_party_lang(self, name=None):
        Config = Pool().get('ir.configuration')
        if self.invoice and self.invoice.party:
            party = self.invoice.party
        else:
            party = self.party
        if party and party.lang:
            return party.lang.code
        return Config.get_language()

    @fields.depends('description')
    def on_change_with_summary(self, name=None):
        return firstline(self.description or '')

    @classmethod
    def search_summary(cls, name, clause):
        return [('description', *clause[1:])]

    @fields.depends(
        'type', 'quantity', 'unit_price', 'taxes_deductible_rate', 'invoice',
        '_parent_invoice.currency', 'currency', 'taxes',
        '_parent_invoice.type', 'invoice_type',
        methods=['_get_taxes'])
    def on_change_with_amount(self):
        if self.type == 'line':
            currency = (self.invoice.currency if self.invoice
                else self.currency)
            amount = (Decimal(str(self.quantity or 0))
                * (self.unit_price or Decimal(0)))
            invoice_type = (
                self.invoice.type if self.invoice else self.invoice_type)
            if (invoice_type == 'in'
                    and self.taxes_deductible_rate is not None
                    and self.taxes_deductible_rate != 1):
                with Transaction().set_context(_deductible_rate=1):
                    tax_amount = sum(
                        t.amount for t in self._get_taxes().values())
                non_deductible_amount = (
                    tax_amount * (1 - self.taxes_deductible_rate))
                amount += non_deductible_amount
            if currency:
                return currency.round(amount)
            return amount
        return Decimal(0)

    def get_amount(self, name):
        if self.type == 'line':
            return self.on_change_with_amount()
        elif self.type == 'subtotal':
            subtotal = Decimal(0)
            for line2 in self.invoice.lines:
                if line2.type == 'line':
                    subtotal += line2.on_change_with_amount()
                elif line2.type == 'subtotal':
                    if self == line2:
                        break
                    subtotal = Decimal(0)
            return subtotal
        else:
            return Decimal(0)

    @property
    def origin_name(self):
        if isinstance(self.origin, self.__class__) and self.origin.id >= 0:
            return self.origin.invoice.rec_name
        if self.origin and self.origin.id >= 0:
            return self.origin.rec_name

    @classmethod
    def default_taxes_deductible_rate(cls):
        return 1

    @property
    def taxable_lines(self):
        # In case we're called from an on_change we have to use some sensible
        # defaults
        context = Transaction().context
        if (getattr(self, 'invoice', None)
                and getattr(self.invoice, 'type', None)):
            invoice_type = self.invoice.type
        else:
            invoice_type = getattr(self, 'invoice_type', None)
        if invoice_type == 'in':
            if context.get('_deductible_rate') is not None:
                deductible_rate = context['_deductible_rate']
            else:
                deductible_rate = getattr(self, 'taxes_deductible_rate', 1)
            if deductible_rate is None:
                deductible_rate = 1
            if not deductible_rate:
                return []
        else:
            deductible_rate = 1
        return [(
                list(getattr(self, 'taxes', None)) or [],
                ((getattr(self, 'unit_price', None) or Decimal(0))
                    * deductible_rate),
                getattr(self, 'quantity', None) or 0,
                getattr(self, 'tax_date', None),
                )]

    @property
    def tax_date(self):
        if getattr(self, 'taxes_date', None):
            return self.taxes_date
        elif hasattr(self, 'invoice') and hasattr(self.invoice, 'tax_date'):
            return self.invoice.tax_date
        else:
            return super().tax_date

    def _get_tax_context(self):
        if self.invoice:
            return self.invoice._get_tax_context()
        else:
            context = {}
            if self.company:
                context['company'] = self.company.id
            return context

    def get_invoice_taxes(self, name):
        if not self.invoice:
            return
        taxes_keys = self._get_taxes().keys()
        taxes = []
        for tax in self.invoice.taxes:
            if tax.manual:
                continue
            key = tax._key
            if key in taxes_keys:
                taxes.append(tax.id)
        return taxes

    @fields.depends('invoice',
        '_parent_invoice.accounting_date', '_parent_invoice.invoice_date')
    def _get_tax_rule_pattern(self):
        '''
        Get tax rule pattern
        '''
        if self.invoice:
            date = self.invoice.accounting_date or self.invoice.invoice_date
        else:
            date = None
        return {
            'date': date,
            }

    @fields.depends(
        'product', 'unit', 'taxes', '_parent_invoice.type',
        '_parent_invoice.party', 'party', 'invoice', 'invoice_type',
        '_parent_invoice.invoice_date', '_parent_invoice.accounting_date',
        'company',
        methods=['_get_tax_rule_pattern'])
    def on_change_product(self):
        if not self.product:
            return

        party = None
        if self.invoice and self.invoice.party:
            party = self.invoice.party
        elif self.party:
            party = self.party

        date = (self.invoice.accounting_date or self.invoice.invoice_date
            if self.invoice else None)
        if self.invoice and self.invoice.type:
            type_ = self.invoice.type
        else:
            type_ = self.invoice_type
        if type_ == 'in':
            with Transaction().set_context(date=date):
                self.account = self.product.account_expense_used
            taxes = set()
            pattern = self._get_tax_rule_pattern()
            for tax in self.product.supplier_taxes_used:
                if party and party.supplier_tax_rule:
                    tax_ids = party.supplier_tax_rule.apply(tax, pattern)
                    if tax_ids:
                        taxes.update(tax_ids)
                    continue
                taxes.add(tax.id)
            if party and party.supplier_tax_rule:
                tax_ids = party.supplier_tax_rule.apply(None, pattern)
                if tax_ids:
                    taxes.update(tax_ids)
            self.taxes = taxes

            if self.company and self.company.purchase_taxes_expense:
                self.taxes_deductible_rate = 0
            else:
                self.taxes_deductible_rate = (
                    self.product.supplier_taxes_deductible_rate_used)
        else:
            with Transaction().set_context(date=date):
                self.account = self.product.account_revenue_used
            taxes = set()
            pattern = self._get_tax_rule_pattern()
            for tax in self.product.customer_taxes_used:
                if party and party.customer_tax_rule:
                    tax_ids = party.customer_tax_rule.apply(tax, pattern)
                    if tax_ids:
                        taxes.update(tax_ids)
                    continue
                taxes.add(tax.id)
            if party and party.customer_tax_rule:
                tax_ids = party.customer_tax_rule.apply(None, pattern)
                if tax_ids:
                    taxes.update(tax_ids)
            self.taxes = taxes

        category = self.product.default_uom.category
        if not self.unit or self.unit.category != category:
            self.unit = self.product.default_uom.id

    @cached_property
    def product_name(self):
        return self.product.rec_name if self.product else ''

    @fields.depends('product')
    def on_change_with_product_uom_category(self, name=None):
        return self.product.default_uom_category if self.product else None

    @fields.depends(
        'account', 'product', 'invoice', 'taxes',
        '_parent_invoice.party', '_parent_invoice.type',
        'party', 'invoice', 'invoice_type',
        methods=['_get_tax_rule_pattern'])
    def on_change_account(self):
        if self.product:
            return
        taxes = set()
        party = None
        if self.invoice and self.invoice.party:
            party = self.invoice.party
        elif self.party:
            party = self.party

        if self.invoice and self.invoice.type:
            type_ = self.invoice.type
        else:
            type_ = self.invoice_type
        if party and type_:
            if type_ == 'in':
                tax_rule = party.supplier_tax_rule
            else:
                tax_rule = party.customer_tax_rule
        else:
            tax_rule = None

        if self.account:
            pattern = self._get_tax_rule_pattern()
            for tax in self.account.taxes:
                if tax_rule:
                    tax_ids = tax_rule.apply(tax, pattern)
                    if tax_ids:
                        taxes.update(tax_ids)
                    continue
                taxes.add(tax.id)
            if tax_rule:
                tax_ids = tax_rule.apply(None, pattern)
                if tax_ids:
                    taxes.update(tax_ids)
        self.taxes = taxes

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

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

    def get_rec_name(self, name):
        pool = Pool()
        Lang = pool.get('ir.lang')
        if self.product:
            lang = Lang.get()
            prefix = (lang.format_number_symbol(
                self.quantity or 0, self.unit, digits=self.unit.digits)
                + ' %s' % self.product.rec_name)
        elif self.account:
            prefix = self.account.rec_name
        else:
            prefix = '(%s)' % self.id
        if self.invoice:
            return '%s @ %s' % (prefix, self.invoice.rec_name)
        else:
            return prefix

    @classmethod
    def search_rec_name(cls, name, clause):
        _, operator, value = clause
        if operator.startswith('!') or operator.startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [bool_op,
            ('invoice.rec_name', *clause[1:]),
            ('product.rec_name', *clause[1:]),
            ('account.rec_name', *clause[1:]),
            ]

    @classmethod
    def check_modification(cls, mode, lines, values=None, external=False):
        super().check_modification(
            mode, lines, values=values, external=external)
        if mode == 'create':
            for line in lines:
                if line.invoice and line.invoice.state != 'draft':
                    raise AccessError(gettext(
                            'account_invoice.msg_invoice_line_create_draft',
                            invoice=line.invoice.rec_name))
        elif (mode == 'delete'
                or (mode == 'write'
                    and values.keys() - cls._check_modify_exclude)):
            for line in lines:
                if line.invoice and not line.invoice.is_modifiable:
                    raise AccessError(gettext(
                            'account_invoice.msg_invoice_line_modify',
                            line=line.rec_name,
                            invoice=line.invoice.rec_name))

    @classmethod
    def view_attributes(cls):
        return super().view_attributes() + [
            ('/form//field[@name="note"]|/form//field[@name="description"]',
                'spell', Eval('party_lang'))]

    @classmethod
    def copy(cls, lines, default=None):
        if default is None:
            default = {}
        else:
            default = default.copy()
        default.setdefault('origin', None)
        return super().copy(lines, default=default)

    def _compute_taxes(self):
        pool = Pool()
        Currency = pool.get('currency.currency')
        TaxLine = pool.get('account.tax.line')

        tax_lines = []
        if self.type != 'line':
            return tax_lines
        taxes = self._get_taxes().values()
        for tax in taxes:
            amount = tax.base
            with Transaction().set_context(
                    date=self.invoice.currency_date):
                amount = Currency.compute(
                    self.invoice.currency, amount,
                    self.invoice.company.currency)
            tax_line = TaxLine()
            tax_line.amount = amount
            tax_line.type = 'base'
            tax_line.tax = tax.tax
            tax_lines.append(tax_line)
        return tax_lines

    def get_move_lines(self):
        '''
        Return a list of move lines instances for invoice line
        '''
        pool = Pool()
        Currency = pool.get('currency.currency')
        MoveLine = pool.get('account.move.line')
        if self.type != 'line':
            return []
        line = MoveLine()
        if self.invoice.currency != self.invoice.company.currency:
            with Transaction().set_context(date=self.invoice.currency_date):
                amount = Currency.compute(self.invoice.currency,
                    self.amount, self.invoice.company.currency)
            line.amount_second_currency = self.amount
            line.second_currency = self.invoice.currency
        else:
            amount = self.amount
            line.amount_second_currency = None
            line.second_currency = None
        if amount >= 0:
            if self.invoice.type == 'out':
                line.debit, line.credit = 0, amount
            else:
                line.debit, line.credit = amount, 0
        else:
            if self.invoice.type == 'out':
                line.debit, line.credit = -amount, 0
            else:
                line.debit, line.credit = 0, -amount
        if line.amount_second_currency:
            line.amount_second_currency = (
                line.amount_second_currency.copy_sign(
                    line.debit - line.credit))
        line.account = self.account
        if self.account.party_required:
            line.party = self.invoice.party
        line.origin = self
        line.tax_lines = self._compute_taxes()
        return [line]

    def _credit(self):
        '''
        Return credit line.
        '''
        line = self.__class__()
        line.origin = self
        if self.quantity:
            line.quantity = -self.quantity
        else:
            line.quantity = self.quantity

        for field in [
                'sequence', 'type', 'invoice_type', 'party', 'currency',
                'company', 'unit_price', 'description', 'unit', 'product',
                'account', 'taxes_deductible_rate']:
            setattr(line, field, getattr(self, field))
        line.taxes_date = self.tax_date
        line.taxes = self.taxes
        return line


class InvoiceLineTax(ModelSQL):
    __name__ = 'account.invoice.line-account.tax'
    line = fields.Many2One(
        'account.invoice.line', "Invoice Line",
        ondelete='CASCADE', required=True)
    tax = fields.Many2One('account.tax', 'Tax', ondelete='RESTRICT',
            required=True)

    @classmethod
    def __setup__(cls):
        super().__setup__()
        t = cls.__table__()
        cls._sql_constraints += [
            ('line_tax_unique', Unique(t, t.line, t.tax),
                'account_invoice.msg_invoice_line_tax_unique'),
            ]

    @classmethod
    def __register__(cls, module):
        # Migration from 7.0: rename to standard name
        backend.TableHandler.table_rename(
            'account_invoice_line_account_tax', cls._table)
        super().__register__(module)


class InvoiceTax(sequence_ordered(), ModelSQL, ModelView):
    __name__ = 'account.invoice.tax'
    _rec_name = 'description'
    _states = {
        'readonly': Eval('invoice_state') != 'draft',
        }

    invoice = fields.Many2One(
        'account.invoice', "Invoice", ondelete='CASCADE', required=True,
        states={
            'readonly': _states['readonly'] & Bool(Eval('invoice')),
            })
    invoice_state = fields.Function(
        fields.Selection('get_invoice_states', "Invoice State"),
        'on_change_with_invoice_state')
    description = fields.Char(
        "Description", size=None, required=True,
        states={
            'readonly': (_states['readonly']
                & ~Id('account', 'group_account_admin').in_(
                    Eval('context', {}).get('groups', []))),
            })
    sequence_number = fields.Function(fields.Integer('Sequence Number'),
            'get_sequence_number')
    account = fields.Many2One('account.account', 'Account', required=True,
        domain=[
            ('type', '!=', None),
            ('closed', '!=', True),
            ('company', '=', Eval('_parent_invoice', {}).get('company', 0)),
            ('id', '!=', Eval('_parent_invoice', {}).get('account', -1)),
            ],
        states=_states, depends={'invoice'})
    base = Monetary(
        "Base", currency='currency', digits='currency', required=True,
        states=_states)
    amount = Monetary(
        "Amount", currency='currency', digits='currency', required=True,
        states=_states,
        depends={'tax', 'base', 'manual'})
    currency = fields.Function(fields.Many2One('currency.currency',
        'Currency'), 'on_change_with_currency')
    manual = fields.Boolean('Manual', states=_states)
    tax = fields.Many2One('account.tax', 'Tax',
        ondelete='RESTRICT',
        domain=[
            ['OR',
                ('group', '=', None),
                ('group.kind', 'in',
                    If(Eval('_parent_invoice', {}).get('type') == 'out',
                        ['sale', 'both'],
                        ['purchase', 'both']),
                    )],
            ('company', '=', Eval('_parent_invoice', {}).get('company', 0)),
            ],
        states={
            'readonly': (
                ~Eval('manual', False) | ~Bool(Eval('invoice'))
                | _states['readonly']),
            },
        depends={'invoice'})
    legal_notice = fields.Text(
        "Legal Notice",
        states={
            'readonly': (_states['readonly']
                & ~Id('account', 'group_account_admin').in_(
                    Eval('context', {}).get('groups', []))),
            })

    del _states

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.__access__.add('invoice')
        cls._check_modify_exclude = {'description', 'legal_notice'}

    @staticmethod
    def default_base():
        return Decimal(0)

    @staticmethod
    def default_amount():
        return Decimal(0)

    @staticmethod
    def default_manual():
        return True

    @classmethod
    def default_invoice_state(cls):
        pool = Pool()
        Invoice = pool.get('account.invoice')
        return Invoice.default_state()

    @classmethod
    def get_invoice_states(cls):
        pool = Pool()
        Invoice = pool.get('account.invoice')
        return Invoice.fields_get(['state'])['state']['selection']

    @fields.depends('invoice', '_parent_invoice.state')
    def on_change_with_invoice_state(self, name=None):
        if self.invoice:
            return self.invoice.state

    @fields.depends('invoice', '_parent_invoice.currency')
    def on_change_with_currency(self, name=None):
        return self.invoice.currency if self.invoice else None

    @fields.depends(
        'tax', 'invoice', '_parent_invoice.party', 'base',
        methods=['_compute_amount'])
    def on_change_tax(self):
        Tax = Pool().get('account.tax')
        if not self.tax:
            return
        if self.invoice:
            context = self.invoice._get_tax_context()
        else:
            context = {}
        with Transaction().set_context(**context):
            tax = Tax(self.tax.id)
        self.description = tax.description
        if self.base is not None:
            if self.base >= 0:
                self.account = tax.invoice_account
            else:
                self.account = tax.credit_note_account
        self._compute_amount()

    @fields.depends('base', 'tax', methods=['_compute_amount'])
    def on_change_base(self):
        if self.base is not None and self.tax:
            if self.base >= 0:
                self.account = self.tax.invoice_account
            else:
                self.account = self.tax.credit_note_account
        self._compute_amount()

    @fields.depends(
        'tax', 'base', 'manual', 'invoice', '_parent_invoice.currency',
        # From_date
        '_parent_invoice.accounting_date', '_parent_invoice.invoice_date',
        '_parent_invoice.company')
    def _compute_amount(self):
        pool = Pool()
        Tax = pool.get('account.tax')
        if self.tax and self.manual:
            tax = self.tax
            base = self.base or Decimal(0)
            if self.invoice and self.invoice.tax_date:
                tax_date = self.invoice.tax_date
                for values in Tax.compute([tax], base, 1, tax_date):
                    if (values['tax'] == tax
                            and values['base'] == base):
                        amount = values['amount']
                        if self.invoice.currency:
                            amount = self.invoice.currency.round(amount)
                        self.amount = amount

    @property
    def _key(self):
        # Same as _TaxLine
        return (self.account, self.tax, (getattr(self, 'base', 0) or 0) >= 0)

    @classmethod
    def check_modification(cls, mode, taxes, values=None, external=False):
        super().check_modification(
            mode, taxes, values=values, external=external)
        if mode == 'create':
            for tax in taxes:
                if tax.invoice.state != 'draft':
                    raise AccessError(gettext(
                            'account_invoice.msg_invoice_line_create_draft',
                            invoice=tax.invoice.rec_name))
        elif (mode == 'delete'
                or (mode == 'write'
                    and values.keys() - cls._check_modify_exclude)):
            for tax in taxes:
                if not tax.invoice.is_modifiable:
                    raise AccessError(gettext(
                            'account_invoice.msg_invoice_tax_modify',
                            tax=tax.rec_name,
                            invoice=tax.invoice.rec_name))

    def get_sequence_number(self, name):
        i = 1
        for tax in self.invoice.taxes:
            if tax == self:
                return i
            i += 1
        return 0

    def get_move_lines(self):
        '''
        Return a list of move lines instances for invoice tax
        '''
        Currency = Pool().get('currency.currency')
        pool = Pool()
        Currency = pool.get('currency.currency')
        MoveLine = pool.get('account.move.line')
        TaxLine = pool.get('account.tax.line')
        line = MoveLine()
        if not self.amount:
            return []
        line.description = self.description
        if self.invoice.currency != self.invoice.company.currency:
            with Transaction().set_context(date=self.invoice.currency_date):
                amount = Currency.compute(self.invoice.currency, self.amount,
                    self.invoice.company.currency)
                base = Currency.compute(self.invoice.currency, self.base,
                    self.invoice.company.currency)
            line.amount_second_currency = self.amount
            line.second_currency = self.invoice.currency
        else:
            amount = self.amount
            base = self.base
            line.amount_second_currency = None
            line.second_currency = None
        if amount >= 0:
            if self.invoice.type == 'out':
                line.debit, line.credit = 0, amount
            else:
                line.debit, line.credit = amount, 0
        else:
            if self.invoice.type == 'out':
                line.debit, line.credit = -amount, 0
            else:
                line.debit, line.credit = 0, -amount
        if line.amount_second_currency:
            line.amount_second_currency = (
                line.amount_second_currency.copy_sign(
                    line.debit - line.credit))
        line.account = self.account
        if self.account.party_required:
            line.party = self.invoice.party
        line.origin = self
        if self.tax:
            tax_lines = []
            tax_line = TaxLine()
            tax_line.amount = amount
            tax_line.type = 'tax'
            tax_line.tax = self.tax
            tax_lines.append(tax_line)
            if self.manual:
                tax_line = TaxLine()
                tax_line.amount = base
                tax_line.type = 'base'
                tax_line.tax = self.tax
                tax_lines.append(tax_line)
            line.tax_lines = tax_lines
        return [line]

    def _credit(self):
        '''
        Return credit tax.
        '''
        line = self.__class__()
        line.base = -self.base
        line.amount = -self.amount

        for field in ['description', 'sequence', 'manual', 'account', 'tax']:
            setattr(line, field, getattr(self, field))
        return line


class PaymentMethod(DeactivableMixin, ModelSQL, ModelView):
    __name__ = 'account.invoice.payment.method'
    company = fields.Many2One('company.company', "Company", required=True)
    name = fields.Char("Name", required=True, translate=True)
    journal = fields.Many2One(
        'account.journal', "Journal", required=True,
        domain=[('type', '=', 'cash')],
        context={
            'company': Eval('company', -1),
            },
        depends={'company'})
    credit_account = fields.Many2One('account.account', "Credit Account",
        required=True,
        domain=[
            ('type', '!=', None),
            ('closed', '!=', True),
            ('company', '=', Eval('company', -1)),
            ])
    debit_account = fields.Many2One('account.account', "Debit Account",
        required=True,
        domain=[
            ('type', '!=', None),
            ('closed', '!=', True),
            ('company', '=', Eval('company', -1)),
            ])

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._order.insert(0, ('name', 'ASC'))

    @classmethod
    def default_company(cls):
        return Transaction().context.get('company')


class InvoiceReportRevision(ModelSQL, ModelView, InvoiceReportMixin):
    __name__ = 'account.invoice.report.revision'
    invoice = fields.Many2One(
        'account.invoice', "Invoice", required=True, ondelete='CASCADE')
    date = fields.DateTime("Date", required=True, readonly=True)
    filename = fields.Function(fields.Char("File Name"), 'get_filename')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.__access__.add('invoice')
        cls._order.insert(0, ('date', 'DESC'))
        cls.invoice_report_cache.filename = 'filename'

    @classmethod
    def default_date(cls):
        return dt.datetime.now()

    @classmethod
    def get_filename(cls, revisions, name):
        pool = Pool()
        ActionReport = pool.get('ir.action.report')

        action_report, = ActionReport.search([
                ('report_name', '=', 'account.invoice'),
                ], limit=1)

        action_report_name = action_report.name[:100]
        if action_report.record_name:
            template = TextTemplate(action_report.record_name)
        else:
            template = None
        filenames = {}
        for revision in revisions:
            invoice = revision.invoice
            if template:
                record_name = template.generate(record=invoice).render()
            else:
                record_name = invoice.rec_name
            filename = '-'.join([action_report_name, record_name])
            filenames[revision.id] = (
                f'{slugify(filename)}.{revision.invoice_report_format}')
        return filenames


class RefreshInvoiceReport(Wizard):
    __name__ = 'account.invoice.refresh_invoice_report'
    start_state = 'archive'
    archive = StateTransition()
    print_ = StateReport('account.invoice')

    def transition_archive(self):
        for record in self.records:
            record.create_invoice_report_revision()
        self.model.save(self.records)
        return 'print_'

    def do_print_(self, action):
        ids = [r.id for r in self.records]
        return action, {'ids': ids}


class InvoiceReport(Report):
    __name__ = 'account.invoice'

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.__rpc__['execute'] = RPC(False)

    @classmethod
    def _execute(cls, records, header, data, action):
        pool = Pool()
        Invoice = pool.get('account.invoice')
        # Re-instantiate because records are TranslateModel
        invoice, = Invoice.browse(records)
        if invoice.invoice_report_cache:
            return (
                invoice.invoice_report_format,
                invoice.invoice_report_cache)
        else:
            result = super()._execute(records, header, data, action)
            if invoice.invoice_report_versioned:
                format_, data = result
                if isinstance(data, str):
                    data = bytes(data, 'utf-8')
                invoice.invoice_report_format = format_
                invoice.invoice_report_cache = \
                    Invoice.invoice_report_cache.cast(data)
                invoice.save()
            return result

    @classmethod
    def render(cls, *args, **kwargs):
        # Reset to default language to always have header and footer rendered
        # in the default language
        with Transaction().set_context(language=False):
            return super().render(*args, **kwargs)

    @classmethod
    def execute(cls, ids, data):
        with Transaction().set_context(address_with_party=True):
            return super().execute(ids, data)

    @classmethod
    def get_context(cls, records, header, data):
        pool = Pool()
        Date = pool.get('ir.date')
        context = super().get_context(records, header, data)
        context['invoice'] = context['record']
        with Transaction().set_context(company=context['invoice'].company.id):
            context['today'] = Date.today()
        return context


class InvoiceEdocument(Wizard):
    __name__ = 'account.invoice.edocument'

    start = StateView(
        'account.invoice.edocument.start',
        'account_invoice.edocument_start_view_form', [
            Button("Cancel", 'end', 'tryton-cancel'),
            Button("Render", 'render', 'tryton-ok', default=True),
            ])
    render = StateTransition()
    result = StateView(
        'account.invoice.edocument.result',
        'account_invoice.edocument_result_view_form', [
            Button("Close", 'end', 'tryton-close', default=True),
            ])

    def transition_render(self):
        pool = Pool()
        Start = pool.get('account.invoice.edocument.start')
        if self.start.format not in dict(Start.format.selection):
            raise ValueError("Unsupported format")
        Edocument = pool.get(self.start.format)
        edocument = Edocument(self.record)
        file = edocument.render(self.start.template)
        if isinstance(file, str):
            file = file.decode('utf-8')
        self.result.file = file
        self.result.filename = edocument.filename
        return 'result'

    def default_result(self, fields):
        file = self.result.file
        self.result.file = None  # No need to store it in the session
        return {
            'file': file,
            'filename': self.result.filename,
            }


class InvoiceEdocumentStart(ModelView):
    __name__ = 'account.invoice.edocument.start'

    format = fields.Selection([
            ], "Format", required=True)
    template = fields.Selection('get_templates', "Template", required=True)

    @fields.depends()
    def get_templates(self):
        return []


class InvoiceEdocumentResult(ModelView):
    __name__ = 'account.invoice.edocument.result'

    file = fields.Binary("File", readonly=True, filename='filename')
    filename = fields.Char("File Name", readonly=True)


class PayInvoiceStart(ModelView):
    __name__ = 'account.invoice.pay.start'

    payee = fields.Many2One(
        'party.party', "Payee", required=True,
        domain=[
            ('id', 'in', Eval('payees', []))
            ],
        context={
            'company': Eval('company', -1),
            },
        depends=['company'])
    payees = fields.Many2Many(
        'party.party', None, None, "Payees", readonly=True,
        context={
            'company': Eval('company', -1),
            },
        depends=['company'])
    amount = Monetary(
        "Amount", currency='currency', digits='currency', required=True)
    currency = fields.Many2One('currency.currency', 'Currency', readonly=True)
    description = fields.Char('Description', size=None)
    company = fields.Many2One('company.company', "Company", readonly=True)
    invoice_account = fields.Many2One(
        'account.account', "Invoice Account", readonly=True)
    payment_method = fields.Many2One(
        'account.invoice.payment.method', "Payment Method", required=True,
        domain=[
            ('company', '=', Eval('company', -1)),
            ('debit_account', '!=', Eval('invoice_account', -1)),
            ('credit_account', '!=', Eval('invoice_account', -1)),
            ],
        depends={'amount'})
    date = fields.Date('Date', required=True)

    @staticmethod
    def default_date():
        Date = Pool().get('ir.date')
        return Date.today()


class PayInvoiceAsk(ModelView):
    __name__ = 'account.invoice.pay.ask'
    type = fields.Selection([
            ('writeoff', "Write-Off"),
            ('partial', "Partial Payment"),
            ('overpayment', "Overpayment"),
            ], 'Type', required=True,
        domain=[
            If(Eval('amount_writeoff', 0) >= 0,
                ('type', 'in', ['writeoff', 'partial']),
                ()),
            ])
    writeoff = fields.Many2One(
        'account.move.reconcile.write_off', "Write Off",
        domain=[
            ('company', '=', Eval('company', -1)),
            ],
        states={
            'invisible': Eval('type') != 'writeoff',
            'required': Eval('type') == 'writeoff',
            })
    amount = Monetary(
        "Payment Amount",
        currency='currency', digits='currency', readonly=True)
    currency = fields.Many2One('currency.currency', "Currency", readonly=True)
    amount_writeoff = Monetary(
        "Write-Off Amount",
        currency='currency', digits='currency',
        readonly=True,
        states={
            'invisible': ~Eval('type').in_(['writeoff', 'overpayment']),
            })
    lines_to_pay = fields.Many2Many('account.move.line', None, None,
            'Lines to Pay', readonly=True)
    lines = fields.Many2Many('account.move.line', None, None, 'Lines',
        domain=[
            ('id', 'in', Eval('lines_to_pay')),
            ('reconciliation', '=', None),
            ],
        states={
            'invisible': ~Eval('type').in_(['writeoff', 'overpayment']),
            'required': Eval('type').in_(['writeoff', 'overpayment']),
            })
    payment_lines = fields.Many2Many('account.move.line', None, None,
        'Payment Lines', readonly=True,
        states={
            'invisible': ~Eval('type').in_(['writeoff', 'overpayment']),
            })
    company = fields.Many2One('company.company', 'Company', readonly=True)
    invoice = fields.Many2One('account.invoice', 'Invoice', readonly=True)

    @staticmethod
    def default_type():
        return 'partial'

    @fields.depends(
        'lines', 'amount', 'currency', 'invoice', 'payment_lines', 'company')
    def on_change_lines(self):
        self.amount_writeoff = Decimal(0)
        if not self.invoice:
            return

        def balance(line):
            if self.currency == line.second_currency:
                return line.amount_second_currency
            elif self.currency == self.company.currency:
                return line.debit - line.credit
            else:
                return 0

        for line in self.lines:
            self.amount_writeoff += balance(line)
        for line in self.payment_lines:
            self.amount_writeoff += balance(line)
        if self.invoice.type == 'in':
            self.amount_writeoff = - self.amount_writeoff - self.amount
        else:
            self.amount_writeoff = self.amount_writeoff - self.amount


class PayInvoice(Wizard):
    __name__ = 'account.invoice.pay'
    start = StateView('account.invoice.pay.start',
        'account_invoice.pay_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('OK', 'choice', 'tryton-ok', default=True),
            ])
    choice = StateTransition()
    ask = StateView('account.invoice.pay.ask',
        'account_invoice.pay_ask_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('OK', 'pay', 'tryton-ok', default=True),
            ])
    pay = StateTransition()

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.__rpc__['create'].fresh_session = True

    def get_reconcile_lines_for_amount(self, invoice, amount, currency):
        if invoice.type == 'in':
            amount *= -1
        return invoice.get_reconcile_lines_for_amount(
            amount, currency, party=self.start.payee)

    def default_start(self, fields):
        default = {}
        invoice = self.record
        payee = None
        if not invoice.alternative_payees:
            payee = invoice.party
        else:
            try:
                payee, = invoice.alternative_payees
            except ValueError:
                pass
        if payee:
            default['payee'] = payee.id
        default['payees'] = (
            [invoice.party.id] + [p.id for p in invoice.alternative_payees])
        default['company'] = invoice.company.id
        default['currency'] = invoice.currency.id
        default['amount'] = (invoice.amount_to_pay_today
            or invoice.amount_to_pay)
        default['invoice_account'] = invoice.account.id
        return default

    def transition_choice(self):
        invoice = self.record
        amount = self.start.amount
        currency = self.start.currency
        _, remainder = self.get_reconcile_lines_for_amount(
            invoice, amount, currency)
        if remainder == Decimal(0) and amount <= invoice.amount_to_pay:
            return 'pay'
        return 'ask'

    def default_ask(self, fields):
        default = {}
        invoice = self.record
        amount = self.start.amount
        currency = self.start.currency
        default['lines_to_pay'] = [x.id for x in invoice.lines_to_pay
                if not x.reconciliation]

        default['amount'] = amount
        default['currency'] = currency.id
        default['company'] = invoice.company.id

        if currency.is_zero(amount):
            lines = invoice.lines_to_pay
        else:
            lines, _ = self.get_reconcile_lines_for_amount(
                invoice, amount, currency)
        default['lines'] = [x.id for x in lines]

        for line_id in default['lines'][:]:
            if line_id not in default['lines_to_pay']:
                default['lines'].remove(line_id)

        default['payment_lines'] = [x.id for x in invoice.payment_lines
                if not x.reconciliation]

        default['invoice'] = invoice.id

        if amount >= invoice.amount_to_pay:
            default['type'] = 'overpayment'
        elif currency.is_zero(amount):
            default['type'] = 'writeoff'
        return default

    def transition_pay(self):
        pool = Pool()
        MoveLine = pool.get('account.move.line')
        Lang = pool.get('ir.lang')

        invoice = self.record
        amount = self.start.amount
        currency = self.start.currency

        reconcile_lines, remainder = (
            self.get_reconcile_lines_for_amount(invoice, amount, currency))

        overpayment = 0
        if (0 <= invoice.amount_to_pay < amount
                or amount < invoice.amount_to_pay <= 0):
            if self.ask.type == 'partial':
                lang = Lang.get()
                raise PayInvoiceError(
                    gettext('account_invoice'
                        '.msg_invoice_pay_amount_greater_amount_to_pay',
                        invoice=invoice.rec_name,
                        amount_to_pay=lang.currency(
                            invoice.amount_to_pay, invoice.currency)))
            else:
                if not invoice.amount_to_pay:
                    raise PayInvoiceError(
                        gettext('account_invoice.msg_invoice_overpay_paid',
                            invoice=invoice.rec_name))
                overpayment = amount - invoice.amount_to_pay

        lines = []
        if not currency.is_zero(amount):
            lines = invoice.pay_invoice(
                amount, self.start.payment_method, self.start.date,
                self.start.description, overpayment, party=self.start.payee)

        if remainder:
            if self.ask.type != 'partial':
                to_reconcile = {l for l in self.ask.lines}
                to_reconcile.update(
                    l for l in invoice.payment_lines
                    if not l.reconciliation
                    and (not invoice.account.party_required
                        or l.party == self.start.payee))
                if self.ask.type == 'writeoff':
                    to_reconcile.update(lines)
                if to_reconcile:
                    MoveLine.reconcile(
                        to_reconcile,
                        writeoff=self.ask.writeoff,
                        date=self.start.date)
        else:
            reconcile_lines += lines
            if reconcile_lines:
                MoveLine.reconcile(reconcile_lines)
        return 'end'


class CreditInvoiceStart(ModelView):
    __name__ = 'account.invoice.credit.start'
    invoice_date = fields.Date("Invoice Date")
    with_refund = fields.Boolean('With Refund',
        states={
            'readonly': ~Eval('with_refund_allowed'),
            'invisible': ~Eval('with_refund_allowed'),
            },
        help='If true, the current invoice(s) will be cancelled.')
    with_refund_allowed = fields.Boolean("With Refund Allowed", readonly=True)


class CreditInvoice(Wizard):
    __name__ = 'account.invoice.credit'
    start = StateView('account.invoice.credit.start',
        'account_invoice.credit_start_view_form', [
            Button('Cancel', 'end', 'tryton-cancel'),
            Button('Credit', 'credit', 'tryton-ok', default=True),
            ])
    credit = StateAction('account_invoice.act_invoice_form')

    def default_start(self, fields):
        default = {
            'with_refund': True,
            'with_refund_allowed': True,
            }
        for invoice in self.records:
            if invoice.state != 'posted' or invoice.type == 'in':
                default['with_refund'] = False
                default['with_refund_allowed'] = False
                break
            if invoice.payment_lines:
                default['with_refund'] = False
        return default

    @property
    def _credit_options(self):
        return dict(
            refund=self.start.with_refund,
            invoice_date=self.start.invoice_date,
            )

    def do_credit(self, action):
        credit_invoices = self.model.credit(
            self.records, **self._credit_options)

        data = {'res_id': [i.id for i in credit_invoices]}
        if len(credit_invoices) == 1:
            action['views'].reverse()
        return action, data


class RescheduleLinesToPay(Wizard):
    __name__ = 'account.invoice.lines_to_pay.reschedule'
    start = StateAction('account.act_reschedule_lines_wizard')

    def do_start(self, action):
        return action, {
            'ids': [
                l.id for l in self.record.lines_to_pay
                if not l.reconciliation],
            'model': 'account.move.line',
            }


class DelegateLinesToPay(Wizard):
    __name__ = 'account.invoice.lines_to_pay.delegate'
    start = StateAction('account.act_delegate_lines_wizard')

    def do_start(self, action):
        return action, {
            'ids': [
                l.id for l in self.record.lines_to_pay
                if not l.reconciliation],
            'model': 'account.move.line',
            }
