From c6b3356cdffe34ad8fb53bb5ddbded388f930c31 Mon Sep 17 00:00:00 2001 From: Nico Carl Date: Fri, 22 Oct 2021 16:27:08 +0200 Subject: [PATCH 1/4] Add provider for French SSN 15 digit French social security number is added. Besides gender, birth month and year it contains information about birthplace. I decided to create a list with the main municipality per department to create realistic data for this part of the SSN (department + municipality id = code INSEE) Fix: #1510 --- faker/providers/ssn/fr_FR/__init__.py | 137 ++++++++++++++++++++++++++ tests/providers/test_ssn.py | 8 ++ 2 files changed, 145 insertions(+) diff --git a/faker/providers/ssn/fr_FR/__init__.py b/faker/providers/ssn/fr_FR/__init__.py index 57a459272b..7c755a5851 100644 --- a/faker/providers/ssn/fr_FR/__init__.py +++ b/faker/providers/ssn/fr_FR/__init__.py @@ -1,6 +1,12 @@ +from typing import Tuple + from .. import Provider as BaseProvider +def calculate_checksum(ssn_without_checksum: int) -> int: + return 97 - (ssn_without_checksum % 97) + + class Provider(BaseProvider): """ A Faker provider for the French VAT IDs @@ -13,6 +19,137 @@ class Provider(BaseProvider): "FR#? #########", ) + # department id, municipality id, name of department, name of municipality + # department id + municipality id = INSEE code + departments_and_municipalities = ( + # France métropolitaine = Mainland France + ("01", "053", "Ain", "Bourg-en-Bresse"), + ("02", "408", "Aisne", "Laon"), + ("03", "190", "Allier", "Moulins"), + ("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"), + ("08", "105", "Ardennes", "Charleville-Mézières"), + ("09", "122", "Ariège", "Foix"), + ("10", "387", "Aube", "Troyes"), + ("11", "069", "Aude", "Carcassonne"), + ("12", "202", "Aveyron", "Rodez"), + ("13", "055", "Bouches-du-Rhône", "Marseille"), + ("14", "118", "Calvados", "Caen"), + ("15", "014", "Cantal", "Aurillac"), + ("16", "015", "Charente", "Angoulême"), + ("17", "300", "Charente-Maritime", "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"), + ("23", "096", "Creuse", "Guéret"), + ("24", "322", "Dordogne", "Périgueux"), + ("25", "056", "Doubs", "Besançon"), + ("26", "362", "Drôme", "Valence"), + ("27", "229", "Eure", "Évreux"), + ("28", "085", "Eure-et-Loir", "Chartres"), + ("29", "232", "Finistère", "Quimper"), + ("30", "189", "Gard", "Nîmes"), + ("31", "555", "Haute-Garonne", "Toulouse"), + ("32", "013", "Gers", "Auch"), + ("33", "063", "Gironde", "Bordeaux"), + ("34", "172", "Hérault", "Montpellier"), + ("35", "238", "Ille-et-Vilaine", "Rennes"), + ("36", "044", "Indre,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"), + ("44", "109", "Loire-Atlantique", "Nantes"), + ("45", "234", "Loiret", "Orléans"), + ("46", "042", "Lot", "Cahors"), + ("47", "001", "Lot-et-Garonne", "Agen"), + ("48", "095", "Lozère", "Mende"), + ("49", "007", "Maine-et-Loire", "Angers"), + ("50", "502", "Manche", "Saint-Lô"), + ("51", "108", "Marne", "Châlons-en-Champagne"), + ("52", "121", "Haute-Marne", "Chaumont"), + ("53", "130", "Mayenne", "Laval"), + ("54", "395", "Meurthe-et-Moselle", "Nancy"), + ("55", "029", "Meuse", "Bar-le-Duc"), + ("56", "260", "Morbihan", "Vannes"), + ("57", "463", "Moselle", "Metz"), + ("58", "194", "Nièvre", "Nevers"), + ("59", "350", "Nord", "Lille"), + ("60", "057", "Oise", "Beauvais"), + ("61", "001", "Orne", "Alençon"), + ("62", "041", "Pas-de-Calais", "Arras"), + ("63", "113", "Puy-de-Dôme", "Clermont-Ferrand"), + ("64", "445", "Pyrénées-Atlantiques", "Pau"), + ("65", "440", "Hautes-Pyrénées", "Tarbes"), + ("66", "136", "Pyrénées-Orientales", "Perpignan"), + ("67", "482", "Bas-Rhin", "Strasbourg"), + ("68", "066", "Haut-Rhin", "Colmar"), + ("69", "123", "Rhône", "Lyon"), + ("70", "550", "Haute-Saône", "Vesoul"), + ("71", "270", "Saône-et-Loire", "Mâcon"), + ("72", "181", "Sarthe", "Mans"), + ("73", "065", "Savoie", "Chambéry"), + ("74", "010", "Haute-Savoie", "Annecy"), + ("75", "056", "Paris", "Paris"), + ("76", "540", "Seine-Maritime", "Rouen"), + ("77", "288", "Seine-et-Marne", "Melun"), + ("78", "646", "Yvelines", "Versailles"), + ("79", "191", "Deux-Sèvres", "Niort"), + ("80", "021", "Somme", "Amiens"), + ("81", "004", "Tarn", "Albi"), + ("82", "121", "Tarn-et-Garonne", "Montauban"), + ("83", "137", "Var", "Toulon"), + ("84", "007", "Vaucluse", "Avignon"), + ("85", "191", "Vendée", "Roche-sur-Yon"), + ("86", "194", "Vienne", "Poitiers"), + ("87", "085", "Haute-Vienne", "Limoges"), + ("88", "160", "Vosges", "Épinal"), + ("89", "024", "Yonne", "Auxerre"), + ("90", "010", "Territoire", "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 + ("971", "05", "Guadeloupe", "Basse-Terre"), + ("972", "09", "Martinique", "Fort-de-France"), + ("973", "02", "Guyane", "Cayenne"), + ("974", "11", "Réunion", "Saint-Denis"), + ("976", "11", "Mayotte", "Mamoudzou") + ) + + def ssn(self) -> str: + """ + Creates a French numéro de sécurité sociale + 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 + """ + gender_id = self.random_int(min=1, max=2) + year_of_birth = self.random_int(min=0, max=99) + month_of_birth = self.random_int(min=0, max=12) + department_and_municipality: Tuple[str, str, str, str] = self.random_element( + self.departments_and_municipalities + ) + code_department = department_and_municipality[0] + code_municipality = department_and_municipality[1] + + 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}" + ) + checksum = calculate_checksum(ssn_without_checksum) + + return f"{ssn_without_checksum}{checksum:02}" + def vat_id(self) -> str: """ http://ec.europa.eu/taxation_customs/vies/faq.html#item_11 diff --git a/tests/providers/test_ssn.py b/tests/providers/test_ssn.py index 77ef5bb066..d6c206efd3 100644 --- a/tests/providers/test_ssn.py +++ b/tests/providers/test_ssn.py @@ -20,6 +20,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 calculate_checksum as fr_calculate_checksum from faker.providers.ssn.hr_HR import checksum as hr_checksum from faker.providers.ssn.no_NO import Provider as no_Provider from faker.providers.ssn.no_NO import checksum as no_checksum @@ -690,6 +691,13 @@ def test_vat_id(self): for _ in range(100): assert re.search(r"^FR[\w\d]{2} \d{9}$", self.fake.vat_id()) + def test_ssn(self) -> None: + for _ in range(100): + assert re.search(r'^\d{15}$', self.fake.ssn()) + + def test_checksum(self) -> None: + assert fr_calculate_checksum(2570533063999) == 3 + class TestFrCH: @pytest.mark.parametrize( From 60f0e22b5fe91eaadbe9dea55d6e4c1b4f326047 Mon Sep 17 00:00:00 2001 From: Nico Carl Date: Fri, 22 Oct 2021 16:32:58 +0200 Subject: [PATCH 2/4] Add missing trailing commas --- faker/providers/ssn/fr_FR/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/faker/providers/ssn/fr_FR/__init__.py b/faker/providers/ssn/fr_FR/__init__.py index 7c755a5851..bc01ca25cf 100644 --- a/faker/providers/ssn/fr_FR/__init__.py +++ b/faker/providers/ssn/fr_FR/__init__.py @@ -122,7 +122,7 @@ class Provider(BaseProvider): ("972", "09", "Martinique", "Fort-de-France"), ("973", "02", "Guyane", "Cayenne"), ("974", "11", "Réunion", "Saint-Denis"), - ("976", "11", "Mayotte", "Mamoudzou") + ("976", "11", "Mayotte", "Mamoudzou"), ) def ssn(self) -> str: @@ -136,7 +136,7 @@ def ssn(self) -> str: year_of_birth = self.random_int(min=0, max=99) month_of_birth = self.random_int(min=0, max=12) department_and_municipality: Tuple[str, str, str, str] = self.random_element( - self.departments_and_municipalities + self.departments_and_municipalities, ) code_department = department_and_municipality[0] code_municipality = department_and_municipality[1] @@ -144,7 +144,7 @@ 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}" + 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) From 8097028a82e6d78506cc0e69ff810f275bc692f1 Mon Sep 17 00:00:00 2001 From: Nico Carl Date: Fri, 22 Oct 2021 16:48:40 +0200 Subject: [PATCH 3/4] Fix error in min value of month_of_birth --- faker/providers/ssn/fr_FR/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/faker/providers/ssn/fr_FR/__init__.py b/faker/providers/ssn/fr_FR/__init__.py index bc01ca25cf..7cb442e020 100644 --- a/faker/providers/ssn/fr_FR/__init__.py +++ b/faker/providers/ssn/fr_FR/__init__.py @@ -134,7 +134,7 @@ def ssn(self) -> str: """ gender_id = self.random_int(min=1, max=2) year_of_birth = self.random_int(min=0, max=99) - month_of_birth = self.random_int(min=0, max=12) + month_of_birth = self.random_int(min=1, max=12) department_and_municipality: Tuple[str, str, str, str] = self.random_element( self.departments_and_municipalities, ) From db19f52951e356b877adaf7cc0ee240dc77168f6 Mon Sep 17 00:00:00 2001 From: Nico Carl Date: Sat, 23 Oct 2021 16:22:17 +0200 Subject: [PATCH 4/4] Reformat with black afte rebase --- tests/providers/test_ssn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/providers/test_ssn.py b/tests/providers/test_ssn.py index d6c206efd3..03ecd0a078 100644 --- a/tests/providers/test_ssn.py +++ b/tests/providers/test_ssn.py @@ -693,7 +693,7 @@ def test_vat_id(self): def test_ssn(self) -> None: for _ in range(100): - assert re.search(r'^\d{15}$', self.fake.ssn()) + assert re.search(r"^\d{15}$", self.fake.ssn()) def test_checksum(self) -> None: assert fr_calculate_checksum(2570533063999) == 3