369 lines
13 KiB
Python
369 lines
13 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 defaultdict
|
|
from functools import wraps
|
|
|
|
from trytond.model import ModelView, Workflow, fields
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.pyson import Bool, Eval
|
|
from trytond.transaction import Transaction
|
|
from trytond.wizard import Button, StateTransition, StateView, Wizard
|
|
|
|
|
|
class Journal(metaclass=PoolMeta):
|
|
__name__ = 'account.payment.journal'
|
|
clearing_account = fields.Many2One('account.account', 'Clearing Account',
|
|
domain=[
|
|
('type', '!=', None),
|
|
('closed', '!=', True),
|
|
('party_required', '=', False),
|
|
],
|
|
states={
|
|
'required': Bool(Eval('clearing_journal')),
|
|
},
|
|
depends=['clearing_journal'])
|
|
clearing_journal = fields.Many2One('account.journal', 'Clearing Journal',
|
|
states={
|
|
'required': Bool(Eval('clearing_account')),
|
|
},
|
|
depends=['clearing_account'])
|
|
clearing_posting_delay = fields.TimeDelta(
|
|
"Clearing Posting Delay",
|
|
help="Post automatically the clearing moves after the delay.\n"
|
|
"Leave empty for no posting.")
|
|
|
|
@classmethod
|
|
def cron_post_clearing_moves(cls, date=None):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
Move = pool.get('account.move')
|
|
if date is None:
|
|
date = Date.today()
|
|
moves = []
|
|
journals = cls.search([
|
|
('clearing_posting_delay', '!=', None),
|
|
])
|
|
for journal in journals:
|
|
move_date = date - journal.clearing_posting_delay
|
|
moves.extend(Move.search([
|
|
('date', '<=', move_date),
|
|
('origin.journal.id', '=', journal.id,
|
|
'account.payment'),
|
|
('state', '=', 'draft'),
|
|
('company', '=', Transaction().context.get('company')),
|
|
]))
|
|
Move.post(moves)
|
|
|
|
|
|
def cancel_clearing_move(func):
|
|
@wraps(func)
|
|
def wrapper(cls, payments, *args, **kwargs):
|
|
pool = Pool()
|
|
Move = pool.get('account.move')
|
|
Line = pool.get('account.move.line')
|
|
Reconciliation = pool.get('account.move.reconciliation')
|
|
|
|
func(cls, payments, *args, **kwargs)
|
|
|
|
to_delete = []
|
|
to_reconcile = defaultdict(lambda: defaultdict(list))
|
|
to_unreconcile = []
|
|
for payment in payments:
|
|
if payment.clearing_move:
|
|
if payment.clearing_move.state == 'draft':
|
|
to_delete.append(payment.clearing_move)
|
|
for line in payment.clearing_move.lines:
|
|
if line.reconciliation:
|
|
to_unreconcile.append(line.reconciliation)
|
|
else:
|
|
cancel_move = payment.clearing_move.cancel()
|
|
for line in (payment.clearing_move.lines
|
|
+ cancel_move.lines):
|
|
if line.reconciliation:
|
|
to_unreconcile.append(line.reconciliation)
|
|
if line.account.reconcile:
|
|
to_reconcile[payment.party][line.account].append(
|
|
line)
|
|
|
|
# Remove clearing_move before delete
|
|
# in case reconciliation triggers use it.
|
|
cls.write(payments, {'clearing_move': None})
|
|
|
|
if to_unreconcile:
|
|
Reconciliation.delete(to_unreconcile)
|
|
if to_delete:
|
|
Move.delete(to_delete)
|
|
for party in to_reconcile:
|
|
for lines in to_reconcile[party].values():
|
|
Line.reconcile(lines)
|
|
return wrapper
|
|
|
|
|
|
class Payment(metaclass=PoolMeta):
|
|
__name__ = 'account.payment'
|
|
account = fields.Many2One(
|
|
'account.account', "Account", ondelete='RESTRICT',
|
|
domain=[
|
|
('closed', '!=', True),
|
|
('company', '=', Eval('company', -1)),
|
|
('type.statement', 'in', ['balance', 'off-balance']),
|
|
['OR',
|
|
('second_currency', '=', Eval('currency', None)),
|
|
[
|
|
('company.currency', '=', Eval('currency', None)),
|
|
('second_currency', '=', None),
|
|
],
|
|
],
|
|
],
|
|
states={
|
|
'readonly': Eval('state') != 'draft',
|
|
'invisible': Bool(Eval('line')),
|
|
},
|
|
depends=['company', 'currency', 'state', 'line'],
|
|
help="Define the account to use for clearing move.")
|
|
clearing_move = fields.Many2One('account.move', 'Clearing Move',
|
|
readonly=True)
|
|
|
|
@property
|
|
def amount_line_paid(self):
|
|
amount = super().amount_line_paid
|
|
|
|
if self.clearing_move:
|
|
clearing_lines = [
|
|
l for l in self.clearing_move.lines
|
|
if l.account == self.clearing_account]
|
|
if clearing_lines:
|
|
clearing_line = clearing_lines[0]
|
|
if (not self.line.reconciliation
|
|
and clearing_line.reconciliation):
|
|
if self.line.second_currency:
|
|
payment_amount = abs(self.line.amount_second_currency)
|
|
else:
|
|
payment_amount = abs(
|
|
self.line.credit - self.line.debit)
|
|
amount -= max(min(self.amount, payment_amount), 0)
|
|
return amount
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(Payment, cls).__setup__()
|
|
line_invisible = Bool(Eval('account'))
|
|
if 'invisible' in cls.line.states:
|
|
cls.line.states['invisible'] &= line_invisible
|
|
else:
|
|
cls.line.states['invisible'] = line_invisible
|
|
cls._buttons.update({
|
|
'succeed_wizard': cls._buttons['succeed'],
|
|
})
|
|
|
|
@classmethod
|
|
@ModelView.button_action('account_payment_clearing.wizard_succeed')
|
|
def succeed_wizard(cls, payments):
|
|
pass
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('succeeded')
|
|
def succeed(cls, payments):
|
|
pool = Pool()
|
|
Move = pool.get('account.move')
|
|
Line = pool.get('account.move.line')
|
|
|
|
super(Payment, cls).succeed(payments)
|
|
|
|
moves = []
|
|
for payment in payments:
|
|
move = payment.create_clearing_move(
|
|
date=Transaction().context.get('clearing_date'))
|
|
if move:
|
|
moves.append(move)
|
|
if moves:
|
|
Move.save(moves)
|
|
cls.write(*sum((([m.origin], {'clearing_move': m.id})
|
|
for m in moves), ()))
|
|
|
|
to_reconcile = []
|
|
for payment in payments:
|
|
if (payment.line
|
|
and not payment.line.reconciliation
|
|
and payment.clearing_move):
|
|
lines = [l for l in payment.clearing_move.lines
|
|
if l.account == payment.line.account] + [payment.line]
|
|
if not sum(l.debit - l.credit for l in lines):
|
|
to_reconcile.append(lines)
|
|
Line.reconcile(*to_reconcile)
|
|
cls.reconcile_clearing(payments)
|
|
|
|
@property
|
|
def clearing_account(self):
|
|
if self.line:
|
|
return self.line.account
|
|
elif self.account:
|
|
return self.account
|
|
|
|
@property
|
|
def clearing_party(self):
|
|
if self.line:
|
|
return self.line.party
|
|
else:
|
|
return self.party
|
|
|
|
def create_clearing_move(self, date=None):
|
|
pool = Pool()
|
|
Move = pool.get('account.move')
|
|
Line = pool.get('account.move.line')
|
|
Currency = pool.get('currency.currency')
|
|
Period = pool.get('account.period')
|
|
Date = pool.get('ir.date')
|
|
|
|
if not self.clearing_account:
|
|
return
|
|
if (not self.journal.clearing_account
|
|
or not self.journal.clearing_journal):
|
|
return
|
|
if self.clearing_move:
|
|
return self.clearing_move
|
|
|
|
if date is None:
|
|
with Transaction().set_context(company=self.company.id):
|
|
date = Date.today()
|
|
period = Period.find(self.company.id, date=date)
|
|
|
|
local_currency = self.journal.currency == self.company.currency
|
|
if not local_currency:
|
|
with Transaction().set_context(date=self.date):
|
|
local_amount = Currency.compute(
|
|
self.journal.currency, self.amount, self.company.currency)
|
|
else:
|
|
local_amount = self.amount
|
|
|
|
move = Move(journal=self.journal.clearing_journal, origin=self,
|
|
date=date, period=period, company=self.company)
|
|
line = Line()
|
|
if self.kind == 'payable':
|
|
line.debit, line.credit = local_amount, 0
|
|
else:
|
|
line.debit, line.credit = 0, local_amount
|
|
line.account = self.clearing_account
|
|
if not local_currency:
|
|
line.amount_second_currency = self.amount.copy_sign(
|
|
line.debit - line.credit)
|
|
line.second_currency = self.journal.currency
|
|
|
|
line.party = (self.clearing_party
|
|
if line.account.party_required else None)
|
|
counterpart = Line()
|
|
if self.kind == 'payable':
|
|
counterpart.debit, counterpart.credit = 0, local_amount
|
|
else:
|
|
counterpart.debit, counterpart.credit = local_amount, 0
|
|
counterpart.account = self.journal.clearing_account
|
|
if not local_currency:
|
|
counterpart.amount_second_currency = self.amount.copy_sign(
|
|
counterpart.debit - counterpart.credit)
|
|
counterpart.second_currency = self.journal.currency
|
|
move.lines = (line, counterpart)
|
|
return move
|
|
|
|
@classmethod
|
|
@Workflow.transition('processing')
|
|
@cancel_clearing_move
|
|
def proceed(cls, payments):
|
|
super().proceed(payments)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('failed')
|
|
@cancel_clearing_move
|
|
def fail(cls, payments):
|
|
super(Payment, cls).fail(payments)
|
|
|
|
@classmethod
|
|
def reconcile_clearing(cls, payments):
|
|
pool = Pool()
|
|
MoveLine = pool.get('account.move.line')
|
|
Group = pool.get('account.payment.group')
|
|
to_reconcile = []
|
|
for payment in payments:
|
|
if not payment.clearing_move:
|
|
continue
|
|
clearing_account = payment.journal.clearing_account
|
|
if not clearing_account or not clearing_account.reconcile:
|
|
continue
|
|
lines = [l for l in payment.clearing_lines if not l.reconciliation]
|
|
if lines and not sum((l.debit - l.credit) for l in lines):
|
|
to_reconcile.append(lines)
|
|
if to_reconcile:
|
|
MoveLine.reconcile(*to_reconcile)
|
|
Group.reconcile_clearing(
|
|
Group.browse(list({p.group for p in payments if p.group})))
|
|
|
|
@property
|
|
def clearing_lines(self):
|
|
clearing_account = self.journal.clearing_account
|
|
if self.clearing_move:
|
|
for line in self.clearing_move.lines:
|
|
if line.account == clearing_account:
|
|
yield line
|
|
|
|
@classmethod
|
|
def copy(cls, payments, default=None):
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
default.setdefault('clearing_move')
|
|
return super(Payment, cls).copy(payments, default=default)
|
|
|
|
|
|
class Group(metaclass=PoolMeta):
|
|
__name__ = 'account.payment.group'
|
|
|
|
@classmethod
|
|
def reconcile_clearing(cls, groups):
|
|
pool = Pool()
|
|
MoveLine = pool.get('account.move.line')
|
|
to_reconcile = []
|
|
for group in groups:
|
|
clearing_account = group.journal.clearing_account
|
|
if not clearing_account or not clearing_account.reconcile:
|
|
continue
|
|
lines = [l for l in group.clearing_lines if not l.reconciliation]
|
|
if lines and not sum((l.debit - l.credit) for l in lines):
|
|
to_reconcile.append(lines)
|
|
if to_reconcile:
|
|
MoveLine.reconcile(*to_reconcile)
|
|
|
|
@property
|
|
def clearing_lines(self):
|
|
for payment in self.payments:
|
|
yield from payment.clearing_lines
|
|
|
|
|
|
class Succeed(Wizard):
|
|
"Succeed Payment"
|
|
__name__ = 'account.payment.succeed'
|
|
start = StateView('account.payment.succeed.start',
|
|
'account_payment_clearing.succeed_start_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Succeed', 'succeed', 'tryton-ok', default=True),
|
|
])
|
|
succeed = StateTransition()
|
|
|
|
def transition_succeed(self):
|
|
with Transaction().set_context(clearing_date=self.start.date):
|
|
self.model.succeed(self.records)
|
|
return 'end'
|
|
|
|
|
|
class SucceedStart(ModelView):
|
|
"Succeed Payment"
|
|
__name__ = 'account.payment.succeed.start'
|
|
date = fields.Date("Date", required=True)
|
|
|
|
@classmethod
|
|
def default_date(cls):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
return Date.today()
|