Add option to create direct debit based on balance

Closes #13084
This commit is contained in:
Cédric Krier
2024-03-08 18:45:37 +01:00
parent 935299cd64
commit 04ce1a0456
10 changed files with 202 additions and 15 deletions
+1
View File
@@ -1,3 +1,4 @@
* Add option to create direct debit based on balance
* Enforce payment amount to be greater or equal to zero
Version 7.2.0 - 2024-04-29
+30 -3
View File
@@ -233,7 +233,7 @@ class MoveLine(metaclass=PoolMeta):
with check_access():
lines = cls.search(cls._pay_direct_debit_domain(date))
payments = []
payments, receptions = [], set()
for line in lines:
if not line.payment_amount:
# SQLite fails to search for payment_amount != 0
@@ -241,10 +241,37 @@ class MoveLine(metaclass=PoolMeta):
pattern = Reception.get_pattern(line)
for reception in line.party.reception_direct_debits:
if reception.match(pattern):
payments.extend(reception.get_payments(line))
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)
return 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):
+50 -9
View File
@@ -71,6 +71,14 @@ class PartyReceptionDirectDebit(
depends={'company'})
journal = fields.Many2One(
'account.payment.journal', "Journal", required=True)
type = fields.Selection([
('line', "Line"),
('balance', "Balance"),
], "Type", required=True,
help_selection={
'line': "Make one payment per line.",
'balance': "Make a single payment.",
})
company = fields.Function(
fields.Many2One('company.company', "Company"),
'on_change_with_company', searcher='search_company')
@@ -81,6 +89,10 @@ class PartyReceptionDirectDebit(
fields.Selection('get_process_methods', "Process Method"),
'on_change_with_process_method')
@classmethod
def default_type(cls):
return 'line'
@fields.depends('journal')
def on_change_with_company(self, name=None):
return self.journal.company if self.journal else None
@@ -107,20 +119,27 @@ class PartyReceptionDirectDebit(
return self.journal.process_method
@classmethod
def get_pattern(cls, line):
def get_pattern(cls, line, type='line'):
return {
'company': line.company.id,
'currency': line.payment_currency.id,
'type': type,
}
def get_payments(self, line):
def get_payments(self, line=None, amount=None, date=None):
pool = Pool()
Date = pool.get('ir.date')
with Transaction().set_context(company=self.company.id):
today = Date.today()
for date, amount in self._compute(
line.maturity_date or today, line.payment_amount):
yield self._get_payment(line, date, amount)
assert not line or self.type == 'line'
if date is None:
with Transaction().set_context(company=self.company.id):
date = Date.today()
if line:
date = min(line.maturity_date, date)
if amount is None:
amount = line.payment_amount
if amount:
for date, amount in self._compute(date, amount):
yield self._get_payment(line, date, amount)
def _compute(self, date, amount):
yield date, amount
@@ -129,14 +148,36 @@ class PartyReceptionDirectDebit(
pool = Pool()
Payment = pool.get('account.payment')
return Payment(
company=line.company,
company=self.company,
journal=self.journal,
kind='receivable',
party=line.party,
party=self.party,
date=date,
amount=amount,
line=line)
def get_balance_domain(self, date):
return [
['OR',
('account.type.receivable', '=', True),
('account.type.payable', '=', True),
],
('party', '=', self.party.id),
('payment_currency', '=', self.currency.id),
('reconciliation', '=', None),
('payment_amount', '!=', 0),
('move_state', '=', 'posted'),
('maturity_date', '<=', date),
('payment_blocked', '!=', True),
]
def get_balance_pending_payment_domain(self):
return [
('party', '=', self.party.id),
('currency', '=', self.currency.id),
('state', 'not in', ['succeeded', 'failed']),
]
class Replace(metaclass=PoolMeta):
__name__ = 'party.replace'
@@ -11,7 +11,7 @@ Imports::
>>> from trytond.modules.account.tests.tools import (
... create_chart, create_fiscalyear, get_accounts)
>>> from trytond.modules.company.tests.tools import create_company, get_company
>>> from trytond.tests.tools import activate_modules, assertEqual
>>> from trytond.tests.tools import activate_modules, assertEqual, assertIsNotNone
>>> today = dt.date.today()
>>> tomorrow = today + dt.timedelta(days=1)
@@ -92,6 +92,7 @@ Create direct debit::
>>> assertEqual(payment.party, customer1)
>>> assertEqual(payment.date, tomorrow)
>>> assertEqual(payment.journal, payment_journal)
>>> assertIsNotNone(payment.line)
Re-run create direct debit does nothing::
@@ -0,0 +1,97 @@
=============================================
Account Payment Direct Debit Balance Scenario
=============================================
Imports::
>>> import datetime as dt
>>> from decimal import Decimal
>>> from proteus import Model, Wizard
>>> from trytond.modules.account.tests.tools import (
... create_chart, create_fiscalyear, get_accounts)
>>> from trytond.modules.company.tests.tools import create_company
>>> from trytond.tests.tools import activate_modules, assertEqual, assertIsNone
>>> today = dt.date.today()
Activate modules::
>>> config = activate_modules('account_payment')
>>> Journal = Model.get('account.journal')
>>> Move = Model.get('account.move')
>>> Party = Model.get('party.party')
>>> Payment = Model.get('account.payment')
>>> PaymentJournal = Model.get('account.payment.journal')
Create company::
>>> _ = create_company()
Create fiscal year::
>>> fiscalyear = create_fiscalyear()
>>> fiscalyear.click('create_period')
Create chart of accounts::
>>> _ = create_chart()
>>> accounts = get_accounts()
>>> revnue_journal, = Journal.find([('code', '=', 'REV')])
Create payment journal::
>>> payment_journal = PaymentJournal(
... name="Manual", process_method='manual')
>>> payment_journal.save()
Create parties::
>>> customer = Party(name="Customer")
>>> _ = customer.reception_direct_debits.new(
... journal=payment_journal, type='balance')
>>> customer.save()
Create receivable moves::
>>> move = Move()
>>> move.journal = revnue_journal
>>> line = move.lines.new(
... account=accounts['receivable'], party=customer,
... debit=Decimal('100.00'), maturity_date=today)
>>> line = move.lines.new(
... account=accounts['revenue'],
... credit=Decimal('100.00'))
>>> move.click('post')
Create direct debit::
>>> create_direct_debit = Wizard('account.move.line.create_direct_debit')
>>> create_direct_debit.execute('create_')
>>> payment, = Payment.find([])
>>> payment.amount
Decimal('100.00')
>>> assertEqual(payment.party, customer)
>>> assertEqual(payment.journal, payment_journal)
>>> assertIsNone(payment.line)
>>> payment.amount = Decimal('25.00')
>>> payment.save()
Re-run create a second direct debit::
>>> create_direct_debit = Wizard('account.move.line.create_direct_debit')
>>> create_direct_debit.execute('create_')
>>> payment2, = Payment.find([('id', '!=', payment.id)])
>>> payment2.amount
Decimal('75.00')
Re-run create direct debit does nothing::
>>> create_direct_debit = Wizard('account.move.line.create_direct_debit')
>>> create_direct_debit.execute('create_')
>>> assertEqual(Payment.find([]), [payment, payment2])
@@ -11,4 +11,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="journal" widget="selection"/>
<label name="currency"/>
<field name="currency"/>
<label name="type"/>
<field name="type"/>
<newline/>
</form>
@@ -5,5 +5,6 @@ this repository contains the full copyright notices and license terms. -->
<field name="party" expand="2"/>
<field name="company" expand="1"/>
<field name="journal" expand="1"/>
<field name="type"/>
<field name="currency"/>
</tree>
+2 -1
View File
@@ -2,11 +2,12 @@
# this repository contains the full copyright notices and license terms.
from trytond.pool import Pool
from . import account, ir, payment, statement
from . import account, ir, party, payment, statement
def register():
Pool.register(
party.PartyReceptionDirectDebit,
payment.Journal,
payment.Payment,
payment.Group,
+14
View File
@@ -0,0 +1,14 @@
# 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.pool import PoolMeta
class PartyReceptionDirectDebit(metaclass=PoolMeta):
__name__ = 'party.party.reception_direct_debit'
def get_balance_pending_payment_domain(self):
return [
super().get_balance_pending_payment_domain(),
('clearing_move', '=', None),
]
@@ -2,7 +2,7 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="//field[@name='currency']" position="after">
<xpath expr="/form" position="inside">
<label name="sepa_mandate"/>
<field name="sepa_mandate"/>
<newline/>