Skip to content

Commit

Permalink
[IMP] l10n_br_base: add pix fields for partners
Browse files Browse the repository at this point in the history
  • Loading branch information
antoniospneto authored and rvalyi committed Oct 23, 2022
1 parent c06e5d7 commit 50fc5e5
Show file tree
Hide file tree
Showing 12 changed files with 437 additions and 1 deletion.
5 changes: 4 additions & 1 deletion l10n_br_base/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@
"demo/res_partner_demo.xml",
"demo/res_company_demo.xml",
"demo/res_users_demo.xml",
"demo/res_partner_pix_demo.xml",
],
"installable": True,
"pre_init_hook": "pre_init_hook",
"development_status": "Mature",
"external_dependencies": {"python": ["num2words", "erpbrasil.base"]},
"external_dependencies": {
"python": ["num2words", "erpbrasil.base", "phonenumbers", "email_validator"]
},
}
32 changes: 32 additions & 0 deletions l10n_br_base/demo/res_partner_pix_demo.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>

<!--
Resource: res.partner.pix
Partner: AMD do Brasil
-->
<record id="res_partner_amd_pix_cnpj" model="res.partner.pix">
<field name="partner_id" ref="res_partner_amd" />
<field name="key_type">cnpj_cpf</field>
<field name="key">62228384000151</field>
</record>

<record id="res_partner_amd_pix_phone" model="res.partner.pix">
<field name="partner_id" ref="res_partner_amd" />
<field name="key_type">phone</field>
<field name="key">1144576060</field>
</record>

<record id="res_partner_amd_pix_email" model="res.partner.pix">
<field name="partner_id" ref="res_partner_amd" />
<field name="key_type">email</field>
<field name="key">[email protected]</field>
</record>

<record id="res_partner_amd_evp" model="res.partner.pix">
<field name="partner_id" ref="res_partner_amd" />
<field name="key_type">evp</field>
<field name="key">123e4567-e12b-12d1-a456-426655440000</field>
</record>

</odoo>
1 change: 1 addition & 0 deletions l10n_br_base/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
from . import state_tax_numbers
from . import res_company
from . import res_config_settings
from . import res_partner_pix
22 changes: 22 additions & 0 deletions l10n_br_base/models/res_partner.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ class Partner(models.Model):

union_entity_code = fields.Char(string="Union Entity code")

pix_key_ids = fields.One2many(
string="Pix Keys",
comodel_name="res.partner.pix",
inverse_name="partner_id",
help="Keys for Brazilian instant payment (pix)",
)

show_l10n_br = fields.Boolean(
compute="_compute_show_l10n_br",
help="Indicates if Brazilian localization fields should be displayed.",
)

@api.constrains("cnpj_cpf", "inscr_est")
def _check_cnpj_inscr_est(self):
for record in self:
Expand Down Expand Up @@ -184,3 +196,13 @@ def _set_street(self):
@api.onchange("city_id")
def _onchange_city_id(self):
self.city = self.city_id.name

def _compute_show_l10n_br(self):
"""
Defines when Brazilian localization fields should be displayed.
"""
for rec in self:
if rec.company_id and rec.company_id.country_id != self.env.ref("base.br"):
rec.show_l10n_br = False
else:
rec.show_l10n_br = True
52 changes: 52 additions & 0 deletions l10n_br_base/models/res_partner_bank.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
("13", _("Conta depósito judicial/Depósito em consignação conjunta")),
]

TRANSACTIONAL_ACCOUNT_TYPE = [
("checking", _("Checking Account (Conta Corrente)")),
("saving", _("Saving Account (Conta Poupança)")),
("payment", _("Prepaid Payment Account (Conta Pagamento)")),
]


class ResPartnerBank(models.Model):
"""Adiciona campos necessários para o cadastramentos de contas
Expand All @@ -28,6 +34,19 @@ class ResPartnerBank(models.Model):
default="01",
)

transactional_acc_type = fields.Selection(
selection=TRANSACTIONAL_ACCOUNT_TYPE,
string="Account Type",
help="Type of transactional account, classification used in "
"the Brazilian instant payment system (PIX)",
)

partner_pix_ids = fields.One2many(
comodel_name="res.partner.pix",
inverse_name="partner_bank_id",
string="Pix Keys",
)

acc_number = fields.Char(
string="Account Number",
size=64,
Expand Down Expand Up @@ -55,9 +74,42 @@ class ResPartnerBank(models.Model):
help="Last part of BIC/Swift Code.",
)

company_country_id = fields.Many2one(
comodel_name="res.country",
string="Company Country",
related="company_id.country_id",
)

@api.constrains("bra_number")
def _check_bra_number(self):
for b in self:
if b.bank_id.code_bc:
if len(b.bra_number) > 4:
raise UserError(_("Bank branch code must be four caracteres."))

@api.constrains(
"transactional_acc_type",
"bank_id",
"acc_number",
"bra_number",
"acc_number_dig",
)
def _check_transc_acc_type(self):
for rec in self:
if rec.transactional_acc_type:
if not rec.bank_id or not rec.bank_id.code_bc or not rec.acc_number:
raise UserError(
_(
"a transactional account must contain the bank "
"information (code_bc) and the account number"
)
)
if rec.transactional_acc_type in ["checking", "saving"]:
if not rec.bra_number or not rec.acc_number_dig:
raise UserError(
_(
"A Checking Account or Saving Account transactional account "
"must contain the branch number and the account verification "
"digit."
)
)
150 changes: 150 additions & 0 deletions l10n_br_base/models/res_partner_pix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import phonenumbers
from email_validator import EmailSyntaxError, validate_email
from erpbrasil.base.fiscal import cnpj_cpf

from odoo import _, api, fields, models
from odoo.exceptions import ValidationError


class PartnerPix(models.Model):
_name = "res.partner.pix"
_description = "Brazilian instant payment ecosystem (Pix)"
_order = "sequence, id"
_rec_name = "key"

_sql_constraints = [
(
"partner_pix_key_unique",
"unique(key_type, key, partner_id)",
"A Pix Key with this values already exists in this partner.",
)
]

KEY_TYPES = [
("cnpj_cpf", _("CPF or CNPJ")),
("phone", _("Phone Number")),
("email", _("E-mail")),
("evp", _("Random Key")),
]

partner_id = fields.Many2one(
comodel_name="res.partner",
string="Partner",
ondelete="cascade",
required=True,
)
sequence = fields.Integer(default=10)
key_type = fields.Selection(
selection=KEY_TYPES,
string="Type",
required=True,
)
key = fields.Char(
help="PIX Addressing key",
required=True,
)

partner_bank_id = fields.Many2one(
comodel_name="res.partner.bank",
string="Bank Account",
domain="[('partner_id', '=', partner_id)]",
)

def _normalize_email(self, email):
try:
result = validate_email(
email,
check_deliverability=False,
)
except EmailSyntaxError:
raise ValidationError(_(f"{email.strip()} is an invalid email"))
normalized_email = result["local"].lower() + "@" + result["domain_i18n"]
if len(normalized_email) > 77:
raise ValidationError(
_(
f"The email is too long, "
f"a maximum of 77 characters is allowed: \n{email.strip()}"
)
)
return normalized_email

def _normalize_phone(self, phone):
try:
phonenumber = phonenumbers.parse(phone, "BR")
except phonenumbers.phonenumberutil.NumberParseException as e:
raise ValidationError(_(f"Unable to parse {phone}: {str(e)}"))
if not phonenumbers.is_possible_number(phonenumber):
raise ValidationError(
_(f"Impossible number {phone}: probably invalid number of digits.")
)
if not phonenumbers.is_valid_number(phonenumber):
raise ValidationError(
_(f"Invalid number {phone}: probably incorrect prefix.")
)
phone = phonenumbers.format_number(
phonenumber, phonenumbers.PhoneNumberFormat.E164
)
return phone

def _normalize_cnpj_cpf(self, doc_number):
doc_number = "".join(char for char in doc_number if char.isdigit())
if not 11 <= len(doc_number) <= 14:
raise ValidationError(
_(
f"Invalid Document Number {doc_number}: "
f"\nThe CPF must have 11 digits and the CNPJ 14 digits."
)
)
is_valid = cnpj_cpf.validar(doc_number)
if not is_valid:
raise ValidationError(_(f"Invalid Document Number: {doc_number}"))
return doc_number

def _normalize_evp(self, key):
# EVP: Endereço Virtual de Pagamento (chave aleatória)
# ex: 123e4567-e12b-12d1-a456-426655440000
key = "".join(key.split())
if len(key) != 36:
raise ValidationError(
_(f"Invalid Random Key: {key}, cannot be longer than 35 characters")
)
blocks = key.split("-")
if len(blocks) != 5:
raise ValidationError(
_(f"Invalid Random Key: {key}, the key must consist of five blocks.")
)
for block in blocks:
try:
int(block, 16)
except ValueError:
raise ValidationError(
_(
f"Invalid Random Key: {key} \nthe block {block} "
f"is not a valid hexadecimal format."
)
)
return key

@api.model
def create(self, vals):
self.check_vals(vals)
return super(PartnerPix, self).create(vals)

def write(self, vals):
self.check_vals(vals)
return super(PartnerPix, self).write(vals)

def check_vals(self, vals):
key_type = vals.get("key_type") or self.key_type
key = vals.get("key") or self.key
if not key or not key_type:
return
if key_type == "email":
key = self._normalize_email(key)
elif key_type == "phone":
key = self._normalize_phone(key)
elif key_type == "cnpj_cpf":
key = self._normalize_cnpj_cpf(key)
elif key_type == "evp":
key = self._normalize_evp(key)
vals["key"] = key
2 changes: 2 additions & 0 deletions l10n_br_base/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"state_tax_numbers_user","State Tax Numbers for User","model_state_tax_numbers","base.group_user",1,0,0,0
"state_tax_numbers_manager","State Tax Numbers for Manager","model_state_tax_numbers","base.group_system",1,1,1,1
"res_partner_pix_user","Partner PIX for User","model_res_partner_pix","base.group_user",1,0,0,0
"res_partner_pix_manager","Partner PIX for Partner Manager","model_res_partner_pix","base.group_partner_manager",1,1,1,1
2 changes: 2 additions & 0 deletions l10n_br_base/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@
from . import test_valid_createid
from . import test_base_onchange
from . import test_other_ie
from . import test_valid_pix
from . import test_partner_bank
36 changes: 36 additions & 0 deletions l10n_br_base/tests/test_partner_bank.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from odoo.exceptions import UserError
from odoo.tests import TransactionCase


class PartnerBankTest(TransactionCase):
def setUp(self):
super().setUp()
self.partner_bank_model = self.env["res.partner.bank"]
self.partner_id = self.env.ref("l10n_br_base.res_partner_amd")
self.bank_id = self.env.ref("l10n_br_base.res_bank_001")

def test_ok_transactional_acc_type(self):
ok_bank_vals = {
"partner_id": self.partner_id.id,
"transactional_acc_type": "checking",
"bank_id": self.bank_id.id,
"bra_number": "1020",
"acc_number": "102030",
"acc_number_dig": "9",
}
ok_acc_bank = self.partner_bank_model.with_context(
tracking_disable=True
).create(ok_bank_vals)
self.assertTrue(ok_acc_bank.exists())

def test_wrong_transactional_acc_type(self):
wrong_bank_vals = {
"partner_id": self.partner_id.id,
"transactional_acc_type": "checking",
"bra_number": "1020",
"acc_number_dig": "9",
}
with self.assertRaises(UserError):
self.partner_bank_model.with_context(tracking_disable=True).create(
wrong_bank_vals
)
Loading

0 comments on commit 50fc5e5

Please sign in to comment.