details: https://code.tryton.org/tryton/commit/221bab7434f5
branch: default
user: Cédric Krier <[email protected]>
date: Fri Feb 13 14:22:58 2026 +0100
description:
Check invoice payment means when submitting payment
diffstat:
modules/account_payment/account.py
| 10 +
modules/account_payment/exceptions.py
| 4 +
modules/account_payment/message.xml
| 4 +
modules/account_payment/payment.py
| 32 ++-
modules/account_payment/tests/scenario_account_payment_invoice_payment_means.rst
| 30 ++-
modules/account_payment_sepa/account.py
| 19 +-
modules/account_payment_sepa/payment.py
| 44 +++
modules/account_payment_sepa/tests/scenario_account_payment_sepa_invoice_payment_means.rst
| 123 ++++++++++
modules/account_payment_sepa/tryton.cfg
| 1 +
9 files changed, 264 insertions(+), 3 deletions(-)
diffs (368 lines):
diff -r 7feb597bd64a -r 221bab7434f5 modules/account_payment/account.py
--- a/modules/account_payment/account.py Mon Feb 16 12:01:18 2026 +0100
+++ b/modules/account_payment/account.py Fri Feb 13 14:22:58 2026 +0100
@@ -866,6 +866,16 @@
'account_payment.msg_invoice_payment_mean_direct_debit')
return name
+ def is_valid_with_payment(self, payment):
+ pool = Pool()
+ ReceptionDirectDebit = pool.get('party.party.reception_direct_debit')
+ valid = None
+ if isinstance(self.instrument, ReceptionDirectDebit):
+ valid = self.instrument.journal == payment.journal
+ elif payment.journal.process_method == 'manual':
+ valid = True
+ return valid
+
class Statement(metaclass=PoolMeta):
__name__ = 'account.statement'
diff -r 7feb597bd64a -r 221bab7434f5 modules/account_payment/exceptions.py
--- a/modules/account_payment/exceptions.py Mon Feb 16 12:01:18 2026 +0100
+++ b/modules/account_payment/exceptions.py Fri Feb 13 14:22:58 2026 +0100
@@ -17,6 +17,10 @@
pass
+class PaymentMeanWarning(UserWarning):
+ pass
+
+
class PaymentValidationError(ValidationError):
pass
diff -r 7feb597bd64a -r 221bab7434f5 modules/account_payment/message.xml
--- a/modules/account_payment/message.xml Mon Feb 16 12:01:18 2026 +0100
+++ b/modules/account_payment/message.xml Fri Feb 13 14:22:58 2026 +0100
@@ -15,6 +15,10 @@
<record model="ir.message" id="msg_payment_reconciled">
<field name="text">The line "%(line)s" of payment "%(payment)s" is
already reconciled.</field>
</record>
+ <record model="ir.message" id="msg_payment_means">
+ <field name="text">The journal "%(journal)s" of payment
"%(payment)s" is not valid with the payment means of invoice
"%(invoice)s".</field>
+ </record>
+
<record model="ir.message" id="msg_payment_reference_invalid">
<field name="text">The %(type)s "%(reference)s" on payment
"%(payment)s" is not valid.</field>
</record>
diff -r 7feb597bd64a -r 221bab7434f5 modules/account_payment/payment.py
--- a/modules/account_payment/payment.py Mon Feb 16 12:01:18 2026 +0100
+++ b/modules/account_payment/payment.py Fri Feb 13 14:22:58 2026 +0100
@@ -28,7 +28,8 @@
from trytond.wizard import StateAction, Wizard
from .exceptions import (
- OverpayWarning, PaymentValidationError, ReconciledWarning)
+ OverpayWarning, PaymentMeanWarning, PaymentValidationError,
+ ReconciledWarning)
KINDS = [
('payable', 'Payable'),
@@ -826,6 +827,35 @@
reference = invoice.customer_payment_reference
return reference
+ @classmethod
+ @ModelView.button
+ @Workflow.transition('submitted')
+ def submit(cls, payments):
+ super().submit(payments)
+ cls._check_payment_means(payments)
+
+ @classmethod
+ def _check_payment_means(cls, payments):
+ pool = Pool()
+ Warning = pool.get('res.user.warning')
+ Invoice = pool.get('account.invoice')
+ for payment in payments:
+ if (payment.line
+ and isinstance(payment.line.move.origin, Invoice)):
+ invoice = payment.line.move.origin
+ if invoice.payment_means:
+ if not any(
+ mean.is_valid_with_payment(payment)
+ for mean in invoice.payment_means):
+ key = Warning.format('payment_means', [payment])
+ if Warning.check(key):
+ raise PaymentMeanWarning(
+ key, gettext(
+ 'account_payment.msg_payment_means',
+ journal=payment.journal.rec_name,
+ payment=payment.rec_name,
+ invoice=invoice.rec_name))
+
class ProcessPayment(Wizard):
__name__ = 'account.payment.process'
diff -r 7feb597bd64a -r 221bab7434f5
modules/account_payment/tests/scenario_account_payment_invoice_payment_means.rst
---
a/modules/account_payment/tests/scenario_account_payment_invoice_payment_means.rst
Mon Feb 16 12:01:18 2026 +0100
+++
b/modules/account_payment/tests/scenario_account_payment_invoice_payment_means.rst
Fri Feb 13 14:22:58 2026 +0100
@@ -4,7 +4,9 @@
Imports::
- >>> from proteus import Model
+ >>> 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.account_invoice.tests.tools import (
@@ -56,8 +58,34 @@
>>> invoice = Invoice(type='out')
>>> invoice.party = customer
+ >>> line = invoice.lines.new()
+ >>> line.account = accounts['revenue']
+ >>> line.quantity = 1
+ >>> line.unit_price = Decimal('10.0000')
>>> 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])
+
+Try to pay with a different journal::
+
+ >>> other_payment_journal, = payment_journal.duplicate()
+
+ >>> line_to_pay, = invoice.lines_to_pay
+ >>> pay_line = Wizard('account.move.line.pay', [line_to_pay])
+ >>> pay_line.execute('next_')
+ >>> pay_line.form.journal = other_payment_journal
+ >>> pay_line.execute('next_')
+ >>> payment, = pay_line.actions[0]
+ >>> payment.click('submit')
+ Traceback (most recent call last):
+ ...
+ PaymentMeanWarning: ...
+
+Pay with the same journal::
+
+ >>> payment.journal = payment_journal
+ >>> payment.click('submit')
+ >>> payment.state
+ 'submitted'
diff -r 7feb597bd64a -r 221bab7434f5 modules/account_payment_sepa/account.py
--- a/modules/account_payment_sepa/account.py Mon Feb 16 12:01:18 2026 +0100
+++ b/modules/account_payment_sepa/account.py Fri Feb 13 14:22:58 2026 +0100
@@ -4,7 +4,7 @@
from trytond.i18n import gettext
from trytond.model import ModelSQL, fields
from trytond.modules.company.model import CompanyValueMixin
-from trytond.pool import PoolMeta
+from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval, Id
@@ -45,3 +45,20 @@
mandate=mandate.identification,
account_number=mandate.account_number.number)
return name
+
+ def is_valid_with_payment(self, payment):
+ pool = Pool()
+ BankAccount = pool.get('bank.account')
+ ReceptionDirectDebit = pool.get('party.party.reception_direct_debit')
+ valid = super().is_valid_with_payment(payment)
+ if payment.journal.process_method == 'sepa':
+ if isinstance(self.instrument, BankAccount):
+ bank_account = self.instrument
+ valid = (
+ payment.sepa_bank_account_number in bank_account.numbers)
+ elif isinstance(self.instrument, ReceptionDirectDebit):
+ reception = self.instrument
+ if reception.journal == payment.journal:
+ if payment.sepa_mandate and reception.sepa_mandate:
+ valid &= payment.sepa_mandate == reception.sepa_mandate
+ return valid
diff -r 7feb597bd64a -r 221bab7434f5 modules/account_payment_sepa/payment.py
--- a/modules/account_payment_sepa/payment.py Mon Feb 16 12:01:18 2026 +0100
+++ b/modules/account_payment_sepa/payment.py Fri Feb 13 14:22:58 2026 +0100
@@ -490,6 +490,50 @@
]
+class Payment_Invoice(metaclass=PoolMeta):
+ __name__ = 'account.payment'
+
+ @property
+ def sepa_bank_account_number(self):
+ pool = Pool()
+ Invoice = pool.get('account.invoice')
+ BankAccount = pool.get('bank.account')
+ sepa_number = super().sepa_bank_account_number
+ if (self.kind == 'payable'
+ and not self.sepa_payable_bank_account_number
+ and self.line
+ and isinstance(self.line.move.origin, Invoice)):
+ invoice = self.line.move.origin
+ for mean in invoice.payment_means:
+ if isinstance(mean.instrument, BankAccount):
+ bank_account = mean.instrument
+ for number in bank_account.numbers:
+ if number.type == 'iban':
+ sepa_number = number
+ break
+ return sepa_number
+
+ @classmethod
+ def get_sepa_mandates(cls, payments):
+ pool = Pool()
+ Invoice = pool.get('account.invoice')
+ ReceptionDirectDebit = pool.get('party.party.reception_direct_debit')
+ mandates = []
+ for payment, mandate in zip(
+ payments,
+ super().get_sepa_mandates(payments)):
+ if (not payment.sepa_mandate
+ and payment.line
+ and isinstance(payment.line.move.origin, Invoice)):
+ invoice = payment.line.move.origin
+ for mean in invoice.payment_means:
+ if (isinstance(mean.instrument, ReceptionDirectDebit)
+ and mean.instrument.sepa_mandate):
+ mandate = mean.instrument.sepa_mandate
+ mandates.append(mandate)
+ return mandates
+
+
class Mandate(Workflow, ModelSQL, ModelView):
__name__ = 'account.payment.sepa.mandate'
party = fields.Many2One(
diff -r 7feb597bd64a -r 221bab7434f5
modules/account_payment_sepa/tests/scenario_account_payment_sepa_invoice_payment_means.rst
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++
b/modules/account_payment_sepa/tests/scenario_account_payment_sepa_invoice_payment_means.rst
Fri Feb 13 14:22:58 2026 +0100
@@ -0,0 +1,123 @@
+==============================
+Invoice Payment Means 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.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
+
+ >>> today = dt.date.today()
+
+Activate modules::
+
+ >>> config = activate_modules(
+ ... ['account_payment_sepa', 'account_invoice'], create_company,
create_chart)
+
+ >>> Bank = Model.get('bank')
+ >>> BankAccount = Model.get('bank.account')
+ >>> 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 party::
+
+ >>> supplier = Party(name="Supplier")
+ >>> supplier.save()
+
+Create bank accounts::
+
+ >>> bank_party = Party(name="EU Bank")
+ >>> bank_party.save()
+
+ >>> company_bank = Bank(party=bank_party, bic='CTBKBEBX')
+ >>> company_bank.save()
+ >>> company_account = BankAccount(bank=company_bank)
+ >>> company_account.owners.append(company.party)
+ >>> company_account_number = company_account.numbers.new()
+ >>> company_account_number.type = 'iban'
+ >>> company_account_number.number = 'BE70953368654125'
+ >>> company_account.save()
+
+ >>> supplier_bank1 = Bank(party=bank_party, bic='KREDBEBB')
+ >>> supplier_bank1.save()
+ >>> supplier_account1 = BankAccount(bank=supplier_bank1)
+ >>> supplier_account1.owners.append(Party(supplier.id))
+ >>> supplier_account1_number = supplier_account1.numbers.new()
+ >>> supplier_account1_number.type = 'iban'
+ >>> supplier_account1_number.number = 'BE85735556927306'
+ >>> supplier_account1.save()
+ >>> supplier_bank2 = Bank(party=bank_party, bic='CITIBEBX')
+ >>> supplier_bank2.save()
+ >>> supplier_account2 = BankAccount(bank=supplier_bank2)
+ >>> supplier_account2.owners.append(Party(supplier.id))
+ >>> supplier_account2_number = supplier_account2.numbers.new()
+ >>> supplier_account2_number.type = 'iban'
+ >>> supplier_account2_number.number = 'BE96570728435605'
+ >>> supplier_account2.save()
+
+Create payment journal::
+
+ >>> payment_journal = PaymentJournal(name="SEPA", process_method='sepa')
+ >>> payment_journal.sepa_bank_account_number = company_account.numbers[0]
+ >>> payment_journal.sepa_payable_flavor = 'pain.001.001.03'
+ >>> payment_journal.sepa_receivable_flavor = 'pain.008.001.02'
+ >>> payment_journal.save()
+
+Create a supplier invoice with a payment means::
+
+ >>> invoice = Invoice(type='in')
+ >>> invoice.invoice_date = today
+ >>> invoice.party = supplier
+ >>> line = invoice.lines.new()
+ >>> line.account = accounts['expense']
+ >>> line.quantity = 1
+ >>> line.unit_price = Decimal('10.0000')
+ >>> _ = invoice.payment_means.new(instrument=supplier_account2)
+ >>> invoice.click('post')
+ >>> invoice.state
+ 'posted'
+
+Try to pay with a different bank account::
+
+ >>> line_to_pay, = invoice.lines_to_pay
+ >>> pay_line = Wizard('account.move.line.pay', [line_to_pay])
+ >>> pay_line.execute('next_')
+ >>> pay_line.execute('next_')
+ >>> payment, = pay_line.actions[0]
+ >>> payment.sepa_payable_bank_account_number, = supplier_account1.numbers
+ >>> payment.click('submit')
+ Traceback (most recent call last):
+ ...
+ PaymentMeanWarning: ...
+
+Pay with no bank account::
+
+ >>> payment.sepa_payable_bank_account_number = None
+ >>> payment.click('submit')
+ >>> payment.state
+ 'submitted'
+ >>> payment.click('approve')
+ >>> _ = payment.click('process_wizard')
+ >>> assertEqual(
+ ... payment.sepa_payable_bank_account_number,
+ ... supplier_account2.numbers[0])
diff -r 7feb597bd64a -r 221bab7434f5 modules/account_payment_sepa/tryton.cfg
--- a/modules/account_payment_sepa/tryton.cfg Mon Feb 16 12:01:18 2026 +0100
+++ b/modules/account_payment_sepa/tryton.cfg Fri Feb 13 14:22:58 2026 +0100
@@ -37,3 +37,4 @@
[register account_invoice]
model:
account.InvoicePaymentMean
+ payment.Payment_Invoice