Merge pull request #33841 from barredterra/iban-fixes

feat: format IBAN in read-only fields
This commit is contained in:
Akhil Narang 2025-09-01 09:01:44 +05:30 committed by GitHub
commit a2e5621683
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 76 additions and 1 deletions

View file

@ -993,6 +993,7 @@ class BaseDocument:
from frappe.utils import (
split_emails,
validate_email_address,
validate_iban,
validate_name,
validate_phone_number,
validate_phone_number_with_country_code,
@ -1031,6 +1032,9 @@ class BaseDocument:
if data_field_options == "URL":
validate_url(data, throw=True)
if data_field_options == "IBAN":
validate_iban(data, throw=True)
def _validate_constants(self):
if frappe.flags.in_import or self.is_new() or self.flags.ignore_validate_constants:
return

View file

@ -267,7 +267,7 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlInp
}
format_for_input(val) {
if (this.df.options == "IBAN" && val) {
return val.replaceAll(" ", "").replace(/(.{4})(?=.)/g, "$1 ");
return frappe.utils.get_formatted_iban(val);
}
return val == null ? "" : val;
}

View file

@ -38,6 +38,10 @@ frappe.form.formatters = {
if (!value) return;
return `<a href="${value}" title="Open Link" target="_blank">${value}</a>`;
}
if (df && df.options == "IBAN") {
if (!value) return;
return frappe.utils.get_formatted_iban(value);
}
value = value == null ? "" : value;
return frappe.form.formatters._apply_custom_formatter(value, df);

View file

@ -1134,6 +1134,14 @@ Object.assign(frappe.utils, {
return duration;
},
get_formatted_iban(value) {
if (!value || ["BI", "SV", "EG", "LY"].some((country) => value.startsWith(country))) {
return value;
}
return value.replaceAll(" ", "").replace(/(.{4})(?=.)/g, "$1 ");
},
seconds_to_duration(seconds, duration_options) {
const round = seconds > 0 ? Math.floor : Math.ceil;
const total_duration = {

View file

@ -34,6 +34,7 @@ from frappe.utils import (
get_site_info,
get_sites,
get_url,
is_valid_iban,
money_in_words,
parse_and_map_trackers_from_url,
parse_timedelta,
@ -479,6 +480,26 @@ class TestValidationUtils(IntegrationTestCase):
for name in invalid_names:
self.assertRaises(frappe.InvalidNameError, validate_name, name, True)
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",
]
for iban in valid_ibans:
self.assertTrue(is_valid_iban(iban))
for not_iban in invalid_ibans:
self.assertFalse(is_valid_iban(not_iban))
class TestImage(IntegrationTestCase):
def test_strip_exif_data(self):

View file

@ -261,6 +261,44 @@ def validate_url(
return is_valid
def validate_iban(iban: str, throw: bool = False) -> bool:
from frappe import _
valid = is_valid_iban(iban)
if not valid and throw:
frappe.throw(frappe._("'{0}' is not a valid IBAN").format(frappe.bold(iban)))
return valid
def is_valid_iban(iban: str) -> bool:
"""
Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN
"""
if not iban:
return False
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 = iban.replace(" ", "").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]
try:
to_check = int("".join(encoded))
except ValueError:
return False
return to_check % 97 == 1
def random_string(length: int) -> str:
"""generate a random string"""
import string