Skip to content

Commit

Permalink
Add provider for French SSN (#1559)
Browse files Browse the repository at this point in the history
* 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

* Add missing trailing commas

* Fix error in min value of month_of_birth

* Reformat with black afte rebase
  • Loading branch information
nicarl committed Oct 27, 2021
1 parent 71e89a7 commit 4cec62e
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 0 deletions.
137 changes: 137 additions & 0 deletions 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
Expand All @@ -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=1, 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
Expand Down
8 changes: 8 additions & 0 deletions tests/providers/test_ssn.py
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit 4cec62e

Please sign in to comment.