From 04ce1a0456cf9a983a258e4a4090abc2c36ecc94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Fri, 8 Mar 2024 18:45:37 +0100 Subject: [PATCH] Add option to create direct debit based on balance Closes #13084 --- modules/account_payment/CHANGELOG | 1 + modules/account_payment/account.py | 33 ++++++- modules/account_payment/party.py | 59 +++++++++-- .../scenario_account_payment_direct_debit.rst | 3 +- ...o_account_payment_direct_debit_balance.rst | 97 +++++++++++++++++++ .../party_reception_direct_debit_form.xml | 4 + .../party_reception_direct_debit_list.xml | 1 + modules/account_payment_clearing/__init__.py | 3 +- modules/account_payment_clearing/party.py | 14 +++ .../party_reception_direct_debit_form.xml | 2 +- 10 files changed, 202 insertions(+), 15 deletions(-) create mode 100644 modules/account_payment/tests/scenario_account_payment_direct_debit_balance.rst create mode 100644 modules/account_payment_clearing/party.py 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. -->