974 lines
34 KiB
Python
974 lines
34 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 decimal import Decimal
|
|
from itertools import groupby
|
|
|
|
from sql import Null
|
|
from sql.aggregate import Sum
|
|
from sql.conditionals import Case, Coalesce
|
|
from sql.functions import Abs
|
|
|
|
from trytond.i18n import gettext
|
|
from trytond.model import Index, ModelSQL, ModelView, fields
|
|
from trytond.modules.account.exceptions import (
|
|
CancelWarning, DelegateLineWarning, GroupLineWarning,
|
|
RescheduleLineWarning)
|
|
from trytond.modules.company.model import CompanyValueMixin
|
|
from trytond.modules.currency.fields import Monetary
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.pyson import Bool, Eval, Id, If
|
|
from trytond.tools import grouped_slice, reduce_ids
|
|
from trytond.transaction import Transaction, check_access, without_check_access
|
|
from trytond.wizard import (
|
|
Button, StateAction, StateTransition, StateView, Wizard)
|
|
|
|
from .exceptions import BlockedWarning, GroupWarning
|
|
from .payment import KINDS
|
|
|
|
|
|
def _payment_amount_expression(table):
|
|
return (Case(
|
|
(table.second_currency == Null,
|
|
Abs(table.credit - table.debit)),
|
|
else_=Abs(table.amount_second_currency))
|
|
- Coalesce(table.payment_amount_cache, 0))
|
|
|
|
|
|
class MoveLine(metaclass=PoolMeta):
|
|
__name__ = 'account.move.line'
|
|
|
|
payment_amount = fields.Function(Monetary(
|
|
"Amount to Pay",
|
|
currency='payment_currency', digits='payment_currency',
|
|
states={
|
|
'invisible': ~Eval('payment_kind'),
|
|
}),
|
|
'get_payment_amount')
|
|
payment_amount_cache = Monetary(
|
|
"Amount to Pay Cache",
|
|
currency='payment_currency', digits='payment_currency', readonly=True,
|
|
states={
|
|
'invisible': ~Eval('payment_kind'),
|
|
})
|
|
payment_currency = fields.Function(fields.Many2One(
|
|
'currency.currency', "Payment Currency"),
|
|
'get_payment_currency', searcher='search_payment_currency')
|
|
payments = fields.One2Many('account.payment', 'line', 'Payments',
|
|
readonly=True,
|
|
states={
|
|
'invisible': ~Eval('payment_kind'),
|
|
})
|
|
payment_kind = fields.Function(fields.Selection([
|
|
(None, ''),
|
|
] + KINDS, 'Payment Kind'), 'get_payment_kind')
|
|
payment_blocked = fields.Boolean('Blocked', readonly=True)
|
|
payment_direct_debit = fields.Boolean("Direct Debit",
|
|
states={
|
|
'invisible': ~(
|
|
(Eval('payment_kind') == 'payable')
|
|
& ((Eval('credit', 0) > 0) | (Eval('debit', 0) < 0))),
|
|
},
|
|
help="Check if the line will be paid by direct debit.")
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
t = cls.__table__()
|
|
cls._sql_indexes.update({
|
|
Index(
|
|
t, (_payment_amount_expression(t), Index.Range())),
|
|
})
|
|
cls._buttons.update({
|
|
'pay': {
|
|
'invisible': (
|
|
~Eval('payment_kind').in_(list(dict(KINDS).keys()))
|
|
| Eval('reconciliation')),
|
|
'depends': ['payment_kind'],
|
|
},
|
|
'payment_block': {
|
|
'invisible': (
|
|
~Eval('payment_kind').in_(list(dict(KINDS).keys()))
|
|
| Eval('reconciliation')
|
|
| Eval('payment_blocked', False)),
|
|
'depends': ['payment_blocked'],
|
|
},
|
|
'payment_unblock': {
|
|
'invisible': (
|
|
~Eval('payment_kind').in_(list(dict(KINDS).keys()))
|
|
| Eval('reconciliation')
|
|
| ~Eval('payment_blocked', False)),
|
|
'depends': ['payment_blocked'],
|
|
},
|
|
})
|
|
cls._check_modify_exclude.update(
|
|
['payment_blocked', 'payment_direct_debit',
|
|
'payment_amount_cache'])
|
|
|
|
@classmethod
|
|
def __register__(cls, module):
|
|
table_h = cls.__table_handler__(module)
|
|
set_payment_amount = not table_h.column_exist('payment_amount_cache')
|
|
|
|
super().__register__(module)
|
|
|
|
# Migration from 7.2: store payment_amount
|
|
if set_payment_amount:
|
|
cls.set_payment_amount()
|
|
|
|
@classmethod
|
|
def default_payment_direct_debit(cls):
|
|
return False
|
|
|
|
def get_payment_amount(self, name):
|
|
if self.account.type.payable or self.account.type.receivable:
|
|
if self.second_currency:
|
|
amount = abs(self.amount_second_currency)
|
|
else:
|
|
amount = abs(self.credit - self.debit)
|
|
if self.payment_amount_cache:
|
|
amount -= self.payment_amount_cache
|
|
else:
|
|
amount = None
|
|
return amount
|
|
|
|
@classmethod
|
|
def domain_payment_amount(cls, domain, tables):
|
|
pool = Pool()
|
|
Account = pool.get('account.account')
|
|
AccountType = pool.get('account.account.type')
|
|
account = Account.__table__()
|
|
account_type = AccountType.__table__()
|
|
|
|
table, _ = tables[None]
|
|
|
|
accounts = (account
|
|
.join(account_type, condition=account.type == account_type.id)
|
|
.select(
|
|
account.id,
|
|
where=account_type.payable | account_type.receivable))
|
|
|
|
_, operator, operand = domain
|
|
Operator = fields.SQL_OPERATORS[operator]
|
|
|
|
payment_amount = _payment_amount_expression(table)
|
|
|
|
expression = Operator(payment_amount, operand)
|
|
expression &= table.account.in_(accounts)
|
|
return expression
|
|
|
|
@classmethod
|
|
def order_payment_amount(cls, tables):
|
|
table, _ = tables[None]
|
|
return [_payment_amount_expression(table)]
|
|
|
|
@classmethod
|
|
@without_check_access
|
|
def set_payment_amount(cls, lines=None):
|
|
pool = Pool()
|
|
Payment = pool.get('account.payment')
|
|
Account = pool.get('account.account')
|
|
AccountType = pool.get('account.account.type')
|
|
|
|
cursor = Transaction().connection.cursor()
|
|
table = cls.__table__()
|
|
payment = Payment.__table__()
|
|
account = Account.__table__()
|
|
account_type = AccountType.__table__()
|
|
|
|
accounts = (account
|
|
.join(account_type, condition=account.type == account_type.id)
|
|
.select(
|
|
account.id,
|
|
where=account_type.payable | account_type.receivable))
|
|
|
|
query = (table.update(
|
|
[table.payment_amount_cache],
|
|
[payment.select(
|
|
Sum(Coalesce(payment.amount, 0)),
|
|
where=(payment.line == table.id)
|
|
& (payment.state != 'failed'))],
|
|
where=table.account.in_(accounts)))
|
|
|
|
if lines:
|
|
for sub_lines in grouped_slice(lines):
|
|
query.where = (
|
|
table.account.in_(accounts)
|
|
& reduce_ids(table.id, map(int, sub_lines)))
|
|
cursor.execute(*query)
|
|
else:
|
|
cursor.execute(*query)
|
|
|
|
if lines:
|
|
# clear cache
|
|
cls.write(lines, {})
|
|
|
|
def get_payment_currency(self, name):
|
|
if self.second_currency:
|
|
return self.second_currency.id
|
|
elif self.currency:
|
|
return self.currency.id
|
|
|
|
@classmethod
|
|
def search_payment_currency(cls, name, clause):
|
|
return ['OR',
|
|
[('second_currency', *clause[1:]),
|
|
('second_currency', '!=', None),
|
|
],
|
|
[('currency', *clause[1:]),
|
|
('second_currency', '=', None)],
|
|
]
|
|
|
|
def get_payment_kind(self, name):
|
|
if self.account.type.receivable or self.account.type.payable:
|
|
if self.debit > 0 or self.credit < 0:
|
|
return 'receivable'
|
|
elif self.credit > 0 or self.debit < 0:
|
|
return 'payable'
|
|
|
|
@classmethod
|
|
def default_payment_blocked(cls):
|
|
return False
|
|
|
|
@classmethod
|
|
def copy(cls, lines, default=None):
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
default.setdefault('payments', None)
|
|
return super().copy(lines, default=default)
|
|
|
|
@classmethod
|
|
@ModelView.button_action('account_payment.act_pay_line')
|
|
def pay(cls, lines):
|
|
pass
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def payment_block(cls, lines):
|
|
pool = Pool()
|
|
Payment = pool.get('account.payment')
|
|
|
|
cls.write(lines, {
|
|
'payment_blocked': True,
|
|
})
|
|
draft_payments = [p for l in lines for p in l.payments
|
|
if p.state == 'draft']
|
|
if draft_payments:
|
|
Payment.delete(draft_payments)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def payment_unblock(cls, lines):
|
|
cls.write(lines, {
|
|
'payment_blocked': False,
|
|
})
|
|
|
|
@classmethod
|
|
def _pay_direct_debit_domain(cls, date):
|
|
return [
|
|
['OR',
|
|
('account.type.receivable', '=', True),
|
|
('account.type.payable', '=', True),
|
|
],
|
|
('party', '!=', None),
|
|
('reconciliation', '=', None),
|
|
('payment_amount', '!=', 0),
|
|
('move_state', '=', 'posted'),
|
|
['OR',
|
|
('debit', '>', 0),
|
|
('credit', '<', 0),
|
|
],
|
|
['OR',
|
|
('maturity_date', '<=', date),
|
|
('maturity_date', '=', None),
|
|
],
|
|
('payment_blocked', '!=', True),
|
|
]
|
|
|
|
@classmethod
|
|
def pay_direct_debit(cls, date=None):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
Payment = pool.get('account.payment')
|
|
Reception = pool.get('party.party.reception_direct_debit')
|
|
if date is None:
|
|
date = Date.today()
|
|
with check_access():
|
|
lines = cls.search(cls._pay_direct_debit_domain(date))
|
|
|
|
payments, receptions = [], set()
|
|
for line in lines:
|
|
if not line.payment_amount:
|
|
# SQLite fails to search for payment_amount != 0
|
|
continue
|
|
pattern = Reception.get_pattern(line)
|
|
for reception in line.party.reception_direct_debits:
|
|
if reception.match(pattern):
|
|
payments.extend(
|
|
reception.get_payments(line=line, date=date))
|
|
break
|
|
else:
|
|
pattern = Reception.get_pattern(line, 'balance')
|
|
for reception in line.party.reception_direct_debits:
|
|
if reception.match(pattern):
|
|
receptions.add(reception)
|
|
break
|
|
Payment.save(payments)
|
|
|
|
balance_payments = []
|
|
for reception in receptions:
|
|
lines = cls.search(reception.get_balance_domain(date))
|
|
amount = (
|
|
sum(l.payment_amount for l in lines
|
|
if l.payment_kind == 'receivable')
|
|
- sum(l.payment_amount for l in lines
|
|
if l.payment_kind == 'payable'))
|
|
pending_payments = Payment.search(
|
|
reception.get_balance_pending_payment_domain())
|
|
amount -= (
|
|
sum(p.amount for p in pending_payments
|
|
if p.kind == 'receivable')
|
|
- sum(p.amount for p in pending_payments
|
|
if p.kind == 'payable'))
|
|
if amount > 0:
|
|
balance_payments.extend(
|
|
reception.get_payments(amount=amount, date=date))
|
|
Payment.save(balance_payments)
|
|
return payments + balance_payments
|
|
|
|
|
|
class CreateDirectDebit(Wizard):
|
|
__name__ = 'account.move.line.create_direct_debit'
|
|
|
|
start = StateView('account.move.line.create_direct_debit.start',
|
|
'account_payment.move_line_create_direct_debit_view_form', [
|
|
Button("Cancel", 'end', 'tryton-cancel'),
|
|
Button("Create", 'create_', 'tryton-ok', default=True),
|
|
])
|
|
create_ = StateAction('account_payment.act_payment_form')
|
|
|
|
def do_create_(self, action):
|
|
pool = Pool()
|
|
Line = pool.get('account.move.line')
|
|
payments = Line.pay_direct_debit(date=self.start.date)
|
|
action['domains'] = []
|
|
return action, {
|
|
'res_id': [p.id for p in payments],
|
|
}
|
|
|
|
|
|
class CreateDirectDebitStart(ModelView):
|
|
__name__ = 'account.move.line.create_direct_debit.start'
|
|
|
|
date = fields.Date(
|
|
"Date", required=True,
|
|
help="Create direct debit for lines due up to this date.")
|
|
|
|
@classmethod
|
|
def default_date(cls):
|
|
return Pool().get('ir.date').today()
|
|
|
|
|
|
class PayLineStart(ModelView):
|
|
__name__ = 'account.move.line.pay.start'
|
|
date = fields.Date(
|
|
"Date",
|
|
help="When the payments are scheduled to happen.\n"
|
|
"Leave empty to use the lines' maturity dates.")
|
|
|
|
|
|
class PayLineAskJournal(ModelView):
|
|
__name__ = 'account.move.line.pay.ask_journal'
|
|
company = fields.Many2One('company.company', 'Company', readonly=True)
|
|
currency = fields.Many2One('currency.currency', 'Currency', readonly=True)
|
|
journal = fields.Many2One('account.payment.journal', 'Journal',
|
|
required=True, domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
('currency', '=', Eval('currency', -1)),
|
|
])
|
|
|
|
journals = fields.One2Many(
|
|
'account.payment.journal', None, 'Journals', readonly=True)
|
|
|
|
|
|
class PayLine(Wizard):
|
|
__name__ = 'account.move.line.pay'
|
|
start = StateView(
|
|
'account.move.line.pay.start',
|
|
'account_payment.move_line_pay_start_view_form', [
|
|
Button("Cancel", 'end', 'tryton-cancel'),
|
|
Button("Pay", 'next_', 'tryton-ok', default=True),
|
|
])
|
|
next_ = StateTransition()
|
|
ask_journal = StateView('account.move.line.pay.ask_journal',
|
|
'account_payment.move_line_pay_ask_journal_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Pay', 'next_', 'tryton-ok', default=True),
|
|
])
|
|
pay = StateAction('account_payment.act_payment_form')
|
|
|
|
def default_start(self, fields):
|
|
pool = Pool()
|
|
Line = pool.get('account.move.line')
|
|
Warning = pool.get('res.user.warning')
|
|
|
|
reverse = {'receivable': 'payable', 'payable': 'receivable'}
|
|
companies = {}
|
|
lines = self.records
|
|
for line in lines:
|
|
types = companies.setdefault(line.move.company, {
|
|
kind: {
|
|
'parties': set(),
|
|
'lines': list(),
|
|
}
|
|
for kind in reverse.keys()})
|
|
for kind in types:
|
|
if getattr(line.account.type, kind):
|
|
types[kind]['parties'].add(line.party)
|
|
types[kind]['lines'].append(line)
|
|
|
|
for company, types in companies.items():
|
|
for kind in types:
|
|
parties = types[kind]['parties']
|
|
others = Line.search([
|
|
('move.company', '=', company.id),
|
|
('account.type.' + reverse[kind], '=', True),
|
|
('party', 'in', [p.id for p in parties]),
|
|
('reconciliation', '=', None),
|
|
('payment_amount', '!=', 0),
|
|
('move_state', '=', 'posted'),
|
|
])
|
|
for party in parties:
|
|
party_lines = [l for l in others if l.party == party]
|
|
if not party_lines:
|
|
continue
|
|
lines = [l for l in types[kind]['lines']
|
|
if l.party == party]
|
|
warning_name = Warning.format(
|
|
'%s:%s' % (reverse[kind], party), lines)
|
|
if Warning.check(warning_name):
|
|
names = ', '.join(l.rec_name for l in lines[:5])
|
|
if len(lines) > 5:
|
|
names += '...'
|
|
raise GroupWarning(warning_name,
|
|
gettext('account_payment.msg_pay_line_group',
|
|
names=names,
|
|
party=party.rec_name,
|
|
line=party_lines[0].rec_name))
|
|
return {}
|
|
|
|
def _get_journals(self):
|
|
journals = {}
|
|
if self.ask_journal.journals:
|
|
for journal in self.ask_journal.journals:
|
|
journals[self._get_journal_key(journal)] = journal
|
|
if journal := self.ask_journal.journal:
|
|
journals[self._get_journal_key(journal)] = journal
|
|
return journals
|
|
|
|
def _get_journal_key(self, record):
|
|
pool = Pool()
|
|
Journal = pool.get('account.payment.journal')
|
|
Line = pool.get('account.move.line')
|
|
if isinstance(record, Journal):
|
|
return (record.company, record.currency)
|
|
elif isinstance(record, Line):
|
|
company = record.move.company
|
|
currency = record.second_currency or company.currency
|
|
return (company, currency)
|
|
|
|
def _missing_journal(self):
|
|
lines = self.records
|
|
journals = self._get_journals()
|
|
|
|
for line in lines:
|
|
key = self._get_journal_key(line)
|
|
if key not in journals:
|
|
return key
|
|
|
|
def transition_next_(self):
|
|
if self._missing_journal():
|
|
return 'ask_journal'
|
|
else:
|
|
return 'pay'
|
|
|
|
def default_ask_journal(self, fields):
|
|
pool = Pool()
|
|
Journal = pool.get('account.payment.journal')
|
|
values = {}
|
|
company, currency = self._missing_journal()[:2]
|
|
journals = Journal.search([
|
|
('company', '=', company),
|
|
('currency', '=', currency),
|
|
])
|
|
if len(journals) == 1:
|
|
journal, = journals
|
|
values['journal'] = journal.id
|
|
values['company'] = company.id
|
|
values['currency'] = currency.id
|
|
values['journals'] = [j.id for j in self._get_journals().values()]
|
|
return values
|
|
|
|
def get_payment(self, line, journals):
|
|
pool = Pool()
|
|
Payment = pool.get('account.payment')
|
|
|
|
if (line.debit > 0) or (line.credit < 0):
|
|
kind = 'receivable'
|
|
else:
|
|
kind = 'payable'
|
|
journal = journals[self._get_journal_key(line)]
|
|
payment = Payment(
|
|
company=line.move.company,
|
|
journal=journal,
|
|
party=line.party,
|
|
kind=kind,
|
|
amount=line.payment_amount,
|
|
line=line,
|
|
)
|
|
payment.date = self.start.date or line.maturity_date
|
|
return payment
|
|
|
|
def do_pay(self, action):
|
|
pool = Pool()
|
|
Payment = pool.get('account.payment')
|
|
Warning = pool.get('res.user.warning')
|
|
|
|
lines = self.records
|
|
journals = self._get_journals()
|
|
|
|
payments = []
|
|
for line in lines:
|
|
if line.payment_blocked:
|
|
warning_name = 'blocked:%s' % line
|
|
if Warning.check(warning_name):
|
|
raise BlockedWarning(warning_name,
|
|
gettext('account_payment.msg_pay_line_blocked',
|
|
line=line.rec_name))
|
|
payments.append(self.get_payment(line, journals))
|
|
Payment.save(payments)
|
|
return action, {
|
|
'res_id': [p.id for p in payments],
|
|
}
|
|
|
|
|
|
class Configuration(metaclass=PoolMeta):
|
|
__name__ = 'account.configuration'
|
|
payment_sequence = fields.MultiValue(fields.Many2One(
|
|
'ir.sequence', "Payment Sequence", required=True,
|
|
domain=[
|
|
('company', 'in',
|
|
[Eval('context', {}).get('company', -1), None]),
|
|
('sequence_type', '=',
|
|
Id('account_payment',
|
|
'sequence_type_account_payment')),
|
|
]))
|
|
payment_group_sequence = fields.MultiValue(fields.Many2One(
|
|
'ir.sequence', 'Payment Group Sequence', required=True,
|
|
domain=[
|
|
('company', 'in',
|
|
[Eval('context', {}).get('company', -1), None]),
|
|
('sequence_type', '=',
|
|
Id('account_payment',
|
|
'sequence_type_account_payment_group')),
|
|
]))
|
|
|
|
@classmethod
|
|
def default_payment_sequence(cls, **pattern):
|
|
return cls.multivalue_model(
|
|
'payment_sequence').default_payment_sequence()
|
|
|
|
@classmethod
|
|
def default_payment_group_sequence(cls, **pattern):
|
|
return cls.multivalue_model(
|
|
'payment_group_sequence').default_payment_group_sequence()
|
|
|
|
|
|
class ConfigurationPaymentSequence(ModelSQL, CompanyValueMixin):
|
|
__name__ = 'account.configuration.payment_sequence'
|
|
payment_sequence = fields.Many2One(
|
|
'ir.sequence', "Payment Sequence", required=True,
|
|
domain=[
|
|
('company', 'in', [Eval('company', -1), None]),
|
|
('sequence_type', '=',
|
|
Id('account_payment', 'sequence_type_account_payment')),
|
|
])
|
|
|
|
@classmethod
|
|
def default_payment_sequence(cls):
|
|
pool = Pool()
|
|
ModelData = pool.get('ir.model.data')
|
|
try:
|
|
return ModelData.get_id(
|
|
'account_payment', 'sequence_account_payment')
|
|
except KeyError:
|
|
return None
|
|
|
|
|
|
class ConfigurationPaymentGroupSequence(ModelSQL, CompanyValueMixin):
|
|
__name__ = 'account.configuration.payment_group_sequence'
|
|
payment_group_sequence = fields.Many2One(
|
|
'ir.sequence', "Payment Group Sequence", required=True,
|
|
domain=[
|
|
('company', 'in', [Eval('company', -1), None]),
|
|
('sequence_type', '=',
|
|
Id('account_payment', 'sequence_type_account_payment_group')),
|
|
])
|
|
|
|
@classmethod
|
|
def default_payment_group_sequence(cls):
|
|
pool = Pool()
|
|
ModelData = pool.get('ir.model.data')
|
|
try:
|
|
return ModelData.get_id(
|
|
'account_payment', 'sequence_account_payment_group')
|
|
except KeyError:
|
|
return None
|
|
|
|
|
|
class MoveCancel(metaclass=PoolMeta):
|
|
__name__ = 'account.move.cancel'
|
|
|
|
def transition_cancel(self):
|
|
pool = Pool()
|
|
Warning = pool.get('res.user.warning')
|
|
moves_w_payments = []
|
|
for move in self.records:
|
|
for line in move.lines:
|
|
if any(p.state != 'failed' for p in line.payments):
|
|
moves_w_payments.append(move)
|
|
break
|
|
if moves_w_payments:
|
|
names = ', '.join(
|
|
m.rec_name for m in moves_w_payments[:5])
|
|
if len(moves_w_payments) > 5:
|
|
names += '...'
|
|
key = Warning.format('cancel_payments', moves_w_payments)
|
|
if Warning.check(key):
|
|
raise CancelWarning(
|
|
key, gettext(
|
|
'account_payment.msg_move_cancel_payments',
|
|
moves=names))
|
|
return super().transition_cancel()
|
|
|
|
|
|
class MoveLineGroup(metaclass=PoolMeta):
|
|
__name__ = 'account.move.line.group'
|
|
|
|
def do_group(self, action):
|
|
pool = Pool()
|
|
Warning = pool.get('res.user.warning')
|
|
lines_w_payments = []
|
|
for line in self.records:
|
|
if any(p.state != 'failed' for p in line.payments):
|
|
lines_w_payments.append(line)
|
|
if lines_w_payments:
|
|
names = ', '.join(
|
|
m.rec_name for m in lines_w_payments[:5])
|
|
if len(lines_w_payments) > 5:
|
|
names += '...'
|
|
key = Warning.format('group_payments', lines_w_payments)
|
|
if Warning.check(key):
|
|
raise GroupLineWarning(
|
|
key, gettext(
|
|
'account_payment.msg_move_line_group_payments',
|
|
lines=names))
|
|
return super().do_group(action)
|
|
|
|
|
|
class MoveLineReschedule(metaclass=PoolMeta):
|
|
__name__ = 'account.move.line.reschedule'
|
|
|
|
def do_reschedule(self, action):
|
|
pool = Pool()
|
|
Warning = pool.get('res.user.warning')
|
|
lines_w_payments = []
|
|
for line in self.records:
|
|
if any(p.state != 'failed' for p in line.payments):
|
|
lines_w_payments.append(line)
|
|
if lines_w_payments:
|
|
names = ', '.join(
|
|
m.rec_name for m in lines_w_payments[:5])
|
|
if len(lines_w_payments) > 5:
|
|
names += '...'
|
|
key = Warning.format('reschedule_payments', lines_w_payments)
|
|
if Warning.check(key):
|
|
raise RescheduleLineWarning(
|
|
key, gettext(
|
|
'account_payment.msg_move_line_reschedule_payments',
|
|
lines=names))
|
|
return super().do_reschedule(action)
|
|
|
|
|
|
class MoveLineDelegate(metaclass=PoolMeta):
|
|
__name__ = 'account.move.line.delegate'
|
|
|
|
def do_delegate(self, action):
|
|
pool = Pool()
|
|
Warning = pool.get('res.user.warning')
|
|
lines_w_payments = []
|
|
for line in self.records:
|
|
if any(p.state != 'failed' for p in line.payments):
|
|
lines_w_payments.append(line)
|
|
if lines_w_payments:
|
|
names = ', '.join(
|
|
m.rec_name for m in lines_w_payments[:5])
|
|
if len(lines_w_payments) > 5:
|
|
names += '...'
|
|
key = Warning.format('delegate_payments', lines_w_payments)
|
|
if Warning.check(key):
|
|
raise DelegateLineWarning(
|
|
key, gettext(
|
|
'account_payment.msg_move_line_delegate_payments',
|
|
lines=names))
|
|
return super().do_delegate(action)
|
|
|
|
|
|
class Invoice(metaclass=PoolMeta):
|
|
__name__ = 'account.invoice'
|
|
|
|
payment_direct_debit = fields.Boolean("Direct Debit",
|
|
states={
|
|
'invisible': Eval('type') != 'in',
|
|
'readonly': Eval('state') != 'draft',
|
|
},
|
|
help="Check if the invoice is paid by direct debit.")
|
|
|
|
@classmethod
|
|
def default_payment_direct_debit(cls):
|
|
return False
|
|
|
|
@fields.depends('party')
|
|
def on_change_party(self):
|
|
super().on_change_party()
|
|
if self.party:
|
|
self.payment_direct_debit = self.party.payment_direct_debit
|
|
|
|
def _get_move_line(self, date, amount):
|
|
line = super()._get_move_line(date, amount)
|
|
line.payment_direct_debit = self.payment_direct_debit
|
|
return line
|
|
|
|
@classmethod
|
|
def get_amount_to_pay(cls, invoices, name):
|
|
pool = Pool()
|
|
Currency = pool.get('currency.currency')
|
|
Date = pool.get('ir.date')
|
|
context = Transaction().context
|
|
|
|
amounts = super().get_amount_to_pay(invoices, name)
|
|
|
|
if context.get('with_payment', True):
|
|
for company, c_invoices in groupby(
|
|
invoices, key=lambda i: i.company):
|
|
with Transaction().set_context(company=company.id):
|
|
today = Date.today()
|
|
for invoice in c_invoices:
|
|
for line in invoice.lines_to_pay:
|
|
if line.reconciliation:
|
|
continue
|
|
if (name == 'amount_to_pay_today'
|
|
and line.maturity_date
|
|
and line.maturity_date > today):
|
|
continue
|
|
payment_amount = Decimal(0)
|
|
for payment in line.payments:
|
|
amount_line_paid = payment.amount_line_paid
|
|
if ((invoice.type == 'in'
|
|
and payment.kind == 'receivable')
|
|
or (invoice.type == 'out'
|
|
and payment.kind == 'payable')):
|
|
amount_line_paid *= -1
|
|
with Transaction().set_context(date=payment.date):
|
|
payment_amount += Currency.compute(
|
|
payment.currency, amount_line_paid,
|
|
invoice.currency)
|
|
amounts[invoice.id] -= payment_amount
|
|
return amounts
|
|
|
|
|
|
class Statement(metaclass=PoolMeta):
|
|
__name__ = 'account.statement'
|
|
|
|
@classmethod
|
|
def create_move(cls, statements):
|
|
moves = super().create_move(statements)
|
|
cls._process_payments(moves)
|
|
return moves
|
|
|
|
@classmethod
|
|
def _process_payments(cls, moves):
|
|
pool = Pool()
|
|
Payment = pool.get('account.payment')
|
|
|
|
to_success = defaultdict(set)
|
|
to_fail = defaultdict(set)
|
|
for move, statement, lines in moves:
|
|
for line in lines:
|
|
for kind, payments in line.payments():
|
|
if (kind == 'receivable') == (line.amount >= 0):
|
|
to_success[line.date].update(payments)
|
|
else:
|
|
to_fail[line.date].update(payments)
|
|
# The failing should be done last because success is usually not a
|
|
# definitive state
|
|
if to_success:
|
|
for date, payments in to_success.items():
|
|
with Transaction().set_context(clearing_date=date):
|
|
Payment.succeed(Payment.browse(payments))
|
|
if to_fail:
|
|
for date, payments in to_fail.items():
|
|
with Transaction().set_context(clearing_date=date):
|
|
Payment.fail(Payment.browse(payments))
|
|
if to_success or to_fail:
|
|
payments = set.union(*to_success.values(), *to_fail.values())
|
|
else:
|
|
payments = []
|
|
return list(payments)
|
|
|
|
|
|
class StatementLine(metaclass=PoolMeta):
|
|
__name__ = 'account.statement.line'
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.related_to.domain['account.payment'] = [
|
|
('company', '=', Eval('company', -1)),
|
|
If(Bool(Eval('party')),
|
|
('party', '=', Eval('party', -1)),
|
|
()),
|
|
('state', 'in', ['processing', 'succeeded', 'failed']),
|
|
If(Eval('second_currency'),
|
|
('currency', '=', Eval('second_currency', -1)),
|
|
('currency', '=', Eval('currency', -1))),
|
|
('kind', '=',
|
|
If(Eval('amount', 0) > 0, 'receivable',
|
|
If(Eval('amount', 0) < 0, 'payable', ''))),
|
|
]
|
|
cls.related_to.search_order['account.payment'] = [
|
|
('amount', 'ASC'),
|
|
('state', 'ASC'),
|
|
]
|
|
cls.related_to.search_context.update({
|
|
'amount_order': Eval('amount', 0),
|
|
})
|
|
|
|
@classmethod
|
|
def _get_relations(cls):
|
|
return super()._get_relations() + ['account.payment']
|
|
|
|
@property
|
|
@fields.depends('related_to')
|
|
def payment(self):
|
|
pool = Pool()
|
|
Payment = pool.get('account.payment')
|
|
related_to = getattr(self, 'related_to', None)
|
|
if isinstance(related_to, Payment) and related_to.id >= 0:
|
|
return related_to
|
|
|
|
@payment.setter
|
|
def payment(self, value):
|
|
self.related_to = value
|
|
|
|
@fields.depends(
|
|
'party', 'statement', '_parent_statement.journal',
|
|
methods=['payment'])
|
|
def on_change_related_to(self):
|
|
super().on_change_related_to()
|
|
if self.payment:
|
|
if not self.party:
|
|
self.party = self.payment.party
|
|
if self.payment.line:
|
|
self.account = self.payment.line.account
|
|
|
|
@fields.depends('party', methods=['payment'])
|
|
def on_change_party(self):
|
|
super().on_change_party()
|
|
if self.payment:
|
|
if self.payment.party != self.party:
|
|
self.payment = None
|
|
|
|
def payments(self):
|
|
"Yield payments per kind"
|
|
if self.payment:
|
|
yield self.payment.kind, [self.payment]
|
|
|
|
|
|
class StatementRule(metaclass=PoolMeta):
|
|
__name__ = 'account.statement.rule'
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.description.help += "\n'payment'"
|
|
|
|
|
|
class StatementRuleLine(metaclass=PoolMeta):
|
|
__name__ = 'account.statement.rule.line'
|
|
|
|
def _get_related_to(self, origin, keywords, party=None, amount=0):
|
|
return super()._get_related_to(
|
|
origin, keywords, party=party, amount=amount) | {
|
|
self._get_payment(origin, keywords, party=party, amount=amount),
|
|
}
|
|
|
|
def _get_party_from(self, related_to):
|
|
pool = Pool()
|
|
Payment = pool.get('account.payment')
|
|
party = super()._get_party_from(related_to)
|
|
if isinstance(related_to, Payment):
|
|
party = related_to.party
|
|
return party
|
|
|
|
@classmethod
|
|
def _get_payment_domain(cls, payment, origin):
|
|
return [
|
|
('rec_name', '=', payment),
|
|
('company', '=', origin.company.id),
|
|
('currency', '=', origin.currency.id),
|
|
('state', 'in', ['processing', 'succeeded', 'failed']),
|
|
]
|
|
|
|
def _get_payment(self, origin, keywords, party=None, amount=0):
|
|
pool = Pool()
|
|
Payment = pool.get('account.payment')
|
|
if keywords.get('payment'):
|
|
domain = self._get_payment_domain(keywords['payment'], origin)
|
|
if party:
|
|
domain.append(('party', '=', party.id))
|
|
if amount > 0:
|
|
domain.append(('kind', '=', 'receivable'))
|
|
elif amount < 0:
|
|
domain.append(('kind', '=', 'payable'))
|
|
payments = Payment.search(domain)
|
|
if len(payments) == 1:
|
|
payment, = payments
|
|
return payment
|
|
|
|
|
|
class Dunning(metaclass=PoolMeta):
|
|
__name__ = 'account.dunning'
|
|
|
|
def get_active(self, name):
|
|
return super().get_active(name) and self.line.payment_amount > 0
|
|
|
|
@classmethod
|
|
def search_active(cls, name, clause):
|
|
if tuple(clause[1:]) in [('=', True), ('!=', False)]:
|
|
domain = ('line.payment_amount', '>', 0)
|
|
elif tuple(clause[1:]) in [('=', False), ('!=', True)]:
|
|
domain = ('line.payment_amount', '<=', 0)
|
|
else:
|
|
domain = []
|
|
return [super().search_active(name, clause), domain]
|
|
|
|
@classmethod
|
|
def _overdue_line_domain(cls, date):
|
|
return [super()._overdue_line_domain(date),
|
|
('payment_amount', '>', 0),
|
|
]
|