diff --git a/docs/index.rst b/docs/index.rst index a543b398..0d22086e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -130,6 +130,7 @@ Available formats be.eid be.iban be.nn + be.ogm_vcs be.ssn be.vat bg.egn diff --git a/docs/stdnum.be.ogm_vcs.rst b/docs/stdnum.be.ogm_vcs.rst new file mode 100644 index 00000000..ff035f0d --- /dev/null +++ b/docs/stdnum.be.ogm_vcs.rst @@ -0,0 +1,5 @@ +stdnum.be.ogm_vcs +================= + +.. automodule:: stdnum.be.ogm_vcs + :members: diff --git a/stdnum/be/ogm_vcs.py b/stdnum/be/ogm_vcs.py new file mode 100644 index 00000000..01c7bf70 --- /dev/null +++ b/stdnum/be/ogm_vcs.py @@ -0,0 +1,102 @@ +# ogm_vcs.py - functions for handling Belgian OGM-VCS +# coding: utf-8 +# +# Copyright (C) 2025 Cédric Krier +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +"""Belgian OGM-VCS. + +The OGM-VCS is used in bank transfer as structured communication. + +* https://febelfin.be/en/publications/2023/febelfin-banking-standards-for-online-banking + +>>> compact('+++010/8068/17183+++') +'010806817183' +>>> validate('+++010/8068/17183+++') +'010806817183' +>>> validate('foo') +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> validate('010/8068/1718') +Traceback (most recent call last): + ... +InvalidLength: ... +>>> validate('010/8068/17180') +Traceback (most recent call last): + ... +InvalidChecksum: ... +>>> is_valid('010/8068/17183') +True +>>> is_valid('010/8068/17180') +False +>>> format('010806817183') +'010/8068/17183' +>>> checksum('0108068171') +83 +>>> calc_check_digit('0108068171') +'83' +""" + +from __future__ import annotations + +from stdnum.exceptions import ( + InvalidChecksum, InvalidFormat, InvalidLength, ValidationError) +from stdnum.util import clean, isdigits + + +def compact(number: str) -> str: + """Convert the number to the minimal representation. This strips the number + of any invalid separators and removes surrounding whitespace.""" + return clean(number, ' +/').strip() + + +def checksum(number: str) -> int: + """Calculate the checksum.""" + return (int(number) % 97) or 97 + + +def calc_check_digit(number: str) -> str: + """Calculate the check digit that should be added.""" + return '%02d' % checksum(number) + + +def validate(number: str) -> str: + """Check if the number is a valid OGM-VCS.""" + number = compact(number) + if not isdigits(number) or int(number) <= 0: + raise InvalidFormat() + if len(number) != 12: + raise InvalidLength() + if checksum(number[:-2]) != int(number[-2:]): + raise InvalidChecksum() + return number + + +def is_valid(number: str) -> bool: + """Check if the number is a valid VAT number.""" + try: + return bool(validate(number)) + except ValidationError: + return False + + +def format(number: str) -> str: + """Format the number provided for output.""" + number = compact(number) + number = number.rjust(12, '0') + return f'{number[:3]}/{number[3:7]}/{number[7:]}'