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

Reply via email to