The post method can be called multiple times for an invoice by process but only one deferral is allowed by line. Also the initial draft deferral may be delete in case of mistake so we must not create automatically a new one later. Closes #12567 (grafted from 7c72d42d78d92db1fbe36217d3ba4720bb621521) --HG-- branch : 6.0
501 lines
18 KiB
Python
501 lines
18 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 trytond.i18n import gettext
|
|
from trytond.model import ModelSQL, ModelView, Workflow, fields, Unique
|
|
from trytond.model.exceptions import AccessError
|
|
from trytond.pool import PoolMeta, Pool
|
|
from trytond.pyson import Eval, If
|
|
from trytond.transaction import Transaction
|
|
from trytond.wizard import Wizard, StateTransition
|
|
|
|
|
|
class Configuration(metaclass=PoolMeta):
|
|
__name__ = 'account.configuration'
|
|
|
|
deferred_account_revenue = fields.MultiValue(fields.Many2One(
|
|
'account.account', "Deferred Account Revenue",
|
|
domain=[
|
|
('type.statement', '=', 'balance'),
|
|
('company', '=', Eval('context', {}).get('company', -1)),
|
|
]))
|
|
deferred_account_expense = fields.MultiValue(fields.Many2One(
|
|
'account.account', "Deferred Account Expense",
|
|
domain=[
|
|
('type.statement', '=', 'balance'),
|
|
('company', '=', Eval('context', {}).get('company', -1)),
|
|
]))
|
|
|
|
@classmethod
|
|
def multivalue_model(cls, field):
|
|
pool = Pool()
|
|
if field in {'deferred_account_revenue', 'deferred_account_expense'}:
|
|
return pool.get('account.configuration.default_account')
|
|
return super().multivalue_model(field)
|
|
|
|
|
|
class ConfigurationDefaultAccount(metaclass=PoolMeta):
|
|
__name__ = 'account.configuration.default_account'
|
|
|
|
deferred_account_revenue = fields.Many2One(
|
|
'account.account', "Deferred Account Revenue",
|
|
domain=[
|
|
('type.statement', '=', 'balance'),
|
|
('company', '=', Eval('company', -1)),
|
|
],
|
|
depends=['company'])
|
|
deferred_account_expense = fields.Many2One(
|
|
'account.account', "Deferred Account Expense",
|
|
domain=[
|
|
('type.statement', '=', 'balance'),
|
|
('company', '=', Eval('company', -1)),
|
|
],
|
|
depends=['company'])
|
|
|
|
|
|
class InvoiceDeferred(Workflow, ModelSQL, ModelView):
|
|
"Invoice Deferred"
|
|
__name__ = 'account.invoice.deferred'
|
|
|
|
_states = {
|
|
'readonly': Eval('state') != 'draft',
|
|
}
|
|
_depends = ['state']
|
|
|
|
company = fields.Many2One(
|
|
'company.company', "Company", required=True,
|
|
states={
|
|
'readonly': (Eval('state') != 'draft') & Eval('invoice_line'),
|
|
},
|
|
depends=['state', 'invoice_line'])
|
|
type = fields.Selection([
|
|
('out', "Customer"),
|
|
('in', "Supplier"),
|
|
], "Type", required=True,
|
|
states=_states, depends=_depends)
|
|
journal = fields.Many2One(
|
|
'account.journal', "Journal", required=True,
|
|
states=_states,
|
|
context={
|
|
'company': Eval('company', -1),
|
|
},
|
|
depends=_depends + ['company'])
|
|
invoice_line = fields.Many2One(
|
|
'account.invoice.line', "Invoice Line", required=True,
|
|
domain=[
|
|
('product.type', '=', 'service'),
|
|
('invoice.type', '=', Eval('type')),
|
|
('invoice.state', 'in', ['posted', 'paid']),
|
|
('invoice.company', '=', Eval('company', -1)),
|
|
],
|
|
states=_states, depends=_depends + ['type', 'company'])
|
|
amount = fields.Numeric(
|
|
"Amount", digits=(16, Eval('currency_digits', 2)), required=True,
|
|
states=_states, depends=_depends + ['currency_digits'])
|
|
start_date = fields.Date(
|
|
"Start Date", required=True,
|
|
domain=[
|
|
('start_date', '<', Eval('end_date', None)),
|
|
],
|
|
states=_states, depends=_depends + ['end_date'])
|
|
end_date = fields.Date(
|
|
"End Date", required=True,
|
|
domain=[
|
|
('end_date', '>', Eval('start_date', None)),
|
|
],
|
|
states=_states, depends=_depends + ['start_date'])
|
|
moves = fields.One2Many(
|
|
'account.move', 'origin', "Moves", readonly=True,
|
|
order=[
|
|
('period.start_date', 'ASC'),
|
|
])
|
|
state = fields.Selection([
|
|
('draft', "Draft"),
|
|
('running', "Running"),
|
|
('closed', "Closed"),
|
|
], "State", readonly=True, required=True)
|
|
|
|
currency = fields.Function(fields.Many2One(
|
|
'currency.currency', "Currency"), 'on_change_with_currency')
|
|
currency_digits = fields.Function(fields.Integer("Currency Digits"),
|
|
'on_change_with_currency_digits')
|
|
|
|
del _states, _depends
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
table = cls.__table__()
|
|
cls._sql_constraints = [
|
|
('invoice_line_unique', Unique(table, table.invoice_line),
|
|
'account_invoice_defer.msg_defer_invoice_line_unique'),
|
|
]
|
|
cls.journal.domain = [
|
|
If(Eval('type') == 'out',
|
|
('type', 'in', cls._journal_types('out')),
|
|
('type', 'in', cls._journal_types('in'))),
|
|
]
|
|
cls.journal.depends += ['type']
|
|
cls._transitions |= set((
|
|
('draft', 'running'),
|
|
('running', 'closed'),
|
|
))
|
|
cls._buttons.update({
|
|
'run': {
|
|
'invisible': Eval('state') != 'draft',
|
|
'depends': ['state'],
|
|
},
|
|
})
|
|
|
|
@classmethod
|
|
def _journal_types(cls, type):
|
|
if type == 'out':
|
|
return ['revenue']
|
|
else:
|
|
return ['expense']
|
|
|
|
@fields.depends('type')
|
|
def on_change_type(self):
|
|
Journal = self.__class__.journal.get_target()
|
|
journal_types = self.__class__._journal_types(self.type)
|
|
journals = Journal.search([
|
|
('type', 'in', journal_types),
|
|
], limit=1)
|
|
if journals:
|
|
self.journal, = journals
|
|
|
|
@fields.depends('invoice_line', 'start_date', 'company')
|
|
def on_change_invoice_line(self):
|
|
pool = Pool()
|
|
Currency = pool.get('currency.currency')
|
|
if self.invoice_line:
|
|
if not self.start_date:
|
|
self.start_date = self.invoice_line.invoice.invoice_date
|
|
invoice = self.invoice_line.invoice
|
|
if self.company and invoice.currency != self.company.currency:
|
|
with Transaction().set_context(date=invoice.currency_date):
|
|
self.amount = Currency.compute(
|
|
invoice.currency, self.invoice_line.amount,
|
|
self.company.currency)
|
|
else:
|
|
self.amount = self.invoice_line.amount
|
|
|
|
@classmethod
|
|
def default_state(cls):
|
|
return 'draft'
|
|
|
|
@fields.depends('company')
|
|
def on_change_with_currency(self, name=None):
|
|
if self.company:
|
|
return self.company.currency.id
|
|
|
|
@fields.depends('company')
|
|
def on_change_with_currency_digits(self, name=None):
|
|
if self.company:
|
|
return self.company.currency.digits
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('running')
|
|
def run(cls, deferrals):
|
|
pool = Pool()
|
|
Period = pool.get('account.period')
|
|
# Ensure it starts at an opened period
|
|
for deferral in deferrals:
|
|
Period.find(deferral.company.id, deferral.start_date)
|
|
# Set state before create moves and defer amount to pass assert
|
|
cls.write(deferrals, {'state': 'running'})
|
|
cls.create_moves(deferrals)
|
|
# defer_amount is called after create_moves to be sure that
|
|
# create_moves call get_move with the invoice period if needed.
|
|
cls.defer_amount(deferrals)
|
|
cls.close_try(deferrals)
|
|
|
|
@classmethod
|
|
def close_try(cls, deferrals):
|
|
"Try to close the deferrals if last move has been created"
|
|
to_close = []
|
|
for deferral in deferrals:
|
|
if deferral.moves:
|
|
last_move = deferral.moves[-1]
|
|
if last_move.period.end_date >= deferral.end_date:
|
|
to_close.append(deferral)
|
|
cls.close(to_close)
|
|
|
|
@classmethod
|
|
@Workflow.transition('closed')
|
|
def close(cls, deferrals):
|
|
for deferral in deferrals:
|
|
assert (deferral.moves
|
|
and deferral.moves[-1].period.end_date >= deferral.end_date)
|
|
|
|
@classmethod
|
|
def delete(cls, deferrals):
|
|
for deferral in deferrals:
|
|
if deferral.state != 'draft':
|
|
raise AccessError(
|
|
gettext('account_invoice_defer'
|
|
'.msg_invoice_deferred_delete_draft',
|
|
deferral=deferral.rec_name))
|
|
return super().delete(deferrals)
|
|
|
|
@classmethod
|
|
def defer_amount(cls, deferrals):
|
|
pool = Pool()
|
|
Move = pool.get('account.move')
|
|
moves = []
|
|
for deferral in deferrals:
|
|
assert deferral.state == 'running'
|
|
moves.append(deferral.get_move())
|
|
Move.save(moves)
|
|
Move.post(moves)
|
|
|
|
@classmethod
|
|
def create_moves(cls, deferrals):
|
|
pool = Pool()
|
|
Period = pool.get('account.period')
|
|
Move = pool.get('account.move')
|
|
moves = []
|
|
for deferral in deferrals:
|
|
assert deferral.state == 'running'
|
|
periods = Period.search([
|
|
('company', '=', deferral.company.id),
|
|
('type', '=', 'standard'),
|
|
('start_date', '<=', deferral.end_date),
|
|
('end_date', '>=', deferral.start_date),
|
|
])
|
|
for period in sorted(
|
|
set(periods) - {m.period for m in deferral.moves},
|
|
key=lambda p: p.start_date):
|
|
moves.append(deferral.get_move(period))
|
|
Move.save(moves)
|
|
to_save = []
|
|
for deferral in deferrals:
|
|
if deferral.moves:
|
|
last_move = deferral.moves[-1]
|
|
if last_move.period.end_date >= deferral.end_date:
|
|
remainder = deferral.amount_remainder
|
|
if remainder:
|
|
for line in last_move.lines:
|
|
if line.debit:
|
|
line.debit -= remainder
|
|
else:
|
|
line.credit -= remainder
|
|
last_move.lines = last_move.lines
|
|
to_save.append(last_move)
|
|
Move.save(to_save)
|
|
Move.post(moves)
|
|
|
|
@property
|
|
def amount_daily(self):
|
|
days = (self.end_date - self.start_date).days + 1
|
|
return self.amount / days
|
|
|
|
@property
|
|
def amount_remainder(self):
|
|
balance = 0
|
|
for move in self.moves:
|
|
income_account = self.invoice_line.account.current(move.date)
|
|
for line in move.lines:
|
|
if line.account == income_account:
|
|
balance += line.debit - line.credit
|
|
return balance
|
|
|
|
def get_move(self, period=None):
|
|
pool = Pool()
|
|
Move = pool.get('account.move')
|
|
Line = pool.get('account.move.line')
|
|
Configuration = pool.get('account.configuration')
|
|
configuration = Configuration(1)
|
|
move = Move(
|
|
company=self.company,
|
|
origin=self,
|
|
journal=self.journal,
|
|
)
|
|
invoice = self.invoice_line.invoice
|
|
|
|
income = Line()
|
|
if period is None:
|
|
move.period = invoice.move.period
|
|
move.date = invoice.move.date
|
|
amount = self.amount
|
|
if amount >= 0:
|
|
if invoice.type == 'out':
|
|
income.debit, income.credit = amount, 0
|
|
else:
|
|
income.debit, income.credit = 0, amount
|
|
else:
|
|
if invoice.type == 'out':
|
|
income.debit, income.credit = 0, -amount
|
|
else:
|
|
income.debit, income.credit = -amount, 0
|
|
else:
|
|
move.period = period
|
|
move.date = period.start_date
|
|
days = (
|
|
min(period.end_date, self.end_date)
|
|
- max(period.start_date, self.start_date)).days + 1
|
|
amount = self.company.currency.round(self.amount_daily * days)
|
|
if amount >= 0:
|
|
if invoice.type == 'out':
|
|
income.debit, income.credit = 0, amount
|
|
else:
|
|
income.debit, income.credit = amount, 0
|
|
else:
|
|
if invoice.type == 'out':
|
|
income.debit, income.credit = -amount, 0
|
|
else:
|
|
income.debit, income.credit = 0, -amount
|
|
income.account = self.invoice_line.account.current(move.date)
|
|
if income.account.party_required:
|
|
income.party = invoice.party
|
|
|
|
balance = Line()
|
|
if invoice.type == 'out':
|
|
balance.account = configuration.get_multivalue(
|
|
'deferred_account_revenue', company=self.company.id)
|
|
else:
|
|
balance.account = configuration.get_multivalue(
|
|
'deferred_account_expense', company=self.company.id)
|
|
balance.debit, balance.credit = income.credit, income.debit
|
|
if balance.account.party_required:
|
|
balance.party = invoice.party
|
|
|
|
move.lines = [balance, income]
|
|
return move
|
|
|
|
|
|
class InvoiceDeferredCreateMoves(Wizard):
|
|
"Invoice Deferred Create Moves"
|
|
__name__ = 'account.invoice.deferred.create_moves'
|
|
start_state = 'create_moves'
|
|
create_moves = StateTransition()
|
|
|
|
def transition_create_moves(self):
|
|
pool = Pool()
|
|
InvoiceDeferred = pool.get('account.invoice.deferred')
|
|
with Transaction().set_context(_check_access=True):
|
|
deferrals = InvoiceDeferred.search([
|
|
('state', '=', 'running'),
|
|
])
|
|
deferrals = InvoiceDeferred.browse(deferrals)
|
|
InvoiceDeferred.create_moves(deferrals)
|
|
InvoiceDeferred.close_try(deferrals)
|
|
return 'end'
|
|
|
|
|
|
class Move(metaclass=PoolMeta):
|
|
__name__ = 'account.move'
|
|
|
|
@classmethod
|
|
def _get_origin(cls):
|
|
return super()._get_origin() + ['account.invoice.deferred']
|
|
|
|
|
|
class Period(metaclass=PoolMeta):
|
|
__name__ = 'account.period'
|
|
|
|
@classmethod
|
|
def close(cls, periods):
|
|
for period in periods:
|
|
period.check_invoice_deferred_running()
|
|
super().close(periods)
|
|
|
|
def check_invoice_deferred_running(self):
|
|
"""
|
|
Check if it exists any invoice deferred
|
|
without account move for the period.
|
|
"""
|
|
pool = Pool()
|
|
InvoiceDeferred = pool.get('account.invoice.deferred')
|
|
deferrals = InvoiceDeferred.search([
|
|
('state', '=', 'running'),
|
|
('company', '=', self.company.id),
|
|
['OR', [
|
|
('start_date', '<=', self.start_date),
|
|
('end_date', '>=', self.start_date),
|
|
], [
|
|
('start_date', '<=', self.end_date),
|
|
('end_date', '>=', self.end_date),
|
|
], [
|
|
('start_date', '>=', self.start_date),
|
|
('end_date', '<=', self.end_date),
|
|
],
|
|
],
|
|
('moves', 'not where', [
|
|
('date', '>=', self.start_date),
|
|
('date', '<=', self.end_date),
|
|
]),
|
|
], limit=6)
|
|
if deferrals:
|
|
names = ', '.join(d.rec_name for d in deferrals[:5])
|
|
if len(deferrals) > 5:
|
|
names += '...'
|
|
raise AccessError(
|
|
gettext('account_invoice_defer'
|
|
'.msg_invoice_deferred_running_close_period',
|
|
period=self.rec_name,
|
|
deferrals=names))
|
|
|
|
|
|
class Invoice(metaclass=PoolMeta):
|
|
__name__ = 'account.invoice'
|
|
|
|
@classmethod
|
|
def _post(cls, invoices):
|
|
pool = Pool()
|
|
InvoiceDeferred = pool.get('account.invoice.deferred')
|
|
# defer invoices only the first time post is called
|
|
invoices_to_defer = [i for i in invoices if not i.move]
|
|
super()._post(invoices)
|
|
deferrals = []
|
|
for invoice in invoices_to_defer:
|
|
for line in invoice.lines:
|
|
if line.deferrable and line.defer_from and line.defer_to:
|
|
deferral = InvoiceDeferred(
|
|
company=invoice.company,
|
|
type=invoice.type,
|
|
journal=invoice.journal,
|
|
invoice_line=line,
|
|
start_date=line.defer_from,
|
|
end_date=line.defer_to)
|
|
deferral.on_change_invoice_line()
|
|
deferrals.append(deferral)
|
|
InvoiceDeferred.save(deferrals)
|
|
|
|
|
|
class InvoiceLine(metaclass=PoolMeta):
|
|
__name__ = 'account.invoice.line'
|
|
|
|
deferrable = fields.Function(
|
|
fields.Boolean("Deferrable"), 'on_change_with_deferrable')
|
|
defer_from = fields.Date(
|
|
"Defer From",
|
|
domain=[
|
|
If(Eval('deferrable', False) & Eval('defer_to', None),
|
|
('defer_from', '<', Eval('defer_to', None)),
|
|
()),
|
|
],
|
|
states={
|
|
'readonly': Eval('invoice_state') != 'draft',
|
|
'invisible': ~Eval('deferrable', False),
|
|
},
|
|
depends=['deferrable', 'defer_to', 'invoice_state'])
|
|
defer_to = fields.Date(
|
|
"Defer To",
|
|
domain=[
|
|
If(Eval('deferrable', False) & Eval('defer_from', None),
|
|
('defer_to', '>', Eval('defer_from', None)),
|
|
()),
|
|
],
|
|
states={
|
|
'readonly': Eval('invoice_state') != 'draft',
|
|
'invisible': ~Eval('deferrable', False),
|
|
},
|
|
depends=['deferrable', 'defer_from', 'invoice_state'])
|
|
|
|
@fields.depends('product')
|
|
def on_change_with_deferrable(self, name=None):
|
|
if self.product:
|
|
return self.product.type == 'service'
|