From 962e3584922def1a4faa04a318b711ebd5fff645 Mon Sep 17 00:00:00 2001 From: Marcel Savegnago Date: Wed, 3 Dec 2025 11:35:55 -0300 Subject: [PATCH 01/10] [FIX] l10n_br_nfse_barueri: fixes --- l10n_br_nfse_barueri/constants/barueri.py | 2 +- l10n_br_nfse_barueri/models/document.py | 448 ++++++++++++++++++---- 2 files changed, 378 insertions(+), 72 deletions(-) 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/models/document.py b/l10n_br_nfse_barueri/models/document.py index 321ab7f5820f..129d0a2a6c3c 100644 --- a/l10n_br_nfse_barueri/models/document.py +++ b/l10n_br_nfse_barueri/models/document.py @@ -1,10 +1,28 @@ # 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 +from datetime import datetime +import requests +import unicodedata 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 @@ -12,10 +30,15 @@ MODELO_FISCAL_NFSE, PROCESSADOR_OCA, SITUACAO_EDOC_AUTORIZADA, + 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,6 +55,24 @@ 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 == "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" @@ -50,33 +91,201 @@ def _serialize_barueri_dados_tomador(self): dados = self._prepare_dados_tomador() return dados + def _sem_acento(self, value): + return unicodedata.normalize( + "NFKD", value or "" + ).encode("ASCII", "ignore").decode("ASCII") + def _serialize_barueri_lote_rps(self): 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" + 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.TipoRegistro = 2 + registro_tipo2.TipoRPS = "RPS" + 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.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.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 = dados_tomador.get("razao_social", "") + registro_tipo2.EnderecoLogradouroTomador = ( + dados_tomador.get("logradouro", "") or "R Pedra Sabao" + ) + registro_tipo2.NumeroLogradouroTomador = str( + dados_tomador.get("numero", "") or "10" + ) + registro_tipo2.ComplementoLogradouroTomador = str( + 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("nbs", "")) if c.isdigit()) + registro_tipo4.CodigoIndicadorOperacaoFornecimento = "100301" + registro_tipo4.CodigoClassificacaoTributariaIBSCBS = "000001" + registro_tipo4.CodigoSituacaoTributariaIBSCBS = "000" + 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,7 +295,6 @@ def _serialize_barueri_lote_rps(self): " de bytes." ) - rps = base64.b64encode(rps) return rps def serialize_nfse_barueri(self): @@ -119,31 +327,61 @@ def _document_status(self): 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 +402,114 @@ 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() return vals def _eletronic_document_send(self): @@ -222,26 +521,33 @@ 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 From 34ce0fc7149eae3074cbfe10735af780855ca720 Mon Sep 17 00:00:00 2001 From: CristianoMafraJunior Date: Fri, 19 Dec 2025 11:27:29 -0300 Subject: [PATCH 02/10] [IMP] l10n_br_nfse_barueri: document print --- l10n_br_nfse_barueri/README.rst | 6 +- l10n_br_nfse_barueri/__manifest__.py | 3 + l10n_br_nfse_barueri/models/__init__.py | 1 + l10n_br_nfse_barueri/models/account_move.py | 12 +++ l10n_br_nfse_barueri/models/document.py | 75 ++++++++++++++++--- .../static/description/index.html | 30 +++----- l10n_br_nfse_barueri/views/document_view.xml | 36 +++++++++ 7 files changed, 131 insertions(+), 32 deletions(-) create mode 100644 l10n_br_nfse_barueri/models/account_move.py create mode 100644 l10n_br_nfse_barueri/views/document_view.xml 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..f7b084adb454 100644 --- a/l10n_br_nfse_barueri/__manifest__.py +++ b/l10n_br_nfse_barueri/__manifest__.py @@ -20,6 +20,9 @@ "nfselib.barueri", ], }, + "data": [ + "views/document_view.xml", + ], "depends": [ "l10n_br_nfse", ], 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 129d0a2a6c3c..edd47a61e446 100644 --- a/l10n_br_nfse_barueri/models/document.py +++ b/l10n_br_nfse_barueri/models/document.py @@ -2,10 +2,10 @@ # Copyright 2025 - TODAY, Cristiano Mafra Junior # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import unicodedata from datetime import datetime import requests -import unicodedata from nfselib.barueri.NFeLoteEnviarArquivo import NFeLoteEnviarArquivo from nfselib.barueri.nfse import ( NFSeRegistroTipo1, @@ -24,7 +24,7 @@ RegistroTipo9, ) -from odoo import _, models +from odoo import _, fields, models from odoo.addons.l10n_br_fiscal.constants.fiscal import ( MODELO_FISCAL_NFSE, @@ -76,6 +76,61 @@ def parse_linha_exporta(line: str): 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): @@ -92,9 +147,11 @@ def _serialize_barueri_dados_tomador(self): return dados def _sem_acento(self, value): - return unicodedata.normalize( - "NFKD", value or "" - ).encode("ASCII", "ignore").decode("ASCII") + return ( + unicodedata.normalize("NFKD", value or "") + .encode("ASCII", "ignore") + .decode("ASCII") + ) def _serialize_barueri_lote_rps(self): dados = self._prepare_lote_rps() @@ -229,7 +286,9 @@ def _serialize_barueri_lote_rps(self): 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("nbs", "")) if c.isdigit()) + registro_tipo4.CodigoNBS = "".join( + c for c in str(dados_servico.get("nbs", "")) if c.isdigit() + ) registro_tipo4.CodigoIndicadorOperacaoFornecimento = "100301" registro_tipo4.CodigoClassificacaoTributariaIBSCBS = "000001" registro_tipo4.CodigoSituacaoTributariaIBSCBS = "000" @@ -275,9 +334,7 @@ def _serialize_barueri_lote_rps(self): 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 - ) + 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) 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/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 + + + + + + + From bd4da9870c35e03a297848c179afab717aa78317 Mon Sep 17 00:00:00 2001 From: CristianoMafraJunior Date: Tue, 23 Dec 2025 15:14:10 -0300 Subject: [PATCH 03/10] [IMP] l10n_br_nfse_barueri: cancelamento --- l10n_br_nfse_barueri/models/document.py | 147 +++++++++++++++++++++--- 1 file changed, 130 insertions(+), 17 deletions(-) diff --git a/l10n_br_nfse_barueri/models/document.py b/l10n_br_nfse_barueri/models/document.py index edd47a61e446..ed3175e2683b 100644 --- a/l10n_br_nfse_barueri/models/document.py +++ b/l10n_br_nfse_barueri/models/document.py @@ -27,9 +27,12 @@ from odoo import _, 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, ) @@ -65,6 +68,8 @@ def parse_linha_exporta(line: str): 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: @@ -153,7 +158,7 @@ def _sem_acento(self, value): .decode("ASCII") ) - def _serialize_barueri_lote_rps(self): + 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() @@ -162,7 +167,11 @@ def _serialize_barueri_lote_rps(self): registro_tipo1.TipoRegistro = 1 registro_tipo1.InscricaoContribuinte = self.company_inscr_mun registro_tipo1.VersaoLayout = "PMB004" - data_emissao = dados["data_emissao"].split("T")[0] + 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}" @@ -171,16 +180,36 @@ def _serialize_barueri_lote_rps(self): registro_tipo2 = RegistroTipo2() registro_tipo2.TipoRegistro = 2 registro_tipo2.TipoRPS = "RPS" - 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 = "" + 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" ] @@ -289,9 +318,16 @@ def _serialize_barueri_lote_rps(self): registro_tipo4.CodigoNBS = "".join( c for c in str(dados_servico.get("nbs", "")) if c.isdigit() ) - registro_tipo4.CodigoIndicadorOperacaoFornecimento = "100301" - registro_tipo4.CodigoClassificacaoTributariaIBSCBS = "000001" - registro_tipo4.CodigoSituacaoTributariaIBSCBS = "000" + registro_tipo4.CodigoIndicadorOperacaoFornecimento = dados_servico.get( + "indop", "" + ).zfill(6) + registro_tipo4.CodigoClassificacaoTributariaIBSCBS = dados_servico.get( + "cclass_trib", "" + ).zfill(6) + + registro_tipo4.CodigoSituacaoTributariaIBSCBS = dados_servico.get( + "cst_trib", "" + ).zfill(3) registro_tipo4.OperacaoUsoConsumoPessoal = "0" registro_tipo4.IndicadorDestinatarioServico = "0" @@ -354,7 +390,7 @@ def _serialize_barueri_lote_rps(self): 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 @@ -375,7 +411,7 @@ 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 @@ -608,3 +644,80 @@ def _eletronic_document_send(self): 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 in (-1, -2, 0): + 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(l) for l 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() + ) From c9f969112d04773c9a7510dc639101c1607a1b5d Mon Sep 17 00:00:00 2001 From: CristianoMafraJunior Date: Tue, 23 Dec 2025 15:42:39 -0300 Subject: [PATCH 04/10] [IMP] l10n_br_nfse_barueri: add new fields - tax classification and others --- l10n_br_nfse_barueri/models/document.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/l10n_br_nfse_barueri/models/document.py b/l10n_br_nfse_barueri/models/document.py index ed3175e2683b..fe5ea59880b1 100644 --- a/l10n_br_nfse_barueri/models/document.py +++ b/l10n_br_nfse_barueri/models/document.py @@ -316,17 +316,17 @@ def _serialize_barueri_lote_rps(self, cancel=False): self.partner_id.city_id.ibge_code or "" ).zfill(7) registro_tipo4.CodigoNBS = "".join( - c for c in str(dados_servico.get("nbs", "")) if c.isdigit() + c for c in str(dados_servico.get("codigo_nbs", "")) if c.isdigit() ) registro_tipo4.CodigoIndicadorOperacaoFornecimento = dados_servico.get( - "indop", "" + "codigo_indicador_operacao", "" ).zfill(6) registro_tipo4.CodigoClassificacaoTributariaIBSCBS = dados_servico.get( - "cclass_trib", "" + "codigo_classificacao_tributaria", "" ).zfill(6) registro_tipo4.CodigoSituacaoTributariaIBSCBS = dados_servico.get( - "cst_trib", "" + "codigo_situacao_tributaria", "" ).zfill(3) registro_tipo4.OperacaoUsoConsumoPessoal = "0" registro_tipo4.IndicadorDestinatarioServico = "0" From 620ccd11a94aa58ffe6fab8b6c8fa1d8241995ae Mon Sep 17 00:00:00 2001 From: CristianoMafraJunior Date: Tue, 23 Dec 2025 15:46:29 -0300 Subject: [PATCH 05/10] [FIX] l10n_br_nfse_barueri: fix tests --- l10n_br_nfse_barueri/models/document.py | 24 +++++++++++++----------- l10n_br_nfse_barueri/tests/nfse/nfse.xml | 4 ++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/l10n_br_nfse_barueri/models/document.py b/l10n_br_nfse_barueri/models/document.py index fe5ea59880b1..0d2efe1e44ce 100644 --- a/l10n_br_nfse_barueri/models/document.py +++ b/l10n_br_nfse_barueri/models/document.py @@ -318,16 +318,17 @@ def _serialize_barueri_lote_rps(self, cancel=False): 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", "" - ).zfill(6) - registro_tipo4.CodigoClassificacaoTributariaIBSCBS = dados_servico.get( - "codigo_classificacao_tributaria", "" - ).zfill(6) - - registro_tipo4.CodigoSituacaoTributariaIBSCBS = dados_servico.get( - "codigo_situacao_tributaria", "" - ).zfill(3) + 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" @@ -417,6 +418,7 @@ def serialize_nfse_barueri(self, cancel=False): 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() @@ -685,7 +687,7 @@ def _cancel_document_barueri(self): arquivo = processo_retorno.retorno.ArquivoRPSBase64.decode("latin1") linhas = arquivo.splitlines() - registros = [parse_linha_exporta(l) for l in linhas] + registros = [parse_linha_exporta(linha) for linha in linhas] nfse_status = registros[1].campos[10].valor if nfse_status == "C": 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== From 4290c104966796fc45f2e4fac556ecaa38177900 Mon Sep 17 00:00:00 2001 From: CristianoMafraJunior Date: Tue, 23 Dec 2025 15:57:01 -0300 Subject: [PATCH 06/10] fix test-requirements.txt --- test-requirements.txt | 2 ++ 1 file changed, 2 insertions(+) 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 From 9bf1cdbc4caafe8aace09d6f3a2687a1fe632991 Mon Sep 17 00:00:00 2001 From: CristianoMafraJunior Date: Mon, 5 Jan 2026 13:28:04 -0300 Subject: [PATCH 07/10] [FIX] l10n_br_nfse_barueri: fix cancelamento --- l10n_br_fiscal/constants/fiscal.py | 1 + l10n_br_nfse_barueri/models/document.py | 32 +++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) 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/models/document.py b/l10n_br_nfse_barueri/models/document.py index 0d2efe1e44ce..cf144a42ecf7 100644 --- a/l10n_br_nfse_barueri/models/document.py +++ b/l10n_br_nfse_barueri/models/document.py @@ -605,6 +605,30 @@ def _set_response(record, processador, protocolo, vals): ) 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): @@ -668,8 +692,12 @@ def _cancel_document_barueri(self): lot_receipt_number=protocolo, ) status, _mensagem = processador.analisa_retorno_consulta(processo_consulta) - - if status in (-1, -2, 0): + 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 = ( From f876a8933f244cab072cdebda3b544274d9ab53f Mon Sep 17 00:00:00 2001 From: CristianoMafraJunior Date: Mon, 5 Jan 2026 13:42:25 -0300 Subject: [PATCH 08/10] [IMP] l10n_br_nfse_barueri: cron document status --- l10n_br_nfse_barueri/__manifest__.py | 1 + .../data/l10n_br_nfse_barueri_cron.xml | 18 ++++++++++++++++ l10n_br_nfse_barueri/models/document.py | 21 ++++++++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 l10n_br_nfse_barueri/data/l10n_br_nfse_barueri_cron.xml diff --git a/l10n_br_nfse_barueri/__manifest__.py b/l10n_br_nfse_barueri/__manifest__.py index f7b084adb454..374f92929b40 100644 --- a/l10n_br_nfse_barueri/__manifest__.py +++ b/l10n_br_nfse_barueri/__manifest__.py @@ -22,6 +22,7 @@ }, "data": [ "views/document_view.xml", + "data/l10n_br_nfse_barueri_cron.xml", ], "depends": [ "l10n_br_nfse", 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/document.py b/l10n_br_nfse_barueri/models/document.py index cf144a42ecf7..4d3b045ea556 100644 --- a/l10n_br_nfse_barueri/models/document.py +++ b/l10n_br_nfse_barueri/models/document.py @@ -24,7 +24,7 @@ RegistroTipo9, ) -from odoo import _, fields, models +from odoo import _, api, fields, models from odoo.addons.l10n_br_fiscal.constants.fiscal import ( EVENT_ENV_HML, @@ -751,3 +751,22 @@ def _exec_before_SITUACAO_EDOC_CANCELADA(self, old_state, new_state): .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() From a414845d5146e7b62e47cce3f76f036b561963f7 Mon Sep 17 00:00:00 2001 From: CristianoMafraJunior Date: Tue, 6 Jan 2026 16:38:16 -0300 Subject: [PATCH 09/10] [IMP] l10n_br_nfse_barueri: add tests --- .../test_fiscal_document_nfse_barueri.py | 112 +++++++++++++++++- 1 file changed, 106 insertions(+), 6 deletions(-) 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, "") From 531a1ae8c689d796690c416a2b7d3d2da22e2398 Mon Sep 17 00:00:00 2001 From: CristianoMafraJunior Date: Mon, 12 Jan 2026 08:00:52 -0300 Subject: [PATCH 10/10] [FIX] l10n_br_nfse_barueri: fields _sem_acento --- l10n_br_nfse_barueri/models/document.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/l10n_br_nfse_barueri/models/document.py b/l10n_br_nfse_barueri/models/document.py index 4d3b045ea556..a92d06a9e4ea 100644 --- a/l10n_br_nfse_barueri/models/document.py +++ b/l10n_br_nfse_barueri/models/document.py @@ -256,14 +256,16 @@ def _serialize_barueri_lote_rps(self, cancel=False): else: registro_tipo2.IndicadorCPFCNPJTomador = "1" registro_tipo2.CPFCNPJTomador = "0" * 14 - registro_tipo2.RazaoSocialNomeTomador = dados_tomador.get("razao_social", "") - registro_tipo2.EnderecoLogradouroTomador = ( - dados_tomador.get("logradouro", "") or "R Pedra Sabao" + 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 = str( + registro_tipo2.ComplementoLogradouroTomador = self._sem_acento( dados_tomador.get("complemento", "") or "N/A" ) registro_tipo2.BairroLogradouroTomador = self._sem_acento(