Add direct debit as instrument for invoice payment means

This commit is contained in:
Cédric Krier
2026-02-16 12:01:18 +01:00
parent 6246ea244a
commit cc1f6e424c
11 changed files with 153 additions and 2 deletions
+1
View File
@@ -1,3 +1,4 @@
* Add direct debit as instrument for invoice payment means
* Warn against creating an overpayment
Version 7.8.0 - 2025-12-15
+46
View File
@@ -820,6 +820,52 @@ class Invoice(metaclass=PoolMeta):
amounts[invoice.id] -= payment_amount
return amounts
@classmethod
def set_payment_means(cls, invoices):
pool = Pool()
PaymentMean = pool.get('account.invoice.payment.mean')
Reception = pool.get('party.party.reception_direct_debit')
payment_means = []
for invoice in invoices:
if invoice.type == 'out' and not invoice.payment_means:
pattern = Reception.get_pattern_for_invoice(invoice)
for reception in invoice.party.reception_direct_debits:
if reception.match(pattern):
payment_mean = PaymentMean.from_reception_direct_debit(
reception)
payment_mean.invoice = invoice
payment_means.append(payment_mean)
PaymentMean.save(payment_means)
super().set_payment_means(invoices)
class InvoicePaymentMean(metaclass=PoolMeta):
__name__ = 'account.invoice.payment.mean'
@classmethod
def __setup__(cls):
super().__setup__()
cls.instrument.domain['party.party.reception_direct_debit'] = [
('party', 'in', Eval('payers', [])),
('company', '=', Eval('company', -1)),
]
@classmethod
def _get_instruments(cls):
yield from super()._get_instruments()
yield 'party.party.reception_direct_debit'
@classmethod
def from_reception_direct_debit(cls, reception):
return cls(instrument=reception)
def get_rec_name(self, name):
name = super().get_rec_name(name)
if self.instrument.__name__ == 'party.party.reception_direct_debit':
name = gettext(
'account_payment.msg_invoice_payment_mean_direct_debit')
return name
class Statement(metaclass=PoolMeta):
__name__ = 'account.statement'
+3
View File
@@ -3,6 +3,9 @@
this repository contains the full copyright notices and license terms. -->
<tryton>
<data grouped="1">
<record model="ir.message" id="msg_invoice_payment_mean_direct_debit">
<field name="text">Direct debit</field>
</record>
<record model="ir.message" id="msg_payment_delete_draft">
<field name="text">To delete payment "%(payment)s" you must reset it to draft state.</field>
</record>
+7
View File
@@ -124,6 +124,13 @@ class PartyReceptionDirectDebit(
'type': type,
}
@classmethod
def get_pattern_for_invoice(cls, invoice):
return {
'company': invoice.company.id,
'currency': invoice.currency.id,
}
def get_payments(self, line=None, amount=None, date=None):
pool = Pool()
Date = pool.get('ir.date')
@@ -0,0 +1,63 @@
==============================
Invoice Payment Means Scenario
==============================
Imports::
>>> from proteus import Model
>>> from trytond.modules.account.tests.tools import (
... create_chart, create_fiscalyear, get_accounts)
>>> from trytond.modules.account_invoice.tests.tools import (
... set_fiscalyear_invoice_sequences)
>>> from trytond.modules.company.tests.tools import create_company, get_company
>>> from trytond.tests.tools import activate_modules, assertEqual, assertFalse
Activate modules::
>>> config = activate_modules(
... ['account_payment', 'account_invoice'], create_company, create_chart)
>>> Invoice = Model.get('account.invoice')
>>> Party = Model.get('party.party')
>>> PaymentJournal = Model.get('account.payment.journal')
>>> company = get_company()
Create fiscal year::
>>> fiscalyear = set_fiscalyear_invoice_sequences(create_fiscalyear())
>>> fiscalyear.click('create_period')
Get accounts::
>>> accounts = get_accounts()
Create payment journal::
>>> payment_journal = PaymentJournal(name="Manual", process_method='manual')
>>> payment_journal.save()
Create party::
>>> customer = Party(name="Customer")
>>> _ = customer.reception_direct_debits.new(journal=payment_journal)
>>> customer.save()
Create invoice without payment means::
>>> invoice = Invoice(type='out')
>>> invoice.party = customer
>>> assertFalse(invoice.payment_means)
>>> invoice.click('validate_invoice')
>>> payment_mean, = invoice.payment_means
>>> assertEqual(payment_mean.instrument, customer.reception_direct_debits[0])
Create invoice with payment means::
>>> invoice = Invoice(type='out')
>>> invoice.party = customer
>>> payment_mean = invoice.payment_means.new()
>>> payment_mean.instrument = customer.reception_direct_debits[0]
>>> invoice.click('post')
>>> payment_mean, = invoice.payment_means
>>> assertEqual(payment_mean.instrument, customer.reception_direct_debits[0])
+1
View File
@@ -51,6 +51,7 @@ model:
[register account_invoice]
model:
account.Invoice
account.InvoicePaymentMean
payment.Payment_Invoice
[register account_statement]
+18
View File
@@ -1,5 +1,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.
from trytond.i18n import gettext
from trytond.model import ModelSQL, fields
from trytond.modules.company.model import CompanyValueMixin
from trytond.pool import PoolMeta
@@ -27,3 +29,19 @@ class ConfigurationSepaMandateSequence(ModelSQL, CompanyValueMixin):
'account_payment_sepa', 'sequence_type_mandate')),
('company', 'in', [Eval('company', -1), None]),
])
class InvoicePaymentMean(metaclass=PoolMeta):
__name__ = 'account.invoice.payment.mean'
def get_rec_name(self, name):
name = super().get_rec_name(name)
if (self.instrument.__name__ == 'party.party.reception_direct_debit'
and self.instrument.journal.process_method == 'sepa'
and self.instrument.sepa_mandate):
mandate = self.instrument.sepa_mandate
name = gettext(
'account_payment_sepa.msg_invoice_payment_mean_direct_debit',
mandate=mandate.identification,
account_number=mandate.account_number.number)
return name
+3
View File
@@ -3,6 +3,9 @@
this repository contains the full copyright notices and license terms. -->
<tryton>
<data grouped="1">
<record model="ir.message" id="msg_invoice_payment_mean_direct_debit">
<field name="text">SEPA direct debit using mandate %(mandate)s on %(account_number)s</field>
</record>
<record model="ir.message" id="msg_payment_process_no_mandate">
<field name="text">To process payment "%(payment)s" you must define a SEPA mandate for it.</field>
</record>
+4 -1
View File
@@ -50,7 +50,10 @@ for dep in info.get('depends', []):
requires.append(get_require_version('trytond_%s' % dep))
requires.append(get_require_version('trytond'))
tests_require = [get_require_version('proteus')]
tests_require = [
get_require_version('proteus'),
get_require_version('trytond_account_invoice'),
]
setup(name=name,
version=version,
@@ -154,6 +154,7 @@ class AccountPaymentSepaTestCase(
PartyCheckReplaceMixin, CompanyTestMixin, ModuleTestCase):
'Test Account Payment SEPA module'
module = 'account_payment_sepa'
extras = ['account_invoice']
@with_transaction()
def test_pain001_001_03(self):
@@ -279,7 +280,7 @@ class AccountPaymentSepaTestCase(
party = Party(
bank_accounts_used=[bank_account])
payment = Payment(
kind='payable', party=party,
kind='payable', party=party, line=None,
sepa_payable_bank_account_number=None)
self.assertEqual(id(payment.sepa_bank_account_number),
+5
View File
@@ -8,6 +8,7 @@ depends:
bank
party
extras_depend:
account_invoice
account_payment_clearing
xml:
payment.xml
@@ -32,3 +33,7 @@ wizard:
report:
payment.MandateReport
payment.MessageReport
[register account_invoice]
model:
account.InvoicePaymentMean