diff --git a/l10n_br_fiscal/constants/fiscal.py b/l10n_br_fiscal/constants/fiscal.py index 1eb68837b722..f8b6e4dab8ea 100644 --- a/l10n_br_fiscal/constants/fiscal.py +++ b/l10n_br_fiscal/constants/fiscal.py @@ -487,6 +487,7 @@ (SITUACAO_EDOC_REJEITADA, SITUACAO_EDOC_AUTORIZADA), (SITUACAO_EDOC_REJEITADA, SITUACAO_EDOC_EM_DIGITACAO), (SITUACAO_EDOC_REJEITADA, SITUACAO_EDOC_REJEITADA), + (SITUACAO_EDOC_AUTORIZADA, SITUACAO_EDOC_ENVIADA), ] EDOC_PURPOSE = [ diff --git a/l10n_br_nfse_barueri/README.rst b/l10n_br_nfse_barueri/README.rst index 4868545ec7a5..f9c8c7aa4e19 100644 --- a/l10n_br_nfse_barueri/README.rst +++ b/l10n_br_nfse_barueri/README.rst @@ -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 (Barueri) =============== @@ -17,7 +13,7 @@ NFS-e (Barueri) .. |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 diff --git a/l10n_br_nfse_barueri/__manifest__.py b/l10n_br_nfse_barueri/__manifest__.py index 183cd1a16227..374f92929b40 100644 --- a/l10n_br_nfse_barueri/__manifest__.py +++ b/l10n_br_nfse_barueri/__manifest__.py @@ -20,6 +20,10 @@ "nfselib.barueri", ], }, + "data": [ + "views/document_view.xml", + "data/l10n_br_nfse_barueri_cron.xml", + ], "depends": [ "l10n_br_nfse", ], diff --git a/l10n_br_nfse_barueri/constants/barueri.py b/l10n_br_nfse_barueri/constants/barueri.py index 3467251122e7..261b48b21810 100644 --- a/l10n_br_nfse_barueri/constants/barueri.py +++ b/l10n_br_nfse_barueri/constants/barueri.py @@ -2,4 +2,4 @@ CONSULTAR_SITUACAO_LOTE_RPS = ["NFeLoteStatusArquivo"] -CONSULTAR_NFSE_POR_RPS = ["NFeLoteListarArquivos"] +CONSULTAR_NFSE_POR_RPS = ["NFeLoteBaixarArquivo"] diff --git a/l10n_br_nfse_barueri/data/l10n_br_nfse_barueri_cron.xml b/l10n_br_nfse_barueri/data/l10n_br_nfse_barueri_cron.xml new file mode 100644 index 000000000000..85f23e626d81 --- /dev/null +++ b/l10n_br_nfse_barueri/data/l10n_br_nfse_barueri_cron.xml @@ -0,0 +1,18 @@ + + + + + NFSe Barueri: Check status and update status of submitted documents. + + code + model._cron_document_status_barueri() + + 15 + minutes + -1 + + + diff --git a/l10n_br_nfse_barueri/models/__init__.py b/l10n_br_nfse_barueri/models/__init__.py index 166dd3bd2493..2cb6d534efb5 100644 --- a/l10n_br_nfse_barueri/models/__init__.py +++ b/l10n_br_nfse_barueri/models/__init__.py @@ -1,2 +1,3 @@ from . import document from . import res_company +from . import account_move diff --git a/l10n_br_nfse_barueri/models/account_move.py b/l10n_br_nfse_barueri/models/account_move.py new file mode 100644 index 000000000000..33b3d10fc2ae --- /dev/null +++ b/l10n_br_nfse_barueri/models/account_move.py @@ -0,0 +1,12 @@ +# Copyright 2025 - TODAY, Cristiano Mafra Junior +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class AccountMove(models.Model): + _inherit = "account.move" + + def action_open_nfse_barueri(self): + self.ensure_one() + return self.fiscal_document_id.action_open_nfse_barueri() diff --git a/l10n_br_nfse_barueri/models/document.py b/l10n_br_nfse_barueri/models/document.py index 321ab7f5820f..a92d06a9e4ea 100644 --- a/l10n_br_nfse_barueri/models/document.py +++ b/l10n_br_nfse_barueri/models/document.py @@ -1,21 +1,47 @@ # Copyright 2023 - KMEE INFORMATICA LTDA +# Copyright 2025 - TODAY, Cristiano Mafra Junior # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -import base64 +import unicodedata +from datetime import datetime +import requests from nfselib.barueri.NFeLoteEnviarArquivo import NFeLoteEnviarArquivo -from nfselib.barueri.rps import RPS, RegistroTipo2 +from nfselib.barueri.nfse import ( + NFSeRegistroTipo1, + NFSeRegistroTipo2, + NFSeRegistroTipo3, + NFSeRegistroTipo4, + NFSeRegistroTipo9, +) +from nfselib.barueri.rps import ( + RPS, + RegistroTipo1, + RegistroTipo2, + RegistroTipo3, + RegistroTipo4, + RegistroTipo5, + RegistroTipo9, +) -from odoo import _, models +from odoo import _, api, fields, models from odoo.addons.l10n_br_fiscal.constants.fiscal import ( + EVENT_ENV_HML, + EVENT_ENV_PROD, MODELO_FISCAL_NFSE, PROCESSADOR_OCA, SITUACAO_EDOC_AUTORIZADA, + SITUACAO_EDOC_CANCELADA, + SITUACAO_EDOC_ENVIADA, SITUACAO_EDOC_REJEITADA, ) -from ..constants.barueri import CONSULTAR_NFSE_POR_RPS, CONSULTAR_SITUACAO_LOTE_RPS +from ..constants.barueri import ( + CONSULTAR_NFSE_POR_RPS, + CONSULTAR_SITUACAO_LOTE_RPS, + ENVIO_LOTE_RPS, +) def filter_oca_nfse(record): @@ -32,9 +58,84 @@ def filter_barueri(record): return False +def parse_linha_exporta(line: str): + tipo = line[0] + if tipo == "1": + reg = NFSeRegistroTipo1.from_line(line) + elif tipo == "2": + reg = NFSeRegistroTipo2.from_line(line) + elif tipo == "3": + reg = NFSeRegistroTipo3.from_line(line) + elif tipo == "4": + reg = NFSeRegistroTipo4.from_line(line) + elif tipo == "5": + reg = NFSeRegistroTipo4.from_line(line) + elif tipo == "9": + reg = NFSeRegistroTipo9.from_line(line) + else: + raise ValueError(f"Tipo de registro desconhecido: {tipo}") + + return reg + + class Document(models.Model): _inherit = "l10n_br_fiscal.document" + url_nfse_barueri = fields.Char( + string="URL of NFSe Barueri", + compute="_compute_url_nfse_barueri", + help="URL to access the Nota Fiscal de Serviços Eletrônicos (NFSe)" + "from the São Paulo City (Barueri).", + ) + is_nfse_barueri = fields.Boolean( + string="Is NFSe Barueri?", + compute="_compute_is_nfse_barueri", + help="Technical field to identify if the document is a NFSe Barueri.", + ) + + def _compute_url_nfse_barueri(self): + for doc in self: + if not doc.is_nfse_barueri: + doc.url_nfse_barueri = "" + continue + + autenticidade = (doc.verify_code or "").strip() + cnpj_tomador = "".join( + ch for ch in (doc.partner_id.cnpj_cpf or "") if ch.isdigit() + ) + if not (autenticidade and cnpj_tomador): + doc.url_nfse_barueri = "" + continue + if doc.nfse_environment == "1": + base_url = "https://www.barueri.sp.gov.br/nfe/wfimagemNota.aspx" + else: + base_url = "https://testeeiss.barueri.sp.gov.br/nfe/wfimagemNota.aspx" + + doc.url_nfse_barueri = ( + f"{base_url}" + f"?CODIGOAUTENTICIDADE={autenticidade}" + f"&NUMDOC={cnpj_tomador}" + ) + + def action_open_nfse_barueri(self): + return { + "type": "ir.actions.act_url", + "url": self.url_nfse_barueri, + "target": "new", + } + + def _compute_is_nfse_barueri(self): + for doc in self: + is_nfse = doc.document_type == "SE" + is_barueri = doc.company_id.city_id == self.env.ref( + "l10n_br_base.city_3505708" + ) + + if is_nfse and is_barueri: + doc.is_nfse_barueri = True + else: + doc.is_nfse_barueri = False + def _serialize(self, edocs): edocs = super()._serialize(edocs) for record in self.filtered(filter_oca_nfse).filtered(filter_barueri): @@ -50,33 +151,237 @@ def _serialize_barueri_dados_tomador(self): dados = self._prepare_dados_tomador() return dados - def _serialize_barueri_lote_rps(self): + def _sem_acento(self, value): + return ( + unicodedata.normalize("NFKD", value or "") + .encode("ASCII", "ignore") + .decode("ASCII") + ) + + def _serialize_barueri_lote_rps(self, cancel=False): dados = self._prepare_lote_rps() dados_servico = self._serialize_barueri_dados_servico() dados_tomador = self._serialize_barueri_dados_tomador() + # Registro tipo 1 - Cabeçalho do arquivo RPS + registro_tipo1 = RegistroTipo1() + registro_tipo1.TipoRegistro = 1 + registro_tipo1.InscricaoContribuinte = self.company_inscr_mun + registro_tipo1.VersaoLayout = "PMB004" + if cancel: + data_base = datetime.now() + data_emissao = data_base.strftime("%Y-%m-%d") + else: + data_emissao = dados["data_emissao"].split("T")[0] + ano_mes_dia = data_emissao.replace("-", "") + sequencial = datetime.now().strftime("%f")[-3:] + registro_tipo1.IdentificacaoRemessaContribuinte = f"{ano_mes_dia}{sequencial}" + + # Registro tipo 2 - Dados do RPS registro_tipo2 = RegistroTipo2() - registro_tipo2.SerieNFe = dados["serie"] - registro_tipo2.DataRPS = dados["data_emissao"].split("T")[0].replace("-", "") - registro_tipo2.HoraRPS = dados["data_emissao"].split("T")[1].replace(":", "") - registro_tipo2.SituacaoRPS = "E" - registro_tipo2.CodigoServicoPrestado = dados_servico["codigo_cnae"] - registro_tipo2.QuantidadeServico = 1 - registro_tipo2.ValorServico = dados_servico["valor_servicos"] - registro_tipo2.ValorTotalRetencoes = self.amount_tax_withholding - registro_tipo2.TomadorEstrangeiro = 2 - registro_tipo2.IndicadorCPFCNPJTomador = 2 - registro_tipo2.CPFCNPJTomador = dados_tomador["cnpj"] - registro_tipo2.RazaoSocialNomeTomador = dados_tomador["razao_social"] - registro_tipo2.EnderecoLogradouroTomador = dados_tomador["endereco"] - registro_tipo2.NumeroLogradouroTomador = dados_tomador["numero"] - registro_tipo2.ComplementoLogradouroTomador = dados_tomador["complemento"] - registro_tipo2.BairroLogradouroTomador = dados_tomador["bairro"] - registro_tipo2.CidadeLogradouroTomador = dados_tomador["descricao_municipio"] - registro_tipo2.UFLogradouroTomador = dados_tomador["uf"] - registro_tipo2.CEPLogradouroTomador = dados_tomador["cep"] - registro_tipo2.EmailTomador = dados_tomador["email"] - registro_tipo2.DiscriminacaoServico = dados_servico["discriminacao"] - rps = RPS([registro_tipo2]).exportar() + registro_tipo2.TipoRegistro = 2 + registro_tipo2.TipoRPS = "RPS" + if cancel: + registro_tipo2.SituacaoRPS = "C" + registro_tipo2.NumeroRPS = "0" * 10 + now = datetime.now() + registro_tipo2.DataRPS = now.strftime("%Y%m%d") + registro_tipo2.HoraRPS = now.strftime("%H%M%S") + registro_tipo2.CodigoMotivoCancelamento = "01" + registro_tipo2.NumeroNFeCancelada = str(int(self.document_number)).zfill(7) + registro_tipo2.SerieNFeCancelada = "" + registro_tipo2.DataEmissaoNFeCancelada = self.authorization_date.strftime( + "%Y%m%d" + ) + descricao = (self.cancel_reason or "").strip() + descricao = self._sem_acento(descricao)[:180] + registro_tipo2.DescricaoCancelamento = descricao + else: + numero_rps = str(self.rps_number or "1").zfill(7) + registro_tipo2.NumeroRPS = f"000{numero_rps}" + registro_tipo2.DataRPS = ( + dados["data_emissao"].split("T")[0].replace("-", "") + ) + registro_tipo2.HoraRPS = ( + dados["data_emissao"].split("T")[1].replace(":", "") + ) + registro_tipo2.SituacaoRPS = "E" + registro_tipo2.CodigoMotivoCancelamento = "" + registro_tipo2.NumeroNFeCancelada = "" + registro_tipo2.SerieNFeCancelada = "" + registro_tipo2.DataEmissaoNFeCancelada = "" + registro_tipo2.DescricaoCancelamento = "" + registro_tipo2.CodigoServicoPrestado = dados_servico[ + "codigo_tributacao_municipio" + ] + registro_tipo2.LocalPrestacaoServico = ( + "1" + ) # String: 1=Município, 2=Fora do Município + registro_tipo2.ServicoPrestadoViasPublicas = "2" # String: 1=Sim, 2=Não + registro_tipo2.EnderecoLogradouroLocalServico = "" + registro_tipo2.NumeroLogradouroLocalServico = "" + registro_tipo2.ComplementoLogradouroLocalServico = "" + registro_tipo2.BairroLogradouroLocalServico = "" + registro_tipo2.CidadeLogradouroLocalServico = "" + registro_tipo2.UFLogradouroLocalServico = "" + registro_tipo2.CEPLogradouroLocalServico = "" + fiscal_line = self.fiscal_line_ids[0] if self.fiscal_line_ids else None + quantidade = int(fiscal_line.quantity or 1) if fiscal_line else 1 + valor_servicos_total = dados_servico.get("valor_servicos", 0) or 0 + valor_unitario = ( + valor_servicos_total / quantidade + if quantidade > 0 + else valor_servicos_total + ) + valor_unitario_centavos = int(round(float(valor_unitario) * 100)) + registro_tipo2.QuantidadeServico = str(quantidade).zfill(6) + registro_tipo2.ValorServico = str(valor_unitario_centavos).zfill(15) + valor_retencoes_total = ( + (dados_servico.get("valor_ir_retido", 0) or 0) + + (dados_servico.get("valor_pis_retido", 0) or 0) + + (dados_servico.get("valor_cofins_retido", 0) or 0) + + (dados_servico.get("valor_csll_retido", 0) or 0) + ) + valor_retencoes_centavos = int(round(float(valor_retencoes_total) * 100)) + registro_tipo2.ValorTotalRetencoes = str(valor_retencoes_centavos).zfill(15) + registro_tipo2.TomadorEstrangeiro = "2" # String: 1=Estrangeiro, 2=Brasileiro + registro_tipo2.ServicoExportacao = "2" # String: 1=Exportado, 2=Não exportado + cnpj_cpf = dados_tomador.get("cnpj") or dados_tomador.get("cpf", "") + if cnpj_cpf: + if len(cnpj_cpf) == 14 and cnpj_cpf.isdigit(): + registro_tipo2.IndicadorCPFCNPJTomador = "2" + registro_tipo2.CPFCNPJTomador = cnpj_cpf.zfill(14) + elif len(cnpj_cpf) == 11 and cnpj_cpf.isdigit(): + registro_tipo2.IndicadorCPFCNPJTomador = "1" + registro_tipo2.CPFCNPJTomador = cnpj_cpf.zfill(14) + else: + registro_tipo2.IndicadorCPFCNPJTomador = "1" + registro_tipo2.CPFCNPJTomador = "0" * 14 + registro_tipo2.RazaoSocialNomeTomador = self._sem_acento( + dados_tomador.get("razao_social", "") + ) + registro_tipo2.EnderecoLogradouroTomador = self._sem_acento( + dados_tomador.get("endereco", "") or "" + ) + registro_tipo2.NumeroLogradouroTomador = str( + dados_tomador.get("numero", "") or "10" + ) + registro_tipo2.ComplementoLogradouroTomador = self._sem_acento( + dados_tomador.get("complemento", "") or "N/A" + ) + registro_tipo2.BairroLogradouroTomador = self._sem_acento( + dados_tomador.get("bairro", "Bairro N/A") + ) + + registro_tipo2.CidadeLogradouroTomador = self._sem_acento( + dados_tomador.get("municipio") + ) + + registro_tipo2.UFLogradouroTomador = dados_tomador.get("uf", "") + cep = ( + str(dados_tomador.get("cep", "") or "") + .replace("-", "") + .replace(".", "") + .replace(" ", "") + ) + registro_tipo2.CEPLogradouroTomador = cep.zfill(8) if cep else "" + registro_tipo2.EmailTomador = dados_tomador.get("email", "tomador@email.com") + registro_tipo2.DiscriminacaoServico = self._sem_acento( + dados_servico.get("discriminacao", "") + ) + # Registro tipo 3 - Valores do serviço (retenções) + registros_tipo3 = [] + retencoes = [ + ("01", "valor_ir_retido"), + ("02", "valor_pis_retido"), + ("03", "valor_cofins_retido"), + ("04", "valor_csll_retido"), + ] + for codigo, campo in retencoes: + valor = dados_servico.get(campo, 0) or 0 + if valor: + reg = RegistroTipo3() + reg.TipoRegistro = 3 + reg.CodigoOutrosValores = codigo + reg.Valor = str(int(round(float(valor) * 100))).zfill(15) + registros_tipo3.append(reg) + + registro_tipo4 = RegistroTipo4() + registro_tipo4.TipoRegistro = 4 + registro_tipo4.OptanteSimplesNacional = ( + 1 + ) # String: 1=Não optante, 2=MEI, 3=ME/EPP + registro_tipo4.RegimeApuracaoSN = "" + registro_tipo4.CodigoCidadeLocalPrestacao = str( + self.company_id.city_id.ibge_code or "" + ).zfill(7) + registro_tipo4.CodigoCidadeTomador = str( + self.partner_id.city_id.ibge_code or "" + ).zfill(7) + registro_tipo4.CodigoNBS = "".join( + c for c in str(dados_servico.get("codigo_nbs", "")) if c.isdigit() + ) + registro_tipo4.CodigoIndicadorOperacaoFornecimento = ( + dados_servico.get("codigo_indicador_operacao") or "" + ) + + registro_tipo4.CodigoClassificacaoTributariaIBSCBS = ( + dados_servico.get("codigo_classificacao_tributaria") or "" + ) + + registro_tipo4.CodigoSituacaoTributariaIBSCBS = ( + dados_servico.get("codigo_situacao_tributaria") or "" + ) + registro_tipo4.OperacaoUsoConsumoPessoal = "0" + registro_tipo4.IndicadorDestinatarioServico = "0" + + # Registro tipo 5 - Dados complementares do Ambiente de Dados Nacional + registro_tipo5 = RegistroTipo5() + registro_tipo5.TipoRegistro = 5 + registro_tipo5.CodigoClassificacaoCreditoPresumidoIBSCBS = "" + registro_tipo5.TipoEnteGovernamental = "" + registro_tipo5.TipoOperacaoEntesGovernamentais = "1" + registro_tipo5.ChaveNFSeReferenciada = "" + registro_tipo5.CodigoNCMBemMovelLocacao = "" + registro_tipo5.DescricaoBemMovelLocacao = "" + registro_tipo5.QuantidadeBemMovelLocacao = "" + registro_tipo5.IndicadorOperacaoDoacao = "" + registro_tipo5.DestinatarioServicoEstrangeiro = "" + registro_tipo5.CPFCNPJDestinatarioServico = "" + registro_tipo5.RazaoSocialNomeDestinatarioServico = "" + registro_tipo5.EnderecoLogradouroDestinatarioServico = "" + registro_tipo5.NumeroLogradouroDestinatarioServico = "" + registro_tipo5.ComplementoLogradouroDestinatarioServico = "" + registro_tipo5.BairroLogradouroDestinatarioServico = "" + registro_tipo5.CidadeLogradouroDestinatarioServico = "" + registro_tipo5.CodigoCidadeDestinatarioServico = "" + registro_tipo5.UFLogradouroDestinatarioServico = "" + registro_tipo5.CodigoPaisDestinatarioServico = "" + registro_tipo5.CEPLogradouroDestinatarioServico = "" + registro_tipo5.EmailDestinatarioServico = "" + registro_tipo5.NIFDestinatario = "" + registro_tipo5.CodigoEnderecoPostalDestinatarioEstrangeiro = "" + registro_tipo5.EstadoProvinciaRegiaoDestinatarioEstrangeiro = "" + # Registro tipo 9 - Rodapé do arquivo RPS + registros_dados = [registro_tipo1, registro_tipo2] + if registros_tipo3: + registros_dados.extend(registros_tipo3) + registros_dados.append(registro_tipo4) + + registros_dados.append(registro_tipo5) + + numero_total_linhas = len(registros_dados) + 1 + quantidade_total = int(registro_tipo2.QuantidadeServico) + valor_unitario_centavos = int(registro_tipo2.ValorServico) + valor_total_servicos_centavos = quantidade_total * valor_unitario_centavos + valor_total_registro3 = sum(int(r.Valor) for r in registros_tipo3) + registro_tipo9 = RegistroTipo9() + registro_tipo9.TipoRegistro = 9 + registro_tipo9.NumeroTotalLinhas = str(numero_total_linhas).zfill(7) + registro_tipo9.ValorTotalServicos = str(valor_total_servicos_centavos).zfill(15) + registro_tipo9.ValorTotalValores = str(valor_total_registro3).zfill(15) + + registros_finais = registros_dados + [registro_tipo9] + rps = RPS(registros_finais).exportar() if isinstance(rps, str): rps = rps.encode("utf-8") @@ -86,10 +391,9 @@ def _serialize_barueri_lote_rps(self): " de bytes." ) - rps = base64.b64encode(rps) return rps - def serialize_nfse_barueri(self): + def serialize_nfse_barueri(self, cancel=False): lote_rps = NFeLoteEnviarArquivo( InscricaoMunicipal=self.convert_type_nfselib( NFeLoteEnviarArquivo, "InscricaoMunicipal", self.company_inscr_mun @@ -110,40 +414,71 @@ def serialize_nfse_barueri(self): ArquivoRPSBase64=self.convert_type_nfselib( NFeLoteEnviarArquivo, "ArquivoRPSBase64", - self._serialize_barueri_lote_rps(), + self._serialize_barueri_lote_rps(cancel=cancel), ), ) return lote_rps def _document_status(self): + mensagem = False status = super()._document_status() for record in self.filtered(filter_oca_nfse).filtered(filter_barueri): processador = record._processador_erpbrasil_nfse() + protocolo = record.authorization_event_id.lot_receipt_number processo = processador.consulta_nfse_rps( rps_number=int(record.rps_number), rps_serie=record.document_serie, rps_type=int(record.rps_type), + lot_receipt_number=protocolo, ) - status = _( - processador.analisa_retorno_consulta( - processo, - record.document_number, - record.company_cnpj_cpf, - record.company_legal_name, - ) - ) - return status + status, mensagem = processador.analisa_retorno_consulta(processo) + vals = dict() + if status == 1 and int(record.status_code) in [-1, -2]: + vals[ + "return_filename" + ] = processo.resposta.ListaNfeArquivosRPS.NomeArqRetorno + vals["status_name"] = _("Successfully Processed") + vals["status_code"] = 1 + vals = record._set_response(record, processador, protocolo, vals) + + if status == 2 and int(record.status_code) in [-1, -2]: + vals[ + "return_filename" + ] = processo.resposta.ListaNfeArquivosRPS.NomeArqRetorno + vals["status_name"] = _("Processed with Error") + vals["status_code"] = 2 + vals = record._set_response(record, processador, protocolo, vals) + + return mensagem + + def _baixar_xml_nfse(self, autenticidade, cnpj): + url = ( + "https://testeeiss.barueri.sp.gov.br/nfe/xmlNFe.ashx" + f"?codigoautenticidade={autenticidade}" + f"&numdoc={cnpj}" + ) + resp = requests.get(url, timeout=15) + resp.raise_for_status() + + return resp.text @staticmethod def _get_protocolo(record, processador, vals): for edoc in record.serialize(): + protocolo = None processo = None for p in processador.processar_documento(edoc): processo = p + if processo.webservice in ENVIO_LOTE_RPS: + record.authorization_event_id.lot_receipt_number = ( + processo.resposta.ProtocoloRemessa + ) + protocolo = processo.resposta.ProtocoloRemessa + if processo.webservice in CONSULTAR_NFSE_POR_RPS: - if processo.resposta.Protocolo is None: + if processo.resposta.ProtocoloRemessa is None: mensagem_completa = "" if processo.resposta.ListaMensagemRetorno: lista_msgs = processo.resposta.ListaMensagemRetorno @@ -164,53 +499,138 @@ def _get_protocolo(record, processador, vals): record._change_state(SITUACAO_EDOC_REJEITADA) record.write(vals) return - protocolo = processo.resposta.Protocolo + protocolo = processo.resposta.ProtocoloRemessa - if processo.webservice in CONSULTAR_SITUACAO_LOTE_RPS: - vals["status_code"] = processo.resposta.Situacao + if processo.webservice in CONSULTAR_SITUACAO_LOTE_RPS: + vals["status_code"] = int( + processo.resposta.ListaNfeArquivosRPS.SituacaoArq + ) + vals[ + "return_filename" + ] = processo.resposta.ListaNfeArquivosRPS.NomeArqRetorno return vals, protocolo @staticmethod def _set_response(record, processador, protocolo, vals): - processo = processador.consultar_lote_rps(protocolo) + processo = processador.baixar_lote_rps(vals.get("return_filename")) if processo.resposta: mensagem_completa = "" - if processo.resposta.ListaMensagemRetorno: + if vals.get("status_code") == 2 and processo.resposta.ListaMensagemRetorno: lista_msgs = processo.resposta.ListaMensagemRetorno - for mr in lista_msgs.MensagemRetorno: - correcao = "" - if mr.Correcao: - correcao = mr.Correcao + if lista_msgs.Codigo != "OK200": mensagem_completa += ( - mr.Codigo + lista_msgs.Codigo + " - " - + mr.Mensagem + + lista_msgs.Mensagem + " - Correção: " - + correcao + + lista_msgs.Correcao + "\n" ) - vals["edoc_error_message"] = mensagem_completa - if vals.get("status_code") == 3: + else: + error_messages = { + "000": "Layout Inválido", + "102": "inválida ou já informada em outro arquivo de remessa", + "103": "Versão Incorreta", + } + + file_content = processo.retorno.ArquivoRPSBase64.decode( + "utf-8" + ).strip() + parts = file_content.split(";") + values = [] + for i in range(len(parts) - 1): + segment = parts[i] + if len(segment) >= 3: + last_3 = segment[-3:] + values.append(last_3) + + if values: + for value in values: + mensagem_completa += ( + value + + " - " + + error_messages.get(value, "Erro desconhecido") + + " - Correção: " + + "Efetuar correção do arquivo" + + "\n" + ) + vals["edoc_error_message"] = mensagem_completa + + record.write( + { + "status_name": vals["status_name"], + "status_code": vals["status_code"], + "edoc_error_message": mensagem_completa, + } + ) record._change_state(SITUACAO_EDOC_REJEITADA) + else: + if vals.get("status_code") == 1: + arquivo_bytes = processo.retorno.ArquivoRPSBase64 + arquivo_texto = arquivo_bytes.decode("latin1") + linhas = arquivo_texto.splitlines() + registros_exporta = [parse_linha_exporta(linha) for linha in linhas] + + nfse_number = registros_exporta[1].campos[2].valor + nfse_date = registros_exporta[1].campos[3].valor + nfse_time = registros_exporta[1].campos[4].valor + nfse_auth_code = registros_exporta[1].campos[5].valor + nfse_status = registros_exporta[1].campos[10].valor + nfse_cnpj_cpf = registros_exporta[1].campos[14].valor + + vals["authorization_date"] = datetime.strptime( + nfse_date + nfse_time, "%Y%m%d%H%M%S" + ) - if processo.resposta.ListaNfse: - xml_file = processo.retorno - for comp in processo.resposta.ListaNfse.CompNfse: - vals["document_number"] = comp.Nfse.InfNfse.Numero - vals["authorization_date"] = comp.Nfse.InfNfse.DataEmissao - vals["verify_code"] = comp.Nfse.InfNfse.CodigoVerificacao - record.authorization_event_id.set_done( - status_code=vals["status_code"], - response=vals["status_name"], - protocol_date=vals["authorization_date"], - protocol_number=protocolo, - file_response_xml=xml_file, - ) - record._change_state(SITUACAO_EDOC_AUTORIZADA) + record.write( + { + "verify_code": nfse_auth_code, + "document_number": nfse_number, + "authorization_date": vals["authorization_date"], + "status_name": vals["status_name"], + "status_code": vals["status_code"], + } + ) + xml_file = record._baixar_xml_nfse(nfse_auth_code, nfse_cnpj_cpf) + + if nfse_status == "A": + record.authorization_event_id.set_done( + status_code=vals["status_code"], + response=vals["status_name"], + protocol_date=vals["authorization_date"], + protocol_number=protocolo, + file_response_xml=xml_file, + ) + record._change_state(SITUACAO_EDOC_AUTORIZADA) + record.make_pdf() + + if nfse_status == "C": + event = record.event_ids.create_event_save_xml( + company_id=record.company_id, + environment=( + EVENT_ENV_PROD + if record.nfse_environment == "1" + else EVENT_ENV_HML + ), + event_type="2", + xml_file=xml_file, + document_id=record, + ) + event.write( + { + "status_code": 2, + "response": _("Processado com Sucesso"), + "protocol_number": protocolo, + "protocol_date": fields.Datetime.now(), + "state": "done", + } + ) + record.cancel_event_id = event + record.state_edoc = SITUACAO_EDOC_CANCELADA return vals def _eletronic_document_send(self): @@ -222,26 +642,133 @@ def _eletronic_document_send(self): vals = dict() if not protocolo: - vals, protocolo = self._get_protocolo(record, processador, vals) + vals, protocolo = record._get_protocolo(record, processador, vals) else: - vals["status_code"] = 4 + vals["status_code"] = 0 - if vals.get("status_code") == 1: - vals["status_name"] = _("Not received") + if vals.get("status_code") == -1: + vals["status_name"] = _("Batch not yet processed") + record._change_state(SITUACAO_EDOC_ENVIADA) - elif vals.get("status_code") == 2: + elif vals.get("status_code") == -2: vals["status_name"] = _("Batch not yet processed") + record._change_state(SITUACAO_EDOC_ENVIADA) - elif vals.get("status_code") == 3: - vals["status_name"] = _("Processed with Error") + elif vals.get("status_code") == 0: + vals["status_name"] = _("Validated") - elif vals.get("status_code") == 4: + elif vals.get("status_code") == 1: vals["status_name"] = _("Successfully Processed") vals["authorization_protocol"] = protocolo - if vals.get("status_code") in (3, 4): - vals = self._set_response(record, processador, protocolo, vals) + elif vals.get("status_code") == 2: + vals["status_name"] = _("Processed with Error") + + if vals.get("status_code") in (1, 2): + vals = record._set_response(record, processador, protocolo, vals) + if "return_filename" in vals: + vals.pop("return_filename") record.write(vals) return + + def _cancel_document_barueri(self): + for record in self.filtered(filter_oca_nfse).filtered(filter_barueri): + processador = record._processador_erpbrasil_nfse() + edoc = record.serialize_nfse_barueri(cancel=True) + processo_envio = processador.envia_documento(edoc) + + protocolo = processo_envio.resposta.ProtocoloRemessa + if not protocolo: + return False + record.authorization_event_id.write( + { + "lot_receipt_number": protocolo, + } + ) + processo_consulta = processador.consulta_nfse_rps( + rps_number=int(record.rps_number), + rps_serie=record.document_serie, + rps_type=int(record.rps_type), + lot_receipt_number=protocolo, + ) + status, _mensagem = processador.analisa_retorno_consulta(processo_consulta) + if status == 0: + return False + if status in (-1, -2): + record._change_state(SITUACAO_EDOC_ENVIADA) + record.status_code = -2 + record.status_name = _("Cancel batch not yet processed") + return False + + nome_arquivo = ( + processo_consulta.resposta + and processo_consulta.resposta.ListaNfeArquivosRPS + and processo_consulta.resposta.ListaNfeArquivosRPS.NomeArqRetorno + ) + + if not nome_arquivo: + return False + + processo_retorno = processador.baixar_lote_rps(nome_arquivo) + if not processo_retorno.retorno: + return False + + arquivo = processo_retorno.retorno.ArquivoRPSBase64.decode("latin1") + linhas = arquivo.splitlines() + registros = [parse_linha_exporta(linha) for linha in linhas] + + nfse_status = registros[1].campos[10].valor + if nfse_status == "C": + event = record.event_ids.create_event_save_xml( + company_id=record.company_id, + environment=( + EVENT_ENV_PROD + if record.nfse_environment == "1" + else EVENT_ENV_HML + ), + event_type="2", + xml_file=processo_envio.envio_xml, + document_id=record, + ) + event.write( + { + "status_code": 2, + "response": _("Processado com Sucesso"), + "protocol_number": protocolo, + "protocol_date": fields.Datetime.now(), + "state": "done", + } + ) + record.cancel_event_id = event + record.state_edoc = SITUACAO_EDOC_CANCELADA + return True + return False + + def _exec_before_SITUACAO_EDOC_CANCELADA(self, old_state, new_state): + super()._exec_before_SITUACAO_EDOC_CANCELADA(old_state, new_state) + return ( + self.filtered(filter_oca_nfse) + .filtered(filter_barueri) + ._cancel_document_barueri() + ) + + @api.model + def _cron_document_status_barueri(self): + """Scheduled method to check the status of sent NFSe documents. + + Parameters: + None. + + Returns: + None. Updates the status of each document based + on the NFSe provider's response. + """ + records = ( + self.search([("state", "in", ["enviada"])], limit=25) + .filtered(filter_oca_nfse) + .filtered(filter_barueri) + ) + if records: + records._document_status() diff --git a/l10n_br_nfse_barueri/static/description/index.html b/l10n_br_nfse_barueri/static/description/index.html index 7e7f3caa4a52..d055bd3181f8 100644 --- a/l10n_br_nfse_barueri/static/description/index.html +++ b/l10n_br_nfse_barueri/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +NFS-e (Barueri) -
+
+

NFS-e (Barueri)

- - -Odoo Community Association - -
-

NFS-e (Barueri)

-

Beta License: AGPL-3 OCA/l10n-brazil Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/l10n-brazil Translate me on Weblate Try me on Runboat

Esse módulo completa o documento criado pelo l10n_br_nfse para permite a criação e transmissão de Notas Fiscais de Serviço Eletrônicas (NFS-e) pela prefeitura de Barueri.

Table of contents

@@ -392,7 +387,7 @@

NFS-e (Barueri)

-

Installation

+

Installation

  • Este módulo tem uma depedencia do pacote python erpbrasil.edoc
  • Este módulo tem uma depedencia do pacote python erpbrasil.assinatura
  • @@ -402,15 +397,15 @@

    Installation

-

Configuration

+

Configuration

É apenas necessário a instalação e configuração do módulo l10n_br_nfse.

-

Usage

+

Usage

Após ser criado uma Nota Fiscal de Serviço Eletrônicas (NFS-e) é possível confirmá-la e transmiti-la.

-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -418,21 +413,21 @@

Bug Tracker

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • KMEE
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -447,6 +442,5 @@

Maintainers

-
diff --git a/l10n_br_nfse_barueri/tests/nfse/nfse.xml b/l10n_br_nfse_barueri/tests/nfse/nfse.xml index e0422008de5f..cb1e1a67deb2 100644 --- a/l10n_br_nfse_barueri/tests/nfse/nfse.xml +++ b/l10n_br_nfse_barueri/tests/nfse/nfse.xml @@ -1,8 +1,8 @@ - 35172/> + 35172 59594315000157 Empresa Simples Nacional/SE/001/50.txt false TWs1dmJtVWdUbTl1WlRBd01TQWdNREF3TURBd1RtOXVaVEl3TWpBd05qQTBNVE0xT0RRMlJVNXZNREF3VG05dVpVNXZibVVnTURBd01FNXZibVZPYjI1bElDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0F3TURNeE1ERXlNREJPVGs1dmJtVWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lFNXZibVVnSUNBZ0lFNXZibVVnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJRTV2Ym1VZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNCT2IyNWxJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdUbTlPYjI1bElDQWdJREF3TURBd01UQXdNREF3TURBd01EQXhNREF1TUU1dmJtVWdNREF3TURBd01EQXdNREF3TUM0d01rNXZiazR5T0RFME9UTTVOemt3TURBeE9EbERiR2xsYm5SbElERWdVMUFnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNCU2RXRWdVMkZ0ZFdWc0lFMXZjbk5sSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQXhNelVnSUNBZ0lDQk9iMjVsSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0JDY205dmEyeHBiaUFnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ1U4T2pieUJRWVhWc2J5QWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0JUVURRMU56WXdOakFnWTJ4cFpXNTBaVEZBWTJ4cFpXNTBaVEV1WTI5dExtSnlJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0F3TUU1dmJtVXdNREF3TURBd01EQXdNRTV2Ym1WT2IyNWxJQ0FnSUNBZ0lDQWdJQ0JiVDBSUFQxOUVSVlpkSUVOMWMzUnZiV2w2WldRZ1QyUnZieUJFWlhabGJHOXdiV1Z1ZENBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnSUNBZ0lDQWdJQ0FnRFE9PQ== + >MTM1MTcyICBQTUIwMDQyMDIwMDYwNDg3OQ0KMlJQUyAgICAgICAgICAgMDAwMDAwMDA1MDIwMjAwNjA0MTM1ODQ2RSAgMDAwMDAwMCAgICAgMDAwMDAwMDAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAwMDYzMTE5MDAxMiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAwMDAwMDAwMDAwMDAwMTAwMDAwMDAwMDAxMDAwMCAgICAgMDAwMDAwMDAwMDAwMDAwMjAwMDIyODE0OTM5NzkwMDAxODlDbGllbnRlIDEgU1AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBSIFBlZHJhIFNhYmFvICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAxMzUgICAgICBOL0EgICAgICAgICAgICAgICAgICAgICAgICAgICBCcm9va2xpbiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgU2FvIFBhdWxvICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFNQMDQ1NzYwNjBjbGllbnRlMUBjbGllbnRlMS5jb20uYnIgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDAwMDAwMDAwMDAwMDAwMDAwMDAwMCAgICAgICAgICAgICAgIFtPRE9PX0RFVl0gQ3VzdG9taXplZCBPZG9vIERldmVsb3BtZW50ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICANCjQxMDAwMDMxMzI0MDQzNTUwMzA4ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDAwMDAwMDAwMCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMA0KNTAwMDEwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMDAwMCAwMDAwMDAwMDAwMDAwMCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMDAwMDAwMCAgICAgMDAwMDAwMDAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIA0KOTAwMDAwMDUwMDAwMDAwMDAwMTAwMDAwMDAwMDAwMDAwMDAwMDANCg== diff --git a/l10n_br_nfse_barueri/tests/test_fiscal_document_nfse_barueri.py b/l10n_br_nfse_barueri/tests/test_fiscal_document_nfse_barueri.py index 77a3b1b7405e..bb6ba84ae7db 100644 --- a/l10n_br_nfse_barueri/tests/test_fiscal_document_nfse_barueri.py +++ b/l10n_br_nfse_barueri/tests/test_fiscal_document_nfse_barueri.py @@ -5,6 +5,7 @@ import logging import os from datetime import datetime +from unittest.mock import MagicMock, patch from xmldiff import main @@ -22,11 +23,10 @@ class TestFiscalDocumentNFSeBarueri(TestFiscalDocumentNFSeCommon): def setUp(self): super().setUp() - self.company.provedor_nfse = "barueri" - def test_nfse_barueri(self): - """Test NFS-e same state.""" + def test_nfse_barueri_xml_esperado(self): + """Garante que o XML gerado para Barueri permaneça compatível com o esperado.""" xml_path = os.path.join( l10n_br_nfse_barueri.__path__[0], "tests", "nfse", "nfse.xml" @@ -62,9 +62,109 @@ def test_nfse_barueri(self): self.cr.dbname, self.nfse_same_state.send_file_id.store_fname, ) - _logger.info("XML file saved at %s" % (output,)) + _logger.info("XML file saved at %s", output) diff = main.diff_files(xml_path, output) - _logger.info("Diff with expected XML (if any): %s" % (diff,)) + _logger.info("Diff with expected XML (if any): %s", diff) + + # Espera-se no máximo pequenas diferenças irrelevantes + assert len(diff) <= 1 + + def test_is_nfse_barueri_flag(self): + """Valida o cálculo do campo técnico is_nfse_barueri.""" + barueri_city = self.env.ref("l10n_br_base.city_3505708") + other_city = self.env.ref("l10n_br_base.city_3132404") + + self.company.city_id = barueri_city + self.nfse_same_state.document_type = "SE" + + self.assertTrue( + self.nfse_same_state.is_nfse_barueri, + ) + + self.company.city_id = other_city + self.nfse_same_state.invalidate_cache(fnames=["is_nfse_barueri"]) + self.assertFalse( + self.nfse_same_state.is_nfse_barueri, + ) + + def test_compute_url_nfse_barueri_ambientes(self): + """Testa a geração da URL de consulta da + NFSe em ambiente produção e homologação.""" + barueri_city = self.env.ref("l10n_br_base.city_3505708") + self.company.city_id = barueri_city + self.nfse_same_state.document_type = "SE" + self.nfse_same_state.partner_id.cnpj_cpf = "12.345.678/0001-95" + self.nfse_same_state.verify_code = "ABC123" + + self.nfse_same_state.nfse_environment = "1" + self.nfse_same_state._compute_url_nfse_barueri() + + self.assertEqual( + self.nfse_same_state.url_nfse_barueri, + "https://www.barueri.sp.gov.br/nfe/wfimagemNota.aspx" + "?CODIGOAUTENTICIDADE=ABC123&NUMDOC=12345678000195", + ) + + self.nfse_same_state.nfse_environment = "2" + self.nfse_same_state._compute_url_nfse_barueri() + + self.assertEqual( + self.nfse_same_state.url_nfse_barueri, + "https://testeeiss.barueri.sp.gov.br/nfe/wfimagemNota.aspx" + "?CODIGOAUTENTICIDADE=ABC123&NUMDOC=12345678000195", + ) - assert len(diff) == 1 + def test_action_open_nfse_barueri(self): + """Garante que a ação de abrir a NFSe retorna um ir.actions.act_url válido.""" + barueri_city = self.env.ref("l10n_br_base.city_3505708") + self.company.city_id = barueri_city + self.nfse_same_state.document_type = "SE" + self.nfse_same_state.partner_id.cnpj_cpf = "12345678000195" + self.nfse_same_state.verify_code = "COD123" + self.nfse_same_state.nfse_environment = "1" + self.nfse_same_state._compute_url_nfse_barueri() + + action = self.nfse_same_state.action_open_nfse_barueri() + + self.assertEqual(action["type"], "ir.actions.act_url") + self.assertEqual(action["url"], self.nfse_same_state.url_nfse_barueri) + self.assertEqual(action["target"], "new") + + def test_serialize_barueri_lote_rps_retorna_bytes(self): + """Verifica que o lote RPS gerado para + Barueri é retornado em bytes e não vazio.""" + barueri_city = self.env.ref("l10n_br_base.city_3505708") + self.company.city_id = barueri_city + self.nfse_same_state.document_type = "SE" + + now = datetime.now() + self.nfse_same_state.document_date = now + self.nfse_same_state.date_in_out = now + + rps_bytes = self.nfse_same_state._serialize_barueri_lote_rps() + + self.assertIsInstance(rps_bytes, (bytes, bytearray)) + self.assertTrue(rps_bytes, "O conteúdo do RPS não deveria ser vazio.") + + def test_baixar_xml_nfse_monta_url_corretamente(self): + """Garante que o método de download de + XML monta a URL esperada e trata o retorno.""" + autenticidade = "ABC123" + cnpj = "12345678000195" + + with patch( + "odoo.addons.l10n_br_nfse_barueri.models.document.requests.get" + ) as mock_get: + mock_response = MagicMock() + mock_response.text = "" + mock_get.return_value = mock_response + + result = self.nfse_same_state._baixar_xml_nfse(autenticidade, cnpj) + + mock_get.assert_called_once() + called_url = mock_get.call_args[0][0] + self.assertIn("codigoautenticidade=ABC123", called_url) + self.assertIn("numdoc=12345678000195", called_url) + mock_response.raise_for_status.assert_called_once() + self.assertEqual(result, "") diff --git a/l10n_br_nfse_barueri/views/document_view.xml b/l10n_br_nfse_barueri/views/document_view.xml new file mode 100644 index 000000000000..c86d39ff2a8a --- /dev/null +++ b/l10n_br_nfse_barueri/views/document_view.xml @@ -0,0 +1,36 @@ + + + + + l10n_br_nfse_barueri_direct_print.document.form.inherit + l10n_br_fiscal.document + + + + + + + diff --git a/test-requirements.txt b/test-requirements.txt index d24cb431aaba..e5dc847c4729 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,3 +3,5 @@ odoo-test-helper # Needed by spec_driven_model pyopenssl==22.1.0 signxml<3.1.0 xmldiff +# erpbrasil.edoc +erpbrasil.edoc @ git+https://github.com/Escodoo/erpbrasil.edoc.git@master-escodoo-fixes