294 lines
12 KiB
Python
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()
|