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