19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:16 +01:00
parent 89c6e82fe7
commit 1b82c20a58
572 changed files with 43570 additions and 53303 deletions

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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',
)

View file

@ -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

View 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

View file

@ -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)
)

View 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}]")
)

View file

@ -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'

View file

@ -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

View 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
)

View 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',
},
}