portal_publico/portal_api/indicadores/CLAUDE.md

33 KiB
Raw Blame History

Indicador de Desempenho (Geradoc)

Movido do CLAUDE.md da raiz em 2026-08-26 para reduzir conflito de edição entre aplicações (documentação por app, código continua no mesmo lugar). Ver CLAUDE.md na raiz para arquitetura geral/transversal do Portal (modelo de permissões, API, CSS, etc.) — este arquivo é carregado automaticamente ao trabalhar dentro de portal_api/indicadores/. Ver também a skill indicador-desempenho (.claude/skills/) para contexto de negócio (por quê, limitações conhecidas).

Segunda ferramenta de Geradoc — substitui a apuração manual do indicador de desempenho do Fiscontábil (departamentos Contábil/Fiscal), antes feita numa planilha (FISCO CONTABIL *.ods, em projects/Indicadores/) com fórmulas quebradas por edições manuais acumuladas. Mesmo padrão de permissão de toggle único de Simulação de Custo de Contratação (apps["indicador-desempenho"] em permissoes["geradoc"], checado por PermissaoApp("geradoc", "indicador-desempenho") em todos os ModelViewSet relacionados). Pacote de negócio em portal_api/indicadores/ (sem ORM): tipos.py, leiaute.py, pipeline.py, entregas.py, calculo.py, recibo.py, departamentos.py.

  • Escopo v1: só o Fiscontábil, papéis Balancete/Liberação Fiscal/Conciliação Financeira. Outros departamentos ficam pra rodada futura — exceto pela estrutura de cadastro em si (ver "Departamento organizacional" abaixo), que já suporta múltiplos departamentos com critérios/percentuais próprios, mesmo que só o Fisco/Contábil tenha regras cadastradas até agora.

  • 8 models (migrações 0024–0028, 0033–0035): IndicadorDepartamento (cadastro de departamentos — nome/ativo — usado pra escopar critérios, percentuais e metas de Departamento; ver "Departamento organizacional" abaixo), IndicadorDepartamentoGerente (relação gerente→departamento, mantida manualmente pela aplicação), IndicadorPercentualTipo (percentuais individual/grupo/departamento por tipo de colaborador e por departamento, histórico via vigente_desde — nunca editado in-place), IndicadorCriterio (cadastro genérico de critério: departamento/nome/grupo/peso/período/papel/calculo_automatico/limiar_percentual), IndicadorApuracao (uma apuração mensal — competencia, status revisao/concluida, as 2 planilhas anexadas, avisos de processamento), IndicadorApuracaoColaborador (um colaborador dentro de uma apuração, com pct_individual/pct_grupo/pct_departamento e respectivos flags *_ajustado_manualmente, mais departamento — FK pra IndicadorDepartamento, resolvida na criação via o gerente do colaborador, ver "Departamento organizacional" abaixo), IndicadorApuracaoEmpresa (uma empresa/honorário do colaborador naquele mês) e IndicadorApuracaoResposta (SIM/NÃO/NÃO FAZ/NÃO SE APLICA de um colaborador para um critério).

  • Tipo do colaborador é derivado por empresa, não é cadastro: TIPO_COLABORADOR_INDICADOR_CHOICES (Contábil+Fiscal/Contador SC/Contador CC/Fiscal/Conciliador) — regra em indicadores/tipos.py, validada contra um recibo-modelo real (~99,99% de precisão no teste com 43 colaboradores).

  • 3 critérios são calculados automaticamente a partir da planilha "Serviços Tareffa" (indicadores/entregas.py/pipeline.py) — entrega de balancetes/liberações fiscais/conciliações no prazo, comparadas contra IndicadorCriterio.limiar_percentual pra decidir SIM/NÃO. Todo o resto é sempre marcação manual do RH (SIM/NÃO/NÃO FAZ/NÃO SE APLICA por critério, individual ou em lote). Um critério automático sem nenhum registro do serviço vira NÃO SE APLICA, não NÃO FAZ — permite deixar papel_aplicavel em branco nesses 3 critérios (um mesmo critério, ex. "balancete", se aplica a 3 tipos ao mesmo tempo, e papel_aplicavel só aceita um valor); quem não presta aquele serviço fica de fora do cálculo por conta própria (NÃO SE APLICA é excluído do denominador em calculo.py).

  • Fórmula: honorario_ajustado = honorario_empresa × pct_individual_do_colaborador; valor_individual = honorario_ajustado × percentual_individual(tipo); valor_grupo/valor_departamento = valor_individual × percentual_grupo/departamento(tipo) × pct_grupo/departamento_do_colaborador. pct_individual não é só a média dos critérios Individual — é a composição ponderada dos 3 níveis (Individual/Grupo/Departamento), cada um pesando conforme o peso médio dos seus próprios critérios aplicáveis na competência (calculo._combina_niveis/_peso_medio_nivel); só pct_grupo/pct_departamento continuam sendo a média simples dos próprios critérios, sem composição.

  • "Cada gerente representa um grupo", cada departamento representa um departamento (não é redundante, ver abaixo): pct_grupo é conceitualmente compartilhado por todos os colaboradores com o mesmo gerente dentro da apuração, e pct_departamento é compartilhado por todos os colaboradores do mesmo IndicadorDepartamento (ver "Departamento organizacional" abaixo — não mais um valor único pra toda a apuração) — por isso não são ajustados colaborador a colaborador (IndicadorApuracaoColaboradorViewSet só cobre pct_individual); IndicadorApuracaoViewSet.ajustar_grupo/recalcular_grupo/ajustar_departamento/recalcular_departamento aplicam a mudança de uma vez a todo o grupo/departamento, mantendo os colaboradores em sincronia (ajustar_departamento/recalcular_departamento recebem departamento — o id do IndicadorDepartamento — no corpo, filtrando com um .filter(departamento_id=...) direto). A tela (indicador-desempenho.js) reflete isso com uma tabela de "Metas de Grupo e Departamento" no topo (uma linha de Departamento por IndicadorDepartamento + uma linha de Grupo por gerente dentro dele) separada da lista de colaboradores abaixo (que serve só pra revisão individual — percentual Individual, respostas de critério, recibo); botões de filtro por departamento (#ind-filtro-departamento, ver "Departamento organizacional" abaixo) restringem a tabela de Metas e a lista de colaboradores a um departamento de cada vez, sem afetar o cálculo de nenhuma meta. Colaborador cujo gerente não está mapeado a nenhum departamento cai num grupo "Sem departamento definido" (sem <select> de meta — não há IndicadorDepartamento pra aplicar). A meta de Grupo/Departamento é sempre Sim/Não (100%/0%), nunca um percentual livre — decisão explícita do usuário ("será pago ou não") — por isso a coluna "Meta (%)" dessa tabela é um <select class="ind-meta-select"> com só essas duas opções (ehSim = valor >= 50 decide qual aparece selecionada ao carregar), não um campo de texto livre; o valor enviado a ajustar-grupo/ajustar-departamento é sempre "100" ou "0". O percentual Individual de cada colaborador continua livre (é uma composição ponderada dos 3 níveis, pode legitimamente ser fracionário — ver acima).

  • Departamento organizacional (IndicadorDepartamento/IndicadorDepartamentoGerente/portal_api.indicadores.departamentos) — substituiu, numa rodada posterior, o mecanismo de "setor" (coluna bruta "departamento" da planilha Tareffa + fusão automática Contabilidade/Fiscal→Fisco-Contábil + IndicadorSetorApelido, cadastro-exceção por colaborador). Agora critérios e percentuais também são configurados por departamento (não só as metas de Grupo/Departamento) — decisão explícita do usuário: a regra do Fisco/Contábil pode ser diferente da regra do Condomínio, por exemplo. IndicadorDepartamento (nome/ativo) é um cadastro simples, mantido pela própria aplicação (Configurações → Departamentos); a relação com gerentes (IndicadorDepartamentoGerente, nome_gerente único — um gerente pertence a só um departamento, mas um departamento pode ter vários gerentes, ex.: Fisco/Contábil tem "João Candido Rodrigues" e "Lhais Vergilio Delavy") também é mantida manualmente por ora — alimentar isso automaticamente a partir da planilha fica pra uma rodada futura (decisão explícita do usuário). Pra não obrigar o RH a redigitar nomes (arriscando um typo que faria uma apuração futura não casar com o departamento certo), o popup "Gerenciar Gerentes" (indicador-desempenho.js) mostra uma lista de sugestões clicáveis — carregarGerentesSugeridos() busca a apuração mais recente (GET /api/indicadores-apuracoes/, já ordenada por -competencia/-criado_em) e lista os nomes distintos de colaborador.gerente que ainda não estão em nenhum IndicadorDepartamentoGerente; clicar numa sugestão já cria a relação pra aquele departamento. É só um atalho de UI (não muda a origem do dado) — o campo de texto livre continua disponível pra gerentes que não apareceram na última apuração.

    Resolução do departamento de um colaborador: IndicadorApuracaoViewSet.create() monta mapa_gerentes (departamentos.carrega_mapa_gerentes(), {nome_gerente: departamento_id}) uma vez e passa pro pipeline.processa_apuracao(), que resolve departamento_id = mapa_gerentes.get(colaborador.gerente) pra cada colaborador antes de decidir quais critérios automáticos calcular pra ele (críticos automáticos também são agrupados por departamento_id — criterios_automaticos_por_departamento, já que departamentos diferentes podem ter critérios/limiares diferentes). O resultado (IndicadorApuracaoColaborador.departamento, FK nullable) é um retrato daquele momento — mesmo espírito de gerente/setor antes dele: se a relação gerente→departamento mudar depois, apurações já criadas não mudam sozinhas. Colaborador cujo gerente não está mapeado a nenhum departamento fica com departamento=None e vira um aviso no processamento ("Gerente X não está associado a nenhum departamento..."), além de não ganhar nenhuma IndicadorApuracaoResposta (sem departamento, não há de onde vir nenhum critério). IndicadorApuracaoColaboradorSerializer expõe departamento (id) + departamento_nome (com fallback None, mesmo padrão de criado_por_nome).

    Migração em 3 passos (mesmo padrão de qualquer FK obrigatória adicionada a uma tabela já populada): 0033 cria os 2 models novos + adiciona departamento nullable em IndicadorCriterio/IndicadorPercentualTipo/IndicadorApuracaoColaborador (e remove setor/IndicadorSetorApelido); 0034 (RunPython) cria o departamento "Fisco/Contábil" e aponta todo IndicadorCriterio/IndicadorPercentualTipo já existente pra ele (é literalmente o que a regra única representava até então); 0035 torna departamento obrigatório em IndicadorCriterio/IndicadorPercentualTipo (não em IndicadorApuracaoColaborador, que continua nullable). Apurações criadas antes desta migração (e qualquer apuração nova, até o admin mapear os gerentes relevantes em Configurações → Departamentos) ficam com departamento em branco em todos os colaboradores — precisam de um backfill pontual ou de serem reprocessadas depois que a relação gerente→departamento existir.

    Limitação conhecida, validada com dados reais: como a resolução é por gerente (não por colaborador), dois subordinados diretos do mesmo gerente sempre caem no mesmo departamento — isso quebra o caso de uma gerente que supervisiona pessoas de departamentos diferentes. Ex. real: "Elizangela de Paula Kuhn" supervisiona diretamente os líderes de Fisco/Contábil (João Candido Rodrigues, Lhais Vergilio Delavy, Paloma Ramão, Elizangela dos Santos — departamento "Gerentes") e Luciane Gonzaga (que deveria cair em "Rocket", já que ela chefia esse outro departamento) — como todos compartilham o mesmo gerente, mapear "Elizangela de Paula Kuhn" → "Gerentes" também classifica Luciane Gonzaga como "Gerentes", não "Rocket". Não existe mais um mecanismo de exceção por colaborador individual (o antigo IndicadorSetorApelido cobria exatamente esse tipo de caso) — se isso for um problema real, precisa ser resolvido numa rodada futura (ex.: reintroduzindo uma exceção por nome de colaborador, por cima da relação gerente→departamento).

  • Detalhamento da composição no card do colaborador (portal_api.indicadores.calculo.composicao_individual, exposto como o campo composicao_individual de IndicadorApuracaoColaboradorSerializer): reconstrói, só pra exibição, o percentual bruto de Individual (antes da composição) e o peso médio de cada um dos 3 níveis (_peso_medio_nivel) — dados que recalcula_colaborador calcula mas não persiste, por não precisar deles depois de gravar pct_individual. No cabeçalho do card (indicador-desempenho.js), essa linha ("Individual: X% (peso Y%) · Grupo: X% (peso Y%) · Departamento: X% (peso Y%)") fica ao lado do nome/gerente, numa coluna própria do grid centralizada — não embaixo — e cada um dos 3 níveis fica verde/vermelho conforme bateu 100% ou não; o "Total Indicador" (renomeado de "Individual", que é pct_individual, com o lápis de ajuste manual sempre ao lado do valor numa linha que não quebra) fica neutro, sem cor, pra não repetir a mesma informação 4 vezes. composicao_individual() usa colaborador.respostas.all() (não .select_related("criterio")) de propósito, pra reaproveitar o prefetch_related("colaboradores__respostas__criterio") que IndicadorApuracaoViewSet.get_queryset() aplica só na action retrieve — evita 1 query extra por colaborador ao abrir a tela de revisão.

  • Tabela "Metas de Grupo e Departamento" só tem uma forma de responder Sim/Não por critério — a coluna "Meta (%)" (ajusta pct_grupo/pct_departamento direto). Existia um segundo <select> Sim/Não ao lado do texto de cada critério (bulk, via aplicar-em-lote), removido por ser redundante com o da direita; a lista de critérios ali agora é só informativa (nome + peso). Responder um critério específico continua possível por colaborador, dentro da lista de colaboradores abaixo (renderRespostasGrupoHtml).

  • "Corrigir Responsável" (#ind-corrigir-responsavel-btn, popup próprio): busca uma empresa (por nome ou código, entre todas as empresas da apuração, não só as com problema de honorário — empresasAgrupadasPorCodigo(() => true)) e mostra, pra cada responsável dela (uma linha por IndicadorApuracaoEmpresa, ex.: "Valéria Bonete — Fiscal"), um <select> com os demais colaboradores da apuração pra reatribuir aquele papel. Reatribuir chama POST /api/indicadores-apuracoes-empresas/{id}/trocar-responsavel/ (IndicadorApuracaoEmpresaViewSet.trocar_responsavel, serializer IndicadorApuracaoEmpresaTrocarResponsavelSerializer com {colaborador_id}), que só troca a FK colaborador da linha (codigo_empresa/tipo/honorário continuam os mesmos) e recalcula os dois colaboradores envolvidos (o que perdeu a empresa e o que ganhou) — validado no backend contra: colaborador de outra apuração, colaborador igual ao atual, e colaborador que já é responsável por essa mesma empresa/tipo (evitaria duas linhas duplicadas pra ele). O <select> exclui o colaborador atual das opções e nasce com um placeholder desabilitado ("Selecionar novo responsável…") pra nunca reatribuir sem escolha explícita.

  • Checklist de revisão do RH (IndicadorApuracaoColaborador.validado, migração 0031): um checkbox no início de cada card (.ind-colaborador-card__validado, primeira coluna do grid do cabeçalho), sem relação com nenhum cálculo — só ajuda o RH a controlar quem já conferiu numa apuração com muitos colaboradores. Marcado, a borda do card inteiro fica verde (.ind-colaborador-card.is-validado, mesma largura de sempre, só muda a cor, pra não deslocar layout). POST /api/indicadores-apuracoes-colaboradores/{id}/marcar-validado/ (marcar_validado, serializer IndicadorApuracaoColaboradorValidadoSerializer com {validado}) só grava o campo, sem chamar recalcula_colaborador. Diferente dos outros ajustes desta tela, o frontend não recarrega a apuração inteira depois de marcar/desmarcar: atualiza só o card clicado localmente. Os outros ajustes recarregam por recarregarRevisao(), que desde a revisão de interface de 30/09/2026 (rodada 47 do CHANGELOG) guarda os cards abertos, a rolagem e as respostas marcadas para o lote e os restaura depois do redesenho, então nenhum ajuste fecha mais o card em conferência; erro de rede reverte o checkbox e o estado em memória (mesmo padrão de outros toggles imediatos do app, ex. inativar usuário). O <label> inteiro (não só o <input>) precisa ficar de fora do gate de clique que expande/recolhe o card no cabeçalho, senão um clique na área do label (fora do glifo do checkbox) expande/recolhe o card ao mesmo tempo que marca/desmarca o validado — resultado de como labels HTML disparam dois eventos de clique encadeados.

  • Forçar SIM num critério automático não vira 100% na média — a média ponderada usa o percentual real medido (_fracao_atingida em calculo.py), mesmo que o RH marque SIM por cima; só critério manual (sem percentual_calculado) é binário SIM=100%/resto=0%. Pra dar crédito cheio apesar do percentual medido baixo, o RH ajusta o percentual agregado direto (nível 2 acima), não o critério.

  • create() é atômico: IndicadorApuracaoViewSet.create() roda o pipeline inteiro (parse das 2 planilhas + persistência de colaboradores/empresas/respostas) dentro de transaction.atomic() — uma falha no meio (planilha fora do leiaute, overflow decimal) desfaz tudo no banco, devolvendo 400 genérico. As 2 planilhas não são gravadas (rodada 48 do CHANGELOG desta pasta, pedido do usuário: nada as lia depois da criação): como leiaute.py pede caminho, a view as grava em arquivos temporários (_salva_arquivo_temporario()), apagados num finally; o limite de 15MB fica no serializer (validar_tamanho_arquivo_indicador).

  • POST /api/indicadores-apuracoes/{id}/gerar/ monta um ZIP com um PDF de recibo por colaborador (indicadores/recibo.py, reportlab) a partir do que já está salvo — não reprocessa as planilhas, reflete qualquer ajuste manual feito na revisão. Recibo é documento interno (só quem tem a permissão do RH acessa/baixa) — sem visão própria do colaborador no Portal nesta v1. Botão "Gerar Recibos" (indicador-desempenho.js) abre um modal antes de chamar o endpoint — mesmo componente de busca por nome + filtro por departamento + checklist (com "marcar todos os resultados da busca") do "Ajuste Indicador em Lote", só que já nasce com todo mundo marcado (reproduz o comportamento antigo de "gerar pra todos" sem precisar marcar um por um); desmarcar alguns permite gerar recibo avulso de um colaborador só, de alguns específicos, ou de um departamento inteiro. O endpoint recebe colaborador_ids (lista, opcional) e só marca a apuração como concluida quando o conjunto pedido bate com todos os colaboradores da apuração (sem colaborador_ids, ou uma seleção que cobre o total) — gerar um recibo avulso pra conferência não fecha a apuração inteira como se o mês estivesse todo revisado.

  • Layout do PDF do recibo (indicadores/recibo.py): o banner "PERCENTUAL DO INDICADOR INDIVIDUAL" sempre mostra o percentual efetivo/medido (calculo.composicao_individual()["total_calculado"] — a composição dos 3 níveis recalculada na hora, ignorando qualquer ajuste manual), não colaborador.pct_individual puro — decisão explícita do usuário: se o RH/Diretoria sobrescreveu o Individual pra 100%, o banner precisa continuar mostrando o que o colaborador de fato atingiu (ex.: 74,29%), não o valor pago. Quando pct_individual_ajustado_manualmente=True, uma linha de detalhe abaixo do banner mostra "Percentual Individual Ajustado Pela Direção: 100,00%." (rótulo renomeado de "ajustado manualmente pelo RH", com o valor ajustado ao lado — antes só dizia que tinha sido ajustado, sem mostrar pra quanto) — os dois números lado a lado deixam claro o que foi medido e o que foi pago. Tabela "Empresas": toda célula (antes só "Empresa" era Paragraph, o resto strings soltas) virou Paragraph com estilo de alinhamento próprio (celula_centro/celula_direita/celula_negrito/celula_direita_negrito, _estilos()) — 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, mais largos que a mesma string em peso normal) vazava visualmente por cima da célula vizinha em vez de quebrar linha — bug real visto com dados reais (coluna Tipo cobria "Hon. Ajustado"). Coluna "Tipo" ganhou um dicionário de labels curtos só pro PDF (TIPO_LABEL_CURTO: "Contador SC"/"Contador CC" em vez de "Contador (sem/com conciliador)" — mesma abreviação já usada informalmente neste documento) porque o label completo não cabia nem quebrando linha numa coluna estreita; a linha de total virou "Total do Indicador" (era "Total Resultado"). Larguras de coluna e padding lateral (LEFTPADDING/RIGHTPADDING, reduzidos de 6pt padrão do reportlab pra 3pt) ajustados pra caber os maiores valores reais vistos na apuração (ex.: R$ 28.023,16) numa linha só. _moeda() usa &nbsp; (não espaço comum) entre "R$" e o número — com espaço comum, quando o valor não cabia numa linha só, o reportlab quebrava exatamente ali, deixando "R$" sozinho numa linha acima do número; com espaço não separável, o "R$" fica sempre grudado à esquerda do número (mesmo que precise de mais espaço na coluna pra caber tudo numa linha, resolvido junto pelas larguras/padding acima). Rótulo da linha de detalhe é "Percentual individual ajustado pela direção" (minúsculo, só a primeira letra maiúscula — não "Percentual Individual Ajustado Pela Direção").

  • aplicar_em_lote (IndicadorApuracaoRespostaViewSet, POST /api/indicadores-apuracoes-respostas/aplicar-em-lote/) aplica o mesmo valor a várias respostas de critério de uma vez — a "múltipla seleção" pedida pelo usuário na tela de revisão.

  • "Empresas sem Honorário" (#ind-empresas-sem-honorario-btn, cor de atenção — --danger, mesma linguagem visual do input/selo de honorário não encontrado, só enquanto houver alguma empresa pendente — sem nada pra resolver, o botão perde a classe .ind-empresas-sem-honorario-btn (volta a .btn-outline neutro) e o texto vira "Visualizar Empresas com Honorário Ajustado (N)", apontando direto pra revisão do que já foi ajustado — substituiu o antigo checkbox "Só com honorário não encontrado" que filtrava a lista de colaboradores): abre um modal que agrupa por codigo_empresa todas as IndicadorApuracaoEmpresa com honorario_nao_encontrado=True da apuração — a mesma empresa pode aparecer sob mais de um colaborador (um responsável pelo balancete, outro pela liberação fiscal, outro pela conciliação financeira), mas o honorário é da empresa, não da pessoa. Preencher um valor ali chama POST /api/indicadores-apuracoes/{id}/ajustar-honorario-empresa/ (IndicadorApuracaoViewSet.ajustar_honorario_empresa, serializer IndicadorApuracaoAjusteHonorarioEmpresaSerializer com {codigo_empresa, honorario}), que atualiza todas as linhas daquele código nesta apuração de uma vez (recalculando cada colaborador afetado) — diferente de PATCH /api/indicadores-apuracoes-empresas/{id}/ (ainda existe, ajusta só uma linha por id, usado direto na tabela "Empresas" de dentro do card do colaborador). Os dois caminhos (linha única e em lote) marcam honorario_ajustado_manualmente=True na(s) linha(s) afetada(s) — fica permanente (sem UI de reverter, diferente de pct_individual_ajustado_manualmente/etc., já que não existe um "automático" pra voltar quando o código nunca casou com a planilha) e vira uma nota "honorário ajustado manualmente" (cor --accent) ao lado do valor, na tabela "Empresas" de dentro do card do colaborador — visível só depois que honorario_nao_encontrado já foi resolvido (os dois nunca aparecem juntos). Essa tabela também passou a listar por codigo_empresa (IndicadorApuracaoEmpresa.Meta.ordering, migração 0029), não mais por nome. Como codigo_empresa é CharField, ordenar só por ele é ordem alfabética, não numérica — "80"/"503" apareciam depois de "2134" (o caractere '8'/'5' é "maior" que '1'/'2', mesmo o número sendo menor). Corrigido (migração 0030) ordenando primeiro pelo tamanho da string (Length("codigo_empresa")) e só depois pelo valor — reproduz a ordem numérica certa pra códigos sem zero à esquerda (string mais curta = número menor, sempre) sem converter pra inteiro, o que quebraria com erro de banco se algum código um dia não fosse só dígitos.

  • "Empresas ajustadas manualmente" é uma segunda seção dentro do mesmo popup "Empresas sem Honorário" — não um segundo botão/modal (revertido de propósito: nasceu como um botão separado, "Verificar Empresas Ajustadas Manualmente", e o usuário pediu pra unificar num popup só, "facilitando a usabilidade da ferramenta"). Fica escondida por padrão, atrás de um botão de largura cheia no final da lista principal (#ind-empresas-ajustadas-toggle-btn, .ind-esh-toggle-btn, com contador — "Visualizar"/"Ocultar Empresas Ajustadas Manualmente (N)") — pedido explícito do usuário logo depois de testar a versão anterior (as duas seções sempre visíveis de uma vez): a lista secundária só deve aparecer sob demanda, no final do modal. Lista, também agrupada por codigo_empresa, as empresas com honorario_ajustado_manualmente=True — permite corrigir um valor já ajustado (campo já vem preenchido com o honorário atual, ao contrário da lista principal, que começa em branco). Reaproveita o mesmo endpoint ajustar-honorario-empresa — o filtro do backend cobre Q(honorario_nao_encontrado=True) | Q(honorario_ajustado_manualmente=True), nunca uma empresa cujo honorário só veio certo da planilha e nunca foi mexido. As duas listas compartilham as funções de agrupamento/renderização/ordenação (empresasAgrupadasPorCodigo, renderEmpresaGrupoItemHtml) em indicador-desempenho.js, parametrizadas só pelo filtro; renderEmpresasHonorario() sempre re-renderiza a lista principal e só re-renderiza a de "ajustadas" se a seção já estiver aberta (ao abrir o popup, essa seção sempre volta a fechar) — necessário porque corrigir uma empresa "sem honorário" faz ela migrar pra lista de "ajustadas" na hora, e essa migração só precisa refletir de imediato se o usuário já estiver olhando pra ela. As duas ficam dentro de um único wrapper que rola (.ind-esh-scroll), com título e "Fechar" sempre visíveis fora dele (mesmo max-height:85vh do popup). Em cada item, o código aparece antes do nome da empresa no cabeçalho (.ind-esh-codigo seguido de .ind-esh-nome), mesma ordem da tabela "Empresas" do colaborador.

  • "Ajuste Indicador em Lote" (#ind-lote-global-btn, indicador-desempenho.js): modal separado do anterior — ajusta pct_individual (não critérios) de vários colaboradores selecionados por nome de uma vez, pra dois casos binários só: "Ajustar" (#ind-lote-global-ajustar-btn, aplica pct_individual=100 a todos, via PATCH /api/indicadores-apuracoes-colaboradores/{id}/) ou "Reverter" (chama a action recalcular de cada um, voltando ao cálculo automático) — sem campo de percentual livre. Os dois botões são btn-solid (mesma cor) — só "Cancelar" fica btn-outline, já que as duas ações são igualmente "reais", não uma primária e uma secundária. Não existe endpoint de lote dedicado pra isso; o frontend dispara um PATCH/POST por colaborador em paralelo (Promise.all).

  • Percentuais/critérios são cadastro editável pela tela, não hardcoded — decisão explícita do usuário pra não fixar no código números incertos vindos da planilha antiga já quebrada. Primeiro histórico populado via python manage.py seed_indicador_desempenho (idempotente), com os valores exatos da planilha antiga (vigente_desde fixado em 01/01/2024 por falta de data documentada — ajustar se o usuário informar a data real).

  • Bugs de robustez corrigidos ao testar com 43 colaboradores reais: openpyxl.load_workbook(..., read_only=True) precisa de .close() explícito (indicadores/leiaute.py), senão o Windows mantém o upload memory-mapped e bloqueia excluir a apuração depois; campos percentuais precisaram de max_digits=7 (não 6) — qualquer DecimalField que representa um percentual "de 0 a 100" precisa de max_digits >= decimal_places + 3 pra caber o "100" exato sem DataError: numeric field overflow.

API

Endpoint Método Uso
/api/indicadores-percentuais-tipo/ GET/POST/DELETE histórico de percentuais individual/grupo/departamento por tipo de colaborador (IndicadorPercentualTipo) — nunca editado in-place, só criado com vigente_desde novo; mesma permissão de toggle único apps["indicador-desempenho"] em permissoes["geradoc"]
/api/indicadores-criterios/, /api/indicadores-criterios/{id}/ GET/POST/PATCH/DELETE CRUD do cadastro genérico de critérios (IndicadorCriterio) — nome/grupo/peso/período/papel/cálculo automático livres, editável pelo RH
/api/indicadores-apuracoes/, /api/indicadores-apuracoes/{id}/ GET/POST/DELETE apuração mensal (IndicadorApuracao); POST é multipart (2 planilhas) e roda indicadores.pipeline.processa_apuracao() de forma síncrona dentro de um transaction.atomic(), persistindo colaboradores/empresas/respostas já calculados; DELETE também apaga os 2 arquivos de MEDIA_ROOT
/api/indicadores-apuracoes/{id}/gerar/ POST gera um ZIP com um PDF de recibo por colaborador (indicadores.recibo.gera_pdf_recibo), a partir do que já está salvo (não reprocessa as planilhas); colaborador_ids opcional no corpo restringe a geração a só esses colaboradores (modal "Gerar Recibos" — um colaborador só, alguns específicos, por departamento ou todos); marca a apuração como concluida só quando a seleção cobre todos os colaboradores
/api/indicadores-apuracoes/{id}/ajustar-grupo/, /recalcular-grupo/ POST ajusta (ou reverte) o pct_grupo de todos os colaboradores de um mesmo gerente na apuração de uma vez — "cada gerente representa um grupo"
/api/indicadores-apuracoes/{id}/ajustar-departamento/, /recalcular-departamento/ POST idem, mas aplica a todos os colaboradores do departamento (id de um IndicadorDepartamento) informado no corpo ({departamento, pct_departamento}/{departamento}) — cada departamento tem sua própria meta de Departamento
/api/indicadores-departamentos/, /api/indicadores-departamentos/{id}/ GET/POST/PATCH/DELETE cadastro de departamentos (IndicadorDepartamento, nome/ativo) — mesma permissão de toggle único do Indicador de Desempenho
/api/indicadores-departamentos-gerentes/, /api/indicadores-departamentos-gerentes/{id}/ GET/POST/PATCH/DELETE relação gerente→departamento (IndicadorDepartamentoGerente, nome_gerente único) — mesma permissão
/api/indicadores-apuracoes/{id}/ajustar-honorario-empresa/ POST {codigo_empresa, honorario} — preenche (ou corrige) o honorário de uma empresa com honorario_nao_encontrado=True ou honorario_ajustado_manualmente=True de uma vez pra todos os colaboradores desta apuração que a têm (mesmo código), recalculando cada um
/api/indicadores-apuracoes-colaboradores/{id}/ GET/PATCH ajuste manual do pct_individual de um colaborador (pct_individual_ajustado_manualmente=True); recalcula valor_total via indicadores.calculo.recalcula_colaborador
/api/indicadores-apuracoes-colaboradores/{id}/recalcular/ POST reverte pct_individual pro modo automático (limpa o ajuste manual) e recalcula
/api/indicadores-apuracoes-colaboradores/{id}/marcar-validado/ POST {validado} — checklist de revisão do RH, só grava o campo, sem recalcular nada
/api/indicadores-apuracoes-empresas/{id}/trocar-responsavel/ POST {colaborador_id} — reatribui essa linha (empresa+tipo) pra outro colaborador da mesma apuração, recalculando os dois
/api/indicadores-apuracoes-empresas/{id}/ GET/PATCH preenchimento manual do honorario de uma empresa com honorario_nao_encontrado=True (código não casou com a planilha de Honorários Por Cliente); zera essa flag e recalcula o colaborador
/api/indicadores-apuracoes-respostas/{id}/ GET/PATCH edição de uma resposta de critério (SIM/NÃO/NÃO FAZ/NÃO SE APLICA) já existente; recalcula o colaborador
/api/indicadores-apuracoes-respostas/aplicar-em-lote/ POST {resposta_ids, valor} — aplica o mesmo valor a várias respostas de uma vez (seleção múltipla da tela de revisão), recalculando todos os colaboradores afetados

Estes endpoints moravam na tabela de API do CLAUDE.md da raiz e foram trazidos para cá: endpoint de aplicação mora na doc da aplicação. A raiz mantém só os transversais (auth, /api/me/, catálogo, perfis, usuários, favoritos, widgets, compromissos, notificações).