diff --git a/faker/providers/ssn/fr_FR/__init__.py b/faker/providers/ssn/fr_FR/__init__.py index 7cb442e020..731834f91d 100644 --- a/faker/providers/ssn/fr_FR/__init__.py +++ b/faker/providers/ssn/fr_FR/__init__.py @@ -1,15 +1,17 @@ -from typing import Tuple +from typing import Tuple, Union 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: Union[int, str]) -> int: + # For Corsican birthplaces, the NIR checksum is computed with 2A -> 19 and 2B -> 18. + normalized_ssn = str(ssn_without_checksum).replace("2A", "19").replace("2B", "18") + return 97 - (int(normalized_ssn) % 97) class Provider(BaseProvider): """ - A Faker provider for the French VAT IDs + A Faker provider for French social security and VAT IDs. """ vat_id_formats = ( @@ -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"), @@ -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"), @@ -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"), @@ -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"), @@ -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"), @@ -106,28 +111,28 @@ 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 @@ -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) diff --git a/tests/providers/test_ssn.py b/tests/providers/test_ssn.py index 990bcd9cf3..42c5b0a18b 100644 --- a/tests/providers/test_ssn.py +++ b/tests/providers/test_ssn.py @@ -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 @@ -893,10 +894,19 @@ 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 assert fr_calculate_checksum(2570533063999) == 3 + 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" class TestHrHR(unittest.TestCase):