portal_publico/portal_api/custo_contratacao/pdf.py

294 lines
12 KiB
Python

"""Geração do PDF de "Simulação de Custo de Contratação" — identidade visual
do escritório (dourado/marrom do logo, cores extraídas de `static/img/logo.png`),
pronto para ser enviado ao cliente."""
import io
from pathlib import Path
from PIL import Image as PILImage
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import SimpleDocTemplate, Spacer, Table, TableStyle
from .calculo import ResultadoSimulacaoEmpregado, formata_moeda, formata_percentual
_LOGO_BRANCO_PATH = Path(__file__).resolve().parents[2] / "static" / "img" / "logo-branco.png"
# Cores extraídas do próprio static/img/logo.png (pixel do "D" e da sombra do
# degradê) — a identidade visual real do escritório, não o roxo do tema do
# Portal (que é só a cor de interface escolhida pelo usuário logado).
_COR_GOLD = colors.HexColor("#D3AF4D")
_COR_GOLD_ESCURO = colors.HexColor("#A98C3E")
_COR_MARROM = colors.HexColor("#4A3C28")
_COR_GOLD_CLARO_BANNER = colors.HexColor("#CBB98C")
_COR_TEXTO = colors.HexColor("#2A2118")
_COR_TEXTO_MUTED = colors.HexColor("#7A6F5D")
_COR_LINHA = colors.HexColor("#E7DEC7")
_COR_ZEBRA = colors.HexColor("#FAF6EC")
_COR_SUBTOTAL_BG = colors.HexColor("#F0E0B0")
_LARGURA_PAGINA, _ALTURA_PAGINA = A4
_MARGEM = 1.8 * cm
_ALTURA_BANNER = 3.1 * cm
_LARGURA_COL_ROTULO = 11 * cm
_LARGURA_COL_VALOR = 4.5 * cm
def _dimensoes_imagem(caminho: Path, largura_pt: float) -> tuple[float, float]:
with PILImage.open(caminho) as img:
largura_px, altura_px = img.size
return largura_pt, largura_pt * (altura_px / largura_px)
def _linha_identificacao(nome_referencia: str, empresa: str, cargo: str) -> str:
partes = [parte for parte in (nome_referencia, empresa, cargo) if parte]
return " — ".join(partes) if partes else "De Paula Contadores"
def _desenha_banner(canvas: Canvas, nome_referencia: str, empresa: str, cargo: str, gerado_em: str) -> None:
topo = _ALTURA_PAGINA
canvas.setFillColor(_COR_MARROM)
canvas.rect(0, topo - _ALTURA_BANNER, _LARGURA_PAGINA, _ALTURA_BANNER, stroke=0, fill=1)
canvas.setFillColor(_COR_GOLD)
canvas.rect(0, topo - _ALTURA_BANNER - 0.1 * cm, _LARGURA_PAGINA, 0.1 * cm, stroke=0, fill=1)
logo_largura, logo_altura = _dimensoes_imagem(_LOGO_BRANCO_PATH, 3.4 * cm)
logo_y = topo - _ALTURA_BANNER / 2 - logo_altura / 2
canvas.drawImage(
str(_LOGO_BRANCO_PATH), _MARGEM, logo_y, width=logo_largura, height=logo_altura, mask="auto"
)
texto_x = _MARGEM + logo_largura + 0.7 * cm
centro_y = topo - _ALTURA_BANNER / 2
canvas.setFillColor(colors.white)
canvas.setFont("Helvetica-Bold", 17)
canvas.drawString(texto_x, centro_y + 0.22 * cm, "Simulação de Custo de Contratação")
canvas.setFont("Helvetica", 10.5)
canvas.setFillColor(_COR_GOLD)
canvas.drawString(texto_x, centro_y - 0.32 * cm, _linha_identificacao(nome_referencia, empresa, cargo))
canvas.setFont("Helvetica", 8.5)
canvas.setFillColor(_COR_GOLD_CLARO_BANNER)
canvas.drawString(texto_x, centro_y - 0.72 * cm, f"Gerado em {gerado_em}")
def _desenha_rodape(canvas: Canvas) -> None:
canvas.setStrokeColor(_COR_GOLD_ESCURO)
canvas.setLineWidth(0.6)
canvas.line(_MARGEM, 1.55 * cm, _LARGURA_PAGINA - _MARGEM, 1.55 * cm)
canvas.setFont("Helvetica", 7.5)
canvas.setFillColor(_COR_TEXTO_MUTED)
canvas.drawCentredString(
_LARGURA_PAGINA / 2,
1.15 * cm,
"De Paula Contadores · Simulação de caráter estimativo, sujeita à confirmação",
)
def _pagina(
canvas: Canvas, doc: SimpleDocTemplate, nome_referencia: str, empresa: str, cargo: str, gerado_em: str
) -> None:
canvas.saveState()
_desenha_banner(canvas, nome_referencia, empresa, cargo, gerado_em)
_desenha_rodape(canvas)
canvas.setFont("Helvetica", 7.5)
canvas.setFillColor(_COR_TEXTO_MUTED)
canvas.drawRightString(_LARGURA_PAGINA - _MARGEM, 1.15 * cm, f"Página {doc.page}")
canvas.restoreState()
def _tabela_secao(
titulo: str,
linhas: list[tuple[str, str]],
indices_subtotal: frozenset[int] = frozenset(),
indices_total: frozenset[int] = frozenset(),
) -> Table:
"""Uma "seção" é uma única Table: a primeira linha é a barra de título
(dourada, colspan completo) e as demais são os pares rótulo/valor —
dá o efeito de card com cabeçalho colorido sem precisar de um Paragraph
separado antes da tabela."""
dados = [[titulo, ""]] + [[rotulo, valor] for rotulo, valor in linhas]
tabela = Table(dados, colWidths=[_LARGURA_COL_ROTULO, _LARGURA_COL_VALOR])
estilo = [
("SPAN", (0, 0), (-1, 0)),
("BACKGROUND", (0, 0), (-1, 0), _COR_GOLD),
("TEXTCOLOR", (0, 0), (-1, 0), _COR_MARROM),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, 0), 10.5),
("TOPPADDING", (0, 0), (-1, 0), 7),
("BOTTOMPADDING", (0, 0), (-1, 0), 7),
("LEFTPADDING", (0, 0), (-1, -1), 10),
("RIGHTPADDING", (0, 0), (-1, -1), 10),
("FONTNAME", (0, 1), (-1, -1), "Helvetica"),
("FONTSIZE", (0, 1), (-1, -1), 9.5),
("TEXTCOLOR", (0, 1), (-1, -1), _COR_TEXTO),
("ALIGN", (1, 0), (1, -1), "RIGHT"),
("LINEBELOW", (0, 1), (-1, -2), 0.5, _COR_LINHA),
("TOPPADDING", (0, 1), (-1, -1), 4.5),
("BOTTOMPADDING", (0, 1), (-1, -1), 4.5),
("BOX", (0, 0), (-1, -1), 0.75, _COR_GOLD_ESCURO),
]
linhas_especiais = {indice + 1 for indice in (indices_subtotal | indices_total)}
for i in range(1, len(dados)):
if i not in linhas_especiais and (i % 2 == 0):
estilo.append(("BACKGROUND", (0, i), (-1, i), _COR_ZEBRA))
for indice in indices_subtotal:
i = indice + 1
estilo += [
("BACKGROUND", (0, i), (-1, i), _COR_SUBTOTAL_BG),
("FONTNAME", (0, i), (-1, i), "Helvetica-Bold"),
]
for indice in indices_total:
i = indice + 1
estilo += [
("BACKGROUND", (0, i), (-1, i), _COR_MARROM),
("TEXTCOLOR", (0, i), (-1, i), colors.white),
("FONTNAME", (0, i), (-1, i), "Helvetica-Bold"),
("FONTSIZE", (0, i), (-1, i), 10.5),
("TOPPADDING", (0, i), (-1, i), 7),
("BOTTOMPADDING", (0, i), (-1, i), 7),
]
tabela.setStyle(TableStyle(estilo))
return tabela
def _secao_dados_informados(resultado: ResultadoSimulacaoEmpregado) -> Table:
entrada = resultado.entrada
linhas = [
("Salário contratual", formata_moeda(entrada.salario_contratual)),
("Adicional Art. 62.", formata_moeda(entrada.adicional_art62)),
("Comissão", formata_moeda(entrada.comissao)),
("DSR", formata_moeda(entrada.dsr)),
]
linhas += [(nome, formata_moeda(valor)) for nome, valor in entrada.outros_itens_remuneracao]
linhas += [
("% de INSS Empregador", formata_percentual(entrada.percentual_inss_empregador)),
("% de FGTS", formata_percentual(entrada.percentual_fgts)),
("Dependentes menores de 21 anos", str(entrada.dependentes)),
("Vale Transporte", formata_moeda(entrada.vale_transporte)),
("Seguro de vida", formata_moeda(entrada.seguro_vida)),
("Vale Alimentação", formata_moeda(entrada.vale_alimentacao)),
("Assiduidade", formata_moeda(entrada.assiduidade)),
("Fundo de Formação", formata_moeda(entrada.fundo_formacao)),
]
linhas += [(nome, formata_moeda(valor)) for nome, valor in entrada.outros_beneficios]
linhas += [
("Desconto Vale Transporte", formata_moeda(entrada.desconto_vale_transporte)),
("Desconto Vale Alimentação", formata_moeda(entrada.desconto_vale_alimentacao)),
("Desconto Sindicato", formata_moeda(entrada.desconto_sindicato)),
]
linhas += [(nome, formata_moeda(valor)) for nome, valor in entrada.outros_descontos]
return _tabela_secao("DADOS INFORMADOS", linhas)
def _secao_custo_empregador(resultado: ResultadoSimulacaoEmpregado) -> Table:
entrada = resultado.entrada
linhas = [
("Salário", formata_moeda(entrada.salario_contratual)),
("Adicional Art. 62.", formata_moeda(entrada.adicional_art62)),
("Comissão", formata_moeda(entrada.comissao)),
("DSR", formata_moeda(entrada.dsr)),
]
linhas += [(nome, formata_moeda(valor)) for nome, valor in entrada.outros_itens_remuneracao]
linhas += [
("INSS s/ salário", formata_moeda(resultado.inss_salario)),
("FGTS s/ salário", formata_moeda(resultado.fgts_salario)),
("13º salário (1/12)", formata_moeda(resultado.decimo_terceiro_avo)),
("INSS s/ 13º", formata_moeda(resultado.inss_decimo_terceiro)),
("FGTS s/ 13º", formata_moeda(resultado.fgts_decimo_terceiro)),
("Férias (1/12)", formata_moeda(resultado.ferias_avo)),
("1/3 Constitucional", formata_moeda(resultado.ferias_um_terco)),
("INSS s/ férias", formata_moeda(resultado.inss_ferias)),
("FGTS s/ férias", formata_moeda(resultado.fgts_ferias)),
("Multa 40% FGTS", formata_moeda(resultado.multa_40_fgts)),
]
indice_subtotal_fixos = len(linhas)
linhas.append(("Subtotal Custos Fixos", formata_moeda(resultado.subtotal_custos_fixos)))
linhas += [
("Vale Transporte", formata_moeda(entrada.vale_transporte)),
("Seguro de vida", formata_moeda(entrada.seguro_vida)),
("Vale Alimentação", formata_moeda(entrada.vale_alimentacao)),
("Assiduidade", formata_moeda(entrada.assiduidade)),
("Fundo de Formação", formata_moeda(entrada.fundo_formacao)),
]
linhas += [(nome, formata_moeda(valor)) for nome, valor in entrada.outros_beneficios]
indice_subtotal_variaveis = len(linhas)
linhas.append(("Subtotal Custos Variáveis", formata_moeda(resultado.subtotal_custos_variaveis)))
indice_total = len(linhas)
linhas.append(("Custo Total Empresa", formata_moeda(resultado.custo_total_empresa)))
return _tabela_secao(
"CUSTO PARA O EMPREGADOR",
linhas,
indices_subtotal=frozenset({indice_subtotal_fixos, indice_subtotal_variaveis}),
indices_total=frozenset({indice_total}),
)
def _secao_liquido_empregado(resultado: ResultadoSimulacaoEmpregado) -> Table:
entrada = resultado.entrada
linhas = [
("Salário Bruto", formata_moeda(entrada.salario_contratual)),
("Adicional Art. 62.", formata_moeda(entrada.adicional_art62)),
("Comissão", formata_moeda(entrada.comissao)),
("DSR", formata_moeda(entrada.dsr)),
]
linhas += [(nome, formata_moeda(valor)) for nome, valor in entrada.outros_itens_remuneracao]
linhas += [
("Base INSS", formata_moeda(resultado.base_inss_empregado)),
("Alíquota INSS", formata_percentual(resultado.aliquota_inss_empregado)),
("Desconto INSS", formata_moeda(resultado.desconto_inss_empregado)),
("Base IRRF", formata_moeda(resultado.base_irrf)),
("Alíquota IRRF", formata_percentual(resultado.aliquota_irrf)),
("Desconto IRRF", formata_moeda(resultado.desconto_irrf)),
("Desconto Vale Transporte", formata_moeda(entrada.desconto_vale_transporte)),
("Desconto Vale Alimentação", formata_moeda(entrada.desconto_vale_alimentacao)),
("Desconto Sindicato", formata_moeda(entrada.desconto_sindicato)),
]
linhas += [(nome, formata_moeda(valor)) for nome, valor in entrada.outros_descontos]
indice_total = len(linhas)
linhas.append(("Valor Líquido do Empregado", formata_moeda(resultado.liquido_empregado)))
return _tabela_secao("VALOR LÍQUIDO DO EMPREGADO", linhas, indices_total=frozenset({indice_total}))
def gera_pdf_simulacao(resultado: ResultadoSimulacaoEmpregado, gerado_em: str) -> bytes:
buffer = io.BytesIO()
doc = SimpleDocTemplate(
buffer,
pagesize=A4,
topMargin=_ALTURA_BANNER + 0.9 * cm,
bottomMargin=2.1 * cm,
leftMargin=_MARGEM,
rightMargin=_MARGEM,
title="Simulação de Custo de Contratação",
)
story: list = [
_secao_dados_informados(resultado),
Spacer(1, 0.5 * cm),
_secao_custo_empregador(resultado),
Spacer(1, 0.5 * cm),
_secao_liquido_empregado(resultado),
]
def _desenha_pagina(canvas: Canvas, doc: SimpleDocTemplate) -> None:
_pagina(canvas, doc, resultado.entrada.nome_referencia, resultado.entrada.empresa, resultado.entrada.cargo, gerado_em)
doc.build(story, onFirstPage=_desenha_pagina, onLaterPages=_desenha_pagina)
return buffer.getvalue()