Repository: joke2k/faker

Issue:
Proposal for enhancement of Swedish SSN values (Pers ID, Org ID & MOMS (VAT) ID)
* Faker version: 2.0.2
* OS: *

I'd like to expand and correct some of the behaviour for Swedish ssn and VAT numbers, and add an option to return an organisation ID. 

Some info on how these codes work in Sweden:

1. **Personal number (SSN)** - the code for this is largely correct, but I would like to add a "long=False" parameter to the call. Default behaviour would remain the same, when set to true it would return a 12-digit SSN, which includes the century digits for the year. Most systems actually use the long version of the SSN and it would be handy to get this returned directly.

2. **Organisation number** - the code for this is mostly the same as the pers ID (SSN), but the "month" part of the number is 20 or higher. When taking 12 digit versions these are usually prefixed with "16".

3. **VAT number** (VAT is also called MOMS in Sweden) -  This number is not just a random sequence of numbers in Sweden. The organisation number is part of this. So the pattern is usually SEnnnnnnnnnnss, where n= org ID and s=sequence number (usually 01, unless you are a sole trader with multiple businesses)

..See wikipedia under Sweden for more info: https://en.wikipedia.org/wiki/VAT_identification_number

### Expected behavior

**Pers ID:**
1.  Request a Personal number without setting any params
2.  The 10 digit personal number is returned as it is today. 
3.  Request a personal number setting "long=True"
4.  A 12 digit personal number is returned, which passes Swedish validation checks. 

**Org ID:**
1.  Request an org ID setting no params or setting "long=False"
2.  A valid 10 digit org ID is returned (same checkdigit validation as for pers ID), which passes Swedish validation checks.
3.  Request an org ID setting "long=True"
4.  A valid 12 digit org ID is returned, which passes Swedish validation checks. 

**VAT numbers:**
1.  Request a VAT number
2.  The VAT number returned 
3.  The VAT number passes the validation that the first 10 digits after the "SE" are a valid org ID.

Note: Implementation of Org ID could either be as a new function, or using the existing ssn function with a parameter to set "org_id=True". Whichever is preferable.

### Actual behavior

**Pers ID:** Cannot return a long pers ID with 12 digits
**Org ID:** Not an option today
**VAT number:** currently returns a random string of digits


Discussion:
Just to be clear, I'm happy to take this enhancement myself if you want to assign it to me (I live in Sweden and would like to use this functionality).
Thank you so much! Everything sounds good. I think I would rather have a new method for the Org ID. Feels more explicit to me.

Excellent. I'm away for a few days now, so I'll get on this when I get back next week :)
Actually, minor change to the proposal. From the new org_id method I think it wise to return 2 values - the org ID itself, and a corresponding VAT number, since these match up in reality. The vat routine can still return a random VAT number should that be required by itself. 

How do you feel about 2 return values when requesting an org_id? Or would it be better to have an extra method that aligned the 2 by calling something like `fake.org_and_vat()` do you think?
@michjnich I'd rather have two separate methods. Seems more natural.

Resolving patch:
diff --git a/faker/providers/ssn/sv_SE/__init__.py b/faker/providers/ssn/sv_SE/__init__.py
--- a/faker/providers/ssn/sv_SE/__init__.py
+++ b/faker/providers/ssn/sv_SE/__init__.py
@@ -3,54 +3,96 @@
 from __future__ import unicode_literals
 from .. import Provider as SsnProvider
 import datetime
+import random
 
 
 class Provider(SsnProvider):
 
-    def ssn(self, min_age=18, max_age=90):
+    @staticmethod
+    def _luhn_checksum(number):
+        def digits_of(n):
+            return [int(d) for d in str(n)]
+        digits = digits_of(number)
+        odd_digits = digits[-1::-2]
+        even_digits = digits[-2::-2]
+        checksum = 0
+        checksum += sum(odd_digits)
+        for d in even_digits:
+            checksum += sum(digits_of(d * 2))
+        return checksum % 10
+
+    @staticmethod
+    def _calculate_luhn(partial_number):
+        check_digit = Provider._luhn_checksum(int(partial_number) * 10)
+        return check_digit if check_digit == 0 else 10 - check_digit
+
+    @staticmethod
+    def _org_to_vat(org_id):
+        org_id = org_id.replace('-', '')
+        if len(org_id) == 10:
+            org_id = '16' + org_id
+        return 'SE{0}01'.format(org_id)
+
+    def ssn(self, min_age=18, max_age=90, long=False, dash=True):
         """
-        Returns a 10 digit Swedish SSN, "Personnummer".
+        Returns a 10 or 12 (long=True) digit Swedish SSN, "Personnummer".
 
-        It consists of 10 digits in the form YYMMDD-SSGQ, where
+        It consists of 10 digits in the form (CC)YYMMDD-SSSQ, where
         YYMMDD is the date of birth, SSS is a serial number
         and Q is a control character (Luhn checksum).
 
+        Specifying dash=False will give a purely numeric string, suitable
+        for writing direct to databases.
+
         http://en.wikipedia.org/wiki/Personal_identity_number_(Sweden)
         """
-        def _luhn_checksum(number):
-            def digits_of(n):
-                return [int(d) for d in str(n)]
-            digits = digits_of(number)
-            odd_digits = digits[-1::-2]
-            even_digits = digits[-2::-2]
-            checksum = 0
-            checksum += sum(odd_digits)
-            for d in even_digits:
-                checksum += sum(digits_of(d * 2))
-            return checksum % 10
-
-        def _calculate_luhn(partial_number):
-            check_digit = _luhn_checksum(int(partial_number) * 10)
-            return check_digit if check_digit == 0 else 10 - check_digit
 
         age = datetime.timedelta(
             days=self.generator.random.randrange(min_age * 365, max_age * 365))
         birthday = datetime.datetime.now() - age
-        pnr_date = birthday.strftime('%y%m%d')
+        yr_fmt = '%Y' if long else '%y'
+        pnr_date = birthday.strftime('{}%m%d'.format(yr_fmt))
+        chk_date = pnr_date[2:] if long else pnr_date
         suffix = str(self.generator.random.randrange(0, 999)).zfill(3)
-        luhn_checksum = str(_calculate_luhn(pnr_date + suffix))
-        pnr = '{0}-{1}{2}'.format(pnr_date, suffix, luhn_checksum)
+        luhn_checksum = str(Provider._calculate_luhn(chk_date + suffix))
+        hyphen = '-' if dash else ''
+        pnr = '{0}{1}{2}{3}'.format(pnr_date, hyphen, suffix, luhn_checksum)
 
         return pnr
 
-    vat_id_formats = (
-        'SE############',
-    )
+    ORG_ID_DIGIT_1 = (1, 2, 3, 5, 6, 7, 8, 9)
+
+    def org_id(self, long=False, dash=True):
+        """
+        Returns a 10 or 12 digit Organisation ID for a Swedish
+        company.
+        (In Swedish) https://sv.wikipedia.org/wiki/Organisationsnummer
+        """
+        first_digits = list(self.ORG_ID_DIGIT_1)
+        random.shuffle(first_digits)
+        onr_one = str(first_digits.pop())
+        onr_one += str(self.generator.random.randrange(0, 9)).zfill(1)
+        onr_one += str(self.generator.random.randrange(20, 99))
+        onr_one += str(self.generator.random.randrange(0, 99)).zfill(2)
+        onr_two = str(self.generator.random.randrange(0, 999)).zfill(3)
+        luhn_checksum = str(Provider._calculate_luhn(onr_one + onr_two))
+        prefix = '16' if long else ''
+        hyphen = '-' if dash else ''
+
+        org_id = '{0}{1}{2}{3}{4}'.format(prefix, onr_one, hyphen, onr_two, luhn_checksum)
+        return org_id
 
     def vat_id(self):
         """
         http://ec.europa.eu/taxation_customs/vies/faq.html#item_11
-        :return: A random Swedish VAT ID
+        :return: A random Swedish VAT ID, based on a valid Org ID
         """
+        oid = self.org_id(long=True, dash=False)
+        vid = Provider._org_to_vat(oid)
+        return vid
 
-        return self.bothify(self.random_element(self.vat_id_formats))
+    def org_and_vat_id(self, long=False, dash=True):
+        """Returns matching Org ID and VAT number"""
+        oid = self.org_id(long=long, dash=dash)
+        vid = Provider._org_to_vat(oid)
+        return oid, vid


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
@@ -5,6 +5,7 @@
 import re
 import unittest
 from datetime import datetime
+from itertools import cycle
 
 import freezegun
 import pytest
@@ -23,6 +24,114 @@
                                        curp_checksum as mx_curp_checksum)
 
 
+class TestSvSE(unittest.TestCase):
+    def setUp(self):
+        self.factory = Faker('sv_SE')
+
+    def partial_sum(self, number, mult_factor):
+        quotient, remainder = divmod(number * mult_factor, 10)
+        return quotient + remainder
+
+    def ssn_checksum(self, ssn):
+        """Validates the checksum digit and returns a Boolean"""
+        ssn = ssn.replace('-', '')
+        if len(ssn) == 12:
+            ssn = ssn[2:]
+        if len(ssn) != 10:
+            return False
+
+        mult_factors = cycle([2, 1])
+        final_sum = sum(self.partial_sum(int(char), mf) for char, mf in zip(ssn[:9], mult_factors))
+        chksum = -final_sum % 10
+        return chksum == int(ssn[-1])
+
+    def validate_date_string(self, date_str):
+        date_len = len(date_str)
+        if date_len == 6:
+            year_fmt = '%y'
+        elif date_len == 8:
+            year_fmt = '%Y'
+        else:
+            return False
+
+        try:
+            if date_str != datetime.strptime(date_str, '{}%m%d'.format(year_fmt)).strftime('{}%m%d'.format(year_fmt)):
+                raise ValueError
+            return True
+        except ValueError:
+            return False
+
+    def test_pers_id_short_with_dash(self):
+        """Regression case that ensures previous implementations work as-is"""
+        for _ in range(100):
+            pers_id = self.factory.ssn()
+            assert re.search(r'\d{6}-\d{4}', pers_id)
+            assert self.validate_date_string(pers_id[:6]) is True
+            assert self.ssn_checksum(pers_id) is True
+
+    def test_pers_id_short_no_dash(self):
+        for _ in range(100):
+            pers_id = self.factory.ssn(dash=False)
+            assert re.search(r'\d{10}', pers_id)
+            assert self.validate_date_string(pers_id[:6]) is True
+            assert self.ssn_checksum(pers_id) is True
+
+    def test_pers_id_long_with_dash(self):
+        for _ in range(100):
+            pers_id = self.factory.ssn(long=True)
+            assert re.search(r'\d{8}-\d{4}', pers_id)
+            assert self.validate_date_string(pers_id[:8]) is True
+            assert self.ssn_checksum(pers_id) is True
+
+    def test_pers_id_long_no_dash(self):
+        for _ in range(100):
+            pers_id = self.factory.ssn(long=True, dash=False)
+            assert re.search(r'\d{12}', pers_id)
+            assert self.validate_date_string(pers_id[:8]) is True
+            assert self.ssn_checksum(pers_id) is True
+
+    def test_org_id_short_with_dash(self):
+        for _ in range(100):
+            org_id = self.factory.org_id()
+            assert re.search(r'\d{6}-\d{4}', org_id)
+            assert int(org_id[2:4]) >= 20
+            assert self.ssn_checksum(org_id) is True
+
+    def test_org_id_short_no_dash(self):
+        for _ in range(100):
+            org_id = self.factory.org_id(dash=False)
+            assert re.search(r'\d{10}', org_id)
+            assert int(org_id[2:4]) >= 20
+            assert self.ssn_checksum(org_id) is True
+
+    def test_org_id_long_with_dash(self):
+        for _ in range(100):
+            org_id = self.factory.org_id(long=True)
+            assert re.search(r'\d{8}-\d{4}', org_id)
+            assert int(org_id[4:6]) >= 20
+            assert self.ssn_checksum(org_id) is True
+
+    def test_org_id_long_no_dash(self):
+        for _ in range(100):
+            org_id = self.factory.org_id(long=True, dash=False)
+            assert re.search(r'\d{12}', org_id)
+            assert int(org_id[4:6]) >= 20
+            assert self.ssn_checksum(org_id) is True
+
+    def test_vat_id(self):
+        for _ in range(100):
+            vat_id = self.factory.vat_id()
+            assert re.search(r'SE\d{12}', vat_id)
+            assert int(vat_id[2]) in (1, 2, 3, 5, 6, 7, 8, 9)
+            assert int(vat_id[6:8]) >= 20
+
+    def test_org_and_vat_id(self):
+        for _ in range(100):
+            oid, vid = self.factory.org_and_vat_id()
+            assert oid.replace('-', '')[-10:] == vid[4:-2]
+            assert re.search(r'SE\d{12}', vid)
+
+
 class TestBgBG(unittest.TestCase):
     def setUp(self):
         self.factory = Faker('bg_BG')