portal_publico/portal_api/indicadores/leiaute.py

121 lines
4.7 KiB
Python

"""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()