473 lines
21 KiB
Python
473 lines
21 KiB
Python
"""Geração do recibo em PDF de um colaborador, a partir dos dados já
|
||
calculados/revisados (não reprocessa nada — só formata o que está salvo).
|
||
Layout inspirado na planilha antiga do escritório (ver anexo do usuário),
|
||
com a identidade visual do Portal (logo + paleta roxo/dourado, ver
|
||
tokens.css) — mesma paleta do tema claro, já que o PDF é sempre impresso
|
||
em fundo branco."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import calendar
|
||
import io
|
||
import threading
|
||
from decimal import Decimal
|
||
|
||
from django.conf import settings
|
||
from PIL import Image as PILImage
|
||
from reportlab.lib import colors
|
||
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
|
||
from reportlab.lib.pagesizes import A4
|
||
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
||
from reportlab.lib.units import cm
|
||
from reportlab.platypus import Image, KeepTogether, Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
|
||
|
||
from portal_api.indicadores.calculo import composicao_individual, percentual_vigente
|
||
from portal_api.models import (
|
||
RESPOSTA_CRITERIO_INDICADOR_CHOICES,
|
||
IndicadorApuracao,
|
||
IndicadorApuracaoColaborador,
|
||
IndicadorCriterio,
|
||
TIPO_COLABORADOR_INDICADOR_CHOICES,
|
||
)
|
||
|
||
# Versão curta só pra coluna "Tipo" da tabela de Empresas — o label completo
|
||
# ("Contador (com conciliador)") não cabe na coluna estreita mesmo já
|
||
# quebrando linha (Paragraph); mesma abreviação "Contador SC/CC" já usada
|
||
# informalmente pra esses dois tipos em CLAUDE.md.
|
||
TIPO_LABEL_CURTO = {
|
||
"1": "Contábil+Fiscal",
|
||
"2": "Contador SC",
|
||
"3": "Contador CC",
|
||
"4": "Fiscal",
|
||
"5": "Conciliador",
|
||
}
|
||
RESPOSTA_LABEL = dict(RESPOSTA_CRITERIO_INDICADOR_CHOICES)
|
||
PERIODO_LABEL = {
|
||
IndicadorCriterio.PERIODO_TODOS: "Todos os meses",
|
||
IndicadorCriterio.PERIODO_MAR_A_NOV: "Março a Novembro",
|
||
IndicadorCriterio.PERIODO_DEZ_A_JAN: "Dezembro a Janeiro",
|
||
}
|
||
MES_LABEL = {
|
||
1: "janeiro", 2: "fevereiro", 3: "março", 4: "abril", 5: "maio", 6: "junho",
|
||
7: "julho", 8: "agosto", 9: "setembro", 10: "outubro", 11: "novembro", 12: "dezembro",
|
||
}
|
||
CIDADE_ESCRITORIO = "Foz do Iguaçu"
|
||
LOGO_PATH = settings.BASE_DIR / "static" / "img" / "logo.png"
|
||
|
||
# `logo.png` é 6250×4419px (~480KB) — pensado pra tela, não pra um logo de
|
||
# ~3,6cm impresso num PDF. Embutir o arquivo original em cada recibo (um ZIP
|
||
# chega a ter 40+ colaboradores) inflava o ZIP pra ~19MB e deixava o download
|
||
# lento/travado no navegador. Redimensiona uma vez por processo (cache em
|
||
# memória, protegido por lock — vários recibos podem ser gerados em paralelo
|
||
# dentro do mesmo worker) pra um tamanho generoso pra impressão (600px de
|
||
# largura, ~423 DPI num logo de 3,6cm) e reaproveita os bytes já reduzidos em
|
||
# todos os recibos daquele ZIP.
|
||
_logo_bytes_cache: bytes | None = None
|
||
_logo_bytes_lock = threading.Lock()
|
||
|
||
|
||
def _logo_bytes() -> bytes | None:
|
||
global _logo_bytes_cache
|
||
if _logo_bytes_cache is not None:
|
||
return _logo_bytes_cache or None
|
||
with _logo_bytes_lock:
|
||
if _logo_bytes_cache is None:
|
||
try:
|
||
imagem = PILImage.open(LOGO_PATH)
|
||
imagem.thumbnail((600, 600))
|
||
buffer = io.BytesIO()
|
||
imagem.save(buffer, format="PNG", optimize=True)
|
||
_logo_bytes_cache = buffer.getvalue()
|
||
except Exception:
|
||
_logo_bytes_cache = b""
|
||
return _logo_bytes_cache or None
|
||
|
||
# Paleta do escritório (mesmos valores do tema claro em tokens.css) — o PDF é
|
||
# sempre impresso/visualizado em fundo branco, então usa as cores pensadas
|
||
# pra contraste em fundo claro, não as do tema escuro (padrão da tela).
|
||
COR_ROXO_ESCURO = colors.HexColor("#3d2178")
|
||
COR_ROXO_CLARO = colors.HexColor("#efe8ff")
|
||
COR_DOURADO = colors.HexColor("#b4872a")
|
||
COR_DOURADO_CLARO = colors.HexColor("#f6ecd4")
|
||
COR_TEXTO = colors.HexColor("#211d2b")
|
||
COR_TEXTO_MUTED = colors.HexColor("#5f5a6d")
|
||
COR_BORDA = colors.HexColor("#e4e0ec")
|
||
COR_SUCESSO_BG = colors.HexColor("#dcf5e3")
|
||
COR_SUCESSO_TEXTO = colors.HexColor("#1a7a3d")
|
||
COR_FALHA_BG = colors.HexColor("#fbe2e2")
|
||
COR_FALHA_TEXTO = colors.HexColor("#b3261e")
|
||
COR_NEUTRO_BG = colors.HexColor("#ece9f2")
|
||
COR_NEUTRO_TEXTO = colors.HexColor("#5f5a6d")
|
||
|
||
COR_ATINGIDO_BG = {
|
||
"SIM": COR_SUCESSO_BG,
|
||
"NAO": COR_FALHA_BG,
|
||
"NAO_FAZ": COR_FALHA_BG,
|
||
"NAO_SE_APLICA": COR_NEUTRO_BG,
|
||
}
|
||
COR_ATINGIDO_TEXTO = {
|
||
"SIM": COR_SUCESSO_TEXTO,
|
||
"NAO": COR_FALHA_TEXTO,
|
||
"NAO_FAZ": COR_FALHA_TEXTO,
|
||
"NAO_SE_APLICA": COR_NEUTRO_TEXTO,
|
||
}
|
||
|
||
|
||
def _moeda(valor: Decimal) -> str:
|
||
# ` ` (não `" "`) entre "R$" e o número — todo valor aqui vira
|
||
# Paragraph numa coluna estreita, e um espaço normal é um ponto de quebra
|
||
# de linha válido pro word-wrap do reportlab: com espaço comum, um valor
|
||
# que não coubesse numa linha só quebrava bem ali, deixando "R$" sozinho
|
||
# numa linha acima do número. Não-breaking space mantém os dois sempre
|
||
# juntos, movendo a quebra (se precisar) pra dentro do próprio número.
|
||
texto = f"{valor:,.2f}"
|
||
return "R$ " + texto.replace(",", "_").replace(".", ",").replace("_", ".")
|
||
|
||
|
||
def _percentual(valor: Decimal) -> str:
|
||
texto = f"{Decimal(valor):,.2f}"
|
||
return texto.replace(",", "_").replace(".", ",").replace("_", ".") + "%"
|
||
|
||
|
||
def _respostas_aplicaveis(colaborador: IndicadorApuracaoColaborador, periodo_atual: str) -> list:
|
||
"""Mesma regra de aplicabilidade de `calculo._respostas_do_grupo` (período
|
||
da competência + papel do colaborador pros critérios Individual) — só que
|
||
aqui olhando os 3 níveis juntos (Individual/Grupo/Departamento), pra
|
||
reproduzir o agrupamento por período da planilha antiga (ver docstring do
|
||
módulo)."""
|
||
tipos_do_colaborador = {empresa.tipo for empresa in colaborador.empresas.all()}
|
||
aplicaveis = []
|
||
for resposta in colaborador.respostas.all():
|
||
criterio = resposta.criterio
|
||
if criterio.periodo not in (IndicadorCriterio.PERIODO_TODOS, periodo_atual):
|
||
continue
|
||
if criterio.grupo == IndicadorCriterio.GRUPO_INDIVIDUAL and criterio.papel_aplicavel:
|
||
if criterio.papel_aplicavel not in tipos_do_colaborador:
|
||
continue
|
||
aplicaveis.append(resposta)
|
||
return aplicaveis
|
||
|
||
|
||
def _agrupa_por_periodo(respostas: list) -> list[tuple[str, list]]:
|
||
""""Todos os meses" sempre primeiro, depois o período sazonal ativo —
|
||
mesma ordem da planilha antiga."""
|
||
grupos: dict[str, list] = {}
|
||
for resposta in respostas:
|
||
grupos.setdefault(resposta.criterio.periodo, []).append(resposta)
|
||
ordem = [IndicadorCriterio.PERIODO_TODOS] + [
|
||
periodo for periodo in grupos if periodo != IndicadorCriterio.PERIODO_TODOS
|
||
]
|
||
return [(periodo, grupos[periodo]) for periodo in ordem if periodo in grupos]
|
||
|
||
|
||
def _estilos():
|
||
base = getSampleStyleSheet()
|
||
return {
|
||
"titulo": ParagraphStyle(
|
||
"ind_titulo", parent=base["Heading1"], alignment=TA_LEFT, fontSize=13,
|
||
textColor=COR_ROXO_ESCURO, spaceAfter=2,
|
||
),
|
||
"subtitulo": ParagraphStyle(
|
||
"ind_subtitulo", parent=base["Normal"], alignment=TA_LEFT, fontSize=9, textColor=COR_TEXTO_MUTED,
|
||
),
|
||
"normal": ParagraphStyle("ind_normal", parent=base["Normal"], fontSize=9, textColor=COR_TEXTO),
|
||
"secao": ParagraphStyle(
|
||
"ind_secao", parent=base["Heading2"], fontSize=10.5, textColor=COR_ROXO_ESCURO,
|
||
spaceBefore=10, spaceAfter=4,
|
||
),
|
||
"banner_label": ParagraphStyle(
|
||
"ind_banner_label", parent=base["Normal"], fontSize=9, textColor=colors.white, alignment=TA_LEFT,
|
||
),
|
||
"banner_valor": ParagraphStyle(
|
||
"ind_banner_valor", parent=base["Normal"], fontSize=20, textColor=colors.white,
|
||
fontName="Helvetica-Bold", alignment=TA_RIGHT,
|
||
),
|
||
"rodape": ParagraphStyle("ind_rodape", parent=base["Normal"], fontSize=9, alignment=TA_CENTER, textColor=COR_TEXTO),
|
||
"celula": ParagraphStyle("ind_celula", parent=base["Normal"], fontSize=7.2, textColor=COR_TEXTO, leading=8.6),
|
||
"celula_centro": ParagraphStyle(
|
||
"ind_celula_centro", parent=base["Normal"], fontSize=7.2, leading=8.6,
|
||
textColor=COR_TEXTO, alignment=TA_CENTER,
|
||
),
|
||
"celula_direita": ParagraphStyle(
|
||
"ind_celula_direita", parent=base["Normal"], fontSize=7.2, leading=8.6,
|
||
textColor=COR_TEXTO, alignment=TA_RIGHT,
|
||
),
|
||
"celula_negrito": ParagraphStyle(
|
||
"ind_celula_negrito", parent=base["Normal"], fontSize=7.6, leading=9,
|
||
textColor=COR_TEXTO, fontName="Helvetica-Bold",
|
||
),
|
||
"celula_direita_negrito": ParagraphStyle(
|
||
"ind_celula_direita_negrito", parent=base["Normal"], fontSize=7.6, leading=9,
|
||
textColor=COR_TEXTO, fontName="Helvetica-Bold", alignment=TA_RIGHT,
|
||
),
|
||
"cabecalho_tabela": ParagraphStyle(
|
||
"ind_cabecalho_tabela", parent=base["Normal"], fontSize=7.2, leading=8.4,
|
||
textColor=colors.white, fontName="Helvetica-Bold", alignment=TA_CENTER,
|
||
),
|
||
}
|
||
|
||
|
||
def _cabecalho(apuracao: IndicadorApuracao, estilos: dict) -> Table:
|
||
logo_bytes = _logo_bytes()
|
||
if logo_bytes:
|
||
logo = Image(io.BytesIO(logo_bytes), width=3.6 * cm, height=3.6 * cm / 1.41)
|
||
else:
|
||
logo = Paragraph("De Paula Contadores", estilos["titulo"])
|
||
|
||
titulo = [
|
||
Paragraph("PLANILHA DE APURAÇÃO DO INDICADOR DE DESEMPENHO", estilos["titulo"]),
|
||
Paragraph("Fiscontábil — Geradoc", estilos["subtitulo"]),
|
||
Paragraph(f"Competência: {apuracao.competencia:%m/%Y}", estilos["subtitulo"]),
|
||
]
|
||
tabela = Table([[logo, titulo]], colWidths=[4 * cm, None])
|
||
tabela.setStyle(
|
||
TableStyle(
|
||
[
|
||
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||
("ALIGN", (0, 0), (0, 0), "LEFT"),
|
||
("LINEBELOW", (0, 0), (-1, 0), 1.5, COR_DOURADO),
|
||
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
|
||
]
|
||
)
|
||
)
|
||
return tabela
|
||
|
||
|
||
def _banner_percentual(colaborador: IndicadorApuracaoColaborador, estilos: dict) -> Table:
|
||
# O banner sempre mostra o percentual **efetivo/medido** (a composição
|
||
# dos 3 níveis), independente de qualquer ajuste manual — o valor que a
|
||
# Direção decidiu pagar (quando diferente) aparece à parte, na linha de
|
||
# detalhe abaixo, pra deixar claro os dois números: o que o colaborador
|
||
# atingiu e o que foi de fato considerado.
|
||
composicao = composicao_individual(colaborador)
|
||
percentual_medido = composicao["total_calculado"]
|
||
|
||
linha_titulo = Table(
|
||
[
|
||
[
|
||
Paragraph("PERCENTUAL DO INDICADOR INDIVIDUAL", estilos["banner_label"]),
|
||
Paragraph(_percentual(percentual_medido), estilos["banner_valor"]),
|
||
]
|
||
],
|
||
colWidths=[None, 5 * cm],
|
||
)
|
||
linha_titulo.setStyle(
|
||
TableStyle(
|
||
[
|
||
("BACKGROUND", (0, 0), (-1, -1), COR_ROXO_ESCURO),
|
||
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||
("LEFTPADDING", (0, 0), (0, 0), 12),
|
||
("RIGHTPADDING", (-1, 0), (-1, 0), 12),
|
||
("TOPPADDING", (0, 0), (-1, -1), 8),
|
||
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
|
||
]
|
||
)
|
||
)
|
||
|
||
linhas_detalhe = [
|
||
f"Grupo: <b>{_percentual(colaborador.pct_grupo)}</b>{' (ajustado manualmente)' if colaborador.pct_grupo_ajustado_manualmente else ''}",
|
||
f"Departamento: <b>{_percentual(colaborador.pct_departamento)}</b>{' (ajustado manualmente)' if colaborador.pct_departamento_ajustado_manualmente else ''}",
|
||
]
|
||
if colaborador.pct_individual_ajustado_manualmente:
|
||
linhas_detalhe.insert(
|
||
0, f"Percentual individual ajustado pela direção: <b>{_percentual(colaborador.pct_individual)}</b>."
|
||
)
|
||
subtexto = Paragraph(" · ".join(linhas_detalhe), estilos["normal"])
|
||
|
||
return KeepTogether([linha_titulo, Spacer(1, 0.15 * cm), subtexto])
|
||
|
||
|
||
def _tabela_empresas(colaborador: IndicadorApuracaoColaborador, estilos: dict) -> Table:
|
||
# Toda célula (menos o cabeçalho, já em Paragraph) precisa ser um
|
||
# Paragraph, não uma string solta — string solta não quebra linha dentro
|
||
# da coluna, e com ALIGN à direita/centro um valor mais largo que a
|
||
# coluna (ex.: "Contador (com conciliador)" em Tipo, ou os totais em
|
||
# negrito, que ficam mais largos que a mesma string em peso normal)
|
||
# simplesmente vaza por cima da célula vizinha em vez de quebrar linha.
|
||
empresas = list(colaborador.empresas.all())
|
||
cabecalho = ["Código", "Empresa", "Honorário", "Hon. Ajustado", "Tipo", "Indiv.", "Grupo", "Depto.", "Total"]
|
||
dados = [[Paragraph(texto, estilos["cabecalho_tabela"]) for texto in cabecalho]]
|
||
for empresa in empresas:
|
||
nome = empresa.nome_empresa or empresa.codigo_empresa
|
||
if empresa.honorario_nao_encontrado:
|
||
nome += " (honorário não encontrado)"
|
||
dados.append(
|
||
[
|
||
Paragraph(empresa.codigo_empresa, estilos["celula_centro"]),
|
||
Paragraph(nome, estilos["celula"]),
|
||
Paragraph(_moeda(empresa.honorario), estilos["celula_direita"]),
|
||
Paragraph(_moeda(empresa.honorario_ajustado), estilos["celula_direita"]),
|
||
Paragraph(TIPO_LABEL_CURTO.get(empresa.tipo, empresa.tipo), estilos["celula_centro"]),
|
||
Paragraph(_moeda(empresa.valor_individual), estilos["celula_direita"]),
|
||
Paragraph(_moeda(empresa.valor_grupo), estilos["celula_direita"]),
|
||
Paragraph(_moeda(empresa.valor_departamento), estilos["celula_direita"]),
|
||
Paragraph(_moeda(empresa.valor_total), estilos["celula_direita"]),
|
||
]
|
||
)
|
||
dados.append(
|
||
[
|
||
"",
|
||
Paragraph("Total do Indicador", estilos["celula_negrito"]),
|
||
"",
|
||
"",
|
||
"",
|
||
Paragraph(_moeda(sum((empresa.valor_individual for empresa in empresas), Decimal("0"))), estilos["celula_direita_negrito"]),
|
||
Paragraph(_moeda(sum((empresa.valor_grupo for empresa in empresas), Decimal("0"))), estilos["celula_direita_negrito"]),
|
||
Paragraph(_moeda(sum((empresa.valor_departamento for empresa in empresas), Decimal("0"))), estilos["celula_direita_negrito"]),
|
||
Paragraph(_moeda(colaborador.valor_total), estilos["celula_direita_negrito"]),
|
||
]
|
||
)
|
||
|
||
tabela = Table(
|
||
dados,
|
||
repeatRows=1,
|
||
colWidths=[1.0 * cm, 3.3 * cm, 2.0 * cm, 2.0 * cm, 2.1 * cm, 1.7 * cm, 1.6 * cm, 1.9 * cm, 1.7 * cm],
|
||
)
|
||
estilo = [
|
||
("BACKGROUND", (0, 0), (-1, 0), COR_ROXO_ESCURO),
|
||
("FONTSIZE", (0, 0), (-1, -1), 7.6),
|
||
("GRID", (0, 0), (-1, -1), 0.5, COR_BORDA),
|
||
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||
("TOPPADDING", (0, 0), (-1, -1), 4),
|
||
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
|
||
# Padding lateral padrão do reportlab é 6pt de cada lado — generoso
|
||
# demais pra colunas numéricas estreitas; reduzido pra sobrar mais
|
||
# espaço de verdade pro texto sem precisar alargar a tabela inteira.
|
||
("LEFTPADDING", (0, 0), (-1, -1), 3),
|
||
("RIGHTPADDING", (0, 0), (-1, -1), 3),
|
||
("ROWBACKGROUNDS", (0, 1), (-1, -2), [colors.white, COR_ROXO_CLARO]),
|
||
("BACKGROUND", (0, -1), (-1, -1), COR_DOURADO_CLARO),
|
||
("LINEABOVE", (0, -1), (-1, -1), 1, COR_DOURADO),
|
||
]
|
||
tabela.setStyle(TableStyle(estilo))
|
||
return tabela
|
||
|
||
|
||
def _tabela_indicadores(colaborador: IndicadorApuracaoColaborador, apuracao: IndicadorApuracao, estilos: dict) -> Table:
|
||
periodo_atual = apuracao.periodo_criterio()
|
||
grupos = _agrupa_por_periodo(_respostas_aplicaveis(colaborador, periodo_atual))
|
||
|
||
dados = [["Atingido?", "Critério", "Peso", "% Medido"]]
|
||
estilo = [
|
||
("FONTSIZE", (0, 0), (-1, -1), 8),
|
||
("GRID", (0, 0), (-1, -1), 0.5, COR_BORDA),
|
||
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||
("TOPPADDING", (0, 0), (-1, -1), 4),
|
||
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
|
||
("ALIGN", (0, 0), (0, -1), "CENTER"),
|
||
("ALIGN", (2, 0), (3, -1), "CENTER"),
|
||
("BACKGROUND", (0, 0), (-1, 0), COR_ROXO_ESCURO),
|
||
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
|
||
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||
]
|
||
|
||
linha = 1
|
||
for periodo, respostas in grupos:
|
||
dados.append([PERIODO_LABEL.get(periodo, periodo), "", "", ""])
|
||
estilo.append(("SPAN", (0, linha), (3, linha)))
|
||
estilo.append(("BACKGROUND", (0, linha), (-1, linha), COR_DOURADO_CLARO))
|
||
estilo.append(("FONTNAME", (0, linha), (-1, linha), "Helvetica-Bold"))
|
||
estilo.append(("TEXTCOLOR", (0, linha), (-1, linha), COR_ROXO_ESCURO))
|
||
estilo.append(("ALIGN", (0, linha), (-1, linha), "LEFT"))
|
||
linha += 1
|
||
for resposta in respostas:
|
||
dados.append(
|
||
[
|
||
RESPOSTA_LABEL.get(resposta.valor, resposta.valor),
|
||
Paragraph(resposta.criterio.nome, estilos["celula"]),
|
||
_percentual(resposta.criterio.peso),
|
||
_percentual(resposta.percentual_calculado) if resposta.percentual_calculado is not None else "—",
|
||
]
|
||
)
|
||
cor_bg = COR_ATINGIDO_BG.get(resposta.valor, COR_NEUTRO_BG)
|
||
cor_texto = COR_ATINGIDO_TEXTO.get(resposta.valor, COR_NEUTRO_TEXTO)
|
||
estilo.append(("BACKGROUND", (0, linha), (0, linha), cor_bg))
|
||
estilo.append(("TEXTCOLOR", (0, linha), (0, linha), cor_texto))
|
||
estilo.append(("FONTNAME", (0, linha), (0, linha), "Helvetica-Bold"))
|
||
linha += 1
|
||
|
||
tabela = Table(dados, colWidths=[2 * cm, 11.4 * cm, 1.8 * cm, 2 * cm])
|
||
tabela.setStyle(TableStyle(estilo))
|
||
return tabela
|
||
|
||
|
||
def _tabela_percentuais_por_tipo(apuracao: IndicadorApuracao, departamento_id: int | None) -> Table:
|
||
dados = [["Cód.", "Tipo", "% Individual", "% Grupo", "% Depto."]]
|
||
estilo = [
|
||
("FONTSIZE", (0, 0), (-1, -1), 8),
|
||
("GRID", (0, 0), (-1, -1), 0.5, COR_BORDA),
|
||
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||
("TOPPADDING", (0, 0), (-1, -1), 3),
|
||
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
|
||
("ALIGN", (0, 0), (0, -1), "CENTER"),
|
||
("ALIGN", (2, 0), (-1, -1), "CENTER"),
|
||
("BACKGROUND", (0, 0), (-1, 0), COR_ROXO_ESCURO),
|
||
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
|
||
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, COR_ROXO_CLARO]),
|
||
]
|
||
for tipo, label in TIPO_COLABORADOR_INDICADOR_CHOICES:
|
||
percentual = percentual_vigente(tipo, apuracao.competencia, departamento_id)
|
||
if percentual is None:
|
||
continue
|
||
dados.append(
|
||
[
|
||
tipo,
|
||
label,
|
||
_percentual(percentual.percentual_individual),
|
||
_percentual(percentual.percentual_grupo),
|
||
_percentual(percentual.percentual_departamento),
|
||
]
|
||
)
|
||
tabela = Table(dados, colWidths=[1.5 * cm, 5.5 * cm, 3 * cm, 3 * cm, 3 * cm])
|
||
tabela.setStyle(TableStyle(estilo))
|
||
return tabela
|
||
|
||
|
||
def _rodape(colaborador: IndicadorApuracaoColaborador, apuracao: IndicadorApuracao, estilos: dict) -> list:
|
||
ultimo_dia = calendar.monthrange(apuracao.competencia.year, apuracao.competencia.month)[1]
|
||
data_extenso = f"{CIDADE_ESCRITORIO}, {ultimo_dia} de {MES_LABEL[apuracao.competencia.month]} de {apuracao.competencia.year}."
|
||
return [
|
||
Spacer(1, 1 * cm),
|
||
Paragraph(data_extenso, estilos["rodape"]),
|
||
Spacer(1, 1.4 * cm),
|
||
Paragraph("DE ACORDO,", estilos["rodape"]),
|
||
Spacer(1, 1.3 * cm),
|
||
Paragraph("_" * 45, estilos["rodape"]),
|
||
Paragraph(f"<b>{colaborador.nome}</b>", estilos["rodape"]),
|
||
]
|
||
|
||
|
||
def gera_pdf_recibo(colaborador: IndicadorApuracaoColaborador, apuracao: IndicadorApuracao) -> bytes:
|
||
buffer = io.BytesIO()
|
||
documento = SimpleDocTemplate(
|
||
buffer, pagesize=A4, topMargin=1.3 * cm, bottomMargin=1.3 * cm, leftMargin=1.5 * cm, rightMargin=1.5 * cm
|
||
)
|
||
estilos = _estilos()
|
||
|
||
elementos = [
|
||
_cabecalho(apuracao, estilos),
|
||
Spacer(1, 0.35 * cm),
|
||
Paragraph(
|
||
f"<b>Nome:</b> {colaborador.nome} · <b>Gerente:</b> {colaborador.gerente or '-'} "
|
||
f" · <b>Departamento:</b> "
|
||
f"{colaborador.departamento.nome if colaborador.departamento_id else '-'}",
|
||
estilos["normal"],
|
||
),
|
||
Spacer(1, 0.3 * cm),
|
||
_banner_percentual(colaborador, estilos),
|
||
Spacer(1, 0.5 * cm),
|
||
Paragraph("Empresas", estilos["secao"]),
|
||
_tabela_empresas(colaborador, estilos),
|
||
Spacer(1, 0.3 * cm),
|
||
Paragraph(f"<b>Total do Indicador de Desempenho: {_moeda(colaborador.valor_total)}</b>", estilos["normal"]),
|
||
Paragraph("Critérios do Indicador", estilos["secao"]),
|
||
_tabela_indicadores(colaborador, apuracao, estilos),
|
||
Paragraph("Percentuais por Tipo de Colaborador", estilos["secao"]),
|
||
_tabela_percentuais_por_tipo(apuracao, colaborador.departamento_id),
|
||
]
|
||
elementos.extend(_rodape(colaborador, apuracao, estilos))
|
||
|
||
documento.build(elementos)
|
||
return buffer.getvalue()
|