diff --git a/modules/account_payment/account.py b/modules/account_payment/account.py
index e579199398..0bb63deb73 100644
--- a/modules/account_payment/account.py
+++ b/modules/account_payment/account.py
@@ -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'
diff --git a/modules/account_payment/exceptions.py b/modules/account_payment/exceptions.py
index de61c32ae9..daa72839ba 100644
--- a/modules/account_payment/exceptions.py
+++ b/modules/account_payment/exceptions.py
@@ -17,6 +17,10 @@ class ReconciledWarning(UserWarning):
pass
+class PaymentMeanWarning(UserWarning):
+ pass
+
+
class PaymentValidationError(ValidationError):
pass
diff --git a/modules/account_payment/message.xml b/modules/account_payment/message.xml
index 8b0c80fec9..16951f6677 100644
--- a/modules/account_payment/message.xml
+++ b/modules/account_payment/message.xml
@@ -15,6 +15,10 @@ this repository contains the full copyright notices and license terms. -->
The line "%(line)s" of payment "%(payment)s" is already reconciled.
+
+ The journal "%(journal)s" of payment "%(payment)s" is not valid with the payment means of invoice "%(invoice)s".
+
+
The %(type)s "%(reference)s" on payment "%(payment)s" is not valid.
diff --git a/modules/account_payment/payment.py b/modules/account_payment/payment.py
index 4c5d4904f0..c167e5b893 100644
--- a/modules/account_payment/payment.py
+++ b/modules/account_payment/payment.py
@@ -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'
diff --git a/modules/account_payment/tests/scenario_account_payment_invoice_payment_means.rst b/modules/account_payment/tests/scenario_account_payment_invoice_payment_means.rst
index 848004e351..c3eadc4250 100644
--- a/modules/account_payment/tests/scenario_account_payment_invoice_payment_means.rst
+++ b/modules/account_payment/tests/scenario_account_payment_invoice_payment_means.rst
@@ -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'
diff --git a/modules/account_payment_sepa/account.py b/modules/account_payment_sepa/account.py
index e8f580e37a..f24a966482 100644
--- a/modules/account_payment_sepa/account.py
+++ b/modules/account_payment_sepa/account.py
@@ -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
diff --git a/modules/account_payment_sepa/payment.py b/modules/account_payment_sepa/payment.py
index b86e00cde3..a315397ea8 100644
--- a/modules/account_payment_sepa/payment.py
+++ b/modules/account_payment_sepa/payment.py
@@ -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(
diff --git a/modules/account_payment_sepa/tests/scenario_account_payment_sepa_invoice_payment_means.rst b/modules/account_payment_sepa/tests/scenario_account_payment_sepa_invoice_payment_means.rst
new file mode 100644
index 0000000000..135f57458e
--- /dev/null
+++ b/modules/account_payment_sepa/tests/scenario_account_payment_sepa_invoice_payment_means.rst
@@ -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 --git a/modules/account_payment_sepa/tryton.cfg b/modules/account_payment_sepa/tryton.cfg
index d8132a2f78..fef8dd9b6a 100644
--- a/modules/account_payment_sepa/tryton.cfg
+++ b/modules/account_payment_sepa/tryton.cfg
@@ -37,3 +37,4 @@ report:
[register account_invoice]
model:
account.InvoicePaymentMean
+ payment.Payment_Invoice