From bc086d65ed322e488cc831e91e50490a0acd083b Mon Sep 17 00:00:00 2001 From: Evelyn Caroline <85968488+Evyyl@users.noreply.github.com> Date: Wed, 13 May 2026 12:56:42 -0300 Subject: [PATCH 1/3] =?UTF-8?q?Corre=C3=A7=C3=A3o=20caminho=20do=20arquivo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/adapters/controllers/extrair_dados_r2000_controller.py | 2 +- src/config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/adapters/controllers/extrair_dados_r2000_controller.py b/src/adapters/controllers/extrair_dados_r2000_controller.py index 4f1f043..e9e6a28 100644 --- a/src/adapters/controllers/extrair_dados_r2000_controller.py +++ b/src/adapters/controllers/extrair_dados_r2000_controller.py @@ -20,7 +20,7 @@ def ExtrairDadosR2000Controller(): ) if mes_escolhido: - use_case = factory.create_extrair_dados_r2000_usecase(mes_escolhido[0]) + use_case = factory.create_extrair_dados_r2000_uc(mes_escolhido[0]) use_case.executar(mes_escolhido) break diff --git a/src/config.py b/src/config.py index f7df8c0..213752d 100644 --- a/src/config.py +++ b/src/config.py @@ -7,7 +7,7 @@ # Se falhar, tenta uma localidade comum para Windows locale.setlocale(locale.LC_ALL, "Portuguese_Brazil.1252") -TESTE = False +TESTE = True ANO_ATUAL = datetime.now().year MES_ATUAL = datetime.now().month From 7c09420cfbb6cac5b1b92acfc4a000bd3bda5546 Mon Sep 17 00:00:00 2001 From: Evelyn Caroline <85968488+Evyyl@users.noreply.github.com> Date: Wed, 13 May 2026 15:05:43 -0300 Subject: [PATCH 2/3] --- src/config.py | 2 +- src/core/interfaces/i_pdf_service.py | 4 +- src/core/parsers/demonstrativo_parser.py | 6 +- src/core/usecases/extrair_dados_r2000_uc.py | 26 +++-- src/factories.py | 2 +- src/infrastructure/services/excel_service.py | 2 +- .../services/excel_service_win32.py | 2 + src/infrastructure/services/pdf_service.py | 99 +++++++++++++++++++ 8 files changed, 125 insertions(+), 18 deletions(-) diff --git a/src/config.py b/src/config.py index 213752d..f7df8c0 100644 --- a/src/config.py +++ b/src/config.py @@ -7,7 +7,7 @@ # Se falhar, tenta uma localidade comum para Windows locale.setlocale(locale.LC_ALL, "Portuguese_Brazil.1252") -TESTE = True +TESTE = False ANO_ATUAL = datetime.now().year MES_ATUAL = datetime.now().month diff --git a/src/core/interfaces/i_pdf_service.py b/src/core/interfaces/i_pdf_service.py index 894decc..5fae7d4 100644 --- a/src/core/interfaces/i_pdf_service.py +++ b/src/core/interfaces/i_pdf_service.py @@ -23,8 +23,8 @@ def get_nls_baixadas(self, lista_nls: list[str]) -> list[str]: ... # @abstractmethod # def parse_relatorio_folha(self, fundo_escolhido: str)->dict[str, DataFrame]: ... - # @abstractmethod - # def parse_dados_inss(self): ... + @abstractmethod + def parse_dados_inss(self, caminho_pdf: str) -> dict: ... # @abstractmethod # def parse_dados_provisoes(self, fundo:str) -> dict: ... diff --git a/src/core/parsers/demonstrativo_parser.py b/src/core/parsers/demonstrativo_parser.py index c9504fb..429fafe 100644 --- a/src/core/parsers/demonstrativo_parser.py +++ b/src/core/parsers/demonstrativo_parser.py @@ -29,7 +29,7 @@ def parse_dados_inss(self, caminho_pdf: str): tipo_servico_match.group(1).strip() if tipo_servico_match else None ) - valor_nf_match = re.search(r"VALOR DA NF:\s*([\d.,]+)\s*R\$", text) + valor_nf_match = re.search(r"VALOR DA NF:.*?(?:R\$)?\s*([\d.,]+)", text) valor_nf = ( valor_nf_match.group(1).replace(".", "").replace(",", ".") if valor_nf_match @@ -51,7 +51,7 @@ def parse_dados_inss(self, caminho_pdf: str): tipo_inss = tipo_inss_match.group(1).strip() if tipo_inss_match else None base_calculo_inss_match = re.search( - r"BASE DE CÁLCULO INSS:\s*([\d.,]+)\s*R\$", text + r"BASE DE CÁLCULO INSS:.*?(?:R\$)?\s*([\d.,]+)", text ) base_calculo_inss = ( base_calculo_inss_match.group(1).replace(".", "").replace(",", ".") @@ -60,7 +60,7 @@ def parse_dados_inss(self, caminho_pdf: str): ) valor_inss_retido_match = re.search( - r"VALOR DE INSS RETIDO:\s*([\d.,]+)\s*R\$", text + r"VALOR DE INSS RETIDO:.*?(?:R\$)?\s*([\d.,]+)", text ) valor_inss_retido = ( valor_inss_retido_match.group(1).replace(".", "").replace(",", ".") diff --git a/src/core/usecases/extrair_dados_r2000_uc.py b/src/core/usecases/extrair_dados_r2000_uc.py index da95f68..746dbee 100644 --- a/src/core/usecases/extrair_dados_r2000_uc.py +++ b/src/core/usecases/extrair_dados_r2000_uc.py @@ -20,14 +20,20 @@ def __init__( self.pdf_svc = pdf_svc def executar(self, meses_escolhidos: list[str]): - for pasta_mes in meses_escolhidos: - try: - dados_inss = self.extrair_dados_inss(pasta_mes) - df_r2010_1, df_r2010_2 = self.gerar_dataframes_reinf(dados_inss) - self.exportar_planilhas_r2000(df_r2010_1, df_r2010_2) - except Exception as e: - print(e) - finally: + try: + for pasta_mes in meses_escolhidos: + try: + dados_inss = self.extrair_dados_inss(pasta_mes) + df_r2010_1, df_r2010_2 = self.gerar_dataframes_reinf(dados_inss) + + if df_r2010_1 is not None and df_r2010_2 is not None: + self.exportar_planilhas_r2000(df_r2010_1, df_r2010_2) + else: + print(f"PULANDO EXPORTAÇÃO: Nenhum dado encontrado para o mês {pasta_mes}") + except Exception as e: + print(e) + finally: + if hasattr(self.excel_svc, "__exit__"): self.excel_svc.__exit__() def exportar_valores_pagos(self, meses_escolhidos: list[str]): @@ -45,6 +51,8 @@ def exportar_valores_pagos(self, meses_escolhidos: list[str]): def extrair_dados_inss(self, pasta_mes: str): caminhos_pdf = self.pathing_gw.get_caminhos_demonstrativos(pasta_mes) lista_de_dados_completa = [] + df = pd.DataFrame() + for caminho_pdf in caminhos_pdf: dados_extraidos = self.pdf_svc.parse_dados_inss(caminho_pdf) @@ -55,8 +63,6 @@ def extrair_dados_inss(self, pasta_mes: str): df = DataFrame(lista_de_dados_completa) df = df.sort_values(by=["CNPJ", "NUM_NF"]).reset_index(drop=True) - df = df.sort_values(by=["CNPJ", "NUM_NF"]) - return df def gerar_dataframe_valores_pagos(self, df_principal): diff --git a/src/factories.py b/src/factories.py index 9c0d62d..2316317 100644 --- a/src/factories.py +++ b/src/factories.py @@ -137,7 +137,7 @@ def create_extrair_dados_r2000_uc( ExcelService.copy_to(caminho_reinf_base, caminho_completo) caminho_planilha_reinf = pathing_gw.get_caminho_reinf(pasta_mes_escolhido) - excel_svc = ExcelServiceWin32(caminho_planilha_reinf) + excel_svc = ExcelService(caminho_planilha_reinf) use_case = ExtrairDadosR2000UseCase(excel_svc, pdf_svc, pathing_gw) return use_case diff --git a/src/infrastructure/services/excel_service.py b/src/infrastructure/services/excel_service.py index 5e28942..0e1a87e 100644 --- a/src/infrastructure/services/excel_service.py +++ b/src/infrastructure/services/excel_service.py @@ -198,7 +198,7 @@ def fit_columns(self, sheet): adjusted_width = max_length + 7 sheet.column_dimensions[column_letter].width = adjusted_width - def delete_rows(self, sheet_name: str, start_row: int = 1): + def delete_rows(self, sheet_name: str, start_row: int = 1, end_row: int | None = None): """ Deleta todas as linhas em uma planilha a partir de uma linha específica. diff --git a/src/infrastructure/services/excel_service_win32.py b/src/infrastructure/services/excel_service_win32.py index 8b4cf89..2074437 100644 --- a/src/infrastructure/services/excel_service_win32.py +++ b/src/infrastructure/services/excel_service_win32.py @@ -21,6 +21,8 @@ class ExcelServiceWin32(IExcelService): # workbook: Optional[CDispatch] = None def __init__(self, caminho_arquivo: str | None = None): + self.excel = None + self.workbook = None if caminho_arquivo is not None: self.caminho_arquivo = caminho_arquivo self.__enter__() diff --git a/src/infrastructure/services/pdf_service.py b/src/infrastructure/services/pdf_service.py index f4033e3..fde2f1b 100644 --- a/src/infrastructure/services/pdf_service.py +++ b/src/infrastructure/services/pdf_service.py @@ -98,3 +98,102 @@ def get_nls_baixadas(self, lista_nls: list[str]) -> list[str]: dados_bruto_nls.append(texto_completo_pdf) return dados_bruto_nls + + def parse_dados_inss(self, caminho_pdf: str): + """Extrai dados de um PDF de demonstrativo de INSS.""" + + try: + with open(caminho_pdf, "rb") as file: + reader = PdfReader(file) + text = "" + for page in reader.pages: + text += page.extract_text() + + # Normaliza o texto: remove quebras de linha e espaços extras + text = re.sub(r"\s+", " ", text) + + # --- Extração com Regex Flexíveis --- + + # Processo + processo_match = re.search(r"PROCESSO\s*Nº\s*:?\s*([\d/]+)", text, re.IGNORECASE) + processo = processo_match.group(1) if processo_match else None + + # CNPJ + cnpj_match = re.search(r"(?:CNPJ.*?PRESTADOR.*?|CNPJ.*?EMPRESA.*?)\s*([\d./-]+)", text, re.IGNORECASE) + cnpj = cnpj_match.group(1) if cnpj_match else None + + # Valor da NF + valor_nf_match = re.search(r"VALOR\s*DA\s*NF\s*:?.*?(?:R\$)?\s*([\d.,]+)", text, re.IGNORECASE) + valor_nf = valor_nf_match.group(1).replace(".", "").replace(",", ".") if valor_nf_match else None + + # Número da NF + num_nf_match = re.search(r"([\d.,]+)\s*Emissão:", text, re.IGNORECASE) + num_nf = num_nf_match.group(1).strip().replace(".", "") if num_nf_match else None + + # Data de Emissão + data_emissao_match = re.search(r"Emissão:\s*([\d/]+)", text, re.IGNORECASE) + data_emissao = data_emissao_match.group(1) if data_emissao_match else None + + # Série + serie_match = re.search(r"Série:\s*([\d]+)", text, re.IGNORECASE) + serie = serie_match.group(1).strip() if serie_match else None + + # Tipo INSS + tipo_inss_match = re.search(r"TIPO\s*DE\s*SERVIÇO\s*INSS\s*:?\s*([\d]+)", text, re.IGNORECASE) + tipo_inss = tipo_inss_match.group(1).strip() if tipo_inss_match else None + + # Base de Cálculo INSS + base_calculo_inss_match = re.search(r"BASE\s*DE\s*CÁLCULO\s*INSS\s*:?.*?(?:R\$)?\s*([\d.,]+)", text, re.IGNORECASE) + base_calculo_inss = base_calculo_inss_match.group(1).replace(".", "").replace(",", ".") if base_calculo_inss_match else None + + # Valor INSS Retido + valor_inss_retido_match = re.search(r"VALOR\s*DE\s*INSS\s*RETIDO\s*:?.*?(?:R\$)?\s*([\d.,]+)", text, re.IGNORECASE) + valor_inss_retido = valor_inss_retido_match.group(1).replace(".", "").replace(",", ".") if valor_inss_retido_match else None + + # CPRB + cprb_match = re.search(r"CONTRIBUINTE\s*DA\s*CPRB\s*\?\s*([\w])", text, re.IGNORECASE) + cprb = cprb_match.group(1) if cprb_match else None + + # --- Debug no Terminal --- + nome_arquivo = os.path.basename(caminho_pdf) + if not valor_nf or not num_nf: + print(f" [AVISO] Dados incompletos em: {nome_arquivo} (NF: {num_nf}, Valor: {valor_nf})") + else: + print(f" [OK] Lido: {nome_arquivo} - NF {num_nf}") + + dados_pdf = { + "CHAVE": ( + "0" * (15 - len(processo)) + processo + "," + num_nf + if processo and num_nf + else None + ), + "PROCESSO": processo, + "CNPJ": ( + str(cnpj.replace(".", "").replace("/", "").replace("-", "")) + if cnpj + else None + ), + "TIPO_SERVICO": "", # Pode ser preenchido se necessário + "VALOR_NF": float(valor_nf) if valor_nf else None, + "NUM_NF": num_nf if num_nf else None, + "DATA_EMISSAO": ( + datetime.strptime(data_emissao, "%d/%m/%Y").date() + if data_emissao + else None + ), + "SERIE": serie, + "TIPO_INSS": tipo_inss, + "BASE_CALCULO_INSS": ( + float(base_calculo_inss) if base_calculo_inss else None + ), + "VALOR_INSS_RETIDO": ( + float(valor_inss_retido) if valor_inss_retido else None + ), + "CPRB": 0 if cprb == "N" else 1, + } + + return dados_pdf if cnpj else None + + except Exception as e: + print(f" [ERRO] Falha ao processar {os.path.basename(caminho_pdf)}: {e}") + return None From c71fca9c72aea0d0846b797aae9249662a302dbc Mon Sep 17 00:00:00 2001 From: Evelyn Caroline <85968488+Evyyl@users.noreply.github.com> Date: Wed, 13 May 2026 15:33:08 -0300 Subject: [PATCH 3/3] =?UTF-8?q?Documenta=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DOCUMENTACAO.md | 377 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 377 insertions(+) create mode 100644 DOCUMENTACAO.md diff --git a/DOCUMENTACAO.md b/DOCUMENTACAO.md new file mode 100644 index 0000000..ce319db --- /dev/null +++ b/DOCUMENTACAO.md @@ -0,0 +1,377 @@ +# 📚 DOCUMENTAÇÃO TÉCNICA — secon-auto + +> Sistema de automação de processos contábeis do SECON/TCDF (Tribunal de Contas do Distrito Federal). + +--- + +## 1. Visão Geral + +O `secon-auto` automatiza tarefas repetitivas do setor de contabilidade do TCDF: + +| Opção | Processo | Descrição | +|---|---|---| +| 1 | **Folha de Pagamento** | Lê demofin/PDF de relatório, gera conferência e preenche NLs no SIGGO | +| 2 | **NL Automática** | Preenche Notas de Lançamento no SIGGO automaticamente | +| 3 | **Pagamento de Diárias** | Lê NE de diárias (PDF) e preenche NL no SIGGO | +| 4 | **Extrair Dados R2000** | Lê demonstrativos de INSS (PDF), gera planilhas R-2010-1/2 para EFD-REINF | +| 5 | **Exportar Valores Pagos** | Exporta valores de NFs pagas para planilha de controle | +| 6 | **E-mails DRISS** | Divide PDF do DRISS por empresa e prepara e-mails via Outlook | +| 7 | **Cancelamento de RP** | Lê planilha de Restos a Pagar e preenche NLs de cancelamento | +| 8 | **Baixa de Diárias** | Lê CSV de adiantamentos e preenche NLs de baixa no SIGGO | + +--- + +## 2. Arquitetura — Clean Architecture + +``` +src/ +├── core/ ← REGRAS DE NEGÓCIO (sem dependências externas) +│ ├── entities/ ← Entidades de domínio (dataclasses) +│ ├── interfaces/ ← Contratos/ABCs +│ ├── parsers/ ← Extração de dados de PDFs +│ └── usecases/ ← Lógica de cada processo +│ +├── infrastructure/ ← IMPLEMENTAÇÕES CONCRETAS +│ ├── gateways/ ← Acesso a arquivos e ao SIGGO +│ └── services/ ← Excel, PDF, Outlook, WebDriver +│ +├── adapters/ ← INTERFACE COM USUÁRIO +│ └── controllers/ ← Recebem input e orquestram +│ +├── factories.py ← Injeção de dependências +├── config.py ← Configurações globais +└── main.py ← Menu principal +``` + +### Fluxo de execução + +``` +main.py → Controller → UseCaseFactory → UseCase → Gateway/Service → SIGGO / Excel / PDF +``` + +--- + +## 3. `config.py` — Configurações Globais + +| Variável | Descrição | Exemplo | +|---|---|---| +| `TESTE` | Se `True`, usa pasta `"TESTES"` em tudo | `False` | +| `ANO_ATUAL` | Ano corrente | `2025` | +| `MES_ATUAL` | Mês corrente (1–12) | `5` | +| `MES_ANTERIOR` | Mês anterior (trata virada de ano) | `4` | +| `NOME_MES_ATUAL` | Nome do mês atual | `"MAIO"` | +| `NOME_MES_ANTERIOR` | Nome do mês anterior | `"ABRIL"` | +| `PASTA_MES_ATUAL` | Pasta do mês atual | `"05-MAIO"` | +| `PASTA_MES_ANTERIOR` | Pasta do mês anterior | `"04-ABRIL"` | + +> ⚠️ Para rodar em modo de teste: `TESTE = True` em `config.py`. + +--- + +## 4. `main.py` — Menu Principal + +Exibe menu numerado no console. Usuário digita número ou `X` para sair. Instancia o Controller correspondente e captura erros sem encerrar o programa. + +--- + +## 5. `factories.py` — UseCaseFactory + +Centraliza a criação de use cases com dependências já injetadas. + +| Método | Use Case | +|---|---| +| `create_pagamento_use_case(fundo)` | `PagamentoUseCase` | +| `create_gerar_conferencia_use_case(fundo)` | `GerarConferenciaUseCase` | +| `create_preenchimento_folha_use_case(fundo)` | `PreenchimentoFolhaUseCase` | +| `create_preenchimento_nl_use_case()` | `PreenchimentoNLUseCase` | +| `create_pagamento_diaria_uc()` | `PagamentoDiariaUseCase` | +| `create_extrair_dados_r2000_uc(pasta_mes)` | `ExtrairDadosR2000UseCase` | +| `create_exportar_valores_pagos_uc()` | `ExtrairDadosR2000UseCase` | +| `create_emails_driss_uc()` | `EmailsDrissUseCase` | +| `create_cancelamento_rp_uc()` | `CancelamentoRPUseCase` | +| `create_baixa_diarias_uc()` | `BaixaDiariasUseCase` | +| `create_download_nls_uc()` | `DownloadNLsUsecase` | + +> 💡 Ao criar novo use case, adicione o método `create_X_uc()` aqui. + +--- + +## 6. Entidades (`core/entities/entities.py`) + +### `NotaLancamento` +Dados tabulares de uma NL. Valida automaticamente no `__post_init__`: +- Colunas obrigatórias: `EVENTO`, `INSCRIÇÃO`, `CLASS. CONT`, `CLASS. ORC`, `FONTE`, `VALOR` +- `EVENTO` → 6 dígitos | `CLASS. CONT` → 9 dígitos | `CLASS. ORC` → 8 dígitos + +### `TemplateNL` (herda `NotaLancamento`) +Adiciona colunas: `SOMAR`, `SUBTRAIR`, `TIPO`. + +### `CabecalhoNL` +Campos do cabeçalho da NL no SIGGO: `prioridade`, `credor`, `gestao`, `processo`, `observacao`, `contrato`. + +### `DadosPreenchimento` +Agrupa `NotaLancamento + CabecalhoNL` para passar ao `PreenchimentoGateway`. + +### `NotaEmpenho` / `ItemEmpenho` +Dados de NEs (Notas de Empenho) de diárias. + +--- + +## 7. Interfaces (`core/interfaces/`) + +| Interface | Implementações | +|---|---| +| `IExcelService` | `ExcelService` (openpyxl), `ExcelServiceWin32` (COM) | +| `IPdfService` | `PdfService` | +| `ISiggoService` | `SiggoService` | +| `IOutlookService` | `OutlookService` | +| `IPathingGateway` | `PathingGateway` | +| `IPreenchimentoGateway` | `PreenchimentoGateway` | +| `IConferenciaGateway` | `ConferenciaGateway` | +| `ITemplateFolhaGateway` | `TemplateFolhaGateway` | + +--- + +## 8. Parsers (`core/parsers/`) + +### `DemonstrativoParser.parse_dados_inss(caminho_pdf)` → `dict | None` +Extrai via Regex do PDF de guia de INSS: + +| Campo retornado | Regex buscado no PDF | +|---|---| +| `PROCESSO` | `"PROCESSO Nº : XXXX"` | +| `CNPJ` | `"CNPJ DO PRESTADOR/FORNECEDOR: ..."` | +| `VALOR_NF` | `"VALOR DA NF: R$ X.XXX,XX"` | +| `NUM_NF` | Número antes de `"Emissão:"` | +| `DATA_EMISSAO` | `"Emissão: DD/MM/YYYY"` | +| `TIPO_INSS` | `"TIPO DE SERVIÇO INSS: N"` | +| `BASE_CALCULO_INSS` | `"BASE DE CÁLCULO INSS: ..."` | +| `VALOR_INSS_RETIDO` | `"VALOR DE INSS RETIDO: ..."` | +| `CPRB` | `"CONTRIBUINTE DA CPRB? S/N"` → `1/0` | + +Retorna `None` para Pessoa Física (sem CNPJ). + +> 🔴 Esta classe é a versão antiga. A versão melhorada está em `PdfService.parse_dados_inss()`. Use via `PdfService`. + +--- + +### `FolhaPagamentoParser` + +- `parse_relatorio_folha(fundo)` → `{"PROVENTOS": DataFrame, "DESCONTOS": DataFrame}` + - Lê o PDF de relatório DEMOFIM, separa por fundo (RGPS/FINANCEIRO/CAPITALIZADO) + - Colunas: `NOME NAT`, `COD NAT`, `PROVENTO` ou `DESCONTO` + +- `parse_dados_provisoes(fundo)` → `dict` + - Lê última página do PDF (Provisionamento de Férias) + - Retorna: `{"ADICIONAL DE FÉRIAS": {"PROVISIONADO": 0.0, "REALIZADO": 0.0, "BAIXA": 0.0}, ...}` + - Benefícios: Adicional de Férias, Abono Pecuniário, 13º Salário, Licença Prêmio + +--- + +### `NotaEmpenhoParser.parser_diarias(caminho_pdf)` → `dict` +Extrai campos de PDFs de NE de diárias: `nune`, `credor`, `fonte`, `valor`, `natureza`, `subitem`. + +> ⚠️ O método `executar()` está incompleto (`...`). Apenas `parser_diarias` funciona. + +--- + +### `NotaLancamentoParser` +Extrai dados de NLs diretamente do SIGGO via Selenium (sem baixar PDF). +- `get_lista_nls()` → lê de planilha Excel (aba `lista_nls`, coluna `NUM_NL`) +- `parse_pagina_nl(num_nl)` → acessa URL SIGGO e extrai tabela por XPath + +--- + +## 9. Use Cases (`core/usecases/`) + +### `PagamentoUseCase` — Folha de Pagamento + +| Método | O que faz | +|---|---| +| `get_dados_conferencia(fundo)` | Lê DEMOFIN, filtra por fundo, cria pivot de proventos/descontos | +| `separar_proventos(df)` | Filtra CDG_NAT começando com `"3"` | +| `separar_descontos(df)` | Filtra CDG_NAT começando com `"2"` ou `"4"` | +| `gerar_saldos(...)` | Monta `{TIPO: {COD_NAT: SALDO}}` | +| `get_saldos(fundo)` | Orquestra conferência → retorna saldos finais | +| `gerar_nl_folha(template, nome, saldos, provisoes)` | Preenche template com saldos calculados → `NotaLancamento` | +| `soma_codigos(codigos_str, dicionario)` | Soma valores de múltiplos códigos de natureza | + +**Tipos de despesa**: `ATIVO`, `INATIVO`, `COMPENSATÓRIA`, `PENSIONISTA`, `ADIANTAMENTO FÉRIAS`. + +--- + +### `ExtrairDadosR2000UseCase` — EFD-REINF + +| Método | O que faz | +|---|---| +| `executar(meses)` | Itera meses, extrai INSS, gera DataFrames e exporta | +| `extrair_dados_inss(pasta_mes)` | Coleta PDFs da pasta → DataFrame | +| `gerar_dataframes_reinf(df)` | Filtra INSS retido > 0, monta R-2010-1 e R-2010-2 | +| `exportar_planilhas_r2000(df1, df2)` | Deleta linhas antigas e escreve na planilha REINF | +| `exportar_valores_pagos(meses)` | Exporta valores brutos de NFs por mês | + +**CNPJ tomador fixo**: `00534560000126` + +--- + +### `EmailsDrissUseCase` — E-mails DRISS + +| Método | O que faz | +|---|---| +| `executar()` | Orquestra todo o fluxo | +| `get_pdf_por_empresa()` | Identifica empresa em cada página do PDF via regex | +| `exportar_pdfs_driss(...)` | Salva PDF individual por empresa na pasta `ENVIADOS` | +| `encontrar_emails_empresa(nome)` | Busca e-mail na planilha `EMAIL_EMPRESAS.xlsx` (aba `E-MAIL`) | +| `preparar_envio_emails_driss(...)` | Cria rascunhos no Outlook | +| `gerar_mensagem()` | HTML com saudação dinâmica (bom dia/tarde/noite) | + +> ⚠️ `send=False, display=True` — e-mails são **abertos como rascunho**, não enviados. + +--- + +### `CancelamentoRPUseCase` — Restos a Pagar + +Lê aba `CANCELAMENTO_RP`, agrupa por processo/contrato/NE e monta: +- Evento `540032` (detalhes por NE) +- Evento `550923` (total da natureza) +- Evento `570569` (com fonte alterada: `1→3`, `2→4`, `7→8`) + +--- + +### `BaixaDiariasUseCase` — Baixa de Diárias + +Lê CSV da pasta `BAIXA_DIARIAS`, agrupa por processo e monta NL com: +- Evento fixo: `560379` +- CLASS. CONT fixo: `332110100` +- Observação: `"BAIXA DE ADIANTAMENTO DE VIAGENS..."` + +--- + +## 10. Services (`infrastructure/services/`) + +### `ExcelService` (openpyxl — sem Excel instalado) + +| Método | O que faz | +|---|---| +| `get_sheet(nome, as_dataframe)` | Retorna aba como objeto openpyxl ou DataFrame | +| `exportar_para_planilha(df, sheet, ...)` | Escreve DataFrame com formatação (cabeçalho azul, zebra) | +| `delete_rows(sheet, start, end)` | Apaga intervalo de linhas | +| `fit_columns(sheet)` | Ajusta largura das colunas | +| `copy_to(origem, destino, nome)` | **Estático** — copia arquivo | +| `save()` | Salva o workbook | + +--- + +### `PdfService` + +| Método | O que faz | +|---|---| +| `parse_dados_inss(caminho)` | Extrai dados de demonstrativo INSS (versão com normalização de espaços) | +| `get_pdfs_driss(caminho)` | Retorna páginas do DRISS (exceto última) | +| `export_pages(pages, path)` | Cria novo PDF com páginas selecionadas | +| `get_nls_baixadas(lista_nls)` | Lê PDFs de NLs em `~/Downloads/Automático` | + +--- + +### `SiggoService` + +| Método | O que faz | +|---|---| +| `inicializar(hidden)` | Inicia driver, abre SIGGO e aguarda login | +| `esperar_login(timeout=300)` | Aguarda 5 min pelo elemento `AFC` na tela | +| `preencher_campos(dict)` | Preenche campos por XPath | +| `selecionar_opcoes(dict)` | Seleciona dropdowns por XPath | +| `download_nl(num_nl)` | Baixa PDF da NL, aguarda e renomeia para `{num_nl}.pdf` | + +> ⚠️ Login é **manual** por padrão. O usuário precisa logar em até 5 minutos. + +--- + +## 11. Gateways (`infrastructure/gateways/`) + +### `PathingGateway` — Caminhos de Arquivo + +Detecta automaticamente caminho base ou OneDrive: + +| Método | Caminho | +|---|---| +| `get_caminho_raiz_secon()` | Raiz do SharePoint (`Tribunal de Contas...` ou `OneDrive - ...`) | +| `get_caminho_pasta_folha()` | `.../FOLHA_DE_PAGAMENTO_{ANO}/{MES}` | +| `get_caminho_conferencia(fundo)` | `.../CONFERÊNCIA_{fundo}.xlsx` | +| `get_caminho_template(tipo)` | `.../TEMPLATES/TEMPLATES_NL_{tipo}.xlsx` | +| `get_caminho_tabela_demofin()` | Busca `DEMOFIN*TABELA.xlsx` na pasta do mês | +| `get_caminho_pdf_relatorio()` | Busca PDF que começa com `"relatórios"` | +| `get_caminho_reinf(pasta_mes)` | `.../EFD-REINF/{pasta_mes}/Preenchimento Reinf.xlsx` | +| `get_caminhos_demonstrativos(pasta_mes)` | Todos os PDFs em `.../LIQ_DESPESA/{pasta_mes}` | +| `get_caminho_pdf_driss()` | `.../DRISS_{ANO}/{MES_ANT}/DRISS_{MM}_{ANO}.pdf` | +| `get_caminho_download_nl()` | `~/Downloads/Automático` | + +--- + +### `PreenchimentoGateway` — Automação SIGGO + +| Método | O que faz | +|---|---| +| `executar(dados, divisao_par)` | Abre nova aba por NL, preenche cabeçalho e linhas | +| `preparar_preechimento_cabecalho(cab)` | Mapeia `CabecalhoNL` para XPaths do formulário | +| `preparar_preenchimento_nl(dados)` | Clica "Incluir" e mapeia cada linha para XPaths | +| `separar_por_pagina(df, tamanho)` | Divide em chunks de 24 ou 25 linhas | +| `extrair_dados_preenchidos()` | Lê dados de volta de todas as abas abertas | + +> ⚠️ SIGGO aceita **máx. ~25 linhas por NL**. NLs grandes são divididas automaticamente. + +--- + +## 12. Exceções (`core/exceptions.py`) + +| Exceção | Quando ocorre | +|---|---| +| `LoginSiggoError` | Credenciais inválidas ou senha expirada | +| `ConexaoSiggoError` | Conexão instável com o SIGGO | +| `SiggoIndisponivelError` | Site fora do ar ou timeout | +| `ErroPreenchimentoSiggoError` | Falha ao preencher campo | +| `PlanilhaNaoEncontradaError` | Arquivo `.xlsx` não existe | +| `PlanilhaEmUsoError` | Arquivo aberto no Excel (`PermissionError`) | +| `FormatoPlanilhaInvalidoError` | Colunas obrigatórias ausentes | +| `DadosAusentesError` | Campo obrigatório vazio | +| `PdfIllegivelError` | PDF escaneado sem OCR | +| `CaminhoRedeNaoEncontradoError` | Pasta de rede inacessível (VPN) | +| `OutlookNaoConfiguradoError` | Outlook Desktop não instalado | + +--- + +## 13. Guia de Manutenção + +### Adicionar um novo processo +1. Criar `UseCase` em `src/core/usecases/` +2. Criar método `create_X_uc()` em `src/factories.py` +3. Criar `Controller` em `src/adapters/controllers/` +4. Registrar no dict `opcoes` em `src/main.py` + +### Alterar caminhos de arquivo → `pathing_gateway.py` +### Alterar campos do SIGGO → `preenchimento_gateway.py` (XPaths) +### Alterar lógica de cálculo de NLs → `pagamento_uc.py`, método `gerar_nl_folha()` + +### Troubleshooting + +| Problema | Causa | Solução | +|---|---|---| +| `PermissionError` ao abrir Excel | Planilha aberta no Excel | Fechar o arquivo | +| `FileNotFoundError` | Pasta/arquivo não existe | Verificar se pasta do mês existe | +| `TimeoutException` | Login não feito a tempo | Logar no SIGGO em até 5 min | +| `ValueError: Aba X não encontrada` | Nome da aba errado na planilha | Verificar nomes das abas | +| PDF retornando `None` | PDF de Pessoa Física (sem CNPJ) | Esperado — PF é ignorado | + +### Dependências + +| Biblioteca | Uso | +|---|---| +| `pandas` | DataFrames em todo o sistema | +| `openpyxl` | Leitura/escrita de `.xlsx` sem Excel | +| `pypdf` | Extração de texto de PDFs | +| `selenium` | Automação web do SIGGO | +| `pywin32` | Outlook/Excel via COM (Windows) | + +--- + +*Documentação gerada em 13/05/2026*