first commit
This commit is contained in:
526
modules/account_invoice_defer/account.py
Normal file
526
modules/account_invoice_defer/account.py
Normal file
@@ -0,0 +1,526 @@
|
||||
# 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 Index, ModelSQL, ModelView, Unique, Workflow, fields
|
||||
from trytond.model.exceptions import AccessError
|
||||
from trytond.modules.account.exceptions import AccountMissing
|
||||
from trytond.modules.currency.fields import Monetary
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
from trytond.pyson import Eval, If
|
||||
from trytond.transaction import Transaction, check_access
|
||||
from trytond.wizard import StateTransition, Wizard
|
||||
|
||||
|
||||
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)),
|
||||
])
|
||||
deferred_account_expense = fields.Many2One(
|
||||
'account.account', "Deferred Account Expense",
|
||||
domain=[
|
||||
('type.statement', '=', 'balance'),
|
||||
('company', '=', Eval('company', -1)),
|
||||
])
|
||||
|
||||
|
||||
class InvoiceDeferred(Workflow, ModelSQL, ModelView):
|
||||
__name__ = 'account.invoice.deferred'
|
||||
|
||||
_states = {
|
||||
'readonly': Eval('state') != 'draft',
|
||||
}
|
||||
|
||||
company = fields.Many2One(
|
||||
'company.company', "Company", required=True,
|
||||
states={
|
||||
'readonly': (Eval('state') != 'draft') & Eval('invoice_line'),
|
||||
})
|
||||
type = fields.Selection([
|
||||
('out', "Customer"),
|
||||
('in', "Supplier"),
|
||||
], "Type", required=True,
|
||||
states=_states)
|
||||
journal = fields.Many2One(
|
||||
'account.journal', "Journal",
|
||||
states={
|
||||
'readonly': _states['readonly'],
|
||||
'required': Eval('state') != 'draft',
|
||||
},
|
||||
context={
|
||||
'company': Eval('company', -1),
|
||||
},
|
||||
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)
|
||||
amount = Monetary(
|
||||
"Amount", currency='currency', digits='currency', required=True,
|
||||
states=_states)
|
||||
start_date = fields.Date(
|
||||
"Start Date", required=True,
|
||||
domain=[
|
||||
('start_date', '<', Eval('end_date', None)),
|
||||
],
|
||||
states=_states)
|
||||
end_date = fields.Date(
|
||||
"End Date", required=True,
|
||||
domain=[
|
||||
('end_date', '>', Eval('start_date', None)),
|
||||
],
|
||||
states=_states)
|
||||
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, sort=False)
|
||||
|
||||
currency = fields.Function(fields.Many2One(
|
||||
'currency.currency', "Currency"), 'on_change_with_currency')
|
||||
|
||||
del _states
|
||||
|
||||
@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._sql_indexes.add(
|
||||
Index(
|
||||
table, (table.state, Index.Equality(cardinality='low')),
|
||||
where=table.state.in_(['draft', 'running'])))
|
||||
cls.journal.domain = [
|
||||
If(Eval('type') == 'out',
|
||||
('type', 'in', cls._journal_types('out')),
|
||||
('type', 'in', cls._journal_types('in'))),
|
||||
]
|
||||
cls._transitions |= set((
|
||||
('draft', 'running'),
|
||||
('running', 'closed'),
|
||||
))
|
||||
cls._buttons.update({
|
||||
'run': {
|
||||
'invisible': Eval('state') != 'draft',
|
||||
'depends': ['state'],
|
||||
},
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def __register__(cls, module_name):
|
||||
table_h = cls.__table_handler__(module_name)
|
||||
super().__register__(module_name)
|
||||
|
||||
# Migration from 6.6: drop not null on journal
|
||||
table_h.not_null_action('journal', 'remove')
|
||||
|
||||
@classmethod
|
||||
def _journal_types(cls, type):
|
||||
if type == 'out':
|
||||
return ['revenue']
|
||||
else:
|
||||
return ['expense']
|
||||
|
||||
@fields.depends(methods=['set_journal'])
|
||||
def on_change_type(self):
|
||||
self.set_journal()
|
||||
|
||||
@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)
|
||||
|
||||
@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_company(cls):
|
||||
return Transaction().context.get('company')
|
||||
|
||||
@classmethod
|
||||
def default_state(cls):
|
||||
return 'draft'
|
||||
|
||||
@fields.depends('company')
|
||||
def on_change_with_currency(self, name=None):
|
||||
return self.company.currency if self.company else None
|
||||
|
||||
@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, deferral.start_date)
|
||||
# Set state before create moves and defer amount to pass assert
|
||||
cls.write(deferrals, {'state': 'running'})
|
||||
deferred_moves = cls.defer_amount(deferrals)
|
||||
cls.create_moves(deferrals, _exclude=deferred_moves)
|
||||
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 check_modification(cls, mode, deferrals, values=None, external=False):
|
||||
super().check_modification(
|
||||
mode, deferrals, values=values, external=external)
|
||||
if mode == 'delete':
|
||||
for deferral in deferrals:
|
||||
if deferral.state != 'draft':
|
||||
raise AccessError(gettext(
|
||||
'account_invoice_defer'
|
||||
'.msg_invoice_deferred_delete_draft',
|
||||
deferral=deferral.rec_name))
|
||||
|
||||
@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)
|
||||
return moves
|
||||
|
||||
@classmethod
|
||||
def create_moves(cls, deferrals, _exclude=None):
|
||||
pool = Pool()
|
||||
Period = pool.get('account.period')
|
||||
Move = pool.get('account.move')
|
||||
exclude = set(_exclude or [])
|
||||
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 if m not in exclude},
|
||||
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:
|
||||
invoice_account = self.invoice_line.account.current(move.date)
|
||||
for line in move.lines:
|
||||
if line.account == invoice_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 not income.account:
|
||||
raise AccountMissing(gettext(
|
||||
'account_invoice_defer'
|
||||
'.msg_invoice_deferred_invoice_line_missing_account',
|
||||
deferral=self.rec_name,
|
||||
account=self.invoice_line.account.rec_name))
|
||||
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)
|
||||
if not balance.account:
|
||||
raise AccountMissing(gettext(
|
||||
'account_invoice_defer.'
|
||||
'msg_missing_deferred_account_revenue'))
|
||||
else:
|
||||
balance.account = configuration.get_multivalue(
|
||||
'deferred_account_expense', company=self.company.id)
|
||||
if not balance.account:
|
||||
raise AccountMissing(gettext(
|
||||
'account_invoice_defer.'
|
||||
'msg_missing_deferred_account_expense'))
|
||||
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):
|
||||
__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 check_access():
|
||||
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),
|
||||
})
|
||||
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),
|
||||
})
|
||||
|
||||
@fields.depends('product')
|
||||
def on_change_with_deferrable(self, name=None):
|
||||
if self.product:
|
||||
return self.product.type == 'service'
|
||||
Reference in New Issue
Block a user