"""Leitura das duas planilhas mensais do Indicador de Desempenho (Fiscontábil): "Serviços Tareffa" (uma linha por serviço executado) e "Honorários Por Cliente" (honorário líquido por empresa). Pacote Python puro, sem depender do ORM — mesmo espírito de `portal_api.planos_saude`.""" from __future__ import annotations import datetime as dt from dataclasses import dataclass from decimal import Decimal, InvalidOperation import openpyxl SERVICOS_BALANCETE = {"Balancete Mensal", "Balancete Bimestral"} SERVICO_LIBERACAO_FISCAL = "Liberação Fiscal" SERVICO_CONCILIACAO_FINANCEIRA = "Conciliação Financeira" SERVICOS_INDICADOR = SERVICOS_BALANCETE | {SERVICO_LIBERACAO_FISCAL, SERVICO_CONCILIACAO_FINANCEIRA} STATUS_CANCELADO = "Cancelado" @dataclass class LinhaTareffa: codigo_empresa: str razao_social: str responsavel: str gerente: str servico: str status: str data_meta: dt.date | None data_entrega: dt.date | None def _texto(valor: object) -> str: return str(valor).strip() if valor is not None else "" def _codigo(valor: object) -> str: """Normaliza um código de empresa pra string, tratando o caso comum de o Excel/openpyxl devolver um float "inteiro" (ex.: 792.0) — sem isso, o mesmo código viria como "792" de uma planilha e "792.0" de outra, e a junção por código nunca bateria.""" if isinstance(valor, float) and valor.is_integer(): return str(int(valor)) return _texto(valor) def _para_data(valor: object) -> dt.date | None: if isinstance(valor, dt.datetime): return valor.date() if isinstance(valor, dt.date): return valor return None def _indice_cabecalho(planilha) -> dict[str, int]: primeira_linha = next(planilha.iter_rows(min_row=1, max_row=1, values_only=True)) return {_texto(nome): posicao for posicao, nome in enumerate(primeira_linha)} def le_servicos_tareffa(caminho: str) -> list[LinhaTareffa]: """Lê só as linhas relevantes pro Indicador de Desempenho do Fiscontábil (Balancete/Liberação Fiscal/Conciliação Financeira) — outros serviços (ex.: departamentos "Rocket"/"Condomínio"/"Pessoa Física IRPF", fora do escopo desta v1) são ignorados aqui, não em cada consumidor.""" workbook = openpyxl.load_workbook(caminho, data_only=True, read_only=True) try: planilha = workbook.worksheets[0] indice = _indice_cabecalho(planilha) linhas: list[LinhaTareffa] = [] for linha_planilha in planilha.iter_rows(min_row=2, values_only=True): servico = _texto(linha_planilha[indice["servico"]]) if servico not in SERVICOS_INDICADOR: continue linhas.append( LinhaTareffa( codigo_empresa=_codigo(linha_planilha[indice["codigoempresa"]]), razao_social=_texto(linha_planilha[indice["razaosocial"]]), responsavel=_texto(linha_planilha[indice["responsavel"]]), gerente=_texto(linha_planilha[indice["gerente"]]), servico=servico, status=_texto(linha_planilha[indice["status"]]), data_meta=_para_data(linha_planilha[indice["datameta"]]), data_entrega=_para_data(linha_planilha[indice["dataentrega"]]), ) ) return linhas finally: # Modo read_only do openpyxl mantém o arquivo aberto (memory-mapped) # até `close()` ser chamado — sem isso, o Windows bloqueia qualquer # tentativa de apagar o upload depois (ex.: excluir a apuração). workbook.close() def le_honorarios(caminho: str) -> dict[str, Decimal]: """Devolve {código da empresa: honorário}, somando quando o mesmo código aparecer em mais de uma linha (mais de um serviço faturado pro mesmo cliente no mês).""" workbook = openpyxl.load_workbook(caminho, data_only=True, read_only=True) try: planilha = workbook.worksheets[0] indice = _indice_cabecalho(planilha) coluna_codigo = indice["Cód"] coluna_valor = indice["Total Líquido"] honorarios: dict[str, Decimal] = {} for linha_planilha in planilha.iter_rows(min_row=2, values_only=True): codigo = _codigo(linha_planilha[coluna_codigo]) if not codigo: continue valor_bruto = linha_planilha[coluna_valor] try: valor = Decimal(str(valor_bruto)) if valor_bruto is not None else Decimal("0") except InvalidOperation: valor = Decimal("0") honorarios[codigo] = honorarios.get(codigo, Decimal("0")) + valor return honorarios finally: workbook.close()