portal_publico/portal_api/dashboard_contabil/exportacao.py

147 lines
5.5 KiB
Python

"""Exportação em XLSX de Balancete/DRE a partir do relatório "Gerar
Dashboard" (botão "Exportar XLSX", `dashboard-contabil-relatorio.html`, ver
`ContabilApuracaoViewSet.exportar_xlsx()` em views.py) — funções puras
(openpyxl), recebem os dados já consultados/derivados pela view, nunca tocam
no ORM (mesmo espírito de `indicadores.py`/`regras.py` deste pacote)."""
from __future__ import annotations
import io
from dataclasses import dataclass
from datetime import date
from decimal import Decimal
from openpyxl import Workbook
from openpyxl.styles import Alignment, Font, PatternFill
from openpyxl.worksheet.worksheet import Worksheet
# Mesma paleta roxo/dourado já usada nos outros documentos gerados pelo
# escritório (indicadores/recibo.py, custo_contratacao/pdf.py) — nunca a
# marca "P.I.D." do Portal aqui (ver "Logos" no CLAUDE.md raiz).
COR_ROXO_ESCURO = "FF3D2178"
COR_DOURADO_CLARO = "FFF6ECD4"
COR_BRANCO = "FFFFFFFF"
FORMATO_MOEDA = '"R$" #,##0.00'
@dataclass
class LinhaBalanceteXlsx:
codigo: str
descricao: str
tipo: str # "S" (sintética) ou "A" (analítica) — mesmo campo de ContabilConta.tipo
nivel: int
saldo_anterior: Decimal
debito: Decimal
credito: Decimal
saldo_atual: Decimal
@dataclass
class LinhaDreXlsx:
descricao: str
nivel: int
totalizador: bool
valor: Decimal
def _cabecalho(ws: Worksheet, titulo: str, nome_empresa: str, cnpj: str, competencia: date, ultima_coluna: str) -> int:
"""Escreve título/empresa/CNPJ/competência nas primeiras linhas da
planilha e devolve a linha em que a tabela de dados deve começar."""
ws.merge_cells(f"A1:{ultima_coluna}1")
ws["A1"] = titulo
ws["A1"].font = Font(size=14, bold=True, color=COR_ROXO_ESCURO)
ws.merge_cells(f"A2:{ultima_coluna}2")
ws["A2"] = nome_empresa
ws["A2"].font = Font(size=11, bold=True)
ws.merge_cells(f"A3:{ultima_coluna}3")
ws["A3"] = f"CNPJ {cnpj or 'não informado'} · Competência {competencia:%m/%Y}"
ws["A3"].font = Font(size=9, italic=True)
return 5
def _escreve_cabecalho_colunas(ws: Worksheet, linha: int, titulos: list[str]) -> None:
for coluna, titulo in enumerate(titulos, start=1):
celula = ws.cell(row=linha, column=coluna, value=titulo)
celula.font = Font(bold=True, color=COR_BRANCO)
celula.fill = PatternFill("solid", fgColor=COR_ROXO_ESCURO)
celula.alignment = Alignment(horizontal="left" if coluna <= 2 else "center", vertical="center")
def gera_xlsx_balancete(nome_empresa: str, cnpj: str, competencia: date, linhas: list[LinhaBalanceteXlsx]) -> bytes:
wb = Workbook()
ws = wb.active
ws.title = "Balancete"
linha_inicial = _cabecalho(ws, "Balancete Contábil", nome_empresa, cnpj, competencia, "F")
_escreve_cabecalho_colunas(
ws, linha_inicial, ["Conta", "Descrição", "Saldo Anterior", "Débito", "Crédito", "Saldo Atual"]
)
largura_descricao = 30
for indice, item in enumerate(linhas):
linha = linha_inicial + 1 + indice
negrito = item.tipo == "S" # conta sintética — mesmo destaque visual do PDF original e da tela de revisão
ws.cell(row=linha, column=1, value=item.codigo).font = Font(bold=negrito, size=9)
celula_descricao = ws.cell(row=linha, column=2, value=item.descricao)
celula_descricao.font = Font(bold=negrito)
celula_descricao.alignment = Alignment(indent=item.nivel)
largura_descricao = max(largura_descricao, len(item.descricao) + item.nivel + 2)
for coluna, valor in zip((3, 4, 5, 6), (item.saldo_anterior, item.debito, item.credito, item.saldo_atual)):
celula = ws.cell(row=linha, column=coluna, value=float(valor))
celula.number_format = FORMATO_MOEDA
celula.font = Font(bold=negrito)
if negrito:
for coluna in range(1, 7):
ws.cell(row=linha, column=coluna).fill = PatternFill("solid", fgColor=COR_DOURADO_CLARO)
ws.freeze_panes = f"A{linha_inicial + 1}"
ws.column_dimensions["A"].width = 16
ws.column_dimensions["B"].width = min(largura_descricao, 60)
for coluna in "CDEF":
ws.column_dimensions[coluna].width = 16
buffer = io.BytesIO()
wb.save(buffer)
return buffer.getvalue()
def gera_xlsx_dre(nome_empresa: str, cnpj: str, competencia: date, linhas: list[LinhaDreXlsx]) -> bytes:
wb = Workbook()
ws = wb.active
ws.title = "DRE"
linha_inicial = _cabecalho(ws, "Demonstração do Resultado do Exercício", nome_empresa, cnpj, competencia, "B")
_escreve_cabecalho_colunas(ws, linha_inicial, ["Descrição", "Valor"])
largura_descricao = 30
for indice, item in enumerate(linhas):
linha = linha_inicial + 1 + indice
celula_descricao = ws.cell(row=linha, column=1, value=item.descricao)
celula_descricao.font = Font(bold=item.totalizador)
celula_descricao.alignment = Alignment(indent=item.nivel)
largura_descricao = max(largura_descricao, len(item.descricao) + item.nivel + 2)
celula_valor = ws.cell(row=linha, column=2, value=float(item.valor))
celula_valor.number_format = FORMATO_MOEDA
celula_valor.font = Font(bold=item.totalizador)
if item.totalizador:
for coluna in (1, 2):
ws.cell(row=linha, column=coluna).fill = PatternFill("solid", fgColor=COR_DOURADO_CLARO)
ws.freeze_panes = f"A{linha_inicial + 1}"
ws.column_dimensions["A"].width = min(largura_descricao, 60)
ws.column_dimensions["B"].width = 18
buffer = io.BytesIO()
wb.save(buffer)
return buffer.getvalue()