Files
enterprise-addons/l10n_uk_bacs/tests/test_bacs.py
T

388 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
from odoo import Command, fields
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.addons.l10n_uk_bacs.models.account_journal import format_communication
from odoo.tests import tagged
import itertools
import datetime
import base64
@tagged('post_install', '-at_install')
class TestBACS(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env.user.group_ids |= cls.env.ref('account.group_validate_bank_account')
cls.env.ref('base.GBP').active = True
cls.bank_barclays = cls.env['res.bank'].create({
'name': 'BARCLAYS BANK PLC',
'bic': 'BARCGB22XXX',
})
cls.bank_hsbc = cls.env['res.bank'].create({
'name': 'HSBC',
'bic': 'HBUKGB4BXXX',
})
cls.company_data['company'].write({
'country_id': cls.env.ref('base.be').id,
'bacs_sun': '123456',
})
cls.bank_journal = cls.company_data['default_journal_bank']
cls.bank_journal.write({
'bank_id': cls.bank_barclays.id,
'bank_acc_number': 'GB11BARC20039525689371',
'currency_id': cls.env.ref('base.GBP').id,
})
cls.bacs_dc = cls.bank_journal.outbound_payment_method_line_ids.filtered(lambda l: l.code == 'bacs_dc')
cls.bacs_dc_method = cls.env.ref('l10n_uk_bacs.payment_method_bacs_dc')
cls.bacs_dd = cls.bank_journal.inbound_payment_method_line_ids.filtered(lambda l: l.code == 'bacs_dd')
cls.bacs_dd_method = cls.env.ref('l10n_uk_bacs.payment_method_bacs_dd')
def create_account(self, number, partner, bank, clearing_number=False):
vals = {
'acc_number': number,
'partner_id': partner.id,
'bank_id': bank.id,
'allow_out_payment': True,
}
if clearing_number:
vals['clearing_number'] = clearing_number
return self.env['res.partner.bank'].create(vals)
def create_ddi(self, partner, partner_bank, company, payment_journal):
return self.env['bacs.ddi'].create({
'partner_bank_id': partner_bank.id,
'start_date': fields.Date.today(),
'partner_id': partner.id,
'company_id': company.id,
'payment_journal_id': payment_journal.id
})
def create_invoice(self, partner):
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': partner.id,
'currency_id': self.env.ref('base.GBP').id,
'payment_reference': 'invoice to client',
'invoice_line_ids': [(0, 0, {
'product_id': self.env['product.product'].create({'name': 'A Test Product'}).id,
'quantity': 1,
'price_unit': 42,
'name': 'something',
})],
})
invoice.action_post()
return invoice
def pay_with_mandate(self, invoice, mandate):
self.env['account.payment.register'].with_context(active_model='account.move', active_ids=invoice.ids).create({
'payment_date': invoice.invoice_date_due or invoice.invoice_date,
'journal_id': mandate.payment_journal_id.id,
'payment_method_line_id': self.bacs_dd.id,
})._create_payments()
def create_payment(self, partner, amount, payment_method, journal, date, payment_type, partner_bank=None):
return self.env['account.payment'].create({
'journal_id': journal.id,
'payment_method_line_id': payment_method.id,
'payment_type': payment_type,
'date': date,
'amount': amount,
'partner_id': partner.id,
'partner_bank_id': partner_bank.id if partner_bank else False,
})
def verify_bacs_file_headers(self, company, batch_payment, header_lines):
# test vol1
self.assertEqual(len(header_lines[0]), 80)
self.assertEqual(header_lines[0][:4], 'VOL1')
self.assertEqual(header_lines[0][4:10], batch_payment.bacs_submission_serial)
self.assertEqual(header_lines[0][10:41], ' ' * 31)
self.assertEqual(header_lines[0][41:47], company.bacs_sun)
self.assertEqual(header_lines[0][47:79], ' ' * 32)
self.assertEqual(header_lines[0][79:], '1')
# test HDR1
self.assertEqual(len(header_lines[1]), 80)
self.assertEqual(header_lines[1][:5], 'HDR1A')
self.assertEqual(header_lines[1][5:11], company.bacs_sun)
self.assertEqual(header_lines[1][11:15], 'S 1')
self.assertEqual(header_lines[1][15:21], company.bacs_sun)
self.assertEqual(header_lines[1][21:27], batch_payment.bacs_submission_serial)
self.assertEqual(header_lines[1][27:35], '00010001')
self.assertEqual(header_lines[1][35:41], ' ' * 6)
self.assertEqual(header_lines[1][41:47], batch_payment.date.strftime(' %y%j'))
self.assertEqual(header_lines[1][47:53], batch_payment.bacs_expiry_date.strftime(' %y%j'))
self.assertEqual(header_lines[1][53:], ' ' * 27)
# test HDR2
self.assertEqual(len(header_lines[2]), 80)
self.assertEqual(header_lines[2][:5], 'HDR2F')
self.assertEqual(header_lines[2][5:10], '02000')
self.assertEqual(header_lines[2][10:15], '00100')
self.assertEqual(header_lines[2][15:], ' ' * 65)
# test UHL1
self.assertEqual(len(header_lines[3]), 80)
self.assertEqual(header_lines[3][:4], 'UHL1')
self.assertEqual(header_lines[3][4:10], ' ' * 6 if batch_payment.bacs_multi_mode else batch_payment.bacs_processing_date.strftime(' %y%j'))
self.assertEqual(header_lines[3][10:20], company.bacs_sun.ljust(10))
self.assertEqual(header_lines[3][20:28], '0' * 8)
self.assertEqual(header_lines[3][28:37], '4 MULTI ' if batch_payment.bacs_multi_mode else '1 DAILY ')
self.assertEqual(header_lines[3][37:], ' ' * 43)
def verify_contra_line(self, company, payments, contra_line, is_multi, payment_method_code):
self.assertEqual(len(contra_line), 106 if is_multi else 100)
self.assertEqual(contra_line[:14], self.bank_journal.bank_account_id.sanitized_acc_number[8:])
self.assertEqual(contra_line[14:15], '0')
self.assertEqual(contra_line[15:17], '99' if payment_method_code == 'bacs_dd' else '17')
self.assertEqual(contra_line[17:31], self.bank_journal.bank_account_id.sanitized_acc_number[8:])
self.assertEqual(contra_line[31:35], ' ' * 4)
contra_amount = sum([int(payment.amount * 100) for payment in payments])
self.assertEqual(contra_line[35:46], str(contra_amount).rjust(11, '0'))
self.assertEqual(contra_line[64:82], 'CONTRA'.ljust(18))
self.assertEqual(contra_line[82:100], format_communication(company.name).ljust(18))
if is_multi:
self.assertEqual(contra_line[100:], payments[0].date.strftime(' %y%j'))
def verify_payment_line(self, company, payment, line, is_multi, payment_method_code):
self.assertEqual(len(line), 106 if is_multi else 100)
self.assertEqual(line[:14], payment.partner_bank_id.sanitized_acc_number[8:] if payment_method_code == 'bacs_dc' else payment.bacs_ddi_id.partner_bank_id.sanitized_acc_number[8:])
self.assertEqual(line[14:15], '0')
self.assertIn(line[15:17], ['01', '17', '18', '19'] if payment_method_code == 'bacs_dd' else ['99'])
self.assertEqual(line[17:31], self.bank_journal.bank_account_id.sanitized_acc_number[8:])
self.assertEqual(line[31:35], ' ' * 4)
self.assertEqual(line[35:46], str(int(payment.amount * 100)).rjust(11, '0'))
self.assertEqual(line[46:64], format_communication(company.name[:18]).ljust(18))
self.assertEqual(line[82:100], format_communication(payment.partner_id.name[:18]).ljust(18))
if is_multi:
self.assertEqual(line[100:], payment.date.strftime(' %y%j'))
def verify_bacs_file_payments(self, company, batch_payment, payment_lines):
if not batch_payment.bacs_multi_mode:
self.assertEqual(len(payment_lines), len(batch_payment.payment_ids) + 1)
for i, payment_line in enumerate(payment_lines):
if i == len(payment_lines) - 1:
self.verify_contra_line(company, batch_payment.payment_ids, payment_line, False, batch_payment.payment_method_id.code)
else:
self.verify_payment_line(company, batch_payment.payment_ids[i], payment_line, False, batch_payment.payment_method_id.code)
else:
# group payments by date and each date should have tranaction reccords of the date followed by a contra line
payments_by_date = itertools.groupby(batch_payment.payment_ids.sorted(key=lambda p: p.date), key=lambda p: p.date)
self.assertEqual(len(payment_lines), len(batch_payment.payment_ids) + len(list(payments_by_date)))
start = 0
for _, payments in payments_by_date:
payments = list(payments)
for i, payment in enumerate(payments):
self.verify_payment_line(company, payment, payment_lines[i + start], True, batch_payment.payment_method_id.code)
self.verify_contra_line(company, payments, payment_lines[len(payments) + start], True, batch_payment.payment_method_id.code)
start += len(payments) + 1
def verify_bacs_file_footers(self, company, batch_payment, footer_lines):
# test EOF1
self.assertEqual(len(footer_lines[0]), 80)
self.assertEqual(footer_lines[0][:5], 'EOF1A')
self.assertEqual(footer_lines[0][5:11], company.bacs_sun)
self.assertEqual(footer_lines[0][11:15], 'S 1')
self.assertEqual(footer_lines[0][15:21], company.bacs_sun)
self.assertEqual(footer_lines[0][21:27], batch_payment.bacs_submission_serial)
self.assertEqual(footer_lines[0][27:35], '00010001')
self.assertEqual(footer_lines[0][35:41], ' ' * 6)
self.assertEqual(footer_lines[0][41:47], batch_payment.date.strftime(' %y%j'))
self.assertEqual(footer_lines[0][47:53], batch_payment.bacs_expiry_date.strftime(' %y%j'))
self.assertEqual(footer_lines[0][53:], ' ' * 27)
# test EOF2
self.assertEqual(len(footer_lines[1]), 80)
self.assertEqual(footer_lines[1][:5], 'EOF2F')
self.assertEqual(footer_lines[1][5:10], '02000')
self.assertEqual(footer_lines[1][10:15], '00100')
self.assertEqual(footer_lines[1][15:], ' ' * 65)
# test UTL1
self.assertEqual(len(footer_lines[2]), 80)
self.assertEqual(footer_lines[2][:4], 'UTL1')
total = sum([int(payment.amount * 100) for payment in batch_payment.payment_ids])
self.assertEqual(footer_lines[2][4:17], str(total).rjust(13, '0'))
self.assertEqual(footer_lines[2][17:30], footer_lines[2][4:17])
transaction_record_count = str(len(batch_payment.payment_ids)).rjust(7, '0')
contra_record_count = str(len(set(payment.date for payment in batch_payment.payment_ids))).rjust(7, '0') if batch_payment.bacs_multi_mode else '0000001'
code = batch_payment.payment_method_id.code
self.assertEqual(footer_lines[2][30:37], transaction_record_count if code == 'bacs_dd' else contra_record_count)
self.assertEqual(footer_lines[2][37:44], contra_record_count if code == 'bacs_dd' else transaction_record_count)
self.assertEqual(footer_lines[2][44:], ' ' * 36)
def verify_bacs_file(self, company, batch):
binary_data = base64.b64decode(batch.export_file)
file_string = binary_data.decode('utf-8')
split_file = file_string.rstrip('\n').split('\n')
header_lines = split_file[:4]
payment_lines = split_file[4:-3]
footer_lines = split_file[-3:]
self.assertEqual(len(header_lines), 4, "There should be 4 header lines in the file")
self.assertEqual(len(footer_lines), 3, "There should be 3 footer lines in the file")
self.verify_bacs_file_headers(company, batch, header_lines)
self.verify_bacs_file_payments(company, batch, payment_lines)
self.verify_bacs_file_footers(company, batch, footer_lines)
def verify_multi_payments(self, company, partner, multi_mode, payment_method, payment_method_line, payment_type, partner_bank=None):
payment_1 = self.create_payment(partner, 42, payment_method_line, self.bank_journal, datetime.date.today() + datetime.timedelta(days=10), payment_type, partner_bank)
payment_1.action_post()
payment_2 = self.create_payment(partner, 1337, payment_method_line, self.bank_journal, datetime.date.today() + datetime.timedelta(days=15), payment_type, partner_bank)
payment_2.action_post()
payment_3 = self.create_payment(partner, 21, payment_method_line, self.bank_journal, datetime.date.today() + datetime.timedelta(days=20), payment_type, partner_bank)
payment_3.action_post()
payment_4 = self.create_payment(partner, 1916, payment_method_line, self.bank_journal, datetime.date.today() + datetime.timedelta(days=20), payment_type, partner_bank)
payment_4.action_post()
batch = self.env['account.batch.payment'].create({
'batch_type': payment_type,
'bacs_processing_date': fields.Date.today(),
'bacs_multi_mode': multi_mode,
'payment_ids': [(4, payment.id, None) for payment in [payment_1, payment_2, payment_3, payment_4]],
'journal_id': self.bank_journal.id,
'payment_method_id': payment_method.id,
})
wizard_action = batch.validate_batch()
self.assertFalse(wizard_action, "Validation wizard should not have returned an action")
self.verify_bacs_file(company, batch)
def testBacsDirectDebit(self):
company = self.env.company
partner_boundagani = self.env['res.partner'].create({'name': 'Boundagani'})
partner_bank_boundagani = self.create_account('GB03BARC20031819726663', partner_boundagani, self.bank_hsbc)
ddi_boundagani = self.create_ddi(partner_boundagani, partner_bank_boundagani, self.env.company, self.bank_journal)
ddi_boundagani.action_validate_ddi()
invoice_boundagani = self.create_invoice(partner_boundagani)
self.pay_with_mandate(invoice_boundagani, ddi_boundagani)
payment_boundagani = invoice_boundagani.matched_payment_ids
self.assertEqual(invoice_boundagani.payment_state, self.env['account.move']._get_invoice_in_payment_state(), 'This invoice should have been paid thanks to the mandate')
self.assertEqual(invoice_boundagani.bacs_ddi_id, ddi_boundagani, 'The invoice should have the right mandate')
# test compute_from_moves
ddi_boundagani._compute_from_moves()
self.assertEqual(ddi_boundagani.paid_invoices_len, 1)
self.assertEqual(ddi_boundagani.paid_invoice_ids, invoice_boundagani)
self.assertEqual(ddi_boundagani.payments_len, 1)
self.assertEqual(ddi_boundagani.payment_ids, payment_boundagani)
# test single payment
batch = self.env['account.batch.payment'].create({
'bacs_processing_date': fields.Date.today(),
'bacs_multi_mode': False,
'payment_ids': [(4, payment_boundagani.id, None)],
'journal_id': self.bank_journal.id,
'payment_method_id': self.bacs_dd_method.id,
'batch_type': 'inbound',
})
wizard_action = batch.validate_batch()
self.assertFalse(wizard_action, "Validation wizard should not have returned an action")
self.verify_bacs_file(company, batch)
# test multi payment with single mode
self.verify_multi_payments(company, partner_boundagani, False, self.bacs_dd_method, self.bacs_dd, 'inbound')
# test multi payment with multi mode
self.verify_multi_payments(company, partner_boundagani, True, self.bacs_dd_method, self.bacs_dd, 'inbound')
def testBacsDirectCredit(self):
company = self.env.company
vendor_superlux = self.env['res.partner'].create({'name': 'Superlux'})
vendor_bank_superlux = self.create_account('GB70BARC20038066716256', vendor_superlux, self.bank_hsbc)
payment_superlux = self.create_payment(vendor_superlux, 42, self.bacs_dc, self.bank_journal, datetime.date.today() + datetime.timedelta(days=10), 'outbound', vendor_bank_superlux)
payment_superlux.action_post()
# test single payment
batch = self.env['account.batch.payment'].create({
'bacs_processing_date': fields.Date.today(),
'bacs_multi_mode': False,
'payment_ids': [(4, payment_superlux.id, None)],
'journal_id': self.bank_journal.id,
'payment_method_id': self.bacs_dc_method.id,
'batch_type': 'outbound',
})
wizard_action = batch.validate_batch()
self.assertFalse(wizard_action, "Validation wizard should not have returned an action")
self.verify_bacs_file(company, batch)
# test multi payment with single mode
self.verify_multi_payments(company, vendor_superlux, False, self.bacs_dc_method, self.bacs_dc, 'outbound', vendor_bank_superlux)
# test multi payment with multi mode
self.verify_multi_payments(company, vendor_superlux, True, self.bacs_dc_method, self.bacs_dc, 'outbound', vendor_bank_superlux)
def testBacsLocalBankAccount_vendor(self):
"""
BACS Direct Credit with a local UK bank account (sort code + bare account number).
"""
vendor_local = self.env['res.partner'].create({'name': 'Vendor Local UK'})
vendor_bank_local = self.create_account(
'12345678', vendor_local, self.bank_barclays, clearing_number='20-30-40',
)
payment_dc = self.create_payment(
vendor_local, 100, self.bacs_dc, self.bank_journal,
datetime.date.today() + datetime.timedelta(days=5),
'outbound', vendor_bank_local,
)
payment_dc.action_post()
self.assertEqual(payment_dc.state, 'in_process')
batch_dc = self.env['account.batch.payment'].create({
'batch_type': 'outbound',
'bacs_processing_date': fields.Date.today(),
'bacs_multi_mode': False,
'payment_ids': [Command.link(payment_dc.id)],
'journal_id': self.bank_journal.id,
'payment_method_id': self.bacs_dc_method.id,
})
wizard_action = batch_dc.validate_batch()
self.assertFalse(wizard_action)
raw_dc = base64.b64decode(batch_dc.export_file).decode('utf-8')
lines_dc = raw_dc.rstrip('\n').split('\n')
dc_transaction = lines_dc[4]
self.assertEqual(dc_transaction[:6], '203040', "Sort code (hyphens stripped) must appear in bytes 0–5")
self.assertEqual(dc_transaction[6:14], '12345678', "Account number must appear in bytes 6–13")
def testBacsLocalBankAccount_customer(self):
"""
BACS Direct Debit via DDI with a local UK bank account (sort code + bare account number).
"""
customer_local = self.env['res.partner'].create({'name': 'Customer Local UK'})
customer_bank_local = self.create_account(
'87654321', customer_local, self.bank_barclays, clearing_number='10-20-30',
)
ddi_local = self.create_ddi(customer_local, customer_bank_local, self.env.company, self.bank_journal)
ddi_local.action_validate_ddi()
self.assertEqual(ddi_local.state, 'active')
invoice_local = self.create_invoice(customer_local)
self.pay_with_mandate(invoice_local, ddi_local)
self.assertEqual(
invoice_local.payment_state,
self.env['account.move']._get_invoice_in_payment_state(),
)
payment_dd = invoice_local.matched_payment_ids
batch_dd = self.env['account.batch.payment'].create({
'batch_type': 'inbound',
'bacs_processing_date': fields.Date.today(),
'bacs_multi_mode': False,
'payment_ids': [Command.link(payment_dd.id)],
'journal_id': self.bank_journal.id,
'payment_method_id': self.bacs_dd_method.id,
})
wizard_action = batch_dd.validate_batch()
self.assertFalse(wizard_action)
raw_dd = base64.b64decode(batch_dd.export_file).decode('utf-8')
lines_dd = raw_dd.rstrip('\n').split('\n')
dd_transaction = lines_dd[4]
self.assertEqual(dd_transaction[:6], '102030', "Sort code (hyphens stripped) must appear in bytes 0–5")
self.assertEqual(dd_transaction[6:14], '87654321', "Account number must appear in bytes 6–13")