Coverage for jutil/validators.py: 95%

376 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-07 16:40 -0500

1import random 

2import re 

3import unicodedata 

4from datetime import date 

5from decimal import Decimal 

6from random import randint 

7from typing import Tuple, Optional, Any 

8from django.core.exceptions import ValidationError 

9from django.utils.timezone import now 

10from django.utils.translation import gettext as _ 

11from jutil.bank_const_iban import IBAN_LENGTH_BY_COUNTRY 

12 

13# Country-specific bank constants (abc-order): 

14from jutil.bank_const_be import BE_BIC_BY_ACCOUNT_NUMBER, BE_BANK_NAME_BY_BIC 

15from jutil.bank_const_dk import DK_BANK_CLEARING_MAP 

16from jutil.bank_const_fi import FI_BIC_BY_ACCOUNT_NUMBER, FI_BANK_NAME_BY_BIC 

17from jutil.bank_const_se import SE_BANK_CLEARING_LIST 

18 

19EMAIL_VALIDATOR = re.compile(r"[a-zA-Z0-9\._-]+@[a-zA-Z0-9\._-]+\.[a-zA-Z]+") 

20PHONE_FILTER = re.compile(r"[^+0-9]") 

21PHONE_VALIDATOR = re.compile(r"\+?\d{6,}") 

22PASSPORT_FILTER = re.compile(r"[^-A-Z0-9]") 

23STRIP_NON_NUMBERS = re.compile(r"[^0-9]") 

24STRIP_NON_ALPHANUMERIC = re.compile(r"[^0-9A-Za-z]") 

25VARIABLE_NAME = re.compile(r"[^0-9A-Za-z_]") 

26STRIP_WHITESPACE = re.compile(r"\s+") 

27STRIP_PREFIX_ZEROS = re.compile(r"^0+") 

28IBAN_FILTER = re.compile(r"[^A-Z0-9]") 

29DIGIT_FILTER = re.compile(r"[^0-9]") 

30 

31 

32def phone_filter(v: str) -> str: 

33 return PHONE_FILTER.sub("", str(v)) if v else "" 

34 

35 

36def phone_validator(v0: str): 

37 v = phone_filter(v0) 

38 if not v or not PHONE_VALIDATOR.fullmatch(v): 

39 v_str = _("Missing value") if v is None else str(v0) 

40 raise ValidationError(_("Invalid phone number") + " ({})".format(v_str), code="invalid_phone") 

41 

42 

43def phone_sanitizer(v: str) -> str: 

44 v = phone_filter(v) 

45 if not v or not PHONE_VALIDATOR.fullmatch(v): 

46 return "" 

47 return v 

48 

49 

50def email_filter(v: str) -> str: 

51 return str(v).lower().strip() if v else "" 

52 

53 

54def email_validator(v: str): 

55 if not is_email(v): 

56 v_str = _("Missing value") if not v else str(v) 

57 raise ValidationError(_("Invalid email") + " ({})".format(v_str), code="invalid_email") 

58 

59 

60def email_sanitizer(v: str) -> str: 

61 v = email_filter(v) 

62 if not v or not EMAIL_VALIDATOR.fullmatch(v): 

63 return "" 

64 return v 

65 

66 

67def passport_filter(v: str) -> str: 

68 return PASSPORT_FILTER.sub("", str(v).upper()) if v else "" 

69 

70 

71def passport_validator(v0: str): 

72 v = passport_filter(v0) 

73 if not v or len(v) < 5: 

74 v_str = _("Missing value") if v is None else str(v0) 

75 raise ValidationError(_("Invalid passport number") + " ({})".format(v_str), code="invalid_passport") 

76 

77 

78def passport_sanitizer(v: str): 

79 v = passport_filter(v) 

80 if not v or len(v) < 5: 

81 return "" 

82 return v 

83 

84 

85def country_code_filter(v: str) -> str: 

86 return v.strip().upper() 

87 

88 

89def bic_filter(v: str) -> str: 

90 return v.strip().upper() 

91 

92 

93def country_code_validator(v0: str): 

94 """ 

95 Accepts both ISO-2 and ISO-3 formats. 

96 :param v: str 

97 :return: None 

98 """ 

99 v = country_code_filter(v0) 

100 if not (2 <= len(v) <= 3): 

101 v_str = _("Missing value") if v is None else str(v0) 

102 raise ValidationError(_("Invalid country code") + " ({})".format(v_str), code="invalid_country_code") 

103 

104 

105def country_code_sanitizer(v: str) -> str: 

106 v = country_code_filter(v) 

107 return v if 2 <= len(v) <= 3 else "" 

108 

109 

110def bic_sanitizer(v: str) -> str: 

111 v = bic_filter(v) 

112 return v if 8 <= len(v) <= 11 else "" 

113 

114 

115def variable_name_sanitizer(v: str) -> str: 

116 v = VARIABLE_NAME.sub("", ascii_filter(v).replace(" ", "_")) 

117 if v and v[0].isdigit(): 

118 v = "_" + v 

119 return v 

120 

121 

122def ascii_filter(v: str) -> str: 

123 """ 

124 Replaces Unicode accent characters with plain ASCII. 

125 For example remove_accents('HELÉN') == 'HELEN'. 

126 :param v: str 

127 :return: str 

128 """ 

129 return unicodedata.normalize("NFKD", v).encode("ASCII", "ignore").decode() 

130 

131 

132def digit_filter(v: str) -> str: 

133 return DIGIT_FILTER.sub("", str(v)) if v else "" 

134 

135 

136def iban_filter(v: str) -> str: 

137 return IBAN_FILTER.sub("", str(v).upper()) if v else "" 

138 

139 

140def iban_filter_readable(acct) -> str: 

141 acct = iban_filter(acct) 

142 if acct: 

143 i = 0 

144 j = 4 

145 out = "" 

146 nlen = len(acct) 

147 while i < nlen: 

148 if out: 

149 out += " " 

150 out += acct[i:j] 

151 i = j 

152 j += 4 

153 return out 

154 return acct 

155 

156 

157def bic_validator(v0: str): 

158 """ 

159 Validates bank BIC/SWIFT code (8-11 characters). 

160 :param v: str 

161 :return: None 

162 """ 

163 v = bic_filter(v0) 

164 if not (8 <= len(v) <= 11): 

165 v_str = _("Missing value") if v is None else str(v0) 

166 raise ValidationError(_("Invalid bank BIC/SWIFT code") + " ({})".format(v_str), code="invalid_bic") 

167 

168 

169def iban_validator(v0: str): 

170 """ 

171 Validates IBAN format bank account number. 

172 :param v: str 

173 :return: None 

174 """ 

175 # validate prefix and length 

176 v = iban_filter(v0) 

177 if not v: 

178 raise ValidationError(_("Invalid IBAN account number") + " ({})".format(_("Missing value")), code="invalid_iban") 

179 country = v[:2].upper() 

180 if country not in IBAN_LENGTH_BY_COUNTRY: 

181 raise ValidationError(_("Invalid country code") + " ({})".format(country), code="invalid_country_code") 

182 iban_len = IBAN_LENGTH_BY_COUNTRY[country] 

183 if iban_len != len(v): 

184 raise ValidationError(_("Invalid IBAN account number") + " ({})".format(v0), code="invalid_iban") 

185 

186 # validate IBAN numeric part 

187 if iban_len <= 26: # very long IBANs are unsupported by the numeric part validation 

188 digits = "0123456789" 

189 num = "" 

190 for ch in v[4:] + v[0:4]: 

191 if ch not in digits: 

192 ch = str(ord(ch) - ord("A") + 10) 

193 num += ch 

194 x = Decimal(num) % Decimal(97) 

195 if x != Decimal(1): 

196 raise ValidationError(_("Invalid IBAN account number") + " ({})".format(v0), code="invalid_iban") 

197 

198 

199def iban_generator(country_code: str = "") -> str: 

200 """ 

201 Generates IBAN format bank account number (for testing). 

202 :param country_code: 2-character country code (optional) 

203 :return: str 

204 """ 

205 # pick random country code if not set (with max IBAN length 27) 

206 if not country_code: 

207 country_code = random.choice(list(filter(lambda cc: IBAN_LENGTH_BY_COUNTRY[cc] <= 26, IBAN_LENGTH_BY_COUNTRY.keys()))) # nosec 

208 if country_code not in IBAN_LENGTH_BY_COUNTRY: 

209 raise ValidationError(_("Invalid country code") + " ({})".format(country_code), code="invalid_country_code") 

210 nlen = IBAN_LENGTH_BY_COUNTRY[country_code] 

211 if nlen > 26: 

212 raise ValidationError(_("IBAN checksum generation does not support >26 character IBANs"), code="invalid_iban") 

213 

214 # generate BBAN part 

215 digits = "0123456789" 

216 bban = "".join([random.choice(digits) for n in range(nlen - 4)]) # nosec 

217 

218 # generate valid IBAN numeric part 

219 # (probably not the most efficient way to do this but write a better one if you need faster...) 

220 num0 = "" 

221 for ch in bban + country_code: 

222 if ch not in digits: 

223 ch = str(ord(ch) - ord("A") + 10) 

224 num0 += ch 

225 for checksum in range(1, 100): 225 ↛ 237line 225 didn't jump to line 237, because the loop on line 225 didn't complete

226 num = num0 

227 checksum_str = "{:02}".format(checksum) 

228 for ch in checksum_str: 

229 if ch not in digits: 229 ↛ 230line 229 didn't jump to line 230, because the condition on line 229 was never true

230 ch = str(ord(ch) - ord("A") + 10) 

231 num += ch 

232 # print(num, '/', 97, 'nlen', nlen) 

233 x = Decimal(num) % Decimal(97) 

234 if x == Decimal(1): 

235 return country_code + checksum_str + bban 

236 

237 raise ValidationError(_("Invalid IBAN account number"), code="invalid_iban") # should not get here 

238 

239 

240def validate_country_iban(v0: str, country: str): 

241 v = iban_filter(v0) 

242 if v[0:2] != country: 

243 raise ValidationError(_("Invalid IBAN account number") + " ({})".format(v0), code="invalid_iban") 

244 iban_validator(v) 

245 

246 

247def iban_bank_info(v: str) -> Tuple[str, str]: 

248 """ 

249 Returns BIC code and bank name from IBAN number. 

250 :param v: IBAN account number 

251 :return: (BIC code, bank name) or ('', '') if not found / unsupported country 

252 """ 

253 v = iban_filter(v) 

254 prefix = v[:2] 

255 func_name = prefix.lower() + "_iban_bank_info" # e.g. fi_iban_bank_info, be_iban_bank_info 

256 func = globals().get(func_name) 

257 if func is not None: 

258 return func(v) 

259 return "", "" 

260 

261 

262def iban_bic(v: str) -> str: 

263 """ 

264 Returns BIC code from IBAN number. 

265 :param v: IBAN account number 

266 :return: BIC code or '' if not found 

267 """ 

268 info = iban_bank_info(v) 

269 return info[0] if info else "" 

270 

271 

272def calculate_age(born: date, today: Optional[date] = None) -> int: 

273 if not today: 

274 today = now().date() 

275 return today.year - born.year - ((today.month, today.day) < (born.month, born.day)) 

276 

277 

278def filter_country_company_org_id(country_code: str, v: str): 

279 if country_code == "FI": 

280 return fi_company_org_id_filter(v) 

281 return PASSPORT_FILTER.sub("", v) 

282 

283 

284def validate_country_company_org_id(country_code: str, v: str): 

285 if country_code == "FI": 

286 fi_company_org_id_validator(v) 

287 

288 

289def is_int(v: Any) -> bool: 

290 """ 

291 Returns True if value is int or can be converted to int. 

292 :param v: Any 

293 :return: bool 

294 """ 

295 try: 

296 return str(int(v)) == str(v) 

297 except Exception: 

298 return False 

299 

300 

301def is_iban(v: str) -> bool: 

302 """ 

303 Returns True if account number is valid IBAN format. 

304 :param v: str 

305 :return: bool 

306 """ 

307 try: 

308 iban_validator(v) 

309 return True 

310 except ValidationError: 

311 return False 

312 

313 

314def is_email(v: str) -> bool: 

315 """ 

316 Returns True if v is email address. 

317 :param v: str 

318 :return: bool 

319 """ 

320 v = email_filter(v) 

321 return bool(v and EMAIL_VALIDATOR.fullmatch(v)) 

322 

323 

324# ============================================================================ 

325# Country specific functions (countries in alphabetical order) 

326# ============================================================================ 

327 

328 

329# ---------------------------------------------------------------------------- 

330# Belgium 

331# ---------------------------------------------------------------------------- 

332 

333 

334def be_iban_validator(v: str): 

335 validate_country_iban(v, "BE") 

336 

337 

338def be_iban_bank_info(v: str) -> Tuple[str, str]: 

339 """ 

340 Returns BIC code and bank name from BE IBAN number. 

341 :param v: IBAN account number 

342 :return: (BIC code, bank name) or ('', '') if not found 

343 """ 

344 v = iban_filter(v) 

345 bic = BE_BIC_BY_ACCOUNT_NUMBER.get(v[4:7], None) 

346 return (bic, BE_BANK_NAME_BY_BIC[bic]) if bic is not None else ("", "") 

347 

348 

349# ---------------------------------------------------------------------------- 

350# Denmark 

351# ---------------------------------------------------------------------------- 

352 

353 

354def dk_iban_validator(v: str): 

355 validate_country_iban(v, "DK") 

356 

357 

358def dk_clearing_code_bank_name(v: str) -> str: 

359 v = iban_filter(v) 

360 if v.startswith("DK"): 

361 v = v[4:] 

362 return DK_BANK_CLEARING_MAP.get(v[:4], "") 

363 

364 

365def dk_iban_bank_info(v: str) -> Tuple[str, str]: 

366 """ 

367 Returns empty string (BIC not available) and bank name from DK IBAN number. 

368 DK5000400440116243 

369 :param v: IBAN account number 

370 :return: ('', bank name) or ('', '') if not found 

371 """ 

372 return "", dk_clearing_code_bank_name(v) 

373 

374 

375# ---------------------------------------------------------------------------- 

376# Estonia 

377# ---------------------------------------------------------------------------- 

378 

379 

380def ee_iban_validator(v: str): 

381 validate_country_iban(v, "EE") 

382 

383 

384# ---------------------------------------------------------------------------- 

385# Finland 

386# ---------------------------------------------------------------------------- 

387 

388FI_SSN_FILTER = re.compile(r"[^0-9A-Z+-]") 

389FI_SSN_VALIDATOR = re.compile(r"^\d{6}[+-A]\d{3}[\d\w]$") 

390FI_COMPANY_ORG_ID_FILTER = re.compile(r"[^0-9]") 

391 

392 

393def fi_payment_reference_number(num: str): 

394 """ 

395 Appends Finland reference number checksum to existing number. 

396 :param num: At least 3 digits 

397 :return: Number plus checksum 

398 """ 

399 assert isinstance(num, str) 

400 v = STRIP_WHITESPACE.sub("", num) 

401 if digit_filter(v) != v: 

402 raise ValidationError(_("Invalid payment reference: {}").format(num)) 

403 v = v.lstrip("0") 

404 if len(v) < 3: 

405 raise ValidationError(_("Invalid payment reference: {}").format(num)) 

406 weights = [7, 3, 1] 

407 weighed_sum = 0 

408 vlen = len(v) 

409 for j in range(vlen): 

410 weighed_sum += int(v[vlen - 1 - j]) * weights[j % 3] 

411 return v + str((10 - (weighed_sum % 10)) % 10) 

412 

413 

414def fi_payment_reference_validator(v: str): 

415 v = STRIP_WHITESPACE.sub("", v) 

416 if fi_payment_reference_number(v[:-1]) != v: 

417 raise ValidationError(_("Invalid payment reference: {}").format(v)) 

418 

419 

420def iso_payment_reference_validator(v: str): 

421 """ 

422 Validates ISO reference number checksum. 

423 :param v: Reference number 

424 """ 

425 num = "" 

426 v = STRIP_WHITESPACE.sub("", v) 

427 v = STRIP_PREFIX_ZEROS.sub("", v) 

428 for ch in v[4:] + v[0:4]: 

429 x = ord(ch) 

430 if ord("0") <= x <= ord("9"): 

431 num += ch 

432 else: 

433 x -= 55 

434 if x < 10 or x > 35: 

435 raise ValidationError(_("Invalid payment reference: {}").format(v)) 

436 num += str(x) 

437 res = Decimal(num) % Decimal("97") 

438 if res != Decimal("1"): 438 ↛ 439line 438 didn't jump to line 439, because the condition on line 438 was never true

439 raise ValidationError(_("Invalid payment reference: {}").format(v)) 

440 

441 

442def fi_iban_validator(v: str): 

443 validate_country_iban(v, "FI") 

444 

445 

446def fi_iban_bank_info(v: str) -> Tuple[str, str]: 

447 """ 

448 Returns BIC code and bank name from FI IBAN number. 

449 :param v: IBAN account number 

450 :return: (BIC code, bank name) or ('', '') if not found 

451 """ 

452 v = iban_filter(v) 

453 bic = FI_BIC_BY_ACCOUNT_NUMBER.get(v[4:7], None) 

454 name = FI_BANK_NAME_BY_BIC.get(bic, "") if bic is not None else "" 

455 return bic or "", name 

456 

457 

458def fi_ssn_filter(v: str) -> str: 

459 return FI_SSN_FILTER.sub("", v.upper()) 

460 

461 

462def fi_company_org_id_filter(v: str) -> str: 

463 v = FI_COMPANY_ORG_ID_FILTER.sub("", v) 

464 return v[:-1] + "-" + v[-1:] if len(v) >= 2 else "" 

465 

466 

467def fi_company_org_id_validator(v0: str): 

468 v = re.sub(r"\s+", "", v0) 

469 prefix = v[:2] # retain prefix: either numeric or FI is ok 

470 v = fi_company_org_id_filter(v) 

471 if v[:2] == prefix: 

472 prefix = "FI" 

473 if v[-2:-1] != "-" or prefix != "FI": 

474 raise ValidationError(_("Invalid company organization ID") + " ({})".format(v0), code="invalid_company_org_id") 

475 v = v.replace("-", "", 1) 

476 if len(v) != 8: 476 ↛ 477line 476 didn't jump to line 477, because the condition on line 476 was never true

477 raise ValidationError(_("Invalid company organization ID") + " ({})".format(v0), code="invalid_company_org_id") 

478 multipliers = (7, 9, 10, 5, 8, 4, 2) 

479 x = 0 

480 for i, m in enumerate(multipliers): 

481 x += int(v[i]) * m 

482 remainder = divmod(x, 11)[1] 

483 if remainder == 1: 483 ↛ 484line 483 didn't jump to line 484, because the condition on line 483 was never true

484 raise ValidationError(_("Invalid company organization ID") + " ({})".format(v0), code="invalid_company_org_id") 

485 if remainder >= 2: 

486 check_digit = str(11 - remainder) 

487 if check_digit != v[-1:]: 

488 raise ValidationError(_("Invalid company organization ID") + " ({})".format(v0), code="invalid_company_org_id") 

489 

490 

491def fi_company_org_id_generator() -> str: 

492 remainder = 1 

493 v = "" 

494 while remainder < 2: 

495 v = str(randint(11111111, 99999999)) # nosec 

496 multipliers = (7, 9, 10, 5, 8, 4, 2) 

497 x = 0 

498 for i, m in enumerate(multipliers): 

499 x += int(v[i]) * m 

500 remainder = divmod(x, 11)[1] 

501 check_digit = str(11 - remainder) 

502 return v[:-1] + "-" + check_digit 

503 

504 

505def fi_ssn_validator(v0: str): 

506 v = fi_ssn_filter(v0) 

507 if not FI_SSN_VALIDATOR.fullmatch(v): 

508 raise ValidationError(_("Invalid personal identification number") + " ({})".format(v0), code="invalid_ssn") 

509 d = int(Decimal(v[0:6] + v[7:10]) % Decimal(31)) 

510 digits = { 

511 10: "A", 

512 11: "B", 

513 12: "C", 

514 13: "D", 

515 14: "E", 

516 15: "F", 

517 16: "H", 

518 17: "J", 

519 18: "K", 

520 19: "L", 

521 20: "M", 

522 21: "N", 

523 22: "P", 

524 23: "R", 

525 24: "S", 

526 25: "T", 

527 26: "U", 

528 27: "V", 

529 28: "W", 

530 29: "X", 

531 30: "Y", 

532 } 

533 ch = digits.get(d, str(d)) 

534 if ch != v[-1:]: 

535 raise ValidationError(_("Invalid personal identification number") + " ({})".format(v0), code="invalid_ssn") 

536 

537 

538def fi_ssn_generator(min_year: int = 1920, max_year: int = 1999): 

539 if not (1800 <= min_year < 2100): 

540 raise ValidationError(_("Unsupported year") + " ({})".format(min_year)) 

541 if not (1800 <= max_year < 2100): 541 ↛ 542line 541 didn't jump to line 542, because the condition on line 541 was never true

542 raise ValidationError(_("Unsupported year") + " ({})".format(max_year)) 

543 

544 day = randint(1, 28) # nosec 

545 month = randint(1, 12) # nosec 

546 year = randint(min_year, max_year) # nosec 

547 suffix = randint(100, 999) # nosec 

548 sep = "-" 

549 if year < 1900: 

550 sep = "+" 

551 year2 = year - 1800 

552 elif year >= 2000: 

553 sep = "A" 

554 year2 = year - 2000 

555 else: 

556 year2 = year - 1900 

557 v = "{:02}{:02}{:02}{}{}".format(day, month, year2, sep, suffix) 

558 d = int(Decimal(v[0:6] + v[7:10]) % Decimal(31)) 

559 digits = { 

560 10: "A", 

561 11: "B", 

562 12: "C", 

563 13: "D", 

564 14: "E", 

565 15: "F", 

566 16: "H", 

567 17: "J", 

568 18: "K", 

569 19: "L", 

570 20: "M", 

571 21: "N", 

572 22: "P", 

573 23: "R", 

574 24: "S", 

575 25: "T", 

576 26: "U", 

577 27: "V", 

578 28: "W", 

579 29: "X", 

580 30: "Y", 

581 } 

582 ch = digits.get(d, str(d)) 

583 return v + ch 

584 

585 

586def fi_ssn_birthday(v: str) -> date: 

587 v = fi_ssn_filter(v) 

588 fi_ssn_validator(v) 

589 sep = v[6] # 231298-965X 

590 year = int(v[4:6]) 

591 month = int(v[2:4]) 

592 day = int(v[0:2]) 

593 if sep == "+": # 1800 

594 year += 1800 

595 elif sep == "-": 

596 year += 1900 

597 elif sep == "A": 597 ↛ 599line 597 didn't jump to line 599, because the condition on line 597 was never false

598 year += 2000 

599 return date(year, month, day) 

600 

601 

602def fi_ssn_age(ssn: str, today: Optional[date] = None) -> int: 

603 return calculate_age(fi_ssn_birthday(ssn), today) 

604 

605 

606# ---------------------------------------------------------------------------- 

607# Sweden 

608# ---------------------------------------------------------------------------- 

609 

610SE_SSN_FILTER = re.compile(r"[^-0-9]") 

611SE_SSN_VALIDATOR = re.compile(r"^\d{6}[-]\d{3}[\d]$") 

612 

613 

614def se_iban_bank_info(v: str) -> Tuple[str, str]: 

615 """ 

616 Returns BIC code and bank name from SE IBAN number. 

617 :param v: IBAN account number 

618 :return: (BIC code, bank name) or ('', '') if not found 

619 """ 

620 bank_name = se_clearing_code_bank_info(v)[0] 

621 return "", bank_name 

622 

623 

624def se_iban_validator(v: str): 

625 validate_country_iban(v, "SE") 

626 

627 

628def se_ssn_filter(v: str) -> str: 

629 return SE_SSN_FILTER.sub("", v.upper()) 

630 

631 

632def se_ssn_validator(v0: str): 

633 v = se_ssn_filter(v0) 

634 if not SE_SSN_VALIDATOR.fullmatch(v): 634 ↛ 635line 634 didn't jump to line 635, because the condition on line 634 was never true

635 raise ValidationError(_("Invalid personal identification number") + " ({})".format(v0), code="invalid_ssn") 

636 v = STRIP_NON_NUMBERS.sub("", v) 

637 dsum = 0 

638 for i in range(9): 

639 x = int(v[i]) 

640 if i & 1 == 0: 

641 x += x 

642 # print('summing', v[i], 'as', x) 

643 xsum = x % 10 + int(x / 10) % 10 

644 # print(v[i], 'xsum', xsum) 

645 dsum += xsum 

646 # print('sum', dsum) 

647 rem = dsum % 10 

648 # print('rem', rem) 

649 checksum = 10 - rem 

650 if checksum == 10: 

651 checksum = 0 

652 # print('checksum', checksum) 

653 if int(v[-1:]) != checksum: 

654 raise ValidationError(_("Invalid personal identification number") + " ({})".format(v0), code="invalid_ssn") 

655 

656 

657def se_clearing_code_bank_info(account_number: str) -> Tuple[str, Optional[int]]: 

658 """ 

659 Returns Sweden bank info by clearing code. 

660 :param account_number: Swedish account number with clearing code as prefix 

661 :return: (Bank name, account digit count) or ("", None) if not found 

662 """ 

663 v = iban_filter(account_number) 

664 if v.startswith("SE"): 

665 v = v[4:] 

666 clearing = v[:4] 

667 for name, begin, end, acc_digits in SE_BANK_CLEARING_LIST: 667 ↛ 670line 667 didn't jump to line 670, because the loop on line 667 didn't complete

668 if begin <= clearing <= end: 

669 return name, acc_digits 

670 return "", None