Repository: joke2k/faker

Issue:
Missing logic for generating Mexican SSN
The format for the Mexican SSN should be 11 digits:

* The first two digits represent the office where the person was registered.
* The next two digits are the year when the person was registered to social security.
* The next two digits are the year of birth for the person.
* The next four digits are a sequential number assigned by the registration office.
* The last digit is a checksum.

### Steps to reproduce

```python
from faker import Faker
fake = Faker('es_MX')
print(fake.ssn()  # 105-90-6991)
```
The [docs](https://faker.readthedocs.io/en/master/locales/es_MX.html#faker-providers-ssn) reflect this error.

### Expected behavior

The SSN for locale `"es_MX"` should follow the format described above.

```python
from faker import Faker
fake = Faker('es_MX')
print(fake.ssn()  # 92919084431)
```

### Actual behavior

The SSN format for `"es_MX"` is currently `'##-##-####'`.  


Discussion:

Resolving patch:
diff --git a/faker/providers/ssn/es_MX/__init__.py b/faker/providers/ssn/es_MX/__init__.py
new file mode 100644
--- /dev/null
+++ b/faker/providers/ssn/es_MX/__init__.py
@@ -0,0 +1,254 @@
+# coding=utf-8
+"""
+SSN provider for es_MX.
+
+This module adds a provider for mexican SSN, along with Unique Population
+Registry Code (CURP) and Federal Taxpayer Registry ID (RFC).
+"""
+
+
+from __future__ import unicode_literals
+
+import random
+import string
+
+from .. import Provider as BaseProvider
+
+
+ALPHABET = string.ascii_uppercase
+ALPHANUMERIC = string.digits + ALPHABET
+VOWELS = "AEIOU"
+CONSONANTS = [
+    letter
+    for letter in ALPHABET
+    if letter not in VOWELS
+]
+
+# https://es.wikipedia.org/wiki/Plantilla:Abreviaciones_de_los_estados_de_M%C3%A9xico
+STATES_RENAPO = [
+    "AS",
+    "BC",
+    "BS",
+    "CC",
+    "CS",
+    "CH",
+    "DF",
+    "CL",
+    "CM",
+    "DG",
+    "GT",
+    "GR",
+    "HG",
+    "JC",
+    "MC",
+    "MN",
+    "MS",
+    "NT",
+    "NL",
+    "OC",
+    "PL",
+    "QO",
+    "QR",
+    "SP",
+    "SL",
+    "SR",
+    "TC",
+    "TS",
+    "TL",
+    "VZ",
+    "YN",
+    "ZS",
+    "NE",  # Foreign Born
+]
+
+FORBIDDEN_WORDS = {
+    "BUEI": "BUEX",
+    "BUEY": "BUEX",
+    "CACA": "CACX",
+    "CACO": "CACX",
+    "CAGA": "CAGX",
+    "CAGO": "CAGX",
+    "CAKA": "CAKX",
+    "CAKO": "CAKX",
+    "COGE": "COGX",
+    "COJA": "COJX",
+    "COJE": "COJX",
+    "COJI": "COJX",
+    "COJO": "COJX",
+    "CULO": "CULX",
+    "FETO": "FETX",
+    "GUEY": "GUEX",
+    "JOTO": "JOTX",
+    "KACA": "KACX",
+    "KACO": "KACX",
+    "KAGA": "KAGX",
+    "KAGO": "KAGX",
+    "KOGE": "KOGX",
+    "KOJO": "KOJX",
+    "KAKA": "KAKX",
+    "KULO": "KULX",
+    "MAME": "MAMX",
+    "MAMO": "MAMX",
+    "MEAR": "MEAX",
+    "MEAS": "MEAX",
+    "MEON": "MEOX",
+    "MION": "MIOX",
+    "MOCO": "MOCX",
+    "MULA": "MULX",
+    "PEDA": "PEDX",
+    "PEDO": "PEDX",
+    "PENE": "PENX",
+    "PUTA": "PUTX",
+    "PUTO": "PUTX",
+    "QULO": "QULX",
+    "RATA": "RATX",
+    "RUIN": "RUIN",
+}
+
+CURP_CHARACTERS = "0123456789ABCDEFGHIJKLMNÑOPQRSTUVWXYZ"
+
+
+def _reduce_digits(number):
+    """
+    Sum of digits of a number until sum becomes single digit.
+
+    Example:
+        658 => 6 + 5 + 8 = 19 => 1 + 9 = 10 => 1
+    """
+    if number == 0:
+        return 0
+    if number % 9 == 0:
+        return 9
+
+    return number % 9
+
+
+def ssn_checksum(digits):
+    """
+    Calculate the checksum for the mexican SSN (IMSS).
+    """
+    return -sum(
+        _reduce_digits(n * (i % 2 + 1))
+        for i, n in enumerate(digits)
+    ) % 10
+
+
+def curp_checksum(characters):
+    """
+    Calculate the checksum for the mexican CURP.
+    """
+    start = 18
+    return -sum(
+        (start - i) * CURP_CHARACTERS.index(n)
+        for i, n in enumerate(characters)
+    ) % 10
+
+
+class Provider(BaseProvider):
+    """
+    A Faker provider for the Mexican SSN, RFC and CURP
+    """
+    ssn_formats = ("###########",)
+
+    def ssn(self):
+        """
+        Mexican Social Security Number, as given by IMSS.
+
+        :return: a random Mexican SSN
+        """
+        office = self.random_int(min=1, max=99)
+        birth_year = self.random_int(min=0, max=99)
+        start_year = self.random_int(min=0, max=99)
+        serial = self.random_int(min=1, max=9999)
+
+        num = "{0:02d}{1:02d}{2:02d}{3:04d}".format(
+            office,
+            start_year,
+            birth_year,
+            serial,
+        )
+
+        check = ssn_checksum(map(int, num))
+        num += str(check)
+
+        return num
+
+    def curp(self):
+        """
+        See https://es.wikipedia.org/wiki/Clave_%C3%9Anica_de_Registro_de_Poblaci%C3%B3n.
+
+        :return: a random Mexican CURP (Unique Population Registry Code)
+        """
+        birthday = self.generator.date_of_birth()
+
+        first_surname = random.choice(ALPHABET) + random.choice(VOWELS)
+        second_surname = random.choice(ALPHABET)
+        given_name = random.choice(ALPHABET)
+        name_initials = first_surname + second_surname + given_name
+
+        birth_date = birthday.strftime("%y%m%d")
+        gender = random.choice("HM")
+        state = random.choice(STATES_RENAPO)
+        first_surname_inside = random.choice(CONSONANTS)
+        second_surname_inside = random.choice(CONSONANTS)
+        given_name_inside = random.choice(ALPHABET)
+
+        # This character is assigned to avoid duplicity
+        # It's normally '0' for those born < 2000
+        # and 'A' for those born >= 2000
+        assigned_character = "0" if birthday.year < 2000 else "A"
+
+        name_initials = FORBIDDEN_WORDS.get(name_initials, name_initials)
+
+        random_curp = (
+            name_initials +
+            birth_date +
+            gender +
+            state +
+            first_surname_inside +
+            second_surname_inside +
+            given_name_inside +
+            assigned_character
+        )
+
+        random_curp += str(curp_checksum(random_curp))
+
+        return random_curp
+
+    def rfc(self, natural=True):
+        """
+        See https://es.wikipedia.org/wiki/Registro_Federal_de_Contribuyentes
+
+        :param natural: Whether to return the RFC of a natural person.
+            Otherwise return the RFC of a legal person.
+        :type natural: bool
+        :return: a random Mexican RFC
+        """
+        birthday = self.generator.date_of_birth()
+
+        if natural:
+            first_surname = random.choice(ALPHABET) + random.choice(VOWELS)
+            second_surname = random.choice(ALPHABET)
+            given_name = random.choice(ALPHABET)
+            name_initials = first_surname + second_surname + given_name
+        else:
+            name_initials = (
+                self.random_uppercase_letter() +
+                self.random_uppercase_letter() +
+                self.random_uppercase_letter()
+            )
+
+        birth_date = birthday.strftime("%y%m%d")
+        disambiguation_code = (
+            random.choice(ALPHANUMERIC) +
+            random.choice(ALPHANUMERIC) +
+            random.choice(ALPHANUMERIC)
+        )
+
+        random_rfc = (
+            name_initials +
+            birth_date +
+            disambiguation_code
+        )
+
+        return random_rfc


Test patch:
diff --git a/tests/providers/test_ssn.py b/tests/providers/test_ssn.py
--- a/tests/providers/test_ssn.py
+++ b/tests/providers/test_ssn.py
@@ -19,6 +19,8 @@
 from faker.providers.ssn.no_NO import checksum as no_checksum, Provider as no_Provider
 from faker.providers.ssn.pl_PL import checksum as pl_checksum, calculate_month as pl_calculate_mouth
 from faker.providers.ssn.pt_BR import checksum as pt_checksum
+from faker.providers.ssn.es_MX import (ssn_checksum as mx_ssn_checksum,
+                                       curp_checksum as mx_curp_checksum)
 
 
 class TestBgBG(unittest.TestCase):
@@ -390,6 +392,41 @@ def setUp(self):
         self.factory = Faker('es_CA')
 
 
+class TestEsMX(unittest.TestCase):
+    def setUp(self):
+        self.factory = Faker('es_MX')
+
+    def test_ssn(self):
+        for _ in range(100):
+            ssn = self.factory.ssn()
+
+            assert len(ssn) == 11
+            assert ssn.isnumeric()
+            assert mx_ssn_checksum(map(int, ssn[:-1])) == int(ssn[-1])
+
+    def test_curp(self):
+        for _ in range(100):
+            curp = self.factory.curp()
+
+            assert len(curp) == 18
+            assert re.search(r'^[A-Z]{4}\d{6}[A-Z]{6}[0A]\d$', curp)
+            assert mx_curp_checksum(curp[:-1]) == int(curp[-1])
+
+    def test_rfc_natural(self):
+        for _ in range(100):
+            rfc = self.factory.rfc()
+
+            assert len(rfc) == 13
+            assert re.search(r'^[A-Z]{4}\d{6}[0-9A-Z]{3}$', rfc)
+
+    def test_rfc_legal(self):
+        for _ in range(100):
+            rfc = self.factory.rfc(natural=False)
+
+            assert len(rfc) == 12
+            assert re.search(r'^[A-Z]{3}\d{6}[0-9A-Z]{3}$', rfc)
+
+
 class TestEtEE(unittest.TestCase):
     """ Tests SSN in the et_EE locale """