147 lines
5.5 KiB
Python
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()
|