Skip to content
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

Add provider for French SSN #1559

Merged
merged 4 commits into from Oct 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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