Skip to content
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
11 changes: 11 additions & 0 deletions l10n_br_nfse/constants/nfse.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,14 @@
("5", "Microempresario Individual(MEI)"),
("6", "Microempresario e Empresa de Pequeno Porte(ME EPP)"),
]


ISSQN_TO_TRIBUTACAO_ISS = {
"1": "1", # Exigível → Operação tributável
"2": "4", # Não incidência → Não Incidência
"3": "4", # Isenção → Não Incidência
"4": "3", # Exportação → Exportação de serviço
"5": "2", # Imunidade → Imunidade
"6": "1", # Suspensa (Judicial) → Operação tributável
"7": "1", # Suspensa (Administrativo) → Operação tributável
}
28 changes: 27 additions & 1 deletion l10n_br_nfse/models/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
)

from ..constants.nfse import (
ISSQN_TO_TRIBUTACAO_ISS,
NFSE_ENVIRONMENTS,
OPERATION_NATURE,
RPS_TYPE,
Expand Down Expand Up @@ -171,6 +172,8 @@ def _prepare_dados_servico(self):
cbs_aliquota = 0
ibs_uf_valor = 0
cbs_valor = 0
base_calculo_pis = 0
base_calculo_cofins = 0

for line in lines:
result_line.update(line.prepare_line_servico())
Expand Down Expand Up @@ -199,6 +202,15 @@ def _prepare_dados_servico(self):
cbs_aliquota += result_line.get("cbs_aliquota") or 0
ibs_uf_valor += result_line.get("ibs_uf_valor") or 0
cbs_valor += result_line.get("cbs_valor") or 0
situacao_tributaria_pis = result_line.get("situacao_tributaria_pis")
situacao_tributaria_cofins = result_line.get("situacao_tributaria_cofins")
base_calculo_pis += result_line.get("base_calculo_pis", 0)
base_calculo_cofins += result_line.get("base_calculo_cofins", 0)
aliquota_pis = result_line.get("aliquota_pis") or 0
aliquota_cofins = result_line.get("aliquota_cofins") or 0
tipo_retencao_pis_cofins = (
result_line.get("tipo_retencao_pis_cofins") or "2"
)

result = {
"valor_servicos": valor_servicos,
Expand All @@ -223,10 +235,14 @@ def _prepare_dados_servico(self):
"valor_liquido_nfse": valor_liquido_nfse,
"item_lista_servico": self.fiscal_line_ids[0].service_type_id.code
and self.fiscal_line_ids[0].service_type_id.code.replace(".", ""),
"codigo_tributacao_nacional": self.fiscal_line_ids[
0
].national_taxation_code_id.code
or None,
"codigo_tributacao_municipio": self.fiscal_line_ids[
0
].city_taxation_code_id.code
or "",
or None,
"municipio_prestacao_servico": self.fiscal_line_ids[
0
].issqn_fg_city_id.ibge_code
Expand All @@ -251,6 +267,16 @@ def _prepare_dados_servico(self):
"ibs_uf_valor": ibs_uf_valor if ibs_uf_valor else None,
"ibs_mun_valor": 0.0,
"cbs_valor": cbs_valor if cbs_valor else None,
"situacao_tributaria_pis": situacao_tributaria_pis,
"situacao_tributaria_cofins": situacao_tributaria_cofins,
"base_calculo_pis": round(base_calculo_pis, 2),
"base_calculo_cofins": round(base_calculo_cofins, 2),
"aliquota_pis": round(aliquota_pis, 2),
"aliquota_cofins": round(aliquota_cofins, 2),
"tipo_retencao_pis_cofins": tipo_retencao_pis_cofins,
"codigo_tributacao_iss": ISSQN_TO_TRIBUTACAO_ISS[
self.fiscal_line_ids[0].issqn_eligibility
],
}

result.update(self.company_id.prepare_company_servico())
Expand Down
14 changes: 13 additions & 1 deletion l10n_br_nfse/models/document_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ def prepare_line_servico(self):
"valor_liquido_nfse": round(self.amount_taxed, 2),
"item_lista_servico": self.service_type_id.code
and self.service_type_id.code.replace(".", ""),
"codigo_tributacao_municipio": self.city_taxation_code_id.code or "",
"codigo_tributacao_nacional": self.national_taxation_code_id.code or None,
"codigo_tributacao_municipio": self.city_taxation_code_id.code or None,
"municipio_prestacao_servico": self.issqn_fg_city_id.ibge_code or "",
"discriminacao": str(self.name[:2000] or ""),
"codigo_cnae": misc.punctuation_rm(self.cnae_id.code) or None,
Expand All @@ -108,4 +109,15 @@ def prepare_line_servico(self):
"ibs_uf_valor": round(self.ibs_value, 2) if self.ibs_value else None,
"ibs_mun_valor": 0.0,
"cbs_valor": round(self.cbs_value, 2) if self.cbs_value else None,
"situacao_tributaria_pis": self.pis_cst_code or "",
"situacao_tributaria_cofins": self.cofins_cst_code or "",
"base_calculo_pis": round(self.pis_base, 2),
"base_calculo_cofins": round(self.cofins_base, 2),
"aliquota_pis": round(self.pis_percent, 2) if self.pis_percent else 0.0,
"aliquota_cofins": (
round(self.cofins_percent, 2) if self.cofins_percent else 0.0
),
"tipo_retencao_pis_cofins": (
"1" if (self.pis_wh_value or self.cofins_wh_value) else "2"
),
}
5 changes: 5 additions & 0 deletions l10n_br_nfse_focus/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
from . import base
from . import constants
from . import document
from . import helpers
from . import nfse_municipal
from . import nfse_nacional
from . import res_company
60 changes: 60 additions & 0 deletions l10n_br_nfse_focus/models/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Copyright 2023 - TODAY, Marcel Savegnago <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

"""Base class for FocusNFE NFSe operations."""

import requests

from odoo import _, models
from odoo.exceptions import UserError


class FocusnfeNfseBase(models.AbstractModel):
"""Base class for FocusNFE NFSe operations with shared HTTP request logic."""

_name = "focusnfe.nfse.base"
_description = "FocusNFE NFSE Base"

def _make_focus_nfse_http_request(
self, method, url, token, data=None, params=None, service_name="NFSe"
):
"""Perform a generic HTTP request.

Args:
method (str): The HTTP method to use (e.g., 'GET', 'POST').
url (str): The URL to which the request is sent.
token (str): The authentication token for the service.
data (dict, optional): The payload to send in the request body.
Defaults to None.
params (dict, optional): The URL parameters to append to the URL.
Defaults to None.
service_name (str): Name of the service for error messages.

Returns:
requests.Response: The response object from the requests library.

Raises:
UserError: If the HTTP request fails with a 4xx/5xx response.
"""
auth = (token, "")
try:
response = requests.request( # pylint: disable=external-request-timeout
method,
url,
data=data,
params=params,
auth=auth,
)
if response.status_code == 422:
payload = response.json()
msg = payload.get("mensagem") or ""
raise UserError(
f"Error communicating with {service_name} service: {msg}"
)
response.raise_for_status() # Raises an error for 4xx/5xx responses
return response
except requests.HTTPError as e:
raise UserError(
_("Error communicating with %(service)s service: %(error)s")
% {"service": service_name, "error": e}
) from e
41 changes: 41 additions & 0 deletions l10n_br_nfse_focus/models/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright 2023 - TODAY, Marcel Savegnago <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

"""Constants for FocusNFE NFSe integration."""

NFSE_URL = {
"1": "https://api.focusnfe.com.br",
"2": "https://homologacao.focusnfe.com.br",
}

API_ENDPOINT = {
"envio": "/v2/nfse?",
"status": "/v2/nfse/",
"resposta": "/v2/nfse/",
"cancelamento": "/v2/nfse/",
}

API_ENDPOINT_NACIONAL = {
"envio": "/v2/nfsen",
"status": "/v2/nfsen/",
"resposta": "/v2/nfsen/",
"cancelamento": "/v2/nfsen/",
}

TIMEOUT = 60 # 60 seconds

# Constants for document status
STATUS_AUTORIZADO = "autorizado"
STATUS_CANCELADO = "cancelado"
STATUS_ERRO_AUTORIZACAO = "erro_autorizacao"
STATUS_PROCESSANDO_AUTORIZACAO = "processando_autorizacao"
CODE_NFE_CANCELADA = "nfe_cancelada"
CODE_NFE_AUTORIZADA = "nfe_autorizada"

# CPF/CNPJ length constants
CPF_LENGTH = 11
CNPJ_LENGTH = 14

# PDF validation constants
PDF_HEADER = b"%PDF-"
PDF_FOOTER = b"%%EOF"
Loading