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 @@ -20,6 +20,7 @@
)

from ..constants.nfse import (
ISSQN_TO_TRIBUTACAO_ISS,
NFSE_ENVIRONMENTS,
OPERATION_NATURE,
RPS_TYPE,
Expand Down Expand Up @@ -167,6 +168,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_service())
Expand Down Expand Up @@ -195,6 +198,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 @@ -219,10 +231,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 @@ -246,6 +262,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_service())
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 @@ -92,7 +92,8 @@ def _prepare_line_service(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 @@ -109,4 +110,15 @@ def _prepare_line_service(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"
),
}
67 changes: 53 additions & 14 deletions l10n_br_nfse_focus/README.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

================
NFS-e (FocusNFE)
================
Expand All @@ -17,7 +13,7 @@ NFS-e (FocusNFE)
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fl10n--brazil-lightgray.png?logo=github
Expand All @@ -32,10 +28,16 @@ NFS-e (FocusNFE)

|badge1| |badge2| |badge3| |badge4| |badge5|

Esse módulo integra a emissão de Notas Fiscais de Serviços(NFSe) com a
Esse módulo integra a emissão de Notas Fiscais de Serviços (NFSe) com a
API da FocusNFE permitindo assim, a criação, transmissão, consulta e
cancelamento de documentos fiscais do tipo NFSe.

O módulo suporta dois tipos de emissão:

- **NFSe Municipal:** Emissão de NFSe através da API municipal da
FocusNFE
- **NFSe Nacional:** Emissão de NFSe através da API nacional da FocusNFE

Para mais informações, acesse: https://focusnfe.com.br/

**Table of contents**
Expand Down Expand Up @@ -68,22 +70,59 @@ para a empresa desejada:
- **Provedor NFS-e:** Selecione a opção FocusNFE
- **FocusNFe Token:** Informe o token de acesso da empresa. Obs.
Este token é obtido através da plataforma da FocusNFE

- **Token de Produção:** Token para ambiente de produção
(visível quando Ambiente NFS-e = Produção)
- **Token de Homologação:** Token para ambiente de homologação
(visível quando Ambiente NFS-e = Homologação)

- **Tipo FocusNFe NFSe:** Selecione o tipo de API a ser utilizada:

- **NFSe:** Para emissão de NFSe Municipal (padrão)
- **NFSe Nacional:** Para emissão de NFSe Nacional

- **Valor Tipo de Serviço:** Se necessário configure o campo que
deve preencher o valor de tipo de serviço
deve preencher o valor de tipo de serviço (Service Type ou City
Taxation Code)
- **Valor Código CNAE:** Se necessário configure o campo que deve
preencher o valor do Código CNAE
preencher o valor do Código CNAE (CNAE Code ou City Taxation
Code)
- **Formato Taxa:** Selecione o formato da taxa (Decimal ou
Percentage)
- **Incluir Documentos Autorizados na Verificação de Status:** Se
marcado, documentos autorizados serão incluídos na verificação
de status
- **Forçar DANFSE Odoo:** Se marcado, o sistema sempre usará o
DANFSE do Odoo ao invés do DANFSE da FocusNFE

Usage
=====

Para usar este módulo:

1. Crie uma fatura com o tipo de documento fiscal 'SE'.
2. Preencha os detalhes necessários, como o código tributário da cidade,
impostos e informações correlatas.
3. Valide o documento.
4. Envie o Documento Fiscal.
5. Acompanhe o status de processamento do documento.
1. Configure a empresa conforme descrito na seção de Configuração.

2. Crie uma fatura com o tipo de documento fiscal 'SE'.

3. Preencha os detalhes necessários:

- Para **NFSe Municipal:** Preencha o código tributário municipal,
impostos e informações correlatas
- Para **NFSe Nacional:** Preencha o código tributário nacional
(NBS), código tributário municipal (se aplicável), impostos e
informações correlatas

4. Valide o documento fiscal.

5. Envie o Documento Fiscal através do botão "Enviar Documento Fiscal".

6. Acompanhe o status de processamento do documento. O sistema
verificará automaticamente o status através de um cron job, ou você
pode verificar manualmente através do botão "Verificar Status".

7. Após a autorização, o DANFSE (Documento Auxiliar da Nota Fiscal de
Serviço Eletrônica) será gerado automaticamente, a menos que a opção
"Forçar DANFSE Odoo" esteja marcada na configuração da empresa.

Bug Tracker
===========
Expand Down
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