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

[14.0] l10n_br_base, l10n_br_account: Cadastro de chaves PIX no Parceiro. #2123

Merged
merged 3 commits into from
Sep 30, 2022
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 l10n_br_account/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"views/l10n_br_account_menu.xml",
# Report
# "report/account_invoice_report_view.xml",
"views/res_partner_view.xml",
],
"demo": [
"demo/res_users_demo.xml",
Expand Down
34 changes: 34 additions & 0 deletions l10n_br_account/views/res_partner_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!--
Partners Extension
-->
<record id="l10n_br_account_partner_form" model="ir.ui.view">
<field name="name">l10n_br_account.res.partner.form</field>
<field name="model">res.partner</field>
<field name="priority">99</field>
<field name="inherit_id" ref="account.view_partner_property_form" />
<field name="arch" type="xml">
<group position="after" name="banks">
<div />
<group
antoniospneto marked this conversation as resolved.
Show resolved Hide resolved
string="Brazilian Instant Payment Keys (PIX)"
name="pix_keys"
groups="account.group_account_invoice,account.group_account_readonly"
attrs="{'invisible': [('show_l10n_br', '=', False)]}"
>
<field name="show_l10n_br" invisible="1" />
<field name="pix_key_ids" nolabel="1">
<tree editable="bottom">
<field name="partner_id" invisible="1" />
<field name="sequence" widget="handle" />
<field name="key_type" />
<field name="key" />
<field name="partner_bank_id" />
</tree>
</field>
</group>
</group>
</field>
</record>
</odoo>
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 @@ -12,3 +12,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 @@ -176,3 +188,13 @@ def _address_fields(self):
@api.onchange("city_id")
def _onchange_city_id(self):
self.city = self.city_id.name

def _compute_show_l10n_br(self):
Copy link
Member

@marcelsavegnago marcelsavegnago Sep 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@antoniospneto Estou com dúvida sobre este método já que a principio por default os parceiros não tem um company_id definido. Talvez possamos incrementar testando se o pais do parceiro é diferente de BR. Fora isso, acredita que seja válido considerar a empresa selecionada no momento também?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realmente, a validação que fiz na época não é uma boa.
Acho que o código do país do parceiro vai ser melhor 👍🏻

"""
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(
antoniospneto marked this conversation as resolved.
Show resolved Hide resolved
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ao invés de chamar o check_vals nos métodos create e write você pode adicionar a notação @api.constrains no método check_vals

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@netosjb, você chegou a ver essa mudança? Daria para usar o api.constraints() no método check_vals e não sobre escrever os método create e write...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

que estranho, eu tinha respondido essa questão, mas meu comentário sumiu.

Sim, eu tentei fazer com a notação, porém no meu uso não deu certo, é que além de fazer a validação eu faço a normalização dos campos que altera o valor do mesmo. pelo fato do valor está sendo alterado, com o api.constraints() o método entra em um loop infinito, fica chamando de forma recursiva até atingir o maximo e estourar uma exception.

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
17 changes: 13 additions & 4 deletions l10n_br_base/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
* Renato Lima <[email protected]>
* Raphaël Valyi <[email protected]>
* Luis Felipe Mileo <[email protected]>
* Michell Stuttgart <[email protected]>
* `Akretion <https://www.akretion.com/pt-BR>`_:

* Renato Lima <[email protected]>
* Raphaël Valyi <[email protected]>

* `KMEE <https://www.kmee.com.br>`_:

* Luis Felipe Mileo <[email protected]>
* Michell Stuttgart <[email protected]>

* `Engenere <https://engenere.one>`_:

* Antônio S. Pereira Neto <[email protected]>
3 changes: 2 additions & 1 deletion l10n_br_base/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
Este é o módulo raiz da localização brasileira. Ele traz adaptações nos modelos do módulo ``base`` do Odoo como Parceiros, Empresas e Endereços:
Este é o módulo 'raiz' da localização brasileira. Ele traz adaptações nos modelos do módulo ``base`` do Odoo como Parceiros, Empresas e Endereços:

* Campo CNPJ e CPF com formatação e validação destes campos;
* Campo de Inscrição Estadual com validação;
* Formatação dos campos de endereço;
* Código do Banco Central e Siscomex para países;
* Código do IBGE para estados e municípios;
* Lista dos Bancos brasileiros;
* Contas bancarias e chaves PIX dos parceiros;
* Lista dos municípios brasileiros.

Se trata de um módulo muito simples e maduro. Existem alguns outros módulos simples que dependem apenas desse módulo ou quase como ``l10n_br_crm`` ou ``l10n_br_portal``.
Expand Down
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
Loading