@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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/>
|
||||
|
||||
Reference in New Issue
Block a user