# This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import datetime as dt import os from decimal import Decimal from io import BytesIO, open from unittest.mock import Mock, patch from lxml import etree from trytond.exceptions import UserError from trytond.modules.account.tests import create_chart from trytond.modules.account_payment_sepa.payment import CAMT054 from trytond.modules.company.tests import ( CompanyTestMixin, create_company, set_company) from trytond.modules.currency.tests import create_currency from trytond.modules.party.tests import PartyCheckReplaceMixin from trytond.pool import Pool from trytond.tests.test_tryton import ModuleTestCase, with_transaction from trytond.transaction import Transaction def setup_environment(): pool = Pool() Address = pool.get('party.address') Party = pool.get('party.party') Bank = pool.get('bank') Identifier = pool.get('party.identifier') currency = create_currency('EUR') company = create_company(currency=currency) sepa = Identifier(party=company.party, code='ES23ZZZ47690558N', type='eu_at_02') sepa.save() bank_party = Party(name='European Bank') bank_party.save() bank = Bank(party=bank_party, bic='BICODEBBXXX') bank.save() customer = Party(name='Customer') address = Address(street='street', postal_code='1234', city='City') customer.addresses = [address] customer.save() return { 'company': company, 'bank': bank, 'customer': customer, } def setup_accounts(bank, company, customer): pool = Pool() Account = pool.get('bank.account') return Account.create([{ 'bank': bank, 'owners': [('add', [company.party])], 'currency': company.currency.id, 'numbers': [('create', [{ 'type': 'iban', 'number': 'ES8200000000000000000000', }])]}, { 'bank': bank, 'owners': [('add', [customer])], 'currency': company.currency.id, 'numbers': [('create', [{ 'type': 'iban', 'number': 'ES3600000000050000000001', }])]}]) def setup_mandate(company, customer, account): pool = Pool() Mandate = pool.get('account.payment.sepa.mandate') Date = pool.get('ir.date') return Mandate.create([{ 'company': company, 'party': customer, 'address': customer.addresses[0], 'account_number': account.numbers[0], 'identification': 'MANDATE', 'type': 'recurrent', 'sequence_type_rcur': False, 'signature_date': Date.today(), 'state': 'validated', }])[0] def setup_journal(flavor, kind, company, account): pool = Pool() Journal = pool.get('account.payment.journal') journal = Journal() journal.name = flavor journal.company = company journal.currency = company.currency journal.process_method = 'sepa' journal.sepa_bank_account_number = account.numbers[0] journal.sepa_payable_flavor = 'pain.001.001.03' journal.sepa_receivable_flavor = 'pain.008.001.02' setattr(journal, 'sepa_%s_flavor' % kind, flavor) journal.save() return journal def validate_file(flavor, kind, xsd=None): 'Test generated files are valid' pool = Pool() Payment = pool.get('account.payment') PaymentGroup = pool.get('account.payment.group') Date = pool.get('ir.date') ProcessPayment = pool.get('account.payment.process', type='wizard') if xsd is None: xsd = flavor environment = setup_environment() company = environment['company'] bank = environment['bank'] customer = environment['customer'] with set_company(company): company_account, customer_account = setup_accounts( bank, company, customer) setup_mandate(company, customer, customer_account) journal = setup_journal(flavor, kind, company, company_account) payment, = Payment.create([{ 'company': company, 'party': customer, 'journal': journal, 'kind': kind, 'amount': Decimal('1000.0'), 'state': 'approved' if kind == 'payable' else 'submitted', 'reference': 'PAYMENT', 'date': Date.today(), }]) session_id, _, _ = ProcessPayment.create() process_payment = ProcessPayment(session_id) with Transaction().set_context( active_model=Payment.__name__, active_ids=[payment.id]): _, data = process_payment.do_process(None) group, = PaymentGroup.browse(data['res_id']) message, = group.sepa_messages assert message.type == 'out', message.type assert message.state == 'waiting', message.state sepa_string = bytes(message.message) sepa_xml = etree.fromstring(sepa_string) schema_file = os.path.join(os.path.dirname(__file__), '%s.xsd' % xsd) schema = etree.XMLSchema(etree.parse(schema_file)) schema.assertValid(sepa_xml) class AccountPaymentSepaTestCase( PartyCheckReplaceMixin, CompanyTestMixin, ModuleTestCase): 'Test Account Payment SEPA module' module = 'account_payment_sepa' @with_transaction() def test_pain001_001_03(self): 'Test pain001.001.03 xsd validation' validate_file('pain.001.001.03', 'payable') @with_transaction() def test_pain001_001_05(self): 'Test pain001.001.05 xsd validation' validate_file('pain.001.001.05', 'payable') @with_transaction() def test_pain001_003_03(self): 'Test pain001.003.03 xsd validation' validate_file('pain.001.003.03', 'payable') @with_transaction() def test_pain008_001_02(self): 'Test pain008.001.02 xsd validation' validate_file('pain.008.001.02', 'receivable') @with_transaction() def test_pain008_001_04(self): 'Test pain008.001.04 xsd validation' validate_file('pain.008.001.04', 'receivable') @with_transaction() def test_pain008_003_02(self): 'Test pain008.003.02 xsd validation' validate_file('pain.008.003.02', 'receivable') @with_transaction() def test_sepa_mandate_sequence(self): 'Test SEPA mandate sequence' pool = Pool() Configuration = pool.get('account.configuration') Sequence = pool.get('ir.sequence') SequenceType = pool.get('ir.sequence.type') Party = pool.get('party.party') Mandate = pool.get('account.payment.sepa.mandate') party = Party(name='Test') party.save() company = create_company() with set_company(company): mandate = Mandate(party=party) mandate.save() self.assertFalse(mandate.identification) sequence_type, = SequenceType.search([ ('name', '=', "SEPA Mandate"), ], limit=1) sequence = Sequence(name="Test", sequence_type=sequence_type) sequence.save() config = Configuration(1) config.sepa_mandate_sequence = sequence config.save() mandate = Mandate(party=party) mandate.save() self.assertTrue(mandate.identification) @with_transaction() def test_identification_unique(self): 'Test unique identification constraint' pool = Pool() Party = pool.get('party.party') Mandate = pool.get('account.payment.sepa.mandate') same_id = '1' party = Party(name='Test') party.save() company = create_company() with set_company(company): mandate = Mandate(party=party, identification=same_id) mandate.save() for i in range(2): mandate = Mandate(party=party) mandate.save() mandate = Mandate(party=party, identification='') mandate.save() self.assertEqual(mandate.identification, None) Mandate.write([mandate], { 'identification': '', }) self.assertEqual(mandate.identification, None) self.assertRaises(UserError, Mandate.create, [{ 'party': party.id, 'identification': same_id, }]) @with_transaction() def test_payment_sepa_bank_account_number(self): 'Test Payment.sepa_bank_account_number' pool = Pool() Payment = pool.get('account.payment') Mandate = pool.get('account.payment.sepa.mandate') AccountNumber = pool.get('bank.account.number') Party = pool.get('party.party') BankAccount = pool.get('bank.account') company = create_company() with set_company(company): create_chart(company) account_number = AccountNumber() mandate = Mandate(account_number=account_number) payment = Payment(kind='receivable', sepa_mandate=mandate) self.assertEqual(id(payment.sepa_bank_account_number), id(account_number)) other_account_number = AccountNumber(type='other') iban_account_number = AccountNumber(type='iban') bank_account = BankAccount( numbers=[other_account_number, iban_account_number]) party = Party( bank_accounts_used=[bank_account]) payment = Payment( kind='payable', party=party, sepa_payable_bank_account_number=None) self.assertEqual(id(payment.sepa_bank_account_number), id(iban_account_number)) payment.sepa_payable_bank_account_number = iban_account_number party.bank_accounts_used = [] self.assertEqual( payment.sepa_bank_account_number, iban_account_number) @with_transaction() def test_payment_sequence_type(self): 'Test payment sequence type' pool = Pool() Date = pool.get('ir.date') Payment = pool.get('account.payment') ProcessPayment = pool.get('account.payment.process', type='wizard') environment = setup_environment() company = environment['company'] bank = environment['bank'] customer = environment['customer'] with set_company(company): company_account, customer_account = setup_accounts( bank, company, customer) mandate = setup_mandate(company, customer, customer_account) journal = setup_journal('pain.008.001.02', 'receivable', company, company_account) self.assertFalse(mandate.has_payments) self.assertEqual(mandate.sequence_type, 'FRST') mandate.sequence_type_rcur = True self.assertEqual(mandate.sequence_type, 'RCUR') mandate.sequence_type_rcur = False payment, = Payment.create([{ 'company': company, 'party': customer, 'journal': journal, 'kind': 'receivable', 'amount': Decimal('1000.0'), 'state': 'submitted', 'reference': 'PAYMENT', 'date': Date.today(), }]) session_id, _, _ = ProcessPayment.create() process_payment = ProcessPayment(session_id) with Transaction().set_context( active_model=Payment.__name__, active_ids=[payment.id]): _, data = process_payment.do_process(None) self.assertEqual(payment.sepa_mandate_sequence_type, 'FRST') self.assertTrue(payment.sepa_mandate.has_payments) payments = Payment.create([{ 'company': company, 'party': customer, 'journal': journal, 'kind': 'receivable', 'amount': Decimal('2000.0'), 'state': 'submitted', 'reference': 'PAYMENT', 'date': Date.today(), }, { 'company': company, 'party': customer, 'journal': journal, 'kind': 'receivable', 'amount': Decimal('3000.0'), 'state': 'submitted', 'reference': 'PAYMENT', 'date': Date.today(), }, ]) session_id, _, _ = ProcessPayment.create() process_payment = ProcessPayment(session_id) payment_ids = [p.id for p in payments] with Transaction().set_context( active_model=Payment.__name__, active_ids=payment_ids): _, data = process_payment.do_process(None) for payment in payments: self.assertEqual(payment.sepa_mandate_sequence_type, 'RCUR') self.assertTrue(payment.sepa_info_id) self.assertIsNotNone(payment.group.sepa_id) @with_transaction() def test_payment_sequence_type_ordered_date(self): "Test sequence type by ordered date" pool = Pool() Date = pool.get('ir.date') Payment = pool.get('account.payment') ProcessPayment = pool.get('account.payment.process', type='wizard') environment = setup_environment() company = environment['company'] bank = environment['bank'] customer = environment['customer'] with set_company(company): company_account, customer_account = setup_accounts( bank, company, customer) mandate = setup_mandate(company, customer, customer_account) journal = setup_journal('pain.008.001.02', 'receivable', company, company_account) self.assertEqual(mandate.sequence_type, 'FRST') payments = Payment.create([{ 'company': company, 'party': customer, 'journal': journal, 'kind': 'receivable', 'amount': Decimal('1000.0'), 'state': 'submitted', 'reference': 'PAYMENT', 'date': Date.today() + dt.timedelta(days=1), }, { 'company': company, 'party': customer, 'journal': journal, 'kind': 'receivable', 'amount': Decimal('1500.0'), 'state': 'submitted', 'reference': 'PAYMENT', 'date': Date.today(), }, { 'company': company, 'party': customer, 'journal': journal, 'kind': 'receivable', 'amount': Decimal('2000.0'), 'state': 'submitted', 'reference': 'PAYMENT', 'date': Date.today(), }]) session_id, _, _ = ProcessPayment.create() process_payment = ProcessPayment(session_id) payment_ids = [p.id for p in payments] with Transaction().set_context( active_model=Payment.__name__, active_ids=payment_ids): _, data = process_payment.do_process(None) self.assertEqual( [p.sepa_mandate_sequence_type for p in payments], ['RCUR', 'FRST', 'RCUR']) @with_transaction() def handle_camt054(self, flavor): 'Handle camt.054' pool = Pool() Message = pool.get('account.payment.sepa.message') message_file = os.path.join(os.path.dirname(__file__), '%s.xml' % flavor) with open(message_file, 'rb') as fp: message = fp.read() namespace = Message.get_namespace(message) self.assertEqual(namespace, 'urn:iso:std:iso:20022:tech:xsd:%s' % flavor) payment = Mock() Payment = Mock() Payment.search.return_value = [payment] handler = CAMT054(BytesIO(message), Payment) self.assertEqual(handler.msg_id, 'AAAASESS-FP-00001') Payment.search.assert_called_with([ ('sepa_end_to_end_id', '=', 'MUELL/FINP/RA12345'), ('kind', '=', 'payable'), ]) Payment.succeed.assert_called_with([payment]) payment.reset_mock() Payment.reset_mock() with patch.object(CAMT054, 'is_returned') as is_returned: is_returned.return_value = True handler = CAMT054(BytesIO(message), Payment) Payment.save.assert_called_with([payment]) Payment.fail.assert_called_with([payment]) def test_camt054_001_01(self): 'Test camt.054.001.01 handling' self.handle_camt054('camt.054.001.01') def test_camt054_001_02(self): 'Test camt.054.001.02 handling' self.handle_camt054('camt.054.001.02') def test_camt054_001_03(self): 'Test camt.054.001.03 handling' self.handle_camt054('camt.054.001.03') def test_camt054_001_04(self): 'Test camt.054.001.04 handling' self.handle_camt054('camt.054.001.04') @with_transaction() def test_sepa_mandate_report(self): 'Test sepa mandate report' pool = Pool() Report = pool.get('account.payment.sepa.mandate', type='report') environment = setup_environment() company = environment['company'] bank = environment['bank'] customer = environment['customer'] with set_company(company): company_account, customer_account = setup_accounts( bank, company, customer) mandate = setup_mandate(company, customer, customer_account) oext, content, _, _ = Report.execute([mandate.id], {}) self.assertEqual(oext, 'odt') self.assertTrue(content) del ModuleTestCase