From 4a9127f9a66aa5b3245b8262ae7f3c7d53a608cb Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Mon, 22 Apr 2019 03:46:25 +0200 Subject: [PATCH 1/5] feat(accounts): validate IBAN --- .../doctype/bank_account/bank_account.py | 24 ++++++++++++++ .../doctype/bank_account/test_bank_account.py | 33 ++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index 3c679fa215a..06393e7a54e 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -21,11 +21,35 @@ class BankAccount(Document): def validate(self): self.validate_company() + self.validate_iban() def validate_company(self): if self.is_company_account and not self.company: frappe.throw(_("Company is manadatory for company account")) + def validate_iban(self): + ''' + Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN + ''' + def encode_char(c): + # Position in the alphabet (A=1, B=2, ...) plus nine + return str(9 + ord(c) - 64) + + # remove whitespaces, upper case to get the right number from ord() + iban = ''.join(self.iban.split(' ')).upper() + + # Move country code and checksum from the start to the end + flipped = iban[4:] + iban[:4] + + # Encode characters as numbers + encoded = [encode_char(c) if ord(c) >= 65 and ord(c) <= 90 else c for c in flipped] + + to_check = int(''.join(encoded)) + + if to_check % 97 != 1: + frappe.throw(_('IBAN is not valid')) + + @frappe.whitelist() def make_bank_account(doctype, docname): doc = frappe.new_doc("Bank Account") diff --git a/erpnext/accounts/doctype/bank_account/test_bank_account.py b/erpnext/accounts/doctype/bank_account/test_bank_account.py index 43a3298ec4a..8e1a91bc45f 100644 --- a/erpnext/accounts/doctype/bank_account/test_bank_account.py +++ b/erpnext/accounts/doctype/bank_account/test_bank_account.py @@ -4,9 +4,40 @@ from __future__ import unicode_literals import frappe +from frappe import _ +from frappe import ValidationError import unittest # test_records = frappe.get_test_records('Bank Account') class TestBankAccount(unittest.TestCase): - pass + + def test_validate_iban(self): + valid_ibans = [ + 'GB82 WEST 1234 5698 7654 32', + 'DE91 1000 0000 0123 4567 89', + 'FR76 3000 6000 0112 3456 7890 189' + ] + + invalid_ibans = [ + # wrong checksum (3rd place) + 'GB72 WEST 1234 5698 7654 32', + 'DE81 1000 0000 0123 4567 89', + 'FR66 3000 6000 0112 3456 7890 189' + ] + + bank_account = frappe.get_doc({'doctype':'Bank Account'}) + + for iban in valid_ibans: + bank_account.iban = iban + try: + bank_account.validate_iban() + except ValidationError: + msg = _('BankAccount.validate_iban() failed for valid IBAN {}'.format(iban)) + self.fail(msg=msg) + + for not_iban in invalid_ibans: + bank_account.iban = not_iban + msg = _('BankAccount.validate_iban() accepted invalid IBAN {}'.format(iban)) + with self.assertRaises(ValidationError, msg=msg): + bank_account.validate_iban() From 49f919a4fcf7b4d7b372416fd48d1892e8812203 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Mon, 22 Apr 2019 05:32:35 +0200 Subject: [PATCH 2/5] fix test's error message --- erpnext/accounts/doctype/bank_account/test_bank_account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_account/test_bank_account.py b/erpnext/accounts/doctype/bank_account/test_bank_account.py index 8e1a91bc45f..bd148df33ba 100644 --- a/erpnext/accounts/doctype/bank_account/test_bank_account.py +++ b/erpnext/accounts/doctype/bank_account/test_bank_account.py @@ -38,6 +38,6 @@ class TestBankAccount(unittest.TestCase): for not_iban in invalid_ibans: bank_account.iban = not_iban - msg = _('BankAccount.validate_iban() accepted invalid IBAN {}'.format(iban)) + msg = _('BankAccount.validate_iban() accepted invalid IBAN {}'.format(not_iban)) with self.assertRaises(ValidationError, msg=msg): bank_account.validate_iban() From 70f89462a86e78e33389f668d41e68d35326c6a7 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Mon, 22 Apr 2019 12:27:12 +0200 Subject: [PATCH 3/5] fix: consider empty iban --- erpnext/accounts/doctype/bank_account/bank_account.py | 3 ++- erpnext/accounts/doctype/bank_account/test_bank_account.py | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index 06393e7a54e..aa18b966707 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -21,7 +21,8 @@ class BankAccount(Document): def validate(self): self.validate_company() - self.validate_iban() + if self.validate_iban: + self.validate_iban() def validate_company(self): if self.is_company_account and not self.company: diff --git a/erpnext/accounts/doctype/bank_account/test_bank_account.py b/erpnext/accounts/doctype/bank_account/test_bank_account.py index bd148df33ba..f3bb086fa96 100644 --- a/erpnext/accounts/doctype/bank_account/test_bank_account.py +++ b/erpnext/accounts/doctype/bank_account/test_bank_account.py @@ -28,6 +28,12 @@ class TestBankAccount(unittest.TestCase): bank_account = frappe.get_doc({'doctype':'Bank Account'}) + try: + bank_account.validate_iban() + except AttributeError: + msg = _('BankAccount.validate_iban() failed for empty IBAN') + self.fail(msg=msg) + for iban in valid_ibans: bank_account.iban = iban try: From f3b07495f63784020d3916aeb7e628f2b4a83905 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Mon, 22 Apr 2019 12:38:22 +0200 Subject: [PATCH 4/5] fix typo --- erpnext/accounts/doctype/bank_account/bank_account.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index aa18b966707..e71fca256ea 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -21,7 +21,8 @@ class BankAccount(Document): def validate(self): self.validate_company() - if self.validate_iban: + + if self.iban: self.validate_iban() def validate_company(self): From e534221245a01aff8613e89b3c2556bc6c87e34c Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Wed, 24 Apr 2019 16:22:34 +0200 Subject: [PATCH 5/5] fix: validate IBAN only if it exists --- erpnext/accounts/doctype/bank_account/bank_account.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index e71fca256ea..20ce7ca9a4c 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -21,9 +21,7 @@ class BankAccount(Document): def validate(self): self.validate_company() - - if self.iban: - self.validate_iban() + self.validate_iban() def validate_company(self): if self.is_company_account and not self.company: @@ -33,6 +31,10 @@ class BankAccount(Document): ''' Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN ''' + # IBAN field is optional + if not self.iban: + return + def encode_char(c): # Position in the alphabet (A=1, B=2, ...) plus nine return str(9 + ord(c) - 64)