3922 lines
145 KiB
Python
3922 lines
145 KiB
Python
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
# this repository contains the full copyright notices and license terms.
|
|
import datetime as dt
|
|
import re
|
|
from collections import defaultdict
|
|
from decimal import Decimal
|
|
from itertools import chain, groupby
|
|
|
|
import stdnum.exceptions
|
|
from genshi.template.text import TextTemplate
|
|
from sql import Null
|
|
from sql.aggregate import Sum
|
|
from sql.conditionals import Coalesce
|
|
from sql.functions import CharLength, Round
|
|
from sql.operators import Exists
|
|
from stdnum import iso7064, iso11649
|
|
|
|
from trytond import backend, config
|
|
from trytond.i18n import gettext
|
|
from trytond.model import (
|
|
ChatMixin, DeactivableMixin, Index, ModelSQL, ModelView, Unique, Workflow,
|
|
dualmethod, fields, sequence_ordered)
|
|
from trytond.model.exceptions import AccessError
|
|
from trytond.modules.account.exceptions import AccountMissing
|
|
from trytond.modules.account.tax import TaxableMixin
|
|
from trytond.modules.company.model import (
|
|
employee_field, reset_employee, set_employee)
|
|
from trytond.modules.currency.fields import Monetary
|
|
from trytond.modules.product import price_digits
|
|
from trytond.pool import Pool
|
|
from trytond.pyson import Bool, Eval, Id, If
|
|
from trytond.report import Report
|
|
from trytond.rpc import RPC
|
|
from trytond.tools import (
|
|
cached_property, firstline, grouped_slice, reduce_ids, slugify,
|
|
sqlite_apply_types)
|
|
from trytond.transaction import Transaction
|
|
from trytond.wizard import (
|
|
Button, StateAction, StateReport, StateTransition, StateView, Wizard)
|
|
|
|
from .exceptions import (
|
|
InvoiceFutureWarning, InvoiceNumberError, InvoicePaymentTermDateWarning,
|
|
InvoiceSimilarWarning, InvoiceTaxesWarning, InvoiceTaxValidationError,
|
|
InvoiceValidationError, PayInvoiceError)
|
|
|
|
if config.getboolean('account_invoice', 'filestore', default=False):
|
|
file_id = 'invoice_report_cache_id'
|
|
store_prefix = config.get('account_invoice', 'store_prefix', default=None)
|
|
else:
|
|
file_id = None
|
|
store_prefix = None
|
|
|
|
|
|
class InvoiceReportMixin:
|
|
__slots__ = ()
|
|
|
|
invoice_report_cache = fields.Binary(
|
|
"Invoice Report", readonly=True,
|
|
file_id=file_id, store_prefix=store_prefix)
|
|
invoice_report_cache_id = fields.Char("Invoice Report ID", readonly=True)
|
|
invoice_report_format = fields.Char("Invoice Report Format", readonly=True)
|
|
|
|
|
|
class Invoice(
|
|
Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin,
|
|
ChatMixin):
|
|
__name__ = 'account.invoice'
|
|
_rec_name = 'number'
|
|
_order_name = 'number'
|
|
|
|
_states = {
|
|
'readonly': Eval('state') != 'draft',
|
|
}
|
|
|
|
company = fields.Many2One(
|
|
'company.company', "Company", required=True,
|
|
states={
|
|
'readonly': (
|
|
_states['readonly']
|
|
| Eval('party', True)
|
|
| Eval('lines', [0])),
|
|
},
|
|
context={
|
|
'party_contact_mechanism_usage': 'invoice',
|
|
})
|
|
company_party = fields.Function(
|
|
fields.Many2One(
|
|
'party.party', "Company Party",
|
|
context={
|
|
'company': Eval('company', -1),
|
|
'party_contact_mechanism_usage': 'invoice',
|
|
},
|
|
depends={'company'}),
|
|
'on_change_with_company_party')
|
|
tax_identifier = fields.Many2One(
|
|
'party.identifier', "Tax Identifier", ondelete='RESTRICT',
|
|
states=_states)
|
|
type = fields.Selection([
|
|
('out', "Customer"),
|
|
('in', "Supplier"),
|
|
], "Type", required=True,
|
|
states={
|
|
'readonly': ((Eval('state') != 'draft')
|
|
| Eval('context', {}).get('type')
|
|
| (Eval('lines', [0]) & Eval('type'))),
|
|
})
|
|
type_name = fields.Function(fields.Char('Type'), 'get_type_name')
|
|
number = fields.Char("Number", readonly=True)
|
|
number_alnum = fields.Char("Number Alphanumeric", readonly=True)
|
|
number_digit = fields.Integer("Number Digit", readonly=True)
|
|
number_digit._sql_type = 'BIGINT'
|
|
reference = fields.Char(
|
|
"Reference",
|
|
states={
|
|
'readonly': (
|
|
Eval('has_report_cache', False)
|
|
& ~Id('account', 'group_account_admin').in_(
|
|
Eval('context', {}).get('groups', []))),
|
|
})
|
|
description = fields.Char("Description", size=None,
|
|
states={
|
|
'readonly': (
|
|
(Eval('state') != 'draft')
|
|
& ~Id('account', 'group_account_admin').in_(
|
|
Eval('context', {}).get('groups', []))),
|
|
})
|
|
validated_by = employee_field(
|
|
"Validated By",
|
|
states=['validated', 'posted', 'paid', 'cancelled'])
|
|
posted_by = employee_field(
|
|
"Posted By",
|
|
states=['posted', 'paid', 'cancelled'])
|
|
state = fields.Selection([
|
|
('draft', "Draft"),
|
|
('validated', "Validated"),
|
|
('posted', "Posted"),
|
|
('paid', "Paid"),
|
|
('cancelled', "Cancelled"),
|
|
], "State", readonly=True, sort=False)
|
|
invoice_date = fields.Date('Invoice Date',
|
|
states={
|
|
'readonly': Eval('state').in_(
|
|
If(Eval('type') == 'in',
|
|
['validated', 'posted', 'paid'],
|
|
['posted', 'paid'])),
|
|
'required': Eval('state').in_(
|
|
If(Eval('type') == 'in',
|
|
['validated', 'posted', 'paid'],
|
|
['posted', 'paid'])),
|
|
})
|
|
accounting_date = fields.Date('Accounting Date', states=_states)
|
|
payment_term_date = fields.Date(
|
|
"Payment Term Date", states=_states,
|
|
help="The date from which the payment term is calculated.\n"
|
|
"Leave empty to use the invoice date.")
|
|
supplier_payment_reference_type = fields.Selection([
|
|
(None, ""),
|
|
('creditor_reference', "Creditor Reference"),
|
|
], "Payment Reference Type",
|
|
states={
|
|
'invisible': Eval('type', 'out') != 'in',
|
|
})
|
|
supplier_payment_reference_type_string = (
|
|
supplier_payment_reference_type.translated(
|
|
'supplier_payment_reference_type'))
|
|
supplier_payment_reference = fields.Char(
|
|
"Payment Reference",
|
|
states={
|
|
'invisible': Eval('type', 'out') != 'in',
|
|
})
|
|
customer_payment_reference = fields.Function(fields.Char(
|
|
"Customer Payment Reference",
|
|
states={
|
|
'invisible': Eval('type', 'out') != 'out',
|
|
}),
|
|
'get_customer_payment_reference',
|
|
searcher='search_customer_payment_reference')
|
|
sequence = fields.Integer("Sequence", readonly=True)
|
|
sequence_type_cache = fields.Selection([
|
|
(None, ""),
|
|
('invoice', "Invoice"),
|
|
('credit_note', "Credit Note"),
|
|
], "Sequence Type Cache", readonly=True)
|
|
party = fields.Many2One(
|
|
'party.party', 'Party', required=True, states=_states,
|
|
context={
|
|
'company': Eval('company', -1),
|
|
'party_contact_mechanism_usage': 'invoice',
|
|
},
|
|
depends={'company'})
|
|
party_tax_identifier = fields.Many2One(
|
|
'party.identifier', "Party Tax Identifier", ondelete='RESTRICT',
|
|
states=_states)
|
|
party_lang = fields.Function(fields.Char('Party Language'),
|
|
'on_change_with_party_lang')
|
|
invoice_address = fields.Many2One('party.address', 'Invoice Address',
|
|
required=True, states=_states,
|
|
domain=[('party', '=', Eval('party', -1))])
|
|
currency = fields.Many2One('currency.currency', 'Currency', required=True,
|
|
states={
|
|
'readonly': (
|
|
_states['readonly']
|
|
| (Eval('lines', [0]) & Eval('currency'))),
|
|
})
|
|
currency_date = fields.Function(fields.Date('Currency Date'),
|
|
'on_change_with_currency_date')
|
|
journal = fields.Many2One(
|
|
'account.journal', 'Journal',
|
|
states={
|
|
'readonly': _states['readonly'],
|
|
'required': Eval('state') != 'draft',
|
|
},
|
|
context={
|
|
'company': Eval('company', -1),
|
|
}, depends={'company'})
|
|
move = fields.Many2One('account.move', 'Move', readonly=True,
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
])
|
|
additional_moves = fields.Many2Many(
|
|
'account.invoice-additional-account.move', 'invoice', 'move',
|
|
"Additional Moves", readonly=True,
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
],
|
|
states={
|
|
'invisible': ~Eval('additional_moves'),
|
|
})
|
|
cancel_move = fields.Many2One('account.move', 'Cancel Move', readonly=True,
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
],
|
|
states={
|
|
'invisible': ~Eval('cancel_move'),
|
|
})
|
|
account = fields.Many2One('account.account', 'Account', required=True,
|
|
states=_states,
|
|
domain=[
|
|
('closed', '!=', True),
|
|
('company', '=', Eval('company', -1)),
|
|
If(Eval('type') == 'out',
|
|
('type.receivable', '=', True),
|
|
('type.payable', '=', True)),
|
|
],
|
|
context={
|
|
'date': If(Eval('accounting_date'),
|
|
Eval('accounting_date'),
|
|
Eval('invoice_date')),
|
|
})
|
|
payment_term = fields.Many2One(
|
|
'account.invoice.payment_term', "Payment Term",
|
|
ondelete='RESTRICT', states=_states)
|
|
alternative_payees = fields.Many2Many(
|
|
'account.invoice.alternative_payee', 'invoice', 'party',
|
|
"Alternative Payee", states=_states,
|
|
size=If(~Eval('move'), 1, None),
|
|
context={
|
|
'company': Eval('company', -1),
|
|
},
|
|
depends=['company'])
|
|
lines = fields.One2Many('account.invoice.line', 'invoice', 'Lines',
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
('currency', '=', Eval('currency', -1)),
|
|
['OR',
|
|
('account', '=', None),
|
|
('account', '!=', Eval('account', -1)),
|
|
],
|
|
['OR',
|
|
('invoice_type', '=', Eval('type')),
|
|
('invoice_type', '=', None),
|
|
],
|
|
['OR',
|
|
('party', '=', Eval('party', -1)),
|
|
('party', '=', None),
|
|
],
|
|
],
|
|
states={
|
|
'readonly': (
|
|
(Eval('state') != 'draft')
|
|
| ~Eval('company')
|
|
| ~Eval('currency')
|
|
| ~Eval('account')),
|
|
})
|
|
line_lines = fields.One2Many(
|
|
'account.invoice.line', 'invoice', "Line - Lines", readonly=True,
|
|
filter=[
|
|
('type', '=', 'line'),
|
|
])
|
|
taxes = fields.One2Many(
|
|
'account.invoice.tax', 'invoice', 'Tax Lines',
|
|
domain=[
|
|
('account', '!=', Eval('account', -1)),
|
|
],
|
|
states={
|
|
'readonly': (
|
|
(Eval('state') != 'draft')
|
|
| ~Eval('account')),
|
|
})
|
|
comment = fields.Text("Comment",
|
|
states={
|
|
'readonly': (
|
|
(Eval('state') != 'draft')
|
|
& ~Id('account', 'group_account_admin').in_(
|
|
Eval('context', {}).get('groups', []))),
|
|
})
|
|
origins = fields.Function(fields.Char('Origins'), 'get_origins')
|
|
untaxed_amount = fields.Function(Monetary(
|
|
"Untaxed", currency='currency', digits='currency'),
|
|
'get_amount', searcher='search_untaxed_amount')
|
|
untaxed_amount_cache = fields.Numeric(
|
|
"Untaxed Cache", digits='currency', readonly=True)
|
|
tax_amount = fields.Function(Monetary(
|
|
"Tax", currency='currency', digits='currency'),
|
|
'get_amount', searcher='search_tax_amount')
|
|
tax_amount_cache = fields.Numeric(
|
|
"Tax Cache", digits='currency', readonly=True)
|
|
total_amount = fields.Function(Monetary(
|
|
"Total", currency='currency', digits='currency'),
|
|
'get_amount', searcher='search_total_amount')
|
|
total_amount_cache = fields.Numeric(
|
|
"Total Cache", digits='currency', readonly=True)
|
|
reconciled = fields.Function(fields.Date('Reconciled',
|
|
states={
|
|
'invisible': ~Eval('reconciled'),
|
|
}),
|
|
'get_reconciled')
|
|
lines_to_pay = fields.Function(fields.Many2Many(
|
|
'account.move.line', None, None, 'Lines to Pay'),
|
|
'get_lines_to_pay')
|
|
payment_lines = fields.Many2Many('account.invoice-account.move.line',
|
|
'invoice', 'line', string='Payment Lines',
|
|
domain=[
|
|
('account', '=', Eval('account', -1)),
|
|
['OR',
|
|
('currency', '=', Eval('currency', -1)),
|
|
('second_currency', '=', Eval('currency', -1)),
|
|
],
|
|
['OR',
|
|
('party', 'in', [None, Eval('party', -1)]),
|
|
('party', 'in', Eval('alternative_payees', [])),
|
|
],
|
|
['OR',
|
|
('invoice_payment', '=', None),
|
|
('invoice_payment', '=', Eval('id', -1)),
|
|
],
|
|
If(Eval('type') == 'out',
|
|
If(Eval('total_amount', 0) >= 0,
|
|
('debit', '=', 0),
|
|
('credit', '=', 0)),
|
|
If(Eval('total_amount', 0) >= 0,
|
|
('credit', '=', 0),
|
|
('debit', '=', 0))),
|
|
],
|
|
states={
|
|
'invisible': Eval('state') == 'paid',
|
|
'readonly': Eval('state') != 'posted',
|
|
})
|
|
reconciliation_lines = fields.Function(fields.Many2Many(
|
|
'account.move.line', None, None, "Payment Lines",
|
|
states={
|
|
'invisible': (
|
|
~Eval('state').in_(['paid', 'cancelled'])
|
|
| ~Eval('reconciliation_lines')),
|
|
}),
|
|
'get_reconciliation_lines')
|
|
amount_to_pay_today = fields.Function(Monetary(
|
|
"Amount to Pay Today", currency='currency', digits='currency'),
|
|
'get_amount_to_pay')
|
|
amount_to_pay = fields.Function(Monetary(
|
|
"Amount to Pay", currency='currency', digits='currency'),
|
|
'get_amount_to_pay')
|
|
invoice_report_revisions = fields.One2Many(
|
|
'account.invoice.report.revision', 'invoice',
|
|
"Invoice Report Revisions", readonly=True,
|
|
states={
|
|
'invisible': ~Eval('invoice_report_revisions'),
|
|
})
|
|
allow_cancel = fields.Function(
|
|
fields.Boolean("Allow Cancel Invoice"), 'get_allow_cancel')
|
|
has_payment_method = fields.Function(
|
|
fields.Boolean("Has Payment Method"), 'get_has_payment_method')
|
|
has_report_cache = fields.Function(
|
|
fields.Boolean("Has Report Cached"), 'get_has_report_cache')
|
|
has_account_move = fields.Function(
|
|
fields.Boolean("Has Account Move"), 'on_change_with_has_account_move')
|
|
|
|
del _states
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
pool = Pool()
|
|
Party = pool.get('party.party')
|
|
cls.number.search_unaccented = False
|
|
cls.reference.search_unaccented = False
|
|
super().__setup__()
|
|
t = cls.__table__()
|
|
cls._sql_indexes.update({
|
|
Index(t, (t.reference, Index.Similarity())),
|
|
Index(
|
|
t,
|
|
(t.state, Index.Equality(cardinality='low')),
|
|
where=t.state.in_(['draft', 'validated', 'posted'])),
|
|
Index(t, (t.total_amount_cache, Index.Range())),
|
|
Index(
|
|
t,
|
|
(t.total_amount_cache, Index.Equality(cardinality='low')),
|
|
include=[t.id],
|
|
where=t.total_amount_cache == Null),
|
|
Index(t, (t.untaxed_amount_cache, Index.Range())),
|
|
Index(
|
|
t,
|
|
(t.untaxed_amount_cache,
|
|
Index.Equality(cardinality='low')),
|
|
include=[t.id],
|
|
where=t.untaxed_amount_cache == Null),
|
|
Index(t, (t.tax_amount_cache, Index.Range())),
|
|
Index(
|
|
t,
|
|
(t.tax_amount_cache, Index.Equality(cardinality='low')),
|
|
include=[t.id],
|
|
where=t.tax_amount_cache == Null),
|
|
})
|
|
cls._check_modify_exclude = {
|
|
'state', 'alternative_payees', 'payment_lines',
|
|
'move', 'cancel_move', 'additional_moves', 'description',
|
|
'invoice_report_cache', 'invoice_report_format', 'comment',
|
|
'total_amount_cache', 'tax_amount_cache', 'untaxed_amount_cache',
|
|
'lines', 'reference', 'invoice_report_cache_id',
|
|
'invoice_report_revisions'}
|
|
cls._order = [
|
|
('number', 'DESC NULLS FIRST'),
|
|
('id', 'DESC'),
|
|
]
|
|
cls.journal.domain = [
|
|
If(Eval('type') == 'out',
|
|
('type', 'in', cls._journal_types('out')),
|
|
('type', 'in', cls._journal_types('in'))),
|
|
]
|
|
tax_identifier_types = Party.tax_identifier_types()
|
|
cls.tax_identifier.domain = [
|
|
('party', '=', Eval('company_party', -1)),
|
|
('type', 'in', tax_identifier_types),
|
|
]
|
|
cls.party_tax_identifier.domain = [
|
|
('party', '=', Eval('party', -1)),
|
|
('type', 'in', tax_identifier_types),
|
|
]
|
|
cls._transitions |= set((
|
|
('draft', 'validated'),
|
|
('validated', 'posted'),
|
|
('draft', 'posted'),
|
|
('posted', 'posted'),
|
|
('posted', 'paid'),
|
|
('validated', 'draft'),
|
|
('paid', 'posted'),
|
|
('draft', 'cancelled'),
|
|
('validated', 'cancelled'),
|
|
('posted', 'cancelled'),
|
|
('cancelled', 'draft'),
|
|
))
|
|
cls._buttons.update({
|
|
'cancel': {
|
|
'invisible': ~Eval('allow_cancel', False),
|
|
'depends': ['allow_cancel'],
|
|
},
|
|
'draft': {
|
|
'invisible': (
|
|
~Eval('state').in_(['cancelled', 'validated'])
|
|
| ((Eval('state') == 'cancelled')
|
|
& Eval('cancel_move', -1))),
|
|
'icon': If(Eval('state') == 'cancelled', 'tryton-undo',
|
|
'tryton-back'),
|
|
'depends': ['state'],
|
|
},
|
|
'validate_invoice': {
|
|
'pre_validate':
|
|
['OR',
|
|
('invoice_date', '!=', None),
|
|
('type', '!=', 'in'),
|
|
],
|
|
'invisible': Eval('state') != 'draft',
|
|
'depends': ['state'],
|
|
},
|
|
'post': {
|
|
'pre_validate':
|
|
['OR',
|
|
('invoice_date', '!=', None),
|
|
('type', '!=', 'in'),
|
|
],
|
|
'invisible': (~Eval('state').in_(['draft', 'validated'])
|
|
| ((Eval('state') == 'posted')
|
|
& ~Eval('has_account_move', False))),
|
|
'depends': ['state', 'has_account_move'],
|
|
},
|
|
'pay': {
|
|
'invisible': (
|
|
(Eval('state') != 'posted')
|
|
| ~Eval('has_payment_method', False)),
|
|
'depends': ['state', 'has_payment_method'],
|
|
},
|
|
'reschedule_lines_to_pay': {
|
|
'invisible': (
|
|
~Eval('lines_to_pay') | Eval('reconciled', False)),
|
|
'depends': ['lines_to_pay', 'reconciled'],
|
|
},
|
|
'delegate_lines_to_pay': {
|
|
'invisible': (
|
|
~Eval('lines_to_pay') | Eval('reconciled', False)),
|
|
'depends': ['lines_to_pay', 'reconciled'],
|
|
},
|
|
'process': {
|
|
'invisible': ~Eval('state').in_(
|
|
['posted', 'paid']),
|
|
'depends': ['state'],
|
|
},
|
|
})
|
|
cls.__rpc__.update({
|
|
'post': RPC(
|
|
readonly=False, instantiate=0, fresh_session=True),
|
|
})
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
super().__register__(module_name)
|
|
table = cls.__table_handler__(module_name)
|
|
|
|
# Migration from 6.6: drop not null on journal
|
|
table.not_null_action('journal', 'remove')
|
|
|
|
@classmethod
|
|
def order_number(cls, tables):
|
|
table, _ = tables[None]
|
|
return [
|
|
~((table.state == 'cancelled') & (table.number == Null)),
|
|
CharLength(table.number), table.number]
|
|
|
|
@staticmethod
|
|
def default_type():
|
|
return Transaction().context.get('type', 'out')
|
|
|
|
@staticmethod
|
|
def default_state():
|
|
return 'draft'
|
|
|
|
@staticmethod
|
|
def default_currency():
|
|
Company = Pool().get('company.company')
|
|
if Transaction().context.get('company'):
|
|
company = Company(Transaction().context['company'])
|
|
return company.currency.id
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
@fields.depends(
|
|
'company', 'tax_identifier', methods=['on_change_with_company_party'])
|
|
def on_change_company(self):
|
|
company_party = self.on_change_with_company_party()
|
|
if self.company:
|
|
if self.tax_identifier:
|
|
if self.tax_identifier.party != company_party:
|
|
self.tax_identifier = None
|
|
else:
|
|
self.tax_identifier = None
|
|
|
|
@fields.depends('company')
|
|
def on_change_with_company_party(self, name=None):
|
|
return self.company.party if self.company else None
|
|
|
|
@fields.depends(methods=['set_journal', 'on_change_party'])
|
|
def on_change_type(self):
|
|
self.set_journal()
|
|
self.on_change_party()
|
|
|
|
@classmethod
|
|
def _journal_types(cls, invoice_type):
|
|
if invoice_type == 'out':
|
|
return ['revenue']
|
|
else:
|
|
return ['expense']
|
|
|
|
@fields.depends('type')
|
|
def set_journal(self, pattern=None):
|
|
pool = Pool()
|
|
Journal = pool.get('account.journal')
|
|
pattern = pattern.copy() if pattern is not None else {}
|
|
pattern.setdefault('type', {
|
|
'out': 'revenue',
|
|
'in': 'expense',
|
|
}.get(self.type))
|
|
self.journal = Journal.find(pattern)
|
|
|
|
@classmethod
|
|
def order_accounting_date(cls, tables):
|
|
table, _ = tables[None]
|
|
return [Coalesce(table.accounting_date, table.invoice_date)]
|
|
|
|
@fields.depends('party', 'type', methods=['_update_account'])
|
|
def on_change_party(self):
|
|
if self.party:
|
|
self.invoice_address = self.party.address_get(type='invoice')
|
|
self.party_tax_identifier = self.party.tax_identifier
|
|
if self.type == 'out':
|
|
self.account = self.party.account_receivable_used
|
|
self.payment_term = self.party.customer_payment_term
|
|
elif self.type == 'in':
|
|
self.account = self.party.account_payable_used
|
|
self.payment_term = self.party.supplier_payment_term
|
|
else:
|
|
self.invoice_address = None
|
|
self.account = None
|
|
self.payment_term = None
|
|
self.party_tax_identifier = None
|
|
self._update_account()
|
|
|
|
@fields.depends(methods=['_update_account'])
|
|
def on_change_accounting_date(self):
|
|
self._update_account()
|
|
|
|
@fields.depends(methods=['_update_account'])
|
|
def on_change_invoice_date(self):
|
|
self._update_account()
|
|
|
|
@fields.depends('account', 'accounting_date', 'invoice_date')
|
|
def _update_account(self):
|
|
"Update account to current account"
|
|
if self.account:
|
|
account = self.account.current(
|
|
date=self.accounting_date or self.invoice_date)
|
|
if account != self.account:
|
|
self.account = account
|
|
|
|
@fields.depends('invoice_date', 'company')
|
|
def on_change_with_currency_date(self, name=None):
|
|
Date = Pool().get('ir.date')
|
|
if self.company:
|
|
company_id = self.company.id
|
|
else:
|
|
company_id = Transaction().context.get('company')
|
|
with Transaction().set_context(company=company_id):
|
|
return self.invoice_date or Date.today()
|
|
|
|
@fields.depends(
|
|
'supplier_payment_reference_type',
|
|
'supplier_payment_reference')
|
|
def on_change_with_supplier_payment_reference(self):
|
|
reference = self.supplier_payment_reference
|
|
if reference and (type := self.supplier_payment_reference_type):
|
|
reference = getattr(
|
|
self, f'_format_supplier_payment_reference_{type}')(reference)
|
|
return reference
|
|
|
|
@classmethod
|
|
def _format_supplier_payment_reference_creditor_reference(cls, reference):
|
|
try:
|
|
return iso11649.format(reference)
|
|
except stdnum.exceptions.ValidationError:
|
|
return reference
|
|
|
|
def get_customer_payment_reference(self, name):
|
|
return self.format_customer_payment_reference(
|
|
self._customer_payment_reference_type())
|
|
|
|
@classmethod
|
|
def search_customer_payment_reference(cls, name, clause):
|
|
_, operator, value = clause[:3]
|
|
if operator == '=':
|
|
return ['OR', *cls._search_customer_payment_reference(value)]
|
|
else:
|
|
return []
|
|
|
|
@classmethod
|
|
def _search_customer_payment_reference(cls, value):
|
|
if iso11649.is_valid(value):
|
|
yield ('number_alnum', '=', value[4:])
|
|
|
|
def _customer_payment_reference_type(self):
|
|
return 'creditor_reference'
|
|
|
|
def _customer_payment_reference_number_alnum(self, source):
|
|
if source == 'invoice':
|
|
return self.number_alnum
|
|
elif source == 'party':
|
|
return self.party.code_alnum
|
|
|
|
def _customer_payment_reference_number_digit(self, source):
|
|
if source == 'invoice':
|
|
return self.number_digit
|
|
elif source == 'party':
|
|
return self.party.code_digit
|
|
|
|
def format_customer_payment_reference(self, type):
|
|
pool = Pool()
|
|
Confifugaration = pool.get('account.configuration')
|
|
configuration = Confifugaration(1)
|
|
source = configuration.get_multivalue(
|
|
'customer_payment_reference_number',
|
|
company=self.company.id)
|
|
return getattr(
|
|
self, f'_format_customer_payment_reference_{type}')(source)
|
|
|
|
def _format_customer_payment_reference_creditor_reference(self, source):
|
|
number = self._customer_payment_reference_number_alnum(source)
|
|
if number:
|
|
number = number[-25:].upper()
|
|
check_digits = iso7064.mod_97_10.calc_check_digits(f'{number}RF')
|
|
return iso11649.format(f'RF{check_digits}{number}')
|
|
|
|
@fields.depends('party')
|
|
def on_change_with_party_lang(self, name=None):
|
|
Config = Pool().get('ir.configuration')
|
|
if self.party and self.party.lang:
|
|
return self.party.lang.code
|
|
return Config.get_language()
|
|
|
|
@fields.depends('move')
|
|
def on_change_with_has_account_move(self, name=None):
|
|
return bool(self.move)
|
|
|
|
@classmethod
|
|
def get_type_name(cls, invoices, name):
|
|
type_names = {}
|
|
type2name = {}
|
|
for type, name in cls.fields_get(fields_names=['type']
|
|
)['type']['selection']:
|
|
type2name[type] = name
|
|
for invoice in invoices:
|
|
type_names[invoice.id] = type2name[invoice.type]
|
|
return type_names
|
|
|
|
@fields.depends(methods=['_on_change_lines_taxes'])
|
|
def on_change_lines(self):
|
|
self._on_change_lines_taxes()
|
|
|
|
@fields.depends(methods=['_on_change_lines_taxes'])
|
|
def on_change_taxes(self):
|
|
self._on_change_lines_taxes()
|
|
|
|
@fields.depends(
|
|
'lines', 'taxes', 'currency', methods=['_get_taxes', 'tax_date'])
|
|
def _on_change_lines_taxes(self):
|
|
pool = Pool()
|
|
InvoiceTax = pool.get('account.invoice.tax')
|
|
|
|
self.untaxed_amount = Decimal(0)
|
|
self.tax_amount = Decimal(0)
|
|
self.total_amount = Decimal(0)
|
|
|
|
if self.lines:
|
|
for line in self.lines:
|
|
if getattr(line, 'type', '') == 'line':
|
|
self.untaxed_amount += getattr(line, 'amount', 0) or 0
|
|
computed_taxes = self._get_taxes()
|
|
else:
|
|
computed_taxes = {}
|
|
|
|
def is_zero(amount):
|
|
if self.currency:
|
|
return self.currency.is_zero(amount)
|
|
else:
|
|
return amount == Decimal(0)
|
|
|
|
tax_keys = set()
|
|
taxes = list(self.taxes or [])
|
|
for tax in (self.taxes or []):
|
|
if tax.manual:
|
|
self.tax_amount += tax.amount or Decimal(0)
|
|
continue
|
|
key = tax._key
|
|
if (key not in computed_taxes) or (key in tax_keys):
|
|
taxes.remove(tax)
|
|
continue
|
|
tax_keys.add(key)
|
|
if not is_zero(
|
|
computed_taxes[key].base - (tax.base or Decimal(0))):
|
|
self.tax_amount += computed_taxes[key].amount
|
|
tax.amount = computed_taxes[key].amount
|
|
tax.base = computed_taxes[key].base
|
|
else:
|
|
self.tax_amount += tax.amount or Decimal(0)
|
|
for key in computed_taxes:
|
|
if key not in tax_keys:
|
|
tax = computed_taxes[key].tax
|
|
self.tax_amount += computed_taxes[key].amount
|
|
value = InvoiceTax.default_get(
|
|
list(InvoiceTax._fields.keys()), with_rec_name=False)
|
|
value.update(computed_taxes[key].values())
|
|
value['manual'] = False
|
|
value['description'] = tax.description
|
|
value['legal_notice'] = tax.legal_notice
|
|
value['currency'] = self.currency
|
|
invoice_tax = InvoiceTax(**value)
|
|
if invoice_tax.tax:
|
|
invoice_tax.sequence = invoice_tax.tax.sequence
|
|
taxes.append(invoice_tax)
|
|
self.taxes = taxes
|
|
if self.currency:
|
|
self.untaxed_amount = self.currency.round(self.untaxed_amount)
|
|
self.tax_amount = self.currency.round(self.tax_amount)
|
|
self.total_amount = self.untaxed_amount + self.tax_amount
|
|
if self.currency:
|
|
self.total_amount = self.currency.round(self.total_amount)
|
|
|
|
@classmethod
|
|
def get_amount(cls, invoices, names):
|
|
pool = Pool()
|
|
InvoiceTax = pool.get('account.invoice.tax')
|
|
cursor = Transaction().connection.cursor()
|
|
|
|
untaxed_amount = {i.id: i.currency.round(Decimal(0)) for i in invoices}
|
|
tax_amount = untaxed_amount.copy()
|
|
total_amount = untaxed_amount.copy()
|
|
|
|
invoices_no_cache = []
|
|
for invoice in invoices:
|
|
if (invoice.total_amount_cache is not None
|
|
and invoice.untaxed_amount_cache is not None
|
|
and invoice.tax_amount_cache is not None):
|
|
total_amount[invoice.id] = invoice.total_amount_cache
|
|
untaxed_amount[invoice.id] = invoice.untaxed_amount_cache
|
|
tax_amount[invoice.id] = invoice.tax_amount_cache
|
|
else:
|
|
invoices_no_cache.append(invoice.id)
|
|
invoices_no_cache = cls.browse(invoices_no_cache)
|
|
|
|
type_name = cls.tax_amount._field.sql_type().base
|
|
tax = InvoiceTax.__table__()
|
|
for sub_ids in grouped_slice(invoices_no_cache):
|
|
red_sql = reduce_ids(tax.invoice, sub_ids)
|
|
query = (tax.select(tax.invoice,
|
|
Coalesce(Sum(tax.amount), 0).as_(type_name),
|
|
where=red_sql,
|
|
group_by=tax.invoice))
|
|
if backend.name == 'sqlite':
|
|
sqlite_apply_types(query, [None, 'NUMERIC'])
|
|
cursor.execute(*query)
|
|
tax_amount.update(cursor)
|
|
# Float amount must be rounded to get the right precision
|
|
if backend.name == 'sqlite':
|
|
for invoice in invoices:
|
|
tax_amount[invoice.id] = invoice.currency.round(
|
|
tax_amount[invoice.id])
|
|
|
|
for invoice in invoices_no_cache:
|
|
zero = invoice.currency.round(Decimal(0))
|
|
untaxed_amount[invoice.id] = sum(
|
|
(line.amount for line in invoice.line_lines), zero)
|
|
total_amount[invoice.id] = (
|
|
untaxed_amount[invoice.id] + tax_amount[invoice.id])
|
|
|
|
result = {
|
|
'untaxed_amount': untaxed_amount,
|
|
'tax_amount': tax_amount,
|
|
'total_amount': total_amount,
|
|
}
|
|
for key in list(result.keys()):
|
|
if key not in names:
|
|
del result[key]
|
|
return result
|
|
|
|
def get_reconciled(self, name):
|
|
def get_reconciliation(line):
|
|
if line.reconciliation and line.reconciliation.delegate_to:
|
|
return get_reconciliation(line.reconciliation.delegate_to)
|
|
else:
|
|
return line.reconciliation
|
|
reconciliations = list(map(get_reconciliation, self.lines_to_pay))
|
|
if not reconciliations:
|
|
return None
|
|
elif not all(reconciliations):
|
|
return None
|
|
else:
|
|
return max(r.date for r in reconciliations)
|
|
|
|
@classmethod
|
|
def _query_lines_to_pay(cls, invoices):
|
|
pool = Pool()
|
|
MoveLine = pool.get('account.move.line')
|
|
AdditionalMove = pool.get('account.invoice-additional-account.move')
|
|
line = MoveLine.__table__()
|
|
invoice = cls.__table__()
|
|
additional_move = AdditionalMove.__table__()
|
|
|
|
red_sql = reduce_ids(invoice.id, invoices)
|
|
query = (invoice
|
|
.join(line,
|
|
condition=((invoice.move == line.move)
|
|
& (invoice.account == line.account)))
|
|
.select(
|
|
invoice.id.as_('invoice'),
|
|
line.id.as_('line'),
|
|
line.maturity_date.as_('maturity_date'),
|
|
line.reconciliation.as_('reconciliation'),
|
|
where=red_sql))
|
|
query |= (invoice
|
|
.join(additional_move,
|
|
condition=additional_move.invoice == invoice.id)
|
|
.join(line,
|
|
condition=((additional_move.move == line.move)
|
|
& (invoice.account == line.account)))
|
|
.select(
|
|
invoice.id.as_('invoice'),
|
|
line.id.as_('line'),
|
|
line.maturity_date.as_('maturity_date'),
|
|
line.reconciliation.as_('reconciliation'),
|
|
where=red_sql))
|
|
return query
|
|
|
|
@classmethod
|
|
def get_lines_to_pay(cls, invoices, name):
|
|
cursor = Transaction().connection.cursor()
|
|
lines = defaultdict(list)
|
|
for sub_invoices in grouped_slice(invoices):
|
|
query = cls._query_lines_to_pay(sub_invoices)
|
|
query = query.select(
|
|
query.invoice, query.line,
|
|
order_by=query.maturity_date.nulls_last)
|
|
cursor.execute(*query)
|
|
for invoice_id, line_id in cursor:
|
|
lines[invoice_id].append(line_id)
|
|
return lines
|
|
|
|
@classmethod
|
|
def get_reconciliation_lines(cls, invoices, name):
|
|
pool = Pool()
|
|
Line = pool.get('account.move.line')
|
|
Move = pool.get('account.move')
|
|
line = Line.__table__()
|
|
move = Move.__table__()
|
|
cursor = Transaction().connection.cursor()
|
|
lines = defaultdict(list)
|
|
for sub_invoices in grouped_slice(invoices):
|
|
sub_invoices = list(sub_invoices)
|
|
lines_to_pay = cls._query_lines_to_pay(sub_invoices)
|
|
query = cls._query_lines_to_pay(sub_invoices)
|
|
query = (
|
|
query
|
|
.join(line,
|
|
condition=query.reconciliation == line.reconciliation)
|
|
.join(move, condition=line.move == move.id)
|
|
.select(
|
|
query.invoice, line.id,
|
|
where=~Exists(
|
|
lines_to_pay.select(
|
|
lines_to_pay.line,
|
|
where=(lines_to_pay.invoice == query.invoice)
|
|
& (lines_to_pay.line == line.id))),
|
|
group_by=[query.invoice, line.id, move.date],
|
|
order_by=move.date.asc))
|
|
cursor.execute(*query)
|
|
for invoice_id, line_id in cursor:
|
|
lines[invoice_id].append(line_id)
|
|
return lines
|
|
|
|
@classmethod
|
|
def get_amount_to_pay(cls, invoices, name):
|
|
pool = Pool()
|
|
Currency = pool.get('currency.currency')
|
|
Date = pool.get('ir.date')
|
|
|
|
amounts = defaultdict(Decimal)
|
|
for company, grouped_invoices in groupby(
|
|
invoices, key=lambda i: i.company):
|
|
with Transaction().set_context(company=company.id):
|
|
today = Date.today()
|
|
for invoice in grouped_invoices:
|
|
if invoice.state != 'posted':
|
|
continue
|
|
amount = Decimal(0)
|
|
amount_currency = Decimal(0)
|
|
for line in invoice.lines_to_pay:
|
|
if line.reconciliation:
|
|
continue
|
|
if (name == 'amount_to_pay_today'
|
|
and (not line.maturity_date
|
|
or line.maturity_date > today)):
|
|
continue
|
|
if (line.second_currency
|
|
and line.second_currency == invoice.currency):
|
|
amount_currency += line.amount_second_currency
|
|
else:
|
|
amount += line.debit - line.credit
|
|
for line in invoice.payment_lines:
|
|
if line.reconciliation:
|
|
continue
|
|
if (line.second_currency
|
|
and line.second_currency == invoice.currency):
|
|
amount_currency += line.amount_second_currency
|
|
else:
|
|
amount += line.debit - line.credit
|
|
if amount:
|
|
with Transaction().set_context(date=invoice.currency_date):
|
|
amount_currency += Currency.compute(
|
|
invoice.company.currency, amount, invoice.currency)
|
|
if invoice.type == 'in' and amount_currency:
|
|
amount_currency *= -1
|
|
amounts[invoice.id] = amount_currency
|
|
return amounts
|
|
|
|
@classmethod
|
|
def search_total_amount(cls, name, clause):
|
|
pool = Pool()
|
|
Line = pool.get('account.invoice.line')
|
|
Tax = pool.get('account.invoice.tax')
|
|
Invoice = pool.get('account.invoice')
|
|
Currency = pool.get('currency.currency')
|
|
type_name = cls.total_amount._field.sql_type().base
|
|
line = Line.__table__()
|
|
invoice = Invoice.__table__()
|
|
currency = Currency.__table__()
|
|
tax = Tax.__table__()
|
|
|
|
_, operator, value = clause
|
|
Operator = fields.SQL_OPERATORS[operator]
|
|
# SQLite uses float for sum
|
|
if value is not None and backend.name == 'sqlite':
|
|
value = float(value)
|
|
|
|
union = (
|
|
line
|
|
.join(invoice, condition=(invoice.id == line.invoice))
|
|
.join(currency, condition=(currency.id == invoice.currency))
|
|
.select(
|
|
line.invoice.as_('invoice'),
|
|
Coalesce(Round(
|
|
(line.quantity * line.unit_price).cast(type_name),
|
|
currency.digits), 0).as_('total_amount'),
|
|
where=(invoice.total_amount_cache == Null))
|
|
| tax.select(
|
|
tax.invoice.as_('invoice'),
|
|
tax.amount.as_('total_amount'),
|
|
where=Exists(
|
|
invoice.select(
|
|
invoice.id,
|
|
where=(invoice.total_amount_cache == Null)
|
|
& (invoice.id == tax.invoice)))))
|
|
union |= invoice.select(
|
|
invoice.id.as_('invoice'),
|
|
invoice.total_amount_cache.as_('total_amount'),
|
|
where=(
|
|
(invoice.total_amount_cache != Null)
|
|
& Operator(invoice.total_amount_cache.cast(type_name), value)))
|
|
query = union.select(union.invoice, group_by=union.invoice,
|
|
having=Operator(Sum(union.total_amount).cast(type_name),
|
|
value))
|
|
return [('id', 'in', query)]
|
|
|
|
@classmethod
|
|
def search_untaxed_amount(cls, name, clause):
|
|
pool = Pool()
|
|
Line = pool.get('account.invoice.line')
|
|
Invoice = pool.get('account.invoice')
|
|
Currency = pool.get('currency.currency')
|
|
type_name = cls.untaxed_amount._field.sql_type().base
|
|
line = Line.__table__()
|
|
invoice = Invoice.__table__()
|
|
currency = Currency.__table__()
|
|
|
|
_, operator, value = clause
|
|
Operator = fields.SQL_OPERATORS[operator]
|
|
# SQLite uses float for sum
|
|
if value is not None and backend.name == 'sqlite':
|
|
value = float(value)
|
|
|
|
query = (
|
|
line
|
|
.join(invoice, condition=(invoice.id == line.invoice))
|
|
.join(currency, condition=(currency.id == invoice.currency))
|
|
.select(
|
|
line.invoice,
|
|
where=(invoice.untaxed_amount_cache == Null),
|
|
group_by=line.invoice,
|
|
having=Operator(
|
|
Coalesce(Sum(Round(
|
|
(line.quantity * line.unit_price).cast(
|
|
type_name),
|
|
currency.digits)), 0).cast(type_name),
|
|
value)))
|
|
query |= invoice.select(invoice.id,
|
|
where=(
|
|
(invoice.untaxed_amount_cache != Null)
|
|
& Operator(
|
|
invoice.untaxed_amount_cache.cast(type_name), value)))
|
|
return [('id', 'in', query)]
|
|
|
|
@classmethod
|
|
def search_tax_amount(cls, name, clause):
|
|
pool = Pool()
|
|
Tax = pool.get('account.invoice.tax')
|
|
Invoice = pool.get('account.invoice')
|
|
type_name = cls.tax_amount._field.sql_type().base
|
|
tax = Tax.__table__()
|
|
invoice = Invoice.__table__()
|
|
|
|
_, operator, value = clause
|
|
Operator = fields.SQL_OPERATORS[operator]
|
|
# SQLite uses float for sum
|
|
if value is not None and backend.name == 'sqlite':
|
|
value = float(value)
|
|
|
|
query = tax.select(tax.invoice,
|
|
where=Exists(
|
|
invoice.select(
|
|
invoice.id,
|
|
where=(invoice.tax_amount_cache == Null)
|
|
& (invoice.id == tax.invoice))),
|
|
group_by=tax.invoice,
|
|
having=Operator(
|
|
Coalesce(Sum(tax.amount), 0).cast(type_name), value))
|
|
query |= invoice.select(invoice.id,
|
|
where=(
|
|
(invoice.tax_amount_cache != Null)
|
|
& Operator(invoice.tax_amount_cache.cast(type_name), value)))
|
|
return [('id', 'in', query)]
|
|
|
|
def get_allow_cancel(self, name):
|
|
if self.state in {'draft', 'validated'}:
|
|
return True
|
|
if self.state == 'posted':
|
|
return self.type == 'in' or self.company.cancel_invoice_out
|
|
return False
|
|
|
|
@classmethod
|
|
def get_has_payment_method(cls, invoices, name):
|
|
pool = Pool()
|
|
Method = pool.get('account.invoice.payment.method')
|
|
methods = {}
|
|
for (company, account), sub_invoices in groupby(
|
|
invoices, key=lambda i: (i.company, i.account)):
|
|
sub_invoice_ids = [i.id for i in sub_invoices]
|
|
value = bool(Method.search([
|
|
('company', '=', company.id),
|
|
('debit_account', '!=', account.id),
|
|
('credit_account', '!=', account.id),
|
|
], limit=1))
|
|
methods.update(dict.fromkeys(sub_invoice_ids, value))
|
|
return methods
|
|
|
|
@classmethod
|
|
def get_has_report_cache(cls, invoices, name):
|
|
table = cls.__table__()
|
|
cursor = Transaction().connection.cursor()
|
|
|
|
result = {}
|
|
has_cache = (
|
|
(table.invoice_report_cache_id != Null)
|
|
| (table.invoice_report_cache != Null))
|
|
for sub_invoices in grouped_slice(invoices):
|
|
sub_ids = map(int, sub_invoices)
|
|
cursor.execute(*table.select(table.id, has_cache,
|
|
where=reduce_ids(table.id, sub_ids)))
|
|
result.update(cursor)
|
|
return result
|
|
|
|
@property
|
|
def taxable_lines(self):
|
|
taxable_lines = []
|
|
for line in self.lines:
|
|
if getattr(line, 'type', None) == 'line':
|
|
taxable_lines.extend(line.taxable_lines)
|
|
return taxable_lines
|
|
|
|
@property
|
|
@fields.depends('accounting_date', 'invoice_date', 'company')
|
|
def tax_date(self):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
context = Transaction().context
|
|
with Transaction().set_context(
|
|
company=self.company.id if self.company
|
|
else context.get('company')):
|
|
today = Date.today()
|
|
return self.accounting_date or self.invoice_date or today
|
|
|
|
@fields.depends('party', 'company')
|
|
def _get_tax_context(self):
|
|
context = {}
|
|
if self.party and self.party.lang:
|
|
context['language'] = self.party.lang.code
|
|
if self.company:
|
|
context['company'] = self.company.id
|
|
return context
|
|
|
|
def _compute_taxes(self):
|
|
for key, tax_line in self._get_taxes().items():
|
|
value = dict(tax_line.values())
|
|
value['invoice'] = self.id
|
|
value['manual'] = False
|
|
value['description'] = tax_line.tax.description
|
|
value['legal_notice'] = tax_line.tax.legal_notice
|
|
yield key, value
|
|
|
|
@dualmethod
|
|
def update_taxes(cls, invoices, exception=False):
|
|
Tax = Pool().get('account.invoice.tax')
|
|
to_create = []
|
|
to_delete = []
|
|
to_write = []
|
|
for invoice in invoices:
|
|
if invoice.state in ('posted', 'paid', 'cancelled'):
|
|
continue
|
|
computed_taxes = dict(invoice._compute_taxes())
|
|
if not invoice.taxes:
|
|
to_create.extend(computed_taxes.values())
|
|
else:
|
|
tax_keys = set()
|
|
for tax in invoice.taxes:
|
|
if tax.manual:
|
|
continue
|
|
key = tax._key
|
|
if (key not in computed_taxes) or (key in tax_keys):
|
|
to_delete.append(tax)
|
|
continue
|
|
tax_keys.add(key)
|
|
if not invoice.currency.is_zero(
|
|
computed_taxes[key]['base'] - tax.base):
|
|
to_write.extend(([tax], computed_taxes[key]))
|
|
for key in computed_taxes:
|
|
if key not in tax_keys:
|
|
to_create.append(computed_taxes[key])
|
|
if exception and (to_create or to_delete or to_write):
|
|
raise InvoiceTaxValidationError(
|
|
gettext('account_invoice.msg_invoice_tax_invalid',
|
|
invoice=invoice.rec_name))
|
|
if to_create:
|
|
Tax.create(to_create)
|
|
if to_delete:
|
|
Tax.delete(to_delete)
|
|
if to_write:
|
|
Tax.write(*to_write)
|
|
|
|
def _get_move_line(self, date, amount):
|
|
'''
|
|
Return move line
|
|
'''
|
|
pool = Pool()
|
|
Currency = pool.get('currency.currency')
|
|
MoveLine = pool.get('account.move.line')
|
|
line = MoveLine()
|
|
if self.currency != self.company.currency:
|
|
line.amount_second_currency = amount
|
|
line.second_currency = self.currency
|
|
with Transaction().set_context(date=self.currency_date):
|
|
amount = Currency.compute(
|
|
self.currency, amount, self.company.currency)
|
|
else:
|
|
line.amount_second_currency = None
|
|
line.second_currency = None
|
|
if amount >= 0:
|
|
if self.type == 'out':
|
|
line.debit, line.credit = amount, 0
|
|
else:
|
|
line.debit, line.credit = 0, amount
|
|
else:
|
|
if self.type == 'out':
|
|
line.debit, line.credit = 0, -amount
|
|
else:
|
|
line.debit, line.credit = -amount, 0
|
|
if line.amount_second_currency:
|
|
line.amount_second_currency = (
|
|
line.amount_second_currency.copy_sign(
|
|
line.debit - line.credit))
|
|
line.account = self.account
|
|
if self.account.party_required:
|
|
if self.alternative_payees:
|
|
line.party, = self.alternative_payees
|
|
else:
|
|
line.party = self.party
|
|
line.maturity_date = date
|
|
line.description = self.description
|
|
return line
|
|
|
|
def _get_exchange_move_line(self, amount):
|
|
pool = Pool()
|
|
Configuration = pool.get('account.configuration')
|
|
MoveLine = pool.get('account.move.line')
|
|
configuration = Configuration(1)
|
|
line = MoveLine()
|
|
line.debit = -amount if amount < 0 else 0
|
|
line.credit = amount if amount > 0 else 0
|
|
if line.credit:
|
|
line.account = configuration.get_multivalue(
|
|
'currency_exchange_credit_account', company=self.company.id)
|
|
if not line.account:
|
|
raise AccountMissing(gettext(
|
|
'account_invoice.'
|
|
'msg_invoice_currency_exchange_credit_account_missing',
|
|
invoice=self.rec_name,
|
|
company=self.company.rec_name))
|
|
else:
|
|
line.account = configuration.get_multivalue(
|
|
'currency_exchange_debit_account', company=self.company.id)
|
|
if not line.account:
|
|
raise AccountMissing(gettext(
|
|
'account_invoice.'
|
|
'msg_invoice_currency_exchange_debit_account_missing',
|
|
invoice=self.rec_name,
|
|
company=self.company.rec_name))
|
|
line.amount_second_currency = None
|
|
line.second_currency = None
|
|
return line
|
|
|
|
def get_move(self):
|
|
'''
|
|
Compute account move for the invoice and return the created move
|
|
'''
|
|
pool = Pool()
|
|
Move = pool.get('account.move')
|
|
Period = pool.get('account.period')
|
|
Date = pool.get('ir.date')
|
|
Warning = pool.get('res.user.warning')
|
|
Lang = pool.get('ir.lang')
|
|
|
|
if self.move:
|
|
return self.move
|
|
with Transaction().set_context(company=self.company.id):
|
|
today = Date.today()
|
|
self.update_taxes(exception=True)
|
|
move_lines = []
|
|
for line in self.line_lines:
|
|
move_lines += line.get_move_lines()
|
|
for tax in self.taxes:
|
|
move_lines += tax.get_move_lines()
|
|
|
|
remainder = sum(l.debit - l.credit for l in move_lines)
|
|
if self.payment_term:
|
|
payment_date = self.payment_term_date or self.invoice_date or today
|
|
term_lines = self.payment_term.compute(
|
|
self.total_amount, self.currency, payment_date)
|
|
else:
|
|
term_lines = [(self.payment_term_date or today, self.total_amount)]
|
|
past_payment_term_dates = []
|
|
for date, amount in term_lines:
|
|
line = self._get_move_line(date, amount)
|
|
move_lines.append(line)
|
|
remainder += line.debit - line.credit
|
|
if self.type == 'out' and date < today:
|
|
past_payment_term_dates.append(date)
|
|
if self.currency != self.company.currency and remainder:
|
|
line = self._get_exchange_move_line(remainder)
|
|
move_lines.append(line)
|
|
if any(past_payment_term_dates):
|
|
lang = Lang.get()
|
|
warning_key = Warning.format('invoice_payment_term', [self])
|
|
if Warning.check(warning_key):
|
|
raise InvoicePaymentTermDateWarning(warning_key,
|
|
gettext('account_invoice'
|
|
'.msg_invoice_payment_term_date_past',
|
|
invoice=self.rec_name,
|
|
date=lang.strftime(min(past_payment_term_dates))))
|
|
|
|
accounting_date = self.accounting_date or self.invoice_date or today
|
|
period = Period.find(self.company, date=accounting_date)
|
|
|
|
move = Move()
|
|
move.journal = self.journal
|
|
move.period = period
|
|
move.date = accounting_date
|
|
move.origin = self
|
|
move.company = self.company
|
|
move.lines = move_lines
|
|
return move
|
|
|
|
@classmethod
|
|
def set_number(cls, invoices):
|
|
'''
|
|
Set number to the invoice
|
|
'''
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
Lang = pool.get('ir.lang')
|
|
|
|
sequences = set()
|
|
|
|
for company, grouped_invoices in groupby(
|
|
invoices, key=lambda i: i.company):
|
|
with Transaction().set_context(company=company.id):
|
|
today = Date.today()
|
|
|
|
def invoice_date(invoice):
|
|
return invoice.invoice_date or today
|
|
|
|
to_number = defaultdict(list)
|
|
grouped_invoices = sorted(grouped_invoices, key=invoice_date)
|
|
|
|
for invoice in grouped_invoices:
|
|
# Posted, paid and cancelled invoices are tested by
|
|
# check_modify so we can not modify tax_identifier nor number
|
|
if invoice.state in {'posted', 'paid', 'cancelled'}:
|
|
continue
|
|
if not invoice.tax_identifier:
|
|
invoice.tax_identifier = invoice.get_tax_identifier()
|
|
# Generated invoice may not fill the party tax identifier
|
|
if not invoice.party_tax_identifier:
|
|
invoice.party_tax_identifier = invoice.party.tax_identifier
|
|
|
|
if invoice.number:
|
|
continue
|
|
|
|
if not invoice.invoice_date and invoice.type == 'out':
|
|
invoice.invoice_date = today
|
|
invoice.sequence_type_cache = invoice._sequence_type
|
|
sequence, sequence_date = invoice._number_sequence()
|
|
to_number[(sequence, sequence_date)].append(invoice)
|
|
if invoice.type == 'out' and sequence not in sequences:
|
|
date = invoice_date(invoice)
|
|
# Do not need to lock the table
|
|
# because sequence.get_many is sequential
|
|
after_invoices = cls.search([
|
|
('sequence', '=', sequence),
|
|
('invoice_date', '>', date),
|
|
],
|
|
limit=1, order=[('invoice_date', 'DESC')])
|
|
if after_invoices:
|
|
after_invoice, = after_invoices
|
|
raise InvoiceNumberError(
|
|
gettext('account_invoice.msg_invoice_number_after',
|
|
invoice=invoice.rec_name,
|
|
sequence=sequence.rec_name,
|
|
date=Lang.get().strftime(date),
|
|
after_invoice=after_invoice.rec_name))
|
|
sequences.add(sequence)
|
|
for (sequence, date), n_invoices in to_number.items():
|
|
with Transaction().set_context(
|
|
date=date, company=company.id):
|
|
for invoice, number in zip(
|
|
n_invoices, sequence.get_many(len(n_invoices))):
|
|
invoice.sequence = sequence
|
|
invoice.number = number
|
|
cls.save(invoices)
|
|
|
|
def _number_sequence(self, pattern=None):
|
|
"Returns the sequence and date to use for numbering"
|
|
pool = Pool()
|
|
Period = pool.get('account.period')
|
|
|
|
if pattern is None:
|
|
pattern = {}
|
|
else:
|
|
pattern = pattern.copy()
|
|
|
|
accounting_date = self.accounting_date or self.invoice_date
|
|
period = Period.find(
|
|
self.company, date=accounting_date,
|
|
test_state=self.type != 'in')
|
|
|
|
fiscalyear = period.fiscalyear
|
|
pattern.setdefault('company', self.company.id)
|
|
pattern.setdefault('fiscalyear', fiscalyear.id)
|
|
pattern.setdefault('period', period.id)
|
|
|
|
for invoice_sequence in fiscalyear.invoice_sequences:
|
|
if invoice_sequence.match(pattern):
|
|
return getattr(
|
|
invoice_sequence, self._sequence_field), accounting_date
|
|
else:
|
|
raise InvoiceNumberError(
|
|
gettext('account_invoice.msg_invoice_no_sequence',
|
|
invoice=self.rec_name,
|
|
fiscalyear=fiscalyear.rec_name))
|
|
|
|
@property
|
|
def _sequence_type(self):
|
|
if (all(l.amount <= 0 for l in self.line_lines)
|
|
and self.total_amount < 0):
|
|
return 'credit_note'
|
|
else:
|
|
return 'invoice'
|
|
|
|
@property
|
|
def sequence_type(self):
|
|
return self.sequence_type_cache or self._sequence_type
|
|
|
|
@property
|
|
def _sequence_field(self):
|
|
"Returns the field name of invoice_sequence to use"
|
|
return f'{self.type}_{self.sequence_type}_sequence'
|
|
|
|
def get_tax_identifier(self, pattern=None):
|
|
"Return the default computed tax identifier"
|
|
pattern = pattern.copy() if pattern is not None else {}
|
|
if self.invoice_address and self.invoice_address.country:
|
|
pattern.setdefault('country', self.invoice_address.country.id)
|
|
return self.company.get_tax_identifier(pattern)
|
|
|
|
@property
|
|
def invoice_report_versioned(self):
|
|
return self.state in {'posted', 'paid'} and self.type == 'out'
|
|
|
|
def create_invoice_report_revision(self):
|
|
pool = Pool()
|
|
InvoiceReportRevision = pool.get('account.invoice.report.revision')
|
|
if not self.invoice_report_versioned:
|
|
return
|
|
invoice_report_revision = InvoiceReportRevision(
|
|
invoice=self,
|
|
invoice_report_cache=self.invoice_report_cache,
|
|
invoice_report_cache_id=self.invoice_report_cache_id,
|
|
invoice_report_format=self.invoice_report_format)
|
|
self.invoice_report_revisions += (invoice_report_revision,)
|
|
self.invoice_report_cache = None
|
|
self.invoice_report_cache_id = None
|
|
self.invoice_report_format = None
|
|
return invoice_report_revision
|
|
|
|
@property
|
|
def is_modifiable(self):
|
|
return not (self.state in {'posted', 'paid'}
|
|
or (self.state == 'cancelled'
|
|
and (self.move or self.cancel_move or self.number)))
|
|
|
|
def get_rec_name(self, name):
|
|
items = []
|
|
if self.number:
|
|
items.append(self.number)
|
|
if self.reference:
|
|
items.append('[%s]' % self.reference)
|
|
if not items:
|
|
items.append('(%s)' % self.id)
|
|
return ' '.join(items)
|
|
|
|
@classmethod
|
|
def search_rec_name(cls, name, clause):
|
|
_, operator, value = clause
|
|
if operator.startswith('!') or operator.startswith('not '):
|
|
bool_op = 'AND'
|
|
else:
|
|
bool_op = 'OR'
|
|
return [bool_op,
|
|
('number', *clause[1:]),
|
|
('reference', *clause[1:]),
|
|
]
|
|
|
|
def get_origins(self, name):
|
|
return ', '.join(set(filter(None,
|
|
(l.origin_name for l in self.line_lines))))
|
|
|
|
def chat_language(self, audience='internal'):
|
|
language = super().chat_language(audience=audience)
|
|
if audience == 'public':
|
|
language = self.party.lang.code if self.party.lang else None
|
|
return language
|
|
|
|
@classmethod
|
|
def view_attributes(cls):
|
|
return super().view_attributes() + [
|
|
('/form//field[@name="comment"]', 'spell', Eval('party_lang')),
|
|
('/tree', 'visual',
|
|
If((
|
|
(Eval('type') == 'out')
|
|
& (Eval('amount_to_pay_today', 0) > 0))
|
|
| ((Eval('type') == 'in')
|
|
& (Eval('amount_to_pay_today', 0) < 0)),
|
|
'danger',
|
|
If(Eval('state') == 'cancelled', 'muted', ''))),
|
|
]
|
|
|
|
@classmethod
|
|
def check_modification(cls, mode, invoices, values=None, external=False):
|
|
super().check_modification(
|
|
mode, invoices, values=values, external=external)
|
|
if (mode == 'delete'
|
|
or (mode == 'write'
|
|
and values.keys() - cls._check_modify_exclude)):
|
|
for invoice in invoices:
|
|
if not invoice.is_modifiable:
|
|
raise AccessError(gettext(
|
|
'account_invoice.msg_invoice_modify',
|
|
invoice=invoice.rec_name))
|
|
if mode == 'delete':
|
|
for invoice in invoices:
|
|
if invoice.state not in {'cancelled', 'draft'}:
|
|
raise AccessError(gettext(
|
|
'account_invoice.msg_invoice_delete_cancel',
|
|
invoice=invoice.rec_name))
|
|
if invoice.number:
|
|
raise AccessError(gettext(
|
|
'account_invoice.msg_invoice_delete_numbered',
|
|
invoice=invoice.rec_name))
|
|
|
|
@classmethod
|
|
def on_modification(cls, mode, invoices, field_names=None):
|
|
super().on_modification(mode, invoices, field_names=field_names)
|
|
if mode != 'delete':
|
|
if draft_invoices := [i for i in invoices if i.state == 'draft']:
|
|
cls.update_taxes(draft_invoices)
|
|
|
|
def compute_fields(self, field_names=None):
|
|
values = super().compute_fields(field_names=field_names)
|
|
if (field_names is None or 'number' in field_names):
|
|
values['number_alnum'] = (
|
|
re.sub(r'[\W_]', '', self.number)
|
|
if self.number is not None else None)
|
|
try:
|
|
values['number_digit'] = int(
|
|
re.sub(r'\D', '', self.number or '')[-18:])
|
|
except ValueError:
|
|
values['number_digit'] = None
|
|
return values
|
|
|
|
@classmethod
|
|
def copy(cls, invoices, default=None):
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
|
|
alternative_payees2copy = set()
|
|
for invoice in invoices:
|
|
if len(invoice.alternative_payees) == 1:
|
|
parties = {l.party for l in invoice.lines_to_pay}
|
|
if parties <= set(invoice.alternative_payees):
|
|
alternative_payees2copy.add(invoice.id)
|
|
|
|
def copy_alternative_payees(data):
|
|
if data['id'] in alternative_payees2copy:
|
|
return data.get('alternative_payees', [])
|
|
else:
|
|
return []
|
|
|
|
default.setdefault('number', None)
|
|
default.setdefault('number_alnum', None)
|
|
default.setdefault('number_digit', None)
|
|
default.setdefault('reference')
|
|
default.setdefault('supplier_payment_reference')
|
|
default.setdefault('supplier_payment_reference_type')
|
|
default.setdefault('sequence')
|
|
default.setdefault('move', None)
|
|
default.setdefault('additional_moves', None)
|
|
default.setdefault('cancel_move', None)
|
|
default.setdefault('invoice_report_cache', None)
|
|
default.setdefault('invoice_report_cache_id', None)
|
|
default.setdefault('invoice_report_format', None)
|
|
default.setdefault('alternative_payees', copy_alternative_payees)
|
|
default.setdefault('payment_lines', None)
|
|
default.setdefault('invoice_date', None)
|
|
default.setdefault('accounting_date', None)
|
|
default.setdefault('payment_term_date', None)
|
|
default.setdefault('total_amount_cache', None)
|
|
default.setdefault('untaxed_amount_cache', None)
|
|
default.setdefault('tax_amount_cache', None)
|
|
default.setdefault('validated_by')
|
|
default.setdefault('posted_by')
|
|
default.setdefault('invoice_report_revisions', None)
|
|
return super().copy(invoices, default=default)
|
|
|
|
@classmethod
|
|
def validate(cls, invoices):
|
|
super().validate(invoices)
|
|
for invoice in invoices:
|
|
invoice.check_payment_lines()
|
|
|
|
def check_payment_lines(self):
|
|
def balance(line):
|
|
if self.currency == line.second_currency:
|
|
return line.amount_second_currency
|
|
elif self.currency == self.company.currency:
|
|
return line.debit - line.credit
|
|
else:
|
|
return 0
|
|
amount = sum(map(balance, self.lines_to_pay))
|
|
payment_amount = sum(map(balance, self.payment_lines))
|
|
if abs(amount) < abs(payment_amount):
|
|
raise InvoiceValidationError(
|
|
gettext('account_invoice'
|
|
'.msg_invoice_payment_lines_greater_amount',
|
|
invoice=self.rec_name))
|
|
|
|
@classmethod
|
|
def validate_fields(cls, invoices, field_names):
|
|
super().validate_fields(invoices, field_names)
|
|
cls.check_supplier_payment_reference(invoices, field_names)
|
|
|
|
@classmethod
|
|
def check_supplier_payment_reference(cls, invoices, field_names):
|
|
if field_names and not (
|
|
field_names & {
|
|
'supplier_payment_reference',
|
|
'supplier_payment_reference_type',
|
|
}):
|
|
return
|
|
|
|
for invoice in invoices:
|
|
if type := invoice.supplier_payment_reference_type:
|
|
method = getattr(
|
|
cls, f'_check_supplier_payment_reference_{type}')
|
|
if not method(invoice):
|
|
reference = invoice.supplier_payment_reference
|
|
type = invoice.supplier_payment_reference_type_string
|
|
raise InvoiceValidationError(
|
|
gettext(
|
|
'account_invoice.'
|
|
'msg_invoice_supplier_payment_reference_invalid',
|
|
type=type,
|
|
reference=reference,
|
|
invoice=invoice.rec_name))
|
|
|
|
def _check_supplier_payment_reference_creditor_reference(self):
|
|
return iso11649.is_valid(self.supplier_payment_reference)
|
|
|
|
def get_reconcile_lines_for_amount(self, amount, currency, party=None):
|
|
'''
|
|
Return list of lines and the remainder to make reconciliation.
|
|
'''
|
|
pool = Pool()
|
|
Line = pool.get('account.move.line')
|
|
|
|
assert currency in {self.currency, self.company.currency}
|
|
|
|
if party is None:
|
|
party = self.party
|
|
lines = [
|
|
l for l in self.payment_lines + self.lines_to_pay
|
|
if not l.reconciliation
|
|
and (not self.account.party_required or l.party == party)]
|
|
|
|
return Line.find_best_reconciliation(
|
|
lines, currency, amount=amount)
|
|
|
|
def pay_invoice(
|
|
self, amount, payment_method, date, description=None,
|
|
overpayment=0, party=None):
|
|
'''
|
|
Adds a payment of amount to an invoice using the journal, date and
|
|
description.
|
|
If overpayment is set, then only the amount minus the overpayment is
|
|
used to pay off the invoice.
|
|
Returns the payment lines.
|
|
'''
|
|
pool = Pool()
|
|
Currency = pool.get('currency.currency')
|
|
Move = pool.get('account.move')
|
|
Line = pool.get('account.move.line')
|
|
Period = pool.get('account.period')
|
|
|
|
if party is None:
|
|
party = self.party
|
|
|
|
pay_line = Line(account=self.account)
|
|
counterpart_line = Line()
|
|
lines = [pay_line, counterpart_line]
|
|
|
|
pay_amount = amount - overpayment
|
|
if self.currency != self.company.currency:
|
|
amount_second_currency = pay_amount
|
|
second_currency = self.currency
|
|
overpayment_second_currency = overpayment
|
|
with Transaction().set_context(date=date):
|
|
amount = Currency.compute(
|
|
self.currency, amount, self.company.currency)
|
|
overpayment = Currency.compute(
|
|
self.currency, overpayment, self.company.currency)
|
|
pay_amount = amount - overpayment
|
|
else:
|
|
amount_second_currency = None
|
|
second_currency = None
|
|
overpayment_second_currency = None
|
|
if pay_amount >= 0:
|
|
if self.type == 'out':
|
|
pay_line.debit, pay_line.credit = 0, pay_amount
|
|
else:
|
|
pay_line.debit, pay_line.credit = pay_amount, 0
|
|
else:
|
|
if self.type == 'out':
|
|
pay_line.debit, pay_line.credit = -pay_amount, 0
|
|
else:
|
|
pay_line.debit, pay_line.credit = 0, -pay_amount
|
|
if amount_second_currency is not None:
|
|
pay_line.amount_second_currency = (
|
|
amount_second_currency.copy_sign(
|
|
pay_line.debit - pay_line.credit))
|
|
pay_line.second_currency = second_currency
|
|
|
|
if overpayment:
|
|
overpayment_line = Line(account=self.account)
|
|
lines.insert(1, overpayment_line)
|
|
overpayment_line.debit = (
|
|
abs(overpayment) if pay_line.debit else 0)
|
|
overpayment_line.credit = (
|
|
abs(overpayment) if pay_line.credit else 0)
|
|
if overpayment_second_currency is not None:
|
|
overpayment_line.amount_second_currency = (
|
|
overpayment_second_currency.copy_sign(
|
|
overpayment_line.debit - overpayment_line.credit))
|
|
overpayment_line.second_currency = second_currency
|
|
|
|
counterpart_line.debit = abs(amount) if pay_line.credit else 0
|
|
counterpart_line.credit = abs(amount) if pay_line.debit else 0
|
|
if counterpart_line.debit:
|
|
payment_acccount = 'debit_account'
|
|
else:
|
|
payment_acccount = 'credit_account'
|
|
counterpart_line.account = getattr(
|
|
payment_method, payment_acccount).current(date=date)
|
|
if amount_second_currency is not None:
|
|
counterpart_line.amount_second_currency = (
|
|
amount_second_currency.copy_sign(
|
|
counterpart_line.debit - counterpart_line.credit))
|
|
counterpart_line.second_currency = second_currency
|
|
|
|
for line in lines:
|
|
if line.account.party_required:
|
|
line.party = party
|
|
|
|
period = Period.find(self.company, date=date)
|
|
|
|
move = Move(
|
|
journal=payment_method.journal, period=period, date=date,
|
|
origin=self, description=description,
|
|
company=self.company, lines=lines)
|
|
move.save()
|
|
Move.post([move])
|
|
|
|
payment_lines = [l for l in move.lines if l.account == self.account]
|
|
payment_line = [l for l in payment_lines
|
|
if (l.debit, l.credit) == (pay_line.debit, pay_line.credit)][0]
|
|
self.add_payment_lines({self: [payment_line]})
|
|
return payment_lines
|
|
|
|
@classmethod
|
|
def add_payment_lines(cls, payments):
|
|
"Add value lines to the key invoice from the payment dictionary."
|
|
to_write = []
|
|
for invoice, lines in payments.items():
|
|
if invoice.state == 'paid':
|
|
raise AccessError(
|
|
gettext('account_invoice'
|
|
'.msg_invoice_payment_lines_add_remove_paid',
|
|
invoice=invoice.rec_name))
|
|
to_write.append([invoice])
|
|
to_write.append({'payment_lines': [('add', lines)]})
|
|
if to_write:
|
|
cls.write(*to_write)
|
|
|
|
@classmethod
|
|
def remove_payment_lines(cls, lines):
|
|
"Remove payment lines from their invoices."
|
|
pool = Pool()
|
|
PaymentLine = pool.get('account.invoice-account.move.line')
|
|
|
|
payments = defaultdict(list)
|
|
ids = list(map(int, lines))
|
|
for sub_ids in grouped_slice(ids):
|
|
payment_lines = PaymentLine.search([
|
|
('line', 'in', list(sub_ids)),
|
|
])
|
|
for payment_line in payment_lines:
|
|
payments[payment_line.invoice].append(payment_line.line)
|
|
|
|
to_write = []
|
|
for invoice, lines in payments.items():
|
|
if invoice.state == 'paid':
|
|
raise AccessError(
|
|
gettext('account_invoice'
|
|
'.msg_invoice_payment_lines_add_remove_paid',
|
|
invoice=invoice.rec_name))
|
|
to_write.append([invoice])
|
|
to_write.append({'payment_lines': [('remove', lines)]})
|
|
if to_write:
|
|
cls.write(*to_write)
|
|
|
|
@dualmethod
|
|
def print_invoice(cls, invoices):
|
|
'''
|
|
Generate invoice report and store it in invoice_report field.
|
|
'''
|
|
InvoiceReport = Pool().get('account.invoice', type='report')
|
|
for invoice in invoices:
|
|
if not invoice.invoice_report_cache:
|
|
InvoiceReport.execute([invoice.id], {})
|
|
|
|
def _credit(self, **values):
|
|
'''
|
|
Return values to credit invoice.
|
|
'''
|
|
credit = self.__class__(**values)
|
|
|
|
for field in [
|
|
'company', 'tax_identifier', 'party', 'party_tax_identifier',
|
|
'invoice_address', 'currency', 'journal', 'account',
|
|
'payment_term', 'description', 'comment', 'type']:
|
|
setattr(credit, field, getattr(self, field))
|
|
|
|
credit.lines = [line._credit() for line in self.lines]
|
|
credit.taxes = [tax._credit() for tax in self.taxes if tax.manual]
|
|
return credit
|
|
|
|
@classmethod
|
|
def credit(cls, invoices, refund=False, **values):
|
|
'''
|
|
Credit invoices and return ids of new invoices.
|
|
Return the list of new invoice
|
|
'''
|
|
new_invoices = [i._credit(**values) for i in invoices]
|
|
cls.save(new_invoices)
|
|
if refund:
|
|
cls.post(new_invoices)
|
|
for invoice, new_invoice in zip(invoices, new_invoices):
|
|
if invoice.state != 'posted':
|
|
raise AccessError(
|
|
gettext('account_invoice'
|
|
'.msg_invoice_credit_refund_not_posted',
|
|
invoice=invoice.rec_name))
|
|
invoice.cancel_move = new_invoice.move
|
|
cls.save(invoices)
|
|
cls.cancel(invoices)
|
|
return new_invoices
|
|
|
|
@classmethod
|
|
def _store_cache(cls, invoices):
|
|
invoices = list(invoices)
|
|
cls.write(invoices, {
|
|
'untaxed_amount_cache': None,
|
|
'tax_amount_cache': None,
|
|
'total_amount_cache': None,
|
|
})
|
|
for invoice in invoices:
|
|
invoice.untaxed_amount_cache = invoice.untaxed_amount
|
|
invoice.tax_amount_cache = invoice.tax_amount
|
|
invoice.total_amount_cache = invoice.total_amount
|
|
cls.save(invoices)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('draft')
|
|
@reset_employee('validated_by', 'posted_by')
|
|
def draft(cls, invoices):
|
|
Move = Pool().get('account.move')
|
|
|
|
cls.write(invoices, {
|
|
'tax_amount_cache': None,
|
|
'untaxed_amount_cache': None,
|
|
'total_amount_cache': None,
|
|
})
|
|
moves = []
|
|
for invoice in invoices:
|
|
if invoice.move:
|
|
moves.append(invoice.move)
|
|
if invoice.additional_moves:
|
|
moves.extend(invoice.additional_moves)
|
|
if len(invoice.alternative_payees) > 1:
|
|
invoice.alternative_payees = []
|
|
cls.save(invoices)
|
|
if moves:
|
|
Move.delete(moves)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('validated')
|
|
@set_employee('validated_by')
|
|
def validate_invoice(cls, invoices):
|
|
pool = Pool()
|
|
Move = pool.get('account.move')
|
|
|
|
cls._check_taxes(invoices)
|
|
cls._check_similar(invoices)
|
|
|
|
invoices_in = cls.browse([i for i in invoices if i.type == 'in'])
|
|
cls.set_number(invoices_in)
|
|
cls._store_cache(invoices)
|
|
moves = []
|
|
for invoice in invoices_in:
|
|
move = invoice.get_move()
|
|
if move != invoice.move:
|
|
invoice.move = move
|
|
moves.append(move)
|
|
if moves:
|
|
Move.save(moves)
|
|
cls.save(invoices_in)
|
|
|
|
@classmethod
|
|
@Workflow.transition('posted')
|
|
def post_batch(cls, invoices):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
transaction = Transaction()
|
|
context = transaction.context
|
|
cls.set_number(invoices)
|
|
for company, grouped_invoices in groupby(
|
|
invoices, key=lambda i: i.company):
|
|
with Transaction().set_context(company=company.id):
|
|
today = Date.today()
|
|
for invoice in grouped_invoices:
|
|
if not invoice.payment_term_date:
|
|
invoice.payment_term_date = today
|
|
cls.save(invoices)
|
|
with transaction.set_context(
|
|
_skip_warnings=True,
|
|
queue_batch=context.get('queue_batch', True)):
|
|
cls.__queue__._post(invoices)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('posted')
|
|
@set_employee('posted_by', when='before')
|
|
def post(cls, invoices):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
Warning = pool.get('res.user.warning')
|
|
for company, grouped_invoices in groupby(
|
|
invoices, key=lambda i: i.company):
|
|
with Transaction().set_context(company=company.id):
|
|
today = Date.today()
|
|
future_invoices = [
|
|
i for i in grouped_invoices
|
|
if i.type == 'out'
|
|
and i.invoice_date and i.invoice_date > today]
|
|
if future_invoices:
|
|
names = ', '.join(m.rec_name for m in future_invoices[:5])
|
|
if len(future_invoices) > 5:
|
|
names += '...'
|
|
warning_key = Warning.format(
|
|
'invoice_date_future', future_invoices)
|
|
if Warning.check(warning_key):
|
|
raise InvoiceFutureWarning(warning_key,
|
|
gettext('account_invoice.msg_invoice_date_future',
|
|
invoices=names))
|
|
to_check = [i for i in invoices if i.state != 'validated']
|
|
cls._check_taxes(to_check)
|
|
cls._check_similar(to_check)
|
|
cls._post(invoices)
|
|
|
|
@classmethod
|
|
def _post(cls, invoices):
|
|
pool = Pool()
|
|
Move = pool.get('account.move')
|
|
transaction = Transaction()
|
|
context = transaction.context
|
|
|
|
cls.set_number(invoices)
|
|
cls._store_cache(invoices)
|
|
moves = []
|
|
for invoice in invoices:
|
|
move = invoice.get_move()
|
|
if move != invoice.move:
|
|
invoice.move = move
|
|
moves.append(move)
|
|
if invoice.state != 'posted':
|
|
invoice.state = 'posted'
|
|
if moves:
|
|
Move.save(moves)
|
|
cls.save(invoices)
|
|
Move.post([i.move for i in invoices if i.move.state != 'posted'])
|
|
reconciled = []
|
|
to_print = []
|
|
for invoice in invoices:
|
|
if invoice.type == 'out':
|
|
to_print.append(invoice)
|
|
if invoice.reconciled:
|
|
reconciled.append(invoice)
|
|
if to_print:
|
|
cls.__queue__.print_invoice(to_print)
|
|
if reconciled:
|
|
with transaction.set_context(
|
|
queue_batch=context.get('queue_batch', True)):
|
|
cls.__queue__.process(reconciled)
|
|
|
|
@classmethod
|
|
def _check_taxes(cls, invoices):
|
|
pool = Pool()
|
|
Line = pool.get('account.invoice.line')
|
|
Warning = pool.get('res.user.warning')
|
|
for invoice in invoices:
|
|
different_lines = []
|
|
for line in invoice.line_lines:
|
|
test_line = Line(line.id)
|
|
test_line.on_change_product()
|
|
if (set(test_line.taxes) != set(line.taxes)
|
|
or test_line.taxes_deductible_rate
|
|
!= line.taxes_deductible_rate):
|
|
different_lines.append(line)
|
|
if different_lines:
|
|
warning_key = Warning.format(
|
|
'invoice_taxes', [invoice])
|
|
if Warning.check(warning_key):
|
|
lines = ', '.join(l.rec_name for l in different_lines[:5])
|
|
if len(different_lines) > 5:
|
|
lines += '...'
|
|
raise InvoiceTaxesWarning(warning_key,
|
|
gettext('account_invoice.msg_invoice_default_taxes',
|
|
invoice=invoice.rec_name,
|
|
lines=lines))
|
|
|
|
@classmethod
|
|
def _check_similar(cls, invoices, type='in'):
|
|
pool = Pool()
|
|
Warning = pool.get('res.user.warning')
|
|
for sub_invoices in grouped_slice(invoices):
|
|
sub_invoices = list(sub_invoices)
|
|
domain = list(filter(None,
|
|
(i._similar_domain() for i in sub_invoices
|
|
if i.type == type)))
|
|
if not domain:
|
|
continue
|
|
if cls.search(['OR'] + domain, order=[]):
|
|
for invoice in sub_invoices:
|
|
domain = invoice._similar_domain()
|
|
if not domain:
|
|
continue
|
|
try:
|
|
similar, = cls.search(domain, limit=1)
|
|
except ValueError:
|
|
continue
|
|
warning_key = Warning.format(
|
|
'invoice_similar', [invoice])
|
|
if Warning.check(warning_key):
|
|
raise InvoiceSimilarWarning(warning_key,
|
|
gettext('account_invoice.msg_invoice_similar',
|
|
similar=similar.rec_name,
|
|
invoice=invoice.rec_name))
|
|
|
|
def _similar_domain(self, delay=None):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
if not self.reference:
|
|
return
|
|
with Transaction().set_context(company=self.company.id):
|
|
invoice_date = self.invoice_date or Date.today()
|
|
if delay is None:
|
|
delay = dt.timedelta(days=60)
|
|
return [
|
|
('company', '=', self.company.id),
|
|
('type', '=', self.type),
|
|
('party', '=', self.party.id),
|
|
('reference', '=', self.reference),
|
|
('id', '!=', self.id),
|
|
['OR',
|
|
('invoice_date', '=', None),
|
|
[
|
|
('invoice_date', '>=', invoice_date - delay),
|
|
('invoice_date', '<=', invoice_date + delay),
|
|
],
|
|
],
|
|
]
|
|
|
|
@classmethod
|
|
@ModelView.button_action('account_invoice.wizard_pay')
|
|
def pay(cls, invoices):
|
|
pass
|
|
|
|
@classmethod
|
|
@ModelView.button_action(
|
|
'account_invoice.act_reschedule_lines_to_pay_wizard')
|
|
def reschedule_lines_to_pay(cls, invoices):
|
|
pass
|
|
|
|
@classmethod
|
|
@ModelView.button_action(
|
|
'account_invoice.act_delegate_lines_to_pay_wizard')
|
|
def delegate_lines_to_pay(cls, invoices):
|
|
pass
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def process(cls, invoices):
|
|
to_save = []
|
|
paid = []
|
|
posted = []
|
|
for invoice in invoices:
|
|
if invoice.state in {'posted', 'paid'}:
|
|
if invoice.reconciled:
|
|
paid.append(invoice)
|
|
else:
|
|
posted.append(invoice)
|
|
elif invoice.state == 'cancelled' and invoice.move:
|
|
if not invoice.reconciled:
|
|
if invoice.cancel_move:
|
|
invoice.cancel_move = None
|
|
invoice.save()
|
|
to_save.append(invoice)
|
|
posted.append(invoice)
|
|
cls.save(to_save)
|
|
cls.paid(paid)
|
|
cls._post(posted)
|
|
|
|
@classmethod
|
|
@Workflow.transition('paid')
|
|
def paid(cls, invoices):
|
|
# Remove links to lines which actually do not pay the invoice
|
|
cls._clean_payments(invoices)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('cancelled')
|
|
def cancel(cls, invoices):
|
|
pool = Pool()
|
|
Move = pool.get('account.move')
|
|
Line = pool.get('account.move.line')
|
|
|
|
cancel_moves = []
|
|
delete_moves = []
|
|
to_save = []
|
|
for invoice in invoices:
|
|
if invoice.move or invoice.number:
|
|
if invoice.move and invoice.move.state == 'draft':
|
|
delete_moves.append(invoice.move)
|
|
delete_moves.extend(invoice.additional_moves)
|
|
elif not invoice.cancel_move:
|
|
if (invoice.type == 'out'
|
|
and not invoice.company.cancel_invoice_out):
|
|
raise AccessError(
|
|
gettext('account_invoice'
|
|
'.msg_invoice_customer_cancel_move',
|
|
invoice=invoice.rec_name))
|
|
if invoice.move:
|
|
invoice.cancel_move = invoice.move.cancel()
|
|
additional_cancel_moves = [
|
|
m.cancel() for m in invoice.additional_moves]
|
|
invoice.additional_moves += tuple(
|
|
additional_cancel_moves)
|
|
to_save.append(invoice)
|
|
cancel_moves.append(invoice.cancel_move)
|
|
cancel_moves.extend(additional_cancel_moves)
|
|
if cancel_moves:
|
|
Move.save(cancel_moves)
|
|
cls._store_cache(invoices)
|
|
cls.save(to_save)
|
|
if delete_moves:
|
|
Move.delete(delete_moves)
|
|
if cancel_moves:
|
|
Move.post(cancel_moves)
|
|
# Write state before reconcile to prevent invoice to go to paid state
|
|
cls.write(invoices, {
|
|
'state': 'cancelled',
|
|
})
|
|
|
|
for invoice in invoices:
|
|
if not invoice.move or not invoice.cancel_move:
|
|
continue
|
|
to_reconcile = []
|
|
for move in chain(
|
|
[invoice.move, invoice.cancel_move],
|
|
invoice.additional_moves):
|
|
for line in move.lines:
|
|
if (not line.reconciliation
|
|
and line.account == invoice.account):
|
|
to_reconcile.append(line)
|
|
Line.reconcile(to_reconcile)
|
|
|
|
cls._clean_payments(invoices)
|
|
|
|
@classmethod
|
|
def _clean_payments(cls, invoices):
|
|
to_write = []
|
|
for invoice in invoices:
|
|
to_remove = []
|
|
reconciliations = [l.reconciliation for l in invoice.lines_to_pay]
|
|
for payment_line in invoice.payment_lines:
|
|
if payment_line.reconciliation not in reconciliations:
|
|
to_remove.append(payment_line.id)
|
|
if to_remove:
|
|
to_write.append([invoice])
|
|
to_write.append({
|
|
'payment_lines': [('remove', to_remove)],
|
|
})
|
|
if to_write:
|
|
cls.write(*to_write)
|
|
|
|
|
|
class InvoiceAdditionalMove(ModelSQL):
|
|
__name__ = 'account.invoice-additional-account.move'
|
|
invoice = fields.Many2One(
|
|
'account.invoice', "Invoice", ondelete='CASCADE', required=True)
|
|
move = fields.Many2One(
|
|
'account.move', "Additional Move", ondelete='CASCADE')
|
|
|
|
|
|
class AlternativePayee(ModelSQL):
|
|
__name__ = 'account.invoice.alternative_payee'
|
|
|
|
invoice = fields.Many2One(
|
|
'account.invoice', "Invoice", ondelete='CASCADE', required=True)
|
|
party = fields.Many2One(
|
|
'party.party', "Payee", ondelete='RESTRICT', required=True)
|
|
|
|
|
|
class InvoicePaymentLine(ModelSQL):
|
|
__name__ = 'account.invoice-account.move.line'
|
|
invoice = fields.Many2One(
|
|
'account.invoice', "Invoice", ondelete='CASCADE', required=True)
|
|
invoice_account = fields.Function(
|
|
fields.Many2One('account.account', "Invoice Account"),
|
|
'get_invoice')
|
|
invoice_party = fields.Function(
|
|
fields.Many2One('party.party', "Invoice Party"), 'get_invoice')
|
|
invoice_alternative_payees = fields.Function(
|
|
fields.Many2Many(
|
|
'party.party', None, None, "Invoice Alternative Payees"),
|
|
'get_invoice')
|
|
line = fields.Many2One(
|
|
'account.move.line', "Payment Line", ondelete='CASCADE', required=True,
|
|
domain=[
|
|
('account', '=', Eval('invoice_account', -1)),
|
|
['OR',
|
|
('party', '=', Eval('invoice_party', -1)),
|
|
('party', 'in', Eval('invoice_alternative_payees', [])),
|
|
],
|
|
])
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
t = cls.__table__()
|
|
cls._sql_constraints = [
|
|
('line_unique', Unique(t, t.line),
|
|
'account_invoice.msg_invoice_payment_line_unique'),
|
|
]
|
|
|
|
@classmethod
|
|
def get_invoice(cls, records, names):
|
|
result = {}
|
|
for name in names:
|
|
result[name] = {}
|
|
invoice_account = 'invoice_account' in result
|
|
invoice_party = 'invoice_party' in result
|
|
invoice_alternative_payees = 'invoice_alternative_payees' in result
|
|
for record in records:
|
|
if invoice_account:
|
|
result['invoice_account'][record.id] = (
|
|
record.invoice.account.id)
|
|
if invoice_party:
|
|
if record.invoice.account.party_required:
|
|
party = record.invoice.party.id
|
|
else:
|
|
party = None
|
|
result['invoice_party'][record.id] = party
|
|
if invoice_alternative_payees:
|
|
result['invoice_alternative_payees'][record.id] = [
|
|
p.id for p in record.invoice.alternative_payees]
|
|
return result
|
|
|
|
|
|
class InvoiceLine(sequence_ordered(), ModelSQL, ModelView, TaxableMixin):
|
|
__name__ = 'account.invoice.line'
|
|
_states = {
|
|
'readonly': Eval('invoice_state') != 'draft',
|
|
}
|
|
|
|
invoice = fields.Many2One(
|
|
'account.invoice', "Invoice", ondelete='CASCADE',
|
|
states={
|
|
'required': (~Eval('invoice_type') & Eval('party')
|
|
& Eval('currency') & Eval('company')),
|
|
'invisible': Bool(Eval('context', {}).get('standalone')),
|
|
'readonly': _states['readonly'] & Bool(Eval('invoice')),
|
|
})
|
|
invoice_party = fields.Function(
|
|
fields.Many2One(
|
|
'party.party', "Party",
|
|
context={
|
|
'company': Eval('company', -1),
|
|
'party_contact_mechanism_usage': 'invoice',
|
|
},
|
|
depends=['company']),
|
|
'on_change_with_invoice_party', searcher='search_invoice_party')
|
|
invoice_description = fields.Function(
|
|
fields.Char("Invoice Description"),
|
|
'on_change_with_invoice_description',
|
|
searcher='search_invoice_description')
|
|
invoice_state = fields.Function(
|
|
fields.Selection('get_invoice_states', "Invoice State"),
|
|
'on_change_with_invoice_state')
|
|
invoice_type = fields.Selection(
|
|
'get_invoice_types', "Invoice Type",
|
|
states={
|
|
'readonly': Eval('context', {}).get('type') | Eval('type'),
|
|
'required': ~Eval('invoice'),
|
|
})
|
|
party = fields.Many2One(
|
|
'party.party', "Party",
|
|
states={
|
|
'required': ~Eval('invoice'),
|
|
'readonly': _states['readonly'],
|
|
},
|
|
context={
|
|
'company': Eval('company', -1),
|
|
'party_contact_mechanism_usage': 'invoice',
|
|
},
|
|
depends={'company'})
|
|
party_lang = fields.Function(fields.Char('Party Language'),
|
|
'on_change_with_party_lang')
|
|
currency = fields.Many2One(
|
|
'currency.currency', "Currency", required=True,
|
|
states=_states)
|
|
company = fields.Many2One(
|
|
'company.company', "Company", required=True,
|
|
states=_states,
|
|
context={
|
|
'party_contact_mechanism_usage': 'invoice',
|
|
})
|
|
type = fields.Selection([
|
|
('line', 'Line'),
|
|
('subtotal', 'Subtotal'),
|
|
('title', 'Title'),
|
|
('comment', 'Comment'),
|
|
], "Type", required=True,
|
|
states={
|
|
'invisible': Bool(Eval('context', {}).get('standalone')),
|
|
'readonly': _states['readonly'],
|
|
})
|
|
quantity = fields.Float(
|
|
"Quantity", digits='unit',
|
|
states={
|
|
'invisible': Eval('type') != 'line',
|
|
'required': Eval('type') == 'line',
|
|
'readonly': _states['readonly'],
|
|
})
|
|
unit = fields.Many2One('product.uom', 'Unit', ondelete='RESTRICT',
|
|
states={
|
|
'required': Bool(Eval('product')),
|
|
'invisible': Eval('type') != 'line',
|
|
'readonly': _states['readonly'],
|
|
},
|
|
domain=[
|
|
If(Bool(Eval('product_uom_category')),
|
|
('category', '=', Eval('product_uom_category')),
|
|
('category', '!=', -1)),
|
|
])
|
|
product = fields.Many2One('product.product', 'Product',
|
|
ondelete='RESTRICT',
|
|
domain=[
|
|
If(Bool(Eval('product_uom_category')),
|
|
('default_uom_category', '=', Eval('product_uom_category')),
|
|
()),
|
|
],
|
|
states={
|
|
'invisible': Eval('type') != 'line',
|
|
'readonly': _states['readonly'],
|
|
},
|
|
context={
|
|
'company': Eval('company', None),
|
|
},
|
|
depends={'company'})
|
|
product_uom_category = fields.Function(
|
|
fields.Many2One(
|
|
'product.uom.category', "Product UoM Category",
|
|
help="The category of Unit of Measure for the product."),
|
|
'on_change_with_product_uom_category')
|
|
account = fields.Many2One('account.account', 'Account',
|
|
ondelete='RESTRICT',
|
|
states={
|
|
'invisible': Eval('type') != 'line',
|
|
'required': Eval('type') == 'line',
|
|
'readonly': _states['readonly'],
|
|
},
|
|
context={
|
|
'date': If(Eval('_parent_invoice', {}).get('accounting_date'),
|
|
Eval('_parent_invoice', {}).get('accounting_date'),
|
|
Eval('_parent_invoice', {}).get('invoice_date')),
|
|
},
|
|
depends={'invoice'})
|
|
unit_price = Monetary(
|
|
"Unit Price", currency='currency', digits=price_digits,
|
|
states={
|
|
'invisible': Eval('type') != 'line',
|
|
'required': Eval('type') == 'line',
|
|
'readonly': _states['readonly'],
|
|
})
|
|
amount = fields.Function(Monetary(
|
|
"Amount", currency='currency', digits='currency',
|
|
states={
|
|
'invisible': ~Eval('type').in_(['line', 'subtotal']),
|
|
}),
|
|
'get_amount')
|
|
description = fields.Text(
|
|
"Description",
|
|
states={
|
|
'readonly': (_states['readonly']
|
|
& ~Id('account', 'group_account_admin').in_(
|
|
Eval('context', {}).get('groups', []))),
|
|
})
|
|
summary = fields.Function(
|
|
fields.Char('Summary'), 'on_change_with_summary',
|
|
searcher='search_summary')
|
|
note = fields.Text('Note')
|
|
taxes = fields.Many2Many('account.invoice.line-account.tax',
|
|
'line', 'tax', 'Taxes',
|
|
order=[('tax.sequence', 'ASC'), ('tax.id', 'ASC')],
|
|
domain=[('parent', '=', None), ['OR',
|
|
('group', '=', None),
|
|
('group.kind', 'in',
|
|
If(Bool(Eval('_parent_invoice')),
|
|
If(Eval('_parent_invoice', {}).get('type') == 'out',
|
|
['sale', 'both'],
|
|
['purchase', 'both']),
|
|
If(Eval('invoice_type') == 'out',
|
|
['sale', 'both'],
|
|
['purchase', 'both']))
|
|
)],
|
|
('company', '=', Eval('company', -1)),
|
|
],
|
|
states={
|
|
'invisible': Eval('type') != 'line',
|
|
'readonly': _states['readonly'] | ~Bool(Eval('account')),
|
|
},
|
|
depends={'invoice'})
|
|
taxes_deductible_rate = fields.Numeric(
|
|
"Taxes Deductible Rate", digits=(None, 10),
|
|
domain=[
|
|
('taxes_deductible_rate', '>=', 0),
|
|
('taxes_deductible_rate', '<=', 1),
|
|
],
|
|
states={
|
|
'invisible': (
|
|
(Eval('invoice_type') != 'in')
|
|
| (Eval('type') != 'line')),
|
|
})
|
|
taxes_date = fields.Date(
|
|
"Taxes Date",
|
|
states={
|
|
'invisible': Eval('type') != 'line',
|
|
'readonly': _states['readonly'],
|
|
},
|
|
help="The date at which the taxes are computed.\n"
|
|
"Leave empty for the accounting date.")
|
|
invoice_taxes = fields.Function(fields.Many2Many('account.invoice.tax',
|
|
None, None, 'Invoice Taxes'), 'get_invoice_taxes')
|
|
origin = fields.Reference("Origin", selection='get_origin', states=_states)
|
|
|
|
del _states
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._check_modify_exclude = {'note', 'origin', 'description'}
|
|
|
|
# Set account domain dynamically for kind
|
|
cls.account.domain = [
|
|
('closed', '!=', True),
|
|
('company', '=', Eval('company', -1)),
|
|
('id', '!=', Eval('_parent_invoice', {}).get('account', -1)),
|
|
If(Bool(Eval('_parent_invoice')),
|
|
If(Eval('_parent_invoice', {}).get('type') == 'out',
|
|
cls._account_domain('out'),
|
|
If(Eval('_parent_invoice', {}).get('type') == 'in',
|
|
cls._account_domain('in'),
|
|
['OR',
|
|
cls._account_domain('out'),
|
|
cls._account_domain('in')])),
|
|
If(Eval('invoice_type') == 'out',
|
|
cls._account_domain('out'),
|
|
If(Eval('invoice_type') == 'in',
|
|
cls._account_domain('in'),
|
|
['OR',
|
|
cls._account_domain('out'),
|
|
cls._account_domain('in')]))),
|
|
]
|
|
cls.sequence.states.update({
|
|
'invisible': Bool(Eval('context', {}).get('standalone')),
|
|
})
|
|
|
|
@staticmethod
|
|
def _account_domain(type_):
|
|
if type_ == 'out':
|
|
return ['OR', ('type.revenue', '=', True)]
|
|
elif type_ == 'in':
|
|
return ['OR',
|
|
('type.expense', '=', True),
|
|
('type.debt', '=', True),
|
|
]
|
|
|
|
@classmethod
|
|
def get_invoice_types(cls):
|
|
pool = Pool()
|
|
Invoice = pool.get('account.invoice')
|
|
return Invoice.fields_get(['type'])['type']['selection'] + [(None, '')]
|
|
|
|
@fields.depends(
|
|
'invoice', '_parent_invoice.currency', '_parent_invoice.company',
|
|
'_parent_invoice.type',
|
|
methods=['on_change_company'])
|
|
def on_change_invoice(self):
|
|
if self.invoice:
|
|
self.currency = self.invoice.currency
|
|
self.company = self.invoice.company
|
|
self.on_change_company()
|
|
self.invoice_type = self.invoice.type
|
|
|
|
@fields.depends('company', 'invoice',
|
|
'_parent_invoice.type', 'invoice_type')
|
|
def on_change_company(self):
|
|
invoice_type = self.invoice.type if self.invoice else self.invoice_type
|
|
if (invoice_type == 'in'
|
|
and self.company
|
|
and self.company.purchase_taxes_expense):
|
|
self.taxes_deductible_rate = 0
|
|
|
|
@staticmethod
|
|
def default_currency():
|
|
Company = Pool().get('company.company')
|
|
if Transaction().context.get('company'):
|
|
company = Company(Transaction().context['company'])
|
|
return company.currency.id
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
@staticmethod
|
|
def default_type():
|
|
return 'line'
|
|
|
|
@fields.depends('party', 'invoice', '_parent_invoice.party')
|
|
def on_change_with_invoice_party(self, name=None):
|
|
if self.invoice and self.invoice.party:
|
|
return self.invoice.party
|
|
elif self.party:
|
|
return self.party
|
|
|
|
@classmethod
|
|
def search_invoice_party(cls, name, clause):
|
|
nested = clause[0][len(name):]
|
|
return ['OR',
|
|
('invoice.party' + nested, *clause[1:]),
|
|
('party' + nested, *clause[1:]),
|
|
]
|
|
|
|
@fields.depends('invoice', '_parent_invoice.description')
|
|
def on_change_with_invoice_description(self, name=None):
|
|
if self.invoice:
|
|
return self.invoice.description
|
|
|
|
@classmethod
|
|
def search_invoice_description(cls, name, clause):
|
|
return [('invoice.description', *clause[1:])]
|
|
|
|
@classmethod
|
|
def default_invoice_state(cls):
|
|
return 'draft'
|
|
|
|
@classmethod
|
|
def get_invoice_states(cls):
|
|
pool = Pool()
|
|
Invoice = pool.get('account.invoice')
|
|
return Invoice.fields_get(['state'])['state']['selection']
|
|
|
|
@fields.depends('invoice', '_parent_invoice.state')
|
|
def on_change_with_invoice_state(self, name=None):
|
|
if self.invoice:
|
|
state = self.invoice.state
|
|
if state == 'cancelled' and self.invoice.cancel_move:
|
|
state = 'paid'
|
|
else:
|
|
state = 'draft'
|
|
return state
|
|
|
|
@fields.depends('invoice', '_parent_invoice.party', 'party')
|
|
def on_change_with_party_lang(self, name=None):
|
|
Config = Pool().get('ir.configuration')
|
|
if self.invoice and self.invoice.party:
|
|
party = self.invoice.party
|
|
else:
|
|
party = self.party
|
|
if party and party.lang:
|
|
return party.lang.code
|
|
return Config.get_language()
|
|
|
|
@fields.depends('description')
|
|
def on_change_with_summary(self, name=None):
|
|
return firstline(self.description or '')
|
|
|
|
@classmethod
|
|
def search_summary(cls, name, clause):
|
|
return [('description', *clause[1:])]
|
|
|
|
@fields.depends(
|
|
'type', 'quantity', 'unit_price', 'taxes_deductible_rate', 'invoice',
|
|
'_parent_invoice.currency', 'currency', 'taxes',
|
|
'_parent_invoice.type', 'invoice_type',
|
|
methods=['_get_taxes'])
|
|
def on_change_with_amount(self):
|
|
if self.type == 'line':
|
|
currency = (self.invoice.currency if self.invoice
|
|
else self.currency)
|
|
amount = (Decimal(str(self.quantity or 0))
|
|
* (self.unit_price or Decimal(0)))
|
|
invoice_type = (
|
|
self.invoice.type if self.invoice else self.invoice_type)
|
|
if (invoice_type == 'in'
|
|
and self.taxes_deductible_rate is not None
|
|
and self.taxes_deductible_rate != 1):
|
|
with Transaction().set_context(_deductible_rate=1):
|
|
tax_amount = sum(
|
|
t.amount for t in self._get_taxes().values())
|
|
non_deductible_amount = (
|
|
tax_amount * (1 - self.taxes_deductible_rate))
|
|
amount += non_deductible_amount
|
|
if currency:
|
|
return currency.round(amount)
|
|
return amount
|
|
return Decimal(0)
|
|
|
|
def get_amount(self, name):
|
|
if self.type == 'line':
|
|
return self.on_change_with_amount()
|
|
elif self.type == 'subtotal':
|
|
subtotal = Decimal(0)
|
|
for line2 in self.invoice.lines:
|
|
if line2.type == 'line':
|
|
subtotal += line2.on_change_with_amount()
|
|
elif line2.type == 'subtotal':
|
|
if self == line2:
|
|
break
|
|
subtotal = Decimal(0)
|
|
return subtotal
|
|
else:
|
|
return Decimal(0)
|
|
|
|
@property
|
|
def origin_name(self):
|
|
if isinstance(self.origin, self.__class__) and self.origin.id >= 0:
|
|
return self.origin.invoice.rec_name
|
|
if self.origin and self.origin.id >= 0:
|
|
return self.origin.rec_name
|
|
|
|
@classmethod
|
|
def default_taxes_deductible_rate(cls):
|
|
return 1
|
|
|
|
@property
|
|
def taxable_lines(self):
|
|
# In case we're called from an on_change we have to use some sensible
|
|
# defaults
|
|
context = Transaction().context
|
|
if (getattr(self, 'invoice', None)
|
|
and getattr(self.invoice, 'type', None)):
|
|
invoice_type = self.invoice.type
|
|
else:
|
|
invoice_type = getattr(self, 'invoice_type', None)
|
|
if invoice_type == 'in':
|
|
if context.get('_deductible_rate') is not None:
|
|
deductible_rate = context['_deductible_rate']
|
|
else:
|
|
deductible_rate = getattr(self, 'taxes_deductible_rate', 1)
|
|
if deductible_rate is None:
|
|
deductible_rate = 1
|
|
if not deductible_rate:
|
|
return []
|
|
else:
|
|
deductible_rate = 1
|
|
return [(
|
|
list(getattr(self, 'taxes', None)) or [],
|
|
((getattr(self, 'unit_price', None) or Decimal(0))
|
|
* deductible_rate),
|
|
getattr(self, 'quantity', None) or 0,
|
|
getattr(self, 'tax_date', None),
|
|
)]
|
|
|
|
@property
|
|
def tax_date(self):
|
|
if getattr(self, 'taxes_date', None):
|
|
return self.taxes_date
|
|
elif hasattr(self, 'invoice') and hasattr(self.invoice, 'tax_date'):
|
|
return self.invoice.tax_date
|
|
else:
|
|
return super().tax_date
|
|
|
|
def _get_tax_context(self):
|
|
if self.invoice:
|
|
return self.invoice._get_tax_context()
|
|
else:
|
|
context = {}
|
|
if self.company:
|
|
context['company'] = self.company.id
|
|
return context
|
|
|
|
def get_invoice_taxes(self, name):
|
|
if not self.invoice:
|
|
return
|
|
taxes_keys = self._get_taxes().keys()
|
|
taxes = []
|
|
for tax in self.invoice.taxes:
|
|
if tax.manual:
|
|
continue
|
|
key = tax._key
|
|
if key in taxes_keys:
|
|
taxes.append(tax.id)
|
|
return taxes
|
|
|
|
@fields.depends('invoice',
|
|
'_parent_invoice.accounting_date', '_parent_invoice.invoice_date')
|
|
def _get_tax_rule_pattern(self):
|
|
'''
|
|
Get tax rule pattern
|
|
'''
|
|
if self.invoice:
|
|
date = self.invoice.accounting_date or self.invoice.invoice_date
|
|
else:
|
|
date = None
|
|
return {
|
|
'date': date,
|
|
}
|
|
|
|
@fields.depends(
|
|
'product', 'unit', 'taxes', '_parent_invoice.type',
|
|
'_parent_invoice.party', 'party', 'invoice', 'invoice_type',
|
|
'_parent_invoice.invoice_date', '_parent_invoice.accounting_date',
|
|
'company',
|
|
methods=['_get_tax_rule_pattern'])
|
|
def on_change_product(self):
|
|
if not self.product:
|
|
return
|
|
|
|
party = None
|
|
if self.invoice and self.invoice.party:
|
|
party = self.invoice.party
|
|
elif self.party:
|
|
party = self.party
|
|
|
|
date = (self.invoice.accounting_date or self.invoice.invoice_date
|
|
if self.invoice else None)
|
|
if self.invoice and self.invoice.type:
|
|
type_ = self.invoice.type
|
|
else:
|
|
type_ = self.invoice_type
|
|
if type_ == 'in':
|
|
with Transaction().set_context(date=date):
|
|
self.account = self.product.account_expense_used
|
|
taxes = set()
|
|
pattern = self._get_tax_rule_pattern()
|
|
for tax in self.product.supplier_taxes_used:
|
|
if party and party.supplier_tax_rule:
|
|
tax_ids = party.supplier_tax_rule.apply(tax, pattern)
|
|
if tax_ids:
|
|
taxes.update(tax_ids)
|
|
continue
|
|
taxes.add(tax.id)
|
|
if party and party.supplier_tax_rule:
|
|
tax_ids = party.supplier_tax_rule.apply(None, pattern)
|
|
if tax_ids:
|
|
taxes.update(tax_ids)
|
|
self.taxes = taxes
|
|
|
|
if self.company and self.company.purchase_taxes_expense:
|
|
self.taxes_deductible_rate = 0
|
|
else:
|
|
self.taxes_deductible_rate = (
|
|
self.product.supplier_taxes_deductible_rate_used)
|
|
else:
|
|
with Transaction().set_context(date=date):
|
|
self.account = self.product.account_revenue_used
|
|
taxes = set()
|
|
pattern = self._get_tax_rule_pattern()
|
|
for tax in self.product.customer_taxes_used:
|
|
if party and party.customer_tax_rule:
|
|
tax_ids = party.customer_tax_rule.apply(tax, pattern)
|
|
if tax_ids:
|
|
taxes.update(tax_ids)
|
|
continue
|
|
taxes.add(tax.id)
|
|
if party and party.customer_tax_rule:
|
|
tax_ids = party.customer_tax_rule.apply(None, pattern)
|
|
if tax_ids:
|
|
taxes.update(tax_ids)
|
|
self.taxes = taxes
|
|
|
|
category = self.product.default_uom.category
|
|
if not self.unit or self.unit.category != category:
|
|
self.unit = self.product.default_uom.id
|
|
|
|
@cached_property
|
|
def product_name(self):
|
|
return self.product.rec_name if self.product else ''
|
|
|
|
@fields.depends('product')
|
|
def on_change_with_product_uom_category(self, name=None):
|
|
return self.product.default_uom_category if self.product else None
|
|
|
|
@fields.depends(
|
|
'account', 'product', 'invoice', 'taxes',
|
|
'_parent_invoice.party', '_parent_invoice.type',
|
|
'party', 'invoice', 'invoice_type',
|
|
methods=['_get_tax_rule_pattern'])
|
|
def on_change_account(self):
|
|
if self.product:
|
|
return
|
|
taxes = set()
|
|
party = None
|
|
if self.invoice and self.invoice.party:
|
|
party = self.invoice.party
|
|
elif self.party:
|
|
party = self.party
|
|
|
|
if self.invoice and self.invoice.type:
|
|
type_ = self.invoice.type
|
|
else:
|
|
type_ = self.invoice_type
|
|
if party and type_:
|
|
if type_ == 'in':
|
|
tax_rule = party.supplier_tax_rule
|
|
else:
|
|
tax_rule = party.customer_tax_rule
|
|
else:
|
|
tax_rule = None
|
|
|
|
if self.account:
|
|
pattern = self._get_tax_rule_pattern()
|
|
for tax in self.account.taxes:
|
|
if tax_rule:
|
|
tax_ids = tax_rule.apply(tax, pattern)
|
|
if tax_ids:
|
|
taxes.update(tax_ids)
|
|
continue
|
|
taxes.add(tax.id)
|
|
if tax_rule:
|
|
tax_ids = tax_rule.apply(None, pattern)
|
|
if tax_ids:
|
|
taxes.update(tax_ids)
|
|
self.taxes = taxes
|
|
|
|
@classmethod
|
|
def _get_origin(cls):
|
|
'Return list of Model names for origin Reference'
|
|
return [cls.__name__]
|
|
|
|
@classmethod
|
|
def get_origin(cls):
|
|
Model = Pool().get('ir.model')
|
|
get_name = Model.get_name
|
|
models = cls._get_origin()
|
|
return [(None, '')] + [(m, get_name(m)) for m in models]
|
|
|
|
def get_rec_name(self, name):
|
|
pool = Pool()
|
|
Lang = pool.get('ir.lang')
|
|
if self.product:
|
|
lang = Lang.get()
|
|
prefix = (lang.format_number_symbol(
|
|
self.quantity or 0, self.unit, digits=self.unit.digits)
|
|
+ ' %s' % self.product.rec_name)
|
|
elif self.account:
|
|
prefix = self.account.rec_name
|
|
else:
|
|
prefix = '(%s)' % self.id
|
|
if self.invoice:
|
|
return '%s @ %s' % (prefix, self.invoice.rec_name)
|
|
else:
|
|
return prefix
|
|
|
|
@classmethod
|
|
def search_rec_name(cls, name, clause):
|
|
_, operator, value = clause
|
|
if operator.startswith('!') or operator.startswith('not '):
|
|
bool_op = 'AND'
|
|
else:
|
|
bool_op = 'OR'
|
|
return [bool_op,
|
|
('invoice.rec_name', *clause[1:]),
|
|
('product.rec_name', *clause[1:]),
|
|
('account.rec_name', *clause[1:]),
|
|
]
|
|
|
|
@classmethod
|
|
def check_modification(cls, mode, lines, values=None, external=False):
|
|
super().check_modification(
|
|
mode, lines, values=values, external=external)
|
|
if mode == 'create':
|
|
for line in lines:
|
|
if line.invoice and line.invoice.state != 'draft':
|
|
raise AccessError(gettext(
|
|
'account_invoice.msg_invoice_line_create_draft',
|
|
invoice=line.invoice.rec_name))
|
|
elif (mode == 'delete'
|
|
or (mode == 'write'
|
|
and values.keys() - cls._check_modify_exclude)):
|
|
for line in lines:
|
|
if line.invoice and not line.invoice.is_modifiable:
|
|
raise AccessError(gettext(
|
|
'account_invoice.msg_invoice_line_modify',
|
|
line=line.rec_name,
|
|
invoice=line.invoice.rec_name))
|
|
|
|
@classmethod
|
|
def view_attributes(cls):
|
|
return super().view_attributes() + [
|
|
('/form//field[@name="note"]|/form//field[@name="description"]',
|
|
'spell', Eval('party_lang'))]
|
|
|
|
@classmethod
|
|
def copy(cls, lines, default=None):
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
default.setdefault('origin', None)
|
|
return super().copy(lines, default=default)
|
|
|
|
def _compute_taxes(self):
|
|
pool = Pool()
|
|
Currency = pool.get('currency.currency')
|
|
TaxLine = pool.get('account.tax.line')
|
|
|
|
tax_lines = []
|
|
if self.type != 'line':
|
|
return tax_lines
|
|
taxes = self._get_taxes().values()
|
|
for tax in taxes:
|
|
amount = tax.base
|
|
with Transaction().set_context(
|
|
date=self.invoice.currency_date):
|
|
amount = Currency.compute(
|
|
self.invoice.currency, amount,
|
|
self.invoice.company.currency)
|
|
tax_line = TaxLine()
|
|
tax_line.amount = amount
|
|
tax_line.type = 'base'
|
|
tax_line.tax = tax.tax
|
|
tax_lines.append(tax_line)
|
|
return tax_lines
|
|
|
|
def get_move_lines(self):
|
|
'''
|
|
Return a list of move lines instances for invoice line
|
|
'''
|
|
pool = Pool()
|
|
Currency = pool.get('currency.currency')
|
|
MoveLine = pool.get('account.move.line')
|
|
if self.type != 'line':
|
|
return []
|
|
line = MoveLine()
|
|
if self.invoice.currency != self.invoice.company.currency:
|
|
with Transaction().set_context(date=self.invoice.currency_date):
|
|
amount = Currency.compute(self.invoice.currency,
|
|
self.amount, self.invoice.company.currency)
|
|
line.amount_second_currency = self.amount
|
|
line.second_currency = self.invoice.currency
|
|
else:
|
|
amount = self.amount
|
|
line.amount_second_currency = None
|
|
line.second_currency = None
|
|
if amount >= 0:
|
|
if self.invoice.type == 'out':
|
|
line.debit, line.credit = 0, amount
|
|
else:
|
|
line.debit, line.credit = amount, 0
|
|
else:
|
|
if self.invoice.type == 'out':
|
|
line.debit, line.credit = -amount, 0
|
|
else:
|
|
line.debit, line.credit = 0, -amount
|
|
if line.amount_second_currency:
|
|
line.amount_second_currency = (
|
|
line.amount_second_currency.copy_sign(
|
|
line.debit - line.credit))
|
|
line.account = self.account
|
|
if self.account.party_required:
|
|
line.party = self.invoice.party
|
|
line.origin = self
|
|
line.tax_lines = self._compute_taxes()
|
|
return [line]
|
|
|
|
def _credit(self):
|
|
'''
|
|
Return credit line.
|
|
'''
|
|
line = self.__class__()
|
|
line.origin = self
|
|
if self.quantity:
|
|
line.quantity = -self.quantity
|
|
else:
|
|
line.quantity = self.quantity
|
|
|
|
for field in [
|
|
'sequence', 'type', 'invoice_type', 'party', 'currency',
|
|
'company', 'unit_price', 'description', 'unit', 'product',
|
|
'account', 'taxes_deductible_rate']:
|
|
setattr(line, field, getattr(self, field))
|
|
line.taxes_date = self.tax_date
|
|
line.taxes = self.taxes
|
|
return line
|
|
|
|
|
|
class InvoiceLineTax(ModelSQL):
|
|
__name__ = 'account.invoice.line-account.tax'
|
|
line = fields.Many2One(
|
|
'account.invoice.line', "Invoice Line",
|
|
ondelete='CASCADE', required=True)
|
|
tax = fields.Many2One('account.tax', 'Tax', ondelete='RESTRICT',
|
|
required=True)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
t = cls.__table__()
|
|
cls._sql_constraints += [
|
|
('line_tax_unique', Unique(t, t.line, t.tax),
|
|
'account_invoice.msg_invoice_line_tax_unique'),
|
|
]
|
|
|
|
@classmethod
|
|
def __register__(cls, module):
|
|
# Migration from 7.0: rename to standard name
|
|
backend.TableHandler.table_rename(
|
|
'account_invoice_line_account_tax', cls._table)
|
|
super().__register__(module)
|
|
|
|
|
|
class InvoiceTax(sequence_ordered(), ModelSQL, ModelView):
|
|
__name__ = 'account.invoice.tax'
|
|
_rec_name = 'description'
|
|
_states = {
|
|
'readonly': Eval('invoice_state') != 'draft',
|
|
}
|
|
|
|
invoice = fields.Many2One(
|
|
'account.invoice', "Invoice", ondelete='CASCADE', required=True,
|
|
states={
|
|
'readonly': _states['readonly'] & Bool(Eval('invoice')),
|
|
})
|
|
invoice_state = fields.Function(
|
|
fields.Selection('get_invoice_states', "Invoice State"),
|
|
'on_change_with_invoice_state')
|
|
description = fields.Char(
|
|
"Description", size=None, required=True,
|
|
states={
|
|
'readonly': (_states['readonly']
|
|
& ~Id('account', 'group_account_admin').in_(
|
|
Eval('context', {}).get('groups', []))),
|
|
})
|
|
sequence_number = fields.Function(fields.Integer('Sequence Number'),
|
|
'get_sequence_number')
|
|
account = fields.Many2One('account.account', 'Account', required=True,
|
|
domain=[
|
|
('type', '!=', None),
|
|
('closed', '!=', True),
|
|
('company', '=', Eval('_parent_invoice', {}).get('company', 0)),
|
|
('id', '!=', Eval('_parent_invoice', {}).get('account', -1)),
|
|
],
|
|
states=_states, depends={'invoice'})
|
|
base = Monetary(
|
|
"Base", currency='currency', digits='currency', required=True,
|
|
states=_states)
|
|
amount = Monetary(
|
|
"Amount", currency='currency', digits='currency', required=True,
|
|
states=_states,
|
|
depends={'tax', 'base', 'manual'})
|
|
currency = fields.Function(fields.Many2One('currency.currency',
|
|
'Currency'), 'on_change_with_currency')
|
|
manual = fields.Boolean('Manual', states=_states)
|
|
tax = fields.Many2One('account.tax', 'Tax',
|
|
ondelete='RESTRICT',
|
|
domain=[
|
|
['OR',
|
|
('group', '=', None),
|
|
('group.kind', 'in',
|
|
If(Eval('_parent_invoice', {}).get('type') == 'out',
|
|
['sale', 'both'],
|
|
['purchase', 'both']),
|
|
)],
|
|
('company', '=', Eval('_parent_invoice', {}).get('company', 0)),
|
|
],
|
|
states={
|
|
'readonly': (
|
|
~Eval('manual', False) | ~Bool(Eval('invoice'))
|
|
| _states['readonly']),
|
|
},
|
|
depends={'invoice'})
|
|
legal_notice = fields.Text(
|
|
"Legal Notice",
|
|
states={
|
|
'readonly': (_states['readonly']
|
|
& ~Id('account', 'group_account_admin').in_(
|
|
Eval('context', {}).get('groups', []))),
|
|
})
|
|
|
|
del _states
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.__access__.add('invoice')
|
|
cls._check_modify_exclude = {'description', 'legal_notice'}
|
|
|
|
@staticmethod
|
|
def default_base():
|
|
return Decimal(0)
|
|
|
|
@staticmethod
|
|
def default_amount():
|
|
return Decimal(0)
|
|
|
|
@staticmethod
|
|
def default_manual():
|
|
return True
|
|
|
|
@classmethod
|
|
def default_invoice_state(cls):
|
|
pool = Pool()
|
|
Invoice = pool.get('account.invoice')
|
|
return Invoice.default_state()
|
|
|
|
@classmethod
|
|
def get_invoice_states(cls):
|
|
pool = Pool()
|
|
Invoice = pool.get('account.invoice')
|
|
return Invoice.fields_get(['state'])['state']['selection']
|
|
|
|
@fields.depends('invoice', '_parent_invoice.state')
|
|
def on_change_with_invoice_state(self, name=None):
|
|
if self.invoice:
|
|
return self.invoice.state
|
|
|
|
@fields.depends('invoice', '_parent_invoice.currency')
|
|
def on_change_with_currency(self, name=None):
|
|
return self.invoice.currency if self.invoice else None
|
|
|
|
@fields.depends(
|
|
'tax', 'invoice', '_parent_invoice.party', 'base',
|
|
methods=['_compute_amount'])
|
|
def on_change_tax(self):
|
|
Tax = Pool().get('account.tax')
|
|
if not self.tax:
|
|
return
|
|
if self.invoice:
|
|
context = self.invoice._get_tax_context()
|
|
else:
|
|
context = {}
|
|
with Transaction().set_context(**context):
|
|
tax = Tax(self.tax.id)
|
|
self.description = tax.description
|
|
if self.base is not None:
|
|
if self.base >= 0:
|
|
self.account = tax.invoice_account
|
|
else:
|
|
self.account = tax.credit_note_account
|
|
self._compute_amount()
|
|
|
|
@fields.depends('base', 'tax', methods=['_compute_amount'])
|
|
def on_change_base(self):
|
|
if self.base is not None and self.tax:
|
|
if self.base >= 0:
|
|
self.account = self.tax.invoice_account
|
|
else:
|
|
self.account = self.tax.credit_note_account
|
|
self._compute_amount()
|
|
|
|
@fields.depends(
|
|
'tax', 'base', 'manual', 'invoice', '_parent_invoice.currency',
|
|
# From_date
|
|
'_parent_invoice.accounting_date', '_parent_invoice.invoice_date',
|
|
'_parent_invoice.company')
|
|
def _compute_amount(self):
|
|
pool = Pool()
|
|
Tax = pool.get('account.tax')
|
|
if self.tax and self.manual:
|
|
tax = self.tax
|
|
base = self.base or Decimal(0)
|
|
if self.invoice and self.invoice.tax_date:
|
|
tax_date = self.invoice.tax_date
|
|
for values in Tax.compute([tax], base, 1, tax_date):
|
|
if (values['tax'] == tax
|
|
and values['base'] == base):
|
|
amount = values['amount']
|
|
if self.invoice.currency:
|
|
amount = self.invoice.currency.round(amount)
|
|
self.amount = amount
|
|
|
|
@property
|
|
def _key(self):
|
|
# Same as _TaxLine
|
|
return (self.account, self.tax, (getattr(self, 'base', 0) or 0) >= 0)
|
|
|
|
@classmethod
|
|
def check_modification(cls, mode, taxes, values=None, external=False):
|
|
super().check_modification(
|
|
mode, taxes, values=values, external=external)
|
|
if mode == 'create':
|
|
for tax in taxes:
|
|
if tax.invoice.state != 'draft':
|
|
raise AccessError(gettext(
|
|
'account_invoice.msg_invoice_line_create_draft',
|
|
invoice=tax.invoice.rec_name))
|
|
elif (mode == 'delete'
|
|
or (mode == 'write'
|
|
and values.keys() - cls._check_modify_exclude)):
|
|
for tax in taxes:
|
|
if not tax.invoice.is_modifiable:
|
|
raise AccessError(gettext(
|
|
'account_invoice.msg_invoice_tax_modify',
|
|
tax=tax.rec_name,
|
|
invoice=tax.invoice.rec_name))
|
|
|
|
def get_sequence_number(self, name):
|
|
i = 1
|
|
for tax in self.invoice.taxes:
|
|
if tax == self:
|
|
return i
|
|
i += 1
|
|
return 0
|
|
|
|
def get_move_lines(self):
|
|
'''
|
|
Return a list of move lines instances for invoice tax
|
|
'''
|
|
Currency = Pool().get('currency.currency')
|
|
pool = Pool()
|
|
Currency = pool.get('currency.currency')
|
|
MoveLine = pool.get('account.move.line')
|
|
TaxLine = pool.get('account.tax.line')
|
|
line = MoveLine()
|
|
if not self.amount:
|
|
return []
|
|
line.description = self.description
|
|
if self.invoice.currency != self.invoice.company.currency:
|
|
with Transaction().set_context(date=self.invoice.currency_date):
|
|
amount = Currency.compute(self.invoice.currency, self.amount,
|
|
self.invoice.company.currency)
|
|
base = Currency.compute(self.invoice.currency, self.base,
|
|
self.invoice.company.currency)
|
|
line.amount_second_currency = self.amount
|
|
line.second_currency = self.invoice.currency
|
|
else:
|
|
amount = self.amount
|
|
base = self.base
|
|
line.amount_second_currency = None
|
|
line.second_currency = None
|
|
if amount >= 0:
|
|
if self.invoice.type == 'out':
|
|
line.debit, line.credit = 0, amount
|
|
else:
|
|
line.debit, line.credit = amount, 0
|
|
else:
|
|
if self.invoice.type == 'out':
|
|
line.debit, line.credit = -amount, 0
|
|
else:
|
|
line.debit, line.credit = 0, -amount
|
|
if line.amount_second_currency:
|
|
line.amount_second_currency = (
|
|
line.amount_second_currency.copy_sign(
|
|
line.debit - line.credit))
|
|
line.account = self.account
|
|
if self.account.party_required:
|
|
line.party = self.invoice.party
|
|
line.origin = self
|
|
if self.tax:
|
|
tax_lines = []
|
|
tax_line = TaxLine()
|
|
tax_line.amount = amount
|
|
tax_line.type = 'tax'
|
|
tax_line.tax = self.tax
|
|
tax_lines.append(tax_line)
|
|
if self.manual:
|
|
tax_line = TaxLine()
|
|
tax_line.amount = base
|
|
tax_line.type = 'base'
|
|
tax_line.tax = self.tax
|
|
tax_lines.append(tax_line)
|
|
line.tax_lines = tax_lines
|
|
return [line]
|
|
|
|
def _credit(self):
|
|
'''
|
|
Return credit tax.
|
|
'''
|
|
line = self.__class__()
|
|
line.base = -self.base
|
|
line.amount = -self.amount
|
|
|
|
for field in ['description', 'sequence', 'manual', 'account', 'tax']:
|
|
setattr(line, field, getattr(self, field))
|
|
return line
|
|
|
|
|
|
class PaymentMethod(DeactivableMixin, ModelSQL, ModelView):
|
|
__name__ = 'account.invoice.payment.method'
|
|
company = fields.Many2One('company.company', "Company", required=True)
|
|
name = fields.Char("Name", required=True, translate=True)
|
|
journal = fields.Many2One(
|
|
'account.journal', "Journal", required=True,
|
|
domain=[('type', '=', 'cash')],
|
|
context={
|
|
'company': Eval('company', -1),
|
|
},
|
|
depends={'company'})
|
|
credit_account = fields.Many2One('account.account', "Credit Account",
|
|
required=True,
|
|
domain=[
|
|
('type', '!=', None),
|
|
('closed', '!=', True),
|
|
('company', '=', Eval('company', -1)),
|
|
])
|
|
debit_account = fields.Many2One('account.account', "Debit Account",
|
|
required=True,
|
|
domain=[
|
|
('type', '!=', None),
|
|
('closed', '!=', True),
|
|
('company', '=', Eval('company', -1)),
|
|
])
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._order.insert(0, ('name', 'ASC'))
|
|
|
|
@classmethod
|
|
def default_company(cls):
|
|
return Transaction().context.get('company')
|
|
|
|
|
|
class InvoiceReportRevision(ModelSQL, ModelView, InvoiceReportMixin):
|
|
__name__ = 'account.invoice.report.revision'
|
|
invoice = fields.Many2One(
|
|
'account.invoice', "Invoice", required=True, ondelete='CASCADE')
|
|
date = fields.DateTime("Date", required=True, readonly=True)
|
|
filename = fields.Function(fields.Char("File Name"), 'get_filename')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.__access__.add('invoice')
|
|
cls._order.insert(0, ('date', 'DESC'))
|
|
cls.invoice_report_cache.filename = 'filename'
|
|
|
|
@classmethod
|
|
def default_date(cls):
|
|
return dt.datetime.now()
|
|
|
|
@classmethod
|
|
def get_filename(cls, revisions, name):
|
|
pool = Pool()
|
|
ActionReport = pool.get('ir.action.report')
|
|
|
|
action_report, = ActionReport.search([
|
|
('report_name', '=', 'account.invoice'),
|
|
], limit=1)
|
|
|
|
action_report_name = action_report.name[:100]
|
|
if action_report.record_name:
|
|
template = TextTemplate(action_report.record_name)
|
|
else:
|
|
template = None
|
|
filenames = {}
|
|
for revision in revisions:
|
|
invoice = revision.invoice
|
|
if template:
|
|
record_name = template.generate(record=invoice).render()
|
|
else:
|
|
record_name = invoice.rec_name
|
|
filename = '-'.join([action_report_name, record_name])
|
|
filenames[revision.id] = (
|
|
f'{slugify(filename)}.{revision.invoice_report_format}')
|
|
return filenames
|
|
|
|
|
|
class RefreshInvoiceReport(Wizard):
|
|
__name__ = 'account.invoice.refresh_invoice_report'
|
|
start_state = 'archive'
|
|
archive = StateTransition()
|
|
print_ = StateReport('account.invoice')
|
|
|
|
def transition_archive(self):
|
|
for record in self.records:
|
|
record.create_invoice_report_revision()
|
|
self.model.save(self.records)
|
|
return 'print_'
|
|
|
|
def do_print_(self, action):
|
|
ids = [r.id for r in self.records]
|
|
return action, {'ids': ids}
|
|
|
|
|
|
class InvoiceReport(Report):
|
|
__name__ = 'account.invoice'
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.__rpc__['execute'] = RPC(False)
|
|
|
|
@classmethod
|
|
def _execute(cls, records, header, data, action):
|
|
pool = Pool()
|
|
Invoice = pool.get('account.invoice')
|
|
# Re-instantiate because records are TranslateModel
|
|
invoice, = Invoice.browse(records)
|
|
if invoice.invoice_report_cache:
|
|
return (
|
|
invoice.invoice_report_format,
|
|
invoice.invoice_report_cache)
|
|
else:
|
|
result = super()._execute(records, header, data, action)
|
|
if invoice.invoice_report_versioned:
|
|
format_, data = result
|
|
if isinstance(data, str):
|
|
data = bytes(data, 'utf-8')
|
|
invoice.invoice_report_format = format_
|
|
invoice.invoice_report_cache = \
|
|
Invoice.invoice_report_cache.cast(data)
|
|
invoice.save()
|
|
return result
|
|
|
|
@classmethod
|
|
def render(cls, *args, **kwargs):
|
|
# Reset to default language to always have header and footer rendered
|
|
# in the default language
|
|
with Transaction().set_context(language=False):
|
|
return super().render(*args, **kwargs)
|
|
|
|
@classmethod
|
|
def execute(cls, ids, data):
|
|
with Transaction().set_context(address_with_party=True):
|
|
return super().execute(ids, data)
|
|
|
|
@classmethod
|
|
def get_context(cls, records, header, data):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
context = super().get_context(records, header, data)
|
|
context['invoice'] = context['record']
|
|
with Transaction().set_context(company=context['invoice'].company.id):
|
|
context['today'] = Date.today()
|
|
return context
|
|
|
|
|
|
class InvoiceEdocument(Wizard):
|
|
__name__ = 'account.invoice.edocument'
|
|
|
|
start = StateView(
|
|
'account.invoice.edocument.start',
|
|
'account_invoice.edocument_start_view_form', [
|
|
Button("Cancel", 'end', 'tryton-cancel'),
|
|
Button("Render", 'render', 'tryton-ok', default=True),
|
|
])
|
|
render = StateTransition()
|
|
result = StateView(
|
|
'account.invoice.edocument.result',
|
|
'account_invoice.edocument_result_view_form', [
|
|
Button("Close", 'end', 'tryton-close', default=True),
|
|
])
|
|
|
|
def transition_render(self):
|
|
pool = Pool()
|
|
Start = pool.get('account.invoice.edocument.start')
|
|
if self.start.format not in dict(Start.format.selection):
|
|
raise ValueError("Unsupported format")
|
|
Edocument = pool.get(self.start.format)
|
|
edocument = Edocument(self.record)
|
|
file = edocument.render(self.start.template)
|
|
if isinstance(file, str):
|
|
file = file.decode('utf-8')
|
|
self.result.file = file
|
|
self.result.filename = edocument.filename
|
|
return 'result'
|
|
|
|
def default_result(self, fields):
|
|
file = self.result.file
|
|
self.result.file = None # No need to store it in the session
|
|
return {
|
|
'file': file,
|
|
'filename': self.result.filename,
|
|
}
|
|
|
|
|
|
class InvoiceEdocumentStart(ModelView):
|
|
__name__ = 'account.invoice.edocument.start'
|
|
|
|
format = fields.Selection([
|
|
], "Format", required=True)
|
|
template = fields.Selection('get_templates', "Template", required=True)
|
|
|
|
@fields.depends()
|
|
def get_templates(self):
|
|
return []
|
|
|
|
|
|
class InvoiceEdocumentResult(ModelView):
|
|
__name__ = 'account.invoice.edocument.result'
|
|
|
|
file = fields.Binary("File", readonly=True, filename='filename')
|
|
filename = fields.Char("File Name", readonly=True)
|
|
|
|
|
|
class PayInvoiceStart(ModelView):
|
|
__name__ = 'account.invoice.pay.start'
|
|
|
|
payee = fields.Many2One(
|
|
'party.party', "Payee", required=True,
|
|
domain=[
|
|
('id', 'in', Eval('payees', []))
|
|
],
|
|
context={
|
|
'company': Eval('company', -1),
|
|
},
|
|
depends=['company'])
|
|
payees = fields.Many2Many(
|
|
'party.party', None, None, "Payees", readonly=True,
|
|
context={
|
|
'company': Eval('company', -1),
|
|
},
|
|
depends=['company'])
|
|
amount = Monetary(
|
|
"Amount", currency='currency', digits='currency', required=True)
|
|
currency = fields.Many2One('currency.currency', 'Currency', readonly=True)
|
|
description = fields.Char('Description', size=None)
|
|
company = fields.Many2One('company.company', "Company", readonly=True)
|
|
invoice_account = fields.Many2One(
|
|
'account.account', "Invoice Account", readonly=True)
|
|
payment_method = fields.Many2One(
|
|
'account.invoice.payment.method', "Payment Method", required=True,
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
('debit_account', '!=', Eval('invoice_account', -1)),
|
|
('credit_account', '!=', Eval('invoice_account', -1)),
|
|
],
|
|
depends={'amount'})
|
|
date = fields.Date('Date', required=True)
|
|
|
|
@staticmethod
|
|
def default_date():
|
|
Date = Pool().get('ir.date')
|
|
return Date.today()
|
|
|
|
|
|
class PayInvoiceAsk(ModelView):
|
|
__name__ = 'account.invoice.pay.ask'
|
|
type = fields.Selection([
|
|
('writeoff', "Write-Off"),
|
|
('partial', "Partial Payment"),
|
|
('overpayment', "Overpayment"),
|
|
], 'Type', required=True,
|
|
domain=[
|
|
If(Eval('amount_writeoff', 0) >= 0,
|
|
('type', 'in', ['writeoff', 'partial']),
|
|
()),
|
|
])
|
|
writeoff = fields.Many2One(
|
|
'account.move.reconcile.write_off', "Write Off",
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
],
|
|
states={
|
|
'invisible': Eval('type') != 'writeoff',
|
|
'required': Eval('type') == 'writeoff',
|
|
})
|
|
amount = Monetary(
|
|
"Payment Amount",
|
|
currency='currency', digits='currency', readonly=True)
|
|
currency = fields.Many2One('currency.currency', "Currency", readonly=True)
|
|
amount_writeoff = Monetary(
|
|
"Write-Off Amount",
|
|
currency='currency', digits='currency',
|
|
readonly=True,
|
|
states={
|
|
'invisible': ~Eval('type').in_(['writeoff', 'overpayment']),
|
|
})
|
|
lines_to_pay = fields.Many2Many('account.move.line', None, None,
|
|
'Lines to Pay', readonly=True)
|
|
lines = fields.Many2Many('account.move.line', None, None, 'Lines',
|
|
domain=[
|
|
('id', 'in', Eval('lines_to_pay')),
|
|
('reconciliation', '=', None),
|
|
],
|
|
states={
|
|
'invisible': ~Eval('type').in_(['writeoff', 'overpayment']),
|
|
'required': Eval('type').in_(['writeoff', 'overpayment']),
|
|
})
|
|
payment_lines = fields.Many2Many('account.move.line', None, None,
|
|
'Payment Lines', readonly=True,
|
|
states={
|
|
'invisible': ~Eval('type').in_(['writeoff', 'overpayment']),
|
|
})
|
|
company = fields.Many2One('company.company', 'Company', readonly=True)
|
|
invoice = fields.Many2One('account.invoice', 'Invoice', readonly=True)
|
|
|
|
@staticmethod
|
|
def default_type():
|
|
return 'partial'
|
|
|
|
@fields.depends(
|
|
'lines', 'amount', 'currency', 'invoice', 'payment_lines', 'company')
|
|
def on_change_lines(self):
|
|
self.amount_writeoff = Decimal(0)
|
|
if not self.invoice:
|
|
return
|
|
|
|
def balance(line):
|
|
if self.currency == line.second_currency:
|
|
return line.amount_second_currency
|
|
elif self.currency == self.company.currency:
|
|
return line.debit - line.credit
|
|
else:
|
|
return 0
|
|
|
|
for line in self.lines:
|
|
self.amount_writeoff += balance(line)
|
|
for line in self.payment_lines:
|
|
self.amount_writeoff += balance(line)
|
|
if self.invoice.type == 'in':
|
|
self.amount_writeoff = - self.amount_writeoff - self.amount
|
|
else:
|
|
self.amount_writeoff = self.amount_writeoff - self.amount
|
|
|
|
|
|
class PayInvoice(Wizard):
|
|
__name__ = 'account.invoice.pay'
|
|
start = StateView('account.invoice.pay.start',
|
|
'account_invoice.pay_start_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('OK', 'choice', 'tryton-ok', default=True),
|
|
])
|
|
choice = StateTransition()
|
|
ask = StateView('account.invoice.pay.ask',
|
|
'account_invoice.pay_ask_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('OK', 'pay', 'tryton-ok', default=True),
|
|
])
|
|
pay = StateTransition()
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.__rpc__['create'].fresh_session = True
|
|
|
|
def get_reconcile_lines_for_amount(self, invoice, amount, currency):
|
|
if invoice.type == 'in':
|
|
amount *= -1
|
|
return invoice.get_reconcile_lines_for_amount(
|
|
amount, currency, party=self.start.payee)
|
|
|
|
def default_start(self, fields):
|
|
default = {}
|
|
invoice = self.record
|
|
payee = None
|
|
if not invoice.alternative_payees:
|
|
payee = invoice.party
|
|
else:
|
|
try:
|
|
payee, = invoice.alternative_payees
|
|
except ValueError:
|
|
pass
|
|
if payee:
|
|
default['payee'] = payee.id
|
|
default['payees'] = (
|
|
[invoice.party.id] + [p.id for p in invoice.alternative_payees])
|
|
default['company'] = invoice.company.id
|
|
default['currency'] = invoice.currency.id
|
|
default['amount'] = (invoice.amount_to_pay_today
|
|
or invoice.amount_to_pay)
|
|
default['invoice_account'] = invoice.account.id
|
|
return default
|
|
|
|
def transition_choice(self):
|
|
invoice = self.record
|
|
amount = self.start.amount
|
|
currency = self.start.currency
|
|
_, remainder = self.get_reconcile_lines_for_amount(
|
|
invoice, amount, currency)
|
|
if remainder == Decimal(0) and amount <= invoice.amount_to_pay:
|
|
return 'pay'
|
|
return 'ask'
|
|
|
|
def default_ask(self, fields):
|
|
default = {}
|
|
invoice = self.record
|
|
amount = self.start.amount
|
|
currency = self.start.currency
|
|
default['lines_to_pay'] = [x.id for x in invoice.lines_to_pay
|
|
if not x.reconciliation]
|
|
|
|
default['amount'] = amount
|
|
default['currency'] = currency.id
|
|
default['company'] = invoice.company.id
|
|
|
|
if currency.is_zero(amount):
|
|
lines = invoice.lines_to_pay
|
|
else:
|
|
lines, _ = self.get_reconcile_lines_for_amount(
|
|
invoice, amount, currency)
|
|
default['lines'] = [x.id for x in lines]
|
|
|
|
for line_id in default['lines'][:]:
|
|
if line_id not in default['lines_to_pay']:
|
|
default['lines'].remove(line_id)
|
|
|
|
default['payment_lines'] = [x.id for x in invoice.payment_lines
|
|
if not x.reconciliation]
|
|
|
|
default['invoice'] = invoice.id
|
|
|
|
if amount >= invoice.amount_to_pay:
|
|
default['type'] = 'overpayment'
|
|
elif currency.is_zero(amount):
|
|
default['type'] = 'writeoff'
|
|
return default
|
|
|
|
def transition_pay(self):
|
|
pool = Pool()
|
|
MoveLine = pool.get('account.move.line')
|
|
Lang = pool.get('ir.lang')
|
|
|
|
invoice = self.record
|
|
amount = self.start.amount
|
|
currency = self.start.currency
|
|
|
|
reconcile_lines, remainder = (
|
|
self.get_reconcile_lines_for_amount(invoice, amount, currency))
|
|
|
|
overpayment = 0
|
|
if (0 <= invoice.amount_to_pay < amount
|
|
or amount < invoice.amount_to_pay <= 0):
|
|
if self.ask.type == 'partial':
|
|
lang = Lang.get()
|
|
raise PayInvoiceError(
|
|
gettext('account_invoice'
|
|
'.msg_invoice_pay_amount_greater_amount_to_pay',
|
|
invoice=invoice.rec_name,
|
|
amount_to_pay=lang.currency(
|
|
invoice.amount_to_pay, invoice.currency)))
|
|
else:
|
|
if not invoice.amount_to_pay:
|
|
raise PayInvoiceError(
|
|
gettext('account_invoice.msg_invoice_overpay_paid',
|
|
invoice=invoice.rec_name))
|
|
overpayment = amount - invoice.amount_to_pay
|
|
|
|
lines = []
|
|
if not currency.is_zero(amount):
|
|
lines = invoice.pay_invoice(
|
|
amount, self.start.payment_method, self.start.date,
|
|
self.start.description, overpayment, party=self.start.payee)
|
|
|
|
if remainder:
|
|
if self.ask.type != 'partial':
|
|
to_reconcile = {l for l in self.ask.lines}
|
|
to_reconcile.update(
|
|
l for l in invoice.payment_lines
|
|
if not l.reconciliation
|
|
and (not invoice.account.party_required
|
|
or l.party == self.start.payee))
|
|
if self.ask.type == 'writeoff':
|
|
to_reconcile.update(lines)
|
|
if to_reconcile:
|
|
MoveLine.reconcile(
|
|
to_reconcile,
|
|
writeoff=self.ask.writeoff,
|
|
date=self.start.date)
|
|
else:
|
|
reconcile_lines += lines
|
|
if reconcile_lines:
|
|
MoveLine.reconcile(reconcile_lines)
|
|
return 'end'
|
|
|
|
|
|
class CreditInvoiceStart(ModelView):
|
|
__name__ = 'account.invoice.credit.start'
|
|
invoice_date = fields.Date("Invoice Date")
|
|
with_refund = fields.Boolean('With Refund',
|
|
states={
|
|
'readonly': ~Eval('with_refund_allowed'),
|
|
'invisible': ~Eval('with_refund_allowed'),
|
|
},
|
|
help='If true, the current invoice(s) will be cancelled.')
|
|
with_refund_allowed = fields.Boolean("With Refund Allowed", readonly=True)
|
|
|
|
|
|
class CreditInvoice(Wizard):
|
|
__name__ = 'account.invoice.credit'
|
|
start = StateView('account.invoice.credit.start',
|
|
'account_invoice.credit_start_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Credit', 'credit', 'tryton-ok', default=True),
|
|
])
|
|
credit = StateAction('account_invoice.act_invoice_form')
|
|
|
|
def default_start(self, fields):
|
|
default = {
|
|
'with_refund': True,
|
|
'with_refund_allowed': True,
|
|
}
|
|
for invoice in self.records:
|
|
if invoice.state != 'posted' or invoice.type == 'in':
|
|
default['with_refund'] = False
|
|
default['with_refund_allowed'] = False
|
|
break
|
|
if invoice.payment_lines:
|
|
default['with_refund'] = False
|
|
return default
|
|
|
|
@property
|
|
def _credit_options(self):
|
|
return dict(
|
|
refund=self.start.with_refund,
|
|
invoice_date=self.start.invoice_date,
|
|
)
|
|
|
|
def do_credit(self, action):
|
|
credit_invoices = self.model.credit(
|
|
self.records, **self._credit_options)
|
|
|
|
data = {'res_id': [i.id for i in credit_invoices]}
|
|
if len(credit_invoices) == 1:
|
|
action['views'].reverse()
|
|
return action, data
|
|
|
|
|
|
class RescheduleLinesToPay(Wizard):
|
|
__name__ = 'account.invoice.lines_to_pay.reschedule'
|
|
start = StateAction('account.act_reschedule_lines_wizard')
|
|
|
|
def do_start(self, action):
|
|
return action, {
|
|
'ids': [
|
|
l.id for l in self.record.lines_to_pay
|
|
if not l.reconciliation],
|
|
'model': 'account.move.line',
|
|
}
|
|
|
|
|
|
class DelegateLinesToPay(Wizard):
|
|
__name__ = 'account.invoice.lines_to_pay.delegate'
|
|
start = StateAction('account.act_delegate_lines_wizard')
|
|
|
|
def do_start(self, action):
|
|
return action, {
|
|
'ids': [
|
|
l.id for l in self.record.lines_to_pay
|
|
if not l.reconciliation],
|
|
'model': 'account.move.line',
|
|
}
|