# 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 `` 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 `` 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 ``) 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 e apaga os 2 arquivos recém-gravados em `MEDIA_ROOT` (upload não é transacional), devolvendo 400 genérico. - **`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 ` ` (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`.