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