diff --git a/modules/account_payment/CHANGELOG b/modules/account_payment/CHANGELOG
index ee72e344aa..a492b7856e 100644
--- a/modules/account_payment/CHANGELOG
+++ b/modules/account_payment/CHANGELOG
@@ -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
diff --git a/modules/account_payment/account.py b/modules/account_payment/account.py
index 2390c30f7c..044317cb45 100644
--- a/modules/account_payment/account.py
+++ b/modules/account_payment/account.py
@@ -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):
diff --git a/modules/account_payment/party.py b/modules/account_payment/party.py
index e2438a4090..92e7b60882 100644
--- a/modules/account_payment/party.py
+++ b/modules/account_payment/party.py
@@ -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'
diff --git a/modules/account_payment/tests/scenario_account_payment_direct_debit.rst b/modules/account_payment/tests/scenario_account_payment_direct_debit.rst
index 54f1354900..7e424a49bd 100644
--- a/modules/account_payment/tests/scenario_account_payment_direct_debit.rst
+++ b/modules/account_payment/tests/scenario_account_payment_direct_debit.rst
@@ -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::
diff --git a/modules/account_payment/tests/scenario_account_payment_direct_debit_balance.rst b/modules/account_payment/tests/scenario_account_payment_direct_debit_balance.rst
new file mode 100644
index 0000000000..3c143093fd
--- /dev/null
+++ b/modules/account_payment/tests/scenario_account_payment_direct_debit_balance.rst
@@ -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])
diff --git a/modules/account_payment/view/party_reception_direct_debit_form.xml b/modules/account_payment/view/party_reception_direct_debit_form.xml
index 3e8d86de86..4ac8ba0b19 100644
--- a/modules/account_payment/view/party_reception_direct_debit_form.xml
+++ b/modules/account_payment/view/party_reception_direct_debit_form.xml
@@ -11,4 +11,8 @@ this repository contains the full copyright notices and license terms. -->
+
+
+
+
diff --git a/modules/account_payment/view/party_reception_direct_debit_list.xml b/modules/account_payment/view/party_reception_direct_debit_list.xml
index f819ff461e..c6eecc8f4b 100644
--- a/modules/account_payment/view/party_reception_direct_debit_list.xml
+++ b/modules/account_payment/view/party_reception_direct_debit_list.xml
@@ -5,5 +5,6 @@ this repository contains the full copyright notices and license terms. -->
+
diff --git a/modules/account_payment_clearing/__init__.py b/modules/account_payment_clearing/__init__.py
index 43236590a0..ca8bb52387 100644
--- a/modules/account_payment_clearing/__init__.py
+++ b/modules/account_payment_clearing/__init__.py
@@ -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,
diff --git a/modules/account_payment_clearing/party.py b/modules/account_payment_clearing/party.py
new file mode 100644
index 0000000000..70cda37e38
--- /dev/null
+++ b/modules/account_payment_clearing/party.py
@@ -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),
+ ]
diff --git a/modules/account_payment_sepa/view/party_reception_direct_debit_form.xml b/modules/account_payment_sepa/view/party_reception_direct_debit_form.xml
index 784e6ed023..afebec2c1d 100644
--- a/modules/account_payment_sepa/view/party_reception_direct_debit_form.xml
+++ b/modules/account_payment_sepa/view/party_reception_direct_debit_form.xml
@@ -2,7 +2,7 @@
-
+