Skip to content

Implement the new ro.onrc identifier #465

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 71 additions & 24 deletions stdnum/ro/onrc.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# onrc.py - functions for handling Romanian ONRC numbers
# coding: utf-8
#
# Copyright (C) 2020 Dimitrios Josef Moustos
# Copyright (C) 2024 Dimitrios Josef Moustos
# Copyright (C) 2020 Arthur de Jong
#
# This library is free software; you can redistribute it and/or
Expand All @@ -19,20 +19,23 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

"""ONRC (Ordine din Registrul Comerţului, Romanian Trade Register identifier).
"""ONRC (Ordine din Registrul Comer\xc5\xa3ului, Romanian Trade Register identifier).

All businesses in Romania have the to register with the National Trade
Register Office to receive a registration number. The number contains
information about the type of company, county, a sequence number and
registration year. This number can change when registration information
changes.
information about the type of company, registration year, a sequence number,
county and a control sum.
On 2024-07-26 a new format was introduced and for a while both old and new
formats need to be valid.

>>> validate('J52/750/2012')
'J52/750/2012'
>>> validate('X52/750/2012')
Traceback (most recent call last):
...
InvalidComponent: ...
>>> validate('J2012000750528')
'J2012000750528'
"""

import datetime
Expand All @@ -46,16 +49,29 @@
_cleanup_re = re.compile(r'[ /\\-]+')

# This pattern should match numbers that for some reason have a full date
# as last field
_onrc_fulldate_re = re.compile(r'^([A-Z][0-9]+/[0-9]+/)\d{2}[.]\d{2}[.](\d{4})$')
# as last field for the old format
_old_onrc_fulldate_re = re.compile(r'^([A-Z][0-9]+/[0-9]+/)\d{2}[.]\d{2}[.](\d{4})$')

# This pattern should match all valid old format numbers
_old_onrc_re = re.compile(r'^[A-Z][0-9]+/[0-9]+/[0-9]+$')

# This pattern should match all valid new format numbers
_onrc_re = re.compile(r'^[A-Z]\d{4}\d{6}\d{2}\d$')

# This pattern should match all valid numbers
_onrc_re = re.compile(r'^[A-Z][0-9]+/[0-9]+/[0-9]+$')

# List of valid counties
_counties = set(list(range(1, 41)) + [51, 52])

# Calculate the control digit which is used in the last position of the new format
def _calculate_control_digit(number):
"""Calculate the control digit for the new ONRC format."""
values = {'J': 10, 'F': 6, 'C': 3}
num = number[:-1] # Exclude control digit
total = values.get(num[0], 0) # Map letter to value
total += sum(int(d) for d in num[1:] if d.isdigit())
return (total + 4) % 10

# This function is only necessary for the old format
def compact(number):
"""Convert the number to the minimal representation. This strips the
number of any valid separators and removes surrounding whitespace."""
Expand All @@ -67,29 +83,60 @@ def compact(number):
if number[2:3] == '/':
number = number[:1] + '0' + number[1:]
# convert trailing full date to year only
m = _onrc_fulldate_re.match(number)
m = _old_onrc_fulldate_re.match(number)
if m:
number = ''.join(m.groups())
return number


def validate(number):
"""Check if the number is a valid ONRC."""
number = compact(number)
if not _onrc_re.match(number):
if _onrc_re.match(number):
if number[0] not in 'JFC':
raise InvalidComponent("Invalid register type. Must be J, F, or C.")
year = int(number[1:5])
sequence = number[5:11]
county = int(number[11:13])
control_digit = int(number[13])

# Validate year
if year < 1990 or year > datetime.date.today().year:
raise InvalidComponent("Year out of valid range.")

# Validate sequence number (6 digits)
if len(sequence) != 6 or not sequence.isdigit():
raise InvalidLength("Sequence number must be exactly 6 digits.")

# Companies registered before 2024-07-26 have the county code
if (year <= 2024) and (county not in _counties):
raise InvalidComponent("Invalid county code.")
# Companies registered after 2024-07-26 have 00 as county code.
if (year >= 2024) and (county == 0):
raise InvalidComponent("Invalid county code.")

# Validate control digit
expected_control = _calculate_control_digit(number)
if control_digit != expected_control:
raise InvalidChecksum(f"Control digit {control_digit} does not match expected {expected_control}.")

return number

elif _old_onrc_re.match(number):
number = compact(number)
if number[:1] not in 'JFC':
raise InvalidComponent()
county, serial, year = number[1:].split('/')
if len(serial) > 5:
raise InvalidLength()
if len(county) not in (1, 2) or int(county) not in _counties:
raise InvalidComponent()
if len(year) != 4:
raise InvalidLength()
if int(year) < 1990 or int(year) > 2024:
raise InvalidComponent()
return number
else:
raise InvalidFormat()
if number[:1] not in 'JFC':
raise InvalidComponent()
county, serial, year = number[1:].split('/')
if len(serial) > 5:
raise InvalidLength()
if len(county) not in (1, 2) or int(county) not in _counties:
raise InvalidComponent()
if len(year) != 4:
raise InvalidLength()
if int(year) < 1990 or int(year) > datetime.date.today().year:
raise InvalidComponent()
return number


def is_valid(number):
Expand Down
Loading