mirror of
https://github.com/bringout/oca-ocb-l10n_americas.git
synced 2026-04-27 06:52:04 +02:00
19.0 vanilla
This commit is contained in:
parent
89c6e82fe7
commit
1b82c20a58
572 changed files with 43570 additions and 53303 deletions
|
|
@ -1,11 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
# Copyright (C) 2009 Renato Lima - Akretion
|
||||
|
||||
from . import template_br
|
||||
from . import account
|
||||
from . import account_fiscal_position_template
|
||||
from . import account_journal
|
||||
from . import account_move
|
||||
from . import account_fiscal_position
|
||||
from . import account_chart_template
|
||||
from . import res_company
|
||||
from . import l10n_br_zip_range
|
||||
from . import res_partner
|
||||
from . import res_city
|
||||
from . import res_company
|
||||
from . import res_partner_bank
|
||||
|
|
|
|||
|
|
@ -4,23 +4,11 @@
|
|||
from odoo import fields, models
|
||||
|
||||
|
||||
class AccountTaxTemplate(models.Model):
|
||||
""" Add fields used to define some brazilian taxes """
|
||||
_inherit = 'account.tax.template'
|
||||
|
||||
tax_discount = fields.Boolean(string='Discount this Tax in Prince',
|
||||
help="Mark it for (ICMS, PIS e etc.).")
|
||||
base_reduction = fields.Float(string='Redution', digits=0, required=True,
|
||||
help="Um percentual decimal em % entre 0-1.", default=0)
|
||||
amount_mva = fields.Float(string='MVA Percent', digits=0, required=True,
|
||||
help="Um percentual decimal em % entre 0-1.", default=0)
|
||||
|
||||
|
||||
class AccountTax(models.Model):
|
||||
""" Add fields used to define some brazilian taxes """
|
||||
_inherit = 'account.tax'
|
||||
|
||||
tax_discount = fields.Boolean(string='Discount this Tax in Prince',
|
||||
tax_discount = fields.Boolean(string='Discount this Tax in Price',
|
||||
help="Mark it for (ICMS, PIS e etc.).")
|
||||
base_reduction = fields.Float(string='Redution', digits=0, required=True,
|
||||
help="Um percentual decimal em % entre 0-1.", default=0)
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class AccountChartTemplate(models.Model):
|
||||
_inherit = 'account.chart.template'
|
||||
|
||||
def _get_fp_vals(self, company, position):
|
||||
res = super()._get_fp_vals(company, position)
|
||||
if company.country_id.code == 'BR':
|
||||
res['l10n_br_fp_type'] = position['l10n_br_fp_type']
|
||||
return res
|
||||
|
|
@ -26,17 +26,19 @@ class AccountFiscalPosition(models.Model):
|
|||
if not delivery:
|
||||
delivery = partner
|
||||
|
||||
if self.env.company.country_id.code != "BR" or delivery.country_id.code != 'BR':
|
||||
company = self.env.company
|
||||
|
||||
if company.country_id.code != "BR" or delivery.country_id.code != 'BR':
|
||||
return super()._get_fiscal_position(partner, delivery=delivery)
|
||||
|
||||
# manually set fiscal position on partner has a higher priority
|
||||
manual_fiscal_position = delivery.property_account_position_id or partner.property_account_position_id
|
||||
manual_fiscal_position = delivery.with_company(company).property_account_position_id or partner.with_company(company).property_account_position_id
|
||||
if manual_fiscal_position:
|
||||
return manual_fiscal_position
|
||||
|
||||
# Taxation in Brazil depends on both the state of the partner and the state of the company
|
||||
if self.env.company.state_id == delivery.state_id:
|
||||
return self.search([('l10n_br_fp_type', '=', 'internal'), ('company_id', '=', self.env.company.id)], limit=1)
|
||||
if self.env.company.state_id.code in SOUTH_SOUTHEAST and delivery.state_id.code in NORTH_NORTHEAST_MIDWEST:
|
||||
return self.search([('l10n_br_fp_type', '=', 'ss_nnm'), ('company_id', '=', self.env.company.id)], limit=1)
|
||||
return self.search([('l10n_br_fp_type', '=', 'interstate'), ('company_id', '=', self.env.company.id)], limit=1)
|
||||
if company.state_id == delivery.state_id:
|
||||
return self.search([('l10n_br_fp_type', '=', 'internal'), ('company_id', '=', company.id)], limit=1)
|
||||
if company.state_id.code in SOUTH_SOUTHEAST and delivery.state_id.code in NORTH_NORTHEAST_MIDWEST:
|
||||
return self.search([('l10n_br_fp_type', '=', 'ss_nnm'), ('company_id', '=', company.id)], limit=1)
|
||||
return self.search([('l10n_br_fp_type', '=', 'interstate'), ('company_id', '=', company.id)], limit=1)
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class AccountFiscalPositionTemplate(models.Model):
|
||||
_inherit = 'account.fiscal.position.template'
|
||||
|
||||
l10n_br_fp_type = fields.Selection(
|
||||
selection=[
|
||||
('internal', 'Internal'),
|
||||
('ss_nnm', 'South/Southeast selling to North/Northeast/Midwest'),
|
||||
('interstate', 'Other interstate'),
|
||||
],
|
||||
string='Interstate Fiscal Position Type',
|
||||
)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import fields, models, api
|
||||
|
||||
|
||||
class AccountJournal(models.Model):
|
||||
_inherit = 'account.journal'
|
||||
|
||||
l10n_br_invoice_serial = fields.Char(
|
||||
'Series', copy=False,
|
||||
help='Brazil: Series number associated with this Journal. If more than one Series needs to be used, duplicate this Journal and assign the new Series to the duplicated Journal.'
|
||||
)
|
||||
|
||||
@api.depends('l10n_br_invoice_serial')
|
||||
def _compute_display_name(self):
|
||||
res = super()._compute_display_name()
|
||||
for journal in self.filtered('l10n_br_invoice_serial'):
|
||||
journal.display_name = f'{journal.l10n_br_invoice_serial}-{journal.display_name}'
|
||||
|
||||
return res
|
||||
24
odoo-bringout-oca-ocb-l10n_br/l10n_br/models/account_move.py
Normal file
24
odoo-bringout-oca-ocb-l10n_br/l10n_br/models/account_move.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import models
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = "account.move"
|
||||
|
||||
def _compute_l10n_latam_document_type(self):
|
||||
""" Override for debit notes. This sets the same document type as the one on the origin. Cannot
|
||||
override the defaults in the account.move.debit wizard because l10n_latam_invoice_document explicitly
|
||||
calls _compute_l10n_latam_document_type() after the debit note is created. """
|
||||
br_debit_notes = self.filtered(lambda m: m.state == "draft" and m.country_code == "BR" and m.debit_origin_id.l10n_latam_document_type_id)
|
||||
for move in br_debit_notes:
|
||||
move.l10n_latam_document_type_id = move.debit_origin_id.l10n_latam_document_type_id
|
||||
|
||||
return super(AccountMove, self - br_debit_notes)._compute_l10n_latam_document_type()
|
||||
|
||||
def _get_last_sequence_domain(self, relaxed=False):
|
||||
""" Override to give sequence names in the same journal their own, independent numbering. """
|
||||
where_string, param = super()._get_last_sequence_domain(relaxed)
|
||||
if self.country_code == "BR" and self.l10n_latam_use_documents:
|
||||
where_string += " AND l10n_latam_document_type_id = %(l10n_latam_document_type_id)s "
|
||||
param["l10n_latam_document_type_id"] = self.l10n_latam_document_type_id.id or 0
|
||||
return where_string, param
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import re
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class L10n_BrZipRange(models.Model):
|
||||
_name = 'l10n_br.zip.range'
|
||||
_description = "Brazilian city zip range"
|
||||
|
||||
city_id = fields.Many2one("res.city", string="City", required=True)
|
||||
start = fields.Char(string="From", required=True)
|
||||
end = fields.Char(string="To", required=True)
|
||||
|
||||
_uniq_start = models.Constraint(
|
||||
'unique(start)',
|
||||
'The "from" zip must be unique',
|
||||
)
|
||||
_uniq_end = models.Constraint(
|
||||
'unique("end")',
|
||||
'The "to" zip must be unique.',
|
||||
)
|
||||
|
||||
@api.constrains("start", "end")
|
||||
def _check_range(self):
|
||||
zip_format = re.compile(r"\d{5}-\d{3}")
|
||||
for zip_range in self:
|
||||
if not zip_format.fullmatch(zip_range.start) or not zip_format.fullmatch(zip_range.end):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"Invalid zip range format: %(start)s %(end)s. It should follow this format: 01000-001",
|
||||
start=zip_range.start,
|
||||
end=zip_range.end,
|
||||
)
|
||||
)
|
||||
|
||||
if zip_range.start >= zip_range.end:
|
||||
raise ValidationError(
|
||||
_("Start should be less than end: %(start)s %(end)s", start=zip_range.start, end=zip_range.end)
|
||||
)
|
||||
26
odoo-bringout-oca-ocb-l10n_br/l10n_br/models/res_city.py
Normal file
26
odoo-bringout-oca-ocb-l10n_br/l10n_br/models/res_city.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class ResCity(models.Model):
|
||||
_inherit = "res.city"
|
||||
|
||||
l10n_br_zip_range_ids = fields.One2many(
|
||||
string="Zip Ranges",
|
||||
comodel_name="l10n_br.zip.range",
|
||||
inverse_name="city_id",
|
||||
help="Brazil: technical field that maps a city to one or more zip code ranges.",
|
||||
)
|
||||
|
||||
l10n_br_zip_ranges = fields.Char(
|
||||
string="Frontend Zip Ranges",
|
||||
compute="_compute_l10n_br_zip_ranges",
|
||||
help="Brazil: technical field that maps a city to one or more zip code ranges for the frontend.",
|
||||
)
|
||||
|
||||
@api.depends("l10n_br_zip_range_ids")
|
||||
def _compute_l10n_br_zip_ranges(self):
|
||||
for city in self:
|
||||
city.l10n_br_zip_ranges = " ".join(
|
||||
city.l10n_br_zip_range_ids.mapped(lambda zip_range: f"[{zip_range.start} {zip_range.end}]")
|
||||
)
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
|
@ -8,7 +7,13 @@ class ResCompany(models.Model):
|
|||
_inherit = "res.company"
|
||||
|
||||
# ==== Business fields ====
|
||||
l10n_br_cpf_code = fields.Char(string="CPF", help="Natural Persons Register.")
|
||||
l10n_br_ie_code = fields.Char(string="IE", help="State Tax Identification Number. Should contain 9-14 digits.") # each state has its own format. Not all of the validation rules can be easily found.
|
||||
l10n_br_im_code = fields.Char(string="IM", help="Municipal Tax Identification Number") # each municipality has its own format. There is no information about validation anywhere.
|
||||
l10n_br_ie_code = fields.Char(string="IE", related="partner_id.l10n_br_ie_code", readonly=False) # each state has its own format. Not all of the validation rules can be easily found.
|
||||
l10n_br_im_code = fields.Char(string="IM", related="partner_id.l10n_br_im_code", readonly=False) # each municipality has its own format. There is no information about validation anywhere.
|
||||
l10n_br_nire_code = fields.Char(string="NIRE", help="State Commercial Identification Number. Should contain 11 digits.")
|
||||
|
||||
def _localization_use_documents(self):
|
||||
self.ensure_one()
|
||||
return self.chart_template == 'br' or self.account_fiscal_country_id.code == "BR" or super()._localization_use_documents()
|
||||
|
||||
def _is_latam(self):
|
||||
return super()._is_latam() or self.account_fiscal_country_id.code == 'BR'
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
import re
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
l10n_br_cpf_code = fields.Char(string="CPF", help="Natural Persons Register.")
|
||||
l10n_br_ie_code = fields.Char(string="IE", help="State Tax Identification Number. Should contain 9-14 digits.")
|
||||
l10n_br_im_code = fields.Char(string="IM", help="Municipal Tax Identification Number")
|
||||
l10n_br_isuf_code = fields.Char(string="SUFRAMA code", help="SUFRAMA registration number.")
|
||||
|
||||
def _get_frontend_writable_fields(self):
|
||||
frontend_writable_fields = super()._get_frontend_writable_fields()
|
||||
frontend_writable_fields.update({'city_id', 'street_number', 'street_name', 'street_number2'})
|
||||
|
||||
return frontend_writable_fields
|
||||
|
|
|
|||
133
odoo-bringout-oca-ocb-l10n_br/l10n_br/models/res_partner_bank.py
Normal file
133
odoo-bringout-oca-ocb-l10n_br/l10n_br/models/res_partner_bank.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import re
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.addons.mail.tools.mail_validation import mail_validate
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import float_repr
|
||||
|
||||
|
||||
class ResPartnerBank(models.Model):
|
||||
_inherit = "res.partner.bank"
|
||||
|
||||
proxy_type = fields.Selection(
|
||||
selection_add=[
|
||||
("email", "Email Address"),
|
||||
("mobile", "Mobile Number"),
|
||||
("br_cpf_cnpj", "CPF/CNPJ (BR)"),
|
||||
("br_random", "Random Key (BR)"),
|
||||
],
|
||||
ondelete={
|
||||
"email": "set default",
|
||||
"mobile": "set default",
|
||||
"br_cpf_cnpj": "set default",
|
||||
"br_random": "set default",
|
||||
},
|
||||
)
|
||||
|
||||
@api.constrains("proxy_type", "proxy_value", "partner_id")
|
||||
def _check_br_proxy(self):
|
||||
for bank in self.filtered(lambda bank: bank.country_code == "BR" and bank.proxy_type != "none"):
|
||||
if bank.proxy_type not in ("email", "mobile", "br_cpf_cnpj", "br_random"):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"The proxy type must be Email Address, Mobile Number, CPF/CNPJ (BR) or Random Key (BR) for Pix code generation."
|
||||
)
|
||||
)
|
||||
|
||||
value = bank.proxy_value
|
||||
if bank.proxy_type == "email" and not mail_validate(value):
|
||||
raise ValidationError(_("%s is not a valid email.", value))
|
||||
|
||||
if bank.proxy_type == "br_cpf_cnpj" and (
|
||||
not self.partner_id.check_vat_br(value) or any(not char.isdecimal() for char in value)
|
||||
):
|
||||
raise ValidationError(_("%s is not a valid CPF or CNPJ (don't include periods or dashes).", value))
|
||||
|
||||
if bank.proxy_type == "mobile" and (not value or not value.startswith("+55") or len(value) != 14):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"The mobile number %s is invalid. It must start with +55, contain a 2 digit territory or state code followed by a 9 digit number.",
|
||||
value,
|
||||
)
|
||||
)
|
||||
|
||||
regex = r"%(char)s{8}-%(char)s{4}-%(char)s{4}-%(char)s{4}-%(char)s{12}" % {"char": "[a-fA-F0-9]"}
|
||||
if bank.proxy_type == "br_random" and not re.fullmatch(regex, bank.proxy_value):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"The random key %s is invalid, the format looks like this: 71d6c6e1-64ea-4a11-9560-a10870c40ca2",
|
||||
value,
|
||||
)
|
||||
)
|
||||
|
||||
@api.depends('country_code')
|
||||
def _compute_country_proxy_keys(self):
|
||||
bank_br = self.filtered(lambda b: b.country_code == 'BR')
|
||||
bank_br.country_proxy_keys = 'email,mobile,br_cpf_cnpj,br_random'
|
||||
super(ResPartnerBank, self - bank_br)._compute_country_proxy_keys()
|
||||
|
||||
@api.depends("country_code")
|
||||
def _compute_display_qr_setting(self):
|
||||
"""Override."""
|
||||
bank_br = self.filtered(lambda b: b.country_code == "BR")
|
||||
bank_br.display_qr_setting = True
|
||||
super(ResPartnerBank, self - bank_br)._compute_display_qr_setting()
|
||||
|
||||
def _get_additional_data_field(self, comment):
|
||||
"""Override."""
|
||||
if self.country_code == "BR":
|
||||
# Only include characters allowed by the Pix spec.
|
||||
return self._serialize(5, re.sub(r"[^a-zA-Z0-9*]", "", comment))
|
||||
return super()._get_additional_data_field(comment)
|
||||
|
||||
def _get_qr_code_vals_list(self, *args, **kwargs):
|
||||
"""Override. Force the amount field to always have two decimals. Uppercase the merchant name and merchant city.
|
||||
Although not specified explicitly in the spec, not uppercasing causes errors when scanning the code. Also ensure
|
||||
there is always some comment set."""
|
||||
res = super()._get_qr_code_vals_list(*args, **kwargs)
|
||||
if self.country_code == "BR":
|
||||
res[5] = (res[5][0], float_repr(res[5][1], 2) if res[5][1] else None) # amount
|
||||
res[7] = (res[7][0], re.sub(r"[^a-zA-Z0-9 ]", "", res[7][1]).upper()) # merchant_name
|
||||
res[8] = (res[8][0], res[8][1].upper()) # merchant_city
|
||||
if not res[9][1]:
|
||||
res[9] = (res[9][0], self._get_additional_data_field("***")) # default comment if none is set
|
||||
return res
|
||||
|
||||
def _get_merchant_account_info(self):
|
||||
"""Override."""
|
||||
if self.country_code == "BR":
|
||||
merchant_account_info_data = (
|
||||
(0, "br.gov.bcb.pix"), # GUI
|
||||
(1, self.proxy_value), # key
|
||||
)
|
||||
return 26, "".join(self._serialize(*val) for val in merchant_account_info_data)
|
||||
|
||||
return super()._get_merchant_account_info()
|
||||
|
||||
def _get_error_messages_for_qr(self, qr_method, debtor_partner, currency):
|
||||
"""Override."""
|
||||
if qr_method == "emv_qr" and self.country_code == "BR":
|
||||
if currency.name != "BRL":
|
||||
return _("Can't generate a Pix QR code with a currency other than BRL.")
|
||||
return None
|
||||
|
||||
return super()._get_error_messages_for_qr(qr_method, debtor_partner, currency)
|
||||
|
||||
def _check_for_qr_code_errors(
|
||||
self, qr_method, amount, currency, debtor_partner, free_communication, structured_communication
|
||||
):
|
||||
"""Override."""
|
||||
if (
|
||||
qr_method == "emv_qr"
|
||||
and self.country_code == "BR"
|
||||
and self.proxy_type not in ("email", "mobile", "br_cpf_cnpj", "br_random")
|
||||
):
|
||||
return _(
|
||||
"To generate a Pix code the proxy type for %s must be Email Address, Mobile Number, CPF/CNPJ (BR) or Random Key (BR).",
|
||||
self.display_name,
|
||||
)
|
||||
|
||||
return super()._check_for_qr_code_errors(
|
||||
qr_method, amount, currency, debtor_partner, free_communication, structured_communication
|
||||
)
|
||||
55
odoo-bringout-oca-ocb-l10n_br/l10n_br/models/template_br.py
Normal file
55
odoo-bringout-oca-ocb-l10n_br/l10n_br/models/template_br.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import models
|
||||
from odoo.addons.account.models.chart_template import template
|
||||
|
||||
|
||||
class AccountChartTemplate(models.AbstractModel):
|
||||
_inherit = 'account.chart.template'
|
||||
|
||||
@template('br')
|
||||
def _get_br_template_data(self):
|
||||
return {
|
||||
'code_digits': '6',
|
||||
'property_account_receivable_id': 'account_template_101010401',
|
||||
'property_account_payable_id': 'account_template_201010301',
|
||||
}
|
||||
|
||||
@template('br', 'res.company')
|
||||
def _get_br_res_company(self):
|
||||
return {
|
||||
self.env.company.id: {
|
||||
'account_fiscal_country_id': 'base.br',
|
||||
'bank_account_code_prefix': '1.01.01.02.00',
|
||||
'cash_account_code_prefix': '1.01.01.01.00',
|
||||
'transfer_account_code_prefix': '1.01.01.12.00',
|
||||
'account_default_pos_receivable_account_id': 'account_template_101010402',
|
||||
'income_currency_exchange_account_id': 'br_3_01_01_05_01_47',
|
||||
'expense_currency_exchange_account_id': 'br_3_11_01_09_01_40',
|
||||
'account_journal_early_pay_discount_loss_account_id': 'account_template_31101010202',
|
||||
'account_journal_early_pay_discount_gain_account_id': 'account_template_30101050148',
|
||||
'account_sale_tax_id': 'tax_template_out_icms_interno17',
|
||||
'account_purchase_tax_id': 'tax_template_in_icms_interno17',
|
||||
'expense_account_id': 'account_template_30101030101',
|
||||
'income_account_id': 'account_template_30101010105',
|
||||
'account_stock_journal_id': 'inventory_valuation',
|
||||
'account_stock_valuation_id': 'account_template_101030401',
|
||||
},
|
||||
}
|
||||
|
||||
@template('br', 'account.journal')
|
||||
def _get_br_account_journal(self):
|
||||
return {
|
||||
'sale': {
|
||||
'l10n_br_invoice_serial': '1',
|
||||
'refund_sequence': False,
|
||||
},
|
||||
}
|
||||
|
||||
@template('br', 'account.account')
|
||||
def _get_br_account_account(self):
|
||||
return {
|
||||
'account_template_101030401': {
|
||||
'account_stock_expense_id': 'account_template_30101030102',
|
||||
'account_stock_variation_id': 'account_template_101030405',
|
||||
},
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue