442 lines
15 KiB
Python
442 lines
15 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.
|
|
from collections import OrderedDict
|
|
from itertools import islice
|
|
|
|
from trytond.i18n import gettext
|
|
from trytond.model import (
|
|
MatchMixin, ModelSQL, ModelView, Workflow, fields, sequence_ordered)
|
|
from trytond.modules.account.exceptions import ClosePeriodError
|
|
from trytond.modules.company.model import CompanyValueMixin
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.pyson import Bool, Eval, Id, If
|
|
from trytond.tools import grouped_slice
|
|
from trytond.transaction import Transaction
|
|
|
|
from .exceptions import CancelInvoiceMoveWarning
|
|
|
|
|
|
class Configuration(metaclass=PoolMeta):
|
|
__name__ = 'account.configuration'
|
|
|
|
default_customer_payment_term = fields.MultiValue(
|
|
fields.Many2One(
|
|
'account.invoice.payment_term', "Default Customer Payment Term"))
|
|
customer_payment_reference_number = fields.MultiValue(
|
|
fields.Selection('get_customer_payment_references',
|
|
"Customer Payment Reference Number",
|
|
help="The number used to generate "
|
|
"the customer payment reference."))
|
|
|
|
@classmethod
|
|
def multivalue_model(cls, field):
|
|
pool = Pool()
|
|
if field in 'default_customer_payment_term':
|
|
return pool.get('account.configuration.default_payment_term')
|
|
elif field == 'customer_payment_reference_number':
|
|
return pool.get('account.configuration.payment_reference')
|
|
return super().multivalue_model(field)
|
|
|
|
@classmethod
|
|
def get_customer_payment_references(cls):
|
|
pool = Pool()
|
|
PaymentReference = pool.get('account.configuration.payment_reference')
|
|
field = 'customer_payment_reference_number'
|
|
return PaymentReference.fields_get([field])[field]['selection']
|
|
|
|
@classmethod
|
|
def default_customer_payment_reference_number(cls, **pattern):
|
|
pool = Pool()
|
|
PaymentReference = pool.get('account.configuration.payment_reference')
|
|
return PaymentReference.default_customer_payment_reference_number()
|
|
|
|
|
|
class ConfigurationDefaultPaymentTerm(ModelSQL, CompanyValueMixin):
|
|
__name__ = 'account.configuration.default_payment_term'
|
|
|
|
default_customer_payment_term = fields.Many2One(
|
|
'account.invoice.payment_term', "Default Customer Payment Term")
|
|
|
|
|
|
class ConfigurationPaymentReference(ModelSQL, CompanyValueMixin):
|
|
__name__ = 'account.configuration.payment_reference'
|
|
|
|
customer_payment_reference_number = fields.Selection([
|
|
('invoice', "Invoice"),
|
|
('party', "Party"),
|
|
], "Customer Payment Reference Number")
|
|
|
|
@classmethod
|
|
def default_customer_payment_reference_number(cls):
|
|
return 'invoice'
|
|
|
|
|
|
class FiscalYear(metaclass=PoolMeta):
|
|
__name__ = 'account.fiscalyear'
|
|
invoice_sequences = fields.One2Many(
|
|
'account.fiscalyear.invoice_sequence', 'fiscalyear',
|
|
"Invoice Sequences",
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
])
|
|
|
|
@staticmethod
|
|
def default_invoice_sequences():
|
|
if Transaction().user == 0:
|
|
return []
|
|
return [{}]
|
|
|
|
|
|
class Period(metaclass=PoolMeta):
|
|
__name__ = 'account.period'
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('closed')
|
|
def close(cls, periods):
|
|
pool = Pool()
|
|
Invoice = pool.get('account.invoice')
|
|
company_ids = list({p.company.id for p in periods})
|
|
invoices = Invoice.search([
|
|
('company', 'in', company_ids),
|
|
('state', '=', 'posted'),
|
|
('move', '=', None),
|
|
])
|
|
if invoices:
|
|
names = ', '.join(i.rec_name for i in invoices[:5])
|
|
if len(invoices) > 5:
|
|
names += '...'
|
|
raise ClosePeriodError(
|
|
gettext('account_invoice.msg_close_period_non_posted_invoices',
|
|
invoices=names))
|
|
super().close(periods)
|
|
|
|
|
|
class InvoiceSequence(sequence_ordered(), ModelSQL, ModelView, MatchMixin):
|
|
__name__ = 'account.fiscalyear.invoice_sequence'
|
|
company = fields.Many2One('company.company', "Company", required=True)
|
|
fiscalyear = fields.Many2One(
|
|
'account.fiscalyear', "Fiscal Year", required=True, ondelete='CASCADE',
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
])
|
|
period = fields.Many2One('account.period', 'Period',
|
|
domain=[
|
|
('fiscalyear', '=', Eval('fiscalyear', -1)),
|
|
('type', '=', 'standard'),
|
|
])
|
|
in_invoice_sequence = fields.Many2One('ir.sequence.strict',
|
|
'Supplier Invoice Sequence', required=True,
|
|
domain=[
|
|
('sequence_type', '=',
|
|
Id('account_invoice', 'sequence_type_account_invoice')),
|
|
('company', '=', Eval('company', -1)),
|
|
])
|
|
in_credit_note_sequence = fields.Many2One('ir.sequence.strict',
|
|
'Supplier Credit Note Sequence', required=True,
|
|
domain=[
|
|
('sequence_type', '=',
|
|
Id('account_invoice', 'sequence_type_account_invoice')),
|
|
('company', '=', Eval('company', -1)),
|
|
])
|
|
out_invoice_sequence = fields.Many2One('ir.sequence.strict',
|
|
'Customer Invoice Sequence', required=True,
|
|
domain=[
|
|
('sequence_type', '=',
|
|
Id('account_invoice', 'sequence_type_account_invoice')),
|
|
('company', '=', Eval('company', -1)),
|
|
])
|
|
out_credit_note_sequence = fields.Many2One('ir.sequence.strict',
|
|
'Customer Credit Note Sequence', required=True,
|
|
domain=[
|
|
('sequence_type', '=',
|
|
Id('account_invoice', 'sequence_type_account_invoice')),
|
|
('company', '=', Eval('company', -1)),
|
|
])
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._order.insert(0, ('fiscalyear', 'ASC'))
|
|
|
|
@classmethod
|
|
def default_company(cls):
|
|
return Transaction().context.get('company')
|
|
|
|
|
|
class Move(metaclass=PoolMeta):
|
|
__name__ = 'account.move'
|
|
|
|
@classmethod
|
|
def _get_origin(cls):
|
|
return super()._get_origin() + ['account.invoice']
|
|
|
|
|
|
class MoveLine(metaclass=PoolMeta):
|
|
__name__ = 'account.move.line'
|
|
|
|
invoice_payment = fields.Function(fields.Many2One(
|
|
'account.invoice', "Invoice Payment",
|
|
domain=[
|
|
('account', '=', Eval('account', -1)),
|
|
If(Bool(Eval('party')),
|
|
('party', '=', Eval('party', -1)),
|
|
(),
|
|
),
|
|
],
|
|
states={
|
|
'invisible': Bool(Eval('reconciliation')),
|
|
}),
|
|
'get_invoice_payment',
|
|
setter='set_invoice_payment',
|
|
searcher='search_invoice_payment')
|
|
invoice_payments = fields.Many2Many(
|
|
'account.invoice-account.move.line', 'line', 'invoice',
|
|
"Invoice Payments", readonly=True)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._check_modify_exclude.add('invoice_payment')
|
|
|
|
@classmethod
|
|
def _view_reconciliation_muted(cls):
|
|
pool = Pool()
|
|
ModelData = pool.get('ir.model.data')
|
|
muted = super()._view_reconciliation_muted()
|
|
muted.add(ModelData.get_id(
|
|
'account_invoice', 'move_line_view_list_to_pay'))
|
|
return muted
|
|
|
|
@classmethod
|
|
def _get_origin(cls):
|
|
return super()._get_origin() + [
|
|
'account.invoice.line', 'account.invoice.tax']
|
|
|
|
@classmethod
|
|
def copy(cls, lines, default=None):
|
|
default = {} if default is None else default.copy()
|
|
default.setdefault('invoice_payments', None)
|
|
return super().copy(lines, default=default)
|
|
|
|
@classmethod
|
|
def get_invoice_payment(cls, lines, name):
|
|
pool = Pool()
|
|
InvoicePaymentLine = pool.get('account.invoice-account.move.line')
|
|
|
|
ids = list(map(int, lines))
|
|
result = dict.fromkeys(ids, None)
|
|
for sub_ids in grouped_slice(ids):
|
|
payment_lines = InvoicePaymentLine.search([
|
|
('line', 'in', list(sub_ids)),
|
|
])
|
|
result.update({p.line.id: p.invoice.id for p in payment_lines})
|
|
return result
|
|
|
|
@classmethod
|
|
def set_invoice_payment(cls, lines, name, value):
|
|
pool = Pool()
|
|
Invoice = pool.get('account.invoice')
|
|
Invoice.remove_payment_lines(lines)
|
|
if value:
|
|
Invoice.add_payment_lines({Invoice(value): lines})
|
|
|
|
@classmethod
|
|
def search_invoice_payment(cls, name, clause):
|
|
nested = clause[0][len(name):]
|
|
return [('invoice_payments' + nested, *clause[1:])]
|
|
|
|
@property
|
|
def product(self):
|
|
pool = Pool()
|
|
InvoiceLine = pool.get('account.invoice.line')
|
|
product = super().product
|
|
if (isinstance(self.origin, InvoiceLine)
|
|
and self.origin.product):
|
|
product = self.origin.product
|
|
return product
|
|
|
|
|
|
def _invoices_to_process(reconciliations):
|
|
pool = Pool()
|
|
Reconciliation = pool.get('account.move.reconciliation')
|
|
Invoice = pool.get('account.invoice')
|
|
|
|
move_ids = set()
|
|
others = set()
|
|
for reconciliation in reconciliations:
|
|
for line in reconciliation.lines:
|
|
move_ids.add(line.move.id)
|
|
others.update(line.reconciliations_delegated)
|
|
|
|
invoices = set()
|
|
for sub_ids in grouped_slice(move_ids):
|
|
sub_ids = list(sub_ids)
|
|
invoices.update(Invoice.search(['OR',
|
|
('move', 'in', sub_ids),
|
|
('additional_moves', 'in', sub_ids),
|
|
]))
|
|
if others:
|
|
invoices.update(_invoices_to_process(Reconciliation.browse(others)))
|
|
|
|
return invoices
|
|
|
|
|
|
class Reconciliation(metaclass=PoolMeta):
|
|
__name__ = 'account.move.reconciliation'
|
|
|
|
@classmethod
|
|
def on_modification(cls, mode, reconciliations, field_names=None):
|
|
pool = Pool()
|
|
Invoice = pool.get('account.invoice')
|
|
transaction = Transaction()
|
|
context = transaction.context
|
|
|
|
super().on_modification(mode, reconciliations, field_names=field_names)
|
|
|
|
with transaction.set_context(
|
|
queue_batch=context.get('queue_batch', True)):
|
|
Invoice.__queue__.process(
|
|
list(_invoices_to_process(reconciliations)))
|
|
|
|
|
|
class RenewFiscalYear(metaclass=PoolMeta):
|
|
__name__ = 'account.fiscalyear.renew'
|
|
|
|
def fiscalyear_defaults(self):
|
|
defaults = super().fiscalyear_defaults()
|
|
defaults['invoice_sequences'] = None
|
|
return defaults
|
|
|
|
@property
|
|
def invoice_sequence_fields(self):
|
|
return ['out_invoice_sequence', 'out_credit_note_sequence',
|
|
'in_invoice_sequence', 'in_credit_note_sequence']
|
|
|
|
def create_fiscalyear(self):
|
|
pool = Pool()
|
|
Sequence = pool.get('ir.sequence.strict')
|
|
InvoiceSequence = pool.get('account.fiscalyear.invoice_sequence')
|
|
fiscalyear = super().create_fiscalyear()
|
|
|
|
def standard_period(period):
|
|
return period.type == 'standard'
|
|
|
|
period_mapping = {}
|
|
for previous, new in zip(
|
|
filter(
|
|
standard_period, self.start.previous_fiscalyear.periods),
|
|
filter(standard_period, fiscalyear.periods)):
|
|
period_mapping[previous] = new.id
|
|
|
|
InvoiceSequence.copy(
|
|
self.start.previous_fiscalyear.invoice_sequences,
|
|
default={
|
|
'fiscalyear': fiscalyear.id,
|
|
'period': lambda data: period_mapping.get(data['period']),
|
|
})
|
|
|
|
if not self.start.reset_sequences:
|
|
return fiscalyear
|
|
sequences = OrderedDict()
|
|
for invoice_sequence in fiscalyear.invoice_sequences:
|
|
for field in self.invoice_sequence_fields:
|
|
sequence = getattr(invoice_sequence, field, None)
|
|
sequences[sequence.id] = sequence
|
|
copies = Sequence.copy(list(sequences.values()), default={
|
|
'name': lambda data: data['name'].replace(
|
|
self.start.previous_fiscalyear.name,
|
|
self.start.name)
|
|
})
|
|
Sequence.write(copies, {
|
|
'number_next': Sequence.default_number_next(),
|
|
})
|
|
|
|
mapping = {}
|
|
for previous_id, new_sequence in zip(sequences.keys(), copies):
|
|
mapping[previous_id] = new_sequence.id
|
|
to_write = []
|
|
for new_sequence, old_sequence in zip(
|
|
fiscalyear.invoice_sequences,
|
|
self.start.previous_fiscalyear.invoice_sequences):
|
|
values = {}
|
|
for field in self.invoice_sequence_fields:
|
|
sequence = getattr(old_sequence, field, None)
|
|
values[field] = mapping[sequence.id]
|
|
to_write.extend(([new_sequence], values))
|
|
if to_write:
|
|
InvoiceSequence.write(*to_write)
|
|
return fiscalyear
|
|
|
|
|
|
class RescheduleLines(metaclass=PoolMeta):
|
|
__name__ = 'account.move.line.reschedule'
|
|
|
|
@classmethod
|
|
def reschedule_lines(cls, lines, journal, terms):
|
|
pool = Pool()
|
|
Invoice = pool.get('account.invoice')
|
|
move, balance_line = super().reschedule_lines(lines, journal, terms)
|
|
|
|
move_ids = list({l.move.id for l in lines})
|
|
invoices = Invoice.search(['OR',
|
|
('move', 'in', move_ids),
|
|
('additional_moves', 'in', move_ids),
|
|
])
|
|
Invoice.write(invoices, {
|
|
'additional_moves': [('add', [move.id])],
|
|
})
|
|
return move, balance_line
|
|
|
|
|
|
class DelegateLines(metaclass=PoolMeta):
|
|
__name__ = 'account.move.line.delegate'
|
|
|
|
@classmethod
|
|
def delegate_lines(cls, lines, party, journal, date=None):
|
|
pool = Pool()
|
|
Invoice = pool.get('account.invoice')
|
|
move = super().delegate_lines(lines, party, journal, date=None)
|
|
|
|
move_ids = list({l.move.id for l in lines})
|
|
invoices = Invoice.search(['OR',
|
|
('move', 'in', move_ids),
|
|
('additional_moves', 'in', move_ids),
|
|
])
|
|
Invoice.write(invoices, {
|
|
'alternative_payees': [('add', [party.id])],
|
|
'additional_moves': [('add', [move.id])],
|
|
})
|
|
return move
|
|
|
|
|
|
class CancelMoves(metaclass=PoolMeta):
|
|
__name__ = 'account.move.cancel'
|
|
|
|
def transition_cancel(self):
|
|
pool = Pool()
|
|
Invoice = pool.get('account.invoice')
|
|
Warning = pool.get('res.user.warning')
|
|
|
|
moves_w_invoices = {
|
|
m: m.origin for m in self.records
|
|
if (isinstance(m.origin, Invoice)
|
|
and m.origin.state not in {'paid', 'cancelled'})}
|
|
if moves_w_invoices:
|
|
move_names = ', '.join(m.rec_name
|
|
for m in islice(moves_w_invoices, None, 5))
|
|
invoice_names = ', '.join(i.rec_name
|
|
for i in islice(moves_w_invoices.values(), None, 5))
|
|
if len(moves_w_invoices) > 5:
|
|
move_names += '...'
|
|
invoice_names += '...'
|
|
key = Warning.format('cancel_invoice_move', moves_w_invoices)
|
|
if Warning.check(key):
|
|
raise CancelInvoiceMoveWarning(key,
|
|
gettext('account_invoice.msg_cancel_invoice_move',
|
|
moves=move_names, invoices=invoice_names),
|
|
gettext(
|
|
'account_invoice.msg_cancel_invoice_move_description'))
|
|
|
|
return super().transition_cancel()
|