527 lines
19 KiB
Python
527 lines
19 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 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'
|