Files
tradon/modules/account_invoice/invoice.py
2026-04-09 19:46:08 +02:00

4111 lines
156 KiB
Python
Executable File

# 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
from collections import defaultdict, namedtuple
from decimal import Decimal
from itertools import chain, combinations, groupby
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 trytond import backend
from trytond.config import config
from trytond.i18n import gettext
from trytond.model import (
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)
from trytond.transaction import Transaction
from trytond.wizard import (
Button, StateAction, StateReport, StateTransition, StateView, Wizard)
from trytond.exceptions import UserWarning, UserError
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
import logging
logger = logging.getLogger(__name__)
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):
'Invoice'
__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)
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_(['posted', 'paid', 'cancelled']),
'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.")
sequence = fields.Integer("Sequence", 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'))])
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')),
})
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',
})
rate = fields.Numeric("Rate",digits=(10,6))
warning = fields.Function(fields.Char("Info"),'get_info')
rate_date = fields.Date("Date",states={'invisible': (Eval('selection_rate')!='date')})
selection_rate = fields.Selection([
('manual', "Manual"),
('spot', "Spot"),
('invoice', "Invoice date"),
('forex', "Forex"),
('date', "At date"),
], "Rate origin")
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')
del _states
@classmethod
def __setup__(cls):
pool = Pool()
Party = pool.get('party.party')
cls.number.search_unaccented = False
cls.reference.search_unaccented = False
super(Invoice, cls).__setup__()
t = cls.__table__()
cls._sql_indexes.update({
Index(t, (t.reference, Index.Similarity())),
Index(
t,
(t.state, Index.Equality()),
where=t.state.in_(['draft', 'validated', 'posted'])),
Index(t, (t.total_amount_cache, Index.Range())),
Index(
t,
(t.total_amount_cache, Index.Equality()),
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()),
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()),
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') & Bool(Eval('move')))),
'depends': ['state', '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=False),
})
@classmethod
def __register__(cls, module_name):
sql_table = cls.__table__()
super(Invoice, cls).__register__(module_name)
transaction = Transaction()
cursor = transaction.connection.cursor()
table = cls.__table_handler__(module_name)
# Migration from 5.6: rename state cancel to cancelled
cursor.execute(*sql_table.update(
[sql_table.state], ['cancelled'],
where=sql_table.state == 'cancel'))
# Migration from 5.8: drop foreign key for sequence
table.drop_fk('sequence')
# 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]
@fields.depends('selection_rate','rate_date')
def on_change_with_rate(self, name=None):
return self.get_selected_rate()
def get_info(self, name):
return (
"⚠️ If rate field is blank then invoice date rate or spot rate will be applied!\n"
"💱 Rate must be entered as foreign currency / company currency"
)
def get_selected_rate(self,name=None):
Currency = Pool().get('currency.currency')
Date = Pool().get('ir.date')
company = self.company
currency = self.currency
if not currency or not company:
return None
if currency == company.currency:
return 1
if self.selection_rate == 'forex':
return self.get_forex()
if self.selection_rate == 'manual':
return None
target_date = Date.today()
if self.selection_rate == 'date':
if self.rate_date:
target_date = self.rate_date
elif self.selection_rate == 'spot':
target_date = Date.today()
elif self.selection_rate == 'invoice':
if self.invoice_date:
target_date = self.invoice_date
rates = Currency._get_rate([currency],target_date)
rate = rates.get(currency.id)
return rate or 1
def get_forex(self):
if self.lines:
if self.lines[0].lot:
rate,amt = self.lines[0].lot.get_forex_rate(abs(self.lines[0].amount))
if rate:
return round(1/rate,6) or 1
@staticmethod
def default_type():
return Transaction().context.get('type', 'out')
@staticmethod
def default_state():
return 'draft'
@classmethod
def default_selection_rate(cls):
return 'invoice'
@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('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()
@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)
computed_taxes = {}
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()
def is_zero(amount):
if self.currency:
return self.currency.is_zero(amount)
else:
return amount == Decimal(0)
tax_keys = []
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.append(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:
self.tax_amount += computed_taxes[key]['amount']
value = InvoiceTax.default_get(
list(InvoiceTax._fields.keys()), with_rec_name=False)
value.update(computed_taxes[key])
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__()
to_round = False
for sub_ids in grouped_slice(invoices_no_cache):
red_sql = reduce_ids(tax.invoice, sub_ids)
cursor.execute(*tax.select(tax.invoice,
Coalesce(Sum(tax.amount), 0).as_(type_name),
where=red_sql,
group_by=tax.invoice))
for invoice_id, sum_ in cursor:
# SQLite uses float for SUM
if not isinstance(sum_, Decimal):
sum_ = Decimal(str(sum_))
to_round = True
tax_amount[invoice_id] = sum_
# Float amount must be rounded to get the right precision
if to_round:
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.lines
if line.type == 'line'), 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 get_lines_to_pay(cls, invoices, name):
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__()
cursor = Transaction().connection.cursor()
lines = defaultdict(list)
for sub_ids in grouped_slice(invoices):
red_sql = reduce_ids(invoice.id, sub_ids)
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'),
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'),
where=red_sql))
cursor.execute(*query.select(
query.invoice, query.line,
order_by=query.maturity_date.nulls_last))
for invoice_id, line_id in cursor:
lines[invoice_id].append(line_id)
return lines
def get_reconciliation_lines(self, name):
if not self.move:
return
lines = set()
for move in chain([self.move], self.additional_moves):
for line in move.lines:
if line.account == self.account and line.reconciliation:
for line in line.reconciliation.lines:
if line not in self.lines_to_pay:
lines.add(line)
return [l.id for l in sorted(lines, key=lambda l: l.date)]
@classmethod
def get_amount_to_pay(cls, invoices, name=None):
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()
Rule = pool.get('ir.rule')
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
invoice_query = Rule.query_get('account.invoice')
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(Sum(Round((line.quantity * line.unit_price).cast(
type_name),
currency.digits)), 0).as_('total_amount'),
where=(line.invoice.in_(invoice_query)
& (invoice.total_amount_cache == Null)),
group_by=line.invoice)
| tax.select(tax.invoice.as_('invoice'),
Coalesce(Sum(tax.amount), 0).as_('total_amount'),
where=(tax.invoice.in_(invoice_query)
& Exists(invoice.select(
invoice.id,
where=(invoice.total_amount_cache == Null)
& (invoice.id == tax.invoice)))),
group_by=tax.invoice))
union |= invoice.select(
invoice.id.as_('invoice'),
invoice.total_amount_cache.as_('total_amount'),
where=(invoice.id.in_(invoice_query)
& (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()
Rule = pool.get('ir.rule')
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
invoice_query = Rule.query_get('account.invoice')
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=(line.invoice.in_(invoice_query)
& (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.id.in_(invoice_query)
& (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()
Rule = pool.get('ir.rule')
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
invoice_query = Rule.query_get('account.invoice')
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=(tax.invoice.in_(invoice_query)
& 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.id.in_(invoice_query)
& (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):
taxes = self._get_taxes()
for tax in taxes.values():
tax['invoice'] = self.id
return taxes
@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 = invoice._compute_taxes()
if not invoice.taxes:
to_create.extend(computed_taxes.values())
else:
tax_keys = []
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.append(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')
Date = Pool().get('ir.date')
AccountConfiguration = Pool().get('account.configuration')
account_configuration = AccountConfiguration(1)
line = MoveLine()
line.lot = self.lines[0].lot if self.lines else None
if self.currency != self.company.currency:
line.amount_second_currency = amount
line.second_currency = self.currency
if account_configuration.stock_fx_forex == 'forex':
frate,amt = line.lot.get_forex_rate(abs(amount))
logger.info("GET_PARTY_LINE:%s",frate)
if frate > 0:
amount = round(frate * amount,2)
logger.info("GET_PARTY_LINE2:%s",amount)
with Transaction().set_context(date=Date.today()):
amount += Currency.compute(self.currency,
amt if amount > 0 else -amt, self.company.currency)
logger.info("GET_PARTY_LINE3:%s",amount)
amount = round(amount,2)
else:
with Transaction().set_context(date=self.currency_date):
amount = Currency.compute(
self.currency, amount, self.company.currency)
else:
if self.rate:
amount = round(amount / self.rate,2)
else:
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
logger.info("_GET_MOVE_LINE_ACCOUNT:%s",line.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,party_line):
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 party_line:
line.revaluate = party_line.id
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.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
model = str(self.lines[0].origin).split(",")[0] if self.lines[0].origin else None
logger.info("MODEL:%s",model)
if model:
Line = Pool().get(model)
line = Line(int(str(self.lines[0].origin).split(",")[1]))
logger.info("LINE:%s",line)
term_lines = self.payment_term.compute(
self.total_amount, self.currency, payment_date, line)
else:
term_lines = [(self.payment_term_date or today, self.total_amount)]
past_payment_term_dates = []
party_line = None
for date, amount in term_lines:
line = self._get_move_line(date, amount)
move_lines.append(line)
remainder += line.debit - line.credit
party_line = line.id
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,party_line)
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
logger.info("GET_MOVE:%s",move.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')
Sequence = pool.get('ir.sequence.strict')
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
grouped_invoices = sorted(grouped_invoices, key=invoice_date)
for invoice in grouped_invoices:
# Posted and paid invoices are tested by check_modify so we can
# not modify tax_identifier nor number
if invoice.state in {'posted', 'paid'}:
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.number, invoice.sequence = invoice.get_next_number()
if invoice.type == 'out' and invoice.sequence not in sequences:
date = invoice_date(invoice)
# Do not need to lock the table
# because sequence.get_id is sequential
after_invoices = cls.search([
('sequence', '=', invoice.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(invoice.sequence).rec_name,
date=Lang.get().strftime(date),
after_invoice=after_invoice.rec_name))
sequences.add(invoice.sequence)
cls.save(invoices)
def get_next_number(self, pattern=None):
"Return invoice number and sequence id used"
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):
sequence = getattr(
invoice_sequence, self._sequence_field)
break
else:
raise InvoiceNumberError(
gettext('account_invoice.msg_invoice_no_sequence',
invoice=self.rec_name,
fiscalyear=fiscalyear.rec_name))
with Transaction().set_context(
date=accounting_date,
company=self.company.id):
return sequence.get(), sequence.id
@property
def _sequence_field(self):
"Returns the field name of invoice_sequence to use"
field = self.type
if (all(l.amount <= 0 for l in self.lines if l.product)
and self.total_amount < 0):
field += '_credit_note'
else:
field += '_invoice'
return field + '_sequence'
def get_tax_identifier(self):
"Return the default computed tax identifier"
return self.company.party.tax_identifier
@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)))
@classmethod
def check_modify(cls, invoices):
'''
Check if the invoices can be modified
'''
for invoice in invoices:
if not invoice.is_modifiable:
raise AccessError(
gettext('account_invoice.msg_invoice_modify',
invoice=invoice.rec_name))
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.lines))))
@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 delete(cls, invoices):
cls.check_modify(invoices)
# Cancel before delete
cls.cancel(invoices)
for invoice in invoices:
if invoice.state != 'cancelled':
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))
super(Invoice, cls).delete(invoices)
@classmethod
def create(cls, vlist):
invoices = super().create(vlist)
cls.update_taxes(invoices)
return invoices
@classmethod
def write(cls, *args):
actions = iter(args)
all_invoices = []
for invoices, values in zip(actions, actions):
if set(values) - cls._check_modify_exclude:
cls.check_modify(invoices)
all_invoices += invoices
update_tax = [i for i in all_invoices if i.state == 'draft']
super(Invoice, cls).write(*args)
if update_tax:
cls.update_taxes(update_tax)
@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('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)
return super(Invoice, cls).copy(invoices, default=default)
@classmethod
def validate(cls, invoices):
super(Invoice, cls).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))
def get_reconcile_lines_for_amount(self, amount, currency, party=None):
'''
Return list of lines and the remainder to make reconciliation.
'''
Result = namedtuple('Result', ['lines', 'remainder'])
if party is None:
party = self.party
assert currency in [self.currency, self.company.currency]
def balance(line):
if currency == line.second_currency:
return line.amount_second_currency
elif currency == self.company.currency:
return line.debit - line.credit
else:
return 0
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)]
remainder = sum(map(balance, lines)) - amount
best = Result(lines, remainder)
if remainder:
for n in range(len(lines) - 1, 0, -1):
for comb_lines in combinations(lines, n):
remainder = sum(map(balance, comb_lines)) - amount
result = Result(list(comb_lines), remainder)
if currency.is_zero(remainder):
return result
if abs(remainder) < abs(best.remainder):
best = result
return best
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')
lot = self.lines[0].lot
if party is None:
party = self.party
pay_line = Line(account=self.account)
pay_line.lot = lot
counterpart_line = Line()
counterpart_line.lot = lot
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 invoice.invoice_report_cache:
return
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):
for invoice in invoices:
if (invoice.untaxed_amount == invoice.untaxed_amount_cache
and invoice.tax_amount == invoice.tax_amount_cache
and invoice.total_amount == invoice.total_amount_cache):
continue
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:
move = invoice.get_move()
if move != invoice.move:
invoice.move = move
moves.append(move)
invoice.do_lot_invoicing()
if moves:
Move.save(moves)
cls.cleanMoves(moves)
cls.save(invoices)
def _get_move_lines(self,gl,amount,drop,IsUsd=False,stock_move=None):
Date = Pool().get('ir.date')
AccountConfiguration = Pool().get('account.configuration')
account_configuration = AccountConfiguration(1)
AccountMoveLine = Pool().get('account.move.line')
Currency = Pool().get('currency.currency')
move_lines = []
if amount != 0:
move_line = AccountMoveLine()
move_line_ = AccountMoveLine()
move_line.lot = gl.lot
move_line_.lot = gl.lot
move_line.fee = gl.fee
move_line_.fee = gl.fee
move_line.origin = stock_move
move_line_.origin = stock_move
move_line.second_currency = None
move_line_.second_currency = None
original_amount = round(amount,2)
if self.currency != self.company.currency:
if not IsUsd:
if account_configuration.stock_fx_forex == 'forex':
frate,amt = gl.lot.get_forex_rate(abs(amount))
logger.info("_GET_MOVE_LINES:%s",frate)
if frate > 0:
amount = round(frate * amount,2)
with Transaction().set_context(date=Date.today()):
amount += Currency.compute(self.currency,
amt if amount > 0 else -amt, self.company.currency)
amount = round(amount,2)
else:
with Transaction().set_context(date=self.invoice_date):
amount = round(Currency.compute(self.currency,
amount, self.company.currency),2)
else:
if self.rate:
amount = round(amount / self.rate,2)
else:
with Transaction().set_context(date=self.invoice_date):
amount = round(Currency.compute(self.currency,
amount, self.company.currency),2)
else:
if self.rate:
original_amount = round(amount * self.rate,2)
else:
with Transaction().set_context(date=self.invoice_date):
original_amount = round(Currency.compute(self.company.currency,
amount, self.currency),2)
move_line.second_currency = self.currency
move_line_.second_currency = self.currency
move_line.amount_second_currency = original_amount
move_line_.amount_second_currency = -original_amount
if amount < 0:
move_line.debit = Decimal(0)
move_line.credit = -amount
move_line.account = gl.product.account_stock_used if not (move_line.lot.sale_invoice_line or move_line.lot.sale_invoice_line_prov) else gl.product.account_stock_out_used
move_line.account = gl.product.account_cogs_used if gl.fee else move_line.account
move_line_.credit = Decimal(0)
move_line_.debit = -amount
move_line_.account = gl.product.account_stock_in_used
else:
move_line.debit = amount
move_line.credit = Decimal(0)
move_line.account = gl.product.account_stock_used if not (move_line.lot.sale_invoice_line or move_line.lot.sale_invoice_line_prov) else gl.product.account_stock_out_used
move_line.account = gl.product.account_cogs_used if gl.fee else move_line.account
move_line_.debit = Decimal(0)
move_line_.credit = amount
move_line_.account = gl.product.account_stock_in_used
move_lines = [move_line,move_line_]
if drop:
drop_line = AccountMoveLine()
drop_line_ = AccountMoveLine()
drop_line.lot = gl.lot
drop_line_.lot = gl.lot
drop_line.fee = gl.fee
drop_line_.fee = gl.fee
drop_line.origin = stock_move
drop_line_.origin = stock_move
if amount < 0:
drop_line_.credit = Decimal(0)
drop_line_.debit = -amount
drop_line_.account = gl.product.account_stock_in_used
if self.currency != self.company.currency:
drop_line_.second_currency = self.currency
drop_line_.amount_second_currency = -original_amount
drop_line.debit = Decimal(0)
drop_line.credit = -amount
drop_line.account = gl.product.account_stock_out_used
if self.currency != self.company.currency:
drop_line.second_currency = self.currency
drop_line.amount_second_currency = original_amount
else:
drop_line.debit = amount
drop_line.credit = Decimal(0)
drop_line.account = gl.product.account_stock_out_used
if self.currency != self.company.currency:
drop_line.second_currency = self.currency
drop_line.amount_second_currency = original_amount
drop_line_.debit = Decimal(0)
drop_line_.credit = amount
drop_line_.account = gl.product.account_stock_in_used
if self.currency != self.company.currency:
drop_line_.second_currency = self.currency
drop_line_.amount_second_currency = -original_amount
move_lines = [drop_line,drop_line_]
return move_lines
def do_lot_invoicing(self):
pool = Pool()
StockMove = pool.get('stock.move')
ShipmentIn = pool.get('stock.shipment.in')
Move = pool.get('account.move')
MoveLine = pool.get('account.move.line')
Date = pool.get('ir.date')
Period = pool.get('account.period')
Currency = Pool().get('currency.currency')
AccountConfiguration = pool.get('account.configuration')
account_configuration = AccountConfiguration(1)
logger.info("DO_LOT_INVOICING:%s",self.number)
for lot, grouped_lines in groupby(self.lines, key=lambda i: i.lot):
gl = list(grouped_lines)
var_price = sum([(i.unit_price if i.quantity > 0 else -i.unit_price) for i in gl])
var_qt = sum([i.quantity for i in gl])
logger.info("LOT_TO_PROCESS:%s",lot)
logger.info("FEE_TO_PROCESS:%s",gl[0].fee)
if (gl[0].fee and not gl[0].product.landed_cost):
diff = gl[0].fee.amount - gl[0].fee.get_non_cog(lot)
account_move = gl[0].fee._get_account_move_fee(lot,'in',diff)
Move.save([account_move])
if (lot and not gl[0].fee) or (gl[0].fee and gl[0].product.landed_cost):
adjust_move_lines = []
mov = None
if self.type == 'in':
mov = lot.get_last_supplier_move()
else:
mov = lot.get_current_customer_move()
if not mov:
return
if not mov.effective_date:
mov.effective_date = self.invoice_date
StockMove.save([mov])
if lot.lot_shipment_in:
ShipmentIn.start([lot.lot_shipment_in])
StockMove.do([mov])
cog = lot.get_cog()
if len(gl)==1 and gl[0].fee:
cog = gl[0].fee.get_cog(lot)
logger.info("INVOICING_COG:%s",cog)
drop = (mov.from_location.type == 'supplier') and (mov.to_location.type == 'customer')
logger.info("VALIDATE_DROP:%s",drop)
amount = Decimal(0)
if var_price != 0 and len(gl)==2:
amount = round(Decimal(sum([-i.quantity for i in gl if i.quantity < 0])) * var_price,2)
adjust_move_lines += self._get_move_lines(gl[0],amount,drop,False,mov)
logger.info("ADJUST_LINES_FOR_VAR_PRICE:%s",adjust_move_lines)
if var_qt !=0 and len(gl)==2:
amount = round(sum([i.unit_price for i in gl if i.quantity < 0]) * Decimal(var_qt),2)
adjust_move_lines += self._get_move_lines(gl[0],amount,drop,False,mov)
logger.info("ADJUST_LINES_FOR_VAR_QT:%s",adjust_move_lines)
logger.info("STOCK_CONFIG:%s",account_configuration.stock_fx_trigger)
if account_configuration.stock_fx_trigger != 'reception' and account_configuration.stock_fx_forex == 'spot':
exist_invoice = MoveLine.search([('account','=',account_configuration.default_account_payable),('lot','=',lot.id),('fee','=',gl[0].fee),('revaluate','=',None)])
logger.info("EXIST_INVOICE:%s",exist_invoice)
if (account_configuration.stock_fx_trigger == 'invoice' and not exist_invoice) or account_configuration.stock_fx_trigger == 'payment':
if len(gl)==1:
amount_converted = 0
if self.rate:
amount_converted = round(gl[0].amount / self.rate,2)
else:
with Transaction().set_context(date=self.invoice_date):
amount_converted = Currency.compute(self.currency,
gl[0].amount, self.company.currency)
delta = amount_converted - cog
if abs(delta) > 0.01:
adjust_move_lines += self._get_move_lines(gl[0],delta,drop,True,mov)
if len(gl)==2:
amount_converted = 0
if self.rate:
amount_converted = round(gl[1].amount / self.rate,2)
else:
with Transaction().set_context(date=self.invoice_date):
amount_converted = Currency.compute(self.currency,
gl[1].amount, self.company.currency)
delta = amount_converted - cog
if abs(delta) > 0.01:
adjust_move_lines += self._get_move_lines(gl[1],delta,drop,True,mov)
logger.info("FINAL_ADJUSTED_LINES:%s",adjust_move_lines)
if adjust_move_lines and self.type == 'in':
move_p = Move()
with Transaction().set_context(company=self.company.id):
today = Date.today()
period = Period.find(self.company, date=self.invoice_date)
journal = account_configuration.get_multivalue(
'stock_journal', company=self.company.id)
move_p.description = 'Adjustment'
move_p.journal = journal
move_p.period = period
move_p.date = self.invoice_date
move_p.origin = str(self)
move_p.lines = adjust_move_lines
Move.save([move_p])
Move.post([move_p])
@classmethod
def cleanMoves(cls,moves):
Move = Pool().get('account.move')
for move in moves:
lines = list(move.lines)
removed_ids = set()
for i, line1 in enumerate(lines):
if line1.id in removed_ids:
continue
for j in range(i + 1, len(lines)):
line2 = lines[j]
if line2.id in removed_ids:
continue
if (
line1.account == line2.account and
(
line1.credit == line2.debit and
line1.debit == line2.credit
)
):
removed_ids.add(line1.id)
removed_ids.add(line2.id)
break # stop après un match
move.lines = [line for line in move.lines if line.id not in removed_ids]
Move.save(moves)
@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)
#invoice.do_lot_invoicing()
if invoice.state != 'posted':
invoice.state = 'posted'
if moves:
Move.save(moves)
cls.cleanMoves(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.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):
paid = []
posted = []
for invoice in invoices:
if invoice.state not in ('posted', 'paid'):
continue
if invoice.reconciled:
paid.append(invoice)
else:
posted.append(invoice)
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):
"Invoice Additional Move"
__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):
"Invoice Alternative Payee"
__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):
'Invoice - Payment Line'
__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')),
['OR',
('party', '=', Eval('invoice_party', -1)),
('party', 'in', Eval('invoice_alternative_payees', [])),
],
])
@classmethod
def __setup__(cls):
super(InvoicePaymentLine, cls).__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):
'Invoice Line'
__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', size=None, states=_states)
summary = fields.Function(fields.Char('Summary'), 'on_change_with_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=(14, 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(InvoiceLine, cls).__setup__()
cls._check_modify_exclude = {'note', 'origin'}
# 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.stock', '=', True),
('type.debt', '=', True),
]
@classmethod
def __register__(cls, module_name):
super(InvoiceLine, cls).__register__(module_name)
table = cls.__table_handler__(module_name)
# Migration from 5.0: remove check constraints
table.drop_constraint('type_account')
table.drop_constraint('type_invoice')
@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) + 1:]
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 '')
@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__):
return self.origin.invoice.rec_name
return self.origin.rec_name if self.origin else None
@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
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 = list(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_modify(cls, lines, fields=None):
'''
Check if the lines can be modified
'''
if fields is None or fields - 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 delete(cls, lines):
cls.check_modify(lines)
super(InvoiceLine, cls).delete(lines)
@classmethod
def write(cls, *args):
actions = iter(args)
for lines, values in zip(actions, actions):
cls.check_modify(lines, set(values))
super(InvoiceLine, cls).write(*args)
@classmethod
def create(cls, vlist):
pool = Pool()
Invoice = pool.get('account.invoice')
invoice_ids = filter(None, {v.get('invoice') for v in vlist})
for invoice in Invoice.browse(list(invoice_ids)):
if invoice.state != 'draft':
raise AccessError(
gettext('account_invoice.msg_invoice_line_create_draft',
invoice=invoice.rec_name))
return super(InvoiceLine, cls).create(vlist)
@classmethod
def copy(cls, lines, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('origin', None)
return super(InvoiceLine, cls).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
'''
Currency = Pool().get('currency.currency')
Date = Pool().get('ir.date')
MoveLine = Pool().get('account.move.line')
AccountConfiguration = Pool().get('account.configuration')
account_configuration = AccountConfiguration(1)
if self.type != 'line':
return []
line = MoveLine()
line.lot = self.invoice.lines[0].lot
if self.invoice.currency != self.invoice.company.currency:
if account_configuration.stock_fx_forex == 'forex':
frate,amt = line.lot.get_forex_rate(abs(self.amount))
logger.info("GET_PARTY_LINE:%s",frate)
if frate > 0:
amount = round(frate * self.amount,2)
logger.info("GET_PARTY_LINE2:%s",amount)
with Transaction().set_context(date=Date.today()):
amount += Currency.compute(self.invoice.currency,
amt if self.amount > 0 else -amt, self.invoice.company.currency)
logger.info("GET_PARTY_LINE3:%s",amount)
amount = round(amount,2)
else:
with Transaction().set_context(date=self.invoice.currency_date):
amount = round(Currency.compute(self.invoice.currency,
self.amount, self.invoice.company.currency),2)
else:
if self.invoice.rate:
amount = round(self.amount / self.invoice.rate,2)
else:
with Transaction().set_context(date=self.invoice.currency_date):
amount = round(Currency.compute(self.invoice.currency,
self.amount, self.invoice.company.currency),2)
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
logger.info("GET_PARTY_LINE4:%s",amount)
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
logger.info("GET_MOVE_LINES_ACCOUNT:%s",line.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):
'Invoice Line - Tax'
__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):
'Invoice Tax'
__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=_states)
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=_states)
del _states
@classmethod
def __setup__(cls):
super().__setup__()
cls.__access__.add('invoice')
@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 _TaxKey
tax_id = self.tax.id if getattr(self, 'tax', None) else -1
account_id = (
self.account.id if getattr(self, 'account', None) else None)
return (account_id, tax_id, (getattr(self, 'base', 0) or 0) >= 0)
@classmethod
def check_modify(cls, taxes):
'''
Check if the taxes can be modified
'''
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
@classmethod
def delete(cls, taxes):
cls.check_modify(taxes)
super(InvoiceTax, cls).delete(taxes)
@classmethod
def write(cls, *args):
taxes = sum(args[0::2], [])
cls.check_modify(taxes)
super(InvoiceTax, cls).write(*args)
@classmethod
def create(cls, vlist):
pool = Pool()
Invoice = pool.get('account.invoice')
invoice_ids = filter(None, {v.get('invoice') for v in vlist})
for invoice in Invoice.browse(list(invoice_ids)):
if invoice.state != 'draft':
raise AccessError(
gettext('account_invoice.msg_invoice_tax_create',
invoice=invoice.rec_name))
return super(InvoiceTax, cls).create(vlist)
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):
'Payment Method'
__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')),
])
debit_account = fields.Many2One('account.account', "Debit Account",
required=True,
domain=[
('type', '!=', None),
('closed', '!=', True),
('company', '=', Eval('company')),
])
@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):
"Invoice Report Revision"
__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):
"Refresh Invoice Report"
__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(InvoiceReport, cls).__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)
report_path = cls._get_action_report_path(action)
use_cache = (
report_path in (None, 'account_invoice/invoice.fodt')
and invoice.invoice_report_cache
)
if use_cache:
return (
invoice.invoice_report_format,
invoice.invoice_report_cache)
else:
result = super()._execute(records, header, data, action)
if (invoice.invoice_report_versioned
and report_path in (None, 'account_invoice/invoice.fodt')):
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)
@staticmethod
def _get_action_report_path(action):
if isinstance(action, dict):
return action.get('report')
return getattr(action, 'report', None)
@classmethod
def execute(cls, ids, data):
pool = Pool()
Invoice = pool.get('account.invoice')
invoices = Invoice.browse(ids)
# for invoice in invoices:
# if invoice.description != 'Prepayment':
# raise UserError(
# 'Only prepayments could use this report!'
# )
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 PayInvoiceStart(ModelView):
'Pay Invoice'
__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')),
('debit_account', '!=', Eval('invoice_account')),
('credit_account', '!=', Eval('invoice_account')),
],
depends={'amount'})
date = fields.Date('Date', required=True)
@staticmethod
def default_date():
Date = Pool().get('ir.date')
return Date.today()
class PayInvoiceAsk(ModelView):
'Pay Invoice'
__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')),
],
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):
'Pay Invoice'
__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(PayInvoice, cls).__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,date=self.start.date)
return 'end'
class CreditInvoiceStart(ModelView):
'Credit Invoice'
__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):
'Credit Invoice'
__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):
"Reschedule Lines to Pay"
__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):
"Delegate Lines to Pay"
__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',
}