Check invoice payment means when submitting payment

This commit is contained in:
Cédric Krier committed 2026-02-13 14:22:58 +01:00
1 parent cc1f6e424c
commit 8cb0fb2bbd
9 files changed
+264 -3

No files matched your search

+10
View File
@@ -866,6 +866,16 @@ class InvoicePaymentMean(metaclass=PoolMeta):
'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'
+4
View File
@@ -17,6 +17,10 @@ class ReconciledWarning(UserWarning):
pass
class PaymentMeanWarning(UserWarning):
pass
class PaymentValidationError(ValidationError):
pass
+4
View File
@@ -15,6 +15,10 @@ this repository contains the full copyright notices and license terms. -->
<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>
+31 -1
View File
@@ -28,7 +28,8 @@ from trytond.transaction import Transaction
from trytond.wizard import StateAction, Wizard
from .exceptions import (
OverpayWarning, PaymentValidationError, ReconciledWarning)
OverpayWarning, PaymentMeanWarning, PaymentValidationError,
ReconciledWarning)
KINDS = [
('payable', 'Payable'),
@@ -826,6 +827,35 @@ class Payment_Invoice(metaclass=PoolMeta):
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'
@@ -4,7 +4,9 @@ Invoice Payment Means Scenario
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 @@ Create invoice with payment means::
>>> 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'
+18 -1
View File
@@ -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 @@ class InvoicePaymentMean(metaclass=PoolMeta):
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
+44
View File
@@ -490,6 +490,50 @@ class Payment(metaclass=PoolMeta):
]
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(
@@ -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])
+1
View File
@@ -37,3 +37,4 @@ report:
[register account_invoice]
model:
account.InvoicePaymentMean
payment.Payment_Invoice