Skip to content
41 changes: 23 additions & 18 deletions faker/providers/ssn/fr_FR/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
from .. import Provider as BaseProvider


def calculate_checksum(ssn_without_checksum: int) -> int:
return 97 - (ssn_without_checksum % 97)
def calculate_checksum(ssn_without_checksum: str) -> int:
# For Corsican birthplaces, the NIR checksum is computed with 2A -> 19 and 2B -> 18.
normalized_ssn = ssn_without_checksum.replace("2A", "19").replace("2B", "18")
return 97 - (int(normalized_ssn) % 97)
Comment thread
dancergraham marked this conversation as resolved.
Outdated


class Provider(BaseProvider):
"""
A Faker provider for the French VAT IDs
A Faker provider for French social security and VAT IDs.
"""

vat_id_formats = (
Expand All @@ -21,6 +23,7 @@ class Provider(BaseProvider):

# department id, municipality id, name of department, name of municipality
# department id + municipality id = INSEE code
# municipality ids are only unique within a department
departments_and_municipalities = (
# France métropolitaine = Mainland France
("01", "053", "Ain", "Bourg-en-Bresse"),
Expand All @@ -29,7 +32,7 @@ class Provider(BaseProvider):
("04", "070", "Alpes-de-Haute-Provence", "Digne-les-Bains"),
("05", "061", "Hautes-Alpes", "Gap"),
("06", "088", "Alpes-Maritimes", "Nice"),
("07", "186", "Ardèche", "Orgnac-l'Aven"),
("07", "186", "Ardèche", "Privas"),
("08", "105", "Ardennes", "Charleville-Mézières"),
("09", "122", "Ariège", "Foix"),
("10", "387", "Aube", "Troyes"),
Expand All @@ -39,11 +42,13 @@ class Provider(BaseProvider):
("14", "118", "Calvados", "Caen"),
("15", "014", "Cantal", "Aurillac"),
("16", "015", "Charente", "Angoulême"),
("17", "300", "Charente-Maritime", "Rochelle"),
("17", "300", "Charente-Maritime", "La Rochelle"),
("18", "033", "Cher", "Bourges"),
("19", "272", "Corrèze", "Tulle"),
("21", "231", "Côte-d'Or,Côte-d'Or", "Dijon"),
("22", "278", "Côtes-d'Armor,Côtes-d'Armor", "Saint-Brieuc"),
("2A", "004", "Corse-du-Sud", "Ajaccio"),
("2B", "033", "Haute-Corse", "Bastia"),
("21", "231", "Côte-d'Or", "Dijon"),
("22", "278", "Côtes-d'Armor", "Saint-Brieuc"),
("23", "096", "Creuse", "Guéret"),
("24", "322", "Dordogne", "Périgueux"),
("25", "056", "Doubs", "Besançon"),
Expand All @@ -57,14 +62,14 @@ class Provider(BaseProvider):
("33", "063", "Gironde", "Bordeaux"),
("34", "172", "Hérault", "Montpellier"),
("35", "238", "Ille-et-Vilaine", "Rennes"),
("36", "044", "Indre,Indre", "Châteauroux"),
("36", "044", "Indre", "Châteauroux"),
("37", "261", "Indre-et-Loire", "Tours"),
("38", "185", "Isère", "Grenoble"),
("39", "300", "Jura", "Lons-le-Saunier"),
("40", "192", "Landes", "Mont-de-Marsan"),
("41", "018", "Loir-et-Cher", "Blois"),
("42", "218", "Loire", "Saint-Étienne"),
("43", "157", "Haute-Loire", "Puy-en-Velay"),
("43", "157", "Haute-Loire", "Le Puy-en-Velay"),
("44", "109", "Loire-Atlantique", "Nantes"),
("45", "234", "Loiret", "Orléans"),
("46", "042", "Lot", "Cahors"),
Expand Down Expand Up @@ -93,7 +98,7 @@ class Provider(BaseProvider):
("69", "123", "Rhône", "Lyon"),
("70", "550", "Haute-Saône", "Vesoul"),
("71", "270", "Saône-et-Loire", "Mâcon"),
("72", "181", "Sarthe", "Mans"),
("72", "181", "Sarthe", "Le Mans"),
("73", "065", "Savoie", "Chambéry"),
("74", "010", "Haute-Savoie", "Annecy"),
("75", "056", "Paris", "Paris"),
Expand All @@ -106,31 +111,31 @@ class Provider(BaseProvider):
("82", "121", "Tarn-et-Garonne", "Montauban"),
("83", "137", "Var", "Toulon"),
("84", "007", "Vaucluse", "Avignon"),
("85", "191", "Vendée", "Roche-sur-Yon"),
("85", "191", "Vendée", "La Roche-sur-Yon"),
("86", "194", "Vienne", "Poitiers"),
("87", "085", "Haute-Vienne", "Limoges"),
("88", "160", "Vosges", "Épinal"),
("89", "024", "Yonne", "Auxerre"),
("90", "010", "Territoire", "Belfort"),
("90", "010", "Territoire de Belfort", "Belfort"),
("91", "228", "Essonne", "Évry-Courcouronnes"),
("92", "050", "Hauts-de-Seine", "Nanterre"),
("93", "008", "Seine-Saint-Denis", "Bobigny"),
("94", "028", "Val-de-Marne", "Créteil"),
("95", "500", "Val-d'Oise", "Pontoise"),
# DOM-TOM = Overseas France
# DROM-COM = Overseas France
("971", "05", "Guadeloupe", "Basse-Terre"),
("972", "09", "Martinique", "Fort-de-France"),
("973", "02", "Guyane", "Cayenne"),
("974", "11", "Réunion", "Saint-Denis"),
("974", "11", "La Réunion", "Saint-Denis"),
("976", "11", "Mayotte", "Mamoudzou"),
)

def ssn(self) -> str:
"""
Creates a French numéro de sécurité sociale
Creates a French SSN (numéro de sécurité sociale) with checksum. Can include letters A or B for Corsica.
https://fr.wikipedia.org/wiki/Num%C3%A9ro_de_s%C3%A9curit%C3%A9_sociale_en_France#Signification_des_chiffres_du_NIR
https://www.comptavoo.com/Numero-Securite-sociale,348.html
:return: a French SSN
:returns: a French SSN
Comment thread
dancergraham marked this conversation as resolved.
Outdated
"""
gender_id = self.random_int(min=1, max=2)
year_of_birth = self.random_int(min=0, max=99)
Expand All @@ -143,8 +148,8 @@ def ssn(self) -> str:

order_number = self.random_int(min=1, max=999)

ssn_without_checksum = int(
f"{gender_id:01}{year_of_birth:02}{month_of_birth:02}{code_department}{code_municipality}{order_number:03}",
ssn_without_checksum = (
f"{gender_id:01}{year_of_birth:02}{month_of_birth:02}{code_department}{code_municipality}{order_number:03}"
)
checksum = calculate_checksum(ssn_without_checksum)

Expand Down
15 changes: 14 additions & 1 deletion tests/providers/test_ssn.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from faker.providers.ssn.es_MX import ssn_checksum as mx_ssn_checksum
from faker.providers.ssn.et_EE import checksum as et_checksum
from faker.providers.ssn.fi_FI import Provider as fi_Provider
from faker.providers.ssn.fr_FR import Provider as fr_Provider
from faker.providers.ssn.fr_FR import calculate_checksum as fr_calculate_checksum
from faker.providers.ssn.hr_HR import checksum as hr_checksum
from faker.providers.ssn.it_IT import checksum as it_checksum
Expand Down Expand Up @@ -893,10 +894,22 @@ def test_vat_id(self):

def test_ssn(self) -> None:
for _ in range(100):
assert re.search(r"^\d{15}$", self.fake.ssn())
# 5 birth digits, then either 8 numeric locality digits or Corsica's 2A/2B + 6 digits, then checksum.
assert re.search(r"^\d{5}(?:\d{8}|2[AB]\d{6})\d{2}$", self.fake.ssn())

def test_checksum(self) -> None:
assert fr_calculate_checksum(2570533063999) == 3
Comment thread
dancergraham marked this conversation as resolved.
assert fr_calculate_checksum("100012A004001") == 11
assert fr_calculate_checksum("100012B033001") == 41

def test_ssn_can_generate_corsican_department_codes(self) -> None:
with mock.patch.object(fr_Provider, "random_element", return_value=("2A", "004", "Corse-du-Sud", "Ajaccio")):
with mock.patch.object(fr_Provider, "random_int", side_effect=[1, 0, 1, 1]):
assert self.fake.ssn() == "100012A00400111"

with mock.patch.object(fr_Provider, "random_element", return_value=("2B", "033", "Haute-Corse", "Bastia")):
with mock.patch.object(fr_Provider, "random_int", side_effect=[1, 0, 1, 1]):
assert self.fake.ssn() == "100012B03300141"


class TestHrHR(unittest.TestCase):
Expand Down
Loading