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 br model fields #365

Merged
merged 2 commits into from
Mar 6, 2019
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/authors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Authors
* Alexey Kotlyarov
* Alix Martineau
* Alonisser
* André Ramos
* Andreas Pelme
* Andres Torres Marroquin
* Andrew Godwin
Expand Down
3 changes: 1 addition & 2 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ New flavors:
- None

New fields for existing flavors:

- None
- Added BRPostalCodeField, BRCPFField, BRCNPJField models fields.

Modifications to existing flavors:

Expand Down
6 changes: 6 additions & 0 deletions docs/localflavor/br.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ Forms
.. automodule:: localflavor.br.forms
:members:

Models
------

.. automodule:: localflavor.br.models
:members:

Data
----

Expand Down
84 changes: 23 additions & 61 deletions localflavor/br/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,34 @@

from django.core.validators import EMPTY_VALUES
from django.forms import ValidationError
from django.forms.fields import CharField, Field, RegexField, Select
from django.forms.fields import CharField, Field, Select
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _

from .br_states import STATE_CHOICES
from .validators import BRCNPJValidator, BRCPFValidator, BRPostalCodeValidator

cpf_digits_re = re.compile(r'^(\d{3})\.(\d{3})\.(\d{3})-(\d{2})$')
cnpj_digits_re = re.compile(
r'^(\d{2})[.-]?(\d{3})[.-]?(\d{3})/(\d{4})-(\d{2})$'
)
process_digits_re = re.compile(
r'^(\d{7})-?(\d{2})\.?(\d{4})\.?(\d)\.?(\d{2})\.?(\d{4})$'
)


class BRZipCodeField(RegexField):
"""A form field that validates input as a Brazilian zip code, with the format XXXXX-XXX."""
class BRZipCodeField(CharField):
"""
A form field that validates input as a Brazilian zip code, with the format XXXXX-XXX.

.. versionchanged:: 2.2
Use BRPostalCodeValidator to centralize validation logic and share with equivalent model field.
More details at: https://github.com/django/django-localflavor/issues/334
"""

default_error_messages = {
'invalid': _('Enter a zip code in the format XXXXX-XXX.'),
}

def __init__(self, *args, **kwargs):
super(BRZipCodeField, self).__init__(r'^\d{5}-\d{3}$', *args, **kwargs)
super(BRZipCodeField, self).__init__(*args, **kwargs)
self.validators.append(BRPostalCodeValidator())


class BRStateSelect(Select):
Expand Down Expand Up @@ -65,12 +69,6 @@ def clean(self, value):
return value


def dv_maker(v):
if v >= 2:
return 11 - v
return 0


class BRCPFField(CharField):
"""
A form field that validates a CPF number or a CPF string.
Expand All @@ -79,6 +77,10 @@ class BRCPFField(CharField):

More information:
http://en.wikipedia.org/wiki/Cadastro_de_Pessoas_F%C3%ADsicas

.. versionchanged:: 2.2
Use BRCPFValidator to centralize validation logic and share with equivalent model field.
More details at: https://github.com/django/django-localflavor/issues/334
"""

default_error_messages = {
Expand All @@ -88,37 +90,14 @@ class BRCPFField(CharField):

def __init__(self, max_length=14, min_length=11, *args, **kwargs):
super(BRCPFField, self).__init__(max_length=max_length, min_length=min_length, *args, **kwargs)
self.validators.append(BRCPFValidator())

def clean(self, value):
"""Value can be either a string in the format XXX.XXX.XXX-XX or an 11-digit number."""
value = super(BRCPFField, self).clean(value)
if value in self.empty_values:
return self.empty_value
orig_value = value[:]
if not value.isdigit():
cpf = cpf_digits_re.search(value)
if cpf:
value = ''.join(cpf.groups())
else:
raise ValidationError(self.error_messages['invalid'])

if len(value) != 11:
raise ValidationError(self.error_messages['max_digits'])
orig_dv = value[-2:]

new_1dv = sum([i * int(value[idx])
for idx, i in enumerate(range(10, 1, -1))])
new_1dv = dv_maker(new_1dv % 11)
value = value[:-2] + str(new_1dv) + value[-1]
new_2dv = sum([i * int(value[idx])
for idx, i in enumerate(range(11, 1, -1))])
new_2dv = dv_maker(new_2dv % 11)
value = value[:-1] + str(new_2dv)
if value[-2:] != orig_dv:
raise ValidationError(self.error_messages['invalid'])
if value.count(value[0]) == 11:
raise ValidationError(self.error_messages['invalid'])
return orig_value
return value


class BRCNPJField(CharField):
Expand All @@ -138,6 +117,9 @@ class BRCNPJField(CharField):

.. _Brazilian CNPJ: http://en.wikipedia.org/wiki/National_identification_number#Brazil
.. versionchanged:: 1.4
.. versionchanged:: 2.2
Use BRCNPJValidator to centralize validation logic and share with equivalent model field.
More details at: https://github.com/django/django-localflavor/issues/334
"""

default_error_messages = {
Expand All @@ -147,34 +129,14 @@ class BRCNPJField(CharField):

def __init__(self, min_length=14, max_length=18, *args, **kwargs):
super(BRCNPJField, self).__init__(max_length=max_length, min_length=min_length, *args, **kwargs)
self.validators.append(BRCNPJValidator())

def clean(self, value):
"""Value can be either a string in the format XX.XXX.XXX/XXXX-XX or a group of 14 characters."""
value = super(BRCNPJField, self).clean(value)
if value in self.empty_values:
return self.empty_value
orig_value = value[:]
if not value.isdigit():
cnpj = cnpj_digits_re.search(value)
if cnpj:
value = ''.join(cnpj.groups())
else:
raise ValidationError(self.error_messages['invalid'])

if len(value) != 14:
raise ValidationError(self.error_messages['max_digits'])
orig_dv = value[-2:]

new_1dv = sum([i * int(value[idx]) for idx, i in enumerate(list(range(5, 1, -1)) + list(range(9, 1, -1)))])
new_1dv = dv_maker(new_1dv % 11)
value = value[:-2] + str(new_1dv) + value[-1]
new_2dv = sum([i * int(value[idx]) for idx, i in enumerate(list(range(6, 1, -1)) + list(range(9, 1, -1)))])
new_2dv = dv_maker(new_2dv % 11)
value = value[:-1] + str(new_2dv)
if value[-2:] != orig_dv:
raise ValidationError(self.error_messages['invalid'])

return orig_value
return value


def mod_97_base10(value):
Expand Down
53 changes: 53 additions & 0 deletions localflavor/br/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-

from django.db.models.fields import CharField
from django.utils.translation import ugettext_lazy as _

from . import validators
from .br_states import STATE_CHOICES


Expand All @@ -18,3 +21,53 @@ def deconstruct(self):
name, path, args, kwargs = super(BRStateField, self).deconstruct()
del kwargs['choices']
return name, path, args, kwargs


class BRCPFField(CharField):
"""
A model field for the brazilian document named of CPF (Cadastro de Pessoa Física)

.. versionadded:: 2.2
"""

description = _("CPF Document")

default_error_messages = {
'invalid': _("Invalid CPF number."),
'max_digits': _("This field requires at most 11 digits or 14 characters."),
}

def __init__(self, *args, **kwargs):
kwargs['max_length'] = 14
super(BRCPFField, self).__init__(*args, **kwargs)
self.validators.append(validators.BRCPFValidator())


class BRCNPJField(CharField):
"""
A model field for the brazilian document named of CNPJ (Cadastro Nacional de Pessoa Jurídica)

.. versionadded:: 2.2
"""

description = _("CNPJ Document")

def __init__(self, *args, **kwargs):
kwargs['max_length'] = 18
super(BRCNPJField, self).__init__(*args, **kwargs)
self.validators.append(validators.BRCNPJValidator())


class BRPostalCodeField(CharField):
"""
A model field for the brazilian zip code

.. versionadded:: 2.2
"""

description = _("Postal Code")

def __init__(self, *args, **kwargs):
kwargs['max_length'] = 9
super(BRPostalCodeField, self).__init__(*args, **kwargs)
self.validators.append(validators.BRPostalCodeValidator())
107 changes: 107 additions & 0 deletions localflavor/br/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import re

from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.utils.translation import ugettext_lazy as _

postal_code_re = re.compile(r'^\d{5}-\d{3}$')
cnpj_digits_re = re.compile(r'^(\d{2})[.-]?(\d{3})[.-]?(\d{3})/(\d{4})-(\d{2})$')
cpf_digits_re = re.compile(r'^(\d{3})\.(\d{3})\.(\d{3})-(\d{2})$')


def dv_maker(v):
if v >= 2:
return 11 - v
return 0


class BRPostalCodeValidator(RegexValidator):
"""
A validator for Brazilian Postal Codes (CEP).

.. versionadded:: 2.2
"""

def __init__(self, *args, **kwargs):
self.message = _('Enter a postal code in the format 00000-000.')
self.code = _('Invalid Postal Code')
super(BRPostalCodeValidator, self).__init__(postal_code_re, *args, **kwargs)


class BRCNPJValidator(RegexValidator):
"""
Validator for brazilian CNPJ.

.. versionadded:: 2.2
"""

def __init__(self, *args, **kwargs):
super(BRCNPJValidator, self).__init__(
*args,
regex=cnpj_digits_re,
message=_("Invalid CNPJ number."),
**kwargs
)

def __call__(self, value):
orig_dv = value[-2:]

if not value.isdigit():
cnpj = cnpj_digits_re.search(value)
if cnpj:
value = ''.join(cnpj.groups())
else:
raise ValidationError(self.message, code='invalid')

if len(value) != 14:
raise ValidationError(self.message, code='max_digits')

new_1dv = sum([i * int(value[idx]) for idx, i in enumerate(list(range(5, 1, -1)) + list(range(9, 1, -1)))])
new_1dv = dv_maker(new_1dv % 11)
value = value[:-2] + str(new_1dv) + value[-1]
new_2dv = sum([i * int(value[idx]) for idx, i in enumerate(list(range(6, 1, -1)) + list(range(9, 1, -1)))])
new_2dv = dv_maker(new_2dv % 11)
value = value[:-1] + str(new_2dv)
if value[-2:] != orig_dv:
raise ValidationError(self.message, code='invalid')


class BRCPFValidator(RegexValidator):
"""
Validator for brazilian CPF.

.. versionadded:: 2.2
"""

def __init__(self, *args, **kwargs):
super(BRCPFValidator, self).__init__(
*args,
regex=cpf_digits_re,
message=_("Invalid CPF number."),
**kwargs
)

def __call__(self, value):
if not value.isdigit():
cpf = cpf_digits_re.search(value)
if cpf:
value = ''.join(cpf.groups())
else:
raise ValidationError(self.message, code='invalid')

if len(value) != 11:
raise ValidationError(self.message, code='max_digits')

orig_dv = value[-2:]
new_1dv = sum([i * int(value[idx])
for idx, i in enumerate(range(10, 1, -1))])
new_1dv = dv_maker(new_1dv % 11)
value = value[:-2] + str(new_1dv) + value[-1]
new_2dv = sum([i * int(value[idx])
for idx, i in enumerate(range(11, 1, -1))])
new_2dv = dv_maker(new_2dv % 11)
value = value[:-1] + str(new_2dv)
if value[-2:] != orig_dv:
raise ValidationError(self.message, code='invalid')
if value.count(value[0]) == 11:
raise ValidationError(self.message, code='invalid')
1 change: 1 addition & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

INSTALLED_APPS = [
'localflavor',
'tests.test_br',
'tests.test_au',
'tests.test_ec',
'tests.test_md',
Expand Down
Empty file added tests/test_br/__init__.py
Empty file.
10 changes: 10 additions & 0 deletions tests/test_br/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.forms import ModelForm

from .models import BRPersonProfile


class BRPersonProfileForm(ModelForm):

class Meta:
model = BRPersonProfile
fields = ('cpf', 'cnpj', 'postal_code')
9 changes: 9 additions & 0 deletions tests/test_br/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.db import models

from localflavor.br.models import BRCNPJField, BRCPFField, BRPostalCodeField


class BRPersonProfile(models.Model):
cpf = BRCPFField()
cnpj = BRCNPJField()
postal_code = BRPostalCodeField()
Loading