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