Reestruturação da documentação do portal.
This commit is contained in:
parent
fcb3aec5b9
commit
568d087fa6
@ -98,7 +98,25 @@
|
||||
"Bash(sed -i 's#/api/apuracoes-contabeis/26/#/api/contabil-apuracoes/26/#' \"C:\\\\Users\\\\Depaula\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\c--Users-Depaula-Documents-Portal\\\\cf4a6caf-9798-42a6-bbc8-a8ffbb3debf6\\\\scratchpad\\\\pid_test_iripara3.py\")",
|
||||
"Bash(PYTHONPATH=\"C:\\\\Users\\\\Depaula\\\\Documents\\\\Portal\" .venv/Scripts/python.exe \"C:\\\\Users\\\\Depaula\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\c--Users-Depaula-Documents-Portal\\\\cf4a6caf-9798-42a6-bbc8-a8ffbb3debf6\\\\scratchpad\\\\pid_test_iripara3.py\")",
|
||||
"Bash(awk '/class ContabilApuracao\\(List|Detail\\)Serializer/,/^class [A-Z]/' portal_api/serializers.py)",
|
||||
"Bash(grep -n \"Colapsadas.size ? new Set\\(\\)\\\\|Colapsadas = dcContasColapsadas.size\\\\|Colapsadas.size$\" static/js/dashboard-contabil.js)"
|
||||
"Bash(grep -n \"Colapsadas.size ? new Set\\(\\)\\\\|Colapsadas = dcContasColapsadas.size\\\\|Colapsadas.size$\" static/js/dashboard-contabil.js)",
|
||||
"Bash(awk '/^## /{if\\(h!=\"\"\\){printf \"%7d bytes %s\\\\n\", b, h} h=$0; b=0} {b+=length\\($0\\)+1} END{printf \"%7d bytes %s\\\\n\", b, h}' CLAUDE.md)",
|
||||
"Bash(awk '/^### /{if\\(h!=\"\"\\){printf \"%7d bytes %s\\\\n\", b, h} h=$0; b=0} /^## /{if\\(h!=\"\"\\){printf \"%7d bytes %s\\\\n\", b, h} h=\"[[ \"$0\" ]]\"; b=0} {b+=length\\($0\\)+1} END{printf \"%7d bytes %s\\\\n\", b, h}' CLAUDE.md)",
|
||||
"Bash(grep -c \"^| \\\\`/api/\" CLAUDE.md)",
|
||||
"Bash(grep -c \"^| \\\\`/api/indicadores\" CLAUDE.md)",
|
||||
"Bash(grep -c \"^| \\\\`/api/contabil\" CLAUDE.md)",
|
||||
"Bash(sed 's/.*\"//')",
|
||||
"Bash(git check-ignore *)",
|
||||
"Bash(awk '{printf \"%4d %6d bytes %.110s\\\\n\", NR, length\\($0\\), $0}' portal_api/indicadores/CLAUDE.md)",
|
||||
"Bash(grep -noE \".{0,60}\\(10 shells|11 páginas|7 shells|8 páginas|as 3 páginas\\).{0,10}\" CLAUDE.md)",
|
||||
"Bash(awk 'NR>0')",
|
||||
"Bash(awk '/^### API \\\\\\(sessão/,/^### Frontend consumindo/' CLAUDE.md)",
|
||||
"Bash(grep -n \"^| \\\\`/api/\")",
|
||||
"Bash(grep -oE \"^\\\\| \\\\`/api/[a-z-]+\")",
|
||||
"Bash(awk '/^## /{if\\(h!=\"\"\\){printf \"%7d %s\\\\n\", b, h} h=$0; b=0} {b+=length\\($0\\)+1} END{printf \"%7d %s\\\\n\", b, h}' CLAUDE.md)",
|
||||
"Bash(grep -vE \"\\\\.md$\")",
|
||||
"Bash(grep \"\\\\.md$\")",
|
||||
"Bash(xargs -n1 basename)",
|
||||
"Bash(.venv/Scripts/python.exe -c \"import ast,sys; [ast.parse\\(open\\(f,encoding='utf-8'\\).read\\(\\)\\) for f in ['portal_api/views.py','portal_api/models.py']]; print\\(' .py intactos'\\)\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
164
CLAUDE.md
164
CLAUDE.md
@ -14,7 +14,9 @@ Até uma rodada anterior, todo o estado (sessão, usuários, perfis de acesso, f
|
||||
|
||||
Este arquivo cobre o que é **transversal** ao Portal (arquitetura, modelo de permissões, API, CSS, animações). A partir de 2026-08-26, a documentação detalhada de cada aplicação foi movida pra fora daqui, pra reduzir conflito de edição quando mais de uma pessoa mexe em aplicações diferentes ao mesmo tempo. Ver `prd.md` pra visão de produto (o quê/pra quem), `README.md` na raiz pro mapa de todas as aplicações, e `plano.md` pro histórico de decisões **estruturais/transversais** (o histórico específico de cada aplicação vive no `CHANGELOG.md` dela, ver abaixo).
|
||||
|
||||
Cada aplicação tem uma pasta própria com (a) o detalhamento técnico do estado atual, (b) um `README.md` (resumo enxuto — o quê/pra quem, sem o detalhe técnico) e (c) um `CHANGELOG.md` (histórico rodada a rodada, extraído de `plano.md`, mesma numeração de rodada usada lá):
|
||||
Cada aplicação tem uma pasta própria com (a) o detalhamento técnico do estado atual, (b) um `README.md` (resumo enxuto — o quê/pra quem, sem o detalhe técnico) e (c) um `CHANGELOG.md` (histórico rodada a rodada, extraído de `plano.md`).
|
||||
|
||||
**A numeração de rodada não é global**: cada aplicação conta as próprias, e o mesmo número designa trabalhos diferentes em arquivos diferentes (ex.: "rodada 93" é uma coisa em `plano.md` e outra em `portal_api/dashboard_contabil/CHANGELOG.md`). Ao citar uma rodada, **sempre nomear o arquivo** — "ver rodada 45 em `portal_api/indicadores/CHANGELOG.md`", nunca só o número. Ver o topo de `plano.md` para o detalhamento.
|
||||
|
||||
Aplicações com pacote Python próprio (`CLAUDE.md` **carregado automaticamente** pelo Claude Code ao trabalhar dentro da pasta):
|
||||
|
||||
@ -37,6 +39,9 @@ Aplicações sem pacote Python dedicado (código ainda em `portal_api/models.py`
|
||||
| Perfis de Acesso / Usuários (telas administrativas, Liderança, inativação) | `docs/perfis-usuarios/` — `perfis-usuarios.md` (técnico), `README.md`, `CHANGELOG.md` |
|
||||
| Solicitações | `docs/solicitacoes/` — `solicitacoes.md` (técnico), `README.md`, `CHANGELOG.md` |
|
||||
| Temas Sazonais (marca P.I.D. sazonal — hoje só Halloween) | `docs/temas-sazonais/` — `temas-sazonais.md` (técnico), `README.md`, `CHANGELOG.md` |
|
||||
| Identidade visual (logos, marca P.I.D. na UI, animações genéricas, intro pós-login) — não é aplicação do menu, é camada transversal | `docs/identidade-visual/` — `identidade-visual.md` (técnico), `README.md`, `CHANGELOG.md` |
|
||||
|
||||
**Cada endpoint mora na doc da sua aplicação.** A tabela de API mais abaixo tem só os transversais (auth, `/api/me/`, catálogo, perfis, usuários, favoritos, widgets, compromissos, notificações). Ao criar uma aplicação nova, documentar os endpoints dela na pasta dela.
|
||||
|
||||
## Como rodar / testar localmente
|
||||
|
||||
@ -76,7 +81,7 @@ Portal/
|
||||
├── .env
|
||||
├── config/ # settings.py, urls.py, wsgi.py, asgi.py — pacote de configuração do projeto
|
||||
├── portal_api/ # único app Django (models, serializers, views, admin, migrations, seed)
|
||||
├── templates/ # as 8 páginas HTML (TEMPLATES[0]["DIRS"] em settings.py aponta pra cá)
|
||||
├── templates/ # as 15 páginas HTML (13 shells + index.html + o relatório do Relatório Contábil) (TEMPLATES[0]["DIRS"] em settings.py aponta pra cá)
|
||||
├── static/ # css/, js/, img/ — STATICFILES_DIRS em settings.py aponta pra cá
|
||||
├── media/ # upload de usuário (hoje só ícones de LinkFerramenta) — MEDIA_ROOT em settings.py
|
||||
├── CLAUDE.md
|
||||
@ -85,51 +90,41 @@ Portal/
|
||||
|
||||
### Logos em `static/img/`
|
||||
|
||||
Duas identidades visuais coexistem **de propósito** hoje: o logo cursivo "D De Paula Contadores" (usado só nos documentos/PDFs que a aplicação gera, ver abaixo) e a marca nova "P.I.D." (`.svg`, ver `pid-marca-leiame.md` em `C:\Users\Depaula\Documents\Projetos\LOGOS\pid-marca`), adotada no favicon, no login e na sidebar do Portal. **Essa separação é deliberada, não uma migração incompleta**: um documento gerado pela aplicação (contrato, procuração, recibo do Indicador de Desempenho, PDF da Simulação de Custo de Contratação) é emitido como se o próprio escritório o tivesse gerado — carrega a identidade do escritório perante o cliente, não a identidade do Portal como ferramenta interna. A marca "P.I.D." é só pra UI do Portal em si. Não migrar o logo de um gerador de documento pra "P.I.D." (nem vice-versa numa tela do Portal) sem confirmar de novo com o usuário.
|
||||
**Duas identidades visuais coexistem de propósito**: o logo cursivo "D De Paula Contadores" (`logo.png`/`logo-branco.png`/`logo-mono.png`), usado **só nos documentos e PDFs que a aplicação gera**, e a marca "P.I.D." (`pid-*.svg`), usada **só na UI do Portal** (favicon, login, sidebar). A separação é deliberada, não uma migração incompleta: um documento gerado (contrato, procuração, recibo do Indicador de Desempenho, PDF da Simulação de Custo, relatório do Relatório Contábil) é emitido como se o próprio escritório o tivesse gerado, e carrega a identidade dele perante o cliente. **Não migrar o logo de um gerador de documento para "P.I.D." (nem o contrário numa tela do Portal) sem confirmar com o usuário.** Ver `[[feedback_logos_documentos_vs_portal]]` na memória.
|
||||
|
||||
**Logo cursivo "D De Paula Contadores"** (D em degradê dourado/marrom + texto, PNG com fundo transparente) — não aparece em nenhum template HTML hoje, só nos PDFs gerados pela aplicação:
|
||||
|
||||
- `logo.png` — original, texto **preto**. Serve como fonte pra gerar as outras variantes e é usada diretamente no recibo do Indicador de Desempenho (`indicadores/recibo.py`, `LOGO_PATH`, redimensionada/recomprimida em memória pra impressão — ver `portal_api/indicadores/CLAUDE.md`).
|
||||
- `logo-branco.png` — usada no cabeçalho do PDF de Simulação de Custo de Contratação (`custo_contratacao/pdf.py`, banner marrom escuro, ver `portal_api/custo_contratacao/CLAUDE.md`) — até uma rodada anterior também era usada no `sidebar__brand` dos 10 shells, migrada pra marca "P.I.D." (ver abaixo; a UI do Portal e os documentos gerados usam fontes de logo independentes agora). Mesmo D colorido de `logo.png`, mas com o texto recolorido pra branco; gerada programaticamente a partir de `logo.png` (script Python com Pillow: qualquer pixel opaco quase-neutro/escuro — `max(r,g,b) < 70` e `spread(r,g,b) < 12` — virou branco; o D nunca entra nesse filtro porque mesmo na sombra mais escura do degradê ele mantém um matiz quente nitidamente não-neutro). Se o logo oficial mudar, regerar `logo-branco.png` a partir do novo `logo.png` com o mesmo filtro, não editar à mão.
|
||||
- `logo-mono.png` — versão totalmente monocromática (D **e** texto em branco/cinza claro). Não usada em nenhum consumidor hoje; existe como variante alternativa (útil se algum dia precisar de um logo "chapado" sem o dourado do D).
|
||||
|
||||
**Marca nova "P.I.D."**:
|
||||
|
||||
- `pid-icone.svg` — ícone quadrado, variante pra **fundo claro** (corpo roxo escuro `#4B2E75`); chegou a ser usada no login quando `data-theme="light"`, mas o usuário pediu pra usar sempre a mesma logo nos dois temas — **não tem mais nenhum consumidor** hoje (mesma situação de `pid-favicon.svg`/`pid-logo-horizontal.svg` abaixo).
|
||||
- `pid-icone-escuro.svg` — ícone quadrado, variante pra **fundo escuro** (corpo roxo mais claro `#7B5BA8`, pra manter contraste); usada (a) no favicon (`<link rel="icon" type="image/svg+xml">` no `<head>` das 11 páginas) e (b) no login, **sempre**, nos dois temas — o card do login já é congelado escuro nos dois temas (ver "Card de login" abaixo), e agora o ícone também, pedido explícito do usuário ("deixe no tema claro a mesma logo usada no tema escuro"; antes alternava com `pid-icone.svg` conforme `data-theme`, mecanismo removido — ver "Ícone do login é clicável" abaixo). O ícone da sidebar (expandida e colapsada, 10 shells) e o do card de login usam o **mesmo desenho/cores** desse arquivo, mas como markup `<svg>` inline copiado direto no HTML, não uma referência a este arquivo — ver "Ícone do login/sidebar são clicáveis" abaixo pro motivo (precisa expor os olhos pro CSS/JS animar o piscar ao clicar).
|
||||
- `pid-logo-horizontal-escuro.svg` — assinatura horizontal (ícone + "P.I.D." + "PORTAL INTERNO DA DE PAULA" em texto, viewBox `300×80`, texto claro `#F2EDE3`/dourado `#C6A24A` — variante pra fundo escuro). Mesmo caso do ícone acima: a sidebar expandida (10 shells) usa o mesmo desenho como `<svg>` inline, não uma referência a este arquivo.
|
||||
- `pid-favicon.svg` (versão simplificada sem o sorriso, pro leiame recomendar pra 16–24px — não usada, o favicon usa `pid-icone-escuro.svg` mesmo), `pid-logo-horizontal.svg` (variante fundo claro da assinatura) e `pid-icone.svg` (acima) não têm nenhum consumidor no Portal hoje.
|
||||
|
||||
**`sidebar__brand` com duas marcas sobrepostas (crossfade), não uma redimensionada** (`layout.css`): a sidebar expandida mostra a assinatura com texto (tem texto, ilegível se só encolhida) e a colapsada mostra só o símbolo — são dois `<svg class="sidebar__logo sidebar__logo--full">`/`<svg class="sidebar__logo sidebar__logo--icon">` **sempre presentes no DOM**, sobrepostos via `position:absolute` dentro de `.sidebar__brand` (`position:relative; height:92px`, alto o bastante pra caber as duas sem depender da altura natural de nenhuma, já que filhos absolutos não contribuem pra altura do pai). `.sidebar__logo--full` é `width:250px; max-width:96%` (quase toda a largura útil da sidebar, `264px` menos o padding horizontal de `.sidebar`) — aumentado em duas rodadas a partir do tamanho inicial (`176px`/`70%` → `220px`/`92%` → `250px`/`96%`) porque o subtítulo "PORTAL INTERNO DA DE PAULA" (fonte pequena dentro do SVG, viewBox `300×80`) ficava ilegível menor; `height:92px` acompanhou cada aumento pra sobrar espaço vertical (proporção do SVG é `300:80`, então a altura renderizada escala junto com a largura). A transição entre elas é `opacity`+`transform:scale()` (`var(--transition-base)`, mesma duração das outras animações do sidebar) — pedido explícito do usuário pra não ser uma troca brusca; `.app-shell.is-collapsed` (toggle desktop) e o breakpoint mobile (`@media (max-width:1024px)`, onde o colapsado é o estado *default* e `.is-expanded-mobile` o inverte, mesmo padrão já usado pelos demais elementos do menu nesse breakpoint) alternam qual das duas fica com `opacity:1`.
|
||||
|
||||
**Ícone do login e da sidebar são clicáveis, com os olhos piscando** (pedido explícito do usuário, em duas rodadas — primeiro a sidebar, depois o ícone do login): tanto `.sidebar__brand` (10 shells) quanto `#login-logo-btn` (`index.html`) tiveram o ícone convertido de `<img src="...svg">` pra `<svg>` **inline**, com markup idêntico ao de `pid-icone-escuro.svg` copiado direto no HTML — um `<img>` não expõe seu conteúdo interno pro CSS/JS da página (é uma imagem opaca), então não dava pra animar só os olhos sem inlinear. Dentro de cada SVG, os dois olhos (círculo creme + glint escuro) ficam num `<g class="pid-icon-eye">` próprio, sem nenhum `transform` no XML (a posição já vem dos `cx`/`cy` dos círculos) — importante porque um `transform` de CSS aplicado num elemento que já tem um `transform` de atributo **substitui** o atributo inteiro (perderia a posição); mantendo os dois olhos "limpos" desse jeito, a única transformação deles é a que a animação de piscar aplica. `.pid-icon-eye`/`.is-blinking`/`@keyframes pidIconBlink` moram em `base.css` (`transform-box:fill-box; transform-origin:center` faz o `scaleY()` girar em torno do próprio olho, não da origem do SVG; `scaleY(1)→0.05→1`, 200ms, os dois olhos piscam juntos) — em `base.css`, não em `layout.css`, porque é carregado por **todas** as páginas, inclusive `index.html` (que não carrega `layout.css`).
|
||||
|
||||
- **Sidebar** (`static/js/sidebar-brand.js`, incluído logo depois de `api.js` nos 10 shells): `.sidebar__brand` deixou de ser `<div>` e virou `<a href="portal.html" id="sidebar-brand-link" aria-label="Ir para a tela Principal">` — a mudança de tag não afeta o CSS existente (todo seletor é por classe), então o crossfade descrito acima continua igual. O script escuta o clique: ignora cliques modificados (`ctrl`/`cmd`/`shift`/botão do meio — deixa abrir em nova aba normalmente), senão faz `preventDefault()`, adiciona `.is-blinking` em todo `.pid-icon-eye` dentro do link (pega os olhos das duas marcas — a visível e a escondida pelo crossfade, inofensivo já que a escondida tem `opacity:0`) e só navega pra `portal.html` depois de 260ms, tempo suficiente pra piscada terminar de tocar antes da página trocar — mesmo espírito de "deixar a transição ser vista antes de navegar" já usado na animação de intro do login.
|
||||
- **Login** (`static/js/login-logo-blink.js`, novo): `#login-logo-btn` é um `<button type="button">` (não um link — não há pra onde navegar a partir do próprio login), sem `preventDefault`/delay nenhum, só dispara a piscada ao clicar; é puramente decorativo, sem efeito colateral. Também aqui foi a oportunidade de simplificar um mecanismo que existia só por causa do `<img>`: a logo do login **alternava** entre `pid-icone.svg`(claro)/`pid-icone-escuro.svg`(escuro) conforme `data-theme`, via `pidSyncLoginLogo()` em `theme.js` — removida junto com essa mudança, a pedido do usuário ("deixe no tema claro a mesma logo usada no tema escuro"), já que o card do login já era congelado escuro nos dois temas mesmo antes disso (ver "Card de login" abaixo) — a lógica de alternância nunca fazia muito sentido nesse contexto. Agora `#login-logo-btn` sempre usa as cores/desenho de `pid-icone-escuro.svg`, sem nenhuma checagem de tema.
|
||||
Ver `docs/identidade-visual/identidade-visual.md` para o inventário de cada arquivo, quem consome cada um, o crossfade de `sidebar__brand` e o mecanismo de piscar os olhos do ícone (que exige `<svg>` inline, não `<img>`).
|
||||
|
||||
### Backend serve o frontend (mesma origem)
|
||||
|
||||
`config/urls.py` registra `path("api/", include("portal_api.urls"))` e, para cada página HTML do frontend (`index.html`, `portal.html`, `perfis-acesso.html`, `usuarios.html`, `calendario-individual.html`, `links-ferramentas.html`, `acessos-gerais.html`, `ramais.html`, `importacao-plano-saude.html`, `custo-contratacao.html`, `indicador-desempenho.html`, `nao-conformidades.html`), uma rota `TemplateView` que resolve o arquivo em `templates/`. Os estáticos (`static/css`, `static/js`, `static/img`) são servidos por `django.contrib.staticfiles` automaticamente em `DEBUG` (via `STATICFILES_DIRS`) — não há mais nenhum `re_path`/`static_serve` manual em `urls.py`. Cada template usa `{% load static %}` + `{% static 'css/tokens.css' %}` (nunca um caminho hardcoded tipo `assets/css/...`, que não existe mais). Essa escolha (Django servindo o próprio frontend) existe para evitar CORS/cookie cross-origin: autenticação é por **sessão/cookie do Django**, então frontend e API precisam estar na mesma origem.
|
||||
`config/urls.py` registra `path("api/", include("portal_api.urls"))` e, para cada uma das **14 páginas HTML** do frontend (`index.html` mais os 13 shells — ver a tabela em "Páginas" abaixo), uma rota `TemplateView` que resolve o arquivo em `templates/`. O 15º template, `dashboard-contabil-relatorio.html`, não tem rota própria: é renderizado por uma view do Relatório Contábil, não navegável pela URL. Os estáticos (`static/css`, `static/js`, `static/img`) são servidos por `django.contrib.staticfiles` automaticamente em `DEBUG` (via `STATICFILES_DIRS`) — não há mais nenhum `re_path`/`static_serve` manual em `urls.py`. Cada template usa `{% load static %}` + `{% static 'css/tokens.css' %}` (nunca um caminho hardcoded tipo `assets/css/...`, que não existe mais). Essa escolha (Django servindo o próprio frontend) existe para evitar CORS/cookie cross-origin: autenticação é por **sessão/cookie do Django**, então frontend e API precisam estar na mesma origem.
|
||||
|
||||
Em produção, rodar `python manage.py collectstatic` (junta tudo em `STATIC_ROOT = BASE_DIR / "staticfiles"`) e servir esse diretório via whitenoise/nginx — `django.contrib.staticfiles` só serve automaticamente quando `DEBUG=True`. Uploads de usuário (ícones de `LinkFerramenta`) são um mecanismo separado: `MEDIA_URL`/`MEDIA_ROOT` em `settings.py`, servidos por `config/urls.py` via `static()` só quando `DEBUG=True` (em produção, servir `media/` também por whitenoise/nginx, igual ao `STATIC_ROOT`).
|
||||
|
||||
### Apps Django
|
||||
|
||||
Um único app, `portal_api/`:
|
||||
Um único app Django, `portal_api/`. **Os models, serializers e views de todas as aplicações vivem nos arquivos compartilhados** (`models.py` com ~2.600 linhas, `views.py` com ~5.100, `serializers.py` com ~2.800) — os pacotes abaixo contêm só lógica pura, sem ORM. Consequência prática: mexer nos models ou nas views de uma aplicação **não** carrega o `CLAUDE.md` dela automaticamente, porque esses arquivos não estão dentro do pacote. Ao trabalhar num model `Contabil*`, `NaoConformidade*`, `Indicador*`, `ImportacaoPlanoSaude*`, `Ramal*`, `AcessoGeral*` ou `LinkFerramenta*`, abrir a doc da aplicação correspondente (tabela em "Documentação dividida por aplicação" acima).
|
||||
|
||||
| Arquivo | Conteúdo |
|
||||
|---|---|
|
||||
| `models.py` | `Usuario` (`AbstractUser` + `nome`, M2M `perfis`, M2M `departamentos` (pra `Departamento`, ver abaixo), campos cadastrais opcionais `codigo_folha`/`codigo_questor`/`codigo_tareffa`/`codigo_contabit`/`ramal` (`CharField`, `blank=True`) e `data_aniversario` (`DateField`, `null=True, blank=True`), `lideranca` (`BooleanField`, é gerente/coordenador) e M2M `liderados` (self-referential, `symmetrical=False`, `related_name="lideres"` — ver `docs/perfis-usuarios/perfis-usuarios.md`) — `email` já vem de `AbstractUser`, não precisou de campo novo; método `permissao_app(module_key, app_key)` — união genérica de um flag de `apps` entre os perfis vinculados; método `eh_perfil_inovacao()` — checagem de nome fixo pro perfil "Inovação", ver "Ajuda de aplicação" abaixo), `AjudaAplicacao` (texto de "Mais informações" de uma aplicação, chave natural `app_key`), `Departamento` (só `nome`, `unique=True` — cadastro inline pela própria tela de Usuários, sem tela de administração dedicada como `PerfilAcesso`), `PerfilAcesso` (`permissoes` em `JSONField`, mesmo formato aninhado do frontend; mais o booleano dedicado `gerencia_permissoes`), `CompromissoAgenda`, `Favorito`, `WidgetUsuario`, `NotificacaoDispensada`, `LinkFerramenta` (`icone` é `ImageField`, requer Pillow), `LinkFerramentaFavorito` (favorito por usuário de um cartão de Links & Ferramentas — não confundir com `Favorito`), `AcessoGeralSecao`/`AcessoGeral` (cadastro de logins/acessos compartilhados da aplicação "Acessos Gerais", ver `docs/links-ferramentas-acessos-gerais/links-ferramentas-acessos-gerais.md`), `Ramal` (linha **avulsa** da tela de Ramais, sem `Usuario` por trás — colaboradores de verdade aparecem automaticamente na listagem, sem precisar de uma linha aqui; ver `docs/ramais/ramais.md`), `RamalAusencia` (período de ausência de um colaborador, com `esta_ativa()` calculando "ausente agora" em vez de armazenar), `TelefoneExterno` (subtela "Telefones Externos" de Ramais, sem `Usuario` por trás), `FuncaoTelefonia` (subtela "Funções de Telefonia" de Ramais, `Meta.ordering` por `comando` reproduz a ordem esperada sem campo de ordem manual), `ImportacaoPlanoSaude`/`ImportacaoPlanoSaudeLinha`/`ImportacaoPlanoSaudeAuditoria`/`ImportacaoPlanoSaudeAlteracao`/`VinculoNomeOperadora` (ferramenta "Importação de Plano de Saúde" em Utilitários, ver `portal_api/planos_saude/CLAUDE.md`, inclusive "Vínculos de nome salvos (DE/PARA)"). |
|
||||
| `catalogo.py` | Fonte única da verdade do catálogo de módulos/aplicações do menu (`MODULES`, `MODULE_APPS`) — exposto só leitura via `GET /api/catalogo/`. Ao adicionar uma seção/aplicação nova ao menu, editar **aqui**, não em `static/js/profiles.js` (que só cacheia o payload recebido). |
|
||||
| `serializers.py` | `PerfilAcessoSerializer`, `DepartamentoSerializer`, `UsuarioResumoSerializer` (`id`/`nome`/`departamentos`, usado nos dois lados de `liderados` e por `/api/usuarios-resumo/`), `UsuarioSerializer` (escrita, aceita `senha`+`perfis`+`departamentos`+`liderados`)/`UsuarioListSerializer` (leitura, `perfis`/`departamentos`/`liderados` aninhados), `CompromissoAgendaSerializer` (`sou_dono`, `dono_nome`, `dono_username`), `FavoritoSerializer`, `WidgetUsuarioSerializer`, `NotificacaoDispensadaSerializer`, `LinkFerramentaSerializer`, `LinkFerramentaFavoritoSerializer`, `AcessoGeralSecaoSerializer`, `AcessoGeralSerializer`, `RamalSerializer` (só das linhas avulsas — ver seção "Ramais"), `RamalAusenciaSerializer`, `TelefoneExternoSerializer`, `FuncaoTelefoniaSerializer`, `ImportacaoPlanoSaudeCreateSerializer`/`ImportacaoPlanoSaudeListSerializer`/`ImportacaoPlanoSaudeDetailSerializer`/`ImportacaoPlanoSaudeLinhaSerializer`/`ImportacaoPlanoSaudeAuditoriaSerializer` (ver seção "Importação de Plano de Saúde"). |
|
||||
| `permissions.py` | `PodeGerenciarPermissoes` — gate único de `gerencia_permissoes()` para as telas administrativas; `PermissaoApp(module_key, app_key)` — classe genérica reutilizável que checa `Usuario.permissao_app()`, instanciada por view (ex.: Links & Ferramentas e Ramais, ver `docs/links-ferramentas-acessos-gerais/links-ferramentas-acessos-gerais.md`/`docs/ramais/ramais.md`). |
|
||||
| `views.py` | `login_view`/`logout_view`/`csrf_view` (auth por sessão), `me_view` (usuário + `permissoes_efetivas` já unidas no servidor + `lideranca`/`liderados`), `trocar_senha_view`, `usuarios_resumo_view`, `meus_liderados_view` (ver seção "Liderança"), `departamentos_resumo_view` (ver "Ramais"), `catalogo_view`, e os `ModelViewSet` de perfis/departamentos/usuários/compromissos/favoritos/widgets/notificações dispensadas/links e ferramentas/favoritos de links e ferramentas/seções e linhas de Acessos Gerais/ramais/ausências de ramal/importações de plano de saúde e suas linhas. |
|
||||
| `models.py` | Todos os models do projeto. Os transversais: `Usuario` (`AbstractUser` + `nome`, M2M `perfis`, M2M `departamentos`, campos cadastrais opcionais `codigo_folha`/`codigo_questor`/`codigo_tareffa`/`codigo_contabit`/`ramal`, `data_aniversario`, `lideranca` e M2M `liderados` self-referential com `related_name="lideres"`; método `permissao_app(module_key, app_key)` — união genérica de um flag de `apps` entre os perfis vinculados; método `eh_perfil_inovacao()` — checagem de nome fixo), `PerfilAcesso` (`permissoes` em `JSONField` + o booleano dedicado `gerencia_permissoes`), `Departamento` (só `nome`, cadastrado inline pela tela de Usuários, sem tela própria), `AjudaAplicacao`, `CompromissoAgenda`, `Favorito`, `WidgetUsuario`, `NotificacaoDispensada`. Os models de cada aplicação estão documentados na doc dela. |
|
||||
| `catalogo.py` | Fonte única da verdade do catálogo de módulos/aplicações do menu (`MODULES`, `MODULE_APPS`) — exposto só leitura via `GET /api/catalogo/`. Ao adicionar uma seção/aplicação nova ao menu, editar **aqui**, nunca em `static/js/profiles.js` (que só cacheia o payload recebido). |
|
||||
| `serializers.py` | Idem: todos os serializers. Os transversais são `PerfilAcessoSerializer`, `DepartamentoSerializer`, `UsuarioResumoSerializer` (`id`/`nome`/`departamentos`, usado nos dois lados de `liderados` e por `/api/usuarios-resumo/`), `UsuarioSerializer` (escrita) / `UsuarioListSerializer` (leitura, aninhados), `CompromissoAgendaSerializer`, `FavoritoSerializer`, `WidgetUsuarioSerializer`, `NotificacaoDispensadaSerializer`, `AjudaAplicacaoSerializer`. Também as constantes de allowlist do `nh3` (`RICHTEXT_ALLOWED_TAGS`/`_ATTRS`/`_SCHEMES`), compartilhadas por todos os campos de texto rico do projeto. |
|
||||
| `permissions.py` | `PodeGerenciarPermissoes` — gate único de `gerencia_permissoes()` para as telas administrativas; `PermissaoApp(module_key, app_key)` — classe genérica reutilizável que checa `Usuario.permissao_app()`, instanciada por view. Nenhuma subclasse nova é necessária para uma aplicação adotar o padrão, só instanciar com outra `app_key`. |
|
||||
| `views.py` | Idem: todas as views. As transversais são `login_view`/`logout_view`/`csrf_view`, `me_view` (usuário + `permissoes_efetivas` já unidas no servidor + `lideranca`/`liderados` + `eh_perfil_inovacao`), `trocar_senha_view`, `usuarios_resumo_view`, `meus_liderados_view`, `departamentos_resumo_view`, `catalogo_view`, `ajuda_aplicacao_view`, e os `ModelViewSet` de perfis/departamentos/usuários/compromissos/favoritos/widgets/notificações dispensadas. |
|
||||
| `admin.py` | Django admin básico para todos os models (uso interno, não é a UI do portal). |
|
||||
| `management/commands/seed_portal.py` | Recria os 8 perfis padrão + `gabriel`/`bruno` + as 13 linhas de `FuncaoTelefonia` + o seed de `CategoriaEvento`; também realinha a sequence do Postgres por trás de `PerfilAcesso.codigo` (ver nota abaixo). |
|
||||
| `management/commands/seed_indicador_desempenho.py` | Popula o primeiro histórico do Indicador de Desempenho (7 `IndicadorCriterio` + 5 `IndicadorPercentualTipo`, idempotente) com os valores da planilha antiga — ver `portal_api/indicadores/CLAUDE.md`. |
|
||||
| `planos_saude/` | Pacote Python puro (sem ORM) com o pipeline de extração/casamento de "Importação de Plano de Saúde", portado de `projects/project/` — ver `portal_api/planos_saude/CLAUDE.md`. |
|
||||
| `custo_contratacao/` | Pacote Python puro (sem ORM) da ferramenta "Simulação de Custo de Contratação" (Geradoc) — `tabelas.py` (seed/default das faixas de INSS/IRRF, hoje editáveis via `ParametroFiscalCustoContratacao`), `calculo.py` (`ParametrosFiscais` dataclass + `calcula_custo_empregado`), `pdf.py` (`gera_pdf_simulacao`, via `reportlab`). Ver `portal_api/custo_contratacao/CLAUDE.md`. |
|
||||
| `indicadores/` | Pacote Python puro (sem ORM) da ferramenta "Indicador de Desempenho" (Geradoc) — `tipos.py` (deriva o tipo de colaborador por empresa via Tareffa), `leiaute.py` (leitura das planilhas Tareffa/Honorários via `openpyxl`), `pipeline.py` (orquestração, `processa_apuracao`), `entregas.py` (cálculo dos 3 critérios automáticos), `calculo.py` (composição dos percentuais Individual/Grupo/Departamento e valores em R$), `recibo.py` (PDF do recibo por colaborador, via `reportlab`). Ver `portal_api/indicadores/CLAUDE.md`. |
|
||||
| `templatetags/contabil_extras.py` | Filtros de template (`moeda`/`percentual`/`indice`/`competencia`/`mes_curto`/`moeda_av`/`percentual_av`/`numero_bruto`) — único uso de template tags customizadas no projeto, só pelo relatório do Relatório Contábil. |
|
||||
| `management/commands/seed_portal.py` | Cria os 8 perfis padrão + `gabriel`/`bruno` + as 13 linhas de `FuncaoTelefonia` + o seed de `CategoriaEvento`; também realinha a sequence do Postgres por trás de `PerfilAcesso.codigo`. **Não rodar neste ambiente** — ver "Como rodar / testar localmente" acima. |
|
||||
| `management/commands/seed_indicador_desempenho.py` | Popula o primeiro histórico do Indicador de Desempenho (7 `IndicadorCriterio` + 5 `IndicadorPercentualTipo`, idempotente). |
|
||||
|
||||
Pacotes Python puros (sem ORM), um por ferramenta — cada um com seu próprio `CLAUDE.md`/`README.md`/`CHANGELOG.md`:
|
||||
|
||||
| Pacote | Ferramenta |
|
||||
|---|---|
|
||||
| `planos_saude/` | Pipeline de extração/casamento de "Importação de Plano de Saúde" (parsers por operadora, `matcher`, leiaute do Questor, regras de custeio). Serve as **duas** instâncias da ferramenta (clientes e De Paula). |
|
||||
| `custo_contratacao/` | "Simulação de Custo de Contratação" (Geradoc) — `tabelas.py` (faixas de INSS/IRRF), `calculo.py`, `pdf.py` via `reportlab`. |
|
||||
| `indicadores/` | "Indicador de Desempenho" (Geradoc) — `tipos.py`, `leiaute.py` (`openpyxl`), `pipeline.py`, `entregas.py`, `calculo.py`, `recibo.py` (PDF via `reportlab`), `departamentos.py`. |
|
||||
| `nao_conformidades/` | "Não Conformidades" (Relatórios > Qualidade) — leiautes dos exports do Sigsistem, `diff.py` (reabertura automática), `classificacao.py`, `pipeline.py`. |
|
||||
| `dashboard_contabil/` | "Relatório Contábil" (Relatórios > Contabilidade) — `parser.py` (extração do PDF), `regras.py` (motor de auditoria), `formula.py` (avaliador de fórmula por `ast`), `indicadores.py`, `exportacao.py` (XLSX), `resumo_pdf.py`. |
|
||||
|
||||
### API (sessão + CSRF, não token)
|
||||
|
||||
@ -154,42 +149,8 @@ Um único app, `portal_api/`:
|
||||
| `/api/favoritos/`, `/api/favoritos/{app_id}/` | GET/POST/PATCH/DELETE | chave natural é `app_id`, não um id numérico; `ordem` é gravável via PATCH (drag-and-drop na grade de favoritos, ver `docs/favoritos/favoritos.md`) |
|
||||
| `/api/widgets/`, `/api/widgets/{tipo}/` | GET/POST/PATCH/DELETE | chave natural é `tipo`; `ordem` (reordenar por drag-and-drop) e `largura`/`altura` em px (redimensionamento) também são graváveis via PATCH — ver `docs/calendario-individual/calendario-individual.md` |
|
||||
| `/api/notificacoes-dispensadas/`, `/api/notificacoes-dispensadas/{notif_id}/` | GET/POST/DELETE | chave natural é `notif_id` (ex.: `"tool-widgets"`, `"event-42"`); `notifications.js` usa GET pra filtrar o que já foi dispensado e POST a cada X/"Limpar tudo" |
|
||||
| `/api/links-ferramentas/`, `/api/links-ferramentas/{id}/` | GET/POST/PATCH/DELETE | lista **compartilhada** (não por usuário); leitura exige `apps["links-ferramentas-visualizar"]` e escrita exige `apps["links-ferramentas-editar"]` em `permissoes["links-ferramentas"]` (`PermissaoApp`, gate por método em `get_permissions()` — ver "Modelo de permissões" abaixo); POST é `multipart/form-data` (aceita upload de `icone`); `ordem` sempre é atribuída pelo servidor na criação (ignora o que vier no payload), reordenar é PATCH trocando o `ordem` de dois itens |
|
||||
| `/api/links-ferramentas-favoritos/`, `/api/links-ferramentas-favoritos/{link_id}/` | GET/POST/DELETE | favorito **por usuário** de um cartão (chave natural é `link_id`, o id do `LinkFerramenta` — mesmo padrão de `app_id`/`notif_id`); exige só `apps["links-ferramentas-visualizar"]` (favoritar não precisa de editar); só afeta a ordem de exibição em Links & Ferramentas e o widget "Links Favoritos", nunca o `ordem` compartilhado (ver `docs/links-ferramentas-acessos-gerais/links-ferramentas-acessos-gerais.md`) |
|
||||
| `/api/acessos-gerais-secoes/`, `/api/acessos-gerais-secoes/{id}/` | GET/POST/PATCH/DELETE | seções do cadastro "Acessos Gerais" (aplicação dentro da seção Links & Ferramentas); leitura exige `apps["acessos-gerais-visualizar"]`, escrita exige `apps["acessos-gerais-editar"]`; excluir uma seção também exclui (`CASCADE`) os acessos dela; GET só lista seções sem `perfis_restritos` ou com interseção com os perfis do usuário (ver `docs/links-ferramentas-acessos-gerais/links-ferramentas-acessos-gerais.md`) |
|
||||
| `/api/acessos-gerais/`, `/api/acessos-gerais/{id}/` | GET/POST/PATCH/DELETE | linhas (acessos/logins) dentro de uma seção; mesma permissão de `acessos-gerais-secoes`; `ordem` é escopada por `secao` (servidor calcula `max(ordem)` só entre as linhas da mesma seção) — ver `docs/links-ferramentas-acessos-gerais/links-ferramentas-acessos-gerais.md` |
|
||||
| `/api/ramais/` | GET | diretório **mesclado**: todo usuário ativo (linha montada do cadastro, sem precisar de nenhum registro extra) + linhas avulsas de `Ramal`; leitura exige `apps.visualizar` — ver `docs/ramais/ramais.md` |
|
||||
| `/api/ramais/`, `/api/ramais/{id}/` | POST/PATCH/DELETE | CRUD só das linhas avulsas (`Ramal`, sem `Usuario` por trás); escrita exige `apps.editar` |
|
||||
| `/api/ramais/usuarios/` | GET | lista enxuta (`id`/`nome`) de usuários ativos pra alimentar o `<select>` "Lista de Usuários" do modal de Criar Ausência — não é `/api/usuarios/` de propósito (ver `docs/ramais/ramais.md`) |
|
||||
| `/api/ramais/usuarios/{usuario_id}/` | PATCH | `{numero}` — grava direto em `Usuario.ramal`; é como a tela edita o ramal de um colaborador de verdade (exige `apps.editar`) |
|
||||
| `/api/ramais-ausencias/`, `/api/ramais-ausencias/{id}/` | GET/POST/PATCH/DELETE | períodos de ausência; "ausente agora" nunca é lido daqui direto pelo frontend, vem calculado em `usuario_ausente`/`usuario_ausencia_ativa_id` na listagem de `/api/ramais/`; `PATCH` com `{"encerrada_manualmente": true}` encerra antes do previsto |
|
||||
| `/api/telefones-externos/`, `/api/telefones-externos/{id}/` | GET/POST/PATCH/DELETE | subtela "Telefones Externos" de `ramais.html`; mesma permissão `PermissaoApp("ramais", ...)` do diretório de Ramais |
|
||||
| `/api/funcoes-telefonia/`, `/api/funcoes-telefonia/{id}/` | GET/POST/PATCH/DELETE | subtela "Funções de Telefonia" de `ramais.html`; idem, mesma permissão de `ramais`; as 13 linhas padrão vêm de `seed_portal` |
|
||||
| `/api/importacoes-plano-saude/`, `/api/importacoes-plano-saude/{id}/` | GET/POST | histórico + criação (ver `portal_api/planos_saude/CLAUDE.md`); `PermissaoApp("utilitarios", "importacao-plano-saude")` (toggle único) pra todos os métodos; POST é `multipart/form-data` (planilha padrão + arquivo da operadora) e roda o pipeline de forma síncrona antes de responder |
|
||||
| `/api/importacoes-plano-saude/operadoras/` | GET | `[{key, label}]` das operadoras registradas em `planos_saude.pipeline.OPERADORAS` — alimenta o `<select>` do formulário |
|
||||
| `/api/importacoes-plano-saude/regras-empresa/` | GET | `[{key, label}]` das regras especiais registradas em `planos_saude.regras_empresa.REGRAS_EMPRESA` — alimenta o modal "Selecionar regra" do checkbox "Regra empresa" (ver `portal_api/planos_saude/CLAUDE.md`) |
|
||||
| `/api/importacoes-plano-saude/{id}/gerar/` | POST | monta o CSV (ou ZIP, se mais de um tipo de lançamento) a partir das linhas já revisadas/editadas e devolve como download binário; marca a importação como `concluida` |
|
||||
| `/api/importacoes-plano-saude-linhas/`, `/api/importacoes-plano-saude-linhas/{id}/` | GET/POST/PATCH/DELETE | edição/inclusão/exclusão de uma linha da revisão (todos os campos, não só valores); mesma permissão da importação, sem conceito de "dono"; as três operações também gravam um `ImportacaoPlanoSaudeAlteracao` (ver `portal_api/planos_saude/CLAUDE.md`) |
|
||||
| `/api/importacoes-plano-saude-alteracoes/{id}/reverter/` | POST | desfaz uma alteração específica (edição/inclusão/exclusão de linha) registrada na aba "Alterações" da revisão — ver `portal_api/planos_saude/CLAUDE.md` |
|
||||
| `/api/regras-custeio-plano-saude/`, `/api/regras-custeio-plano-saude/{id}/` | GET/POST/PATCH/DELETE | banco de regras de custeio por empresa+operadora (`codigo_empresa`+`operadora`, únicos juntos+`regra_empresa_chave`+`tipos_lancamento`+`custeio_por_tipo`+`observacoes`; `nome` é sempre derivado, nunca aceito do cliente — ver `portal_api/planos_saude/CLAUDE.md`) — mesma permissão de toggle único da ferramenta; lista compartilhada, sem "dono"; cadastro/edição só pela tela "Cadastro de Regras", nunca em "Nova Importação" |
|
||||
| `/api/simulacao-custo-contratacao/gerar/` | POST | calcula (`portal_api.custo_contratacao.calculo.calcula_custo_empregado`) e devolve o PDF direto na resposta (`application/pdf`, sem persistir nada); `PermissaoApp`-like check manual via `permissao_app("geradoc", "simulacao-custo-contratacao")` — ver `portal_api/custo_contratacao/CLAUDE.md` |
|
||||
| `/api/parametros-fiscais-custo-contratacao/` | GET/PATCH | tabelas de INSS/IRRF + parâmetros da Lei 15.270/2025 usados pela simulação (`ParametroFiscalCustoContratacao`, singleton `pk=1`); mesma permissão da simulação, sem par visualizar/editar dedicado |
|
||||
| `/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" (ver `portal_api/indicadores/CLAUDE.md`) |
|
||||
| `/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, ver `portal_api/indicadores/CLAUDE.md` |
|
||||
| `/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, ver `portal_api/indicadores/CLAUDE.md` |
|
||||
| `/api/indicadores-departamentos-gerentes/`, `/api/indicadores-departamentos-gerentes/{id}/` | GET/POST/PATCH/DELETE | relação gerente→departamento (`IndicadorDepartamentoGerente`, `nome_gerente` único) — mesma permissão, ver `portal_api/indicadores/CLAUDE.md` |
|
||||
| `/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 (ver `portal_api/indicadores/CLAUDE.md`) |
|
||||
| `/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 (ver `portal_api/indicadores/CLAUDE.md`) |
|
||||
| `/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 (ver `portal_api/indicadores/CLAUDE.md`) |
|
||||
| `/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 |
|
||||
|
||||
**Esta tabela lista só os endpoints transversais.** Os de cada aplicação moram na doc dela (ver "Documentação dividida por aplicação" acima): Links & Ferramentas/Acessos Gerais, Ramais/Telefones Externos/Funções de Telefonia, Importação de Plano de Saúde (as duas instâncias), Simulação de Custo de Contratação, Indicador de Desempenho, Não Conformidades e Relatório Contábil. Ao criar uma aplicação nova, documentar os endpoints dela lá, não aqui.
|
||||
|
||||
### Frontend consumindo a API
|
||||
|
||||
@ -214,6 +175,7 @@ Um único app, `portal_api/`:
|
||||
| `acessos-gerais.html` | Cadastro de acessos/logins compartilhados organizados em seções e linhas, com popup de detalhes; criar/editar/excluir seções e linhas só com `apps["acessos-gerais-editar"]` (ver `docs/links-ferramentas-acessos-gerais/links-ferramentas-acessos-gerais.md`). |
|
||||
| `ramais.html` | Diretório de ramais internos, filtros por nome/departamento; adicionar/editar ramal, criar ausência e excluir só com `apps.editar` em `ramais` (ver `docs/ramais/ramais.md`). |
|
||||
| `importacao-plano-saude.html` | Ferramenta de Utilitários: histórico + cadastro de regras de custeio por empresa (separado da execução) + nova importação (upload, só aplica uma regra já cadastrada) + revisão/geração do arquivo de lançamento de plano de saúde (ver `portal_api/planos_saude/CLAUDE.md`). |
|
||||
| `importacao-plano-saude-de-paula.html` | Segunda instância da ferramenta acima, para o plano de saúde dos **próprios colaboradores** do escritório: tabelas, endpoints e permissão próprios, mas mesmo JS/CSS (parametrizados por `window.PID_IPS_CONFIG`) e mesmo pipeline de extração. Ver "Importação de Plano de Saúde - De Paula" em `portal_api/planos_saude/CLAUDE.md`. |
|
||||
| `custo-contratacao.html` | Ferramenta de Geradoc: formulário de simulação de custo de contratação (Empregado CLT) + painel colapsável de parâmetros fiscais, gera um PDF (ver `portal_api/custo_contratacao/CLAUDE.md`). |
|
||||
| `indicador-desempenho.html` | Ferramenta de Geradoc: histórico de apurações + nova apuração (upload das 2 planilhas) + cadastro de critérios/percentuais + revisão/geração dos recibos em PDF do Indicador de Desempenho do Fiscontábil (ver `portal_api/indicadores/CLAUDE.md`). |
|
||||
| `nao-conformidades.html` | Aplicação de Relatórios > Qualidade: gestão contínua das ocorrências/ações do Sigsistem, com dashboard e status interno de tratativa da Qualidade (ver `portal_api/nao_conformidades/CLAUDE.md`). |
|
||||
@ -242,7 +204,7 @@ Padrão de guarda por página: `profiles.js`, `users-admin.js` e `widgets.js` ve
|
||||
|
||||
**Notificação de ferramenta expira em 10 dias** (`PID_NOTIF_TOOL_EXPIRA_DIAS`, `pidNotifToolExpirada()`, comparando `dataIso` da entrada contra a data de hoje): passado esse prazo, `notifications.js` dispensa a notificação sozinho no próprio carregamento da página (`POST /api/notificacoes-dispensadas/`, mesma chamada de quando o usuário clica no X) — daí em diante ela segue as mesmas regras de qualquer notificação dispensada manualmente (some do sino, aparece no histórico, pode ser restaurada). `toolNotificationsAgora` (o recorte usado pro sino, tanto na carga inicial quanto depois de um "Restaurar") já exclui as expiradas por prazo — restaurar uma notificação de ferramenta com mais de 10 dias mantém o rastro no histórico mas não a traz de volta ao sino, mesmo espírito de "restaurar um compromisso antigo não garante reaparecer no sino" (ver abaixo). Ao adicionar uma entrada nova, usar `dataIso` no formato `"AAAA-MM-DD"` (não `date` pré-formatado como antes) — `date`/`pidFormatNotifDate()` derivam o `"DD/MM"` de exibição a partir dele, mesmo padrão já usado pelos eventos do Calendário Individual.
|
||||
|
||||
**Histórico de notificações** (botão "Histórico" ao lado de "Limpar tudo", em `#notif-history-modal` — presente nos 7 shells que têm o sino, tudo exceto `index.html`): não é um model novo, é uma segunda leitura da mesma tabela `NotificacaoDispensada` — o sino ativo mostra `!dismissedIds.includes(id)`, o histórico mostra o inverso (`dismissedIds.includes(id)`). A diferença entre os dois pools de candidatos usados (`notifications.js`) é proposital: o sino usa `pidEventosElegiveisAgora()`/`toolNotificationsAgora` (só eventos com `data >= hoje` e `notificar_em` já atingido, capado em 5 pra não lotar o dropdown; só notificações de ferramenta dentro dos 10 dias de prazo), enquanto o histórico usa `allEventNotifications`/`toolNotifications` (todos os compromissos que o usuário pode ver e toda notificação de ferramenta que ele tem permissão de ver, sem o recorte de "agora") — um item dispensado pode não estar mais no recorte "elegível agora" (compromisso já passou, lembrete não bateu ainda depois de uma edição, ou notificação de ferramenta já passou dos 10 dias), mas ainda precisa aparecer no histórico. Cada item do histórico tem um botão "Restaurar" (`DELETE /api/notificacoes-dispensadas/{notif_id}/`, já existia como endpoint, só não tinha consumidor no frontend) que remove o registro de dispensa; a notificação só volta a aparecer no sino de fato se ainda estiver no pool "elegível agora" (restaurar um compromisso muito antigo, ou uma notificação de ferramenta com mais de 10 dias, não reaparece no sino — o histórico continua mostrando, já que sua lista não tem esse recorte).
|
||||
**Histórico de notificações** (botão "Histórico" ao lado de "Limpar tudo", em `#notif-history-modal` — presente nos 13 shells que têm o sino, tudo exceto `index.html`): não é um model novo, é uma segunda leitura da mesma tabela `NotificacaoDispensada` — o sino ativo mostra `!dismissedIds.includes(id)`, o histórico mostra o inverso (`dismissedIds.includes(id)`). A diferença entre os dois pools de candidatos usados (`notifications.js`) é proposital: o sino usa `pidEventosElegiveisAgora()`/`toolNotificationsAgora` (só eventos com `data >= hoje` e `notificar_em` já atingido, capado em 5 pra não lotar o dropdown; só notificações de ferramenta dentro dos 10 dias de prazo), enquanto o histórico usa `allEventNotifications`/`toolNotifications` (todos os compromissos que o usuário pode ver e toda notificação de ferramenta que ele tem permissão de ver, sem o recorte de "agora") — um item dispensado pode não estar mais no recorte "elegível agora" (compromisso já passou, lembrete não bateu ainda depois de uma edição, ou notificação de ferramenta já passou dos 10 dias), mas ainda precisa aparecer no histórico. Cada item do histórico tem um botão "Restaurar" (`DELETE /api/notificacoes-dispensadas/{notif_id}/`, já existia como endpoint, só não tinha consumidor no frontend) que remove o registro de dispensa; a notificação só volta a aparecer no sino de fato se ainda estiver no pool "elegível agora" (restaurar um compromisso muito antigo, ou uma notificação de ferramenta com mais de 10 dias, não reaparece no sino — o histórico continua mostrando, já que sua lista não tem esse recorte).
|
||||
|
||||
## Modelo de permissões (Perfis de Acesso)
|
||||
|
||||
@ -299,26 +261,26 @@ Duas telas administrativas por cima desse modelo (popup "Nova Aplicação" de `p
|
||||
|
||||
## Ajuda de aplicação ("Mais informações")
|
||||
|
||||
Botão "?" (`.info-tooltip`, `components.css`) ao lado do nome de uma aplicação — ao passar o mouse mostra a dica "Mais informações" (tooltip CSS puro, sem JS de posicionamento, já que a posição relativa ao próprio botão nunca varia); ao clicar, abre um modal com um texto livre descrevendo objetivo/processo/cuidados/resultado esperado daquela ferramenta. Visualizar é liberado a qualquer usuário autenticado; **editar é restrito a quem tem o perfil "Inovação" vinculado** — uma checagem de **nome fixo** (`Usuario.eh_perfil_inovacao()`/`models.PERFIL_INOVACAO_NOME`, mesmo padrão já usado pro selo "Restrito" de Relatórios Gerenciais, nome `=== "Diretoria"`), **não** uma flag na árvore de permissões — decisão explícita do usuário, pra não precisar aparecer em Perfis de Acesso.
|
||||
Botão "?" (`.info-tooltip`, `components.css`) ao lado do nome de uma aplicação: no hover mostra "Mais informações" (tooltip CSS puro, sem JS de posicionamento, já que a posição relativa ao próprio botão nunca varia); no clique abre um modal com um texto livre descrevendo objetivo/processo/cuidados/resultado esperado daquela ferramenta.
|
||||
|
||||
- **Model** (`AjudaAplicacao`): chave natural `app_key` (mesma ideia de `app_id`/`notif_id`/`tipo` de `Favorito`/`NotificacaoDispensada`/`WidgetUsuario` — sem FK pra `Usuario`, é um texto compartilhado, igual pra quem abrir o modal) + `texto` (`TextField`, `blank=True`, `validators=[validar_tamanho_texto_ajuda_aplicacao]` — mesmo teto de 2.000.000 caracteres de `AcessoGeral.observacoes`, generoso o bastante pra várias imagens embutidas) + `atualizado_em`/`atualizado_por`. `AjudaAplicacao.para_app(app_key)` faz `get_or_create` — mesmo padrão de "singleton por chave, criado sob demanda" já usado em `ParametroFiscalCustoContratacao.atual()`/`EmpresaQuestor`.
|
||||
- **Endpoint** `GET`/`PATCH /api/ajuda-aplicacoes/<app_key>/` (`ajuda_aplicacao_view`, função simples — não é `ModelViewSet`, mesmo estilo de `parametros_fiscais_custo_contratacao_view`): GET só exige `IsAuthenticated`; PATCH também exige `request.user.eh_perfil_inovacao()`, senão `PermissionDenied`. `GET /api/me/` ganhou o campo `eh_perfil_inovacao` (calculado no servidor, igual a `gerencia_permissoes`) pra o frontend saber se mostra os botões "Editar" do modal.
|
||||
- **Texto aceita imagens embutidas, igual às Observações de Acessos Gerais** (pedido explícito do usuário) — mesmo mecanismo, reaproveitado: `<div contenteditable>` no cliente com colar (`Ctrl+V`)/arrastar imagem (`insertImageFile()`, até 2MB), convertida em data URI e inserida via `document.execCommand("insertImage", ...)`; sanitizado no servidor com `nh3.clean()` antes de salvar (`AjudaAplicacaoSerializer.validate_texto`). As constantes de allowlist do nh3 (`RICHTEXT_ALLOWED_TAGS`/`_ATTRS`/`_SCHEMES`, `serializers.py`) foram **generalizadas** (antes prefixadas `ACESSO_GERAL_*`) pra serem compartilhadas pelos dois campos — mesmo allowlist estrito (texto básico + `<img>`, sem `<a>`/`<script>`/atributos de evento, `data:` liberado pra imagem embutida). A UI em si (`.ajuda-modal__editor`/`.ajuda-modal__editor-hint` em `components.css`) é uma **reimplementação** de `.ag-richtext` (não um reaproveitamento direto), porque este modal pode aparecer em qualquer página, e `.ag-richtext`/`acessos-gerais.js` são escopados só a `acessos-gerais.html`.
|
||||
- **Frontend** (`static/js/ajuda-aplicacao.js`, reusável — igual ao espírito de `dual-select.js`): `pidCriarBotaoAjuda(botaoId, appKey, tituloApp)` liga o clique de um botão já existente no HTML da página; o modal em si (`#ajuda-aplicacao-modal`) é **criado sob demanda e injetado em `document.body` pelo próprio JS** (não precisa ser hand-authored em cada página) e reaproveitado por todos os botões dela — só um pode estar aberto por vez. Texto em modo leitura via `innerHTML` (seguro porque já vem sanitizado do backend, mesmo raciocínio de `.ag-view-observacoes`); quem tem `eh_perfil_inovacao` vê um botão "Editar" que troca pro editor rico in-place (mesmo padrão "Cancelar"/"Salvar" já usado em "Editar Ausência" de Ramais).
|
||||
- **Confirmação ao sair sem salvar** (pedido explícito do usuário): enquanto o modal está em modo edição (`modal.dataset.editing = "true"`, setado por `entrarEdicao()`/limpo por `renderVisualizacao()`), fechar o modal por qualquer caminho — botão "Fechar", clicar fora (overlay) ou "Cancelar" — dispara `await pidConfirm("Sair sem salvar as alterações?", { perigoso: true })` (ver "Modal de confirmação genérico" abaixo); só fecha/descarta se confirmado. "Salvar" nunca pede confirmação (não há o que descartar). `pidFecharAjudaAplicacaoModal()` é a única função que fecha o modal de fato (agora `async`), então o botão "Fechar" e o clique no overlay (que já chamavam essa função) ganharam a checagem de graça; só "Cancelar" precisou de uma checagem própria antes de chamar `renderVisualizacao()`.
|
||||
**Visualizar é livre a qualquer autenticado; editar é restrito a quem tem o perfil "Inovação" vinculado** — uma checagem de **nome fixo** (`Usuario.eh_perfil_inovacao()`/`models.PERFIL_INOVACAO_NOME`), **não** uma flag na árvore de permissões. Decisão explícita do usuário, pra não precisar aparecer em Perfis de Acesso. Mesmo padrão do selo "Restrito" de Relatórios Gerenciais (nome `=== "Diretoria"`).
|
||||
|
||||
- **Model `AjudaAplicacao`**: chave natural `app_key` (mesma ideia de `app_id`/`notif_id`/`tipo` — sem FK pra `Usuario`, é um texto compartilhado) + `texto` + `atualizado_em`/`atualizado_por`. `AjudaAplicacao.para_app(app_key)` faz `get_or_create`, padrão "singleton por chave criado sob demanda" já usado em `ParametroFiscalCustoContratacao.atual()`.
|
||||
- **Endpoint** `GET`/`PATCH /api/ajuda-aplicacoes/<app_key>/` (função simples, não `ModelViewSet`): GET exige só `IsAuthenticated`, PATCH também exige `eh_perfil_inovacao()`. `GET /api/me/` expõe `eh_perfil_inovacao` pro frontend decidir se mostra o botão "Editar".
|
||||
- **Texto aceita imagens embutidas**: `<div contenteditable>` com colar/arrastar imagem (até 2MB), convertida em data URI, sanitizada no servidor com `nh3.clean()`. As constantes de allowlist (`RICHTEXT_ALLOWED_TAGS`/`_ATTRS`/`_SCHEMES` em `serializers.py`) são **compartilhadas** por este campo, por `AcessoGeral.observacoes` e por `ContabilApuracao.resumo_fechamento` — allowlist estrito (texto básico + `<img>`, sem `<a>`/`<script>`/atributos de evento, `data:` liberado só pra imagem embutida).
|
||||
- **Frontend** (`static/js/ajuda-aplicacao.js`, reusable no espírito de `dual-select.js`): `pidCriarBotaoAjuda(botaoId, appKey, tituloApp)` liga o clique de um botão já existente no HTML. O modal (`#ajuda-aplicacao-modal`) é **criado sob demanda e injetado em `document.body` pelo próprio JS**, não precisa ser escrito à mão em cada página. Fechar em modo edição (por qualquer caminho: "Fechar", overlay ou "Cancelar") dispara `pidConfirm("Sair sem salvar as alterações?", { perigoso: true })`.
|
||||
- **Ligado hoje só em Importação de Plano de Saúde** (`ips-ajuda-btn`). O mecanismo já é genérico: outra aplicação precisa só do botão no HTML e de uma chamada a `pidCriarBotaoAjuda()`, sem nenhum código novo no backend.
|
||||
|
||||
### Modal de confirmação genérico (nunca `window.confirm`/`window.alert`)
|
||||
|
||||
`static/js/confirm-modal.js` (incluído logo depois de `api.js` em **todo** shell, inclusive `index.html`) — `pidConfirm(mensagem, opcoes)` devolve uma `Promise<boolean>` (`true` = confirmado, `false` = cancelado ou fechado clicando fora), num modal `.modal-overlay`/`.modal-card` no padrão visual do Portal, nunca o diálogo nativo do navegador. **Decisão explícita do usuário**: o `window.confirm()` nativo do Chrome mostra o IP/porta do servidor na barra de título do popup e quebra a identidade visual do app — todo popup novo deve usar este modal em vez disso.
|
||||
`static/js/confirm-modal.js` (incluído logo depois de `api.js` em **todo** shell, inclusive `index.html`): `pidConfirm(mensagem, opcoes)` devolve `Promise<boolean>` e `pidAlert(mensagem, opcoes)` devolve `Promise<void>`, os dois num modal `.modal-overlay`/`.modal-card` no padrão visual do Portal.
|
||||
|
||||
- `opcoes` (todas opcionais): `titulo` (default "Confirmar ação"), `textoConfirmar`/`textoCancelar` (defaults "Confirmar"/"Cancelar"), `perigoso` (troca o botão de confirmar pra `.btn-danger-outline`, mesmo estilo já usado em ações destrutivas como "Excluir selecionadas", em vez do `.btn-solid` padrão).
|
||||
- Único modal (`#pid-confirm-modal`), criado sob demanda e reaproveitado — **empilha por cima** de qualquer modal já aberto via `.modal-overlay--top` (`components.css`, só `z-index` maior que o `.modal-overlay` padrão), então não precisa fechar o modal atual antes de perguntar; o modal por trás continua visível (dimmed), só o de confirmação recebe o clique.
|
||||
- Reaproveita `.modal-card__title`/`.modal-card__subtitle` (`components.css`, já genéricos) pro título/mensagem — não precisou de classes novas de texto, só `.confirm-modal__cancelar-btn`/`.confirm-modal__confirmar-btn` como seletores de DOM pro próprio `confirm-modal.js`.
|
||||
- **`pidAlert(mensagem, opcoes)`** é a contrapartida pra `window.alert()` — mesmo modal, um único botão (default "OK", sem "Cancelar"), devolve `Promise<void>`. Compartilha a mesma instância de `#pid-confirm-modal`/`pidConfirmOuAlerta()` internamente — `pidConfirm`/`pidAlert` só chamam essa função com `modoAlerta` diferente.
|
||||
- **Todo `window.confirm()`/`window.alert()` do app foi migrado pra `pidConfirm()`/`pidAlert()`** (rodada de varredura completa, pedido explícito do usuário — "migre as demais"): `acessos-gerais.js` (excluir acesso, excluir seção), `calendar-individual.js` (excluir compromisso), `links-ferramentas.js` (remover link, erro ao favoritar), `importacao-plano-saude.js` (excluir regra de custeio, remover linha, reverter alteração, excluir importação — individual e em lote —, e o "Fechar sem salvar" do Cadastro de Regras, que **tinha um modal bespoke próprio pra esse mesmo motivo** — `#ips-regracad-confirm-fechar-modal`, criado numa rodada anterior só porque `window.confirm()` mostra a URL do servidor; removido e substituído por `pidConfirm()` nesta rodada, consolidando os dois em um único componente), `ramais.js` (remover telefone externo, remover função de telefonia, deletar ausência, remover ramal, erros), `profiles.js` (excluir perfil, erro ao salvar permissão), `indicador-desempenho.js` (excluir apuração/critério/registro de percentual/departamento), `users-admin.js` (excluir departamento/usuário, ativar/desativar usuário, avisos de "não pode excluir/desativar a si mesmo"). Ação destrutiva (excluir/remover/deletar) sempre usa `{ perigoso: true }`; ações não-destrutivas (reverter, ativar/desativar) não.
|
||||
- **Não migrado**: `window.prompt()` em `users-admin.js` (renomear departamento, `data-dep-edit`) — é um tipo de popup diferente (pede texto, não só confirmar/cancelar) e não tinha um componente equivalente pronto; fica pra uma rodada futura se for pedido, junto com um modal de input genérico.
|
||||
- **Só ligado em Importação de Plano de Saúde por ora** (`ips-ajuda-btn` em `importacao-plano-saude.html`, ao lado do `<h2>` dentro de `.ips-title-row`) — o mecanismo (model/endpoint/JS) já é genérico o bastante pra outra aplicação nova só precisar do botão+tooltip no HTML e uma chamada a `pidCriarBotaoAjuda()`, sem nenhum código novo no backend.
|
||||
- Texto inicial de Importação de Plano de Saúde já estruturado (objetivo/como funciona/cuidados necessários/resultado esperado, em HTML simples — `<p>`/`<strong>`/`<ul>`/`<li>`) e salvo direto no banco, pronto pra revisão/edição do usuário pela própria tela (perfil Inovação).
|
||||
**Decisão explícita do usuário**: o `window.confirm()` nativo do Chrome mostra o IP/porta do servidor na barra de título e quebra a identidade visual do app. **Todo popup novo usa este modal.** Ver `[[feedback_popups_no_padrao_do_portal]]` na memória.
|
||||
|
||||
- `opcoes` (todas opcionais): `titulo` (default "Confirmar ação"), `textoConfirmar`/`textoCancelar`, `perigoso` (troca o botão de confirmar pra `.btn-danger-outline`). **Ação destrutiva sempre usa `{ perigoso: true }`**; ação reversível (reverter, ativar/desativar) não.
|
||||
- Um único modal (`#pid-confirm-modal`), criado sob demanda e reaproveitado; `pidConfirm`/`pidAlert` são duas chamadas da mesma função interna com `modoAlerta` diferente. **Empilha por cima** de qualquer modal já aberto via `.modal-overlay--top` (só um `z-index` maior), então não é preciso fechar o modal atual antes de perguntar — o de trás continua visível, dimmed.
|
||||
- Reaproveita `.modal-card__title`/`.modal-card__subtitle` pro título/mensagem; só `.confirm-modal__cancelar-btn`/`.confirm-modal__confirmar-btn` existem como seletores de DOM.
|
||||
- **Nenhum `window.confirm()`/`window.alert()` restou no app.** A única exceção é um `window.prompt()` em `users-admin.js` (renomear departamento) — pede texto, e ainda não existe um modal de input genérico. Se for pedido, criar junto.
|
||||
|
||||
A inativação/reativação de usuário (`is_active`, incluindo o desvínculo automático de perfis/liderança), a Liderança (gerente/coordenador) e o bug do `seed_portal.py` não resetar mais `nome` de um perfil já existente (ver `[[feedback_seed_nao_reseta_gabriel_bruno]]` na memória) estão documentados em `docs/perfis-usuarios/perfis-usuarios.md`.
|
||||
|
||||
@ -390,7 +352,7 @@ Ver `portal_api/dashboard_contabil/CLAUDE.md`.
|
||||
|---|---|
|
||||
| `tokens.css` | Variáveis (`:root`, tema claro em `:root[data-theme="light"]`). |
|
||||
| `base.css` | Reset global, incluindo `[hidden] { display: none !important; }` — necessário porque vários componentes (`.no-access`, `.app-card`, `.notif-badge`) definem seu próprio `display`, o que sem o `!important` sobrescreveria o comportamento nativo de `hidden`. Também os `@keyframes` globais de animação (`pidFadeIn`/`pidFadeSlideUp`/`pidScaleIn`, ver "Animações" abaixo) e `.pid-icon-eye`/`pidIconBlink` (piscar de olho do ícone "P.I.D.", reaproveitado pela sidebar e pelo login — ver "Ícone do login e da sidebar são clicáveis" abaixo), já que é o único CSS carregado por **todas** as páginas sem exceção (inclusive `index.html`). |
|
||||
| `layout.css` | Casca do shell: `.app-shell`, `.sidebar*`, `.nav-*`, `.fav-toggle`, `.topbar*`. A sidebar usa tokens **congelados**, independentes de tema (fundo sempre escuro em claro/escuro) — não trocar por variáveis que espelham `:root[data-theme="light"]`. Exceção deliberada: `--sidebar-text-primary`/`--sidebar-text-secondary`/`--sidebar-text-muted` (texto/ícone do menu) *são* sobrescritas em `:root[data-theme="light"]` (`tokens.css`) pra branco puro — pedido explícito do usuário pra melhorar a legibilidade; só o fundo/borda da sidebar continuam frozen. Também `.page-content` (largura do conteúdo de cada página, `max-width:1200px` centralizado por padrão) + o modificador `.page-content--wide` (`max-width:1600px`) — `portal.html` ("Principal") é a única página que usa só `.page-content` puro (grade de favoritos fica mais confortável de leitura mais estreita); as outras 11 páginas (`perfis-acesso.html`, `usuarios.html`, `links-ferramentas.html`, `acessos-gerais.html`, `ramais.html`, `calendario-individual.html`, `importacao-plano-saude.html`, `custo-contratacao.html`, `indicador-desempenho.html`, `nao-conformidades.html`, `dashboard-contabil.html`) usam `class="page-content page-content--wide"` no `<main>`, decisão explícita do usuário pra aproveitar melhor o espaço entre a sidebar e a borda da tela em telas de tabela/formulário. Uma página nova que seja mais "aplicação" (tabela, formulário, CRUD) do que "dashboard" deve nascer já com `page-content--wide`. |
|
||||
| `layout.css` | Casca do shell: `.app-shell`, `.sidebar*`, `.nav-*`, `.fav-toggle`, `.topbar*`. A sidebar usa tokens **congelados**, independentes de tema (fundo sempre escuro em claro/escuro) — não trocar por variáveis que espelham `:root[data-theme="light"]`. Exceção deliberada: `--sidebar-text-primary`/`--sidebar-text-secondary`/`--sidebar-text-muted` (texto/ícone do menu) *são* sobrescritas em `:root[data-theme="light"]` (`tokens.css`) pra branco puro — pedido explícito do usuário pra melhorar a legibilidade; só o fundo/borda da sidebar continuam frozen. Também `.page-content` (largura do conteúdo de cada página, `max-width:1200px` centralizado por padrão) + o modificador `.page-content--wide` (`max-width:1600px`) — `portal.html` ("Principal") é a única página que usa só `.page-content` puro (grade de favoritos fica mais confortável de leitura mais estreita); os outros 12 shells (`perfis-acesso.html`, `usuarios.html`, `links-ferramentas.html`, `acessos-gerais.html`, `ramais.html`, `calendario-individual.html`, `importacao-plano-saude.html`, `importacao-plano-saude-de-paula.html`, `custo-contratacao.html`, `indicador-desempenho.html`, `nao-conformidades.html`, `dashboard-contabil.html`) usam `class="page-content page-content--wide"` no `<main>`, decisão explícita do usuário pra aproveitar melhor o espaço entre a sidebar e a borda da tela em telas de tabela/formulário. Uma página nova que seja mais "aplicação" (tabela, formulário, CRUD) do que "dashboard" deve nascer já com `page-content--wide`. |
|
||||
| `components.css` | UI genérica reutilizável: `.btn-solid/.btn-outline/.btn-ghost/.btn-danger-outline`, **`.modal-overlay`/`.modal-card`** (moldura genérica de modal, + o modificador `.modal-card--wide` pra quando precisa de mais espaço horizontal) **e também** `.modal-field`/`.modal-field-row`/`.modal-checkbox`/`.modal-error`/`.modal-actions` (campos internos do modal — moldura e campos vivem juntos aqui, apesar do nome sugerir só a moldura, incluindo `select`/`textarea` dentro de `.modal-field`, com seta customizada via `background-image` porque o nativo do browser destoa do tema escuro), `.app-card*`, `.no-access`, `.checklist-box`/`.checklist-item`/`.checklist-item__info`/`.checklist-item__nome`/`.checklist-item__departamento`/`.checklist-empty`/`.checklist-search`/`.checklist-select-all` (lista com checkbox, segunda linha de detalhe e busca — usada nos checklists de Perfis de Acesso/Departamento em `usuarios.html`), `.dual-select`/`.dual-select__*` (vinculação em duas tabelas — não vinculados/vinculados, ver `docs/perfis-usuarios/perfis-usuarios.md` — usada em `usuarios.html` e no modal "Gerenciar Usuário" de todo shell), `.info-tooltip`/`.info-tooltip__*`/`.ajuda-modal__*` (botão "?" + tooltip + modal de "Mais informações", ver seção própria abaixo — usados por `static/js/ajuda-aplicacao.js`) e `.modal-overlay--top` (empilha um modal por cima de outro já aberto — usado só pelo modal de confirmação genérico, `static/js/confirm-modal.js`, ver "Modal de confirmação genérico" abaixo). |
|
||||
| `perfis-acesso.css` | `.pa-*` (tela de Perfis de Acesso), incluindo as seções (`.ua-section*`) e campos específicos (`.ua-inline-add`/`.ua-departamento-item`/`.ua-active-toggle`/`.ua-liderados-field`) do formulário de edição de `usuarios.html`. |
|
||||
| `calendario.css` | Só `.calendar-*` (grade mensal, células de dia, nav do mês) — os campos do modal de compromisso usam as classes genéricas `.modal-field`/`.modal-checkbox`/`.modal-error`/`.modal-actions` de `components.css`. |
|
||||
@ -410,34 +372,12 @@ Ao adicionar uma tela nova que precise de modal, reuse `.modal-overlay`/`.modal-
|
||||
|
||||
## Animações
|
||||
|
||||
Três `@keyframes` genéricos em `base.css` (`pidFadeIn`, `pidFadeSlideUp`, `pidScaleIn`) — reutilizados via `animation` (não `transition`) em elementos que entram/saem do layout via `hidden`/`display:none` (modal, dropdown, `.page-content` a cada navegação, `.login-card`), porque só `animation` reinicia sozinho quando um elemento passa de `display:none` para visível; `transition` não anima essa troca (não há frame intermediário). Duração sempre curta (120–200ms) — pedido explícito do usuário: "fluidas, porém rápidas, otimizando o tempo". Botões (`.btn-solid`/`.btn-outline`/`.btn-ghost`/`.btn-danger-outline`/`.icon-btn`) ganharam `transform: scale()` no `:active` como feedback de clique.
|
||||
Três `@keyframes` genéricos em `base.css` (`pidFadeIn`, `pidFadeSlideUp`, `pidScaleIn`), aplicados via `animation` e **nunca via `transition`** em elementos que entram/saem do layout por `hidden`/`display:none` (modal, dropdown, `.page-content` a cada navegação, `.login-card`) — só `animation` reinicia sozinha quando um elemento passa de `display:none` para visível; `transition` não anima essa troca, porque não há frame intermediário. Duração sempre curta (120–200ms), a pedido do usuário: "fluidas, porém rápidas, otimizando o tempo". Botões têm `transform: scale()` no `:active` como feedback de clique.
|
||||
|
||||
**Nenhuma dessas animações respeita `prefers-reduced-motion`** — decisão deliberada do usuário ("as animações devem ignorar a preferência de não mostrar animações ou de acessibilidade do computador do usuário"), não um descuido. Não adicionar um bloco `@media (prefers-reduced-motion: reduce)` desativando isso sem confirmar de novo com o usuário, já que contraria um pedido explícito.
|
||||
**Nenhuma dessas animações respeita `prefers-reduced-motion`** — decisão deliberada do usuário ("as animações devem ignorar a preferência de não mostrar animações ou de acessibilidade do computador do usuário"), não um descuido. **Não adicionar um bloco `@media (prefers-reduced-motion: reduce)` desativando isso sem confirmar de novo**, já que contraria um pedido explícito.
|
||||
|
||||
### Animação de intro do login
|
||||
Ao apertar "Entrar" com sucesso, toca uma animação de marca em tela cheia (~6,2s) antes de navegar para `portal.html`, e a tela Principal entra pela barra lateral primeiro. Ver `docs/identidade-visual/identidade-visual.md` para as 4 cenas, os timings, as curvas de easing e o mecanismo de revelação em `portal-reveal.js`.
|
||||
|
||||
Ao apertar "Entrar" com sucesso, `index.html` toca uma animação de marca em tela cheia (~6,2s: 500ms de fade + ~5,7s de animação) antes de navegar pra `portal.html` — pedido explícito do usuário, inspirado num arquivo `pid-intro-escuro.html` que ele forneceu (mesma pasta de origem dos SVGs da marca "P.I.D.", `C:\Users\Depaula\Documents\Projetos\LOGOS\pid-marca`).
|
||||
|
||||
**Origem do arquivo — por que não foi só copiado**: `pid-intro-escuro.html` não é HTML/CSS simples, é um bundle auto-contido de um editor de "Design Canvas" (Anthropic) — um `<script type="__bundler/manifest">` com um JSON mapeando uuid → recurso (`{mime, compressed, data}`, `data` sendo base64 de um gzip), incluindo o JSX fonte de verdade (`pid-intro.jsx`, a composição em si) e a biblioteca de motion (`animations-v3.jsx`, easing/interpolação), montados em tempo real por um runtime React + motor de composição por "tempo autorado" (`useComposition`/`CompositionStage`) que não faz sentido carregar em produção (pesado, e pensado pra edição/export de vídeo, não pra rodar dentro de uma página de login). A coreografia foi **extraída** (decodificado gzip+base64 por uuid, script Python ad-hoc) e **portada fielmente** pra vanilla JS/CSS — mesmas durações de cena, mesmas curvas de easing (hand-rolled, estilo Popmotion) e mesmo timing de piscada dos olhos do original; só o destino final do "voo" do ícone foi recalibrado (ver "Cena Portal" abaixo).
|
||||
|
||||
**As 4 cenas** (`PID_INTRO_CUES`/`PID_INTRO_TOTAL` em `login-intro.js` — `Build:0, Face:1, Wordmark:2, Portal:4.8`, total `5.7`): durações **aceleradas** em relação ao arquivo original (`Build`/`Face` de 2,6s/2s pra 1s cada, `Portal` de 2,3s pra ~0,9s — pedido explícito do usuário, "acelere um pouco a velocidade na qual a logo é construída"), exceto `Wordmark` (2,8s, igual ao original) — "a parte onde aparece o nome do portal deve permanecer a mesma", pedido explícito também. Os deslocamentos internos de cada cena (quando cada elemento começa/termina de animar) foram reproporcionados pra caber na duração nova, não são mais os valores originais do arquivo — só a cena Wordmark manteve os originais, já que a duração dela não mudou.
|
||||
- **Build** (1s): o contorno do corpo do ícone se desenha (`stroke-dashoffset`, `pathLength="100"` normaliza o path pra unidades 0–100 independente da geometria real), preenche a cor, a aba dourada "cai" (`easeOutBack`, dá um leve overshoot) e a "tela"/rosto do ícone abre (scale+opacity).
|
||||
- **Face** (1s): os dois olhos aparecem com "pop" (`easeOutBack`) e piscam uma vez (`pidIntroBlinkAt()` — uma janela de 220ms em que a escala vertical do olho vai de 1 a 0 e volta a 1, formando o fecha-e-abre; a mesma função é reaproveitada em 2 momentos diferentes agora, ver abaixo — o terceiro blink do original, na cena Portal, foi removido: a cena ficou curta demais pra caber um blink visível com o ícone já encolhendo/voando), o sorriso dourado se desenha (mesma técnica de `stroke-dashoffset` do corpo).
|
||||
- **Wordmark** (2,8s, **inalterada**): o ícone desliza pra esquerda (`SHIFT_X=-100px`) enquanto "P.I.D." (fonte "Space Grotesk" 700, cada letra com um "." dourado à parte) + uma régua dourada (`scaleX`) + a tagline "Portal Interno da De Paula" (fonte "DM Mono", uppercase, letter-spacing largo) aparecem — cada letra com seu próprio atraso escalonado (`CUES.Wordmark + 0.25 + i*0.13`).
|
||||
- **Portal** (~0,9s): a wordmark esmaece quase na hora (`Portal` a `Portal+0.3`), o ícone encolhe (de 160px pra 40px — o mesmo tamanho de `.sidebar__logo--icon`, ver "Logos em `static/img/`" acima) e "voa" até o canto superior esquerdo em 0,5s (`Portal+0.05` a `Portal+0.55`), e o stage inteiro (ícone+wordmark) esmaece logo depois, sobrepondo o fim do voo (`Portal+0.5` a `Portal+0.9`) — sem pausa parada entre o ícone assentar e o fade começar (ajuste de uma rodada anterior, que já tinha comprimido essa cena de 2,3s pra ~0,95s; esta rodada só encurtou mais um pouco, até ~0,9s). **Diferença deliberada em relação ao original** (além do tempo): lá o destino é um pixel fixo dentro de um frame de vídeo de exportação 1920×1080 (`logoX:-892, logoY:-496`, coordenadas que não existem em página nenhuma); aqui, `pidIntroCornerTarget()` calcula o alvo em tempo real a partir de `window.innerWidth/innerHeight`, mirando um ponto perto do canto real da janela — a ideia de "o ícone termina indo pro cabeçalho/sidebar do app" só faz sentido revisitada assim, já que o original nunca foi pensado pra rodar dentro de uma página de verdade.
|
||||
- As duas piscadas do olho (`Face+0.8`, `Wordmark+1.1`) e as três funções de easing usadas (`easeOutCubic`/`easeInOutQuad`/`easeOutBack`) seguem as fórmulas exatas extraídas de `animations-v3.jsx` — ver o código de `login-intro.js` se precisar ajustar timing, não redesenhar do zero.
|
||||
|
||||
**Fade de entrada, antes do ícone começar a se desenhar** (pedido explícito do usuário — a primeira versão fazia o overlay aparecer de repente por cima do card ainda visível, "de um modo bruto"; a duração foi ajustada de 350ms pra 500ms numa rodada seguinte, "está muito rápido... só pra ficar mais fluído"): `pidPlayLoginIntro()` aplica `.is-leaving` no `.login-card` (`login.css`, `opacity:0; transform:scale(0.98)`, transição de 500ms) e `.is-visible` no `#login-intro` (`opacity:0→1`, mesmos 500ms, `PID_INTRO_FADE_MS`) ao mesmo tempo — o card se dissolve enquanto o overlay (já na cor final sólida) sobe por cima, um cross-fade real, não uma troca instantânea. Só depois desses 500ms (`setTimeout`) é que o loop de `requestAnimationFrame` do ícone começa (`start = performance.now()` é atribuído só nesse momento, não antes).
|
||||
|
||||
**Arquivos**: `static/css/login-intro.css` (só `index.html`) — formas idênticas às já usadas em `pid-icone-escuro.svg`/`pid-logo-horizontal-escuro.svg` (hex hardcoded — `#7b5ba8`/`#c6a24a`/`#f2ede3`, não são tokens de tema, são a paleta fixa da marca, mesmo espírito de `.login-slogan` já hardcodar `#c6a24a`); a **exceção** é o fundo do overlay, que reage ao tema (ver "Tema claro" logo abaixo) em vez de ser um hex fixo da marca. `static/js/login-intro.js` expõe `pidPlayLoginIntro(onDone)` (global, chamada só por `auth.js`) — monta o loop de `requestAnimationFrame`, calcula cada valor (`bodyDraw`, `eyeL`, `shift`, `fly`, ...) a partir de `T` (segundos decorridos desde o início do loop, via `performance.now()`) e escreve direto nos atributos/estilo dos elementos do overlay (`#login-intro`, já presente e `hidden` no HTML de `index.html`); chama `onDone()` quando `T` atinge `PID_INTRO_TOTAL`. Fontes "Space Grotesk" (peso 700) e "DM Mono" adicionadas ao mesmo `<link>` do Google Fonts de `index.html`, junto de "Bree Serif"/"Pinyon Script" já usadas ali.
|
||||
|
||||
**Tema claro**: o fundo do overlay (`#121017`, o valor fixo de `--bg-canvas` no tema escuro) e o texto do wordmark (`.login-intro__letter`/`.login-intro__tagline`, cor clara — pensados pra contrastar com um fundo escuro) só faziam sentido enquanto o overlay era sempre escuro (decisão original, pra login → animação → portal ler como uma coisa só). O usuário pediu que, no tema claro, o fundo da animação também acompanhasse o `--bg-canvas` claro (mesmo raciocínio já aplicado a `.login-page` em `login.css`) — o que por sua vez tornou o texto claro do wordmark ilegível contra um fundo claro. `:root[data-theme="light"]` overrides em `login-intro.css` cobrem os dois: `.login-intro` vira `background: var(--bg-canvas)` e `.login-intro__letter`/`.login-intro__tagline` viram cores escuras (`#241c33`/`rgba(36, 28, 51, 0.62)`, os mesmos tons só invertidos). O **ícone não precisou de override** — o corpo roxo (`#7b5ba8`) tem contraste de sobra contra um fundo claro, e a "tela" do disquete (`#241c33`) já é escura por si só, então os olhos/sorriso continuam legíveis nos dois temas sem mudar nada. Nada disso afeta o tema escuro (padrão), que continua com os valores fixos originais.
|
||||
|
||||
**Fluxo de navegação**: `auth.js`, no sucesso do `POST /api/auth/login/`, chama `pidPlayLoginIntro(() => { sessionStorage.setItem("pid_reveal_portal", "1"); window.location.href = "portal.html"; })` em vez de navegar direto — a navegação só acontece depois da animação inteira (não há como "pular" a animação hoje, nem foi pedido).
|
||||
|
||||
**"A barra lateral surgindo da esquerda para a direita, e em seguida o resto da tela"** (pedido explícito do usuário sobre como a tela Principal deveria surgir depois da animação, refinado duas vezes: a primeira versão só escondia `.main-content` e deixava a sidebar sempre visível desde o início, sem nenhuma entrada própria; a segunda versão deu à sidebar um fade + deslize sutil de 16px, considerado "ainda não satisfatório" — pequeno demais pra ler como "surgindo da esquerda pra direita"): `static/js/portal-reveal.js` (só `portal.html`, incluído `defer` logo depois de `theme.js` — mesmo padrão de "script que roda como IIFE de topo antes da primeira pintura pra evitar flash", ver "Ordem de `<script>`" acima) checa a `sessionStorage` marcada por `auth.js`; se presente, remove a marca (não sobrevive a um F5) e aplica **duas** classes em `<html>` antes do `DOMContentLoaded`: `pid-entering-sidebar` (esconde só `.sidebar`, `opacity:0` + `transform:translateX(-100%)` — a largura inteira dela, não um deslize de poucos pixels, pra realmente ler como um slide de fora da tela) e `pid-entering` (esconde `.main-content`, o `<div>` que envolve topbar **e** conteúdo da página). As duas são removidas **em sequência**, não juntas: `pid-entering-sidebar` sai primeiro (120ms depois do `DOMContentLoaded`), a sidebar desliza da esquerda pra direita ao longo de 420ms (`.sidebar` em `layout.css` tem uma `transition` própria — `opacity 420ms ease, transform 420ms cubic-bezier(0.16, 1, 0.3, 1)`, mais longa e com uma curva de "chegada" suave, não `var(--transition-base)` — genérica demais pra um movimento desse tamanho); só depois de esperar essa mesma duração (420ms) é que `pid-entering` sai, revelando o resto do app num fade, garantindo que as duas entradas não se sobreponham. Acessar `portal.html` direto (sem passar pelo login) nunca aciona nada disso, já que a `sessionStorage` só é setada no caminho de login bem-sucedido.
|
||||
|
||||
Testado pelo usuário em três rodadas (funcional) — ajustes até agora: a cor do overlay (era `#241c33`, virou `#121017`), o fade de entrada antes do ícone começar a se desenhar (não existia, overlay aparecia de repente; a duração foi ajustada depois de 350ms pra 500ms, "muito rápido... só pra ficar mais fluído"), a compressão da cena Portal (era 2,3s com pausa parada, foi pra 0,95s corrida numa rodada e depois ~0,9s), a aceleração de Build/Face (de 2,6s/2s cada pra 1s cada, Wordmark mantida em 2,8s de propósito) e a entrada da sidebar em `portal.html` (de um fade sutil de 16px pra um slide de fora da tela inteiro, `translateX(-100%)`, 420ms). Ainda falta conferir o posicionamento do "voo" final em diferentes tamanhos de tela/com a sidebar colapsada.
|
||||
|
||||
## Temas Sazonais
|
||||
|
||||
|
||||
@ -16,13 +16,14 @@ Configurar as variáveis de ambiente do Postgres 14 antes de migrar — `config/
|
||||
```
|
||||
python manage.py makemigrations portal_api
|
||||
python manage.py migrate
|
||||
python manage.py seed_portal # recria os 8 perfis + usuários gabriel/bruno
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
Abrir `http://localhost:8000/` — o Django serve `index.html` e as demais páginas do frontend diretamente, sem precisar de um segundo servidor pro frontend.
|
||||
|
||||
Contas de demonstração (criadas por `seed_portal`): `gabriel`/`gabriel` (perfil "Inovação", acesso total) e `bruno`/`bruno` (sem perfil vinculado, testa o estado "sem acesso").
|
||||
> **Não rodar `python manage.py seed_portal` neste ambiente.** O Portal já está em produção e o banco apontado pelo `.env` é o banco real, não uma cópia de desenvolvimento. O comando ressincroniza incondicionalmente `permissoes`/`ativo`/`gerencia_permissoes` dos 8 perfis de código fixo a partir de `catalogo.py` a cada execução — rodá-lo reverteria, sem nenhum aviso, qualquer permissão que um admin tenha customizado pela tela de Perfis de Acesso. O comando existe só para criar um ambiente novo/vazio do zero. Ver `CLAUDE.md` → "Como rodar / testar localmente" para o detalhamento.
|
||||
|
||||
As contas de usuário (incluindo `gabriel` e `bruno`) já existem no banco de produção, com senhas e perfis mantidos pelos próprios usuários pelas telas de Perfis de Acesso/Usuários. Num ambiente novo criado do zero, `seed_portal` cria `gabriel` (perfil "Inovação", acesso total) e `bruno` (sem perfil vinculado, para testar o estado "sem acesso").
|
||||
|
||||
Não há suíte de testes, lint ou build configurados neste projeto.
|
||||
|
||||
@ -37,11 +38,13 @@ Não há suíte de testes, lint ou build configurados neste projeto.
|
||||
| Solicitações | Solicitações | [docs/solicitacoes/](docs/solicitacoes/README.md) |
|
||||
| Perfis de Acesso / Usuários | Administração | [docs/perfis-usuarios/](docs/perfis-usuarios/README.md) |
|
||||
| Importação de Plano de Saúde | Utilitários | [portal_api/planos_saude/](portal_api/planos_saude/README.md) |
|
||||
| Importação de Plano de Saúde - De Paula (mesma ferramenta, para o plano dos próprios colaboradores) | Utilitários | [portal_api/planos_saude/](portal_api/planos_saude/README.md) |
|
||||
| Simulação de Custo de Contratação | Geradoc | [portal_api/custo_contratacao/](portal_api/custo_contratacao/README.md) |
|
||||
| Indicador de Desempenho | Geradoc | [portal_api/indicadores/](portal_api/indicadores/README.md) |
|
||||
| Não Conformidades | Relatórios > Qualidade | [portal_api/nao_conformidades/](portal_api/nao_conformidades/README.md) |
|
||||
| Relatório Contábil | Relatórios > Contabilidade | [portal_api/dashboard_contabil/](portal_api/dashboard_contabil/README.md) |
|
||||
| Temas Sazonais (marca P.I.D. sazonal, hoje só Halloween) | — (recurso visual, não é item de menu) | [docs/temas-sazonais/](docs/temas-sazonais/README.md) |
|
||||
| Identidade visual (logos, marca P.I.D. na UI, animações, intro pós-login) | — (camada transversal, não é item de menu) | [docs/identidade-visual/](docs/identidade-visual/README.md) |
|
||||
|
||||
Cada pasta acima tem um `README.md` (o que a aplicação faz, estado atual) e um `CHANGELOG.md` (histórico rodada a rodada, extraído de `plano.md`). As 5 aplicações com pacote Python próprio também têm um `CLAUDE.md` com o detalhamento técnico completo, carregado automaticamente pelo Claude Code; as demais têm esse detalhe no arquivo dentro da própria pasta em `docs/`.
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
# Changelog — Calendário Individual e Widgets
|
||||
|
||||
> Histórico específico desta aplicação, extraído de `plano.md`. Rodadas que mudaram mais de uma aplicação ao mesmo tempo (modelo de permissões, CSS transversal) continuam em `plano.md` — a numeração de rodada é a mesma usada lá, para referência cruzada.
|
||||
> Histórico específico desta aplicação, extraído de `plano.md`. Rodadas que mudaram mais de uma aplicação ao mesmo tempo (modelo de permissões, CSS transversal) continuam em `plano.md`.
|
||||
>
|
||||
> **Formato**: uma entrada por rodada, `### Rodada N — Título`; quando a rodada não tem número registrado, `### Título` só. **A numeração de rodada não é global** — cada aplicação conta as próprias, e o mesmo número designa trabalhos diferentes em arquivos diferentes. Ao citar uma rodada, sempre nomear o arquivo. Ver o topo de `plano.md`.
|
||||
|
||||
### Rodada 9 — Calendário De Paula → link externo
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
# Changelog — Favoritos
|
||||
|
||||
> Histórico específico desta aplicação, extraído de `plano.md`. Rodadas que mudaram mais de uma aplicação ao mesmo tempo (estrutura do portal, CSS transversal, modelo de permissões) continuam em `plano.md` — a numeração de rodada é a mesma usada lá, para referência cruzada.
|
||||
> Histórico específico desta aplicação, extraído de `plano.md`. Rodadas que mudaram mais de uma aplicação ao mesmo tempo (estrutura do portal, CSS transversal, modelo de permissões) continuam em `plano.md`.
|
||||
>
|
||||
> **Formato**: uma entrada por rodada, `### Rodada N — Título`; quando a rodada não tem número registrado, `### Título` só. **A numeração de rodada não é global** — cada aplicação conta as próprias, e o mesmo número designa trabalhos diferentes em arquivos diferentes. Ao citar uma rodada, sempre nomear o arquivo. Ver o topo de `plano.md`.
|
||||
|
||||
### Rodada 8 — Favoritos
|
||||
|
||||
|
||||
25
docs/identidade-visual/CHANGELOG.md
Normal file
25
docs/identidade-visual/CHANGELOG.md
Normal file
@ -0,0 +1,25 @@
|
||||
# Changelog — Identidade visual
|
||||
|
||||
> **Índice, não cópia.** Ao contrário dos outros changelogs de aplicação, este não extraiu as entradas de `plano.md`: as rodadas de identidade visual mudaram todas as telas ao mesmo tempo (as 13 páginas-shell, o favicon, o login), então são transversais por natureza e continuam em `plano.md`, que é onde mora o histórico estrutural/transversal. Este arquivo só diz qual rodada de lá cobre o quê.
|
||||
>
|
||||
> **A numeração de rodada não é global** — cada aplicação conta as próprias. As rodadas abaixo são as de `plano.md`.
|
||||
|
||||
| Rodada em `plano.md` | O quê |
|
||||
|---|---|
|
||||
| 20 | Logo com texto branco (`logo-branco.png` gerada por script Pillow a partir de `logo.png`) + sidebar maior |
|
||||
| 25 | Favicon recortado só do "D" (`favicon.png`), gerado pelo mesmo filtro da rodada 20 |
|
||||
| 28 | Fonte "Bree Serif" no título da tela inicial e do login (primeira fonte externa do projeto) |
|
||||
| 29 | Animações genéricas (`pidFadeIn`/`pidFadeSlideUp`/`pidScaleIn`) e a decisão explícita de **ignorar** `prefers-reduced-motion` |
|
||||
| 73 | Nova identidade "P.I.D." — favicon, login (card congelado escuro) e sidebar (crossfade entre assinatura e ícone) |
|
||||
| 74 | Animação de intro pós-login, portada do bundle `pid-intro-escuro.html`; entrada da sidebar em `portal.html` |
|
||||
| 75 | Logo da sidebar virou link para "Principal", com os olhos do ícone piscando ao clicar |
|
||||
| 76 | Mesma piscada no ícone do login; a logo do login deixou de alternar por tema |
|
||||
| 77 | Fundo da tela de login no tema claro batendo com o fundo real do Portal |
|
||||
| 78 | Mesmo ajuste na intro: fundo claro no tema claro, texto do wordmark escuro |
|
||||
| 79 | Fundo de `.login-page` no tema claro refinado (brilho roxo mais claro, base levemente mais escura) |
|
||||
|
||||
### 2026-09-21 — Documentação separada em `docs/identidade-visual/`
|
||||
|
||||
Rodada de revisão da documentação, não de código. O detalhamento técnico de logos (9,4 KB) e animações (12,0 KB) saiu do `CLAUDE.md` da raiz para `identidade-visual.md` nesta pasta — 21,5 KB de detalhe de uma área específica que era carregado em toda sessão. A raiz ficou com um resumo e o ponteiro para cá.
|
||||
|
||||
Duas correções de fato na mesma passagem: `pid-favicon.svg` e `pid-logo-horizontal.svg` eram descritos como se estivessem em `static/img/` e **nunca foram copiados para lá** (existem só na pasta de marca de origem); e os três SVGs sazonais (`pid-halloween-*`) existiam na pasta sem constar do inventário.
|
||||
16
docs/identidade-visual/README.md
Normal file
16
docs/identidade-visual/README.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Identidade visual
|
||||
|
||||
Não é uma aplicação do menu: é a camada visual transversal do Portal, usada por todas as telas.
|
||||
|
||||
## O que cobre
|
||||
|
||||
- **As duas identidades que coexistem de propósito**: o logo cursivo "D De Paula Contadores", usado só nos documentos e PDFs que a aplicação gera (contrato, procuração, recibo do Indicador de Desempenho, simulação de custo, relatório do cliente), e a marca "P.I.D.", usada só na UI do Portal (favicon, login, sidebar). Um documento gerado carrega a identidade do escritório perante o cliente, não a do Portal como ferramenta interna.
|
||||
- **A marca "P.I.D." na tela**: o crossfade entre assinatura e ícone na sidebar, o ícone clicável com os olhos piscando (sidebar e login), o card de login congelado escuro nos dois temas.
|
||||
- **As animações genéricas** do Portal (entrada de modal, dropdown, troca de página) e a decisão explícita de ignorar `prefers-reduced-motion`.
|
||||
- **A animação de intro pós-login**: as 4 cenas da marca que tocam entre "Entrar" e a tela Principal, e a entrada da sidebar em `portal.html` logo depois.
|
||||
|
||||
## Onde está o resto
|
||||
|
||||
- `identidade-visual.md` — o detalhamento técnico (arquivos, classes CSS, timings, por que cada decisão).
|
||||
- `CHANGELOG.md` — histórico rodada a rodada.
|
||||
- A variação sazonal desta mesma marca (mascote de Halloween, intro alternativa, teias de aranha) fica em `docs/temas-sazonais/`.
|
||||
62
docs/identidade-visual/identidade-visual.md
Normal file
62
docs/identidade-visual/identidade-visual.md
Normal file
@ -0,0 +1,62 @@
|
||||
# Identidade visual do Portal
|
||||
|
||||
> Detalhamento técnico da identidade visual transversal: os arquivos de logo, a marca "P.I.D." na sidebar/login/favicon, as animações genéricas e a animação de intro pós-login. Movido do `CLAUDE.md` da raiz em 2026-09-21, pelo mesmo motivo que levou a documentação das aplicações a sair de lá: era detalhe de uma área específica ocupando ~21 KB de um arquivo carregado em toda sessão.
|
||||
>
|
||||
> Este arquivo **não é carregado automaticamente** pelo Claude Code (não há pacote Python correspondente) — ler manualmente ao mexer em logo, favicon, `sidebar__brand`, animação de entrada ou na intro do login. A variação sazonal dessa mesma marca (mascote de Halloween, intro alternativa, teias) fica em `docs/temas-sazonais/temas-sazonais.md`.
|
||||
|
||||
## Logos em `static/img/`
|
||||
|
||||
Duas identidades visuais coexistem **de propósito** hoje: o logo cursivo "D De Paula Contadores" (usado só nos documentos/PDFs que a aplicação gera, ver abaixo) e a marca nova "P.I.D." (`.svg`, ver `pid-marca-leiame.md` em `C:\Users\Depaula\Documents\Projetos\LOGOS\pid-marca`), adotada no favicon, no login e na sidebar do Portal. **Essa separação é deliberada, não uma migração incompleta**: um documento gerado pela aplicação (contrato, procuração, recibo do Indicador de Desempenho, PDF da Simulação de Custo de Contratação) é emitido como se o próprio escritório o tivesse gerado — carrega a identidade do escritório perante o cliente, não a identidade do Portal como ferramenta interna. A marca "P.I.D." é só pra UI do Portal em si. Não migrar o logo de um gerador de documento pra "P.I.D." (nem vice-versa numa tela do Portal) sem confirmar de novo com o usuário.
|
||||
|
||||
**Logo cursivo "D De Paula Contadores"** (D em degradê dourado/marrom + texto, PNG com fundo transparente) — não aparece em nenhum template HTML hoje, só nos PDFs gerados pela aplicação:
|
||||
|
||||
- `logo.png` — original, texto **preto**. Serve como fonte pra gerar as outras variantes e é usada diretamente no recibo do Indicador de Desempenho (`indicadores/recibo.py`, `LOGO_PATH`, redimensionada/recomprimida em memória pra impressão — ver `portal_api/indicadores/CLAUDE.md`).
|
||||
- `logo-branco.png` — usada no cabeçalho do PDF de Simulação de Custo de Contratação (`custo_contratacao/pdf.py`, banner marrom escuro, ver `portal_api/custo_contratacao/CLAUDE.md`) — até uma rodada anterior também era usada no `sidebar__brand` dos 13 shells, migrada pra marca "P.I.D." (ver abaixo; a UI do Portal e os documentos gerados usam fontes de logo independentes agora). Mesmo D colorido de `logo.png`, mas com o texto recolorido pra branco; gerada programaticamente a partir de `logo.png` (script Python com Pillow: qualquer pixel opaco quase-neutro/escuro — `max(r,g,b) < 70` e `spread(r,g,b) < 12` — virou branco; o D nunca entra nesse filtro porque mesmo na sombra mais escura do degradê ele mantém um matiz quente nitidamente não-neutro). Se o logo oficial mudar, regerar `logo-branco.png` a partir do novo `logo.png` com o mesmo filtro, não editar à mão.
|
||||
- `logo-mono.png` — versão totalmente monocromática (D **e** texto em branco/cinza claro). Não usada em nenhum consumidor hoje; existe como variante alternativa (útil se algum dia precisar de um logo "chapado" sem o dourado do D).
|
||||
|
||||
**Marca nova "P.I.D."**:
|
||||
|
||||
- `pid-icone.svg` — ícone quadrado, variante pra **fundo claro** (corpo roxo escuro `#4B2E75`); chegou a ser usada no login quando `data-theme="light"`, mas o usuário pediu pra usar sempre a mesma logo nos dois temas — **não tem mais nenhum consumidor** hoje, é o único arquivo sem uso que ficou no repositório.
|
||||
- `pid-icone-escuro.svg` — ícone quadrado, variante pra **fundo escuro** (corpo roxo mais claro `#7B5BA8`, pra manter contraste); usada (a) no favicon (`<link rel="icon" type="image/svg+xml">` no `<head>` das 14 páginas, tudo menos o template de relatório do Relatório Contábil) e (b) no login, **sempre**, nos dois temas — o card do login já é congelado escuro nos dois temas (ver "Card de login" abaixo), e agora o ícone também, pedido explícito do usuário ("deixe no tema claro a mesma logo usada no tema escuro"; antes alternava com `pid-icone.svg` conforme `data-theme`, mecanismo removido — ver "Ícone do login é clicável" abaixo). O ícone da sidebar (expandida e colapsada, 13 shells) e o do card de login usam o **mesmo desenho/cores** desse arquivo, mas como markup `<svg>` inline copiado direto no HTML, não uma referência a este arquivo — ver "Ícone do login/sidebar são clicáveis" abaixo pro motivo (precisa expor os olhos pro CSS/JS animar o piscar ao clicar).
|
||||
- `pid-logo-horizontal-escuro.svg` — assinatura horizontal (ícone + "P.I.D." + "PORTAL INTERNO DA DE PAULA" em texto, viewBox `300×80`, texto claro `#F2EDE3`/dourado `#C6A24A` — variante pra fundo escuro). Mesmo caso do ícone acima: a sidebar expandida (13 shells) usa o mesmo desenho como `<svg>` inline, não uma referência a este arquivo.
|
||||
- `pid-halloween-icone-escuro.svg`, `pid-halloween-logo-horizontal-escuro.svg` e `pid-halloween-favicon.svg` — variantes sazonais do mascote vampiro, trocadas em runtime durante a janela do Halloween. Ver `docs/temas-sazonais/temas-sazonais.md`.
|
||||
|
||||
> `pid-favicon.svg` e `pid-logo-horizontal.svg` existem na pasta de marca de origem (`C:\Users\Depaula\Documents\Projetos\LOGOS\pid-marca`), mas **nunca foram copiados pra `static/img/`** — a documentação anterior os descrevia como se estivessem aqui. Se algum dia forem necessários, copiar de lá.
|
||||
|
||||
**`sidebar__brand` com duas marcas sobrepostas (crossfade), não uma redimensionada** (`layout.css`): a sidebar expandida mostra a assinatura com texto (tem texto, ilegível se só encolhida) e a colapsada mostra só o símbolo — são dois `<svg class="sidebar__logo sidebar__logo--full">`/`<svg class="sidebar__logo sidebar__logo--icon">` **sempre presentes no DOM**, sobrepostos via `position:absolute` dentro de `.sidebar__brand` (`position:relative; height:92px`, alto o bastante pra caber as duas sem depender da altura natural de nenhuma, já que filhos absolutos não contribuem pra altura do pai). `.sidebar__logo--full` é `width:250px; max-width:96%` (quase toda a largura útil da sidebar, `264px` menos o padding horizontal de `.sidebar`) — aumentado em duas rodadas a partir do tamanho inicial (`176px`/`70%` → `220px`/`92%` → `250px`/`96%`) porque o subtítulo "PORTAL INTERNO DA DE PAULA" (fonte pequena dentro do SVG, viewBox `300×80`) ficava ilegível menor; `height:92px` acompanhou cada aumento pra sobrar espaço vertical (proporção do SVG é `300:80`, então a altura renderizada escala junto com a largura). A transição entre elas é `opacity`+`transform:scale()` (`var(--transition-base)`, mesma duração das outras animações do sidebar) — pedido explícito do usuário pra não ser uma troca brusca; `.app-shell.is-collapsed` (toggle desktop) e o breakpoint mobile (`@media (max-width:1024px)`, onde o colapsado é o estado *default* e `.is-expanded-mobile` o inverte, mesmo padrão já usado pelos demais elementos do menu nesse breakpoint) alternam qual das duas fica com `opacity:1`.
|
||||
|
||||
**Ícone do login e da sidebar são clicáveis, com os olhos piscando** (pedido explícito do usuário, em duas rodadas — primeiro a sidebar, depois o ícone do login): tanto `.sidebar__brand` (13 shells) quanto `#login-logo-btn` (`index.html`) tiveram o ícone convertido de `<img src="...svg">` pra `<svg>` **inline**, com markup idêntico ao de `pid-icone-escuro.svg` copiado direto no HTML — um `<img>` não expõe seu conteúdo interno pro CSS/JS da página (é uma imagem opaca), então não dava pra animar só os olhos sem inlinear. Dentro de cada SVG, os dois olhos (círculo creme + glint escuro) ficam num `<g class="pid-icon-eye">` próprio, sem nenhum `transform` no XML (a posição já vem dos `cx`/`cy` dos círculos) — importante porque um `transform` de CSS aplicado num elemento que já tem um `transform` de atributo **substitui** o atributo inteiro (perderia a posição); mantendo os dois olhos "limpos" desse jeito, a única transformação deles é a que a animação de piscar aplica. `.pid-icon-eye`/`.is-blinking`/`@keyframes pidIconBlink` moram em `base.css` (`transform-box:fill-box; transform-origin:center` faz o `scaleY()` girar em torno do próprio olho, não da origem do SVG; `scaleY(1)→0.05→1`, 200ms, os dois olhos piscam juntos) — em `base.css`, não em `layout.css`, porque é carregado por **todas** as páginas, inclusive `index.html` (que não carrega `layout.css`).
|
||||
|
||||
- **Sidebar** (`static/js/sidebar-brand.js`, incluído logo depois de `api.js` nos 13 shells): `.sidebar__brand` deixou de ser `<div>` e virou `<a href="portal.html" id="sidebar-brand-link" aria-label="Ir para a tela Principal">` — a mudança de tag não afeta o CSS existente (todo seletor é por classe), então o crossfade descrito acima continua igual. O script escuta o clique: ignora cliques modificados (`ctrl`/`cmd`/`shift`/botão do meio — deixa abrir em nova aba normalmente), senão faz `preventDefault()`, adiciona `.is-blinking` em todo `.pid-icon-eye` dentro do link (pega os olhos das duas marcas — a visível e a escondida pelo crossfade, inofensivo já que a escondida tem `opacity:0`) e só navega pra `portal.html` depois de 260ms, tempo suficiente pra piscada terminar de tocar antes da página trocar — mesmo espírito de "deixar a transição ser vista antes de navegar" já usado na animação de intro do login.
|
||||
- **Login** (`static/js/login-logo-blink.js`, novo): `#login-logo-btn` é um `<button type="button">` (não um link — não há pra onde navegar a partir do próprio login), sem `preventDefault`/delay nenhum, só dispara a piscada ao clicar; é puramente decorativo, sem efeito colateral. Também aqui foi a oportunidade de simplificar um mecanismo que existia só por causa do `<img>`: a logo do login **alternava** entre `pid-icone.svg`(claro)/`pid-icone-escuro.svg`(escuro) conforme `data-theme`, via `pidSyncLoginLogo()` em `theme.js` — removida junto com essa mudança, a pedido do usuário ("deixe no tema claro a mesma logo usada no tema escuro"), já que o card do login já era congelado escuro nos dois temas mesmo antes disso (ver "Card de login" abaixo) — a lógica de alternância nunca fazia muito sentido nesse contexto. Agora `#login-logo-btn` sempre usa as cores/desenho de `pid-icone-escuro.svg`, sem nenhuma checagem de tema.
|
||||
|
||||
## Animações genéricas
|
||||
|
||||
Três `@keyframes` genéricos em `base.css` (`pidFadeIn`, `pidFadeSlideUp`, `pidScaleIn`) — reutilizados via `animation` (não `transition`) em elementos que entram/saem do layout via `hidden`/`display:none` (modal, dropdown, `.page-content` a cada navegação, `.login-card`), porque só `animation` reinicia sozinho quando um elemento passa de `display:none` para visível; `transition` não anima essa troca (não há frame intermediário). Duração sempre curta (120–200ms) — pedido explícito do usuário: "fluidas, porém rápidas, otimizando o tempo". Botões (`.btn-solid`/`.btn-outline`/`.btn-ghost`/`.btn-danger-outline`/`.icon-btn`) ganharam `transform: scale()` no `:active` como feedback de clique.
|
||||
|
||||
**Nenhuma dessas animações respeita `prefers-reduced-motion`** — decisão deliberada do usuário ("as animações devem ignorar a preferência de não mostrar animações ou de acessibilidade do computador do usuário"), não um descuido. Não adicionar um bloco `@media (prefers-reduced-motion: reduce)` desativando isso sem confirmar de novo com o usuário, já que contraria um pedido explícito.
|
||||
|
||||
### Animação de intro do login
|
||||
|
||||
Ao apertar "Entrar" com sucesso, `index.html` toca uma animação de marca em tela cheia (~6,2s: 500ms de fade + ~5,7s de animação) antes de navegar pra `portal.html` — pedido explícito do usuário, inspirado num arquivo `pid-intro-escuro.html` que ele forneceu (mesma pasta de origem dos SVGs da marca "P.I.D.", `C:\Users\Depaula\Documents\Projetos\LOGOS\pid-marca`).
|
||||
|
||||
**Origem do arquivo — por que não foi só copiado**: `pid-intro-escuro.html` não é HTML/CSS simples, é um bundle auto-contido de um editor de "Design Canvas" (Anthropic) — um `<script type="__bundler/manifest">` com um JSON mapeando uuid → recurso (`{mime, compressed, data}`, `data` sendo base64 de um gzip), incluindo o JSX fonte de verdade (`pid-intro.jsx`, a composição em si) e a biblioteca de motion (`animations-v3.jsx`, easing/interpolação), montados em tempo real por um runtime React + motor de composição por "tempo autorado" (`useComposition`/`CompositionStage`) que não faz sentido carregar em produção (pesado, e pensado pra edição/export de vídeo, não pra rodar dentro de uma página de login). A coreografia foi **extraída** (decodificado gzip+base64 por uuid, script Python ad-hoc) e **portada fielmente** pra vanilla JS/CSS — mesmas durações de cena, mesmas curvas de easing (hand-rolled, estilo Popmotion) e mesmo timing de piscada dos olhos do original; só o destino final do "voo" do ícone foi recalibrado (ver "Cena Portal" abaixo).
|
||||
|
||||
**As 4 cenas** (`PID_INTRO_CUES`/`PID_INTRO_TOTAL` em `login-intro.js` — `Build:0, Face:1, Wordmark:2, Portal:4.8`, total `5.7`): durações **aceleradas** em relação ao arquivo original (`Build`/`Face` de 2,6s/2s pra 1s cada, `Portal` de 2,3s pra ~0,9s — pedido explícito do usuário, "acelere um pouco a velocidade na qual a logo é construída"), exceto `Wordmark` (2,8s, igual ao original) — "a parte onde aparece o nome do portal deve permanecer a mesma", pedido explícito também. Os deslocamentos internos de cada cena (quando cada elemento começa/termina de animar) foram reproporcionados pra caber na duração nova, não são mais os valores originais do arquivo — só a cena Wordmark manteve os originais, já que a duração dela não mudou.
|
||||
- **Build** (1s): o contorno do corpo do ícone se desenha (`stroke-dashoffset`, `pathLength="100"` normaliza o path pra unidades 0–100 independente da geometria real), preenche a cor, a aba dourada "cai" (`easeOutBack`, dá um leve overshoot) e a "tela"/rosto do ícone abre (scale+opacity).
|
||||
- **Face** (1s): os dois olhos aparecem com "pop" (`easeOutBack`) e piscam uma vez (`pidIntroBlinkAt()` — uma janela de 220ms em que a escala vertical do olho vai de 1 a 0 e volta a 1, formando o fecha-e-abre; a mesma função é reaproveitada em 2 momentos diferentes agora, ver abaixo — o terceiro blink do original, na cena Portal, foi removido: a cena ficou curta demais pra caber um blink visível com o ícone já encolhendo/voando), o sorriso dourado se desenha (mesma técnica de `stroke-dashoffset` do corpo).
|
||||
- **Wordmark** (2,8s, **inalterada**): o ícone desliza pra esquerda (`SHIFT_X=-100px`) enquanto "P.I.D." (fonte "Space Grotesk" 700, cada letra com um "." dourado à parte) + uma régua dourada (`scaleX`) + a tagline "Portal Interno da De Paula" (fonte "DM Mono", uppercase, letter-spacing largo) aparecem — cada letra com seu próprio atraso escalonado (`CUES.Wordmark + 0.25 + i*0.13`).
|
||||
- **Portal** (~0,9s): a wordmark esmaece quase na hora (`Portal` a `Portal+0.3`), o ícone encolhe (de 160px pra 40px — o mesmo tamanho de `.sidebar__logo--icon`, ver "Logos em `static/img/`" acima) e "voa" até o canto superior esquerdo em 0,5s (`Portal+0.05` a `Portal+0.55`), e o stage inteiro (ícone+wordmark) esmaece logo depois, sobrepondo o fim do voo (`Portal+0.5` a `Portal+0.9`) — sem pausa parada entre o ícone assentar e o fade começar (ajuste de uma rodada anterior, que já tinha comprimido essa cena de 2,3s pra ~0,95s; esta rodada só encurtou mais um pouco, até ~0,9s). **Diferença deliberada em relação ao original** (além do tempo): lá o destino é um pixel fixo dentro de um frame de vídeo de exportação 1920×1080 (`logoX:-892, logoY:-496`, coordenadas que não existem em página nenhuma); aqui, `pidIntroCornerTarget()` calcula o alvo em tempo real a partir de `window.innerWidth/innerHeight`, mirando um ponto perto do canto real da janela — a ideia de "o ícone termina indo pro cabeçalho/sidebar do app" só faz sentido revisitada assim, já que o original nunca foi pensado pra rodar dentro de uma página de verdade.
|
||||
- As duas piscadas do olho (`Face+0.8`, `Wordmark+1.1`) e as três funções de easing usadas (`easeOutCubic`/`easeInOutQuad`/`easeOutBack`) seguem as fórmulas exatas extraídas de `animations-v3.jsx` — ver o código de `login-intro.js` se precisar ajustar timing, não redesenhar do zero.
|
||||
|
||||
**Fade de entrada, antes do ícone começar a se desenhar** (pedido explícito do usuário — a primeira versão fazia o overlay aparecer de repente por cima do card ainda visível, "de um modo bruto"; a duração foi ajustada de 350ms pra 500ms numa rodada seguinte, "está muito rápido... só pra ficar mais fluído"): `pidPlayLoginIntro()` aplica `.is-leaving` no `.login-card` (`login.css`, `opacity:0; transform:scale(0.98)`, transição de 500ms) e `.is-visible` no `#login-intro` (`opacity:0→1`, mesmos 500ms, `PID_INTRO_FADE_MS`) ao mesmo tempo — o card se dissolve enquanto o overlay (já na cor final sólida) sobe por cima, um cross-fade real, não uma troca instantânea. Só depois desses 500ms (`setTimeout`) é que o loop de `requestAnimationFrame` do ícone começa (`start = performance.now()` é atribuído só nesse momento, não antes).
|
||||
|
||||
**Arquivos**: `static/css/login-intro.css` (só `index.html`) — formas idênticas às já usadas em `pid-icone-escuro.svg`/`pid-logo-horizontal-escuro.svg` (hex hardcoded — `#7b5ba8`/`#c6a24a`/`#f2ede3`, não são tokens de tema, são a paleta fixa da marca, mesmo espírito de `.login-slogan` já hardcodar `#c6a24a`); a **exceção** é o fundo do overlay, que reage ao tema (ver "Tema claro" logo abaixo) em vez de ser um hex fixo da marca. `static/js/login-intro.js` expõe `pidPlayLoginIntro(onDone)` (global, chamada só por `auth.js`) — monta o loop de `requestAnimationFrame`, calcula cada valor (`bodyDraw`, `eyeL`, `shift`, `fly`, ...) a partir de `T` (segundos decorridos desde o início do loop, via `performance.now()`) e escreve direto nos atributos/estilo dos elementos do overlay (`#login-intro`, já presente e `hidden` no HTML de `index.html`); chama `onDone()` quando `T` atinge `PID_INTRO_TOTAL`. Fontes "Space Grotesk" (peso 700) e "DM Mono" adicionadas ao mesmo `<link>` do Google Fonts de `index.html`, junto de "Bree Serif"/"Pinyon Script" já usadas ali.
|
||||
|
||||
**Tema claro**: o fundo do overlay (`#121017`, o valor fixo de `--bg-canvas` no tema escuro) e o texto do wordmark (`.login-intro__letter`/`.login-intro__tagline`, cor clara — pensados pra contrastar com um fundo escuro) só faziam sentido enquanto o overlay era sempre escuro (decisão original, pra login → animação → portal ler como uma coisa só). O usuário pediu que, no tema claro, o fundo da animação também acompanhasse o `--bg-canvas` claro (mesmo raciocínio já aplicado a `.login-page` em `login.css`) — o que por sua vez tornou o texto claro do wordmark ilegível contra um fundo claro. `:root[data-theme="light"]` overrides em `login-intro.css` cobrem os dois: `.login-intro` vira `background: var(--bg-canvas)` e `.login-intro__letter`/`.login-intro__tagline` viram cores escuras (`#241c33`/`rgba(36, 28, 51, 0.62)`, os mesmos tons só invertidos). O **ícone não precisou de override** — o corpo roxo (`#7b5ba8`) tem contraste de sobra contra um fundo claro, e a "tela" do disquete (`#241c33`) já é escura por si só, então os olhos/sorriso continuam legíveis nos dois temas sem mudar nada. Nada disso afeta o tema escuro (padrão), que continua com os valores fixos originais.
|
||||
|
||||
**Fluxo de navegação**: `auth.js`, no sucesso do `POST /api/auth/login/`, chama `pidPlayLoginIntro(() => { sessionStorage.setItem("pid_reveal_portal", "1"); window.location.href = "portal.html"; })` em vez de navegar direto — a navegação só acontece depois da animação inteira (não há como "pular" a animação hoje, nem foi pedido).
|
||||
|
||||
**"A barra lateral surgindo da esquerda para a direita, e em seguida o resto da tela"** (pedido explícito do usuário sobre como a tela Principal deveria surgir depois da animação, refinado duas vezes: a primeira versão só escondia `.main-content` e deixava a sidebar sempre visível desde o início, sem nenhuma entrada própria; a segunda versão deu à sidebar um fade + deslize sutil de 16px, considerado "ainda não satisfatório" — pequeno demais pra ler como "surgindo da esquerda pra direita"): `static/js/portal-reveal.js` (só `portal.html`, incluído `defer` logo depois de `theme.js` — mesmo padrão de "script que roda como IIFE de topo antes da primeira pintura pra evitar flash", ver "Ordem de `<script>`" acima) checa a `sessionStorage` marcada por `auth.js`; se presente, remove a marca (não sobrevive a um F5) e aplica **duas** classes em `<html>` antes do `DOMContentLoaded`: `pid-entering-sidebar` (esconde só `.sidebar`, `opacity:0` + `transform:translateX(-100%)` — a largura inteira dela, não um deslize de poucos pixels, pra realmente ler como um slide de fora da tela) e `pid-entering` (esconde `.main-content`, o `<div>` que envolve topbar **e** conteúdo da página). As duas são removidas **em sequência**, não juntas: `pid-entering-sidebar` sai primeiro (120ms depois do `DOMContentLoaded`), a sidebar desliza da esquerda pra direita ao longo de 420ms (`.sidebar` em `layout.css` tem uma `transition` própria — `opacity 420ms ease, transform 420ms cubic-bezier(0.16, 1, 0.3, 1)`, mais longa e com uma curva de "chegada" suave, não `var(--transition-base)` — genérica demais pra um movimento desse tamanho); só depois de esperar essa mesma duração (420ms) é que `pid-entering` sai, revelando o resto do app num fade, garantindo que as duas entradas não se sobreponham. Acessar `portal.html` direto (sem passar pelo login) nunca aciona nada disso, já que a `sessionStorage` só é setada no caminho de login bem-sucedido.
|
||||
|
||||
Testado pelo usuário em três rodadas (funcional) — ajustes até agora: a cor do overlay (era `#241c33`, virou `#121017`), o fade de entrada antes do ícone começar a se desenhar (não existia, overlay aparecia de repente; a duração foi ajustada depois de 350ms pra 500ms, "muito rápido... só pra ficar mais fluído"), a compressão da cena Portal (era 2,3s com pausa parada, foi pra 0,95s corrida numa rodada e depois ~0,9s), a aceleração de Build/Face (de 2,6s/2s cada pra 1s cada, Wordmark mantida em 2,8s de propósito) e a entrada da sidebar em `portal.html` (de um fade sutil de 16px pra um slide de fora da tela inteiro, `translateX(-100%)`, 420ms). Ainda falta conferir o posicionamento do "voo" final em diferentes tamanhos de tela/com a sidebar colapsada.
|
||||
@ -1,6 +1,8 @@
|
||||
# Changelog — Links & Ferramentas / Acessos Gerais
|
||||
|
||||
> Histórico específico desta aplicação, extraído de `plano.md`. Rodadas que mudaram mais de uma aplicação ao mesmo tempo (modelo de permissões visualizar/editar em si, ambiente de desenvolvimento) continuam em `plano.md` — a numeração de rodada é a mesma usada lá, para referência cruzada.
|
||||
> Histórico específico desta aplicação, extraído de `plano.md`. Rodadas que mudaram mais de uma aplicação ao mesmo tempo (modelo de permissões visualizar/editar em si, ambiente de desenvolvimento) continuam em `plano.md`.
|
||||
>
|
||||
> **Formato**: uma entrada por rodada, `### Rodada N — Título`; quando a rodada não tem número registrado, `### Título` só. **A numeração de rodada não é global** — cada aplicação conta as próprias, e o mesmo número designa trabalhos diferentes em arquivos diferentes. Ao citar uma rodada, sempre nomear o arquivo. Ver o topo de `plano.md`.
|
||||
|
||||
### Rodada 16 — Links & Ferramentas — tela nova + permissão de edição dedicada
|
||||
|
||||
|
||||
@ -48,3 +48,14 @@ Nenhuma tela recalcula união de departamentos/liderança aqui — é uma aplica
|
||||
## CSS
|
||||
|
||||
`links-ferramentas.css` (`.lf-*`) e `acessos-gerais.css` (`.ag-*`) — um arquivo por página, só usados em suas respectivas telas.
|
||||
|
||||
## API
|
||||
|
||||
| Endpoint | Método | Uso |
|
||||
|---|---|---|
|
||||
| `/api/links-ferramentas/`, `/api/links-ferramentas/{id}/` | GET/POST/PATCH/DELETE | lista **compartilhada** (não por usuário); leitura exige `apps["links-ferramentas-visualizar"]` e escrita exige `apps["links-ferramentas-editar"]` em `permissoes["links-ferramentas"]` (`PermissaoApp`, gate por método em `get_permissions()` — ver "Modelo de permissões" abaixo); POST é `multipart/form-data` (aceita upload de `icone`); `ordem` sempre é atribuída pelo servidor na criação (ignora o que vier no payload), reordenar é PATCH trocando o `ordem` de dois itens |
|
||||
| `/api/links-ferramentas-favoritos/`, `/api/links-ferramentas-favoritos/{link_id}/` | GET/POST/DELETE | favorito **por usuário** de um cartão (chave natural é `link_id`, o id do `LinkFerramenta` — mesmo padrão de `app_id`/`notif_id`); exige só `apps["links-ferramentas-visualizar"]` (favoritar não precisa de editar); só afeta a ordem de exibição em Links & Ferramentas e o widget "Links Favoritos", nunca o `ordem` compartilhado |
|
||||
| `/api/acessos-gerais-secoes/`, `/api/acessos-gerais-secoes/{id}/` | GET/POST/PATCH/DELETE | seções do cadastro "Acessos Gerais" (aplicação dentro da seção Links & Ferramentas); leitura exige `apps["acessos-gerais-visualizar"]`, escrita exige `apps["acessos-gerais-editar"]`; excluir uma seção também exclui (`CASCADE`) os acessos dela; GET só lista seções sem `perfis_restritos` ou com interseção com os perfis do usuário |
|
||||
| `/api/acessos-gerais/`, `/api/acessos-gerais/{id}/` | GET/POST/PATCH/DELETE | linhas (acessos/logins) dentro de uma seção; mesma permissão de `acessos-gerais-secoes`; `ordem` é escopada por `secao` (servidor calcula `max(ordem)` só entre as linhas da mesma seção) |
|
||||
|
||||
> 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).
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
# Changelog — Perfis de Acesso / Usuários
|
||||
|
||||
> Histórico específico desta aplicação, extraído de `plano.md`. Rodadas que mudaram o modelo de permissões transversal em si, ou o mecanismo de autenticação/menu de conta como um todo, continuam em `plano.md` — a numeração de rodada é a mesma usada lá, para referência cruzada.
|
||||
> Histórico específico desta aplicação, extraído de `plano.md`. Rodadas que mudaram o modelo de permissões transversal em si, ou o mecanismo de autenticação/menu de conta como um todo, continuam em `plano.md`.
|
||||
>
|
||||
> **Formato**: uma entrada por rodada, `### Rodada N — Título`; quando a rodada não tem número registrado, `### Título` só. **A numeração de rodada não é global** — cada aplicação conta as próprias, e o mesmo número designa trabalhos diferentes em arquivos diferentes. Ao citar uma rodada, sempre nomear o arquivo. Ver o topo de `plano.md`.
|
||||
|
||||
### Rodada 2 — Perfis de Acesso
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
# Changelog — Ramais
|
||||
|
||||
> Histórico específico desta aplicação, extraído de `plano.md`. Rodadas que mudaram mais de uma aplicação ao mesmo tempo (favicon, tema, texto do menu) continuam em `plano.md` — a numeração de rodada é a mesma usada lá, para referência cruzada.
|
||||
> Histórico específico desta aplicação, extraído de `plano.md`. Rodadas que mudaram mais de uma aplicação ao mesmo tempo (favicon, tema, texto do menu) continuam em `plano.md`.
|
||||
>
|
||||
> **Formato**: uma entrada por rodada, `### Rodada N — Título`; quando a rodada não tem número registrado, `### Título` só. **A numeração de rodada não é global** — cada aplicação conta as próprias, e o mesmo número designa trabalhos diferentes em arquivos diferentes. Ao citar uma rodada, sempre nomear o arquivo. Ver o topo de `plano.md`.
|
||||
|
||||
### Rodada 21 — Ramais — tela nova
|
||||
|
||||
|
||||
@ -68,3 +68,17 @@ O botão "Ramais" do topbar (`#ramais-btn`, presente em `portal.html`/`links-fer
|
||||
## CSS
|
||||
|
||||
`ramais.css` (`.ram-*`) — a tabela em si reaproveita `.pa-table*`/`.pa-row-actions` de `perfis-acesso.css` (mesmo padrão que `usuarios.html` já usa sem CSS próprio). O modal de consulta rápida (`#ramais-btn`) tem CSS próprio em `ramais-lookup.css` (`.ram-lookup-*`), autocontido (não reaproveita `.pa-table` porque as 3 páginas que o usam não carregam `perfis-acesso.css`).
|
||||
|
||||
## API
|
||||
|
||||
| Endpoint | Método | Uso |
|
||||
|---|---|---|
|
||||
| `/api/ramais/` | GET | diretório **mesclado**: todo usuário ativo (linha montada do cadastro, sem precisar de nenhum registro extra) + linhas avulsas de `Ramal`; leitura exige `apps.visualizar` |
|
||||
| `/api/ramais/`, `/api/ramais/{id}/` | POST/PATCH/DELETE | CRUD só das linhas avulsas (`Ramal`, sem `Usuario` por trás); escrita exige `apps.editar` |
|
||||
| `/api/ramais/usuarios/` | GET | lista enxuta (`id`/`nome`) de usuários ativos pra alimentar o `<select>` "Lista de Usuários" do modal de Criar Ausência — não é `/api/usuarios/` de propósito |
|
||||
| `/api/ramais/usuarios/{usuario_id}/` | PATCH | `{numero}` — grava direto em `Usuario.ramal`; é como a tela edita o ramal de um colaborador de verdade (exige `apps.editar`) |
|
||||
| `/api/ramais-ausencias/`, `/api/ramais-ausencias/{id}/` | GET/POST/PATCH/DELETE | períodos de ausência; "ausente agora" nunca é lido daqui direto pelo frontend, vem calculado em `usuario_ausente`/`usuario_ausencia_ativa_id` na listagem de `/api/ramais/`; `PATCH` com `{"encerrada_manualmente": true}` encerra antes do previsto |
|
||||
| `/api/telefones-externos/`, `/api/telefones-externos/{id}/` | GET/POST/PATCH/DELETE | subtela "Telefones Externos" de `ramais.html`; mesma permissão `PermissaoApp("ramais", ...)` do diretório de Ramais |
|
||||
| `/api/funcoes-telefonia/`, `/api/funcoes-telefonia/{id}/` | GET/POST/PATCH/DELETE | subtela "Funções de Telefonia" de `ramais.html`; idem, mesma permissão de `ramais`; as 13 linhas padrão vêm de `seed_portal` |
|
||||
|
||||
> 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).
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
# Changelog — Solicitações
|
||||
|
||||
> Histórico específico desta aplicação, extraído de `plano.md`.
|
||||
>
|
||||
> **Formato**: uma entrada por rodada, `### Rodada N — Título`; quando a rodada não tem número registrado, `### Título` só. **A numeração de rodada não é global** — cada aplicação conta as próprias, e o mesmo número designa trabalhos diferentes em arquivos diferentes. Ao citar uma rodada, sempre nomear o arquivo. Ver o topo de `plano.md`.
|
||||
|
||||
Nenhuma rodada dedicada foi registrada em `plano.md` para esta seção — os links foram cadastrados/ajustados ao longo de outras rodadas sem um pedido próprio que justificasse uma entrada de changelog. A única decisão de arquitetura relevante (por que os links não podem ser embutidos em iframe) está documentada em `docs/solicitacoes/solicitacoes.md`.
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
# Changelog — Temas Sazonais
|
||||
|
||||
> Histórico específico desta aplicação, extraído de `plano.md`. Rodadas que mudaram mais de uma aplicação ao mesmo tempo continuam em `plano.md` — a numeração de rodada é a mesma usada lá, para referência cruzada.
|
||||
> Histórico específico desta aplicação, extraído de `plano.md`. Rodadas que mudaram mais de uma aplicação ao mesmo tempo continuam em `plano.md`.
|
||||
>
|
||||
> **Formato**: uma entrada por rodada, `### Rodada N — Título`; quando a rodada não tem número registrado, `### Título` só. **A numeração de rodada não é global** — cada aplicação conta as próprias, e o mesmo número designa trabalhos diferentes em arquivos diferentes. Ao citar uma rodada, sempre nomear o arquivo. Ver o topo de `plano.md`.
|
||||
|
||||
### Rodada 94 — Tema sazonal Halloween: logo/mascote vampiro, intro de abertura e "susto" ao clicar
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ Troca sazonal da marca P.I.D. (logo, ícone da sidebar/login, favicon, intro de
|
||||
|
||||
## Origem dos assets (Halloween)
|
||||
|
||||
Mascote "vampiro" (a mesma marca P.I.D. fantasiada de Drácula — capa com gola erguida e forro vinho, lápis de olho, dois dentinhos) desenhada em `C:\Users\Depaula\Documents\Logos\P.I.D. Logo Design Halloween\pid-marca-halloween\` (SVGs de referência + `leiame.md`, fora do repositório, mesmo papel de `pid-marca/` pra marca "P.I.D." normal — ver "Logos em `static/img/`" no `CLAUDE.md` raiz) e a intro de abertura em `...\P.I.D. Logo Design Halloween\Logo intro animation Halloween\export-halloween\pid-intro-halloween.html` (mesmo formato de bundle "Design Canvas" do `pid-intro-escuro.html` original — ver `CLAUDE.md` raiz, "Animação de intro do login" → "Origem do arquivo"; a composição (`PidHalloween`, cenas `Rise`/`Scare`/`Settle`/`Wordmark`, 1,3s/1,5s/1,4s/1,8s) foi extraída do bundle com o mesmo processo ad-hoc (gzip+base64 por uuid) e portada fielmente pra vanilla JS/CSS). Cópias dos SVGs de referência (não usadas diretamente pela UI, que injeta o markup inline — ver abaixo) vivem em `static/img/pid-halloween-favicon.svg`/`pid-halloween-icone-escuro.svg`/`pid-halloween-logo-horizontal-escuro.svg`.
|
||||
Mascote "vampiro" (a mesma marca P.I.D. fantasiada de Drácula — capa com gola erguida e forro vinho, lápis de olho, dois dentinhos) desenhada em `C:\Users\Depaula\Documents\Logos\P.I.D. Logo Design Halloween\pid-marca-halloween\` (SVGs de referência + `leiame.md`, fora do repositório, mesmo papel de `pid-marca/` pra marca "P.I.D." normal — ver "Logos em `static/img/`" em `docs/identidade-visual/identidade-visual.md`) e a intro de abertura em `...\P.I.D. Logo Design Halloween\Logo intro animation Halloween\export-halloween\pid-intro-halloween.html` (mesmo formato de bundle "Design Canvas" do `pid-intro-escuro.html` original — ver `CLAUDE.md` raiz, "Animação de intro do login" → "Origem do arquivo"; a composição (`PidHalloween`, cenas `Rise`/`Scare`/`Settle`/`Wordmark`, 1,3s/1,5s/1,4s/1,8s) foi extraída do bundle com o mesmo processo ad-hoc (gzip+base64 por uuid) e portada fielmente pra vanilla JS/CSS). Cópias dos SVGs de referência (não usadas diretamente pela UI, que injeta o markup inline — ver abaixo) vivem em `static/img/pid-halloween-favicon.svg`/`pid-halloween-icone-escuro.svg`/`pid-halloween-logo-horizontal-escuro.svg`.
|
||||
|
||||
## `static/js/seasonal-theme.js`
|
||||
|
||||
|
||||
95
plano.md
95
plano.md
@ -37,10 +37,23 @@ mapa completo), pelo mesmo motivo que levou a documentação técnica a ser
|
||||
dividida por aplicação (`CLAUDE.md`/`docs/*.md`): reduzir conflito de
|
||||
edição quando mais de uma aplicação está sendo trabalhada ao mesmo tempo, e
|
||||
manter cada changelog focado no que interessa a quem só mexe naquela
|
||||
aplicação. A numeração de rodada usada aqui é a mesma referenciada nos
|
||||
changelogs de cada aplicação (e vice-versa) — uma rodada que só aparece no
|
||||
`CHANGELOG.md` de uma aplicação específica não tem entrada aqui, e
|
||||
vice-versa.
|
||||
aplicação. Uma rodada que só aparece no `CHANGELOG.md` de uma aplicação
|
||||
específica não tem entrada aqui, e vice-versa.
|
||||
|
||||
**A numeração de rodada NÃO é global** (corrigido em 2026-09-21; até então
|
||||
este arquivo afirmava o contrário). Na prática, cada aplicação passou a
|
||||
contar as próprias rodadas, e o mesmo número foi reutilizado por trabalhos
|
||||
diferentes em arquivos diferentes — por exemplo, "rodada 93" é a remoção do
|
||||
placeholder "Relatório Setorial" aqui e a DRE agrupada por árvore em
|
||||
`portal_api/dashboard_contabil/CHANGELOG.md`; "rodada 94" designa quatro
|
||||
assuntos distintos em quatro arquivos; as rodadas 95 a 101 são reivindicadas
|
||||
ao mesmo tempo por Temas Sazonais e pelo Relatório Contábil, com conteúdos
|
||||
sem nenhuma relação. O mesmo vale para 107, 108 e 132.
|
||||
|
||||
Consequência, e a regra a seguir: **toda citação de rodada precisa nomear o
|
||||
arquivo** ("ver rodada 45 em `portal_api/indicadores/CHANGELOG.md`"), nunca
|
||||
só o número. Um "ver rodada 94" solto é ambíguo e não deve ser escrito.
|
||||
Renumerar o histórico inteiro não vale o esforço e não está planejado.
|
||||
|
||||
## Cronologia de construção
|
||||
|
||||
@ -294,7 +307,7 @@ Usuário forneceu uma nova marca (`C:\Users\Depaula\Documents\Projetos\LOGOS\pid
|
||||
- **Login (`index.html`)**: o card ficou **congelado escuro nos dois temas** (tokens dedicados `--login-card-bg`/`--login-field-bg`/`--login-border`/`--login-text-*` em `tokens.css`, nunca redefinidos em `:root[data-theme="light"]` — mesmo padrão da sidebar) — pedido explícito depois de uma primeira tentativa (só a caixa de login/senha escura) não ter sido o que o usuário queria; só o fundo da página ao redor do card continua claro/escuro por tema. A logo alterna entre `pid-icone.svg`/`pid-icone-escuro.svg` conforme `data-theme` (`pidSyncLoginLogo()` em `theme.js` — **removido na rodada 76**), e o texto "Portal De Paula" virou "Portal Interno da De Paula". Um slogan da marca ("Grandes aplicações de todos os tamanhos.", fonte "Pinyon Script"/dourado `#c6a24a`) foi adicionado — testado primeiro no topbar de `portal.html` (rodada 28), removido de lá a pedido do usuário, e reintroduzido no login (rodapé, depois movido pra logo abaixo do título), tamanho ajustado até ficar legível sem competir com o título.
|
||||
- **Sidebar** (10 shells): `sidebar__brand` passou a ter duas logos sempre no DOM, sobrepostas com `position:absolute` dentro de um container de altura fixa, alternando por `opacity`+`scale()` (crossfade, não troca de `src`) conforme a sidebar está expandida (`pid-logo-horizontal-escuro.svg`, assinatura com texto) ou colapsada (`pid-icone-escuro.svg`, só o símbolo) — sempre as variantes "-escuro", já que a sidebar é sempre escura nos dois temas. Tamanho da logo expandida aumentado em duas rodadas (176px/70% → 220px/92% → 250px/96% da largura útil) até o subtítulo "PORTAL INTERNO DA DE PAULA" ficar legível.
|
||||
|
||||
Ver `CLAUDE.md` ("Logos em `static/img/`") pro estado final detalhado de cada arquivo.
|
||||
Ver `docs/identidade-visual/identidade-visual.md` ("Logos em `static/img/`") pro estado final detalhado de cada arquivo.
|
||||
|
||||
### 74. Animação de intro pós-login (porta de `pid-intro-escuro.html`)
|
||||
|
||||
@ -393,6 +406,78 @@ Pedido (2026-09-14): remover o item "Relatório Setorial" (`href="#"`, sem tela
|
||||
|
||||
Pedido (2026-09-15): deixar uma logo e uma introdução de abertura com tema de Halloween, aplicadas só durante outubro, revertendo pro P.I.D. normal depois — sem descartar nada do que já existe. Implementado como mecanismo **100% runtime** (nenhum arquivo original alterado ou removido — fora da janela ativa, tudo volta sozinho): troca de ícone/favicon/assinatura pelo mascote vampiro, uma intro de abertura pós-login alternativa, uma animação de "susto" ao clicar na logo, teias de aranha decorativas nos cantos do login/tela Principal (com uma aranha "matável" ao clicar, óbito persistido) e um toggle no menu da conta pro usuário desligar temas sazonais por preferência própria (ou acessibilidade — aracnofobia). Ainda em preview (`PID_HALLOWEEN_PREVIEW_FORCE` força ligado pra revisão visual, antes de confirmar a janela de outubro definitivamente). Histórico técnico completo (rodadas 94-100) e detalhamento em `docs/temas-sazonais/CHANGELOG.md`/`temas-sazonais.md` — a partir daqui, rodadas específicas desta aplicação não duplicam entrada aqui (ver nota no topo deste arquivo).
|
||||
|
||||
### 149. Revisão da documentação (2026-09-21)
|
||||
|
||||
Rodada de documentação, não de código. **Nenhum arquivo `.py`/`.js`/`.html`/`.css`
|
||||
foi tocado.** O usuário pediu uma avaliação dos `.md` do projeto e depois a
|
||||
aplicação das correções de fato e da reorganização.
|
||||
|
||||
**Correções de fato** (o que estava escrito e era falso):
|
||||
|
||||
- `README.md` mandava rodar `python manage.py seed_portal` no passo a passo de
|
||||
"Como rodar localmente", enquanto o `CLAUDE.md` dedica um parágrafo a dizer
|
||||
que isso nunca deve rodar neste ambiente (o banco do `.env` é produção).
|
||||
Removido do passo a passo e substituído por um aviso.
|
||||
- **Uma aplicação inteira não estava documentada em lugar nenhum**:
|
||||
`importacao-plano-saude-de-paula` (item de menu em `catalogo.py`, rota em
|
||||
`config/urls.py`, 7 models, 5 grupos de rota, template de 911 linhas). Os
|
||||
docstrings de `models.py`/`views.py` já apontavam para uma seção do
|
||||
`CLAUDE.md` de `portal_api/planos_saude/` que nunca tinha sido escrita.
|
||||
Escrita agora, mais entrada no changelog daquela aplicação, no mapa do
|
||||
`README.md`, na tabela de Páginas do `CLAUDE.md` e no `prd.md`.
|
||||
- **A numeração de rodada não é global**, ao contrário do que este arquivo e os
|
||||
12 changelogs afirmavam. 15 números aparecem em mais de um arquivo e em 12
|
||||
deles o assunto é diferente (93, 94, 95-101, 107, 108, 132). Corrigido aqui,
|
||||
no `CLAUDE.md` e no cabeçalho de todos os changelogs; a regra agora é que
|
||||
toda citação de rodada nomeie o arquivo.
|
||||
- **Contagens desatualizadas** no `CLAUDE.md`, que são instruções na prática
|
||||
("replicar nos 10 shells"): "as 8 páginas HTML" (são 15 templates), "10
|
||||
shells" em 5 lugares (são 13), "as 11 páginas" do favicon (são 14), "as
|
||||
outras 11 páginas" com `page-content--wide` (são 12).
|
||||
- `pid-favicon.svg` e `pid-logo-horizontal.svg` eram descritos como se
|
||||
estivessem em `static/img/` e **nunca foram copiados para lá**; os três SVGs
|
||||
sazonais que existem na pasta não constavam do inventário.
|
||||
- O `prd.md` listava Não Conformidades e Relatório Contábil, duas aplicações
|
||||
completas, dentro do bullet "Reservados no menu, sem tela própria ainda".
|
||||
Viraram seção própria, junto de Temas Sazonais e da variante De Paula.
|
||||
|
||||
**Reorganização**:
|
||||
|
||||
- **`portal_api/dashboard_contabil/CLAUDE.md` reescrito como estado atual**:
|
||||
65% dele (129 KB de ~196 KB) eram seções datadas por rodada, ao lado de um
|
||||
changelog de 110 KB com as mesmas rodadas. Reescrito por subsistema, sem
|
||||
cronologia, 196 KB → 79 KB, sem descartar fato técnico (a redução é
|
||||
consolidação de repetição mais a remoção do histórico, que já estava no
|
||||
changelog). Ganhou a tabela dos 22 endpoints da ferramenta, que não existia.
|
||||
- **`CLAUDE.md` da raiz: 100 KB → 67 KB.** É o arquivo carregado em toda
|
||||
sessão, e ~40% dele era detalhe de uma área só. Três movimentos: (a) os 36
|
||||
endpoints de aplicação saíram da tabela de API para a doc de cada aplicação,
|
||||
deixando só os 19 transversais mais um ponteiro — a tabela era metade índice,
|
||||
metade despejo, e não tinha nenhuma linha do Relatório Contábil nem de Não
|
||||
Conformidades; (b) "Logos" (9,4 KB) e "Animações" (12,0 KB) viraram
|
||||
`docs/identidade-visual/`, com resumo e ponteiro na raiz; (c) "Ajuda de
|
||||
aplicação" e "Modal de confirmação" condensados, tirando a lista de migração
|
||||
arquivo a arquivo (que é história, não estado) e corrigindo um bullet que
|
||||
estava na seção errada.
|
||||
- **Formato de changelog padronizado**: `### Rodada N — Título` nos 12 arquivos
|
||||
(o Relatório Contábil usava `### N.` nas 58 entradas, Não Conformidades
|
||||
misturava os dois). O título do changelog do Relatório Contábil ainda dizia
|
||||
"Dashboard Contábil", nome trocado na rodada 120.
|
||||
|
||||
**Não mexido, fica para decisão do usuário**: uma tabela de roteamento por
|
||||
prefixo (model/JS/CSS/rota → doc), que resolveria o problema de fundo descrito
|
||||
abaixo, e o que fazer com `docs/manual/` e `projects/`, que estão fora do mapa
|
||||
do `README.md`.
|
||||
|
||||
**Problema de fundo levantado e não resolvido**: a documentação divide as
|
||||
aplicações entre "com pacote Python" (`CLAUDE.md` carregado automaticamente) e
|
||||
"sem pacote" (ler manualmente), o que sugere que trabalhar numa aplicação traz
|
||||
a doc dela junto. Na prática quase nunca traz: o código de todas elas vive em
|
||||
`portal_api/models.py` (2.638 linhas), `views.py` (5.086) e `serializers.py`
|
||||
(2.764), fora dos pacotes, que têm só 590 a 1.625 linhas de helper puro cada.
|
||||
Editar os models ou as views do Relatório Contábil não dispara o `CLAUDE.md`
|
||||
dele.
|
||||
|
||||
## Limitações conhecidas / decisões assumidas
|
||||
|
||||
- Calendário De Paula (empresa) não tem mais versão interna — depende
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
# Changelog — Simulação de Custo de Contratação
|
||||
|
||||
> Histórico específico desta aplicação, extraído de `plano.md` (mesma numeração de rodada usada lá, para referência cruzada).
|
||||
> Histórico específico desta aplicação, extraído de `plano.md`.
|
||||
>
|
||||
> **Formato**: uma entrada por rodada, `### Rodada N — Título`; quando a rodada não tem número registrado, `### Título` só. **A numeração de rodada não é global** — cada aplicação conta as próprias, e o mesmo número designa trabalhos diferentes em arquivos diferentes. Ao citar uma rodada, sempre nomear o arquivo. Ver o topo de `plano.md`.
|
||||
|
||||
### Rodada 37 — Simulação de Custo de Contratação (Geradoc)
|
||||
|
||||
|
||||
@ -11,3 +11,12 @@ Ferramenta que substitui a planilha manual de custo de contratação (`projects/
|
||||
- **Correção deliberada em relação à planilha original**: a planilha nunca somava a dedução por dependente (R$189,59/dependente) à base do IRRF quando usava o desconto real de INSS — só quando usava o desconto simplificado (que por lei substitui os dois). Confirmado como gap com o usuário e corrigido: ao usar o desconto real de INSS, a dedução por dependente também é subtraída agora (`custo_contratacao/calculo.py`).
|
||||
- **PDF via `reportlab`** (pure-Python, sem dependência nativa problemática no Windows) — cabeçalho é um banner marrom escuro com `logo-branco.png` + "De Paula Contadores", nas cores reais da marca (dourado `#D3AF4D`, marrom `#4A3C28`, amostradas do próprio `logo.png`), não o roxo do tema de interface do Portal.
|
||||
- Localização no menu (dentro de **Geradoc**, ao lado de "Gerar Contrato"/"Gerar Procuração") foi escolha explícita do usuário, não Utilitários.
|
||||
|
||||
## API
|
||||
|
||||
| Endpoint | Método | Uso |
|
||||
|---|---|---|
|
||||
| `/api/simulacao-custo-contratacao/gerar/` | POST | calcula (`portal_api.custo_contratacao.calculo.calcula_custo_empregado`) e devolve o PDF direto na resposta (`application/pdf`, sem persistir nada); `PermissaoApp`-like check manual via `permissao_app("geradoc", "simulacao-custo-contratacao")` |
|
||||
| `/api/parametros-fiscais-custo-contratacao/` | GET/PATCH | tabelas de INSS/IRRF + parâmetros da Lei 15.270/2025 usados pela simulação (`ParametroFiscalCustoContratacao`, singleton `pk=1`); mesma permissão da simulação, sem par visualizar/editar dedicado |
|
||||
|
||||
> 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).
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
# Changelog — Dashboard Contábil
|
||||
# Changelog — Relatório Contábil
|
||||
|
||||
> Histórico específico desta aplicação, extraído de `plano.md` (mesma numeração de rodada usada lá, para referência cruzada).
|
||||
> Histórico específico desta aplicação, extraído de `plano.md`. As entradas anteriores à rodada 120 chamam a aplicação de "Dashboard Contábil", nome visível até aquele rename — os nomes técnicos (`dashboard_contabil`/`dashboard-contabil.*`/`Contabil*`/`/api/contabil-*`) nunca mudaram.
|
||||
>
|
||||
> **Formato**: uma entrada por rodada, `### Rodada N — Título`; quando a rodada não tem número registrado, `### Título` só. **A numeração de rodada não é global** — cada aplicação conta as próprias, e o mesmo número designa trabalhos diferentes em arquivos diferentes. Ao citar uma rodada, sempre nomear o arquivo. Ver o topo de `plano.md`.
|
||||
|
||||
### 92. Dashboard Contábil (Relatórios > Contabilidade) — v1: execução, auditoria e análise
|
||||
### Rodada 92 — Dashboard Contábil (Relatórios > Contabilidade) — v1: execução, auditoria e análise
|
||||
|
||||
Pedido: otimizar a conferência de balancetes hoje feita manualmente pelo Fisco/Contábil (`ITD-FISCO-7513`). Nova aplicação em Relatórios > Contabilidade: o contador anexa o PDF de Balancete + DRE (modelo Questor, mesmo relatório hoje enviado ao cliente), a ferramenta extrai as contas e roda um motor de 10 regras de auditoria (balanceamento Ativo x Passivo, débito ≠ crédito, saldo negativo de caixa, contas transitórias/genéricas com saldo, contas que deveriam ficar zeradas, sinal de saldo invertido, variação atípica de saldo/DRE mês a mês, percentual custo/receita fora do padrão da própria empresa), apresentando os achados numa tela de revisão com observações por conta e conclusão da análise.
|
||||
|
||||
@ -10,7 +12,7 @@ Decisões confirmadas com o usuário: entrada só em PDF (levantada a alternativ
|
||||
|
||||
Desafio técnico principal: o relatório Questor de Balancete/DRE desenha cada caractere em posição própria e inclui, por baixo do texto real, uma grade densa de caracteres de espaço cobrindo toda a linha — isso quebra a extração padrão do pdfplumber (`extract_words`/`extract_text`), que trata esses espaços como separadores reais e fragmenta números em dígitos isolados. Resolvido reconstruindo cada linha direto de `page.chars`, ignorando espaços literais e reinserindo um só quando o vão horizontal indica uma quebra de campo real — validado rodando de fato contra o PDF real de referência do usuário antes de escrever o parser definitivo. 4 models novos (`ContabilApuracao`/`ContabilConta`/`ContabilLinhaDre`/`ContabilAchado`, migração `0055`), pacote `portal_api/dashboard_contabil/` sem ORM (`parser.py`/`regras.py`/`pipeline.py`/`modelos.py`), subgrupo "Contabilidade" novo em `catalogo.py` dentro de `relatorios`. Detalhe técnico completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 93. Dashboard Contábil — DRE agrupada por árvore + botão "Gerar Dashboard" implementado
|
||||
### Rodada 93 — Dashboard Contábil — DRE agrupada por árvore + botão "Gerar Dashboard" implementado
|
||||
|
||||
A DRE (aba "DRE" da revisão) ganhou a mesma árvore recolhível que o Balancete já tinha (`renderDre()` reaproveita o algoritmo de `renderContas()`, agora sobre `ContabilLinhaDre.nivel`). O botão "Gerar Dashboard" (renomeado de "Gerar Dashboard HTML") saiu do estado desabilitado da rodada 92: gera um documento HTML autocontido, com a marca do escritório, com indicadores financeiros (ROA, ROE, Kanitz, EBIT, EBITDA, Liquidez Corrente/Seca/Geral, Composição/Grau de Endividamento, IPL), gráfico de evolução do Resultado Líquido (Chart.js via CDN), DRE/Balancete agrupados e as observações que o contador já registrou na aplicação. Escopo confirmado com o usuário: sempre uma apuração por vez (sem "Filial"/consolidação multi-empresa do BI antigo que este relatório substitui, fora de escopo).
|
||||
|
||||
@ -24,27 +26,27 @@ Nova action `ContabilApuracaoViewSet.dashboard()` renderiza `templates/dashboard
|
||||
|
||||
**Terceiro ajuste, mesmo dia**: usuário achou o visual simples demais e pediu mais bonito/dinâmico/animado. Fontes "Manrope"/"Inter", gradiente+glow no cabeçalho, ícones SVG, abas com indicador deslizante, contagem animada nos cards (sempre terminando no valor exato já formatado pelos filtros Django, nunca recalculado em JS) e cor por sinal/limiar só onde é seguro sem inventar nada (ROA/ROE/EBIT/EBITDA por sinal, Liquidez por ≥1 — convenções de mercado; Kanitz/Endividamento/IPL ficam sem cor, sem limiar validado). Toda animação de entrada segue a regra de nunca fixar `opacity:0` fora de `@keyframes`, pra `@media print` bastar sozinho pra devolver tudo ao normal na impressão. Corrigido também um bug de condição de corrida entre dois listeners de `beforeprint` que competiam entre si quando a aba "Indicadores" nunca tinha sido aberta. Detalhe completo no `CLAUDE.md` desta pasta ("Visual").
|
||||
|
||||
### 94. Dashboard Contábil — observações do relatório redistribuídas por aba
|
||||
### Rodada 94 — Dashboard Contábil — observações do relatório redistribuídas por aba
|
||||
|
||||
Pedido: a seção única "Observações da Análise" (fora de todas as abas) misturava observações de Balancete, D.R.E. e achados de auditoria juntas, sem separar por contexto. Passou a ficar assim: a aba **Balancete** termina com "Observações do Balancete" (só as observações de conta); a aba **D.R.E.** termina com "Observações da D.R.E." (só as observações de linha); a aba **Indicadores** termina com "Todas as Observações da Análise" — as três listas juntas (contas + linhas de DRE + achados), cada item prefixado com a origem ("Balancete — ...", "D.R.E. — ...", "Auditoria — ..."), já que ali não há mais uma aba pra dar esse contexto sozinha. Puramente reorganização de template (`dashboard-contabil-relatorio.html`) — as três listas já vinham prontas do backend (`views.py`), nenhuma mudança de backend foi necessária.
|
||||
|
||||
**Correção ainda na mesma rodada**: o prefixo do terceiro grupo tinha nascido "Achado de Auditoria —", contrariando um pedido já feito antes pelo usuário de nunca expor a palavra "achado" em texto visível (mesmo motivo pelo qual a aba de revisão já se chama "Observações", não "Achados" — `dashboard-contabil.html`, `data-dc-tab="achados"` com o texto "Observações"). Trocado pra "Auditoria —", no mesmo padrão dos outros dois grupos (nomeado pela origem/demonstração, não pelo tipo de registro interno). Vale como regra geral pra qualquer texto novo desta aplicação: `achado`/`Achado` só em nome de variável/model/classe CSS, nunca em texto visível ao usuário. Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 95. Card de achado (tela de revisão) expande a conta usada no apontamento
|
||||
### Rodada 95 — Card de achado (tela de revisão) expande a conta usada no apontamento
|
||||
|
||||
Pedido: nos cards de "Observações" (aba Achados da revisão), permitir expandir e ver a conta do Balancete que embasou aquele apontamento — até então só o título/mensagem da regra apareciam, sem o dado de origem. `renderAchados()` (`dashboard-contabil.js`) resolve `achado.conta` (id) pra objeto completo procurando em `apuracaoAtual.contas`, já carregado junto na mesma resposta de `/api/contabil-apuracoes/{id}/` — nenhuma chamada de API nova, nenhuma mudança de backend. Um botão "Ver conta usada no apontamento" aparece só nos achados vinculados a uma conta específica (achados das regras 3-7, que têm `conta` preenchida); as duas regras gerais (balanceamento Ativo x Passivo, débito ≠ crédito) não mostram o botão, já que não têm uma conta única por trás. Expandido, mostra código/descrição/saldo anterior/débito/crédito/saldo atual da conta, num `<dl>` novo (`.dc-achado-card__conta`, `dashboard-contabil.css`). Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 96. Lista de Observações ordenada por severidade
|
||||
### Rodada 96 — Lista de Observações ordenada por severidade
|
||||
|
||||
Pedido: na aba "Observações" da revisão, as observações apareciam na ordem em que as regras rodaram (mistura de Alta/Média/Baixa), sem prioridade visual — o usuário pediu Alta primeiro, depois Média, depois Baixa. `achadosFiltrados()` (`dashboard-contabil.js`) ganhou um `.sort()` por `PID_DC_SEVERIDADE_ORDEM` (`{alta: 0, media: 1, baixa: 2}`) depois do filtro já existente (severidade/status); dentro de uma mesma severidade a ordem original é preservada (`sort` é estável). Só ordenação de exibição, nenhuma mudança de backend/modelo.
|
||||
|
||||
### 97. Resumo por categoria (donut + cards) no topo da aba Observações
|
||||
### Rodada 97 — Resumo por categoria (donut + cards) no topo da aba Observações
|
||||
|
||||
Pedido: o usuário trouxe capturas de tela da auditoria de outro sistema (checklist de checagens + cards por categoria com a tabela de apontamentos + gráfico de distribuição) perguntando se dava pra estruturar algo parecido. Alinhado por `AskUserQuestion` que: (a) várias checagens daquele sistema dependem de dado que não vem do Balancete/DRE anexado (folha, vencimentos de fornecedor/cliente/imposto, saldo bancário) — fora do escopo já documentado desta ferramenta, não replicadas; (b) o formato escolhido foi "cards por categoria com tabela de apontamentos" + "gráfico de distribuição por categoria", só na tela de revisão do Portal (não no checklist pass/fail, não no relatório "Gerar Dashboard").
|
||||
|
||||
Nova seção `.dc-achados-resumo` no topo da aba Observações (`dashboard-contabil.html`), acima dos chips de filtro já existentes: um donut (CSS puro, `conic-gradient` — sem Chart.js/dependência nova nesta tela interativa, diferente do relatório estático que já usa Chart.js via CDN) com o total de observações no centro e legenda por categoria, mais uma grade de cards — um por regra de auditoria (as mesmas 10 de `regras.py`, `PID_DC_REGRAS` em `dashboard-contabil.js` — enumeradas sempre as 10, mesmo as que não geraram achado nesta apuração, mesmo espírito do "Nenhum registro encontrado" do sistema de referência) com a contagem e uma mini-tabela das contas/linhas apontadas (código+descrição da conta quando `achado.conta` está preenchido, "Geral" quando não — mesmas duas regras gerais que já não mostram conta no card expandido, ver rodada 95) e o status de cada uma. `renderAchadosResumo()` roda sempre sobre `apuracaoAtual.achados` **completo**, não sobre `achadosFiltrados()` — visão geral estável, independente dos chips Alta/Média/Baixa/Pendentes/Todos da lista detalhada logo abaixo. Cores das 10 categorias reaproveitam os tokens `--accent-rgb`/`--danger-rgb`/`--gold-rgb`/`--teal-rgb`/`--slate-rgb`/`--coral-rgb` já existentes (theme-aware) completados até 10 com `color-mix()`, sem hex novo hardcoded. Puramente frontend — nenhuma mudança de backend/model (o campo `regra` já existia em `ContabilAchado`). Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 98. Donut do resumo trocado pra severidade + donut/cards clicáveis (filtram a lista)
|
||||
### Rodada 98 — Donut do resumo trocado pra severidade + donut/cards clicáveis (filtram a lista)
|
||||
|
||||
Dois pedidos, mesma rodada: (a) o donut da rodada 97 mostrava distribuição por categoria/regra — o usuário pediu pra mostrar por nível de complexidade (Alta/Média/Baixa) em vez disso; (b) permitir clicar no gráfico ou nos cards pra filtrar os apontamentos relacionados na lista detalhada abaixo.
|
||||
|
||||
@ -52,19 +54,19 @@ Donut reconstruído em SVG puro (técnica clássica de `<circle r="15.9155">`, c
|
||||
|
||||
Cards de categoria (regra) continuam mostrando a contagem por regra, mas agora clicáveis também: alternam um novo estado `filtroRegra` (`achado.regra` exata ou `null`, clicar de novo no mesmo card limpa) — `achadosFiltrados()` ganhou essa terceira condição, combinando por E lógico com severidade/status. Como não há chip próprio pra esse filtro, uma faixa nova (`#dc-regra-filtro-ativo`) aparece entre os chips e a lista quando `filtroRegra` está ativo, com o nome da categoria e um botão "Limpar". Clicar em qualquer um dos dois (donut/card) dá um scroll suave até a lista (`pidDcScrollParaLista()`). Puramente frontend, nenhuma mudança de backend. Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 99. Exportar Balancete/DRE em XLSX a partir do relatório "Gerar Dashboard"
|
||||
### Rodada 99 — Exportar Balancete/DRE em XLSX a partir do relatório "Gerar Dashboard"
|
||||
|
||||
Pedido: permitir exportar Balancete ou DRE em XLSX a partir do relatório HTML. Novo módulo `portal_api/dashboard_contabil/exportacao.py` (funções puras, openpyxl, mesmo espírito de `indicadores.py`/`regras.py` — sem tocar no ORM) com `gera_xlsx_balancete()`/`gera_xlsx_dre()`, recebendo dataclasses (`LinhaBalanceteXlsx`/`LinhaDreXlsx`) já resolvidas pela view. Nova action `ContabilApuracaoViewSet.exportar_xlsx()` (`GET /api/contabil-apuracoes/{id}/exportar-xlsx/?parte=balancete` ou `?parte=dre`, mesma permissão de toggle único) monta essas linhas reaproveitando a mesma fórmula de nível/indentação já usada por `dashboard()`.
|
||||
|
||||
Cada planilha nasce com cabeçalho (empresa/CNPJ/competência), cabeçalho de colunas com a mesma paleta roxo/dourado dos outros documentos gerados pelo escritório, conta sintética/linha totalizadora em negrito+fundo dourado claro, indentação de hierarquia via `Alignment(indent=nivel)` e colunas monetárias com `number_format` brasileiro (valor gravado como número, não texto — continua editável/somável no Excel). Botão "Exportar XLSX" novo em cada seção do relatório (`.dcr-export-btn`, `dashboard-contabil-relatorio.html`) é um link direto pra API, sem JS — o browser já baixa o arquivo pelo `Content-Disposition` da resposta; escondido na impressão junto do botão "Imprimir". `openpyxl` já era dependência do projeto (usado pra leitura em outras ferramentas), essa é a primeira vez que o projeto **escreve** um XLSX com ele. Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 100. Resumo da rodada 97 reorganizado em 3 cards por grupo temático
|
||||
### Rodada 100 — Resumo da rodada 97 reorganizado em 3 cards por grupo temático
|
||||
|
||||
Pedido: os 10 cards por regra do resumo (rodada 97), cada um com uma mini-tabela de conta+status por achado, ficaram "muito poluídos visualmente" na prática (captura de tela real anexada pelo usuário). Alinhado por `AskUserQuestion` (com preview) o agrupamento das 10 regras em 3 temas fixos: "Divergências de Saldo" (balanceamento Ativo x Passivo, débito x crédito, caixa negativo, sinal de saldo invertido), "Contas Atípicas" (contas transitórias, contas que deveriam zerar, descrição genérica) e "Variações e Indicadores" (variação atípica de saldo, variação atípica na DRE, percentual custo/receita).
|
||||
|
||||
`PID_DC_GRUPOS` (nova constante, `dashboard-contabil.js`) substitui a iteração antes feita direto sobre `PID_DC_REGRAS` na grade de cards — agora 1 card por grupo (cabeçalho com o total do grupo) contendo uma linha por regra (label + contagem, sem mini-tabela de conta/status). A mini-tabela de conta+status por achado saiu do resumo (era a maior fonte de poluição visual), mas continua disponível na lista completa logo abaixo, ao clicar numa linha de regra — o clique por regra individual (não por grupo) foi preservado, mesmo comportamento de filtro da rodada 98. Puramente frontend (JS + CSS), nenhuma mudança de backend/model. Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 101. Aba "Dashboard" na tela de revisão — ocultar cards/observações antes de gerar o relatório
|
||||
### Rodada 101 — Aba "Dashboard" na tela de revisão — ocultar cards/observações antes de gerar o relatório
|
||||
|
||||
Pedido: depois da aba DRE, uma aba "Dashboard" que mostra os indicadores e observações que vão pro relatório "Gerar Dashboard" **antes** de gerá-lo, onde o contador pode ocultar um card de indicador ou uma observação — o que estiver oculto não entra no relatório gerado.
|
||||
|
||||
@ -72,7 +74,7 @@ Modelos novos: `ContabilConta.oculta_no_relatorio`/`ContabilLinhaDre.oculta_no_r
|
||||
|
||||
Nova aba `data-dc-tab="dashboard"` em `dashboard-contabil.html`, carregada sob demanda (`renderDashboardTab()`, só na primeira vez que é aberta) pra não pagar o custo de recalcular os indicadores em toda apuração aberta. Cada card/observação ganha um botão de olho (ícone SVG inline, reaproveitando `.icon-btn`) que alterna o oculto e atualiza só o item local, sem recarregar a apuração inteira; desabilitado quando a apuração já está concluída. Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 102. Banco de indicadores personalizados — fórmulas, componentes e "Ver fórmula"
|
||||
### Rodada 102 — Banco de indicadores personalizados — fórmulas, componentes e "Ver fórmula"
|
||||
|
||||
Pedido: cada card de indicador devia ser "um indicador efetivo calculado a partir das contas contábeis" — o contador poder ver a fórmula de qualquer card (inclusive os 11 de sistema) e criar indicador **novo**, escolhendo contas do Balancete/linhas da DRE/variação entre apurações/outros indicadores já existentes como componentes de uma fórmula de verdade. Escopo alinhado por `AskUserQuestion` antes de implementar: os 11 de sistema continuam com o cálculo Python fixo de sempre (zero risco de mudar um valor já calibrado), só ganharam metadados de exibição; a fórmula personalizada é avaliada por um interpretador restrito (`ast`, nunca `eval()`); "selecionar conta ou grupo" é marcar uma ou mais contas específicas via checklist, não digitar um prefixo de código.
|
||||
|
||||
@ -84,7 +86,7 @@ Metadados dos 11 de sistema (`indicadores.METADADOS_CARDS`) vieram do glossário
|
||||
|
||||
Frontend: card agora é clicável (fora dos botões) e abre um modal "Ver fórmula"; indicador personalizado ganha um botão de editar (lápis) além do de ocultar. Modal "Novo/Editar Indicador" com um construtor de componentes dinâmico, reaproveitando `.checklist-box`/`.checklist-item`/`.checklist-search` (mesmo padrão dos checklists de Perfis/Departamento) pros pickers de conta/linha da DRE — necessário porque uma apuração real tem ~100-150 contas/linhas. Bug real pego antes do usuário testar: editar um indicador configurado a partir de **outra** apuração apagaria silenciosamente qualquer código/descrição que não existisse na apuração atualmente aberta (não aparecia no checklist pra continuar marcado); corrigido mostrando esses como item extra "não encontrado nesta apuração, mantido" no topo da lista. Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 103. Indicador padrão vs. não padrão + "Gerenciar Indicadores" + modal com scroll
|
||||
### Rodada 103 — Indicador padrão vs. não padrão + "Gerenciar Indicadores" + modal com scroll
|
||||
|
||||
Pedido: (a) o modal "Novo/Editar Indicador" da rodada 102 não tinha scroll — com vários componentes, só dava pra alcançar "Salvar" dando zoom out no navegador; (b) precisava de um jeito de ver/editar qualquer indicador já criado, não só os que já estão aparecendo na apuração aberta; (c) indicador personalizado devia poder ser "padrão" (aparece automaticamente em toda apuração, comportamento que já existia) ou "não padrão" (fica salvo/editável mas só aparece numa apuração quando selecionado ali).
|
||||
|
||||
@ -92,7 +94,7 @@ Pedido: (a) o modal "Novo/Editar Indicador" da rodada 102 não tinha scroll —
|
||||
|
||||
Frontend: o botão "Novo Indicador" virou "Gerenciar Indicadores", abrindo um modal-hub com a lista **completa** (padrão e não padrão) — cada linha com nome clicável ("Ver fórmula"), editar, e (só não padrão) um checkbox "ativo nesta apuração". O form de criar/editar agora abre empilhado por cima do hub (`.modal-overlay--top`, mesmo mecanismo do modal de confirmação genérico) e ganhou um checkbox "Indicador padrão". Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 104. Migração dos 11 indicadores de sistema pro banco de indicadores
|
||||
### Rodada 104 — Migração dos 11 indicadores de sistema pro banco de indicadores
|
||||
|
||||
Pedido: reversão da decisão de escopo da rodada 102 (que tinha deixado os 11 indicadores "de sistema" — ROA/ROE/Kanitz/EBIT/EBITDA/Liquidez Corrente/Liquidez Seca/Liquidez Geral/Composição do Endividamento/Grau de Endividamento/IPL — fora do banco, calculados em Python fixo, pelo risco de mapear uma conta errado). Confirmado por `AskUserQuestion` (risco explicado antes) que o usuário queria a fórmula de verdade editável, não só o texto de exibição.
|
||||
|
||||
@ -102,7 +104,7 @@ Os 11 viraram `IndicadorContabilDefinicao` de verdade, com as mesmas chaves de a
|
||||
|
||||
`dashboard_contabil/indicadores.py` teve `CHAVES_CARDS`/`METADADOS_CARDS` removidos (zero consumidor); `calcula_indicadores()` e companhia foram **mantidos deliberadamente**, mesmo sem chamador em produção, como referência/auditoria pra recalcular e comparar se algum valor um dia parecer suspeito — única exceção neste projeto à convenção de apagar código sem uso, justificada pelo risco financeiro. O relatório "Gerar Dashboard" perdeu os 11 `.dcr-card` hardcoded, substituídos por um `{% for %}` genérico único (mesmo caminho que já servia indicador personalizado) — perda real e aceita conscientemente: cada um dos 11 tinha um ícone SVG próprio, agora todo indicador usa o mesmo ícone genérico. Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 105. Scroll aninhado no construtor de componentes + "Calcular com esta apuração"
|
||||
### Rodada 105 — Scroll aninhado no construtor de componentes + "Calcular com esta apuração"
|
||||
|
||||
Dois pedidos na sequência, testando a migração da rodada 104: (a) captura de tela mostrando que a lista de componentes tinha um scroll (320px) por cima do scroll do checklist de contas (160px) — dois scrolls aninhados deixavam a área minúscula, nem um componente inteiro cabia sem rolar duas vezes; (b) além da fórmula em texto, mostrar os valores exatos que cada componente busca e o resultado calculado, pro contador conferir se a fórmula está certa.
|
||||
|
||||
@ -110,33 +112,33 @@ Dois pedidos na sequência, testando a migração da rodada 104: (a) captura de
|
||||
|
||||
Novo botão "Calcular com esta apuração" no modal de indicador, entre "Fórmula" e as ações — chama `POST /api/contabil-apuracoes/{id}/pre-visualizar-indicador/` (`ContabilApuracaoViewSet.pre_visualizar_indicador()`) com o formulário como está na tela (ainda não salvo), calcula contra a apuração aberta e devolve o valor de cada componente + o resultado final, sem persistir nada. Reaproveita a mesma validação (`IndicadorContabilDefinicaoInputSerializer`) e o mesmo motor de cálculo (`_contabil_resolve_componente_personalizado()`/`avalia_formula()`) de criar/editar de verdade, só que sobre componentes construídos em memória, nunca salvos. Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 106. Bug real — exclusão de indicador não padrão travava o hub inteiro
|
||||
### Rodada 106 — Bug real — exclusão de indicador não padrão travava o hub inteiro
|
||||
|
||||
Usuário reportou o modal de aviso "Chave(s) de indicador não padrão inexistente(s): diferença." ao tentar alternar um indicador no hub "Gerenciar Indicadores". Causa: um indicador não padrão de chave `diferença` tinha sido excluído em algum momento, mas `IndicadorContabilDefinicaoViewSet.perform_destroy()` só bloqueava/limpava referência pela **fórmula** (`indicador_referenciado`), nunca pelas referências em `ContabilApuracao.indicadores_selecionados`/`indicadores_ocultos` — a chave ficou órfã na apuração que a tinha selecionada. Como o frontend sempre reenvia a lista **inteira** a cada alternância de checkbox (`pidDcRenderHubIndicadores`/evento `change` em `dashboard-contabil.js`), e `ContabilIndicadoresSelecionadosSerializer`/`ContabilIndicadoresOcultosSerializer` rejeitavam a lista inteira se qualquer chave nela não existisse mais, o usuário ficava travado sem conseguir alternar **nenhum** indicador na apuração afetada, não só o excluído.
|
||||
|
||||
Dois ajustes, um pro sintoma já existente e outro pra causa raiz: (a) as duas validações passaram a **descartar silenciosamente** chave inexistente/órfã em vez de rejeitar a lista inteira — é só estado de exibição (quais cards aparecem/estão ocultos), não dado auditado, então autocorrigir é seguro e resolve o travamento já em produção sem precisar de um script de correção manual; (b) `perform_destroy()` agora também limpa a chave excluída de toda `ContabilApuracao` que a referenciava em `indicadores_selecionados`/`indicadores_ocultos`, pra não deixar mais nenhuma referência órfã nova daqui pra frente.
|
||||
|
||||
### 107. Balancete/D.R.E. nascem recolhidos a partir do "grupo 4"
|
||||
### Rodada 107 — Balancete/D.R.E. nascem recolhidos a partir do "grupo 4"
|
||||
|
||||
Pedido: a árvore do Balancete (e, por extensão confirmada com o usuário, da D.R.E.) nascia sempre totalmente expandida, poluindo visualmente uma apuração com muitas contas analíticas. Passou a nascer recolhida a partir do nível equivalente ao "grupo 4" do código de classificação (ex. `1.01.01.001`, 4 segmentos) — essa conta aparece aberta, mas seus filhos (`1.01.01.001.001` em diante) ficam ocultos até o contador clicar pra expandir; o mesmo limiar de nível é aplicado à D.R.E., sobre `ContabilLinhaDre.nivel`.
|
||||
|
||||
Aplicado nos dois lugares que já compartilhavam o mesmo algoritmo de árvore recolhível: a tela de revisão (`dashboard-contabil.js`, `dcColapsoPadrao()` pré-popula `dcContasColapsadas`/`dcDreColapsadas` em `renderRevisao()`, em vez de nascerem como `Set()` vazio) e o relatório "Gerar Dashboard" (`_contabil_arvore_contexto()` em `views.py` ganhou o campo `colapsado_padrao` por item, calculado a partir da nova constante `_CONTABIL_NIVEL_ABERTO_PADRAO = 3`; `dashboard-contabil-relatorio.html` usa esse campo pra não marcar `is-expanded` no botão e `pidDcrArvore()` semeia o estado `colapsadas` a partir do atributo `data-dcr-colapsado-padrao` antes da primeira renderização). A impressão continua forçando toda linha a aparecer (`tr[hidden] { display: table-row !important }` em `@media print`), então o comportamento de "documento impresso nunca esconde conta atrás de um grupo recolhido" não muda. Nenhuma mudança de modelo/migração — é só o estado inicial da mesma árvore que já existia.
|
||||
|
||||
### 108. Fundo uniforme na tabela do Balancete (revisão)
|
||||
### Rodada 108 — Fundo uniforme na tabela do Balancete (revisão)
|
||||
|
||||
Pedido: a linha sintética do Balancete (grupo do plano de contas, ex. "1.02.05 IMOBILIZADO") tinha um fundo elevado (`--bg-surface-raised`) além do negrito, criando um efeito de cores alternadas entre linha de grupo e linha analítica — o usuário pediu pra unificar, no mesmo padrão já usado na D.R.E. (que só usa negrito na linha totalizadora, sem fundo diferente). Removida a regra `.dc-conta-row--sintetica td { background: var(--bg-surface-raised); }` em `dashboard-contabil.css`, mantendo só o negrito (`.dc-conta-row--sintetica { font-weight: 600; }`) — a tabela do Balancete passa a ter uma única cor de fundo, igual à D.R.E.
|
||||
|
||||
### 109. Limiar de recolhimento ajustado pra nível 3 + destaque acompanha o grupo expandido
|
||||
### Rodada 109 — Limiar de recolhimento ajustado pra nível 3 + destaque acompanha o grupo expandido
|
||||
|
||||
Pedido: (a) o limiar de recolhimento padrão da rodada 107 ("grupo 4", ex. `1.01.01.001` aberto) ficou permissivo demais — passou a recolher a partir do 3º segmento do código (ex. `1.01.01` aberto, `1.01.01.001` em diante só sob demanda). `PID_DC_NIVEL_ABERTO_PADRAO` (`dashboard-contabil.js`) e `_CONTABIL_NIVEL_ABERTO_PADRAO` (`views.py`, usada pelo relatório "Gerar Dashboard") foram de `3` pra `2` — mesma dupla de constantes já documentada na rodada 107, ajustadas juntas.
|
||||
|
||||
(b) Novo mecanismo de destaque: ao expandir uma conta/linha, só as linhas que acabaram de ficar visíveis (filhos diretos, não os netos — que continuam recolhidos pelo limiar acima) ganham um realce dourado; expandir uma delas em seguida move o realce pra seus filhos e o grupo destacado antes volta à cor padrão, nunca acumulando mais de uma "leva" ao mesmo tempo — ajuda o contador a acompanhar visualmente onde ele acabou de descer na árvore. `dcContasDestaque`/`dcDreDestaque` (`dashboard-contabil.js`, dois `Set()` resetados em `renderRevisao()`) guardam só a leva mais recente; `dcFilhosDiretos()` (função genérica, reaproveitada pelas duas árvores) calcula os filhos diretos de um item a partir da lista plana. Classe nova `.dc-conta-row--destaque` (`dashboard-contabil.css`, fundo `rgba(var(--gold-rgb), .16)`). Só na tela de revisão — o relatório "Gerar Dashboard" não ganhou esse destaque (é uma navegação estática, sem o mesmo conceito de "acabei de expandir isto agora"). Puramente visual/em memória, sem persistência nem mudança de modelo. **Nota de rodada seguinte (110)**: essa última frase não vale mais — o relatório ganhou o mesmo destaque logo depois.
|
||||
|
||||
### 110. Código interno da empresa nas telas do Dashboard Contábil
|
||||
### Rodada 110 — Código interno da empresa nas telas do Dashboard Contábil
|
||||
|
||||
Pedido explícito do usuário, com captura de tela do histórico mostrando só o nome da empresa ("GUARANI MUSICAL INSTRUMENTOS MUSICAIS LTDA - EPP") — o Portal sempre referencia empresa pelo código interno (`codigo_empresa`), mesmo padrão já usado em Importação de Plano de Saúde (`${codigo_empresa} - ${nome_empresa}`). O backend já expunha `codigo_empresa` nos dois serializers usados pela tela de revisão (`ContabilApuracaoListSerializer`/`ContabilApuracaoDetailSerializer`) — faltava só o frontend exibir. Dois pontos ajustados em `dashboard-contabil.js`: coluna "Empresa" do histórico (linha da tabela) e o cabeçalho da tela de Revisão, ambos passaram de `nome_empresa` sozinho pra `codigo_empresa - nome_empresa`. **Não alterado de propósito**: o relatório "Gerar Dashboard" (`dashboard-contabil-relatorio.html`, título/`<h1>`) e a planilha XLSX exportada continuam só com o nome — são documentos entregues ao cliente com a identidade do escritório, não telas internas do Portal, então o código de referência interno provavelmente não faz sentido lá (a confirmar com o usuário se for pedido depois).
|
||||
|
||||
### 111. Cor do destaque de expansão ajustada por tema + destaque restaura ao recolher
|
||||
### Rodada 111 — Cor do destaque de expansão ajustada por tema + destaque restaura ao recolher
|
||||
|
||||
Duas rodadas de ajuste fino sobre o destaque da rodada 109 (`.dc-conta-row--destaque`), só na tela de revisão:
|
||||
|
||||
@ -144,7 +146,7 @@ Duas rodadas de ajuste fino sobre o destaque da rodada 109 (`.dc-conta-row--dest
|
||||
|
||||
(b) Ao recolher uma leva destacada, o destaque devia voltar pra leva anterior (a que estava destacada antes de expandir), não simplesmente desaparecer. `dcContasDestaqueHistorico`/`dcDreDestaqueHistorico` (dois arrays, um por árvore) empilham o destaque atual a cada expandir e desempilham a cada recolher — mesmo espírito de undo por nível, sem tentar rastrear relação de parentesco entre expansões não relacionadas.
|
||||
|
||||
### 112. Destaque replicado no relatório "Gerar Dashboard" (revertendo a nota da rodada 109)
|
||||
### Rodada 112 — Destaque replicado no relatório "Gerar Dashboard" (revertendo a nota da rodada 109)
|
||||
|
||||
Pedido explícito do usuário: aplicar as mesmas mudanças de cor/comportamento da rodada 111 também no relatório gerado pro cliente (`dashboard-contabil-relatorio.html`), que até aqui não tinha esse destaque nenhum. Só a variante clara existe aqui (o relatório não tem alternância de tema).
|
||||
|
||||
@ -152,43 +154,43 @@ Implementado com uma custom property por linha (`--dcr-row-bg`, lida por um úni
|
||||
|
||||
Ajuste final, mesmo pedido de "ficar mais bonito" que abriu a rodada: o Balancete nascia com a tela inteira na cor de grupo/total (todo o plano de contas real é `tipo="S"` até um nível bem profundo) enquanto a D.R.E. já nascia com essa aparência "gostosa" (poucas linhas são `totalizador`). `destaque` agora nasce pré-populado com as linhas marcadas `colapsado_padrao` no servidor (o último nível já visível por padrão, ver rodada 109) — nasce com o mesmo efeito de "acabei de expandir até aqui", sem precisar de nenhum clique.
|
||||
|
||||
### 113. Ícone selecionável por indicador + flip card no relatório
|
||||
### Rodada 113 — Ícone selecionável por indicador + flip card no relatório
|
||||
|
||||
Pedido explícito do usuário, escopado por `AskUserQuestion` só pro relatório "Gerar Dashboard" (a aba "Dashboard" da revisão não ganhou flip/ícone, pra não competir com os botões de olho/lápis que já existem em cada card ali). Reverte a "perda aceita conscientemente" da rodada 104 (ícone único genérico pra todo indicador). Detalhe completo no `CLAUDE.md` desta pasta ("Ícone selecionável + flip card no relatório"): campo novo `IndicadorContabilDefinicao.icone` (migração `0063`, default `"barras"` — nenhum indicador já cadastrado muda de aparência sem edição manual), 11 ícones curados tipo KPI, seletor visual no modal "Novo/Editar Indicador", e o card do relatório virou um flip 3D (hover revela descrição + fórmula cadastradas em "Gerenciar Indicadores"). O desenho de cada ícone existe em duas cópias mantidas manualmente em sincronia (Python em `views.py`, JS em `dashboard-contabil.js`) — o relatório é HTML puro sem acesso ao JS do app, e o modal de cadastro é só JS estático sem contexto de servidor.
|
||||
|
||||
### 114. Fórmula do verso separada da fórmula de cálculo + esclarecimento sobre o relatório ser uma foto estática
|
||||
### Rodada 114 — Fórmula do verso separada da fórmula de cálculo + esclarecimento sobre o relatório ser uma foto estática
|
||||
|
||||
Dois pontos levantados pelo usuário testando a rodada 113: (a) o card de EBIT mostrava "Sem descrição cadastrada" mesmo já tendo descrição salva — não era bug, era o relatório sendo uma foto estática do momento em que foi gerado (editar o indicador depois não atualiza um relatório já aberto/baixado, precisa gerar de novo); (b) a fórmula do verso mostrava a expressão técnica de cálculo (`resultado_liquido - despesas_financeiras`, chaves internas dos componentes), inadequada pro cliente final.
|
||||
|
||||
Campo novo `IndicadorContabilDefinicao.formula_exibicao` (`CharField`, `blank=True`, migração `0064`) — texto livre, sem validação de sintaxe (não passa por `avalia_formula()`), editável no modal de indicador logo abaixo do campo técnico (que ganhou o rótulo "Fórmula (cálculo interno)", pra diferenciar do novo "Fórmula (como aparece ao cliente)"). `_contabil_monta_cards_indicadores()` resolve o fallback no servidor (`formula_exibicao` preenchida vence; em branco cai pra `formula` mesmo) — nenhum indicador já cadastrado muda de comportamento até alguém preencher essa preferência pela tela. Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 115. Ajustes de acabamento do flip card + destaque inicial também na tela de revisão
|
||||
### Rodada 115 — Ajustes de acabamento do flip card + destaque inicial também na tela de revisão
|
||||
|
||||
Três pedidos na sequência, testando as rodadas 113/114: (a) a fonte do valor na frente do card (`R$ -143.648,54`) quebrava linha — `.dcr-card__value` de `1.55rem` pra `1.3rem`; (b) o verso do flip card tinha um scroll aninhado (a descrição rolava sozinha, separada da fórmula abaixo) — removido `flex:1`/`overflow-y:auto` de `.dcr-card-face__descricao`, agora descrição e fórmula fluem juntas num único bloco contínuo (a face inteira já rolava, `.dcr-card-face--back`); (c) pedido pra levar o mesmo destaque inicial do relatório (rodada 112) também pra tela de revisão do Balancete/D.R.E, reduzindo a poluição visual de abrir a tela inteira na cor de grupo/total.
|
||||
|
||||
`renderRevisao()` (`dashboard-contabil.js`) agora inicializa `dcContasDestaque`/`dcDreDestaque` como cópia de `dcContasColapsadas`/`dcDreColapsadas` (`new Set(dcContasColapsadas)`) em vez de `new Set()` vazio — mesma lógica já usada no relatório: as linhas colapsadas por padrão são o último nível já visível de cada ramo, então já nascem destacadas sem precisar de clique nenhum. `dcContasDestaqueHistorico`/`dcDreDestaqueHistorico` continuam nascendo vazios (o destaque inicial não empilha nada — só a partir da primeira expansão manual).
|
||||
|
||||
### 116. Botão de observação virou sempre ícone + cor do destaque invertida no tema escuro
|
||||
### Rodada 116 — Botão de observação virou sempre ícone + cor do destaque invertida no tema escuro
|
||||
|
||||
Dois pedidos testando a rodada 115: (a) mostrar o texto da observação já preenchida direto na tabela também poluía a coluna (o "+ Observação" das linhas vazias já tinha virado ícone antes, mas a linha com "Cliente não envia o estoque." continuava em texto) — agora o botão é sempre o mesmo ícone, só muda de cor (`.dc-conta-observacao-btn--preenchida`, `--text-muted` vira `--accent`) conforme há observação ou não; o texto continua acessível por tooltip/clique, só sumiu da célula. (b) A cor do destaque no tema escuro estava invertida do que o usuário queria — trocado `--dc-destaque-bg`/`--dc-row-tint-bg` de lugar: destaque agora usa `--bg-canvas` (mais escuro) e o resto `--bg-surface-raised` (mais claro, mas ainda diferente do hover). O tema claro não mudou (já tinha sido invertido por pedido anterior, contrariamente ao escuro). Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 117. Bug real — folha genuína ficava de fora do destaque inicial (D.R.E.)
|
||||
### Rodada 117 — Bug real — folha genuína ficava de fora do destaque inicial (D.R.E.)
|
||||
|
||||
Usuário reportou, testando a rodada 116, que várias linhas-folha da D.R.E. (`(-) DE VENDAS DE MERCADORIAS MERCADO INTERNO`, `(-) SIMPLES NACIONAL`, `DESCONTOS OBTIDOS` e outras linhas de resultado financeiro) não ficavam com o destaque escuro esperado, mesmo sendo visualmente "de baixo" quanto um grupo colapsado vizinho no mesmo nível. Causa: o destaque inicial (rodada 112/115) usava só o conjunto de linhas colapsadas por padrão — que só marca linha com filho escondido —, deixando de fora qualquer folha genuína (sem filho nenhum), que nunca entra nesse conjunto mas é igualmente "o fim do ramo" visualmente.
|
||||
|
||||
Corrigido com um critério novo, mesmo algoritmo nas duas cópias (`dcUltimaLevaVisivel()` em `dashboard-contabil.js`, `calculaDestaqueInicial()` em `dashboard-contabil-relatorio.html`): reconstrói a lista de linhas realmente visíveis e marca destaque em toda linha cuja próxima linha visível não seja mais profunda — cobre grupo colapsado e folha genuína com a mesma regra. Validado manualmente contra os níveis reais da apuração `id=4` antes de aplicar. Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 118. Acabamento do flip card (texto centralizado, fonte) + confirmação ao fechar o modal de indicador sem salvar
|
||||
### Rodada 118 — Acabamento do flip card (texto centralizado, fonte) + confirmação ao fechar o modal de indicador sem salvar
|
||||
|
||||
Dois pedidos independentes: (a) o verso do flip card, com uma captura de tela do equivalente que o usuário já usa no Power BI como referência — texto centralizado (`text-align:center` em `.dcr-card-face--back`), fontes um pouco menores, e a fórmula ganhou "JetBrains Mono" em vez de só `"Courier New", monospace`; (b) o modal "Novo/Editar Indicador" fechava direto ao clicar fora (overlay), sem perguntar nada — `pidDcFecharIndicadorModalComConfirmacao()` (nova) pergunta "Sair sem salvar as alterações?" (`pidConfirm`) antes de fechar de verdade, ligada ao overlay e ao "Cancelar"; fechar depois de salvar/excluir com sucesso continua direto, sem pergunta. Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 119. Resumo do Fechamento — texto rico do contador antes dos indicadores
|
||||
### Rodada 119 — Resumo do Fechamento — texto rico do contador antes dos indicadores
|
||||
|
||||
Pedido explícito do usuário, com um exemplo real de carta que a De Paula já manda ao cliente hoje (considerações/saldos/variações do fechamento) como referência do que deveria caber aqui. A aba "Indicadores" do relatório "Gerar Dashboard" virou "Resumo" (mesmo nome também na aba "Dashboard" da revisão) — passa a carregar duas coisas: o texto livre do contador ("Resumo do Fechamento", no topo) e, embaixo, os grupos de cards de indicador de sempre.
|
||||
|
||||
Campo novo `ContabilApuracao.resumo_fechamento` (`TextField`, `blank=True`, migração `0065`) — texto rico (HTML sanitizado por nh3, mesma allowlist de `AcessoGeral.observacoes`/`AjudaAplicacao.texto`), preenchido via editor `contenteditable` com colar/arrastar imagem (mesmo padrão dos outros dois campos ricos do projeto, duplicado de propósito) na aba "Dashboard" da revisão. Nova `@action` `POST /api/contabil-apuracoes/{id}/resumo-fechamento/` (`ContabilApuracaoViewSet` não tem PATCH genérico, de propósito) substitui o campo inteiro de uma vez, validado por `ContabilResumoFechamentoSerializer`. No relatório, `{{ apuracao.resumo_fechamento|safe }}` — primeiro `|safe` de template Django do projeto (até agora todo texto rico só existia via `innerHTML` em tela SPA), seguro porque já vem sanitizado antes de salvar; seção some inteira quando o campo está vazio. Testado de ponta a ponta via shell, inclusive confirmando que um `<script>` embutido é removido pelo nh3. Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 120. Rebatizado pra "Relatório Contábil" + validação por conta/linha + observação virou editor inline (sem popup) + resumo por aba
|
||||
### Rodada 120 — Rebatizado pra "Relatório Contábil" + validação por conta/linha + observação virou editor inline (sem popup) + resumo por aba
|
||||
|
||||
Pedido explícito do usuário: deixar a ferramenta soar como um aliado do trabalho do contador, não mais um processo novo. Quatro mudanças na mesma rodada:
|
||||
|
||||
@ -199,23 +201,23 @@ Pedido explícito do usuário: deixar a ferramenta soar como um aliado do trabal
|
||||
|
||||
Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 121. Correções de acabamento da rodada 120 + observação também no relatório do cliente
|
||||
### Rodada 121 — Correções de acabamento da rodada 120 + observação também no relatório do cliente
|
||||
|
||||
Três pedidos testando a rodada 120: (a) o checkbox "Mostrar ao cliente" do editor inline nascia marcado mesmo numa conta sem observação nenhuma (arrastava o `oculta_no_relatorio=False` antigo de contas criadas antes do default mudar) — corrigido pra só refletir o valor salvo quando já existe observação (`conta.observacao && !conta.oculta_no_relatorio`), senão nasce sempre desmarcada; (b) a caixa de texto do editor tinha `max-width:640px`, cortando antes do fim da linha — removido, agora estica a linha inteira; (c) um bug real de renderização — a célula dos 2 botões (validado + observação) tinha `display:flex` aplicado direto no `<td>`, o que tira a célula do comportamento normal de tabela (fundo/altura não acompanhavam mais a linha, dando a impressão de "quebrar antes dos botões") — corrigido movendo o `flex` pra uma `<div>` dentro do `<td>`. De quebra, também corrigido um bug real no destaque: linhas de resultado consecutivas no nível raiz da DRE (`RESULTADO ANTES DA CS E IR` etc.) estavam todas ficando escuras só por serem vizinhas no mesmo nível — `dcUltimaLevaVisivel()`/`calculaDestaqueInicial()` agora só aplicam destaque no nível raiz quando o item de fato esconde algo (grupo colapsado), validado contra os dados reais da apuração.
|
||||
|
||||
Pedido separado na mesma rodada: o relatório "Gerar Dashboard" ganhou o mesmo ícone de observação da tela de revisão — coluna "Observação" nova no Balancete/DRE do relatório, ícone só quando há observação marcada como visível ao cliente, clique abre um painel inline abaixo da linha (`pidDcrObs()`, nova função em `dashboard-contabil-relatorio.html`). Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 122. Botão "validado" de uma conta/linha sintética virou tri-state (nenhum/parcial/completo)
|
||||
### Rodada 122 — Botão "validado" de uma conta/linha sintética virou tri-state (nenhum/parcial/completo)
|
||||
|
||||
Pedido explícito do usuário: numa conta/linha com filhos (sintética), marcar o próprio check não deveria mais só alternar True/False — passou a ter 3 estados calculados a partir dos descendentes (`dcEstadoValidacaoGrupo()`, nova): amarelo enquanto nem todos os descendentes estiverem validados (mesmo já tendo marcado a própria sintética), verde quando 100% dos descendentes estiverem validados. Ciclo de clique implementado em `dcClicarValidadoConta()`/`dcClicarValidadoLinha()` (novas): 1º clique marca só a sintética (fica amarela); clicar de novo (já amarela) pergunta "Deseja validar todas as contas deste grupo?" e, confirmado, valida tudo em lote (fica verde); clicar de novo (já verde) desmarca o grupo inteiro sem perguntar. Folhas (sem filhos) continuam com o toggle simples de antes. Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 123. Demonstração Mensal (Análise Vertical) — nova aba na revisão e no relatório
|
||||
### Rodada 123 — Demonstração Mensal (Análise Vertical) — nova aba na revisão e no relatório
|
||||
|
||||
Pedido explícito do usuário: a seção "Demonstração Mensal (Análise Vertical)" do mesmo PDF (páginas finais, histórico de 3 meses com valor+variação percentual por linha, mesma árvore da DRE) — até então ignorada de propósito pelo parser — passou a ser extraída, persistida e exibida. Escopo confirmado por `AskUserQuestion` antes de implementar: aba própria (não embutida na aba D.R.E.) e mesmos recursos por linha que Balancete/D.R.E. (observação inline, tri-state "validado", ocultar do relatório).
|
||||
|
||||
Extração validada rodando de fato contra o PDF de referência do usuário (`792 - balancete 072026.pdf`) antes de escrever o parser definitivo — mesmo cuidado de sempre (nunca desenhar regex só de texto colado). Model novo `ContabilLinhaAnaliseVertical` (mesma árvore/descrição/nível da DRE, `valores` como `JSONField` de texto — um `{valor, percentual}` por mês, alinhado por posição com `ContabilApuracao.analise_vertical_meses`) + `ContabilLinhaAnaliseVerticalViewSet` (réplica de `ContabilLinhaDreViewSet`). Nova aba na tela de revisão (`renderAnaliseVertical()`, réplica de `renderDre()` com N colunas dinâmicas de Valor/Variação) e no relatório "Gerar Dashboard" (4ª aba, entre D.R.E. e Resumo) — as duas somem por completo quando a apuração não tem essa seção (relatório antigo). Dois filtros de template novos (`moeda_av`/`percentual_av`) porque o percentual desta seção já vem "pronto" do PDF (não é uma fração como os indicadores). Testado ponta a ponta via `Client.force_login()` contra o PDF real (extração, persistência, PATCH de observação/validado, relatório gerado), sem deixar resíduo em produção. Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 124. Botão "Reprocessar" — anexar um PDF novo pra mesma empresa/competência sem perder observação/validação/achado
|
||||
### Rodada 124 — Botão "Reprocessar" — anexar um PDF novo pra mesma empresa/competência sem perder observação/validação/achado
|
||||
|
||||
Pedido explícito do usuário: até aqui, corrigir uma apuração com o arquivo errado/incompleto exigia excluir e recriar do zero, perdendo toda observação/validação/achado já tratado. Botão novo (ícone ao lado de "Abrir" na lista, só em apuração "Em revisão") abre um modal com um campo de arquivo; o PDF precisa ser da mesma empresa/competência (senão 400 — trocar de empresa é uma análise nova). Escopo confirmado com o usuário: conta/linha sem mudança mantém observação/validado como estavam; conta/linha que mudou volta pra `validado=False` e ganha um alerta visual (campo novo `alterada_reprocessamento`, migração `0068`, nos 3 models de linha); achados **nunca são apagados nem têm status/justificativa sobrescritos** (confirmado via `AskUserQuestion`), mesmo os que não disparam mais com os dados novos — mantém o histórico de tratativa completo.
|
||||
|
||||
@ -223,13 +225,13 @@ Decisão de design central: as 4 funções novas de sincronização (`_contabil_
|
||||
|
||||
Dois ajustes de UX pedidos pelo usuário depois de ver o badge funcionando: (1) marcar a conta/linha como validada de novo **não limpa mais o alerta** — ele só muda de cor (vermelho → verde), pra dar pra ver depois quais itens já foram reprocessados E revalidados; (2) o tooltip do badge agora mostra o valor de antes do reprocessamento (`valor_anterior_reprocessamento`/`valores_anterior_reprocessamento`, migração `0069`, gravado por `_contabil_sincroniza_*` sempre que a conta/linha muda, `null`/limpo quando não muda). Ambos os campos novos são `read_only` no serializer.
|
||||
|
||||
### 125. PDF de fonte atípica — título de seção sem acento + aviso ao contador
|
||||
### Rodada 125 — PDF de fonte atípica — título de seção sem acento + aviso ao contador
|
||||
|
||||
Cliente novo (`1751 - Balancete 07.2026.pdf`) deu 400 "Nenhuma linha de DRE encontrada" ao processar. Causa raiz confirmada rodando `pdfplumber` de verdade contra o arquivo: a fonte embutida nesse PDF perde o til do "Ã" ao extrair "DEMONSTRAÇÃO DO RESULTADO DO EXERCÍCIO" (sai "DEMONSTRAÇAO..."), e `parser.py` comparava esse título por igualdade exata — a seção DRE nunca era reconhecida. Corrigido com `_normaliza_titulo()` (remove acento antes de comparar), mesmo espírito de `_RE_PERIODO` já aceitar `Per[ií]odo` pra essa mesma classe de variação de fonte entre clientes/instalações do Questor.
|
||||
|
||||
O mesmo PDF também tem algumas descrições de conta com palavras coladas (ex. "BANCÁRIOSA VISTA") — investigado a fundo (medição real dos vãos entre caracteres, tentativa de usar os espaços literais do PDF como sinal), mas não há correção automática segura: o espaçamento dessa fonte é inconsistente a ponto de um vão "dentro de palavra" às vezes ser maior que um vão real "entre palavras". Cheguei a propor um botão de lápis pra edição manual da descrição, mas o usuário suspendeu essa ideia e pediu algo mais simples: `ContabilApuracao.fonte_pdf_atipica` (migração `0070`) fica `True` quando a normalização de acento foi realmente necessária pra reconhecer a seção — sinal indireto de que este PDF usa fonte diferente da de referência, calculado em `extrai_balancete_dre()`/persistido por `create()`/`reprocessar()`. Frontend mostra um ícone de aviso (cor `--gold`) ao lado do nome da empresa, na lista e no cabeçalho da revisão, avisando pra conferir os nomes de conta com atenção — puramente informativo. Validado `True` só no PDF com o problema, `False` nos dois PDFs de referência já confirmados corretos (`792`, `2017`). Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 126. Observação virou histórico por empresa+conta, atravessando competências
|
||||
### Rodada 126 — Observação virou histórico por empresa+conta, atravessando competências
|
||||
|
||||
Pedido explícito do usuário: uma observação registrada num mês (ex. um ajuste de estoque) precisava reaparecer na análise do mês seguinte, assinada por quem escreveu e com a data, bloqueada pra edição por ser registro histórico, com três caminhos pro contador (manter o histórico, que é o padrão; ocultar das próximas execuções; incluir uma observação nova) e filtro de visibilidade ao cliente em todas elas. Antes disso a observação era um campo da linha da apuração, então morria junto com a competência.
|
||||
|
||||
@ -241,17 +243,17 @@ Na tela, o editor inline virou uma thread: o histórico da conta em cima (cada i
|
||||
|
||||
Testado ponta a ponta via `Client.force_login()` dentro de uma transação com rollback (listagem por vigência, criação com chave derivada no servidor, alvo de outra apuração recusado, edição, bloqueio do texto quando a origem está concluída, visibilidade ainda alternável nesse caso, encerrar/reativar com as fronteiras de competência, herança numa competência seguinte e o relatório nos dois meses) — nada gravado em produção além da migração em si. Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 127. Botão de excluir observação removido da thread
|
||||
### Rodada 127 — Botão de excluir observação removido da thread
|
||||
|
||||
Pedido explícito do usuário, ainda na mesma funcionalidade da rodada 126: a exclusão de uma observação pela tela ficou limitada ao botão de "ocultar das próximas competências" (`encerrar`) — o botão de lixeira ao lado de cada item da thread (`dcObsItemHtml()`) e seu handler (`dcTrataCliqueObservacao()`) foram removidos, junto da função `pidExcluirObservacaoContabil()` que ficou sem consumidor. O `DELETE /api/contabil-observacoes/{id}/` do backend não foi alterado (continua sujeito à mesma regra de imutabilidade de sempre), só deixou de ser chamado pelo frontend.
|
||||
|
||||
### 128. Selo "Editada" + histórico de edições de uma observação
|
||||
### Rodada 128 — Selo "Editada" + histórico de edições de uma observação
|
||||
|
||||
Pedido explícito do usuário, complementando a rodada 127: sem a opção de excluir, uma edição de texto por cima do que já estava escrito virou uma forma indireta de "apagar" uma observação importante sem deixar rastro — "evitamos a ocultação de observações importantes". Model novo `ContabilObservacaoEdicao` (migração `0072`, FK `CASCADE` pra `ContabilObservacao`) grava `texto_anterior`/`texto_novo`/`editado_por`/`editado_em` toda vez que um `PATCH` muda o texto de fato (`ContabilObservacaoViewSet.partial_update()`, comparando antes de salvar — reenviar o mesmo texto ou só alternar `mostrar_ao_cliente`/`encerrar`/`reativar` não geram registro). `ContabilObservacaoSerializer` ganhou `editada` (booleano) e `edicoes` (histórico completo, aninhado) — só usados pela tela, o relatório HTML pro cliente nunca os expõe.
|
||||
|
||||
Na tela, cada observação editada ganha o selo "Editada" ao lado dos demais ("Histórico"/"Encerrada"/"Aparece ao cliente"/"Interna") e um botão de relógio que abre um modal listando texto anterior (riscado) → texto novo, autor e data de cada edição, mais recente primeiro — usa o `edicoes` já carregado junto da observação, sem chamada de API própria. Validado ponta a ponta com `Client.force_login()` em transação com rollback forçado (detalhe completo no `CLAUDE.md` desta pasta).
|
||||
|
||||
### 129. Motor de regras de auditoria: 2 removidas, 1 nova, 2 ajustadas
|
||||
### Rodada 129 — Motor de regras de auditoria: 2 removidas, 1 nova, 2 ajustadas
|
||||
|
||||
Pedido explícito do usuário, revisitando as 10 regras da rodada 92: removeu tolerância de centavos de `balanceamento_ativo_passivo`/`debito_credito_divergente` (agora exigem diferença exatamente zero); adicionou `lucro_balancete_diverge_dre` (alta) — resultado do exercício precisa ser o mesmo valor no Balancete (conta "2.04.13.002", código fixo calibrado contra os 2 balancetes reais já em produção) e na DRE; ajustou `conta_transitoria_com_saldo` pra não confundir "dinheiro em trânsito" (`NUMERÁRIOS EM TRANSITO`) com conta transitória de verdade; e reescreveu `variacao_atipica_dre` pra usar a seção "Demonstração Mensal (Análise Vertical)" do próprio PDF em vez do histórico de apurações anteriores do Portal — passou a rodar mesmo na 1ª apuração de uma empresa, desde que o PDF traga essa seção. `variacao_atipica_saldo` (Balancete) e `percentual_custo_receita_atipico` foram removidas: a Análise Vertical não cobre contas do Balancete, e a granularidade maior da nova `variacao_atipica_dre` cobriu em espírito a segunda. Motor passou de 10 pra 9 regras.
|
||||
|
||||
@ -259,7 +261,7 @@ Duas descobertas testando contra os 2 balancetes reais já em produção antes d
|
||||
|
||||
Também confirmado testando os 2 arquivos reais: a Análise Vertical do PDF já vem com o valor **isolado por mês** (não acumulado desde janeiro como a DRE principal) e o `percentual` é a análise vertical de verdade (% da linha sobre a Receita Bruta daquele mês, não uma variação mês a mês) — a nova regra compara esse percentual entre os 2 meses mais recentes da própria tabela. Frontend (`dashboard-contabil.js`): `PID_DC_REGRAS`/`PID_DC_GRUPOS` atualizados pras 9 chaves atuais, "Lucro Balancete x DRE" entrou no grupo "Divergências de Saldo", "Variações e Indicadores" ficou só com "Variação Atípica na DRE". Detalhe completo (incluindo por que `historico`/`_contabil_monta_historico` continuam existindo mesmo sem regra nenhuma os usando hoje) no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 130. Relatório do cliente: observações acima da tabela + clique leva até a conta
|
||||
### Rodada 130 — Relatório do cliente: observações acima da tabela + clique leva até a conta
|
||||
|
||||
Pedido explícito do usuário: no relatório "Gerar Dashboard" (o HTML enviado ao cliente), a seção "Observações do Balancete/D.R.E./Análise Vertical" vinha depois da tabela em cada aba — passou pra **antes**, nas três abas. Além disso, clicar numa observação agora rola até a conta/linha que ela referencia, "se houver" — uma observação histórica cuja conta saiu do plano de contas desta apuração continua aparecendo normalmente, só sem virar link.
|
||||
|
||||
@ -267,23 +269,23 @@ Pedido explícito do usuário: no relatório "Gerar Dashboard" (o HTML enviado a
|
||||
|
||||
**Bug reportado pelo usuário testando em produção, mesma rodada**: "clicar nas observações ainda não funciona". Validei de novo com um Chromium headless (Playwright) contra o relatório real gerado da apuração da própria empresa do print (dados reais, sem alterar nada) e o clique funcionou perfeitamente — o que apontou pra fora do código. Causa real: a mudança que faltava (cálculo da `ancora`) mora em `views.py`, e o `runserver` do usuário ainda não tinha reiniciado pra carregar essa versão — a reordenação de HTML (só template, recarregado a cada request) já aparecia, mascarando que só a parte dependente do `.py` estava desatualizada. Reiniciar o `runserver` resolveu. Lição registrada na memória do projeto pra rodadas futuras.
|
||||
|
||||
### 131. Relatório do cliente: "Limpar formatação" por tabela + "Voltar ao topo"
|
||||
### Rodada 131 — Relatório do cliente: "Limpar formatação" por tabela + "Voltar ao topo"
|
||||
|
||||
Pedido explícito do usuário, complementando a rodada 130: um botão "Limpar formatação" em cada tabela (Balancete/D.R.E./Análise Vertical, no cabeçalho da seção) desfaz qualquer expandir/recolher/destaque que o clique numa observação (ou o próprio usuário) tenha deixado, voltando pro estado inicial de quando a página carregou — sem precisar recarregar o relatório inteiro. E um botão flutuante "Voltar ao topo" (canto inferior direito, aparece só depois de rolar ~320px) rola a página de volta ao início.
|
||||
|
||||
`pidDcrArvore()` ganhou `resetar()` (devolve `colapsadas`/`destaque`/`historico` pro estado de `data-dcr-colapsado-padrao` e limpa qualquer `.dcr-row-flash` restante) e `pidDcrObs()` ganhou `fecharTudo()` (fecha todo painel de observação inline aberto) — os dois retornados junto de `expandeAte`/nada, respectivamente. O botão de cada tabela (`data-dcr-reset="dcr-{balancete,dre,av}-body"`) chama os dois de uma vez, só na própria tabela (não afeta as outras abas). "Voltar ao topo" é puramente `window.scrollY`/`scrollTo`, sem nenhuma dependência de dado. Os dois botões somem em `@media print`, mesmo padrão de "Imprimir"/"Exportar XLSX". Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 132. Relatório do cliente: exportar Resumo em PDF avulso + gráfico de evolução removido
|
||||
### Rodada 132 — Relatório do cliente: exportar Resumo em PDF avulso + gráfico de evolução removido
|
||||
|
||||
Dois pedidos explícitos do usuário na mesma rodada. (1) Removido o gráfico "Evolução do Resultado Líquido" da aba Resumo — "temos a análise vertical para esta visualização", a aba Análise Vertical já cobre esse tipo de comparação mês a mês. Saiu o `<script>` do Chart.js (CDN), o `<canvas>`, `inicializaGrafico()`, o `{{ evolucao|json_script }}` e o loop que montava essa lista em `dashboard()` (`views.py`) — `_contabil_monta_historico_completo()` continua existindo, só perdeu esse consumidor (o cálculo de Depreciação/Amortização do EBITDA continua usando).
|
||||
|
||||
(2) Novo botão "Exportar PDF" na aba Resumo (`ContabilApuracaoViewSet.resumo_pdf()`, `GET /api/contabil-apuracoes/{id}/resumo-pdf/`) gera um PDF avulso com Resumo do Fechamento (texto rico convertido em flowables do reportlab, incluindo listas e imagens embutidas) + Indicadores (uma tabela por grupo) + Observações da Análise (mesmo conteúdo/agrupamento de "Todas as Observações da Análise") — sem o resto do relatório. Identidade visual do escritório (banner roxo/dourado com `logo-branco.png`), mesmo padrão de `custo_contratacao/pdf.py`/`indicadores/recibo.py`. `_contabil_dados_resumo()` (nova função em `views.py`) foi extraída de dentro de `dashboard()` pra ser reaproveitada pelos dois caminhos (indicadores + observações visíveis). Novo módulo `portal_api/dashboard_contabil/resumo_pdf.py` — HTML→reportlab via BeautifulSoup (já era dependência transitiva do projeto, nenhuma adicionada), cobrindo só o allowlist fechado de tags que o editor do Resumo já permite (`RICHTEXT_ALLOWED_TAGS`). Validado com apuração real via `Client.force_login()` e testes unitários isolados (negrito/itálico/lista/imagem, resumo vazio, zero observações) — todos os PDFs conferidos com `pdfplumber`. Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 133. Limiar de `variacao_atipica_dre` ajustado pra 65% + severidade rebaixada pra baixa
|
||||
### Rodada 133 — Limiar de `variacao_atipica_dre` ajustado pra 65% + severidade rebaixada pra baixa
|
||||
|
||||
Pedido explícito do usuário: variação atípica na DRE (Análise Vertical, ver rodada 129) só deveria disparar acima de **65%** de variação relativa (antes 50%) e, quando disparar, entrar como prioridade **baixa** (antes média). `VARIACAO_LIMIAR_PERCENTUAL` (`regras.py`) foi de `Decimal("0.5")` pra `Decimal("0.65")`, e `regra_variacao_atipica_dre` passou a usar `SEVERIDADE_BAIXA` em vez de `SEVERIDADE_MEDIA`. `VARIACAO_AV_PONTOS_PERCENTUAIS_MINIMO` (piso de 1 ponto percentual) não mudou. Só constante+severidade — nenhuma mudança de estrutura, endpoint ou frontend (a regra continua com a mesma chave `variacao_atipica_dre`, mesmo grupo "Variações e Indicadores" em `PID_DC_GRUPOS`/`dashboard-contabil.js`, e a ordenação por severidade da aba Observações já lida com qualquer severidade automaticamente). Efeito prático: menos achados disparam (limiar mais alto) e os que disparam aparecem por último na lista ordenada por severidade (rodada 96) e contam pro terço "Baixa" do donut do resumo (rodada 98), não mais pro terço "Média".
|
||||
|
||||
### 134. Relatório do cliente: "Limpar formatação" virou ícone dentro do cabeçalho da tabela
|
||||
### Rodada 134 — Relatório do cliente: "Limpar formatação" virou ícone dentro do cabeçalho da tabela
|
||||
|
||||
Pedido explícito do usuário: o botão "Limpar formatação" (rodada 131), até então um botão com texto solto dentro do `<h2>` de cada seção (Balancete/D.R.E./Análise Vertical), passou a ser um ícone pequeno dentro da própria célula de cabeçalho "Observação" da tabela — mesmo padrão do botão "Restaurar formatação padrão" que já existe na tela de revisão (`icon-btn dc-reset-formatacao-btn` dentro de `.dc-th-linha`, `dashboard-contabil.html`/`.css`). `dashboard-contabil-relatorio.html` ganhou as classes equivalentes (`.dcr-th-linha`/`.dcr-th-reset-btn`) e o `<th>Observação</th>` das três tabelas virou `<th><span class="dcr-th-linha">Observação<button class="dcr-th-reset-btn" data-dcr-reset="...">...</button></span></th>`. `data-dcr-reset` (o atributo que o JS já usava, `document.querySelectorAll("[data-dcr-reset]")`) foi mantido, então nenhuma linha de script mudou — só HTML/CSS. `.dcr-reset-btn` (a classe antiga, com texto+borda+padding de pílula) foi removida por completo, sem consumidor restante. Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
@ -291,35 +293,35 @@ Pedido explícito do usuário: o botão "Limpar formatação" (rodada 131), até
|
||||
|
||||
**Pedido explícito do usuário, ainda na mesma rodada**: a rolagem horizontal resolvia o corte, mas o usuário preferiu que o texto diminuísse o bastante pra caber tudo sem precisar arrastar a tabela, pelo menos no caso comum (3 meses). Nova classe `dcr-tabela--compacta`, só na tabela de Análise Vertical (Balancete/D.R.E. mantidos do tamanho original — já cabiam sem aperto): padding menor (`9px 14px` → `5px 7px`), fonte menor (corpo `0.85rem` → `0.74rem`, cabeçalho `0.72rem` → `0.6rem`, `letter-spacing` reduzido) e os ícones de dentro da tabela (toggle de expandir, "Limpar formatação", observação) encolhidos proporcionalmente. Novo filtro de template `mes_curto` (`contabil_extras.py`) corta o ano do mês pra 2 dígitos (`"mai/2026"` → `"mai/26"`) só no cabeçalho dessa tabela — o texto repetido "— Valor"/"— Variação" em cada uma das colunas por mês era o maior consumidor de largura. `overflow-x: auto` do ajuste anterior continua como rede de segurança (uma apuração com mais de 3 meses ainda pode precisar rolar), mas o caso comum (3 meses) passa a caber inteiro sem rolagem nenhuma. Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 135. Bug real: observação "vazando" entre contas com a mesma classificação + ícone da conta "mãe" destacado
|
||||
### Rodada 135 — Bug real: observação "vazando" entre contas com a mesma classificação + ícone da conta "mãe" destacado
|
||||
|
||||
Usuário reportou, com print de um balancete real: 6 bancos diferentes (Banco do Brasil, Inter, Itaú, Mercado Pago, PagSeguro, Sicredi) todos sob o mesmo código de classificação "1.01.01.002.001" ("Depósitos Bancários à Vista") — uma observação escrita num deles aparecia em todos os outros. Causa raiz: `ContabilObservacao.chave_conta()` (chave natural do histórico de observações, ver rodada 126) usava só o `codigo` de classificação, que o Questor reaproveita entre várias contas analíticas de mesma natureza — não é único dentro de uma apuração. Corrigido trocando a chave pra `"codigo|descricao"` (models.py, `dashboard-contabil.js`) — mesmo espírito de `chave_linha()` (DRE/Análise Vertical, que já usa `"descricao|nivel"`). `_contabil_sincroniza_contas()` (views.py, reprocessamento) também passou a casar contas por `(codigo, descricao)` em vez de só `codigo`, mesmo trade-off que a sincronização de DRE/Análise Vertical já aceitava (conta renomeada = conta "nova", a antiga é excluída). Migração de dados `0073` recalculou o `alvo_chave` das observações já em produção (2 de conta) a partir do próprio `alvo_rotulo` (a descrição já congelada em cada registro no momento em que foi escrita) — não precisou reconstruir nada a partir da apuração de origem.
|
||||
|
||||
Segundo pedido, mesma rodada: quando uma conta/linha "filha" tem observação e o grupo está recolhido, o contador não tinha como saber sem expandir. O ícone de observação da sintética "mãe" agora ganha um destaque (`.dc-conta-observacao-btn--descendente`, cor `--gold`) sempre que algum descendente (não só filho direto) tem observação vigente e a própria sintética não tem observação própria (`dcTemObservacaoDescendente()`, nova, reaproveita `dcDescendentes()` já usada pelo tri-state do botão "validado") — nas três árvores (Balancete/D.R.E./Análise Vertical). Detalhe completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 136. Coluna "Conta" da revisão mostrava a Classificação, não o número da conta
|
||||
### Rodada 136 — Coluna "Conta" da revisão mostrava a Classificação, não o número da conta
|
||||
|
||||
Usuário comparou com o PDF original (que tem as duas colunas, "Conta" e "S Classificação") e reportou que a aba Balancete da tela de revisão só mostrava a Classificação (`codigo`) numa coluna rotulada "Conta" — o número interno da conta no Questor (`conta_numero`, já extraído e salvo desde sempre, só nunca exibido) não aparecia em lugar nenhum. Confirmado com o usuário que o pedido era só pra tela de revisão (não pro relatório "Gerar Dashboard" que vai pro cliente). `dashboard-contabil.html` ganhou uma coluna nova ("Conta", `conta_numero`) antes da já existente, renomeada pra "Classificação" (`codigo`); `renderContas()` (`dashboard-contabil.js`) passou a emitir as duas células. Ajustes de acompanhamento: colspan do editor inline de observação (7 → 8) e os seletores CSS que dependiam de posição de coluna (`.dc-contas-table td:nth-child(...)`, alinhamento numérico das colunas de valor e o estilo apagado/`nowrap` da(s) coluna(s) de identificação da conta) deslocados em uma posição.
|
||||
|
||||
### 137. Histórico de execuções ganhou ordenação e filtro por coluna, igual Importação de Plano de Saúde
|
||||
### Rodada 137 — Histórico de execuções ganhou ordenação e filtro por coluna, igual Importação de Plano de Saúde
|
||||
|
||||
Usuário pediu que a tabela de execuções (histórico de análises, `#dc-list-table`) tivesse a mesma ordenação/filtro por coluna já existente na tabela equivalente de Importação de Plano de Saúde, com a última execução aparecendo primeiro por padrão e um botão de limpar filtros. Mecanismo portado 1:1 (mesmas classes/lógica, prefixo `dc-` em vez de `ips-`): funil "estilo Excel" no cabeçalho de Empresa/Status/Criado por (busca + checklist de valores distintos), as 6 colunas ordenáveis por clique no `<th>`, ordenação padrão por `criado_em` decrescente (antes disso a ordem vinha só do `Meta.ordering` do model, que prioriza competência antes de data de criação) e botão "borracha" que limpa os filtros e volta a ordenação pro padrão de uma vez. `carregarLista()` passou a só buscar a API; a filtragem/ordenação/render virou uma função separada (`renderList()`) que roda sobre o array já carregado, sem round-trip novo a cada clique. Sem paginação, diferente da outra tela (não foi pedida, e o histórico tende a ser mais curto).
|
||||
|
||||
**Ajuste na mesma rodada, antes do usuário terminar de testar**: "Competência" também ganhou o funil de filtro (nasceu só ordenável) — indexado pela data ISO bruta, com o popup mostrando o rótulo `MM/AAAA` já usado no resto da tela. De brinde, a ordenação dos valores dentro de qualquer popup de filtro passou a usar o valor bruto em vez do rótulo formatado, porque ordenar "Competência" pelo rótulo (`localeCompare` de "MM/AAAA") agruparia por mês antes do ano; a data ISO já ordena cronologicamente certo por comparação de string. Detalhe técnico completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 138. Bug real: valores em R$ dentro do texto dos achados sem formatação brasileira
|
||||
### Rodada 138 — Bug real: valores em R$ dentro do texto dos achados sem formatação brasileira
|
||||
|
||||
Usuário reportou, com print da aba Observações da revisão, que valores monetários dentro do texto dos achados apareciam sem padronização (ex.: "R$ 643545.85" em vez de "R$ 643.545,85"), inconsistente com o padrão já usado em Balancete/DRE/relatório "Gerar Dashboard". Causa raiz: `regras.py` interpola `Decimal` direto num f-string (`f"R$ {conta.saldo_atual}"`) pra montar `AchadoDetectado.mensagem` — usa a formatação padrão do Python (sem separador de milhar, ponto decimal), nunca o padrão BR. Corrigido com um helper novo, `_moeda()` (`regras.py`), duplicado localmente em vez de importado — mesmo padrão já usado (por arquivo) em `indicadores/recibo.py`/`custo_contratacao/pdf.py`/`templatetags/contabil_extras.py`, já que este pacote é Python puro. As 9 mensagens de achado que interpolavam `R$ {valor}` direto foram todas ajustadas.
|
||||
|
||||
Só vale pra achados gerados dali em diante — um achado já persistido mantém o texto antigo até a apuração ser reprocessada ou recriada; sem migração de dados pra reformatar texto já gravado (parsear número dentro de frase livre por regex arriscaria confundir valor monetário com código de conta, ex. "1.01.01.001"). Fora do escopo desta rodada: `regra_variacao_atipica_dre` ainda formata percentual com ponto decimal (`"12.34%"`, não `"12,34%"`) — mesma família de bug, não pedida desta vez. Detalhe técnico completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 139. Bug real: cor "validado" não subia pra conta "mãe" quando só os "netos" eram marcados
|
||||
### Rodada 139 — Bug real: cor "validado" não subia pra conta "mãe" quando só os "netos" eram marcados
|
||||
|
||||
Usuário reportou, com print de uma árvore de 3 níveis (Balancete), que uma conta "mãe" (ex. "CAIXA E EQUIVALENTES DE CAIXA") não ficava verde mesmo depois de toda conta-folha dentro dela ("netos", ex. os 4 bancos dentro de "DEPÓSITOS BANCÁRIOS A VISTA") já estarem validadas uma a uma — mesmo a conta "filha" intermediária ("DEPÓSITOS BANCÁRIOS A VISTA") já aparecendo corretamente verde. Causa raiz: `dcEstadoValidacaoGrupo()` (`dashboard-contabil.js`, tri-state do botão "validado" de uma sintética, ver rodada de "Tri-state numa conta/linha sintética") decidia o estado "completo" checando `descendentes.every(d => d.validado)` sobre **todos** os descendentes (filhos, netos, bisnetos), não só os de folha — quando os netos são marcados diretamente (sem clicar na própria "filha" intermediária pra cascatear), o campo `validado` da "filha" no banco continua `False` mesmo que ela já apareça "completa" visualmente (calculada a cada render); a "mãe", ao olhar todos os descendentes, incluía essa "filha" com `validado=False` e nunca fechava o grupo.
|
||||
|
||||
Corrigido restringindo o critério de "completo" só aos descendentes **folha** (sem filhos) — `dcDescendentes()` ganhou um quarto parâmetro opcional (`somenteFolhas`), reaproveitando o mesmo cálculo de "tem filho" já usado por `temFilhos`; `dcEstadoValidacaoGrupo(item, descendentes, descendentesFolhas)` passou a checar `descendentesFolhas.every(d => d.validado)` pro estado "completo", mantendo `descendentes` (todos) pro "parcial" (uma sintética marcada sozinha, sem cascatear, ainda deve sinalizar "em andamento" num ancestral). Os 4 pontos que calculam o estado (`dcValidadoInfo()` + os 3 handlers de clique — Balancete/D.R.E./Análise Vertical) foram atualizados pra passar os dois conjuntos. Puramente lógica de exibição/cálculo no frontend — nenhuma mudança de model/endpoint.
|
||||
|
||||
### 140. Bug real: destaque de um grupo já expandido desaparecia ao expandir outro
|
||||
### Rodada 140 — Bug real: destaque de um grupo já expandido desaparecia ao expandir outro
|
||||
|
||||
Usuário reportou, com print de uma árvore de 3 níveis (Balancete), que expandir uma segunda conta apagava visualmente o destaque dourado da primeira já expandida — o esperado era as duas ficarem destacadas ao mesmo tempo, pra facilitar a validação em sequência de várias contas abertas de uma vez. Causa: o destaque das linhas recém-reveladas ao expandir (ver rodada "Destaque acompanha o último grupo expandido") era desenhado pra **substituir** a leva anterior por design — cada expandir trocava `dcContasDestaque`/`dcDreDestaque`/`dcAvDestaque` pelos filhos diretos do grupo recém-aberto, empilhando o destaque anterior numa pilha (`dc*DestaqueHistorico`) só restaurada ao recolher o mesmo grupo.
|
||||
|
||||
@ -334,7 +336,7 @@ Validado simulando o algoritmo em Python contra as contas reais da apuração `2
|
||||
|
||||
**Lição registrada na memória do projeto**: nas duas primeiras tentativas a resposta ao "não funcionou" incluiu a hipótese de cache do navegador/JS antigo em memória — o usuário deixou claro que sempre testa com hard refresh e restart do servidor antes de reportar. A causa era sempre bug real, e só apareceu quando o algoritmo foi **simulado com os dados reais** (script Python ad-hoc lendo as contas da apuração pelo ORM) em vez de analisado estaticamente.
|
||||
|
||||
### 141. Botão "+" (expandir tudo) no cabeçalho das tabelas da revisão
|
||||
### Rodada 141 — Botão "+" (expandir tudo) no cabeçalho das tabelas da revisão
|
||||
|
||||
Pedido explícito do usuário: um botão pequeno no canto esquerdo do cabeçalho da primeira coluna que expande todas as contas até o último nível de uma vez. A ideia inicial tinha também um "−" que recolheria um nível por vez, descartada pelo próprio usuário na mesma conversa ("poderia apenas um botão, o de +").
|
||||
|
||||
@ -342,7 +344,7 @@ Pedido explícito do usuário: um botão pequeno no canto esquerdo do cabeçalho
|
||||
|
||||
Detalhe não-óbvio da implementação: expandir tudo **zera** o conjunto de grupos expandidos (`dc*Expandidos`) em vez de populá-lo com todos os grupos — com ele vazio, o destaque volta a sair de `dcUltimaLevaVisivel()`, que sem nada recolhido marca exatamente as folhas (as contas analíticas finais). Conferido contra a apuração real `2021`/`08-2026`: 133 contas visíveis e 74 destacadas, exatamente as 74 contas sem filhos. Popular o conjunto com todos os grupos destacaria quase toda linha da tabela, recaindo no mesmo "tudo destacado, nada se destaca" da rodada 140. Só frontend (HTML/CSS/JS), sem mudança de model/endpoint; o relatório "Gerar Dashboard" não ganhou esse botão nesta rodada.
|
||||
|
||||
### 142. Log de reprocessamentos — quem reprocessou e quantas vezes
|
||||
### Rodada 142 — Log de reprocessamentos — quem reprocessou e quantas vezes
|
||||
|
||||
Pedido explícito do usuário: "quando o usuário reprocessar a análise, o ideal é que registre um log... pra que seja possível identificar quem reprocessou e quantos reprocessamentos já ocorreu."
|
||||
|
||||
@ -350,7 +352,7 @@ Model novo `ContabilApuracaoReprocessamento` (`reprocessado_por` FK `SET_NULL` +
|
||||
|
||||
Frontend: o modal "Reprocessar análise" ganhou uma seção (nasce escondida numa apuração nunca reprocessada) com a contagem e a lista "Nome · dd/mm/aaaa hh:mm" — sem nenhuma chamada de API própria, já que os dados vêm da lista que a tela já carregou. Novo formatador `pidDcFormatDataHora()` (os demais formatadores de data desta tela só mostram o dia, sem hora — aqui a hora importa porque mais de um reprocessamento pode acontecer no mesmo dia). Validado com um script Python isolado (transação com rollback forçado, contra uma apuração real) confirmando ordem/serialização; nada persistido em produção além da migração. Detalhe técnico completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 143. Achados voltam a ser recriados do zero a cada reprocessamento (revisão da decisão da rodada 124)
|
||||
### Rodada 143 — Achados voltam a ser recriados do zero a cada reprocessamento (revisão da decisão da rodada 124)
|
||||
|
||||
Pedido explícito do usuário: "sempre que o usuário reprocessar um documento, os apontamentos que são realizados pela própria aplicação e constam na tela de Observações devem ser refeitos. Isso já ressalta para o usuário que não há mais inconsistências para validar e pode verificar apenas se surgiram novos. As observações e comentários que ele realizou nas contas devem permanecer independente do reprocessamento." Reverte por completo a decisão da rodada 124 — até aqui, um achado (apontamento automático de auditoria) já tratado pelo contador continuava marcado como "tratado" mesmo depois de a inconsistência sumir dos dados do PDF novo, contrariando o próprio propósito da tela (sinalizar o que ainda precisa de atenção).
|
||||
|
||||
@ -358,7 +360,7 @@ Pedido explícito do usuário: "sempre que o usuário reprocessar um documento,
|
||||
|
||||
**`ContabilObservacao` (comentário/observação do contador numa conta, sistema separado desde a rodada 126) não é tocada** — nunca teve relação com achados, é casada por chave natural (empresa+conta) e sobrevive a qualquer reprocessamento, exatamente como pedido. Validado com um script Python isolado (`transaction.atomic()` com rollback forçado, contra uma apuração real já em produção): um achado marcado manualmente como "tratado" com justificativa desapareceu depois de simular `_contabil_recria_achados()` com a mesma lista de achados detectados, dando lugar a um achado novo `pendente` com o mesmo conteúdo (regra/conta/mensagem) mas `id` diferente; uma `ContabilObservacao` criada na mesma apuração permaneceu intacta. Nada persistido em produção. Detalhe técnico completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 144. Bug real: "Conta do Passivo/Ativo com saldo invertido" disparava pra conta descendente de uma redutora, não só a filha direta
|
||||
### Rodada 144 — Bug real: "Conta do Passivo/Ativo com saldo invertido" disparava pra conta descendente de uma redutora, não só a filha direta
|
||||
|
||||
Usuário reportou, com print de achados reais ("Conta do Passivo 'DANITHI LTDA'"/"'MPASARABIAHOLDINGPARTICIPAÇÕES E'" com saldo devedor): quando a conta "mãe" tem o sinal `"(-)"` na frente (ex. `"(-) CAPITALA INTEGRALIZAR"`), o saldo "invertido" dos filhos dela é o comportamento esperado da própria natureza daquela conta, não uma inconsistência — mas a regra `saldo_sinal_invertido` só excluía a conta que **em si** começava com `"(-)"`, não suas descendentes.
|
||||
|
||||
@ -366,7 +368,7 @@ Corrigido com `_indices_descendentes_de_conta_redutora()` (novo, `regras.py`)
|
||||
|
||||
Não afeta achados já persistidos: os 2 achados reais da apuração `1751` só somem da tela depois de reprocessar essa apuração (o motor de regras roda de novo do zero, ver rodada 143) — nenhuma edição direta no banco foi feita. Detalhe técnico completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 145. Botão de excluir observação volta — restrito a quem criou, só antes de concluir a análise
|
||||
### Rodada 145 — Botão de excluir observação volta — restrito a quem criou, só antes de concluir a análise
|
||||
|
||||
Pedido explícito do usuário: "inclua outro botão, transforme o atual em outro e inclua o da lixeira para excluir a observação. A exclusão só poderá ser realizada pelo usuário que criou ela e só pode ser realizada antes do usuário apertar o botão de concluir análise. Após concluir a análise, edição não deve ser permitida." Reverte parte de uma decisão anterior (o botão de excluir tinha sido tirado, deixando só "encerrar" — ocultar das próximas competências, sem apagar) — agora as duas ações convivem: encerrar continua reversível e liberado pra qualquer um do time; excluir é definitivo e só de quem escreveu a observação.
|
||||
|
||||
@ -374,7 +376,7 @@ O ícone de "Encerrar" por coincidência já era desenhado como um glifo de lixe
|
||||
|
||||
Validado via `Client.force_login()` dentro de uma transação com rollback forçado, contra dados reais em produção: usuário diferente do autor tentando excluir → 403; o próprio autor tentando excluir numa apuração já concluída → 400; o próprio autor excluindo numa apuração em revisão → 204, registro realmente removido. Nada persistido em produção. Detalhe técnico completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 146. Botão "Ver na tabela" nos cards de achado — navega até a linha referida e destaca
|
||||
### Rodada 146 — Botão "Ver na tabela" nos cards de achado — navega até a linha referida e destaca
|
||||
|
||||
Pedido explícito do usuário, a partir de um caso real de "Variação atípica na DRE": várias linhas da Análise Vertical repetem a mesma descrição em centros de custo diferentes ("MATERIAIS E SERVIÇOS APLICADOS NA OBRA" aparecia em mais de um grupo), tornando inviável localizar manualmente qual linha da tabela um achado descreve só pelo texto do card. Pedido generalizado pra "todas as observações identificadas pela aplicação quais referem-se a contas específicas", não só essa regra.
|
||||
|
||||
@ -384,7 +386,7 @@ Todo achado com `conta` (a maioria das regras) ou `linha_analise_vertical` (só
|
||||
|
||||
Validado rodando `regra_variacao_atipica_dre` sobre os dados reais de uma apuração de produção (57 achados) e conferindo que todo `ordem_linha_analise_vertical` retornado casa com uma `ContabilLinhaAnaliseVertical` de verdade via o mesmo dicionário `ordem → linha` usado pela view (transação com rollback, nada persistido). Migração `0076_contabilachado_linha_analise_vertical`. Detalhe técnico completo no `CLAUDE.md` desta pasta.
|
||||
|
||||
### 146.1. Correção na mesma rodada: o botão não aparecia em nenhum achado já existente
|
||||
### Rodada 146.1 — Correção na mesma rodada: o botão não aparecia em nenhum achado já existente
|
||||
|
||||
Usuário reportou que o botão não apareceu, mesmo reiniciando o servidor e forçando a atualização da página. Não era cache nem JS antigo: o vínculo `linha_analise_vertical` só era gravado na **criação** do achado, e todos os achados em produção foram criados antes do campo existir. Diagnóstico no banco real: das 4 apurações, 3 tinham **exclusivamente** achados de `variacao_atipica_dre` (a que estava na tela do usuário tinha 13 de 13), então nenhum card daquela tela tinha como mostrar o botão. Reprocessar devolveria o vínculo, mas reprocessar exige reanexar o PDF — inviável pedir isso por causa de um botão.
|
||||
|
||||
@ -392,7 +394,7 @@ Corrigido com uma migração de dados (`0077_backfill_achado_linha_analise_verti
|
||||
|
||||
O primeiro algoritmo casava só por descrição, avançando um ponteiro na ordem de leitura, e a conferência contra produção reprovou: **4 dos 78 achados vincularam a linha errada**, justamente nas descrições repetidas entre centros de custo (`MATERIAIS E SERVIÇOSAPLICADOS NAOBRA`, 4 ocorrências) — ou seja, exatamente o caso que motivou o pedido do usuário teria ficado errado em silêncio. A regra dispara na ocorrência cujos percentuais estouram o limiar, que não é necessariamente a primeira. Com a assinatura completa: 78 de 78 casados, e a conferência final (comparar os percentuais da linha vinculada com os citados no texto de cada achado) deu 0 divergências. Migração já aplicada em produção; os 13 achados da apuração do print passaram a trazer o botão.
|
||||
|
||||
### 147. Excluir análise bloqueado depois de concluída
|
||||
### Rodada 147 — Excluir análise bloqueado depois de concluída
|
||||
|
||||
Pedido explícito do usuário (com print do histórico, numa linha "Concluída"): "quando estiver Concluída, a exclusão deve ficar bloqueada." Fecha a última ação destrutiva que ainda escapava da trava de "concluiu, não se mexe mais" — reprocessar, editar observação e tratar achado já eram bloqueados, mas excluir a análise inteira continuava liberado, o que é justamente o mais destrutivo dos três.
|
||||
|
||||
@ -402,7 +404,7 @@ Validado via `Client.force_login()` contra dados reais: DELETE numa apuração c
|
||||
|
||||
**Incidente durante essa validação** (registrado de propósito, ver também a rodada 148 abaixo): o teste rodou dentro de `transaction.atomic()` com rollback forçado, o padrão usado o tempo todo aqui, mas `perform_destroy()` chama `instance.arquivo.delete(save=False)` — e **apagar arquivo do storage não é revertido por rollback de transação**. O registro da apuração 26 (PURE TECH ENERGY) voltou pelo rollback, o PDF anexado dela não. Nenhum dado analítico se perdeu (contas, DRE, Análise Vertical, os 57 achados, observações e validações estavam todos no banco) e nenhuma funcionalidade depende desse arquivo — `arquivo` não é exposto em nenhum serializer e `reprocessar()` sempre grava um upload novo, nunca lê o antigo —, mas o anexo original precisa ser reposto reanexando o mesmo PDF pelo botão "Reprocessar". Lição pra qualquer teste futuro de exclusão nesta base: rollback só protege o banco; efeito colateral em disco exige apuração descartável ou storage isolado.
|
||||
|
||||
### 148. Tabelas abrem com tudo expandido; o "+" virou "−"
|
||||
### Rodada 148 — Tabelas abrem com tudo expandido; o "+" virou "−"
|
||||
|
||||
Pedido explícito do usuário: "por padrão, trazer as tabelas com a expansão de todas as contas até o último nível. O comportamento do botão de + deve virar um − e recolher os níveis para que retorne no atual padrão que abre a tabela. O botão de reverter deve reverter ao novo padrão (todas expandidas)." Inverte o padrão de abertura que valia desde as primeiras rodadas nas 3 tabelas da revisão (Balancete, DRE e Análise Vertical).
|
||||
|
||||
@ -415,3 +417,15 @@ O destaque das linhas não precisou de ajuste nenhum: `dcUltimaLevaVisivel()` co
|
||||
**O relatório "Gerar Dashboard" não foi tocado** — continua nascendo recolhido (`colapsado_padrao` calculado server-side em `_contabil_arvore_contexto()`). É o documento que vai pro cliente, tem só o botão de restaurar (nunca teve "+"/"−") e mudar o que o cliente vê não foi pedido; virou o único lugar onde `dcColapsoPadrao`/`_CONTABIL_NIVEL_ABERTO_PADRAO` ainda é padrão de abertura. Se o usuário quiser o mesmo lá, é um ajuste separado.
|
||||
|
||||
Mudança só em `.js`/`.html`/`.css` — não exige reiniciar o `runserver`.
|
||||
|
||||
### Rodada 149 — `CLAUDE.md` reescrito como estado atual, sem cronologia
|
||||
|
||||
Rodada de revisão da documentação do Portal, não de código. Nenhuma mudança em `.py`/`.js`/`.html`/`.css`.
|
||||
|
||||
O `CLAUDE.md` desta pasta tinha virado um segundo changelog: 65% dele (129 KB de ~196 KB) eram seções datadas por rodada ("Análise Vertical (rodada 123)", "Reprocessar (rodada 124)", "Ver na tabela (rodada 146)"), mais cinco ocorrências de "(rodada seguinte)" que só faziam sentido lendo o arquivo na ordem. Somando com este changelog (110 KB), eram ~300 KB para uma aplicação, com a parte que responde "como isso funciona hoje" soterrada no primeiro terço.
|
||||
|
||||
- Reescrito como **documento de estado atual**, organizado por subsistema (extração → regras → models → criação → reprocessamento → observações → indicadores → API → frontend da revisão → relatório do cliente → riscos), sem nenhuma referência a rodada e sem narrativas "antes era X, agora é Y". Onde o motivo de uma decisão importa para não revertê-la por engano (por que a chave da observação inclui a descrição, por que o destaque descarta a leva padrão, por que achado é recriado e conta não, por que o relatório é GET), o motivo ficou — como motivo, não como cronologia.
|
||||
- **Nenhum fato técnico foi descartado**: 196 KB → 79 KB é consolidação de repetição (a mesma mecânica de colapso/destaque era recontada em quatro rodadas diferentes) mais a remoção do histórico, que já estava aqui.
|
||||
- **Ganhou uma tabela de API** com os 22 endpoints da ferramenta, que não existia em lugar nenhum: a tabela do `CLAUDE.md` da raiz tinha (e agora tem de propósito) zero linha `contabil-*`.
|
||||
- Uma seção nova de "Riscos conhecidos e armadilhas" reúne o que antes estava espalhado: códigos de classificação fixos, as duas cópias mantidas à mão (ícones SVG, lógica de árvore), o N+1 de `total_achados_pendentes` e o fato de que testar exclusão nesta ViewSet destrói o PDF de verdade.
|
||||
- O título deste changelog passou de "Dashboard Contábil" para "Relatório Contábil" (o rename de rótulo aconteceu na rodada 120 e nunca tinha chegado aqui), e as 58 entradas passaram de `### N. Título` para `### Rodada N — Título`, o formato usado pelos outros 11 changelogs do projeto.
|
||||
|
||||
@ -1,558 +1,475 @@
|
||||
# Relatório Contábil (Relatórios > Contabilidade)
|
||||
|
||||
> Movido do `CLAUDE.md` da raiz — este arquivo é carregado automaticamente ao trabalhar dentro de `portal_api/dashboard_contabil/`. 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/dashboard_contabil/`. Ver `CLAUDE.md` na raiz para arquitetura geral/transversal do Portal (modelo de permissões, API, CSS, etc.).
|
||||
>
|
||||
> **Este arquivo descreve o estado atual, não o histórico.** Nenhuma seção aqui é datada por rodada e nenhuma narra "antes era X, agora é Y" — quando o motivo de uma decisão importa para não a reverter por engano, ele aparece como motivo, não como cronologia. O histórico rodada a rodada (92 a 148) está em `CHANGELOG.md` nesta mesma pasta. Ao implementar algo novo aqui, atualizar **os dois**: o estado atual neste arquivo, a mudança no changelog.
|
||||
|
||||
**Renomeado de "Dashboard Contábil" pra "Relatório Contábil"** numa rodada específica — pedido explícito do usuário, pra soar como um aliado do trabalho do contador em vez de mais um processo/sistema novo. Rename **só de rótulo visível**: menu (`catalogo.py`), `<title>`/`<h1>`/cabeçalhos de `dashboard-contabil.html` e `dashboard-contabil-relatorio.html`, o botão que gera o relatório ("Gerar Relatório", antes "Gerar Dashboard") e `verbose_name`/`verbose_name_plural` dos models no admin. **Nada técnico mudou**: a pasta continua `dashboard_contabil/`, os arquivos continuam `dashboard-contabil.*`, as classes continuam `Contabil*`/`IndicadorContabil*`, as rotas continuam `/api/contabil-*`/`/api/contabil-apuracoes/{id}/dashboard/`, a permissão continua `apps["dashboard-contabil"]`. Não seguir esse rename pra dentro do código — só onde o texto é literalmente visível ao usuário (ou documentação, como este arquivo).
|
||||
**Nome**: "Relatório Contábil" é o rótulo visível ao usuário; internamente tudo continua `dashboard_contabil`/`dashboard-contabil.*`/`Contabil*`/`/api/contabil-*`/`apps["dashboard-contabil"]`. O rename foi só de texto visível (menu em `catalogo.py`, `<title>`/`<h1>`/cabeçalhos dos dois templates, o botão "Gerar Relatório" e os `verbose_name` do admin) — pedido explícito do usuário, para a ferramenta soar como um aliado do trabalho do contador em vez de mais um sistema. **Não propagar esse rename para dentro do código.**
|
||||
|
||||
Ferramenta que otimiza a conferência de balancetes hoje feita manualmente pelo Fisco/Contábil (processo descrito em `ITD-FISCO-7513`, "Roteiro de Conferência de Balancete"): o contador anexa o PDF de Balancete + DRE de uma empresa (mesmo relatório modelo Questor hoje enviado ao cliente), a ferramenta extrai as contas/linhas e roda um conjunto de checagens automáticas de auditoria, apresentando os achados numa tela de revisão onde o contador analisa, registra observações e conclui a análise. Permissão de **toggle único** (`apps["dashboard-contabil"]` em `permissoes["relatorios"]`, subgrupo "Contabilidade"), checada via `PermissaoApp("relatorios", "dashboard-contabil")` em todos os `ModelViewSet` relacionados. **Nasce restrita só ao perfil "Inovação"** (código 8) — mesmo padrão de "Não Conformidades" (ver override em `seed_portal.py`), já que expõe balancete/DRE completos dos clientes.
|
||||
Ferramenta que otimiza a conferência de balancetes hoje feita manualmente pelo Fisco/Contábil (processo descrito em `ITD-FISCO-7513`, "Roteiro de Conferência de Balancete"): o contador anexa o PDF de Balancete + DRE de uma empresa (mesmo relatório modelo Questor já enviado ao cliente), a ferramenta extrai as contas/linhas, roda um motor de regras de auditoria e apresenta os apontamentos numa tela de revisão, onde o contador analisa, registra observações e conclui a análise. No fim, gera um relatório HTML autocontido para o cliente.
|
||||
|
||||
O botão "Gerar Dashboard" (relatório final em HTML para o administrador da empresa) já está implementado — ver "Relatório 'Gerar Dashboard'" abaixo, que inclui exportação de Balancete/DRE em XLSX a partir dele (`exportacao.py`, ver "Exportação em XLSX"). **Ainda fora de escopo**: consolidação entre várias empresas/competências ao mesmo tempo (o BI Contábil externo que este Dashboard substitui tem filtros "Ano-Mês"/"Empresa-Filial" que sugerem isso, mas o escopo confirmado com o usuário é sempre uma apuração por vez — ver seção própria).
|
||||
**Permissão**: toggle único `apps["dashboard-contabil"]` em `permissoes["relatorios"]` (subgrupo "Contabilidade"), checado via `PermissaoApp("relatorios", "dashboard-contabil")` em todos os ViewSets. **Nasce restrita ao perfil "Inovação"** (override em `seed_portal.py`), já que expõe balancete/DRE completos dos clientes.
|
||||
|
||||
## Decisões de escopo (confirmadas com o usuário)
|
||||
|
||||
- **Entrada: só PDF.** O Questor também exporta Balancete/DRE em XLSX estruturado (mais confiável de extrair, sem o risco de parsing de texto), levantado como alternativa — o usuário optou por manter só PDF, como pedido originalmente. Não há suporte a XLSX nesta ferramenta.
|
||||
- **Histórico para variação mês a mês fica no próprio Portal**, não depende do relatório "Análise Comparativa Mensal"/"Acompanhamento Mensal" (que é só um relatório complementar, derivado do balancete, enviado ao cliente separadamente — não é upload desta ferramenta). Cada apuração processada fica salva (`ContabilApuracao`, chave natural `codigo_empresa`+`competencia`), e as regras de variação comparam contra as apurações anteriores da mesma empresa já no banco.
|
||||
- **Escopo das regras**: só o que é derivável do próprio balancete/DRE anexado — nenhuma checagem do ITD que dependa de sistemas externos (Questor, extratos bancários, folha de pagamento, PID legado). A ferramenta é analítica ("Auditoria de Balancetes" em Auditorias > Fisco/Contábil, hoje só um placeholder `href="#"` no menu, referencia o antigo sistema PID legado — **não confundir com este Dashboard Contábil**, são coisas diferentes), não substitui as etapas operacionais do roteiro (zeramento de saldos etc.).
|
||||
- **Entrada: só PDF.** O Questor também exporta Balancete/DRE em XLSX estruturado (mais confiável de extrair), levantado como alternativa e recusado pelo usuário. Não há suporte a XLSX na entrada.
|
||||
- **Sempre uma apuração por vez.** Sem consolidação entre empresas ou competências — isso seria um BI à parte. O BI Contábil externo que esta ferramenta substitui tem filtros "Ano-Mês"/"Empresa-Filial" que sugerem o contrário; o escopo confirmado é uma apuração por vez.
|
||||
- **O histórico para comparação mês a mês fica no próprio Portal.** Cada apuração processada fica salva (`ContabilApuracao`, chave natural `codigo_empresa`+`competencia`), e é contra ela que se compara. Não depende do relatório "Análise Comparativa Mensal"/"Acompanhamento Mensal", que é um relatório complementar enviado ao cliente por fora, não uma entrada desta ferramenta.
|
||||
- **As regras só usam o que é derivável do próprio Balancete/DRE anexado.** Nenhuma checagem do ITD que dependa de sistema externo (Questor, extratos bancários, folha, PID legado). A ferramenta é analítica, não substitui as etapas operacionais do roteiro (zeramento de saldos etc.).
|
||||
- **"Auditoria de Balancetes" (Auditorias > Fisco/Contábil) é outra coisa** — placeholder `href="#"` no menu que referencia o antigo sistema PID legado. Não confundir com esta ferramenta.
|
||||
|
||||
## Extração do PDF (`parser.py`)
|
||||
|
||||
O relatório Questor de Balancete + DRE tem uma particularidade de renderização que quebra a extração padrão do pdfplumber: **cada caractere do texto real é desenhado em posição própria** (sem kerning) e, por baixo dele, **o PDF inclui uma grade densa de caracteres de espaço cobrindo toda a largura de cada linha** — um artefato do gerador de relatório, não intencional para leitura. Isso faz `page.extract_words()`/`page.extract_text()` do pdfplumber tratarem esses espaços "de fundo" como separadores de palavra reais, quebrando números em dígitos isolados (ex.: "34.245.469,57" vira uma sequência de tokens `'3'`, `'4'`, `'.'`, `'2'`...).
|
||||
O relatório Questor tem uma particularidade de renderização que quebra a extração padrão do pdfplumber: **cada caractere é desenhado em posição própria** (sem kerning) e, por baixo, **o PDF inclui uma grade densa de caracteres de espaço cobrindo toda a largura de cada linha** — artefato do gerador, não intencional. Isso faz `page.extract_words()`/`extract_text()` tratarem esses espaços de fundo como separadores reais, quebrando números em dígitos isolados ("34.245.469,57" vira `'3'`, `'4'`, `'.'`, `'2'`...).
|
||||
|
||||
`_reconstroi_linhas()` contorna isso trabalhando direto com `page.chars`: ignora todo caractere de texto igual a `" "` e reconstrói cada linha a partir da posição real (`x0`/`x1`) dos caracteres não-espaço, reinserindo um único espaço só quando o vão horizontal entre dois caracteres consecutivos ultrapassa `_GAP_ESPACO` (0.8pt) — calibrado contra `792 - balancete 072026.pdf` (arquivo de referência do usuário, salvo fora do repositório em `Projetos\Balancetes`): o vão dentro de uma palavra/número é ~0, entre duas palavras da mesma descrição é ~1.7-1.9pt, e entre campos da tabela (conta → flag S/A → código → descrição, ou entre colunas de valor) é sempre ≥5pt. Essa reconstrução foi validada rodando de fato contra o PDF real antes de escrever o parser definitivo (nunca desenhar regex só de texto colado — mesmo cuidado documentado na skill `importacao-plano-saude`).
|
||||
`_reconstroi_linhas()` contorna trabalhando direto com `page.chars`: ignora todo caractere igual a `" "` e reconstrói cada linha pela posição real (`x0`/`x1`) dos não-espaços, reinserindo um único espaço só quando o vão horizontal entre dois caracteres consecutivos passa de `_GAP_ESPACO` (0.8pt). Calibrado contra `792 - balancete 072026.pdf` (arquivo de referência, fora do repositório, em `Projetos\Balancetes`): o vão dentro de uma palavra/número é ~0, entre palavras da mesma descrição ~1.7-1.9pt, entre campos da tabela sempre ≥5pt. Validado rodando contra o PDF real antes de escrever o parser definitivo — **nunca desenhar regex só a partir de texto colado**, ver `[[feedback_pdf_parser_precisa_arquivo_real]]` na memória.
|
||||
|
||||
- **Balancete**: cada linha casa com `_RE_LINHA_BALANCETE` (`^(conta)\s+(S)?\s*(código)\s+(resto)$`), e os últimos 4 tokens monetários de `resto` (via `_RE_MONETARIO`) são Saldo Anterior/Débito/Crédito/Saldo Atual, nessa ordem — o texto antes deles é a descrição. `tipo` é `"S"` (sintética) quando o flag aparece, `"A"` (analítica) quando não.
|
||||
- **DRE**: cada linha é descrição + um único valor final (sem código de classificação, diferente do Balancete). `nivel` (indentação) é derivado do `x0` do primeiro caractere da linha, em relação ao menor `x0` visto na seção (a raiz, nível 0); `totalizador` é `True` quando algum caractere da linha usa fonte em negrito (`fontname` contendo `"bold"`, case-insensitive) — confirmado contra o PDF real: linhas como "RECEITA OPERACIONAL BRUTA"/"(-) CUSTOS TOTAIS"/"(=) LUCRO BRUTO" usam `Times-Bold`, as demais `Times-Roman`.
|
||||
- **Demonstração Mensal (Análise Vertical)** (páginas finais do mesmo PDF, quando presentes) — **passou a ser extraída** (rodada 123; antes o parser parava aí de propósito, já que o histórico próprio do Portal cobre variação mês a mês de forma mais confiável — decisão revisitada numa rodada seguinte, ver `regra_variacao_atipica_dre` abaixo). É a mesma árvore da DRE (mesma descrição/ordem/negrito), só que cada linha repete N pares "Valor Variação" (um por mês mostrado, ex. "mai - 2026 jun - 2026 jul - 2026" — normalmente os 3 meses até a competência do PDF) em vez de um valor único; **cada valor já vem isolado por mês** (não acumulado desde janeiro como a DRE principal — confirmado comparando os dois: `linhas_dre` de julho é o YTD jan-jul, `linhas_analise_vertical` de julho é só o mês de julho). `percentual` é a análise vertical de verdade (percentual da linha sobre a Receita Operacional Bruta *daquele mês*, não uma variação percentual mês a mês). `_RE_PAR_VALOR_VARIACAO` casa um par de cada vez (`(valor, percentual)`, na ordem em que aparecem na linha); `_RE_MES_ANALISE_VERTICAL` captura o cabeçalho de mês uma única vez (primeira página da seção — as páginas seguintes repetem o mesmo cabeçalho, ignorado depois da primeira captura). O nível de indentação usa o mesmo divisor `/7.0` da DRE, calibrado contra o mesmo PDF de referência. Ver "Análise Vertical" mais abaixo pra models/views/frontend/relatório.
|
||||
- `extrai_balancete_dre(origem)` aceita tanto um caminho em disco quanto um arquivo já aberto em memória (`io.BytesIO`) — a view chama isto **antes** de salvar qualquer coisa no banco, já que `codigo_empresa`/`competencia` (a chave natural da apuração) só são conhecidos depois de ler o PDF, não informados pelo usuário no upload (diferente de `IndicadorApuracao`, que recebe a competência como campo do formulário).
|
||||
`extrai_balancete_dre(origem)` aceita caminho em disco **ou** arquivo já aberto em memória (`io.BytesIO`) — a view chama isto **antes** de salvar qualquer coisa, já que `codigo_empresa`/`competencia` só são conhecidos depois de ler o PDF.
|
||||
|
||||
## Regras de auditoria v1 (`regras.py`)
|
||||
### As três seções
|
||||
|
||||
Cada `regra_*` é uma função pura: `(ResultadoExtracao da apuração atual, list[SnapshotHistorico]) -> list[AchadoDetectado]`. `historico` (só as últimas 2 apurações já persistidas da mesma empresa, resolvidas por `_contabil_monta_historico()` em `views.py`) continua na assinatura de toda regra por uniformidade (`gera_achados()` chama todas do mesmo jeito) — mas **nenhuma das 9 regras atuais usa esse parâmetro** desde que `variacao_atipica_dre` migrou pra Análise Vertical (rodada seguinte, ver abaixo); as duas regras que dependiam de `historico` (`variacao_atipica_saldo`/`percentual_custo_receita_atipico`) foram removidas na mesma rodada. `_contabil_monta_historico`/o parâmetro `busca_historico` de `pipeline.processa_apuracao()` continuam existindo de propósito (ponto de extensão já desenhado — ver "`ContabilApuracaoViewSet.create()`" abaixo), não foram arrancados só porque nada os usa hoje.
|
||||
- **Balancete**: cada linha casa com `_RE_LINHA_BALANCETE` (`^(conta)\s+(S)?\s*(código)\s+(resto)$`); os últimos 4 tokens monetários de `resto` (via `_RE_MONETARIO`) são Saldo Anterior/Débito/Crédito/Saldo Atual, nessa ordem, e o texto antes deles é a descrição. `tipo` é `"S"` (sintética) quando o flag aparece, `"A"` (analítica) quando não.
|
||||
- **DRE**: descrição + um único valor final, sem código de classificação. `nivel` (indentação) vem do `x0` do primeiro caractere em relação ao menor `x0` da seção (raiz, nível 0), com divisor `/7.0`. `totalizador` é `True` quando algum caractere da linha usa fonte negrito (`fontname` contendo `"bold"`) — confirmado no PDF real: "RECEITA OPERACIONAL BRUTA"/"(-) CUSTOS TOTAIS"/"(=) LUCRO BRUTO" usam `Times-Bold`, as demais `Times-Roman`.
|
||||
- **Demonstração Mensal (Análise Vertical)** (páginas finais, quando presentes): mesma árvore da DRE (mesma descrição/ordem/negrito), mas cada linha repete N pares "Valor Variação", um por mês mostrado (normalmente os 3 meses até a competência do PDF). **Cada valor já vem isolado por mês**, não acumulado desde janeiro como a DRE principal — confirmado comparando os dois: `linhas_dre` de julho é o YTD jan-jul, `linhas_analise_vertical` de julho é só julho. `percentual` é a análise vertical de verdade (percentual da linha sobre a Receita Operacional Bruta **daquele mês**), não uma variação mês a mês. `_RE_PAR_VALOR_VARIACAO` casa um par por vez; `_RE_MES_ANALISE_VERTICAL` captura o cabeçalho de mês uma única vez (as páginas seguintes repetem o mesmo cabeçalho, ignorado depois da primeira captura). Mesmo divisor `/7.0` da DRE para o nível.
|
||||
|
||||
1. **`balanceamento_ativo_passivo`** (alta) — soma do grupo Ativo (`codigo="1"`) deve fechar **exatamente** com a do Passivo (`codigo="2"`, já vem negativo no relatório) — diferença precisa ser zero, sem tolerância de centavos (removida numa rodada seguinte, pedido explícito do usuário).
|
||||
2. **`debito_credito_divergente`** (alta) — soma de Débito das contas-raiz (`codigo` sem ponto, ou seja só "1" e "2") deve bater **exatamente** com a soma de Crédito (mesma remoção de tolerância). **Não é uma checagem trivial de "todo balancete sempre bate"**: como a DRE (Resultado) não tem colunas de débito/crédito próprias neste relatório (só um valor líquido por linha), a identidade só fecha porque a movimentação de Resultado também transita pelas contas de Patrimônio Líquido do Passivo (ex.: "LUCROS/PREJUÍZOS DO EXERCÍCIO") — confirmado empiricamente contra `792 - balancete 072026.pdf` (débito total = crédito total = R$ 416.271.243,32 nas contas-raiz).
|
||||
### PDF de fonte atípica (`fonte_pdf_atipica`)
|
||||
|
||||
Nem todo cliente/instalação do Questor embute a mesma fonte. Num PDF real (`1751 - Balancete 07.2026.pdf`, TAROBA) a fonte perde o til do "Ã" na extração: "DEMONSTRAÇÃO DO RESULTADO DO EXERCÍCIO" sai "DEMONSTRAÇAO..." (só falta o til, o resto do caractere sai certo — não é um replacement character). Como o parser comparava o título por igualdade exata, a seção da DRE nunca era detectada e a criação da apuração devolvia 400 "Nenhuma linha de DRE encontrada no PDF".
|
||||
|
||||
`_normaliza_titulo()` compara o título **sem acento** (`unicodedata.normalize("NFKD", ...)` + remoção de acento), com `_TITULO_DRE_NORM`/`_TITULO_ANALISE_VERTICAL_NORM` calculados uma vez no import. Mesmo espírito de `_RE_PERIODO` já aceitar `Per[ií]odo` para essa mesma classe de variação.
|
||||
|
||||
**Problema relacionado que não tem correção segura**: o mesmo tipo de fonte também gruda palavras em descrições de conta ("DEPÓSITOS BANCÁRIOS A VISTA" → "...BANCÁRIOSA VISTA"). Investigado de verdade, não teoricamente: (1) medindo a distribuição de vãos nesse PDF, o vão *dentro* de uma palavra (entre "U" e "I" de "EQUIVALENTES") chega a ser **maior** que um vão real entre duas palavras curtas, então nenhum limiar único separa os dois casos; (2) usar os caracteres de espaço literais do próprio PDF como sinal não funciona, porque a posição vertical deles às vezes arredonda para uma linha diferente da do texto da mesma linha visual; (3) baixar `_GAP_ESPACO` para `0.3` conserta alguns casos e quebra outros que hoje saem certos (`EQUIVALENTES` vira `EQU IVALENTES`). **Decisão explícita do usuário: não arriscar.** Valores monetários nunca são afetados, só a descrição de algumas contas, então o custo de regressão supera o benefício.
|
||||
|
||||
A solução adotada é **avisar, não corrigir**: `ContabilApuracao.fonte_pdf_atipica` é `True` quando `_normaliza_titulo()` precisou de verdade (o título bateu sem acento mas não bateria com acento) para reconhecer a seção DRE ou Análise Vertical — sinal indireto mas real de que o PDF usa uma fonte diferente da de referência. `ResultadoExtracao.fonte_pdf_atipica` (`modelos.py`) carrega o valor; `create()`/`reprocessar()` persistem. Exposto em leitura nos dois serializers, sem rota de escrita. O frontend mostra um badge com tooltip ao lado do nome da empresa pedindo atenção redobrada aos nomes de conta; não bloqueia nem oculta nada.
|
||||
|
||||
Validado contra os 3 PDFs reais disponíveis: `True` só para o `1751`, `False` para `792` e `2017` (sem falso positivo nos já confirmados corretos).
|
||||
|
||||
## Motor de regras de auditoria (`regras.py`)
|
||||
|
||||
Cada `regra_*` é uma função pura: `(ResultadoExtracao da apuração atual, list[SnapshotHistorico]) -> list[AchadoDetectado]`.
|
||||
|
||||
**`historico` continua na assinatura de toda regra por uniformidade** (`gera_achados()` chama todas do mesmo jeito), mas **nenhuma das 9 regras atuais usa esse parâmetro**. `_contabil_monta_historico()` (em `views.py`, limitado às 2 apurações anteriores) e o parâmetro `busca_historico` de `pipeline.processa_apuracao()` continuam existindo de propósito, como ponto de extensão já desenhado — não foram arrancados só porque nada os usa hoje.
|
||||
|
||||
### As 9 regras
|
||||
|
||||
1. **`balanceamento_ativo_passivo`** (alta) — soma do grupo Ativo (`codigo="1"`) deve fechar **exatamente** com a do Passivo (`codigo="2"`, que já vem negativo no relatório). Sem tolerância de centavos.
|
||||
2. **`debito_credito_divergente`** (alta) — soma de Débito das contas-raiz (`codigo` sem ponto, ou seja só "1" e "2") deve bater **exatamente** com a soma de Crédito. **Não é uma checagem trivial**: como a DRE não tem colunas de débito/crédito próprias neste relatório (só um valor líquido por linha), a identidade só fecha porque a movimentação de Resultado transita pelas contas de Patrimônio Líquido do Passivo ("LUCROS/PREJUÍZOS DO EXERCÍCIO") — confirmado empiricamente contra o PDF de referência (débito total = crédito total = R$ 416.271.243,32 nas contas-raiz).
|
||||
3. **`saldo_negativo_caixa`** (alta) — conta com `codigo` começando em `1.01.01.001` (grupo Caixa) e `saldo_atual < 0`.
|
||||
4. **`saldo_sinal_invertido`** (média) — conta analítica (`tipo="A"`) do Ativo (`1.`) com saldo credor, ou do Passivo (`2.`) com saldo devedor, exceto contas redutoras (descrição começando com `"(-)"`, que são esperadas ter o sinal oposto ao grupo) **e exceto toda conta descendente de uma conta redutora** (`_indices_descendentes_de_conta_redutora()`, rodada 144 — bug real, ver abaixo): se a conta "mãe" (sintética, em qualquer nível acima, não só o pai direto) começa com `"(-)"`, o sinal "invertido" dos analíticos dentro dela é o comportamento esperado da própria natureza daquela conta, não uma inconsistência.
|
||||
5. **`lucro_balancete_diverge_dre`** (alta, rodada seguinte) — o resultado do exercício (lucro **ou** prejuízo) precisa ser o mesmo valor no Balancete e na DRE, sem tolerância. Lê `CODIGO_LUCRO_PREJUIZO_EXERCICIO` (`"2.04.13.002"`, código de classificação fixo pra linha sintética "LUCROS/PREJUÍZOS DO EXERCÍCIO" dentro do Patrimônio Líquido — calibrado contra os 2 balancetes reais já em produção, mesmo padrão de risco de `CODIGO_CAIXA`/`indicadores.CODIGO_*`; agrega "LUCROS DO EXERCÍCIO" ou "(-) PREJUÍZOS DO EXERCÍCIO" conforme o resultado do mês), negado (mesma convenção Passivo/PL com sinal invertido de `balanceamento_ativo_passivo`) contra `linhas_dre[-1].valor` (última linha da DRE — mesma fonte que `_ContabilDadosIndicadores.resultado_liquido` em `views.py` já usa pros indicadores). Validado batendo exato contra os 2 balancetes reais antes de entrar em produção.
|
||||
6. **`conta_transitoria_com_saldo`** (média) — descrição contém "TRANSIT" com `saldo_atual != 0`, **exceto** a palavra isolada "TRANSITO" (`\bTRANSITO\b`, "dinheiro em trânsito" — conceito diferente de conta transitória/de compensação, excluído numa rodada seguinte). A exclusão é por palavra isolada, não um trecho maior como "TRANSITOR": testando contra `1751 - Balancete 07.2026.pdf` antes de decidir, a fonte embutida corrompe o acento de "TRANSITÓRIA" num caractere ilegível (não recuperável) na extração — "TRANSITOR" nunca bateria com essa conta (que tem saldo real), então a mudança pra um trecho positivo mais longo foi descartada a favor de manter "TRANSIT" e só excluir o falso positivo conhecido.
|
||||
7. **`conta_deveria_zerar`** (média) — `TRECHOS_CONTA_DEVERIA_ZERAR` (lista curta e deliberadamente restrita — só "ADIANTAMENTOS DE SALÁRIOS", que o ITD confirma dever ficar zerada todo mês; **não** inclui "Adiantamento de Férias"/"13º Salário", que legitimamente carregam saldo entre meses).
|
||||
8. **`descricao_generica`** (baixa) — descrição exatamente `"DIVERSOS"` com saldo relevante (o ITD cita esse caso especificamente: "o contador deverá realocar estes lançamentos a conta pertinente").
|
||||
9. **`variacao_atipica_dre`** (baixa — era média até uma rodada seguinte, ver abaixo; **reescrita numa rodada anterior** — pedido explícito do usuário, "utilizar a análise vertical") — não depende mais de `historico`/apurações anteriores do Portal. Usa a própria seção "Demonstração Mensal (Análise Vertical)" do PDF (`atual.linhas_analise_vertical`, quando presente): compara só os **2 meses mais recentes** dessa tabela (ex. jun → jul), pelo `percentual` (já isolado por mês, não acumulado) de cada linha sobre a Receita Operacional Bruta. Dispara quando o salto entre os 2 meses é de pelo menos `VARIACAO_AV_PONTOS_PERCENTUAIS_MINIMO` (1 ponto percentual — piso pra não disparar em saltos %-mente grandes só porque a base de comparação já era perto de zero) **e**, quando o percentual anterior não é zero, `VARIACAO_LIMIAR_PERCENTUAL` (**65%** de variação relativa sobre ele — era 50%, ajustado a pedido explícito do usuário numa rodada seguinte, junto da troca de severidade pra baixa: "variações acima de 65%... como prioridade baixa"). Roda mesmo na 1ª apuração de uma empresa nova, desde que o PDF traga essa seção — validado contra os 2 balancetes reais (16 e 15 achados respectivamente; ambos com bastante movimento real entre os 2 meses do próprio PDF) no limiar original de 50%, antes do ajuste pra 65%.
|
||||
4. **`saldo_sinal_invertido`** (média) — conta analítica (`tipo="A"`) do Ativo com saldo credor, ou do Passivo com saldo devedor. Duas exceções: conta redutora (descrição começando com `"(-)"`, que é esperado ter o sinal oposto ao grupo) e **toda conta descendente de uma redutora** — ver abaixo.
|
||||
5. **`lucro_balancete_diverge_dre`** (alta) — o resultado do exercício (lucro **ou** prejuízo) precisa ser o mesmo no Balancete e na DRE, sem tolerância. Lê `CODIGO_LUCRO_PREJUIZO_EXERCICIO` (`"2.04.13.002"`, código fixo da linha sintética "LUCROS/PREJUÍZOS DO EXERCÍCIO" dentro do PL; agrega "LUCROS DO EXERCÍCIO" ou "(-) PREJUÍZOS DO EXERCÍCIO" conforme o resultado), negado (convenção de Passivo/PL com sinal invertido) contra `linhas_dre[-1].valor`. Validado batendo exato contra os 2 balancetes reais antes de entrar em produção.
|
||||
6. **`conta_transitoria_com_saldo`** (média) — descrição contém "TRANSIT" com `saldo_atual != 0`, **exceto** a palavra isolada "TRANSITO" (`\bTRANSITO\b`, "dinheiro em trânsito", conceito diferente). A exclusão é por palavra isolada e não por um trecho positivo mais longo como "TRANSITOR" porque, no PDF `1751`, a fonte corrompe o acento de "TRANSITÓRIA" num caractere não recuperável — "TRANSITOR" nunca casaria com essa conta, que tem saldo real.
|
||||
7. **`conta_deveria_zerar`** (média) — `TRECHOS_CONTA_DEVERIA_ZERAR`, lista curta e deliberadamente restrita: só "ADIANTAMENTOS DE SALÁRIOS", que o ITD confirma dever ficar zerada todo mês. **Não** inclui "Adiantamento de Férias"/"13º Salário", que legitimamente carregam saldo entre meses.
|
||||
8. **`descricao_generica`** (baixa) — descrição exatamente `"DIVERSOS"` com saldo relevante (o ITD cita esse caso: "o contador deverá realocar estes lançamentos a conta pertinente").
|
||||
9. **`variacao_atipica_dre`** (baixa) — usa a seção "Demonstração Mensal (Análise Vertical)" do próprio PDF (`atual.linhas_analise_vertical`), não o histórico do Portal. Compara os **2 meses mais recentes** dessa tabela pelo `percentual` de cada linha sobre a Receita Operacional Bruta. Dispara quando o salto entre os 2 meses é de pelo menos `VARIACAO_AV_PONTOS_PERCENTUAIS_MINIMO` (**1 ponto percentual** — piso para não disparar em saltos percentualmente grandes só porque a base já era perto de zero) **e**, quando o percentual anterior não é zero, `VARIACAO_LIMIAR_PERCENTUAL` (**65%** de variação relativa). Roda mesmo na 1ª apuração de uma empresa, desde que o PDF traga a seção.
|
||||
|
||||
**Duas regras removidas na mesma rodada** (pedido explícito do usuário, "utilizar a análise vertical" no lugar delas): `variacao_atipica_saldo` (variação de saldo de conta do Balancete contra a apuração anterior) — a Análise Vertical do PDF só cobre linhas da DRE, não contas do Balancete, então não tinha como reaproveitar a mesma fonte pra ela, e a alternativa de mantê-la como estava (usando `historico`) foi descartada a favor de simplificar o motor; `percentual_custo_receita_atipico` (razão Custos/Receita Líquida do mês contra o mês anterior, via `historico`) — a granularidade maior de `variacao_atipica_dre` sobre a Análise Vertical já cobre esse caso (e qualquer outra linha da DRE) sem precisar de uma regra dedicada.
|
||||
**Por que não existe uma regra de variação sobre o Balancete**: a Análise Vertical do PDF só cobre linhas da DRE. A alternativa (comparar saldo de conta contra a apuração anterior, via `historico`) existiu e foi removida a pedido do usuário, junto de uma regra de razão Custos/Receita — a granularidade de `variacao_atipica_dre` sobre a Análise Vertical já cobre qualquer linha da DRE sem precisar de regra dedicada.
|
||||
|
||||
**Bug real, rodada 144 — `saldo_sinal_invertido` disparava pra conta descendente de uma redutora, não só a filha direta**: usuário reportou, com print de achados reais ("Conta do Passivo 'DANITHI LTDA'"/"'MPASARABIAHOLDINGPARTICIPAÇÕES E'" com saldo devedor, ambas `2.04.01.003.001`), que o filtro de conta redutora (`_eh_conta_redutora()`, checa se a própria descrição começa com `"(-)"`) só excluía a conta que **em si** tem o prefixo — não bastava a "mãe" (sintética, em qualquer nível acima, não só o pai direto) ter o sinal `"(-)"`: as duas contas do print são analíticas dentro de `2.04.01.003` `"(-) CAPITALA INTEGRALIZAR"` (sic — espaço grudado, mesma classe de artefato de extração já documentada em "PDF de fonte atípica" acima; confirmado contra a apuração real `1751`/TAROBA em produção), então herdam o sinal devedor esperado da conta-mãe, mas não tinham `"(-)"` na própria descrição — geravam achado indevido.
|
||||
### Descendente de conta redutora não dispara `saldo_sinal_invertido`
|
||||
|
||||
Corrigido com `_indices_descendentes_de_conta_redutora(contas)` (novo, `regras.py`) — calcula, pra toda a árvore de uma vez, quais índices têm **algum** ancestral redutora, usando o mesmo algoritmo de pilha de níveis já usado em todo o resto da aplicação pra árvore de contas (`codigo.count(".")` + ordem de leitura do PDF, mesmo espírito de `dcUltimaLevaVisivel()`/`_contabil_arvore_contexto()`) — **não** comparação de prefixo de código: o Questor reaproveita o mesmo código de classificação entre contas analíticas irmãs (mesmo problema já documentado em `ContabilObservacao.chave_conta()`), então string matching por código não seria confiável pra achar o pai; a pilha de níveis segue a ordem/profundidade real da árvore impressa no PDF, funciona mesmo com códigos repetidos entre irmãos. `regra_saldo_sinal_invertido()` passou a pular toda conta cujo índice está nesse conjunto, além da checagem já existente na própria conta.
|
||||
Se a conta "mãe" (sintética, em **qualquer** nível acima, não só o pai direto) começa com `"(-)"`, o sinal "invertido" dos analíticos dentro dela é o comportamento esperado daquela natureza de conta, não uma inconsistência. Caso real: duas analíticas dentro de `2.04.01.003` `"(-) CAPITALA INTEGRALIZAR"` (sic, espaço grudado — a mesma classe de artefato descrita em "PDF de fonte atípica") herdam o sinal devedor esperado, mas não têm `"(-)"` na própria descrição.
|
||||
|
||||
Validado de duas formas: (1) árvore sintética reproduzindo exatamente a estrutura de um exemplo do usuário (conta "(-) LUCROS DISTRIBUÍDOS" com 2 sócios dentro) — confirmado que o achado deixa de ser gerado com a correção, e que **seria** gerado sem ela (não um teste vazio por acidente); (2) rodado contra as **3 apurações reais já em produção** — a apuração `1751`/TAROBA (a mesma do print do usuário) tem exatamente os 2 achados dos sócios suprimidos, e as 3 apurações somadas têm 32 contas identificadas como "descendente de redutora" (a maioria contas de depreciação acumulada, `1.02.05.007.*`, mesmo padrão "(-) DEPREC. ..."), sem nenhum falso positivo óbvio nos nomes. **Não afeta achados já persistidos** — os 2 achados reais da apuração `1751` (`id=20`/`21`, ainda `pendente` no banco) só somem da tela depois que essa apuração for reprocessada (`_contabil_recria_achados()`, rodada 143, roda o motor de regras de novo do zero); não foi feita nenhuma edição direta no banco pra removê-los manualmente.
|
||||
`_indices_descendentes_de_conta_redutora(contas)` calcula, para a árvore inteira de uma vez, quais índices têm **algum** ancestral redutora, usando a pilha de níveis (`codigo.count(".")` + ordem de leitura do PDF) — **não** comparação de prefixo de código. O motivo é importante: o Questor reaproveita o mesmo código de classificação entre contas analíticas irmãs (ver também `ContabilObservacao.chave_conta()` abaixo), então string matching por código não é confiável para achar o pai; a pilha de níveis segue a profundidade real da árvore impressa, e funciona mesmo com códigos repetidos entre irmãos.
|
||||
|
||||
Validado de duas formas: árvore sintética reproduzindo o caso do usuário (conta "(-) LUCROS DISTRIBUÍDOS" com 2 sócios dentro), confirmando que o achado deixa de ser gerado **e que seria gerado sem a correção**; e rodando contra as 3 apurações reais em produção, com 32 contas identificadas como descendente de redutora (a maioria depreciação acumulada, `1.02.05.007.*`), sem falso positivo aparente.
|
||||
|
||||
### Formatação de valores nas mensagens
|
||||
|
||||
`_moeda(valor: Decimal) -> str` (local em `regras.py`, duplicado em vez de importado — este pacote é Python puro, sem tocar no ORM/app registry; mesmo padrão por arquivo de `indicadores/recibo.py`/`custo_contratacao/pdf.py`/`templatetags/contabil_extras.py`). Todas as mensagens de achado que interpolam um valor usam `_moeda()`, nunca `f"R$ {valor}"` direto (que usa `str(Decimal(...))` e nunca tem separador de milhar).
|
||||
|
||||
**Pendência conhecida**: `regra_variacao_atipica_dre` ainda formata percentual com `f"{valor:.2f}%"` (ponto decimal, "12.34%"), inconsistente com o padrão BR. Mesmo tipo de ajuste, ainda não pedido.
|
||||
|
||||
**Mensagem de achado é congelada no momento em que o achado é criado** — um achado já persistido mantém o texto com que nasceu até a apuração ser reprocessada (o que recria todo achado do zero) ou até uma apuração nova da mesma empresa ser criada. Não existe migração de dados reformatando texto já gravado: parsear números dentro de frase livre por regex é arriscado, já que o mesmo texto tem números que não são valores (código de conta "1.01.01.001").
|
||||
|
||||
## Models (`portal_api/models.py`)
|
||||
|
||||
Padrão cabeçalho → linhas de detalhe → achados (mesma filosofia de `IndicadorApuracao`/`IndicadorApuracaoColaborador`):
|
||||
Padrão cabeçalho → linhas de detalhe → apontamentos, mesma filosofia de `IndicadorApuracao`/`IndicadorApuracaoColaborador`.
|
||||
|
||||
- **`ContabilApuracao`**: `codigo_empresa`/`nome_empresa`/`cnpj`/`competencia` (extraídos do PDF, não informados no upload) + `periodo_inicio`/`periodo_fim` + `arquivo` + `status` (`revisao`/`concluida`). `unique_together` em `codigo_empresa`+`competencia` — reprocessar a mesma competência de uma empresa exige excluir a apuração antiga primeiro (sem "reabrir"/reprocessar nesta v1, diferente de `ImportacaoPlanoSaude`).
|
||||
- **`ContabilConta`**: uma linha do Balancete. `observacao` (`TextField`, editável via PATCH em **qualquer** conta, tenha ela gerado achado ou não) — é o espaço de "análise" pedido pelo usuário, independente da auditoria automática. `oculta_no_relatorio` (default `True`, invertido numa rodada — ver "Editor de observação inline" abaixo) e `validado` (`BooleanField`, default `False` — checkbox informativo de "já conferi esta conta", sem efeito em achado/observação/relatório).
|
||||
- **`ContabilLinhaDre`**: uma linha da DRE, sem código de classificação (o relatório não traz um pra DRE, diferente do Balancete). Mesmos `oculta_no_relatorio`/`validado` de `ContabilConta`.
|
||||
- **`ContabilLinhaAnaliseVertical`** (rodada 123): uma linha da Demonstração Mensal (Análise Vertical) — mesma árvore/descrição/nível da DRE, mas `valores` (`JSONField`) guarda um `{"valor": "...", "percentual": "..."}` por mês em vez de um `DecimalField` único (gravado como texto, não float, pra não perder precisão), alinhado por posição com `ContabilApuracao.analise_vertical_meses` (ex.: `["mai/2026", "jun/2026", "jul/2026"]`, lista compartilhada por toda a apuração, não por linha). Mesmos `observacao`/`oculta_no_relatorio`/`validado` de `ContabilConta`/`ContabilLinhaDre` — recursos por linha idênticos (editor inline, tri-state, ocultar do relatório), decisão confirmada com o usuário via `AskUserQuestion` antes de implementar. Lista vazia (`analise_vertical_meses=[]`, nenhuma linha) quando o PDF não tinha essa seção — relatório antigo, ou empresa sem essa seção habilitada no Questor; a aba/tab correspondente some nesse caso (ver "Análise Vertical" abaixo).
|
||||
- **`ContabilAchado`**: achado de auditoria, nasce automático em `create()`, só muda de `status` (`pendente`/`tratado`/`ignorado`) via `ContabilAchadoViewSet`, sempre com `observacao_contador` obrigatória ao mudar de pendente. **Exceção**: um reprocessamento apaga e recria **todos** os achados da apuração do zero (`_contabil_recria_achados()`, rodada 143 — decisão revisada, ver "Reprocessar" abaixo), então "nunca é apagado" só vale fora desse fluxo. `conta` é nullable — achados 1 e 2 (balanceamento/débito-crédito) são gerais, sem uma conta específica.
|
||||
### Apuração e linhas
|
||||
|
||||
## `ContabilApuracaoViewSet.create()` — ordem de operações não-trivial
|
||||
- **`ContabilApuracao`** — `codigo_empresa`/`nome_empresa`/`cnpj`/`competencia` (extraídos do PDF, não informados no upload) + `periodo_inicio`/`periodo_fim` + `arquivo` + `status` (`revisao`/`concluida`). `unique_together` em `codigo_empresa`+`competencia`. Mais: `analise_vertical_meses` (`JSONField`, ex. `["mai/2026", "jun/2026", "jul/2026"]`, lista compartilhada por toda a apuração), `fonte_pdf_atipica`, `resumo_fechamento` (texto rico do contador), `indicadores_ocultos` e `indicadores_selecionados` (`JSONField`, listas de chave de indicador).
|
||||
- **`ContabilConta`** — uma linha do Balancete. `conta_numero` (numeração interna do Questor) e `codigo` (classificação) são coisas diferentes, as duas extraídas. `validado` (`BooleanField`, checkbox informativo de "já conferi", sem gate em nada), `alterada_reprocessamento` + `valor_anterior_reprocessamento`.
|
||||
- **`ContabilLinhaDre`** — uma linha da DRE, sem código de classificação. Mesmos `validado`/`alterada_reprocessamento`/`valor_anterior_reprocessamento`.
|
||||
- **`ContabilLinhaAnaliseVertical`** — mesma árvore/descrição/nível da DRE, mas `valores` (`JSONField`) guarda um `{"valor": "...", "percentual": "..."}` **por mês**, gravado como **texto, não float**, para não perder precisão; alinhado por posição com `ContabilApuracao.analise_vertical_meses`. `valores_anterior_reprocessamento` tem o mesmo formato (não existe um valor único aqui). Lista vazia quando o PDF não traz a seção — relatório antigo ou empresa sem essa seção habilitada no Questor; a aba correspondente some nesse caso.
|
||||
|
||||
Diferente de `IndicadorApuracaoViewSet`/`ImportacaoPlanoSaudeViewSet` (onde a chave natural do registro, ex. `competencia`, vem do formulário do usuário), aqui `codigo_empresa`/`competencia` só são conhecidos **depois** de extrair o PDF. A ordem em `create()`:
|
||||
As três têm os mesmos recursos por linha: observação (via `ContabilObservacao`), tri-state de "validado" e ocultar do relatório.
|
||||
|
||||
1. Lê o arquivo inteiro pra memória (`arquivo.read()`) — nada em disco ainda.
|
||||
2. `dashboard_contabil_pipeline.processa_apuracao(io.BytesIO(conteudo), _contabil_monta_historico)` — extrai o cabeçalho/contas/DRE e, com o `codigo_empresa`/`competencia` já em mãos, chama `_contabil_monta_historico()` (função injetada, consulta o ORM) pra buscar até 2 apurações anteriores da mesma empresa, então roda as regras. Captura `ContabilExtracaoInvalidaError` → 400 genérico.
|
||||
3. Confere se já existe uma apuração pra essa empresa+competência (`.exists()`) → 400 com mensagem específica, **antes** de qualquer escrita (evita depender só do `IntegrityError` do banco, que devolveria um 500 cru).
|
||||
4. Só agora, dentro de `transaction.atomic()`, cria `ContabilApuracao` (grava o arquivo via `ContentFile(conteudo, ...)`) + `bulk_create` de contas/linhas DRE/achados. `except Exception` fora do `with` apaga o arquivo gravado se algo falhar no meio (upload de `FileField` não é transacional).
|
||||
### Apontamentos de auditoria
|
||||
|
||||
`pipeline.processa_apuracao(origem, busca_historico)` recebe `busca_historico` como uma função (não uma lista já pronta) exatamente por essa dependência: a chave de busca do histórico só existe depois da extração, então não dá pra pré-buscar antes de chamar o pipeline como as outras duas ferramentas fazem.
|
||||
- **`ContabilAchado`** — nasce automático em `create()`, muda de `status` (`pendente`/`tratado`/`ignorado`) via `ContabilAchadoViewSet`, sempre com `observacao_contador` obrigatória ao sair de pendente. **Um reprocessamento apaga e recria todos os achados do zero** (ver "Reprocessar" abaixo). Dois campos de alvo, mutuamente exclusivos e os dois opcionais: `conta` (FK para `ContabilConta`) e `linha_analise_vertical` (FK para `ContabilLinhaAnaliseVertical`). As duas regras gerais (balanceamento e débito/crédito) não têm alvo nenhum.
|
||||
|
||||
## Frontend
|
||||
> **"Achado" nunca aparece em texto visível ao usuário** — pedido explícito. Na UI a aba se chama "Observações" e os textos falam em "observação"/"apontamento". `achado`/`ContabilAchado`/`achados_com_observacao` continuam normais como nome de model/variável/classe CSS. Ver `[[feedback_nunca_achado_em_texto_visivel]]` na memória.
|
||||
|
||||
`templates/dashboard-contabil.html` (`page-content--wide`) segue o padrão de 3 sub-views de `indicador-desempenho.html`: `#dc-list-view` (histórico + botão "Nova Análise") / `#dc-form-view` (upload de um único PDF — sem campo de competência, é extraído do arquivo) / `#dc-review-view` (abas Achados/Balancete/DRE, via `.pa-tabs`/`.pa-tab-panel` de `perfis-acesso.css`). Achados têm filtro por severidade e por status (pendentes/todos); tratar/ignorar um achado abre um modal próprio (`#dc-achado-modal`) que exige observação não-vazia. Observação de conta/linha da DRE **não** abre mais modal nenhum — editor inline na própria tabela, ver "Editor de observação inline" abaixo. Botão "Gerar Relatório" (`#dc-gerar-dashboard-btn`, id não mudou) chama `pidGerarDashboardContabil()` — ver "Relatório 'Gerar Dashboard'" abaixo.
|
||||
### Observações (`ContabilObservacao`)
|
||||
|
||||
**Balancete e DRE usam a mesma árvore recolhível** (`dashboard-contabil.js`): o Balancete já construía uma árvore expansível a partir do nível de indentação derivado do código de classificação (`dcContaNivel()`, contando segmentos separados por `.`) — a DRE não tem código de classificação (ver "Extração do PDF" acima), mas já carregava `nivel` pronto do backend (`ContabilLinhaDre.nivel`, derivado do `x0` de cada linha no PDF), então `renderDre()` reaproveita exatamente o mesmo algoritmo de `renderContas()` (pilha de níveis recolhidos, "tem filhos" = a próxima linha tem nível maior) só que sobre `linha.nivel` direto, sem precisar de um `dcContaNivel` equivalente. Reaproveita as mesmas classes CSS do toggle (`.dc-conta-toggle`/`.dc-conta-toggle-spacer`/`.dc-conta-desc-cell`, `dashboard-contabil.css`) — o nome genérico ("conta") já cobre as duas árvores, não precisou de classe nova. Estado de colapso é independente por aba (`dcContasColapsadas`/`dcDreColapsadas`, dois `Set()`).
|
||||
Uma observação **não** é campo da linha: é um registro próprio, escopado por `codigo_empresa` + `alvo_tipo` (`conta`/`dre`/`analise_vertical`) + `alvo_chave`, que atravessa competências. Ver a seção "Observações" abaixo para vigência/imutabilidade. Campos: `alvo_rotulo` (descrição congelada no momento em que foi escrita, para exibir se a conta sumir do plano), `apuracao_origem` (`SET_NULL`) + `competencia_origem` (cópia, para a vigência continuar resolvendo se a apuração for excluída), `texto`, `mostrar_ao_cliente`, `criado_por`/`criado_em` e o trio `encerrada_em_competencia`/`encerrada_por`/`encerrada_em`.
|
||||
|
||||
**Nasce com a árvore inteira expandida** (rodada 148, pedido explícito do usuário — inverte o padrão descrito no parágrafo seguinte, que valia até então): `renderRevisao()` reinicia os três `Set()` de colapso vazios, e o estado compacto de `dcColapsoPadrao()` virou o botão "−" do cabeçalho, aplicado sob demanda (ver "Cabeçalho: dois botões" abaixo).
|
||||
- **`ContabilObservacaoEdicao`** — um registro por `PATCH` que muda `texto` de fato. Nunca por `mostrar_ao_cliente`/`encerrar`/`reativar`, e nunca quando o texto enviado é igual ao já salvo.
|
||||
- **`ContabilApuracaoReprocessamento`** — um registro por reprocessamento bem-sucedido (`reprocessado_por` `SET_NULL`, `reprocessado_em` `auto_now_add`). Criado **dentro** da transação do reprocessamento, então uma tentativa que falha no meio não deixa registro órfão.
|
||||
|
||||
**Estado compacto (`dcColapsoPadrao`), hoje só sob demanda pelo botão "−"** (era o padrão de abertura até a rodada 148, e continua sendo o padrão do relatório "Gerar Dashboard", que é server-side e não mudou): `dcColapsoPadrao(itens, nivelFn)` popula o `Set` com os ids de todo item que tem filhos **e** está no nível `PID_DC_NIVEL_ABERTO_PADRAO` (`2`) ou além — ex. a conta `1.01.01` (3 segmentos, nível 2) aparece aberta, mas seus filhos (`1.01.01.001`, nível 3) ficam ocultos até o contador clicar pra expandir; se expandido, um filho de nível 3 que também tenha netos nasce recolhido de novo pelo mesmo critério, então "nível 3 em diante" precisa sempre de um clique a mais, não só a primeira camada. Mesmo limiar aplicado à DRE, sobre `Math.max(0, linha.nivel)` — decisão confirmada com o usuário (a princípio o pedido citava só o Balancete, mas como a DRE reaproveita o mesmo algoritmo, o mesmo comportamento faz sentido nela também). O relatório "Gerar Dashboard" continua **nascendo** nesse estado (ver abaixo) — desde a rodada 148 é o único lugar onde ele é o padrão de abertura. `PID_DC_NIVEL_ABERTO_PADRAO` (JS) e `_CONTABIL_NIVEL_ABERTO_PADRAO` (`views.py`) são a mesma constante conceitual duplicada nos dois lados (um é SPA, o outro HTML renderizado uma vez) — mudar o limiar exige ajustar os dois.
|
||||
### Indicadores
|
||||
|
||||
**Destaque acompanha todo grupo já expandido** (pedido explícito do usuário, mesma rodada do ajuste de limiar acima, com o mecanismo revisado na rodada 140 abaixo): ao expandir uma conta/linha, as linhas que acabaram de ficar visíveis (só os filhos **diretos**, não os netos — que continuam recolhidos pelo limiar acima) ganham um realce dourado (`.dc-conta-row--destaque`), e **mais nada** fica destacado enquanto houver grupo aberto. Dois `Set()` por árvore (os 6 resetados em `renderRevisao()` e no botão "Restaurar formatação"): `dcContasExpandidos`/`dcDreExpandidos`/`dcAvExpandidos` guardam os ids dos grupos que o contador expandiu manualmente e que continuam expandidos, e `dcContasDestaque`/`dcDreDestaque`/`dcAvDestaque` guardam o resultado **derivado** (as linhas de fato destacadas), sempre recalculado por `dcDestaqueGrupos(itens, nivelFn, colapsadas, expandidos)`:
|
||||
- **`IndicadorContabilDefinicao`** — `chave` (`SlugField` único, sempre **derivada do `nome`** na criação, nunca aceita do cliente; imutável depois, já que pode estar referenciada em `indicadores_ocultos` ou na fórmula de outro indicador), `nome`, `descricao`, `formula` (a expressão de cálculo), `formula_exibicao` (texto **livre**, sem validação nenhuma, só para exibição ao cliente), `formato` (`moeda`/`percentual`/`indice`), `icone` (`choices`, 11 opções, default `"barras"`), `grupo` (livre, só agrupa visualmente), `padrao` (`BooleanField`, default `True`), `criado_por`/`criado_em`/`atualizado_em`.
|
||||
- **`IndicadorContabilComponente`** (`related_name="componentes"`) — uma peça da fórmula. `chave` é um identificador Python válido (`^[a-z][a-z0-9_]*$`, `RegexValidator`) e **não** um `SlugField` comum, porque vira nome de variável dentro da árvore `ast` do avaliador (diferente da `chave` da `Definicao`, que pode ter hífen). `tipo` é `contas`/`linha_dre`/`variacao_conta`/`indicador`/`resultado_liquido`, e só um campo de referência é preenchido conforme o tipo: `contas_codigos`, `linhas_dre_descricoes`, `indicador_referenciado`.
|
||||
|
||||
- `expandidos` vazio (nada aberto ainda) → vale a leva padrão, `dcUltimaLevaVisivel(itens, nivelFn, colapsadas)`, mesma coisa do estado inicial de uma apuração recém-aberta.
|
||||
- `expandidos` com algo → destaque é **só** a união de `dcFilhosDiretos(itens, nivelFn, id)` de cada grupo aberto (função genérica reaproveitada pelas 3 árvores: varre a lista plana a partir do índice do item e para no primeiro item de nível igual ou menor, coletando só os de nível exatamente `pai+1`), descartando a leva padrão por completo.
|
||||
**Guardado por código/descrição, nunca por FK a uma linha de uma apuração específica** — a definição é genérica, reaplicada em qualquer apuração/empresa. Funciona quando a empresa usa o mesmo plano de contas e **sai errado silenciosamente se não** (mesmo risco já aceito pelos códigos fixos das regras).
|
||||
|
||||
O handler de clique do toggle guarda `estavaColapsada = colapsadas.has(id)` **antes** de mutar o `Set` de colapso, e então: expandir adiciona o `id` a `expandidos`; recolher remove o `id` **e todo descendente dele** (`dcDescendentes()`) de `expandidos` — um grupo aninhado que estava aberto dentro do que acabou de fechar deixa de contar pro destaque, senão o destaque apontaria pra linhas agora invisíveis e a árvore nunca voltaria ao estado padrão. Puramente visual, sem persistência — reseta a cada apuração aberta, sem afetar nenhum dado gravado.
|
||||
## Criação de uma apuração (`ContabilApuracaoViewSet.create()`)
|
||||
|
||||
**Bug real, rodada 140 — destaque de um grupo sumia ao expandir outro, e as duas primeiras correções não resolveram**: na versão original, expandir SUBSTITUÍA o `Set` de destaque pelos filhos diretos do grupo recém-aberto (empilhando o anterior numa pilha, restaurada ao recolher) — usuário reportou, com print de uma árvore de 3 níveis, que expandir uma segunda conta "apagava" o destaque da primeira, quando o esperado era as duas conviverem pra facilitar a validação em sequência. Duas tentativas falhas antes da correção final, ambas reportadas pelo usuário testando em produção (sempre com hard refresh + restart do servidor, então nunca foi cache):
|
||||
Diferente de `IndicadorApuracaoViewSet`/`ImportacaoPlanoSaudeViewSet` (onde a chave natural vem do formulário), aqui `codigo_empresa`/`competencia` só são conhecidos **depois** de extrair o PDF. A ordem importa:
|
||||
|
||||
1. **Somar os filhos revelados ao `Set` já existente** (mantendo o que estava): não mudou nada visualmente. Causa, confirmada simulando o algoritmo em Python contra as contas reais da apuração do usuário: o destaque inicial (`dcUltimaLevaVisivel`) já marca **praticamente toda conta de nível ≥ 2** — todas nascem recolhidas pelo limiar padrão, então cada uma é "fim de ramo visível" por si só. Somar os filhos revelados a essa base deixava a tabela inteira destacada, e com tudo destacado nada se destaca. O comportamento original só parecia funcionar porque o "substituir" limpava todo o resto da tabela.
|
||||
2. **Destacar a própria conta expandida** em vez dos filhos: o usuário esclareceu que destacar os filhos estava certo desde o começo — a única mudança pedida era acumular mais de um grupo.
|
||||
1. Lê o arquivo inteiro para memória (`arquivo.read()`) — nada em disco ainda.
|
||||
2. `dashboard_contabil_pipeline.processa_apuracao(io.BytesIO(conteudo), _contabil_monta_historico)` — extrai cabeçalho/contas/DRE/Análise Vertical e, com a empresa/competência em mãos, chama `_contabil_monta_historico()` (função injetada, consulta o ORM) e roda as regras. `ContabilExtracaoInvalidaError` vira 400.
|
||||
3. Confere se já existe apuração para essa empresa+competência (`.exists()`) → 400 com mensagem específica, **antes** de qualquer escrita (evita depender do `IntegrityError` cru, que devolveria 500).
|
||||
4. Só então, dentro de `transaction.atomic()`, cria a `ContabilApuracao` (grava o arquivo via `ContentFile`) + `bulk_create` de contas / linhas DRE / linhas de Análise Vertical / achados. Um `except Exception` **fora** do `with` apaga o arquivo gravado se algo falhar no meio — upload de `FileField` não é transacional.
|
||||
|
||||
Correção final é a descrita acima (`dcDestaqueGrupos()` com a base padrão **descartada** enquanto houver grupo aberto). Validada simulando o algoritmo em Python contra as contas reais da apuração `2021`/`08-2026`, reproduzindo o roteiro exato do usuário: expandir "CAIXA E EQUIVALENTES DE CAIXA" destaca só seus 2 filhos (resto da tabela limpo); expandir "CLIENTES" em seguida mantém os 2 primeiros e soma "DUPLICATAS A RECEBER"; fechar "CLIENTES" volta aos 2 primeiros; fechar "CAIXA" devolve exatamente o `Set` de destaque inicial (comparação de igualdade entre os dois conjuntos bateu). Mesmo ajuste replicado em `pidDcrArvore()` (`dashboard-contabil-relatorio.html`, réplica em JS puro desta mesma lógica no relatório "Gerar Dashboard", com `calculaDestaque()`/`expandidos`/`descendentes()` espelhando as funções do JS) — os dois lados são mantidos manualmente em sincronia, ver "Balancete e D.R.E. têm árvore recolhível igual à tela de revisão" abaixo.
|
||||
`pipeline.processa_apuracao(origem, busca_historico)` recebe `busca_historico` como **função** (não uma lista pronta) exatamente por essa dependência: a chave de busca só existe depois da extração.
|
||||
|
||||
**Cabeçalho: dois botões por tabela, em pontas opostas** — `.dc-recolher-grupos-btn` (`data-dc-recolher-grupos="balancete|dre|analise-vertical"`) na primeira coluna, dentro de um `.dc-th-linha.dc-th-linha--inicio` (modificador que só troca o `justify-content` pra `flex-start` — este botão vem **antes** do rótulo, ao contrário do "Restaurar formatação", colado na borda direita da última coluna), e `.dc-reset-formatacao-btn` na coluna "Observação". Cada uma das 3 tabelas tem o seu par, porque os estados de colapso são independentes.
|
||||
Os achados de `variacao_atipica_dre` são ligados à linha certa por **`ordem`** (a posição de leitura no PDF, já existente em `LinhaAnaliseVerticalExtraida`/`ContabilLinhaAnaliseVertical`): monta-se `av_por_ordem = {linha.ordem: linha ...}` **depois** de persistir as linhas, e resolve-se `achado.ordem_linha_analise_vertical → ContabilLinhaAnaliseVertical`. Não por descrição (ambígua entre centros de custo) nem por id (que não existe no momento em que `regras.py` roda, sendo Python puro sem ORM).
|
||||
|
||||
O botão da primeira coluna nasceu como "+" (expandir tudo) numa rodada em que as tabelas abriam recolhidas; na rodada 148 o padrão de abertura virou "tudo expandido" e ele virou um **toggle** de duas faces: mostra "−" e aplica `dcColapsoPadrao()` (a visão compacta) enquanto nada está recolhido, mostra "+" e zera (`dc*Colapsadas = new Set()`) assim que existe qualquer grupo recolhido. A face sai de `dcAtualizaBotaoArvore(chave, colapsadas, nomeTabela)`, chamada no fim de cada `render*()` — é **derivada de `colapsadas.size`**, não de um flag próprio, então continua correta quando o contador recolhe/expande uma linha pelo toggle dela, sem passar pelo cabeçalho. O botão de restaurar passou a fazer o mesmo que a face "+"; ficou redundante e foi mantido de propósito, por ser a "borracha" que o usuário já conhece de outras telas do Portal.
|
||||
## Reprocessar
|
||||
|
||||
Ponto não-óbvio: os dois **zeram** `dc*Expandidos` em vez de populá-lo com todos os grupos. Com o conjunto vazio, `dcDestaqueGrupos()` cai na leva padrão (`dcUltimaLevaVisivel`), que sem nada recolhido marca exatamente as **folhas** — validado contra a apuração real `2021`/`08-2026`: 133 contas visíveis, 74 destacadas, exatamente as 74 contas sem filhos (igualdade de conjuntos conferida). Popular `dc*Expandidos` com todos os grupos destacaria quase toda linha da tabela, que é o mesmo problema de "tudo destacado, nada se destaca" da tentativa falha nº 1 acima. O cabeçalho da Análise Vertical é montado em JS (`renderAnaliseVerticalHead()`, número de colunas varia com os meses), então lá o botão nasce no template string — nos outros dois é HTML fixo em `dashboard-contabil.html`; os listeners são delegados no `<thead>`, então sobrevivem ao `innerHTML` ser refeito a cada render.
|
||||
`POST /api/contabil-apuracoes/{id}/reprocessar/` (multipart `arquivo`), bloqueado por `_contabil_garante_em_revisao()` — não existe reprocessar apuração concluída. Serve para corrigir uma análise feita com o PDF errado/incompleto **sem perder o trabalho já registrado**. O PDF novo precisa ser da **mesma** empresa+competência (senão 400 — trocar de empresa é uma análise nova).
|
||||
|
||||
**Card de achado expande a conta usada no apontamento** (`renderAchados()`, pedido explícito do usuário): `achado.conta` (id, já vem no payload de `/api/contabil-apuracoes/{id}/`, junto de `conta_codigo`/`conta_descricao`) é resolvido pra objeto completo procurando em `apuracaoAtual.contas` (`.find((c) => c.id === achado.conta)`) — sem chamada de API extra, já que a apuração inteira (contas + linhas de DRE + achados) já vem de uma vez só nesse endpoint. Só achados vinculados a uma conta específica ganham o botão "Ver conta usada no apontamento" (`.dc-achado-card__toggle-conta`) — as duas regras gerais (`balanceamento_ativo_passivo`/`debito_credito_divergente`, `conta` nulo no model) não têm uma conta única por trás, então não mostram o toggle. Expandido, mostra código/descrição/saldo anterior/débito/crédito/saldo atual da conta (`.dc-achado-card__conta`, um `<dl>` em grid). Estado de expansão (`dcAchadosContaExpandida`, um `Set()` de ids de achado) segue o mesmo padrão de `dcContasColapsadas`/`dcDreColapsadas` — reiniciado em `renderRevisao()`.
|
||||
Roda o mesmo `processa_apuracao()` de `create()`, troca o `arquivo` (apagando o antigo só **depois** do commit, pelo mesmo cuidado com storage não-transacional) e delega a resincronização para funções puras com **duas estratégias opostas**:
|
||||
|
||||
**Lista de Observações ordenada por severidade** (pedido explícito do usuário): `achadosFiltrados()` filtra e depois ordena (`PID_DC_SEVERIDADE_ORDEM = {alta: 0, media: 1, baixa: 2}`) — Alta sempre primeiro, Baixa por último, preservando a ordem original (ordem em que as regras rodaram) dentro de uma mesma severidade, já que `Array.prototype.sort` é estável. Puramente ordenação de exibição no frontend, nada mudou no backend/model.
|
||||
- **`_contabil_sincroniza_contas()` / `_linhas_dre()` / `_linhas_analise_vertical()` — atualização no lugar (mesmo `id`), nunca delete+recria.** Casam cada linha extraída contra a existente por chave natural: `(codigo, descricao)` no Balancete, `(descricao, nivel)` na DRE/Análise Vertical. Casada: atualiza os campos brutos no mesmo registro (`.save()`); se algum campo relevante mudou, força `validado=False` e `alterada_reprocessamento=True`, guardando o valor de antes em `valor_anterior_reprocessamento`/`valores_anterior_reprocessamento` (sempre lido **antes** de sobrescrever); senão preserva tudo, inclusive limpando esse campo. Sem match na extração nova: cria, com os defaults de sempre. Sobra no mapa antigo: `.delete()`.
|
||||
- **`_contabil_recria_achados()` — delete+recria total**, o mesmo `bulk_create` de `create()`. Todo achado nasce `pendente`, mesmo que a mesma `(regra, conta)` já estivesse tratada com justificativa escrita — a justificativa antiga some junto.
|
||||
|
||||
**Resumo clicável (donut por severidade + cards por grupo temático) no topo da aba Observações** (`.dc-achados-resumo`, `dashboard-contabil.html`/`.css`/`.js`, pedido explícito do usuário, inspirado numa tela de auditoria de outro sistema — ver rodadas 97/98 do `CHANGELOG.md`): acima dos chips de filtro, um donut em SVG puro (sem Chart.js — essa dependência só existe no relatório estático "Gerar Dashboard", não faz sentido carregar aqui numa tela interativa pequena) com a quantidade de observações por **severidade** (Alta/Média/Baixa, mesmas cores dos badges — `--danger`/`--gold`/`rgb(var(--slate-rgb))`) e o total no centro, mais uma grade de cards. `PID_DC_REGRAS` (constante no topo do arquivo) enumera as **9 regras de `regras.py`** por chave (`achado.regra`, campo que já existia no model `ContabilAchado`) — eram 10 até uma rodada seguinte, quando `lucro_balancete_diverge_dre` entrou e `variacao_atipica_saldo`/`percentual_custo_receita_atipico` saíram (ver "Regras de auditoria v1" acima).
|
||||
**Por que estratégias opostas**: conta/linha é dado extraído que o contador **anota** — o valor de hoje precisa ser atualizado, mas a anotação de ontem sobre a mesma conta continua valendo. Achado é um **apontamento derivado**, recalculado inteiro a cada rodada das regras: não existe "achado que não mudou", ele dispara com os dados de agora ou não dispara. Manter um achado "tratado" que já não dispara equivale a mostrar uma inconsistência que não existe mais, contrariando o propósito de sinalizar o que precisa de atenção. Decisão explícita do usuário, revertendo a escolha original de preservar tratativas.
|
||||
|
||||
**Cards agrupados por tema, não um card por regra** (rodada seguinte à criação do resumo, pedido explícito do usuário: a versão original — um card por regra, 10 ao todo na época, cada um com uma mini-tabela de conta+status por achado — ficou "muito poluída visualmente"). `PID_DC_GRUPOS` (constante logo abaixo de `PID_DC_REGRAS`) agrupa as 9 regras atuais em 3 temas fixos: "Divergências de Saldo" (balanceamento Ativo x Passivo, débito x crédito, caixa negativo, sinal de saldo invertido, lucro do balancete x DRE), "Contas Atípicas" (contas transitórias, contas que deveriam zerar, descrição genérica) e "Variações e Indicadores" (variação atípica na DRE, única regra do grupo desde que as outras duas saíram) — agrupamento original alinhado com o usuário antes de implementar (via pergunta com preview), mantido ao remapear as regras novas/removidas pros mesmos 3 temas. Cada card mostra só o total do grupo no cabeçalho e, por baixo, uma linha por regra (label + contagem, sem mini-tabela de conta/status) — regra sem nenhum achado nesta apuração continua listada com contagem `0`, só sem estar clicável (mesmo espírito de sempre mostrar todas as regras, mesmo as que "passaram"). O detalhe por conta/status de cada achado (antes replicado dentro de cada card) não foi removido, só saiu do resumo — continua disponível na lista completa logo abaixo, ao clicar numa linha de regra pra filtrar. `renderAchadosResumo()` (chamada no início de `renderAchados()`, então atualiza sozinha a cada mudança de status/filtro) conta sempre sobre `apuracaoAtual.achados` **completo**, nunca sobre `achadosFiltrados()` — é uma visão geral estável, não deve mudar quando o usuário filtra a lista detalhada logo abaixo. As cores dos 3 grupos (`PID_DC_CATEGORIA_CORES`, mesma constante de antes, agora indexada por grupo em vez de por regra) reaproveitam os tokens `--accent-rgb`/`--danger-rgb`/`--gold-rgb` de `tokens.css` (já theme-aware, acompanham tema claro/escuro e a cor de tema escolhida pelo usuário), sem nenhum hex novo hardcoded. Decisão explícita de escopo (alinhada por pergunta ao usuário antes de implementar): **não** foi replicado o checklist pass/fail de checagens do sistema de referência (várias delas — folha, vencimento de fornecedor/cliente/imposto, saldo bancário — dependem de dado fora do Balancete/DRE anexado, fora do escopo já documentado desta ferramenta, ver "Decisões de escopo" acima) nem essa visualização foi levada pro relatório "Gerar Dashboard" (só a tela de revisão do Portal).
|
||||
**Isso não quebra FK nenhuma**: não há achado preservado através do delete, então `ContabilAchado.conta` é sempre resolvida fresca contra o mapa de contas **já sincronizadas** (roda antes, na mesma transação). `ContabilObservacao` nunca teve relação com `ContabilAchado` — vive em model separado, casada por chave natural, e `_contabil_recria_achados()` nem a toca. A imunidade da observação ao reprocessamento é de graça: ela não mora na linha.
|
||||
|
||||
**Donut e linhas de regra são clicáveis, filtram a lista detalhada abaixo** (pedido explícito do usuário, rodada 98; a granularidade do clique por regra individual foi preservada na reorganização em grupos da rodada seguinte): cada fatia do donut (ou item da legenda) chama `pidDcSelecionarSeveridade(severidade)` — a mesma função que os chips "Alta"/"Média"/"Baixa" já usavam (extraída pra função reaproveitável, sem duplicar a lógica de atualizar `filtroSeveridade`+classe `.is-active`+`renderAchados()`); clicar numa linha de regra com contagem > 0 alterna `filtroRegra` (`achado.regra` exata ou `null`) — clicar de novo na mesma linha limpa o filtro; o card do grupo em si (cabeçalho) não é clicável, só as linhas de regra dentro dele. `achadosFiltrados()` ganhou uma terceira condição (`filtroRegra`) que se combina por E lógico com severidade/status já existentes — os três filtros funcionam juntos, não um substitui o outro. Como não existe um chip próprio pro filtro por categoria, uma faixa nova (`#dc-regra-filtro-ativo`, escondida quando `filtroRegra` é `null`) aparece entre os chips e a lista mostrando o nome da regra ativa + um botão "Limpar" — sem essa faixa não haveria como o usuário perceber por que a lista ficou filtrada nem como sair do filtro sem adivinhar que precisa clicar de novo na linha. Clicar em qualquer um dos dois (donut/linha de regra) também dá um `scrollIntoView` suave até `#dc-achados-list` (`pidDcScrollParaLista()`), já que o resumo pode empurrar a lista pra fora da tela em telas menores. O donut usa a técnica clássica de pizza/donut em SVG com `<circle r="15.9155">` (circunferência ≈ 100, então `stroke-dasharray`/`stroke-dashoffset` já funcionam direto em unidades de percentual, sem precisar de `pathLength`) — cada segmento é um `<circle>` próprio com seu `stroke-dashoffset` acumulado (offset inicial `25` desloca o início de "3 horas" pra "12 horas"), clicável individualmente porque `pointer-events` de SVG só considera pixel realmente pintado pelo `stroke`, sem precisar de hit-test manual por ângulo.
|
||||
**Marcar como validada de novo NÃO limpa o alerta de alteração** (pedido explícito): o badge muda de cor conforme `validado` — `--danger` enquanto pendente, verde depois de validado — para dar para ver quais itens foram reprocessados **e** revalidados. `alterada_reprocessamento` só é limpo de verdade num próximo reprocessamento em que aquela conta/linha não mudar.
|
||||
|
||||
**Editor de observação inline + checkbox "validado" (rodada de "aliado do contador")**: pedido explícito do usuário pra reduzir a fricção da revisão — o antigo `#dc-observacao-modal` (popup único reaproveitado por conta/linha) foi removido; clicar no ícone de observação agora abre uma `<tr class="dc-obs-edit-row">` extra logo abaixo da própria linha, dentro da mesma tabela (`renderContas()`/`renderDre()`, `dashboard-contabil.js`), com um `<textarea>`, um checkbox "Mostrar esta observação ao cliente no relatório" e os botões Cancelar/Salvar — motivo do usuário: um clique acidental fora do popup não deve mais descartar o texto em digitação, e ver a conta ao lado da observação ajuda a não perder o contexto. Estado de qual editor está aberto (no máximo um por tabela) fica em `dcContaObsEditId`/`dcDreObsEditId` (module-level, resetados em `renderRevisao()`), seguindo o mesmo padrão de `dcContasColapsadas`/`dcAchadosContaExpandida`. Salvar chama `pidAtualizarObservacaoContaContabil(id, {observacao, oculta_no_relatorio})`/a equivalente de DRE — os dois campos juntos numa única chamada PATCH, já que "mostrar ao cliente" é decidido no mesmo instante em que a observação é escrita; `oculta_no_relatorio` agora nasce `True` por padrão no model (antes `False`) — uma observação nova só vai pro relatório do cliente depois que o contador marcar o checkbox explicitamente, nunca por padrão (migração `0066`, só o `default` mudou, sem reescrever linhas já existentes).
|
||||
## Observações: histórico por empresa+conta
|
||||
|
||||
Segundo botão ao lado do de observação (mesma célula, `.dc-obs-cell-actions`): um ícone de check (`.dc-conta-validado-btn`, cor `--teal` quando marcado — deliberadamente não `--accent`, que já é usado pra "observação preenchida" e também é a cor de tema escolhida pelo usuário, ver "Temas de cor" no `tokens.css`) pro campo novo `validado` — puramente informativo ("já conferi esta conta/linha durante a revisão"), sem gate em nada (não bloqueia conclusão, não afeta achado/relatório). PATCH via `pidAtualizarValidadoContaContabil`/`...LinhaDreContabil`.
|
||||
Uma observação registrada num mês reaparece na análise dos meses seguintes, assinada e datada, **bloqueada para edição** por ser registro histórico. Três decisões de escopo, confirmadas antes de implementar:
|
||||
|
||||
**Tri-state numa conta/linha sintética (com filhos)** (pedido explícito do usuário, rodada seguinte): uma folha continua um toggle simples (verde/cinza), mas uma sintética passa a ter 3 estados, calculados a cada render a partir dos **descendentes** (todos, não só os filhos diretos — `dcDescendentes(itens, nivelFn, id)`, generaliza `dcFilhosDiretos` pra não parar no primeiro nível) via `dcEstadoValidacaoGrupo(item, descendentes, descendentesFolhas)`:
|
||||
- `"nenhum"` (cor padrão) — nem a sintética nem nenhum descendente está validado.
|
||||
- `"parcial"` (`--gold`, mesmo amarelo dos badges de severidade média) — qualquer combinação intermediária, **inclusive** só a própria sintética marcada (1º clique) sem nenhum descendente ainda.
|
||||
- `"completo"` (`--teal`, mesma cor de uma folha validada) — **todo** descendente FOLHA está validado, checado primeiro. Usa só `descendentesFolhas` (`dcDescendentes(..., somenteFolhas=true)`), não `descendentes` completo — **bug real, rodada 139**: numa árvore de 3+ níveis (mãe → filha → netos), validar os netos direto sem clicar na própria "filha" intermediária nunca marca o campo `validado` dela no banco (só o estado visual dela é "completo", calculado por render); checar todo `descendentes` (incluindo a "filha") na "mãe" fazia o grupo nunca fechar, mesmo com todo neto validado. `descendentes` (todos, não só folha) continua valendo pro "parcial" — uma sintética marcada sozinha, sem cascatear, ainda deve sinalizar "em andamento" num ancestral.
|
||||
1. **Toda observação propaga por padrão.** Não existe "fixar"; existe o inverso, encerrar explicitamente. Evita histórico que só existe quando alguém lembra de marcar.
|
||||
2. **O histórico cobre Balancete/D.R.E./Análise Vertical.** A justificativa de tratativa de um achado (`ContabilAchado.observacao_contador`) continua presa à apuração — e, como o achado é recriado a cada reprocessamento, ela nem sobrevive a isso. Só `ContabilObservacao` atravessa competências.
|
||||
3. **`mostrar_ao_cliente` é sempre alternável**, inclusive numa observação já travada. O bloqueio protege texto, autor e data; mostrar ou não ao cliente é decisão editorial de cada relatório, e uma marcação errada precisa ser corrigível sem reescrever o histórico.
|
||||
|
||||
`dcClicarValidadoConta(id)`/`dcClicarValidadoLinha(id)` (novas, chamadas pelo listener de clique de `[data-dc-conta-validado]`/`[data-dc-dre-validado]`) implementam o ciclo de 3 cliques pedido pelo usuário, recalculando o estado a cada clique (nunca guardado à parte):
|
||||
1. `"nenhum"` → PATCH só na própria sintética (`validado=true`) → vira `"parcial"` (a menos que, coincidentemente, todo descendente já estivesse validado).
|
||||
2. `"parcial"` → `pidConfirm("Deseja validar todas as contas deste grupo?")` → se confirmado, PATCH em lote (`Promise.all`) de todo descendente ainda não validado (mais a própria sintética, se ainda não) → vira `"completo"`. Se cancelado, nada muda.
|
||||
3. `"completo"` → PATCH em lote desmarcando a própria sintética + todos os descendentes, **sem perguntar** (pedido explícito: "apertar novamente desmarca todo o grupo").
|
||||
**Chave natural, nunca FK para a linha**: `"codigo|descricao"` no Balancete, `"descricao|nivel"` na DRE/Análise Vertical (`chave_conta()`/`chave_linha()` no model, `_contabil_chave_alvo()` na view, `dcChaveObsConta()` no JS — mesmo formato nos três). São exatamente as chaves que a sincronização do reprocessamento usa.
|
||||
|
||||
Cada PATCH é individual (`ContabilContaViewSet`/`ContabilLinhaDreViewSet` continuam sem uma action de lote) — o "lote" é só client-side via `Promise.all`, aceitável dado que um grupo real tem no máximo algumas dezenas de contas. Todo caminho termina chamando `renderContas()`/`renderDre()` inteiro (abandonando o ajuste direto no DOM que existia antes só pro caso de folha) — necessário porque o estado de uma sintética **ancestral** também pode ter mudado de cor e precisa recalcular, o que só um re-render completo garante; o efeito colateral aceito é que um editor de observação aberto numa outra linha perde o texto ainda não salvo nesse recálculo (like-for-like com o comportamento já aceito ao alternar entre linhas, ver "Editor de observação inline" acima).
|
||||
> **A descrição faz parte da chave do Balancete, e isso não é redundância.** O Questor reaproveita o mesmo código de classificação para várias contas analíticas de mesma natureza — confirmado pelo usuário com 6 bancos diferentes (Banco do Brasil, Inter, Itaú, Mercado Pago, PagSeguro, Sicredi) sob o mesmo código de "Depósitos Bancários à Vista". Com a chave só por `codigo`, uma observação escrita num banco aparecia em todos os outros. Trade-off aceito: uma conta **renomeada** com o mesmo código vira uma conta "nova" no reprocessamento (a antiga é excluída, outra é criada) — o mesmo trade-off que a DRE já aceitava.
|
||||
|
||||
**Resumo de observações no final de cada aba** (pedido explícito do usuário: "inclua no final das páginas um resumo da quantidade de observações e as observações realizadas"): Balancete e DRE ganharam cada um sua própria seção `.dc-obs-resumo` logo abaixo da tabela (`renderContasObsResumo()`/`renderDreObsResumo()`, chamadas no fim de `renderContas()`/`renderDre()` — sempre em sincronia com a tabela) com a contagem (`<span class="dc-obs-resumo__count">`) e a lista de observações já registradas **daquela aba**, cada uma com o mesmo botão de olho (mostrar/ocultar do relatório) que já existia na lista combinada da aba "Dashboard". É **adicional**, não substitui: a lista combinada de `#dc-dash-observacoes-list` (Balancete + DRE + Auditoria juntos) continua existindo do jeito que estava — decisão confirmada com o usuário, já que cada resumo serve um propósito diferente (visão específica de uma aba vs. visão consolidada antes de gerar o relatório). `pidDcObsItemHtml()` (função nova) fatora a marcação de um item de lista (`.dc-dash-obs`), reaproveitada pelos dois resumos novos — a lista combinada da aba "Dashboard" manteve sua própria montagem inline (precisa do rótulo de origem "Balancete"/"D.R.E."/"Auditoria" e de uma chave composta `tipo:id`, que os resumos por aba não precisam por já serem de um tipo só).
|
||||
**Vigência** (`vigentes_para()`/`vigente_em()`): a observação aparece em toda apuração da mesma empresa com `competencia_origem <= C` e (`encerrada_em_competencia` nulo ou `>= C`). Daí saem os três caminhos: **manter** é não fazer nada; **encerrar** grava a competência aberta (a observação continua visível nela e some da seguinte em diante — o histórico nunca é reescrito); **incluir uma nova** cria outro registro, então uma conta tem uma thread, não um texto único.
|
||||
|
||||
**Ícone de observação + painel inline também no relatório do cliente** (pedido explícito do usuário, "assim como temos na aplicação"): as tabelas de Balancete/DRE do relatório ganharam uma coluna "Observação" (igual à da tela de revisão) com um ícone que só aparece quando `item.conta.observacao`/`item.linha.observacao` está preenchida **e** `oculta_no_relatorio` é `False` (a mesma condição de `observacoes_contas`/`observacoes_dre` no contexto do view, ver "Relatório 'Gerar Dashboard'" abaixo) — uma observação marcada como não-visível ao cliente não aparece nem como ícone. Clicar abre um `<tr class="dcr-obs-inline-row">` já presente no HTML (nasce `hidden`) logo abaixo da conta/linha, com o texto puro (sem `|safe`, é `TextField` simples, não rich text). `pidDcrObs(tbodyId)` (nova função em `dashboard-contabil-relatorio.html`, chamada logo depois de `pidDcrArvore(tbodyId)` pro mesmo `tbody`) controla o abrir/fechar; a visibilidade da linha de observação é sincronizada (`sincroniza()`, chamada a **todo** clique no corpo da tabela, inclusive os de expandir/recolher grupo) a partir de dois fatores: se o usuário marcou aquele painel como aberto E se a linha-pai (o `previousElementSibling`) está visível — assim, colapsar um grupo ancestral também fecha visualmente qualquer painel de observação aberto dentro dele, sem duplicar a lógica de pilha/nível de `pidDcrArvore`. Importante: essas linhas de observação são **excluídas** da lista `linhas` que `pidDcrArvore()` percorre (`filter` por `data-dcr-obs-row`) — incluí-las quebraria a pilha de colapso, já que elas não têm `data-dcr-nivel` próprio.
|
||||
**Imutabilidade e exclusão**:
|
||||
- `texto` só é aceito enquanto a apuração de origem estiver "Em revisão" (`_garante_texto_editavel()`); depois disso a edição é recusada com 400, pedindo para registrar uma nova ou encerrar a existente.
|
||||
- Uma edição de texto que passa deixa rastro em `ContabilObservacaoEdicao`, exibida por um botão de relógio na thread — para editar não virar uma forma indireta de apagar uma observação importante reescrevendo por cima.
|
||||
- `DELETE` é permitido, mas com **duas** travas somadas: a mesma regra de "só em revisão" **e** `criado_por_id == request.user.id` (senão `PermissionDenied`). Mostrar ou esconder o botão no frontend é só UX; o servidor confere as duas de novo.
|
||||
- Excluir e encerrar convivem de propósito: excluir é definitivo e só de quem criou; encerrar é reversível e qualquer um do time pode usar.
|
||||
|
||||
## Relatório "Gerar Dashboard" (`indicadores.py` + `dashboard-contabil-relatorio.html`)
|
||||
> Os ícones de "encerrar" e "excluir" **não podem ser o mesmo desenho**. Encerrar usa um ícone de arquivo (caixa com uma linha); a lixeira (`PID_DC_ICON_LIXEIRA`) é só da exclusão de verdade. Os dois aparecem lado a lado na mesma linha de ações, e uma é reversível e a outra não.
|
||||
|
||||
`ContabilApuracaoViewSet.dashboard()` (`GET /api/contabil-apuracoes/{id}/dashboard/`, mesma permissão de toggle único das outras actions) gera um documento HTML autocontido (não estende o shell do Portal — nunca a marca "P.I.D.", ver "Logos" no CLAUDE.md raiz) com os indicadores financeiros, a DRE/Balancete agrupados por nível (recolhível, ver "Árvore recolhível" abaixo) e as observações que o contador já registrou (contas, linhas de DRE, achados tratados/ignorados com `observacao_contador`). Escopo confirmado com o usuário: **sempre uma apuração por vez** (a que está sendo revisada), sem "Filial" (não existe no modelo) nem consolidação entre empresas — isso ficaria pra um BI à parte, fora de escopo aqui.
|
||||
## Indicadores
|
||||
|
||||
**É GET, não POST** (diferente do padrão `/gerar/` de outras ferramentas, que mutam estado ou recebem multipart) — decisão de uma rodada seguinte, corrigindo um bug real: a primeira versão era POST, e o frontend chamava via `fetch` + `URL.createObjectURL(blob)` + `window.open(url)` pra abrir numa nova aba (mesmo padrão de `pidGerarArquivoPlanoSaude`). Só que um documento carregado de uma URL `blob:` tem uma origem sintética — URLs relativas dentro do HTML (como as que `{% static %}` gera, ex. `/static/img/logo-branco.png`) não resolvem de forma confiável contra a origem real do Portal nesse contexto, e a logo do escritório no cabeçalho simplesmente não carregava. Com GET, o frontend abre a URL da API direto (`window.open(`/api/contabil-apuracoes/${id}/dashboard/`, "_blank")`, `pidGerarDashboardContabil()` em `dashboard-contabil.js`) — navegação de verdade, mesma origem, sem blob nenhum de permeio; `{% static %}` funciona igual a qualquer outra página do Portal. Também simplificou o JS (sem `pidEnsureCsrfCookie`/CSRF manual — GET não precisa).
|
||||
Todo indicador é um `IndicadorContabilDefinicao` no banco — **os 11 "de sistema" (ROA, ROE, Kanitz, EBIT, EBITDA, as três liquidezes, composição/grau de endividamento, IPL) também**. Não são caso especial em lugar nenhum do código: CRUD, cálculo e exibição passam pelo mesmo caminho de qualquer indicador personalizado. Qualquer contador com acesso à ferramenta pode criar/editar/excluir — não é tela administrativa restrita ao perfil "Inovação" (decisão deliberada: quem cria os indicadores é o próprio contador usando a ferramenta).
|
||||
|
||||
### Indicadores financeiros (`indicadores.py`)
|
||||
### Motor de fórmula (`formula.py`)
|
||||
|
||||
Funções puras, mesmo espírito de `regras.py` — não tocam no ORM, recebem os dados já extraídos. **Os grupos do Balancete são identificados por código de classificação fixo** (`CODIGO_*`), calibrado contra o balancete de referência do usuário (`792 - balancete 072026.pdf`) — mesma decisão de risco já aceita em `regra_saldo_negativo_caixa` (código fixo `"1.01.01.001"` pro grupo Caixa). **Se um cliente usar uma numeração de plano de contas diferente da vista até agora, os indicadores desse cliente saem errados silenciosamente** — revisar contra mais balancetes reais de outras empresas antes de confiar cegamente no valor exibido ao cliente.
|
||||
`avalia_formula(expressao, valores)` faz `ast.parse(expressao, mode="eval")` e anda pela árvore com um **allowlist** restrito: só `BinOp` (`+ - * /`), `UnaryOp` (`+ -`), número literal e `Name` (resolvido contra o dict `valores`). Qualquer outro nó (chamada de função, atributo, import, comparação) levanta `FormulaInvalidaError` **antes de qualquer coisa ser executada** — nunca `eval()`/`compile()` sobre texto digitado pelo contador.
|
||||
|
||||
Códigos calibrados: Ativo Total `"1"`, Ativo Circulante `"1.01"`, Estoques `"1.01.08"`, Imobilizado `"1.02.05"`, Depreciação Acumulada `"1.02.05.007"`, Passivo Total `"2"`, Passivo Circulante `"2.01"`, Patrimônio Líquido `"2.04"`. O Passivo Não Circulante/Exigível a Longo Prazo **não tem código calibrado** — não aparece no balancete de referência, que não tem dívida de longo prazo — e é calculado **por eliminação** (Passivo Total − Passivo Circulante − Patrimônio Líquido), sempre exato pela identidade contábil, sem depender de adivinhar mais um código.
|
||||
Se qualquer nome referenciado valer `None`, ou a fórmula dividir por zero, o resultado inteiro é `None`. **"Indisponível" nunca vira 0** — disciplina seguida em todo o pacote. `valida_formula(expressao, chaves_disponiveis)` roda a mesma árvore com valores fictícios só para validar sintaxe/nomes na hora de salvar.
|
||||
|
||||
**Validados byte a byte contra a captura de tela do Balancete anexada pelo usuário** (Ativo Total 2.721.721,59 / Ativo Circulante 2.582.105,87 / Estoques 1.702.326,05 / Passivo Circulante 408.765,09 / Patrimônio Líquido 2.312.956,50 / Imobilizado 134.203,88 — bateram exatamente):
|
||||
- Liquidez Corrente = Ativo Circulante / Passivo Circulante → 6,32
|
||||
- Liquidez Seca = (Ativo Circulante − Estoques) / Passivo Circulante → 2,15
|
||||
- Composição do Endividamento = Passivo Circulante / Exigível Total → 100,00% (bate porque essa empresa não tem Passivo Não Circulante)
|
||||
- Grau de Endividamento = Exigível Total / Patrimônio Líquido → 17,67%
|
||||
- IPL (Imobilização do Patrimônio Líquido) = Imobilizado / Patrimônio Líquido → 5,80%
|
||||
### Resolução contra uma apuração (`views.py`)
|
||||
|
||||
**Aproximado, sem exemplo real pra validar**: Liquidez Geral = Ativo Circulante / Exigível Total — trata o Realizável a Longo Prazo como indisponível/0, porque o Ativo Não Circulante (`"1.02"`) hoje mistura Investimentos/Imobilizado com um eventual Realizável a Longo Prazo, sem separar (o parser não distingue isso). Coincide com a Liquidez Corrente quando a empresa não tem Passivo Não Circulante (era o caso do balancete de referência).
|
||||
- `_ContabilDadosIndicadores` (dataclass) + `_contabil_coleta_dados_indicadores(apuracao)` juntam `contas_atuais`/`dre_atual`/`resultado_liquido`/`historico_completo` e derivam `contas_anteriores` (só a apuração anterior imediata). Reaproveitado por todos os caminhos de cálculo, evitando duas idas ao banco pelos mesmos dados.
|
||||
- `_contabil_resolve_componente_personalizado()` resolve **um** componente: `contas`/`variacao_conta` somam em **valor absoluto** (Passivo/PL vêm negativos no relatório); `linha_dre` soma com o sinal já impresso (a DRE não segue essa convenção); `variacao_conta` é `soma_atual − soma_anterior`, `None` sem apuração anterior; `indicador` faz `valores.get(chave)`; `resultado_liquido` resolve sempre para `dados.resultado_liquido`.
|
||||
- `_contabil_calcula_indicadores_personalizados()` resolve **todas** as definições. **Iterativo, não ordenação topológica de verdade**: a cada rodada calcula todo indicador cujos componentes `tipo="indicador"` já têm valor, repetindo até não sobrar progresso. Cobre encadeamento sem ordenar dependências explicitamente. Um indicador cuja dependência nunca resolve (referência quebrada ou **ciclo** entre dois personalizados) fica `None` para sempre, **sem lançar erro** — uma fórmula mal configurada não pode derrubar o relatório inteiro.
|
||||
|
||||
**DRE**: `resultado_liquido` é passado pra `calcula_indicadores()` já resolvido pela view (`linhas_dre[-1].valor`, a última linha na ordem do relatório) — não por texto, mais seguro (mesma fonte que `regra_lucro_balancete_diverge_dre` usa em `regras.py`, ver "Regras de auditoria v1" acima). EBIT = Resultado Líquido − linha "(+/-) Despesas/Receitas Financeiras" (casada por substring via `LINHA_DRE_DESPESAS_RECEITAS_FINANCEIRAS` em `parser.py`, **não validada** contra um PDF real). Depreciação/Amortização do mês = variação do saldo da conta de depreciação acumulada entre a apuração atual e a anterior da mesma empresa — **indisponível na primeira apuração de uma empresa**; EBITDA = EBIT + essa variação, também indisponível quando ela for. ROA = Resultado Líquido / Ativo Total; ROE = Resultado Líquido / Patrimônio Líquido.
|
||||
> **Por que existe o tipo `resultado_liquido`** em vez de um componente `linha_dre` apontando para a última linha: a última linha da DRE **muda de rótulo conforme o sinal do resultado** — uma empresa com prejuízo termina em "(=) PREJUÍZO LÍQUIDO DO EXERCÍCIO", com lucro terminaria em "(=) LUCRO LÍQUIDO DO EXERCÍCIO". Um `linha_dre` (que casa por texto exato) quebraria assim que o resultado de uma empresa virasse de sinal entre uma competência e outra. `resultado_liquido` resolve por **posição**, não por texto.
|
||||
|
||||
**Kanitz (Termômetro de Insolvência)** usa a fórmula-livro-texto padrão (`FI = 0,05×ROE + 1,65×LiquidezGeral + 3,55×LiquidezSeca − 1,06×LiquidezCorrente − 0,33×GrauEndividamento`), **não validada** contra o BI antigo que este Dashboard substitui — o valor mostrado numa captura do usuário ("11,31") está fora da faixa clássica do índice (−7 a +7), sugerindo que o BI antigo usa uma variação/escala diferente. O relatório marca esse card como estimativa; ajustar se o usuário trouxer a fórmula exata usada pelo BI antigo.
|
||||
### Padrão vs. não padrão
|
||||
|
||||
Todo indicador que pode ficar indisponível é `Decimal | None` no dataclass `IndicadoresFinanceiros` — `None` significa indisponível, nunca é tratado como zero (os filtros de template descritos abaixo respeitam essa distinção).
|
||||
`padrao=True` (default) faz o indicador aparecer em toda apuração. `padrao=False` deixa o indicador salvo e editável, mas ele só aparece numa apuração em que sua chave esteja em `ContabilApuracao.indicadores_selecionados`. Um indicador não padrão não selecionado **não aparece em lugar nenhum** daquela apuração (nem relatório, nem aba "Dashboard"), mas continua na listagem completa do hub "Gerenciar Indicadores", que nunca é filtrada por apuração.
|
||||
|
||||
### Template (`templates/dashboard-contabil-relatorio.html`) e filtros (`portal_api/templatetags/contabil_extras.py`)
|
||||
`indicadores_ocultos` é ortogonal: esconde do **relatório do cliente** um indicador que está aparecendo.
|
||||
|
||||
Documento HTML autocontido — `<style>` inline com a mesma paleta já usada nos documentos gerados (`indicadores/recibo.py`: roxo `#3d2178`, dourado `#b4872a`), cabeçalho com `static/img/logo-branco.png` (texto branco — não `logo.png`, que ficaria ilegível sobre o banner roxo escuro, ver "Logos em `static/img/`" no CLAUDE.md raiz). Botão "Imprimir" (`onclick="window.print()"`) escondido via `@media print`.
|
||||
### Exclusão de definição
|
||||
|
||||
**Conteúdo dividido em abas** (`.dcr-tabs`/`.dcr-tab`/`[data-dcr-panel]`, JS puro inline no próprio template — não reaproveita `.pa-tabs` de `perfis-acesso.css`, que não é carregado neste documento autocontido), nesta ordem: **Balancete** (aba inicial) → **D.R.E.** → **Análise Vertical** (só quando a apuração tem essa seção, ver rodada 123 abaixo) → **Resumo** (rótulo visível; `data-dcr-tab`/`data-dcr-panel` internos continuam `"indicadores"`, nome que nasceu antes de virar uma aba com mais coisa além dos cards — os dois grupos de indicador, o Resumo do Fechamento e as Observações consolidadas). Na impressão (`@media print`), a barra de abas some e todas ficam visíveis ao mesmo tempo, cada uma numa página própria (`page-break-after`) — documento impresso não deve esconder conteúdo atrás de uma aba não clicada.
|
||||
`perform_destroy()` bloqueia (400) se outra definição referencia esta pela fórmula, listando os nomes dependentes — para não deixar fórmula alheia quebrada em silêncio. Depois de excluir, **limpa a chave de toda `ContabilApuracao.indicadores_selecionados`/`indicadores_ocultos` que a referenciava**: sem essa limpeza a chave fica órfã e, como os dois serializers validam a lista **inteira** a cada alternância de checkbox, o usuário fica travado sem conseguir alternar nenhum indicador naquela apuração — não só o excluído. As duas validações também descartam chave inexistente em silêncio, como rede de segurança (é só estado de exibição, não dado auditado).
|
||||
|
||||
**Observações espalhadas nas 3 abas, cada uma com o recorte certo** (pedido explícito do usuário — antes ficavam todas juntas numa única seção fora das abas): a aba **Balancete** termina com "Observações do Balancete" (só `observacoes_contas`); a aba **D.R.E.** termina com "Observações da D.R.E." (só `observacoes_dre`); a aba **Indicadores** termina com "Todas as Observações da Análise" (`observacoes_contas` + `observacoes_dre` + `achados_com_observacao` juntos, cada item com um prefixo indicando a origem — "Balancete — ...", "D.R.E. — ...", "Auditoria — ..." — já que aqui não há mais uma aba própria pra inferir o contexto). **Nunca "Achado"/"Achado de Auditoria" em texto visível** — pedido explícito do usuário, mesmo motivo pelo qual a aba de revisão já se chama "Observações" (`data-dc-tab="achados"` com o texto "Observações", `dashboard-contabil.html`) e não "Achados"; `achado`/`ContabilAchado`/`achados_com_observacao` continuam normais como nome de variável/model/classe, só não podem aparecer como palavra na tela. As três seções reaproveitam o mesmo markup (`.dcr-obs-lista`/`.dcr-obs-item`), só filtrando quais das três listas do contexto (`observacoes_contas`/`observacoes_dre`/`achados_com_observacao`, já vindas prontas de `views.py`) cada uma itera — nenhuma mudança no backend foi necessária, é só reorganização do template. Estado vazio próprio por seção ("Nenhuma observação registrada no Balancete."/"...na D.R.E."/"...nesta análise.").
|
||||
### Fórmula de cálculo vs. fórmula exibida
|
||||
|
||||
**Balancete e D.R.E. têm árvore recolhível igual à tela de revisão** (pedido explícito do usuário — a primeira versão do relatório vinha totalmente expandida, sem toggle). Diferença de arquitetura em relação a `renderContas()`/`renderDre()` em `dashboard-contabil.js`: lá é uma SPA que re-renderiza a tabela inteira a cada clique; aqui é HTML estático gerado uma vez, então o nível de cada linha e se ela "tem filhos" (`nivel`/`tem_filhos`) são calculados **no servidor** (`_contabil_arvore_contexto()` em `views.py`, mesmo algoritmo — "tem filhos" = a próxima linha tem nível maior) e ficam como atributos `data-dcr-nivel`/`data-dcr-tem-filhos`/`data-dcr-id` em cada `<tr>` já renderizada. `pidDcrArvore(tbodyId)` (JS inline no template) só alterna o atributo `hidden` das `<tr>` existentes com a mesma lógica de pilha de níveis recolhidos, sem reconstruir HTML nenhum. Na impressão, `.dcr-tabela tbody tr[hidden] { display: table-row !important; }` força toda linha a aparecer mesmo que o usuário tenha recolhido algum grupo na tela — documento impresso não deve esconder conta nenhuma atrás de um grupo recolhido.
|
||||
`formula` é a expressão validada, usada só para calcular, e mostra as chaves internas dos componentes (`resultado_liquido - despesas_financeiras`). `formula_exibicao` é texto livre, nunca passa por `avalia_formula()`/`ast`, e é o que o cliente vê. No modal, os campos são rotulados "Fórmula (cálculo interno)" e "Fórmula (como aparece ao cliente)". O servidor resolve o fallback: `definicao.formula_exibicao.strip() or definicao.formula` — indicador sem a preferência preenchida mostra a fórmula técnica, nunca fica sem fórmula nenhuma.
|
||||
|
||||
**Nasce recolhido a partir do "grupo 4", mesmo limiar da tela de revisão** (rodada seguinte, mesmo pedido — ver "Nasce recolhida a partir do 'grupo 4'" acima): `_contabil_arvore_contexto()` ganhou um terceiro campo por item, `colapsado_padrao` (`tem_filhos and nivel >= _CONTABIL_NIVEL_ABERTO_PADRAO`, constante módulo-level `= 3`), viram `data-dcr-colapsado-padrao="1"/"0"` em cada `<tr>`; o botão de toggle só ganha a classe `is-expanded` inicial quando `not item.colapsado_padrao`. `pidDcrArvore()` lê esse atributo **antes** do primeiro clique e semeia o objeto `colapsadas` (antes só populado por interação do usuário) com os ids marcados, chamando `atualiza()` uma vez na inicialização — o resto do algoritmo (pilha de níveis, alternar `hidden`) não mudou. Mesma constante conceitual dos dois lados (`_CONTABIL_NIVEL_ABERTO_PADRAO` em `views.py` / `PID_DC_NIVEL_ABERTO_PADRAO` em `dashboard-contabil.js`), duplicada porque um é Python renderizado uma vez e o outro é JS de uma SPA — se o limiar mudar, ajustar os dois.
|
||||
### Ícones: duas cópias mantidas à mão
|
||||
|
||||
**Gráfico de evolução do Resultado Líquido removido numa rodada seguinte** (pedido explícito do usuário: "temos a análise vertical para esta visualização" — a aba Análise Vertical já cobre esse tipo de comparação mês a mês, o gráfico ficava redundante). Existia via Chart.js (`<script src="https://cdn.jsdelivr.net/npm/chart.js@4">`) com a série injetada por `{{ evolucao|json_script:"dcr-evolucao-data" }}`; os três (o `<script>` do CDN, o `json_script`, e a função `inicializaGrafico()`/`<canvas id="dcr-grafico-evolucao">`) foram removidos do template, e `evolucao`/o loop que a montava a partir de `dados.historico_completo` saiu de `dashboard()` em `views.py` — `_contabil_monta_historico_completo()` continua existindo, só não alimenta mais esse gráfico (ver "Depreciação/Amortização" acima, seu outro consumidor). A variável de controle que só existia pra adiar a inicialização do gráfico (`graficoInicializado`) virou `cardsInicializados`, já que sobrou só a contagem animada dos cards de indicador pra adiar.
|
||||
O miolo `<svg>` de cada um dos 11 ícones existe em `_CONTABIL_ICONES_SVG` (`views.py`, montado em `card["icone_svg"]` via `mark_safe()`) **e** em `PID_DC_INDICADOR_ICONES` (`dashboard-contabil.js`, desenha a grade do seletor). Duplicação proposital: o relatório é HTML servido pelo Django (sem acesso ao JS do app) e o modal de cadastro é JS sobre uma `TemplateView` sem contexto de servidor — não há fonte única sem inventar mais uma ida ao backend. **Editar ou adicionar um ícone exige mexer nos dois lugares.**
|
||||
|
||||
`portal_api/templatetags/contabil_extras.py` — **primeiro uso de template tags customizadas no projeto** (precisou de `portal_api/templatetags/__init__.py`, auto-descoberto pelo Django por `portal_api` já estar em `INSTALLED_APPS`). Filtros `moeda`/`percentual`/`indice`/`competencia`, todos no padrão brasileiro (separador de milhar `.`, decimal `,`) — mesmo espírito do helper `_moeda()` que já existe, duplicado por arquivo, nos dois geradores de PDF (`indicadores/recibo.py`, `custo_contratacao/pdf.py`), mas como filtro reaproveitável, já que este template tem tabelas inteiras de valores monetários (Balancete/DRE), não um valor por vez. `None` sempre vira "—", nunca "R$ 0,00"/"0,00%" — ver acima por quê. Um quinto filtro, `numero_bruto`, existe só pra alimentar a animação de contagem dos cards (ver "Visual" abaixo) — devolve o valor cru (`str(float(valor))`, `""` se `None`) exclusivamente para um atributo `data-count`, nunca pro texto exibido.
|
||||
### Riscos e calibragens herdadas
|
||||
|
||||
### Visual: fontes, animações e contagem animada dos cards
|
||||
- **Os códigos de classificação são fixos e calibrados contra um balancete de referência.** Ativo `"1"`, Ativo Circulante `"1.01"`, Estoques `"1.01.08"`, Imobilizado `"1.02.05"`, Depreciação Acumulada `"1.02.05.007"`, Passivo `"2"`, Passivo Circulante `"2.01"`, PL `"2.04"`. **Se um cliente usar numeração de plano de contas diferente, os indicadores dele saem errados silenciosamente.** O Passivo Não Circulante não tem código calibrado (não aparece no balancete de referência) e é calculado **por eliminação** (Passivo Total − Passivo Circulante − PL), sempre exato pela identidade contábil.
|
||||
- **Kanitz não foi validado** contra o BI antigo que esta ferramenta substitui. Usa a fórmula-livro-texto padrão (`0,05×ROE + 1,65×LiqGeral + 3,55×LiqSeca − 1,06×LiqCorrente − 0,33×GrauEndiv`), mas um valor visto numa captura do usuário ("11,31") está fora da faixa clássica (−7 a +7), sugerindo escala diferente no BI antigo. O relatório marca o card como estimativa.
|
||||
- **Liquidez Geral é aproximada**, sem exemplo real para validar: trata o Realizável a Longo Prazo como 0, porque o Ativo Não Circulante (`"1.02"`) mistura Investimentos/Imobilizado com um eventual RLP sem separar.
|
||||
- **EBIT é calculado "de baixo para cima"** (`resultado_liquido − despesas_receitas_financeiras`), não pela definição-livro-texto "de cima para baixo" (Receita Líquida − Custos − Despesas Operacionais). A `formula_exibicao` documenta o que o código faz, não a definição conceitual. Os dois caminhos tendem a convergir num DRE bem formado, mas não foram provados equivalentes. A linha de despesas/receitas financeiras é casada por substring (`LINHA_DRE_DESPESAS_RECEITAS_FINANCEIRAS` em `parser.py`), **não validada** contra PDF real.
|
||||
- **EBITDA e Depreciação do mês ficam indisponíveis na primeira apuração de uma empresa** (dependem da variação do saldo de depreciação acumulada contra a apuração anterior).
|
||||
- **Grau de Endividamento e IPL não multiplicam por 100 na fórmula** — o texto original do glossário do usuário dizia "÷ (PL × 100)", que é só a forma de dizer "o resultado vira %". O cálculo é `A ÷ B`, exibido pelo filtro `percentual`.
|
||||
|
||||
Pedido explícito do usuário — "deixar mais bonito, complexo, dinâmico, com animações fluidas". Fontes "Manrope" (títulos/cards/abas) + "Inter" (corpo, `font-variant-numeric: tabular-nums` nas colunas de valor) via Google Fonts. Paleta estendida da mesma família roxo/dourado já usada nos documentos gerados, com gradiente + glow radial sutil no cabeçalho (`.dcr-header::before`, `@keyframes dcrGlow`) e no fundo da página.
|
||||
> **`dashboard_contabil/indicadores.py` está sem consumidor em produção e é mantido de propósito.** `calcula_indicadores()`/`IndicadoresFinanceiros`/`_saldo()`/`_divide()`/`CODIGO_*` continuam lá mesmo sem chamador (a wrapper `_contabil_calcula_indicadores()` em `views.py` existe e não é mais invocada). É a **única exceção neste projeto à convenção de apagar código sem uso**, justificada pelo risco de indicador financeiro: se um valor um dia parecer suspeito, dá para recalcular pelo caminho antigo e comparar. Antes da migração para o banco, um dry-run comparou os 11 indicadores calculados pelos dois caminhos contra a mesma apuração real e **bateram exatos até a vigésima casa decimal**, incluindo o `None` do EBITDA. Revisar se ainda vale manter depois de a migração provar estabilidade por um tempo.
|
||||
|
||||
**Regra de ouro pra toda animação de entrada aqui**: nunca fixar `opacity:0`/`transform` como estilo estático fora de um `@keyframes` — só via `animation: nome duração easing both;`. Isso garante que `@media print { * { animation: none !important; } }` sozinho já basta pra devolver o elemento ao estado normal (visível, posição natural) na impressão, sem precisar de um reset explícito por seletor — se alguma animação nova for adicionada aqui, seguir essa mesma disciplina, senão a impressão pode sair com conteúdo em branco. O restante do `@media print` já existente (abas somem, todas as 3 aparecem juntas, linhas recolhidas forçadas a aparecer) continua igual.
|
||||
## API
|
||||
|
||||
Cards ganharam `data-count`/`data-final`/`data-format`/`data-color-rule` (ver `pidDcrAnimaContadores()`): a contagem anima de 0 até o valor com `requestAnimationFrame`/easing, formatando os quadros intermediários com `Intl`/`toLocaleString("pt-BR", ...)` nativo do navegador — mas o texto **final** escrito ao fim da animação é sempre `data-final`, a mesma string que os filtros Django já geraram (nunca um valor recalculado em JS, só decoração da transição). Cor por sinal/threshold só onde é seguro sem inventar limiar nenhum: `data-color-rule="sign"` (ROA/ROE/EBIT/EBITDA — positivo verde, negativo vermelho, convenção universal) e `data-color-rule="liquidez"` (Liquidez Corrente/Seca/Geral — verde se ≥ 1, âmbar/vermelho se < 1, também convenção padrão de mercado). Kanitz, Composição/Grau de Endividamento e IPL **não** ganham cor nenhuma — não existe um limiar validado pra eles nesta implementação (ver "Fórmulas" acima), então colorir feito "bom"/"ruim" daria uma falsa segurança num número que o próprio card já avisa ser estimativa/aproximação.
|
||||
Todos os endpoints usam `PermissaoApp("relatorios", "dashboard-contabil")`. Nenhum deles aparece na tabela de API do `CLAUDE.md` da raiz de propósito — endpoint de aplicação mora na doc da aplicação.
|
||||
|
||||
**Cards e gráfico só existem/animam depois da aba "Indicadores" ser aberta pela primeira vez** (mesmo motivo do gráfico: canvas com tamanho zero não desenha certo, e contar um número invisível não faz sentido). Isso criou um risco real de impressão: se o usuário nunca abrir essa aba e mandar imprimir direto, a contagem começaria do zero bem na hora que o navegador captura a página. `pidDcrAnimaContadores(true)` (parâmetro `instantaneo`) resolve isso — o único listener de `beforeprint` do documento decide entre inicializar tudo já no valor final (se a aba nunca foi aberta) ou só finalizar uma contagem já em andamento (`finalizadores`, um array de callbacks que força cada card pro texto final) — nunca as duas coisas competindo (era um bug real de uma versão intermediária desta mesma rodada, corrigido antes do usuário testar: dois listeners de `beforeprint` separados podiam disparar uma animação nova bem na hora de imprimir, sem tempo de terminar).
|
||||
| Endpoint | Método | Uso |
|
||||
|---|---|---|
|
||||
| `/api/contabil-apuracoes/` | GET/POST | histórico + criação (multipart, um PDF; empresa/competência vêm do arquivo) |
|
||||
| `/api/contabil-apuracoes/{id}/` | GET/DELETE | detalhe (contas + linhas DRE + Análise Vertical + achados de uma vez) / excluir. **DELETE é bloqueado (400) em apuração concluída** |
|
||||
| `/api/contabil-apuracoes/{id}/reprocessar/` | POST | multipart, PDF novo da mesma empresa+competência |
|
||||
| `/api/contabil-apuracoes/{id}/concluir/` | POST | fecha a análise (trava edições) |
|
||||
| `/api/contabil-apuracoes/{id}/observacoes/` | GET | recorte de vigência das `ContabilObservacao`, já com `historica`/`encerrada` calculados contra a competência desta apuração |
|
||||
| `/api/contabil-apuracoes/{id}/dashboard/` | GET | **o relatório HTML do cliente** (ver por que é GET, abaixo) |
|
||||
| `/api/contabil-apuracoes/{id}/indicadores/` | GET | `{indicadores, indicadores_ocultos, metadados}` para a aba "Dashboard" |
|
||||
| `/api/contabil-apuracoes/{id}/indicadores-ocultos/` | POST | substitui a lista inteira de chaves ocultas |
|
||||
| `/api/contabil-apuracoes/{id}/indicadores-selecionados/` | POST | idem, para indicador não padrão ativado nesta apuração |
|
||||
| `/api/contabil-apuracoes/{id}/resumo-fechamento/` | POST | texto rico do contador (sanitizado por `nh3`) |
|
||||
| `/api/contabil-apuracoes/{id}/pre-visualizar-indicador/` | POST | calcula um indicador **ainda não salvo** contra esta apuração, sem persistir nada |
|
||||
| `/api/contabil-apuracoes/{id}/exportar-xlsx/?parte=balancete\|dre` | GET | planilha da tabela |
|
||||
| `/api/contabil-apuracoes/{id}/resumo-pdf/` | GET | PDF avulso da aba Resumo |
|
||||
| `/api/contabil-contas/{id}/` | GET/PATCH | `validado` (e demais campos graváveis) de uma conta |
|
||||
| `/api/contabil-linhas-dre/{id}/` | GET/PATCH | idem, linha da DRE |
|
||||
| `/api/contabil-linhas-analise-vertical/{id}/` | GET/PATCH | idem, linha da Análise Vertical |
|
||||
| `/api/contabil-observacoes/` | POST | cria (recebe `apuracao` + `alvo_tipo` + `alvo_id`; empresa, competência, chave e rótulo são derivados no servidor — alvo de outra apuração é 400) |
|
||||
| `/api/contabil-observacoes/{id}/` | PATCH/DELETE | `texto` e/ou `mostrar_ao_cliente`, cada um com sua regra / exclusão restrita ao autor e à revisão |
|
||||
| `/api/contabil-observacoes/{id}/encerrar/`, `/reativar/` | POST | recebem a apuração aberta no corpo, que define a competência de corte |
|
||||
| `/api/contabil-achados/{id}/` | GET/PATCH | tratar/ignorar (exige `observacao_contador` não vazia) |
|
||||
| `/api/contabil-achados/{id}/alternar-oculto/` | POST | esconder a observação do achado no relatório, **independente da tratativa** (um achado pode continuar pendente e ter a observação escondida) |
|
||||
| `/api/contabil-indicadores-definicoes/`, `/{id}/` | GET/POST/PATCH/DELETE | CRUD de indicador (corpo sempre o indicador **inteiro**, inclusive em PATCH) |
|
||||
|
||||
Abas ganharam um indicador deslizante (`.dcr-tabs__indicator`, `getBoundingClientRect`-like via `offsetLeft`/`offsetWidth`, recalculado no `resize` e no `load` — texto de fonte customizada pode mudar a largura da aba depois do primeiro paint). Árvore recolhível do Balancete/D.R.E. ganhou um fade rápido só nas linhas que acabaram de aparecer (`.dcr-row-in`, reflow forçado via `void tr.offsetWidth` pra poder reiniciar a animação a cada clique), não a tabela inteira — evita flicker num clique que só afeta um grupo pequeno.
|
||||
Notas de desenho:
|
||||
- **`ContabilApuracaoViewSet` não tem PATCH genérico** (`http_method_names` exclui `"patch"` de propósito). Toda edição de campo da apuração passa por uma `@action` dedicada que substitui aquele campo de uma vez, sempre guardada por `_contabil_garante_em_revisao()`.
|
||||
- **`ContabilObservacaoViewSet` não tem `list`/`retrieve`** de propósito: a leitura é sempre pelo recorte de vigência de uma apuração.
|
||||
- **`indicadores()` devolve `dataclasses.asdict(...)` puro**, sem passar por `Serializer` — os `Decimal`/`None` já chegam certos no JSON porque o `JSONRenderer` do DRF aplica seu encoder recursivamente em qualquer estrutura de resposta, não só em campo de `Serializer`.
|
||||
- `get_queryset()` faz `prefetch_related` de `achados__conta`, `achados__linha_analise_vertical`, `reprocessamentos__reprocessado_por` (este só em `list`) e `edicoes__editado_por` nas observações. **`total_achados_pendentes` ainda gera N+1 na listagem** — conhecido, não corrigido.
|
||||
|
||||
### Frontend (`static/js/dashboard-contabil.js`)
|
||||
## Frontend — tela de revisão (`dashboard-contabil.html` / `.js` / `.css`)
|
||||
|
||||
`pidGerarDashboardContabil(id)` é só `window.open(`/api/contabil-apuracoes/${id}/dashboard/`, "_blank")` — navegação direta, sem `fetch`/blob/CSRF nenhum (ver por que isso importa em "É GET, não POST" acima). Bem mais simples do que o padrão de `pidGerarArquivoPlanoSaude` (`importacao-plano-saude.js`), que precisa de fetch manual porque `/gerar/` ali é POST e devolve um arquivo pra download, não uma página pra navegar.
|
||||
Três sub-views no padrão de `indicador-desempenho.html`: `#dc-list-view` (histórico + "Nova Análise") / `#dc-form-view` (upload de um PDF, sem campo de competência) / `#dc-review-view` (abas via `.pa-tabs`/`.pa-tab-panel` de `perfis-acesso.css`).
|
||||
|
||||
### Exportação em XLSX (`exportacao.py`)
|
||||
Abas da revisão: **Observações** (os achados) / **Balancete** / **D.R.E.** / **Análise Vertical** (só quando `analise_vertical_meses` não está vazio) / **Dashboard**.
|
||||
|
||||
Pedido explícito do usuário: um botão "Exportar XLSX" dentro do relatório "Gerar Dashboard", um por seção (Balancete/D.R.E.), pra baixar aquela tabela em planilha. `ContabilApuracaoViewSet.exportar_xlsx()` (`GET /api/contabil-apuracoes/{id}/exportar-xlsx/?parte=balancete` ou `?parte=dre`, mesma permissão de toggle único das outras actions) monta as linhas já com o nível de indentação calculado (mesma fórmula de `dashboard()`: `conta.codigo.count(".")` pro Balancete, `max(0, linha.nivel)` pra DRE) e chama `dashboard_contabil.exportacao.gera_xlsx_balancete()`/`gera_xlsx_dre()` — funções puras (openpyxl, sem tocar no ORM, mesmo espírito de `indicadores.py`/`regras.py`) que recebem dataclasses (`LinhaBalanceteXlsx`/`LinhaDreXlsx`) já prontas, não os models do Django.
|
||||
> Se a apuração aberta não tem Análise Vertical mas essa aba estava ativa (o contador vinha de outra apuração), `renderRevisao()` força a volta para "Observações" — senão sobraria um painel visível com o botão de aba escondido.
|
||||
|
||||
Cada planilha nasce com cabeçalho (título/empresa/CNPJ/competência, linhas 1-3, mescladas), uma linha de cabeçalho de colunas com fundo roxo (`#3d2178`, mesma paleta dos outros documentos gerados pelo escritório — `indicadores/recibo.py`/`custo_contratacao/pdf.py`, nunca a marca "P.I.D." do Portal) e a tabela de dados a partir da linha 6, com `freeze_panes` logo abaixo do cabeçalho de colunas. Conta sintética (Balancete, `tipo="S"`) e linha totalizadora (DRE, `totalizador=True`) ganham negrito + um fundo dourado claro (`#f6ecd4`), mesmo destaque visual que essas linhas já têm na tela de revisão e no relatório HTML. A hierarquia (nível de indentação) vira `Alignment(indent=nivel)` na célula de descrição — não dá pra reproduzir o toggle recolher/expandir de uma planilha, então a árvore sempre nasce "totalmente expandida" (mesmo espírito do relatório HTML impresso). Colunas monetárias usam `number_format = '"R$" #,##0.00'` (valor gravado como `float`, não como texto formatado — continua editável/somável no Excel).
|
||||
### As três árvores
|
||||
|
||||
Botão "Exportar XLSX" (`.dcr-export-btn`, `dashboard-contabil-relatorio.html`) é um `<a href="/api/contabil-apuracoes/{{ apuracao.id }}/exportar-xlsx/?parte=...">` puro — sem JS nenhum, mesmo espírito de link direto de download; o browser já lida com o `Content-Disposition: attachment` da resposta. Escondido em `@media print` junto do botão "Imprimir" (`.dcr-print-btn`), já que exportar não faz sentido numa versão impressa.
|
||||
Balancete, D.R.E. e Análise Vertical usam **o mesmo algoritmo de árvore recolhível**. O Balancete deriva o nível do código de classificação (`dcContaNivel()`, conta segmentos separados por `.`); as outras duas já recebem `nivel` pronto do backend. "Tem filhos" é sempre "a próxima linha tem nível maior". As funções genéricas (`dcColapsoPadrao`/`dcUltimaLevaVisivel`/`dcFilhosDiretos`/`dcDescendentes`/`dcEstadoValidacaoGrupo`/`dcValidadoInfo`) recebem `itens`/`nivelFn` como parâmetro e servem as três sem alteração. Estado de colapso é independente por aba (`dcContasColapsadas`/`dcDreColapsadas`/`dcAvColapsadas`).
|
||||
|
||||
### Exportação em PDF do Resumo (`resumo_pdf.py`, rodada seguinte)
|
||||
O cabeçalho da Análise Vertical é montado em JS (`renderAnaliseVerticalHead()`) porque o número de colunas varia com os meses; nas outras duas é HTML fixo. Os listeners são delegados no `<thead>`, então sobrevivem ao `innerHTML` ser refeito.
|
||||
|
||||
Pedido explícito do usuário: um "Exportar PDF" dentro da aba **Resumo** do relatório "Gerar Dashboard", pra baixar Resumo do Fechamento + Indicadores + Observações num documento avulso — sem o resto do relatório (Balancete/D.R.E./Análise Vertical, que já têm seu próprio caminho de exportação em XLSX). `ContabilApuracaoViewSet.resumo_pdf()` (`GET /api/contabil-apuracoes/{id}/resumo-pdf/`, mesma permissão de toggle único, mesmo `GET`-não-`POST` de `dashboard()` — abre via `window.open()`/link direto, não fetch+blob) chama `_contabil_dados_resumo(apuracao)` (a mesma função que `dashboard()` usa pra montar `indicadores_grupos`/observações da aba Resumo — extraída numa refatoração desta mesma rodada, ver função em `views.py`) e `dashboard_contabil.resumo_pdf.gera_pdf_resumo()`.
|
||||
**As tabelas nascem com tudo expandido.** `renderRevisao()` reinicia os três `Set()` de colapso vazios. A visão compacta virou ação sob demanda (ver o botão abaixo).
|
||||
|
||||
**`_contabil_dados_resumo(apuracao)`** (`views.py`) — extraído de dentro de `dashboard()` pra ser reaproveitado pelos dois: calcula `indicadores_grupos` (mesmo caminho de sempre — `_contabil_coleta_dados_indicadores`/`_contabil_calcula_indicadores_personalizados`/`_contabil_monta_cards_indicadores`/`_contabil_agrupa_indicadores_cards`) e devolve `observacoes_visiveis` **sem** separar por `alvo_tipo` nem calcular `ancora` (isso é específico do relatório HTML, que separa em 3 listas e pendura `ancora` pra permitir o clique-até-a-conta — ver rodada anterior) — o PDF só lista todas juntas, na mesma ordem/agrupamento de "Todas as Observações da Análise". `dashboard()` chama essa função e continua fazendo, por cima, a separação por tipo + `_com_ancora()` que só ele precisa.
|
||||
**`dcColapsoPadrao(itens, nivelFn)`** é a visão compacta: popula o `Set` com todo item que tem filhos **e** está no nível `PID_DC_NIVEL_ABERTO_PADRAO` (`2`) ou além. Não é padrão de abertura aqui, mas **continua sendo o padrão do relatório do cliente** (que é server-side). `PID_DC_NIVEL_ABERTO_PADRAO` (JS) e `_CONTABIL_NIVEL_ABERTO_PADRAO` (`views.py`) são a mesma constante conceitual duplicada nos dois lados — mudar o limiar exige ajustar os dois.
|
||||
|
||||
**`resumo_pdf.py`** (pacote puro, sem ORM — recebe a `ContabilApuracao` já carregada e o dict de `_contabil_dados_resumo()`, mesmo espírito de `indicadores/recibo.py`/`custo_contratacao/pdf.py`): banner roxo/dourado com `logo-branco.png` (identidade do escritório, nunca "P.I.D." — ver "Logos" no CLAUDE.md raiz), replicando a paleta do próprio relatório HTML (`#3d2178`/`#281552`/`#b4872a`). Três seções, na mesma ordem da aba Resumo:
|
||||
- **Resumo do Fechamento**: o texto rico do contador (`apuracao.resumo_fechamento`, já sanitizado por `nh3` — allowlist fechado `RICHTEXT_ALLOWED_TAGS` em `serializers.py`: `p`/`br`/`div`/`b`/`strong`/`i`/`em`/`u`/`ul`/`ol`/`li`/`img`) convertido em flowables do reportlab por `_resumo_fechamento_flowables()` (BeautifulSoup, já em `requirements.txt` como dependência transitiva de outra ferramenta — não precisou adicionar nada novo). `_inline_markup()` reconstrói `b`/`i`/`u`/`br` aninhados na marcação nativa que o `Paragraph` do reportlab já entende (`<b>`/`<i>`/`<u>`/`<br/>`), `strong`→`b`/`em`→`i`; `ul`/`ol` viram `ListFlowable`; `<img src="data:image/...">` (a única forma de imagem que o editor produz, colar/arrastar arquivo) vira um `Image` decodificado de base64 direto em memória, redimensionado pra caber na largura útil da página. Seção inteira pulada se `resumo_fechamento` estiver vazio.
|
||||
- **Indicadores**: uma `Table` por grupo (`_tabela_indicadores()`, mesmo padrão de "seção = uma Table com barra de título" de `_tabela_secao()` em `custo_contratacao/pdf.py`) — nome/valor formatado de cada card, sem fórmula/descrição/ícone (esses só fazem sentido no verso do flip card da tela, não num PDF estático).
|
||||
- **Observações da Análise**: todo `ContabilObservacao` visível + `achados_com_observacao`, cada um com o mesmo prefixo de origem do relatório HTML ("Balancete — ...", "D.R.E. — ...", "Análise Vertical — ...", "Auditoria — ..."), texto e assinatura. "Nenhuma observação registrada nesta análise." quando vazio (mesmo texto do relatório HTML).
|
||||
### Cabeçalho: dois botões por tabela, em pontas opostas
|
||||
|
||||
Botão (`<a class="dcr-export-btn" href="/api/contabil-apuracoes/{{ apuracao.id }}/resumo-pdf/">Exportar PDF</a>`, `.dcr-resumo-toolbar` — uma barra dedicada no topo da aba Resumo, já que a aba tem várias seções sem um único `<h2>` fixo pra pendurar o botão, diferente de Balancete/D.R.E./Análise Vertical) — escondido em `@media print` (reaproveita a regra já existente de `.dcr-export-btn`, só precisou esconder o wrapper `.dcr-resumo-toolbar` também, pra não sobrar um espaço vazio).
|
||||
- **`.dc-recolher-grupos-btn`** (`data-dc-recolher-grupos="balancete|dre|analise-vertical"`), na primeira coluna, dentro de `.dc-th-linha.dc-th-linha--inicio` (modificador que só troca o `justify-content`, já que este botão vem **antes** do rótulo). É um **toggle de duas faces**: mostra "−" e aplica `dcColapsoPadrao()` enquanto nada está recolhido; mostra "+" e zera o `Set` assim que existe qualquer grupo recolhido. A face sai de `dcAtualizaBotaoArvore()`, chamada no fim de cada `render*()`, e é **derivada de `colapsadas.size`**, não de um flag próprio — então continua correta quando o contador recolhe uma linha pelo toggle dela, sem passar pelo cabeçalho.
|
||||
- **`.dc-reset-formatacao-btn`** ("Restaurar formatação"), colado à direita na coluna "Observação". Faz o mesmo que a face "+". Ficou redundante e foi mantido de propósito, por ser a "borracha" que o usuário já conhece de outras telas do Portal.
|
||||
|
||||
Validado com `Client.force_login()` contra uma apuração real já em produção (leitura, nada escrito) e com testes unitários isolados de `_resumo_fechamento_flowables()`/`gera_pdf_resumo()` (sem tocar no banco) cobrindo negrito/itálico/lista/imagem embutida, resumo vazio e zero observações — os três casos renderizam um PDF válido (`pdfplumber` confirmou o texto esperado em cada página).
|
||||
**Ponto não-óbvio: os dois zeram `dc*Expandidos` em vez de populá-lo com todos os grupos.** Com o conjunto vazio, `dcDestaqueGrupos()` cai na leva padrão, que sem nada recolhido marca exatamente as **folhas**. Popular `dc*Expandidos` com todos os grupos destacaria quase toda linha da tabela, e com tudo destacado nada se destaca.
|
||||
|
||||
### Aba "Dashboard" na tela de revisão — ocultar cards/observações do relatório antes de gerar
|
||||
### Destaque de linha
|
||||
|
||||
Pedido explícito do usuário: uma 4ª aba na tela de revisão (`dashboard-contabil.html`, depois de Observações/Balancete/DRE, `data-dc-tab="dashboard"`) mostrando **antes de gerar o relatório** os mesmos 11 cards de indicador e a mesma lista de observações (contas + linhas de DRE + achados com `observacao_contador`) que vão pro relatório "Gerar Dashboard", com um botão de olho em cada item pra escondê-lo do relatório final — sem apagar o dado em si (a conta/linha/achado continua normal no Balancete/DRE/Observações da revisão, só não entra na versão que vai pro administrador da empresa).
|
||||
Duas famílias de `Set()` por árvore (as 6 resetadas em `renderRevisao()` e no botão de restaurar): `dc*Expandidos` guarda os grupos que o contador abriu e continuam abertos; `dc*Destaque` guarda o resultado **derivado**, sempre recalculado por `dcDestaqueGrupos(itens, nivelFn, colapsadas, expandidos)`:
|
||||
|
||||
**Modelos** (`portal_api/models.py`): `ContabilConta.oculta_no_relatorio`/`ContabilLinhaDre.oculta_no_relatorio`/`ContabilAchado.oculto_no_relatorio` (`BooleanField`, default `False` — afeta só a seção "Observações" do relatório, nunca a linha em si no Balancete/DRE/Observações da revisão) e `ContabilApuracao.indicadores_ocultos` (`JSONField`, default `list` — lista de chaves de `IndicadoresFinanceiros`, ex. `["kanitz", "ipl"]`). Migração `0059_contabilachado_oculto_no_relatorio_and_more`.
|
||||
- `expandidos` vazio → vale a leva padrão, `dcUltimaLevaVisivel(itens, nivelFn, colapsadas)`.
|
||||
- `expandidos` com algo → o destaque é **só** a união de `dcFilhosDiretos()` de cada grupo aberto, **descartando a leva padrão por completo**.
|
||||
|
||||
**Chaves válidas de indicador** (`dashboard_contabil/indicadores.py`, `CHAVES_CARDS`/`CHAVE_CARDS_RESULTADO`/`CHAVE_CARDS_LIQUIDEZ`): as 11 chaves que viram card no relatório (`roa`/`roe`/`kanitz`/`ebit`/`ebitda`/`liquidez_corrente`/`liquidez_seca`/`liquidez_geral`/`composicao_endividamento`/`grau_endividamento`/`ipl`) — subconjunto dos ~20 campos de `IndicadoresFinanceiros` (os demais, ex. `ativo_total`/`estoques`, só alimentam fórmulas, nunca tiveram card próprio). Usada tanto pra validar o corpo de `indicadores_ocultos()` (`ContabilIndicadoresOcultosSerializer`, `ChoiceField` por chave) quanto pelos dois grupos do relatório.
|
||||
O handler de clique guarda `estavaColapsada` **antes** de mutar o `Set` de colapso; expandir adiciona o id a `expandidos`, recolher remove o id **e todo descendente dele** (senão o destaque apontaria para linhas agora invisíveis e a árvore nunca voltaria ao estado padrão). Puramente visual, sem persistência.
|
||||
|
||||
**Endpoints novos** (`views.py`):
|
||||
- `GET /api/contabil-apuracoes/{id}/indicadores/` (`ContabilApuracaoViewSet.indicadores()`) — `{indicadores: {...}, indicadores_ocultos: [...]}`; `indicadores` é `dataclasses.asdict(IndicadoresFinanceiros)` puro (não passa por um `Serializer` — os `Decimal`/`None` já chegam certos no JSON porque o `JSONRenderer` do DRF aplica seu `JSONEncoder` recursivamente em qualquer estrutura de resposta, não só em campo de `Serializer`). Reaproveita o cálculo de `dashboard()` via o helper novo `_contabil_calcula_indicadores(apuracao)` (extraído do que antes estava só dentro de `dashboard()`) — mesma fórmula, duas telas (pré-visualização + relatório final).
|
||||
- `POST /api/contabil-apuracoes/{id}/indicadores-ocultos/` (`.indicadores_ocultos()`) — substitui a lista **inteira** de chaves ocultas de uma vez (`ContabilIndicadoresOcultosSerializer`, corpo `{indicadores_ocultos: [...]}`) — o frontend já tem a lista atual (via GET acima), só alterna uma chave e reenvia tudo; mais simples que um endpoint de toggle por chave. Gate `_contabil_garante_em_revisao()`, mesmo de qualquer edição de observação/achado.
|
||||
- `POST /api/contabil-achados/{id}/alternar-oculto/` (`ContabilAchadoViewSet.alternar_oculto()`) — **não** reaproveita `update()`/`ContabilAchadoAjusteSerializer` (que exige `status` + `observacao_contador` não-vazia pra "tratar"/"ignorar"): ocultar do relatório é uma decisão independente de tratativa, um achado pode continuar "Pendente" e mesmo assim ter sua observação escondida caso um dia venha a ser preenchida sem mudar o status. Serializer próprio (`ContabilAchadoOcultoSerializer`, só `{oculto_no_relatorio: bool}`), gate igual. `http_method_names` do viewset ganhou `"post"` só por causa desta action (`GET`/`PATCH` continuam cobrindo o resto).
|
||||
- `ContabilConta`/`ContabilLinhaDre` **não** precisaram de action nova — `oculta_no_relatorio` só entrou nos `fields` de `ContabilContaSerializer`/`ContabilLinhaDreSerializer` (ao lado de `observacao`, já gravável) e o `PATCH` genérico que essas duas telas já expõem (`ContabilContaViewSet`/`ContabilLinhaDreViewSet`, sem "Ajuste" nenhum no meio) aceita o campo isolado, sem exigir os demais.
|
||||
> **Por que a leva padrão é descartada quando há grupo aberto, e não somada.** Somar os filhos revelados ao conjunto já existente parece o comportamento natural e **não funciona**: a leva padrão já marca praticamente toda linha de nível ≥ 2, então somar deixa a tabela inteira destacada. Confirmado simulando o algoritmo em Python contra contas reais. A regra correta é acumular **entre grupos abertos** (abrir um segundo grupo mantém o destaque do primeiro) mas ignorar a base padrão enquanto houver qualquer grupo aberto.
|
||||
|
||||
**`dashboard()` filtra pelo que está oculto** (`views.py`): `observacoes_contas`/`observacoes_dre`/`achados_com_observacao` no contexto do relatório ganharam `and not X.oculta_no_relatorio`/`oculto_no_relatorio`; `indicadores_ocultos` (a lista crua) e dois booleanos por grupo (`indicadores_grupo_resultado_visivel`/`indicadores_grupo_liquidez_visivel`, `any(chave not in ocultos ...)`) entram no contexto pro template. `dashboard-contabil-relatorio.html` envolve cada um dos 11 `.dcr-card` num `{% if "chave" not in indicadores_ocultos %}` (o operador `in`/`not in` do Django Template Language já faz teste de pertencimento numa lista direto, sem precisar de filtro customizado novo) e cada um dos 2 `<h2>+.dcr-cards` de grupo num `{% if indicadores_grupo_*_visivel %}` — evita um cabeçalho de seção "Indicadores de Resultado" sobrando sozinho, sem nenhum card embaixo, se o contador ocultar os 5 de uma vez. A exportação XLSX de Balancete/DRE **não** é afetada — não é "card de indicador" nem "observação", ficou fora do escopo desta rodada.
|
||||
**`dcUltimaLevaVisivel()` marca folha genuína, não só grupo colapsado.** Reconstrói a lista de linhas realmente visíveis dado o colapso e marca toda linha cuja **próxima linha visível não seja mais profunda que ela**. Isso cobre os dois casos com uma regra só: nada foi revelado abaixo dela, seja porque está colapsada ou porque é uma folha sem filho nenhum. Um critério baseado só no `Set` de colapso deixa de fora folhas de verdade (como `(-) SIMPLES NACIONAL` na DRE), que visualmente estão no mesmo nível de um grupo colapsado vizinho.
|
||||
|
||||
**Frontend** (`dashboard-contabil.js`): `dcIndicadoresAtual` (`{indicadores, indicadores_ocultos, metadados}`, ver "Banco de indicadores personalizados" abaixo pro terceiro campo — ou `null`) é buscado sob demanda só na primeira vez que a aba "Dashboard" é aberta depois de abrir/criar a apuração (`renderDashboardTab()`, chamado pelo handler de `#dc-tabs`; resetado pra `null` em `renderRevisao()` e após qualquer criação/edição/exclusão de indicador personalizado) — evita um cálculo/consulta extra ao histórico completo da empresa (`_contabil_monta_historico_completo`) toda vez que uma apuração é aberta, já que boa parte das revisões não chega a abrir essa aba. A lista de observações **não** tem fetch próprio — é derivada direto de `apuracaoAtual.contas`/`.linhas_dre`/`.achados`, já carregados no payload principal da apuração. Cada card/linha tem um botão de olho (`.dc-dash-toggle-btn`, reaproveita `.icon-btn` de `components.css` + ícone SVG de olho aberto/fechado inline, sem depender de `pid-icone-escuro.svg`) que chama a action correspondente e atualiza só o item local (sem re-buscar a apuração inteira); desabilitado (`disabled`) quando a apuração já está `concluida`, mesmo espírito de "não pode mais editar observações/achados" já aplicado ao textarea/botões dos outros dois modais desta ferramenta. `pidDcFormatIndicadorMoeda`/`Percentual`/`Indice` espelham os filtros `moeda`/`percentual`/`indice` de `contabil_extras.py` só que em JS — **`null`/`undefined` sempre vira "—", nunca "R$ 0,00"** (mesmo cuidado do relatório: em `IndicadoresFinanceiros`, `None` é "indisponível", ex. EBITDA sem apuração anterior, não zero) — não reaproveita `pidDcFormatMoeda()` já existente no arquivo, que trata `null` como `0` de propósito (usado só pra valores de conta/DRE, que nunca são `None`).
|
||||
**Cores** (`dashboard-contabil.css`): `--dc-destaque-bg` é o tom **mais escuro** (`--bg-canvas`) e `--dc-row-tint-bg` o mais claro (`--bg-surface-raised`) no tema escuro. `--dc-row-tint-bg` não pode ser `--card-bg-hover`, senão o hover das linhas não destacadas fica sem efeito visível. O tema claro usa a inversão oposta (não-destaque colorido, destaque em branco), também por pedido do usuário.
|
||||
|
||||
### Banco de indicadores personalizados — fórmulas, componentes e a aba "Dashboard"
|
||||
### Checkbox "validado" com tri-state
|
||||
|
||||
Pedido explícito do usuário, rodada seguinte à aba "Dashboard" acima: cada card de indicador devia ser "um indicador efetivo calculado a partir das contas contábeis" — o contador poder ver a fórmula de cada um (inclusive os 11 de sistema) e criar indicador **novo**, escolhendo contas do Balancete/linhas da DRE/variação entre apurações/outros indicadores já existentes como componentes da fórmula. Escopo alinhado por `AskUserQuestion` antes de implementar: (a) os 11 indicadores de sistema **não** foram migrados pra este banco nesta rodada — continuam com o cálculo Python fixo de sempre em `calcula_indicadores()`, risco zero de mudar silenciosamente um valor já calibrado contra balancete real; só ganharam metadados de exibição (descrição/fórmula em texto) pro botão "Ver fórmula"; (b) a fórmula de um indicador personalizado é uma expressão de verdade (não só "A ÷ B"), avaliada por um interpretador restrito, não um `eval()`; (c) "selecionar conta ou grupo de contas" significa marcar uma ou mais contas específicas do Balancete (checklist), não digitar um prefixo de código.
|
||||
Botão de check ao lado do de observação (`.dc-conta-validado-btn`, cor `--teal` quando marcado — deliberadamente não `--accent`, que já significa "observação preenchida" e é a cor de tema escolhida pelo usuário). Puramente informativo: não bloqueia conclusão, não afeta achado nem relatório.
|
||||
|
||||
**Modelos** (`portal_api/models.py`, migração `0060_indicadorcontabildefinicao_and_more`):
|
||||
- `IndicadorContabilDefinicao` — `chave` (`SlugField` único, sempre **derivada do `nome`** na criação, nunca aceita do cliente — mesmo espírito de `RegraCusteioPlanoSaude.nome`, ver `_gera_chave_indicador_contabil()` em `views.py`; imutável depois, já que pode estar referenciada em `ContabilApuracao.indicadores_ocultos` ou na fórmula de outro indicador), `nome`, `descricao` (texto simples, sem rich-text/imagens como `AjudaAplicacao` — não pedido, indicador é um card curto), `formula` (`CharField`, a expressão), `formato` (`moeda`/`percentual`/`indice`, mesmos 3 formatos dos indicadores de sistema), `criado_por`/`criado_em`/`atualizado_em`.
|
||||
- `IndicadorContabilComponente` (`related_name="componentes"`) — uma peça da fórmula, `chave` (identificador Python válido — `^[a-z][a-z0-9_]*$`, `RegexValidator`, **não** um `SlugField` comum porque vira nome de variável dentro da árvore `ast` do avaliador; diferente da `chave` da própria `Definicao`, que pode ter hífen à vontade) + `tipo` (`contas`/`linha_dre`/`variacao_conta`/`indicador`) + os 3 campos de referência, só um preenchido por vez conforme o `tipo`: `contas_codigos` (lista de `ContabilConta.codigo`, usado por `contas`/`variacao_conta`), `linhas_dre_descricoes` (lista de `ContabilLinhaDre.descricao` exata, usado por `linha_dre`), `indicador_referenciado` (chave de outro indicador — de sistema ou personalizado, usado por `indicador`). **Guardado por código/descrição, nunca por FK a uma linha de uma apuração específica** — a definição é genérica, reaplicada em qualquer apuração/empresa que o relatório for gerado; funciona quando a empresa usa o mesmo plano de contas, sai errado silenciosamente se não (mesmo risco já aceito pelos códigos fixos de `calcula_indicadores()`).
|
||||
Uma **folha** é um toggle simples. Uma **sintética** tem 3 estados, calculados a cada render a partir dos descendentes (todos, não só os diretos) via `dcEstadoValidacaoGrupo()`:
|
||||
|
||||
**Motor de fórmula** (`dashboard_contabil/formula.py`, função pura, mesmo espírito de `regras.py`): `avalia_formula(expressao, valores)` faz `ast.parse(expressao, mode="eval")` e anda pela árvore com um **allowlist** restrito — só `BinOp` (`+ - * /`), `UnaryOp` (`+ -`), número literal e `Name` (resolvido contra o dict `valores`, chave → `Decimal | None`); qualquer outro nó (chamada de função, atributo, `import`, comparação...) levanta `FormulaInvalidaError` antes de qualquer coisa ser executada — nunca `eval()`/`compile()` puro sobre um texto digitado pelo contador. Se qualquer nome referenciado valer `None` em qualquer ponto da árvore, ou a fórmula dividir por zero, o resultado inteiro é `None` — "indisponível" nunca vira 0, mesma disciplina de `IndicadoresFinanceiros`. `valida_formula(expressao, chaves_disponiveis)` roda a mesma árvore com valores fictícios (`1`) só pra validar sintaxe/nomes na hora de salvar a definição, sem se importar com o resultado numérico.
|
||||
- `"nenhum"` — nem ela nem nenhum descendente está validado.
|
||||
- `"parcial"` (`--gold`) — qualquer combinação intermediária, **inclusive** só a própria sintética marcada.
|
||||
- `"completo"` (`--teal`) — **todo descendente FOLHA** está validado, checado primeiro.
|
||||
|
||||
**Resolução de um indicador personalizado contra uma apuração** (`views.py`):
|
||||
- `_ContabilDadosIndicadores` (dataclass) + `_contabil_coleta_dados_indicadores(apuracao)` — junta `contas_atuais`/`dre_atual`/`resultado_liquido`/`historico_completo` (mesma query que já existia) e deriva `contas_anteriores` (`historico_completo[0].contas` se houver — só a apuração anterior imediata, mesma convenção de "Depreciação/Amortização do mês" em `calcula_indicadores()`). Reaproveitada tanto por `_contabil_calcula_indicadores()` (os 11 de sistema, sem mudança de comportamento, só recebe o dataclass em vez de recalcular tudo) quanto pelos personalizados abaixo — evita duas idas ao banco pelos mesmos dados quando `dashboard()`/`indicadores()` (que pedem os dois cálculos juntos) rodam.
|
||||
- `_contabil_resolve_componente_personalizado(componente, dados, valores)` — resolve **um** componente pro valor que entra na fórmula. `contas`/`variacao_conta` somam em **valor absoluto** (mesma convenção de `indicadores._saldo()`: Passivo/PL vêm negativos no relatório); `linha_dre` soma com o sinal já impresso (a DRE não segue essa convenção); `variacao_conta` é `soma_atual − soma_anterior` (ambas em valor absoluto), `None` se não houver apuração anterior; `indicador` só faz `valores.get(chave_referenciada)`.
|
||||
- `_contabil_calcula_indicadores_personalizados(dados, valores_sistema)` — resolve **todas** as definições cadastradas de uma vez. `valores_sistema` já chega só com as 11 chaves de `CHAVES_CARDS` (o que um componente `tipo="indicador"` pode referenciar de sistema — ver validação no serializer). **Iterativo, não uma ordem topológica "de verdade"**: a cada rodada, calcula todo indicador cujos componentes `tipo="indicador"` já têm valor conhecido (de sistema, ou personalizado já resolvido numa rodada anterior), repete até não sobrar progresso — cobre encadeamento entre indicadores personalizados (um referenciando o outro) sem precisar ordenar por dependência explicitamente. Um indicador cuja dependência nunca resolve (referência quebrada, ou **ciclo** entre dois personalizados — ex. A referencia B e B referencia A) fica com valor `None` pra sempre, sem lançar erro — uma fórmula mal configurada não pode derrubar o cálculo do relatório inteiro. Validado com um teste manual (rollback, sem persistir nada) cobrindo encadeamento de 2 níveis, referência a indicador de sistema e um ciclo de 2 — os três se comportam como descrito.
|
||||
- `_contabil_formata_indicador(valor, formato)` — só pros personalizados: o Django Template Language não permite escolher um filtro (`moeda`/`percentual`/`indice`) por nome vindo de uma variável, então o valor de um indicador personalizado chega **já formatado como texto** no contexto do relatório (`dashboard()`), reaproveitando as mesmas 3 funções de `contabil_extras.py` chamadas direto (um filtro de template continua sendo uma função Python comum, só registrada).
|
||||
> **O "completo" usa só `descendentesFolhas`, não `descendentes` completo.** Numa árvore de 3+ níveis (mãe → filha → netos), validar os netos direto sem clicar na "filha" intermediária nunca marca o campo `validado` dela no banco — só o estado visual dela é "completo", calculado por render. Checar todo `descendentes` na "mãe" fazia o grupo nunca fechar mesmo com todo neto validado. `descendentes` (todos) continua valendo para o "parcial": uma sintética marcada sozinha ainda deve sinalizar "em andamento" num ancestral.
|
||||
|
||||
**Endpoints novos** (`views.py`/`urls.py`, `IndicadorContabilDefinicaoViewSet`, `router.register("contabil-indicadores-definicoes", ...)`) — mesma permissão de toggle único do resto da ferramenta (`PermissaoApp("relatorios", "dashboard-contabil")`): **qualquer contador com acesso já pode criar/editar/excluir indicador personalizado, não é uma tela administrativa restrita ao perfil "Inovação"** (diferente do padrão de "Mais informações"/`AjudaAplicacao`, decisão deliberada — quem cria os indicadores aqui é o próprio contador usando a ferramenta, não a Integração e Inovação).
|
||||
- `POST`/`PATCH /api/contabil-indicadores-definicoes/` (`create()`/`partial_update()`) — corpo sempre o indicador **inteiro** (`IndicadorContabilDefinicaoInputSerializer`: nome/descrição/formula/formato/`componentes[]`), mesmo em PATCH — um indicador personalizado é pequeno o bastante (poucos componentes) pra não valer a pena editar incrementalmente; `_salva_componentes()` sempre **substitui todos** os componentes de uma vez (`delete()` + `bulk_create()`), nunca faz diff. Validação em duas camadas: `validate_componentes()` confere chave única por componente + campo de referência preenchido conforme o `tipo` + `indicador_referenciado` existente (`CHAVES_CARDS` ∪ chaves de outras definições, excluindo a própria ao editar); `validate()` chama `formula.valida_formula()` contra o conjunto de chaves dos componentes.
|
||||
- `DELETE /api/contabil-indicadores-definicoes/{id}/` (`perform_destroy()`) — bloqueia (`ValidationError`) se outra definição referencia esta pela fórmula (`tipo="indicador"`), listando os nomes dependentes — evitar deixar uma fórmula alheia quebrada silenciosamente. Depois de excluir, também limpa a chave de toda `ContabilApuracao.indicadores_selecionados`/`indicadores_ocultos` que a referenciava (bug real, rodada 106: sem essa limpeza a chave ficava órfã, e como `ContabilIndicadoresSelecionadosSerializer`/`ContabilIndicadoresOcultosSerializer` reenviam/validam a lista **inteira** a cada alternância de checkbox no hub, o usuário ficava travado sem conseguir alternar nenhum indicador na apuração afetada — não só o excluído). As duas validações também passaram a descartar chave inexistente silenciosamente em vez de rejeitar a lista inteira (rede de segurança pra referência órfã que já existia antes desta limpeza, já que é só estado de exibição, não dado auditado).
|
||||
- `GET /api/contabil-apuracoes/{id}/indicadores/` (`ContabilApuracaoViewSet.indicadores()`, estendido) — `indicadores` agora é `{**valores_sistema_cards, **valores_personalizados}` (as 11 chaves de sistema mais toda chave personalizada); ganhou `metadados` — dict chave → `{nome, grupo, formato, descricao, formula_texto, personalizado, definicao_id}`, uniforme pra indicador de sistema (`grupo`/`formato`/`descricao`/`formula_texto` vêm de `METADADOS_CARDS`, `personalizado=False`, `definicao_id=None`) ou personalizado (`grupo` sempre `"Indicadores Personalizados"`, resto vem da própria `IndicadorContabilDefinicao`, `personalizado=True`) — é o que alimenta o botão "Ver fórmula" e a decisão de que grupo cada card cai no frontend, sem precisar de uma segunda fonte de verdade lá.
|
||||
- `dashboard()` (relatório) ganhou `indicadores_personalizados` no contexto — lista pronta (`{chave, nome, valor_formatado}`, já filtrando `indicadores_ocultos` e já com o valor formatado via `_contabil_formata_indicador`) pro `{% for %}` genérico do template (ver abaixo), diferente dos 11 de sistema que continuam com um `.dcr-card` hardcoded cada.
|
||||
Ciclo de 3 cliques numa sintética (`dcClicarValidadoConta()`/`Linha()`/`Av()`), recalculando o estado a cada clique:
|
||||
1. `"nenhum"` → PATCH só na própria sintética → vira `"parcial"`.
|
||||
2. `"parcial"` → `pidConfirm("Deseja validar todas as contas deste grupo?")` → PATCH em lote de todo descendente ainda não validado → `"completo"`. Cancelado, nada muda.
|
||||
3. `"completo"` → PATCH em lote desmarcando tudo, **sem perguntar**.
|
||||
|
||||
**Metadados dos 11 indicadores de sistema** (`dashboard_contabil/indicadores.py`, `MetaIndicador`/`METADADOS_CARDS`) — textos-base fornecidos pelo usuário (glossário de KPI já em uso pelo escritório), **ajustados em dois pontos** pra bater com o que o código realmente calcula, não copiados ao pé da letra:
|
||||
- **Grau de Endividamento e IPL**: o texto original dizia "÷ (Patrimônio Líquido × 100)" — alinhado por pergunta que isso é só a forma de dizer "o resultado vira %", não uma divisão a mais; `calcula_indicadores()` já fazia (e continua fazendo) só `A ÷ B`, exibido com o filtro `percentual` (que multiplica por 100 pra exibição). `formula_texto` desses dois reflete o cálculo real (`... ÷ Patrimônio Líquido, exibido em %`), não o texto literal do usuário — mostrar "÷100" enganaria o contador que olhar o resultado e não bater a conta.
|
||||
- **EBIT**: o texto original ("Receita Líquida − Custos − Despesas Operacionais", a definição-livro-texto "de cima pra baixo") não é como o código calcula (`resultado_liquido − despesas_receitas_financeiras`, "de baixo pra cima", partindo do Lucro Líquido já apurado) — `formula_texto` documenta o que o código realmente faz, não a definição conceitual. Os dois caminhos tendem a convergir num DRE bem formado, mas não foram provados matematicamente equivalentes; se um dia esse EBIT for migrado pro banco de indicadores (fora de escopo desta rodada, ver acima), vale revisitar qual das duas fórmulas usar.
|
||||
- Os demais 9 batem com o texto do usuário sem ajuste.
|
||||
Cada PATCH é individual (não existe action de lote); o "lote" é `Promise.all` client-side, aceitável porque um grupo real tem no máximo algumas dezenas de contas. Todo caminho termina re-renderizando a tabela inteira — necessário porque o estado de uma sintética **ancestral** também pode ter mudado de cor. Efeito colateral aceito: um editor de observação aberto em outra linha perde texto ainda não salvo nesse recálculo.
|
||||
|
||||
**Frontend** (`dashboard-contabil.js`/`.html`/`.css`) — `PID_DC_DASH_GRUPOS_INDICADORES`/`PID_DC_DASH_INDICADOR_INFO` (constantes hardcoded da rodada anterior) foram **removidas**: `renderDashboardIndicadores()` agora agrupa as chaves de `dcIndicadoresAtual.metadados` pelo campo `.grupo` de cada uma (vem do servidor), só a ORDEM de exibição dos grupos continua fixa no frontend (`PID_DC_DASH_ORDEM_GRUPOS`, grupo desconhecido cai no fim) — único jeito de "Indicadores Personalizados" aparecer sem precisar hardcodar nada de novo aqui a cada indicador criado.
|
||||
- **Card é clicável** (fora dos botões) → abre `#dc-indicador-formula-modal` (`pidDcAbrirFormulaModal()`) mostrando nome/descrição/`formula_texto` de `metadados[chave]` — funciona igual pra indicador de sistema ou personalizado, já que os dois têm a mesma forma de metadado.
|
||||
- **Card de indicador personalizado ganha um botão extra** (lápis, `data-dc-dash-indicador-editar`) ao lado do olho — busca a definição completa (`GET /contabil-indicadores-definicoes/{id}/`, precisa dos `componentes` que `metadados` não carrega) e abre `#dc-indicador-modal` prefiltrada pra edição; indicador de sistema não tem esse botão (`meta.personalizado === false`).
|
||||
- **Modal "Novo/Editar Indicador"** (`#dc-indicador-modal`, `.modal-card--wide`): nome/descrição/formato/fórmula + uma lista dinâmica de componentes (`#dc-indicador-componentes`, `pidDcCriaLinhaComponente()`/`pidDcRenderComponentePicker()`). Cada linha tem chave+tipo (sempre presentes) e um "picker" que muda conforme o `tipo` escolhido: `contas`/`variacao_conta` e `linha_dre` reaproveitam `.checklist-box`/`.checklist-item`/`.checklist-search` de `components.css` (mesmo componente visual já usado nos checklists de Perfis/Departamento em `usuarios.html`) — necessário porque uma apuração real tem ~100-150 contas/linhas de DRE, sem busca a lista seria inutilizável; `indicador` vira um `<select>` das chaves de `dcIndicadoresAtual.metadados` (exclui a própria chave, se estiver editando). O picker de cada linha é reconstruído (`pidDcRenderComponentePicker`) só quando o `<select>` de tipo muda (não a cada tecla digitada na busca, que só filtra via `hidden` nos itens já renderizados) — troca de tipo descarta a seleção anterior daquele componente, mesmo espírito de "começar do zero" ao mudar de tipo.
|
||||
- **Componente "órfão" ao editar de uma apuração diferente da original**: os pickers de `contas`/`linha_dre` são montados a partir de `apuracaoAtual` (a que está sendo revisada agora), mas um componente pode ter sido configurado a partir de **outra** apuração (outra empresa/competência) — se um código/descrição salvo não existe na apuração atual, ele não apareceria na lista pra ser marcado, e salvar sem tocar naquele componente **apagaria silenciosamente** essa referência (bug real, pego antes do usuário testar). Corrigido: todo código/descrição selecionado que não existe na apuração atual entra como um item extra no topo do checklist, já marcado e com uma nota "não encontrada nesta apuração, mantida" — continua salvo de volta do jeito que estava, a menos que o contador desmarque de propósito.
|
||||
- **Ao salvar, sempre reenvia `componentes` inteiro** (não um diff) — reflete o "substitui tudo" do backend (`_salva_componentes()`); ao concluir (criar/editar/excluir), `dcIndicadoresAtual = null` força `renderDashboardTab()` buscar tudo de novo (valores recalculados, metadados atualizados).
|
||||
- **Botão "Excluir" só aparece editando** (`#dc-indicador-modal-excluir-btn`, `hidden` na criação) — usa `pidConfirm({perigoso: true})`, nunca `window.confirm` (ver `[[feedback_popups_no_padrao_do_portal]]` na memória).
|
||||
### Observações: thread inline por linha
|
||||
|
||||
### Indicador padrão vs. não padrão + "Gerenciar Indicadores" + modal com scroll
|
||||
As observações são carregadas **à parte** da apuração (`dcCarregarObservacoes()`, ao abrir/criar uma análise) e indexadas por chave natural (`dcObsIndice`), porque não pertencem ao payload da apuração.
|
||||
|
||||
Pedido explícito do usuário, rodada seguinte: (a) o modal "Novo/Editar Indicador" não tinha scroll — com vários componentes, o conteúdo crescia pra fora da tela e só dava pra alcançar "Salvar" dando zoom out no navegador; (b) precisava de um jeito de **ver e editar** qualquer indicador já criado, não só os que já estão aparecendo nesta apuração; (c) indicador personalizado precisava poder ser **padrão** (aparece automaticamente em toda apuração, comportamento que já existia) ou **não padrão** (fica salvo/editável, mas só aparece numa apuração específica quando o contador seleciona ali).
|
||||
Clicar no ícone de observação abre uma `<tr class="dc-obs-edit-row">` extra logo abaixo da própria linha, dentro da mesma tabela — **não um modal**. Motivo: um clique acidental fora de um popup não deve descartar texto em digitação, e ver a conta ao lado da observação ajuda a não perder o contexto. O painel é uma thread (`dcObsPainelHtml()`/`dcObsItemHtml()`): histórico em cima (autor, data, competência de origem, selos "Histórico"/"Encerrada"/"Editada"/"Aparece ao cliente"/"Interna" e as ações de olho, ver edições, editar, encerrar/reativar, excluir), campo de observação nova embaixo.
|
||||
|
||||
**Modal com scroll** (`components.css`): `.modal-card` ganhou `max-height: calc(100vh - var(--space-5) * 2)` + `overflow-y: auto` — mudança global (toda tela que usa `.modal-card`), sem efeito em modal que já cabia na tela, e a rede de segurança que faltava pra qualquer modal futuro com conteúdo dinâmico. Adicionalmente, só a lista de componentes (`#dc-indicador-componentes`, classe `.dc-ind-componentes-lista`, `dashboard-contabil.css`) tem seu próprio `max-height:320px; overflow-y:auto` — rola só ela, não o modal inteiro, então nome/descrição/formato (cabeçalho) e fórmula/ações (rodapé) continuam alcançáveis sem precisar rolar duas vezes.
|
||||
Um handler único (`dcTrataCliqueObservacao()`) atende as 3 tabelas e as 4 listas de resumo — **a mesma observação pode estar visível em mais de um lugar ao mesmo tempo**, então cada mutação refaz o fetch e re-renderiza tudo (`dcRenderObservacoesTudo()`).
|
||||
|
||||
**Modelos**: `IndicadorContabilDefinicao.padrao` (`BooleanField`, default `True` — indicador já existente antes desta rodada continua aparecendo em toda apuração, sem regressão) e `ContabilApuracao.indicadores_selecionados` (`JSONField`, default `list` — chaves de indicador **não padrão** ativadas especificamente nesta apuração; um indicador padrão nunca precisa aparecer aqui). Migração `0061_contabilapuracao_indicadores_selecionados_and_more`.
|
||||
**O botão da coluna mostra sempre só o ícone, nunca o texto** (`PID_DC_OBSERVACAO_ICONE`, botão circular de 26px), com um contador (`.dc-obs-contador`, já que uma conta pode ter várias). A cor é o sinal:
|
||||
- `.dc-conta-observacao-btn--preenchida` (`--accent`) — esta linha tem observação própria.
|
||||
- `.dc-conta-observacao-btn--descendente` (`--gold`) — a linha é sintética, **não** tem observação própria, mas algum descendente tem. Serve para o contador ver que há observação dentro de um grupo recolhido sem precisar expandir. Clicar continua abrindo o painel da própria linha (que nasce vazio), é só sinal visual. Calculado por `dcTemObservacaoDescendente()`.
|
||||
|
||||
**Cálculo** (`views.py`, `_contabil_calcula_indicadores_personalizados()`): ganhou o parâmetro `indicadores_selecionados` — filtra `IndicadorContabilDefinicao.objects.all()` logo no início pra só os que têm `padrao=True` **ou** `chave in indicadores_selecionados`; o resto do algoritmo (resolução iterativa, ciclo vira `None`) não mudou. `dashboard()`/`indicadores()` passam `apuracao.indicadores_selecionados` adiante e também filtram por esse mesmo critério ao montar `indicadores_personalizados_cards`/`metadados` — um indicador não padrão não selecionado **não aparece em lugar nenhum** desta apuração (nem no relatório, nem na aba "Dashboard"), mas continua existindo/editável via a listagem completa (`GET /api/contabil-indicadores-definicoes/`, usada pelo hub — nunca filtrada por apuração, sempre lista tudo).
|
||||
Os chips "Todas / Visíveis ao cliente / Internas" (`dcObsFiltro`) existem nas quatro listas e **compartilham a mesma variável**: filtrar numa aba filtra em todas.
|
||||
|
||||
**Endpoint novo**: `POST /api/contabil-apuracoes/{id}/indicadores-selecionados/` (`ContabilApuracaoViewSet.indicadores_selecionados()`) — mesmo padrão de `indicadores_ocultos()` (substitui a lista inteira de uma vez), `ContabilIndicadoresSelecionadosSerializer` valida que toda chave enviada corresponde a uma definição com `padrao=False` (selecionar uma chave padrão não faz sentido, ela já aparece sempre — rejeitado com 400). `IndicadorContabilDefinicaoInputSerializer` ganhou `padrao` (opcional, default `True`) — `create()`/`partial_update()` gravam o campo.
|
||||
Cada aba termina com um resumo próprio (`.dc-obs-resumo`, via `renderContasObsResumo()` e irmãs, chamadas no fim de cada `render*()`, sempre em sincronia com a tabela) e a aba "Dashboard" tem a lista consolidada das três + auditoria. As duas coisas convivem de propósito: uma é a visão de uma aba, a outra é a visão consolidada antes de gerar o relatório.
|
||||
|
||||
**Frontend — "Gerenciar Indicadores"** (`dashboard-contabil.js`/`.html`): o botão que antes abria "Novo Indicador" direto virou `#dc-indicadores-gerenciar-btn`, abrindo um modal-hub novo (`#dc-indicadores-hub-modal`) com duas listas — "Padrão" e "Não padrão" (`GET /contabil-indicadores-definicoes/`, sempre a lista **completa**, sem recorte por apuração). Cada linha (`pidDcCriaLinhaHub()`): nome (clicável, abre "Ver fórmula"), botão de editar (lápis, abre `#dc-indicador-modal` — o mesmo form de sempre, agora empilhado por cima do hub via `.modal-overlay--top`, reaproveitando o mecanismo já usado por `confirm-modal.js`) e, só nas linhas "Não padrão", um checkbox "ativo nesta apuração" (`data-dc-hub-toggle`, chama `indicadores-selecionados/`). O botão "+ Novo Indicador" fica dentro do hub agora, não solto na aba — reflete o pedido do usuário ("selecionados... quando acessar o botão de novo indicador").
|
||||
- `pidDcAbrirFormulaModal()` deixou de receber uma chave (só resolvia contra `dcIndicadoresAtual.metadados`, que só lista indicador **ativo nesta apuração**) e passou a receber um objeto `{nome, descricao, formula_texto}` direto — necessário pro hub poder mostrar a fórmula de um indicador não padrão ainda não selecionado (que não está em `metadados`), buscando a definição completa (`GET .../{id}/`) na hora do clique.
|
||||
- Depois de criar/editar/excluir um indicador (`pidDcAposSalvarOuExcluirIndicador()`), além de recarregar os cards da aba "Dashboard" (`dcIndicadoresAtual = null` + `renderDashboardTab()`), também re-renderiza o hub se ele estiver aberto por trás — sem isso a lista do hub ficaria desatualizada até fechar e abrir de novo.
|
||||
### Aba "Observações" (achados)
|
||||
|
||||
### Migração dos 11 indicadores de sistema pro banco (rodada seguinte)
|
||||
Filtros por severidade, por status e por regra, combinados por E lógico. Como não há chip próprio para o filtro por regra, uma faixa (`#dc-regra-filtro-ativo`) aparece entre os chips e a lista, mostrando a regra ativa e um botão "Limpar" — sem ela não haveria como perceber por que a lista filtrou nem como sair.
|
||||
|
||||
Pedido explícito do usuário — reversão deliberada da decisão de escopo da rodada anterior ("Banco de indicadores personalizados"), que tinha deixado os 11 indicadores "de sistema" de fora do banco de propósito, pelo risco de mapear uma conta errado e mudar silenciosamente um valor já calibrado. Confirmado por `AskUserQuestion` (com o risco explicado antes) que o usuário queria a fórmula **de verdade** editável, não só o texto de exibição — então os 11 (`roa`/`roe`/`kanitz`/`ebit`/`ebitda`/`liquidez_corrente`/`liquidez_seca`/`liquidez_geral`/`composicao_endividamento`/`grau_endividamento`/`ipl`) viraram `IndicadorContabilDefinicao` de verdade, com as mesmas chaves de antes (preserva qualquer `indicadores_ocultos` já salvo). Não são mais um caso especial em lugar nenhum do código — CRUD, cálculo, exibição, tudo passa pelo mesmo caminho de qualquer indicador personalizado.
|
||||
Ordenação sempre por severidade (`PID_DC_SEVERIDADE_ORDEM = {alta:0, media:1, baixa:2}`), preservando a ordem original dentro da mesma severidade (`Array.prototype.sort` é estável).
|
||||
|
||||
**Novo tipo de componente: `resultado_liquido`** (`IndicadorContabilComponente.TIPO_RESULTADO_LIQUIDO`, sem nenhum campo de referência — nem `contas_codigos`, nem `linhas_dre_descricoes`, nem `indicador_referenciado`) — resolve sempre pra `dados.resultado_liquido` (a última linha da DRE, por **posição**, não por texto). Necessário porque ROA/ROE/EBIT precisam do resultado líquido do período, e a última linha da DRE **muda de rótulo** conforme o sinal do resultado — confirmado contra um balancete real (`792 - balancete 072026.pdf`... a mesma apuração de referência já usada em `regras.py`): uma empresa com prejuízo termina em "(=) PREJUÍZO LÍQUIDO DO EXERCÍCIO", uma com lucro terminaria em "(=) LUCRO LÍQUIDO DO EXERCÍCIO" — um componente `linha_dre` comum (que casa por texto exato) quebraria assim que o resultado de uma empresa virasse de sinal entre uma apuração e outra. Resolvido em `_contabil_resolve_componente_personalizado()` (`views.py`) com um `if` a mais; no frontend, `pidDcRenderComponentePicker()` mostra só um texto explicativo pra esse tipo (nada pra selecionar).
|
||||
**Resumo no topo** (`.dc-achados-resumo`): um donut em SVG puro com a contagem por severidade e o total no centro, mais uma grade de 3 cards por **grupo temático** (`PID_DC_GRUPOS`): "Divergências de Saldo" (regras 1-5), "Contas Atípicas" (6-8) e "Variações e Indicadores" (9). Cada card mostra o total do grupo e, por baixo, uma linha por regra (label + contagem); regra sem achado continua listada com `0`, só não clicável. `renderAchadosResumo()` conta sempre sobre `apuracaoAtual.achados` **completo**, nunca sobre a lista filtrada — é uma visão geral estável.
|
||||
|
||||
**Novo campo `IndicadorContabilDefinicao.grupo`** (`CharField`, default `"Indicadores Personalizados"`) — livre, só pra agrupar visualmente (mesmo conceito que já existia fixo no relatório antes da migração: "Indicadores de Resultado"/"Indicadores de Liquidez e Endividamento"). **Não exposto no formulário de criar/editar** (`IndicadorContabilDefinicaoInputSerializer` não aceita `grupo`) — só a migração de dados setou os dois grupos originais pros 11 migrados; um indicador criado pela tela sempre nasce em "Indicadores Personalizados" e não tem como mudar de grupo pela UI (poderia virar um campo editável numa rodada futura, se pedido). Migração `0062_indicadorcontabildefinicao_grupo_and_more` (schema) — os 11 registros em si foram inseridos por um script Python ad-hoc rodado uma única vez direto no banco de produção (não uma management command, não fica no repositório), não pelo endpoint da API.
|
||||
> Sem Chart.js aqui de propósito (essa dependência só existe no relatório estático). O donut usa a técnica clássica de `<circle r="15.9155">` — circunferência ≈ 100, então `stroke-dasharray`/`stroke-dashoffset` já funcionam em unidades de percentual sem `pathLength`. Cada segmento é um `<circle>` próprio, clicável individualmente porque `pointer-events` de SVG só considera pixel realmente pintado pelo `stroke`, sem hit-test manual por ângulo.
|
||||
|
||||
**Verificação antes de ir pra produção** (rigor extra por lidar com valor financeiro já exibido a cliente): antes de gravar qualquer coisa, um dry-run (mesmo script, dentro de uma `transaction.atomic()` com rollback forçado no final) criou os 11 registros, calculou os valores via `_contabil_calcula_indicadores_personalizados()` (motor novo) e comparou contra `dashboard_contabil.indicadores.calcula_indicadores()` (cálculo Python antigo) pra mesma apuração real (`id=4`, a única em produção nesta rodada) — **os 11 bateram exatos até a vigésima casa decimal** (inclusive o `None` do EBITDA, que não tem apuração anterior desta empresa). Só depois dessa conferência o script rodou de verdade (sem rollback). Testado de ponta a ponta em seguida contra a API/relatório reais (`GET /indicadores/`, `GET /dashboard/`, `PATCH` de edição incluindo um componente `resultado_liquido`) — tudo consistente.
|
||||
Donut, legenda e linhas de regra são clicáveis e filtram a lista abaixo, com `scrollIntoView` suave até ela (o resumo pode empurrar a lista para fora da tela). O cabeçalho do card de grupo **não** é clicável, só as linhas de regra dentro dele.
|
||||
|
||||
**`dashboard_contabil/indicadores.py` foi esvaziado, mas não apagado**: `CHAVES_CARDS`/`CHAVE_CARDS_RESULTADO`/`CHAVE_CARDS_LIQUIDEZ`/`MetaIndicador`/`METADADOS_CARDS` foram removidos (zero consumidor depois da migração — mantê-los seria texto duplicado e defasável em relação à `descricao`/`formula` reais do banco). `calcula_indicadores()`/`IndicadoresFinanceiros`/`_saldo()`/`_divide()`/`_busca_linha_por_trecho()`/`CODIGO_*` foram **deliberadamente mantidos**, mesmo sem nenhum chamador em produção (`ContabilApuracaoViewSet.dashboard()`/`.indicadores()` não chamam mais `_contabil_calcula_indicadores()` — a função wrapper em `views.py` continua existindo, só não é mais invocada) — é a única exceção neste projeto à convenção de apagar código sem uso, justificada pelo risco real de indicador financeiro: se um valor um dia parecer suspeito, dá pra recalcular pelo caminho antigo e comparar contra o banco sem precisar reconstruir a fórmula de cabeça. Revisar se ainda vale a pena manter numa rodada futura, depois que a migração provar estabilidade em produção por um tempo.
|
||||
**Cada card de achado** pode expandir a conta citada (`.dc-achado-card__toggle-conta`), resolvida procurando em `apuracaoAtual.contas` — sem chamada de API extra. As duas regras gerais não têm conta, então não mostram o toggle.
|
||||
|
||||
**Relatório "Gerar Dashboard" perdeu os 11 `.dcr-card` hardcoded** — `dashboard-contabil-relatorio.html` agora tem um único `{% for grupo in indicadores_grupos %}` genérico (mesmo conceito do `{% for indicador in indicadores_personalizados %}` que já existia pra indicador personalizado antes desta rodada, agora é o **único** caminho de renderização, sem duplicação). `_contabil_monta_cards_indicadores()`/`_contabil_agrupa_indicadores_cards()` (`views.py`) montam a lista já com tudo pronto: valor formatado, valor cru (`data-count`), formato pro contador animado (`data-format`), regra de cor e destaque dourado só pros 11 migrados (`_CONTABIL_INDICADOR_COR_REGRA`/`_CONTABIL_INDICADOR_DOURADO`/`_CONTABIL_INDICADOR_NOTA`, dicts chave-fixa em `views.py` — réplica visual exata do que esses 11 já tinham hardcoded no template antes; indicador personalizado criado depois não entra em nenhum desses dicts, nasce sem cor/destaque/nota, mesmo como já era). ~~**Perda real, aceita conscientemente**: os 11 cards tinham um ícone SVG próprio cada (gráfico de barras pro ROA, gota pra Liquidez Seca, etc.) — o loop genérico usa um ícone único pra todo indicador agora, não haveria como manter 11 ícones distintos sem mais uma tabela chave→SVG; mencionado ao usuário, não pedido de volta ainda.~~ Revertido numa rodada posterior — ver "Ícone selecionável + flip card no relatório" mais abaixo.
|
||||
**Botão "Ver na tabela"** (`PID_DC_ICON_LOCALIZAR`), condicionado a `achado.conta != null || achado.linha_analise_vertical != null`. `dcIrParaLinha(tipo, id)`:
|
||||
1. Acha o índice da linha no array certo (via `DC_IR_PARA_CONFIG`).
|
||||
2. Calcula só os **ancestrais** dela (`dcAncestraisIds()`) e tira só esses ids do `Set` de colapso — **não** a árvore inteira, preservando o resto do estado de expansão que o contador já montou.
|
||||
3. Marca `dcFocoLinha` e re-renderiza, aplicando `.dc-conta-row--foco` só na linha alvo.
|
||||
4. Clica programaticamente no botão da aba certa, reaproveitando o listener de troca de aba.
|
||||
5. Num `requestAnimationFrame` (depois do painel visível), `scrollIntoView({block:"center"})`.
|
||||
6. Um `setTimeout` de 2,4s limpa o foco e re-renderiza — o pulso nunca fica grudado.
|
||||
|
||||
### Dois ajustes de acabamento no construtor de fórmula (mesma rodada da migração)
|
||||
`.dc-conta-row--foco` anima `box-shadow`, **não `background-color`** (que já está em disputa entre o tingimento padrão e `.dc-conta-row--destaque`), então o pulso aparece por cima de qualquer estado que a linha já tenha.
|
||||
|
||||
**Scroll aninhado no construtor de componentes** (pedido explícito do usuário, com captura de tela mostrando a área minúscula): `.dc-ind-componentes-lista` tinha seu próprio `max-height:320px; overflow-y:auto` (ver "Indicador padrão vs. não padrão" acima) **por cima** do `.checklist-box` de cada componente (`max-height:160px`, já rolável) — dois scrolls aninhados deixavam a área útil tão pequena que nem um componente inteiro cabia sem rolar duas vezes. Removido o scroll de `.dc-ind-componentes-lista` (volta a crescer no fluxo normal do modal, que já rola inteiro via `.modal-card` de `components.css`) e aumentado `.dc-ind-comp-picker .checklist-box` de 160px pra 260px — sobra só **um** scroll aninhado (o checklist em si, genuinamente necessário pelas ~100-150 contas/linhas de uma apuração real), não mais dois.
|
||||
> **Não existe "ir para a DRE"** porque nenhuma regra hoje referencia `ContabilLinhaDre` diretamente. Se uma regra nova precisar, o padrão se replica sem reprojetar nada: FK `linha_dre` + `ordem_linha_dre` em `AchadoDetectado` + uma entrada em `DC_IR_PARA_CONFIG` com `tab: "dre"`.
|
||||
|
||||
**"Calcular com esta apuração"** (pedido explícito do usuário: conferir os valores buscados, não só ler a fórmula em texto) — novo botão no modal "Novo/Editar Indicador", entre "Fórmula" e as ações do rodapé, que chama `POST /api/contabil-apuracoes/{id}/pre-visualizar-indicador/` (`ContabilApuracaoViewSet.pre_visualizar_indicador()`) com o formulário **ainda não salvo** (nome/formato/fórmula/componentes tal como estão na tela) e mostra o valor de cada componente + o resultado final, calculados contra a apuração que está aberta na revisão — sem persistir nada, funciona tanto criando quanto editando. Reaproveita `IndicadorContabilDefinicaoInputSerializer` inteiro pra validar o corpo (inclusive a checagem de fórmula/componentes que criar/editar já fazem — `nome` é exigido pelo serializer mas não usado pra nada aqui, o frontend manda o que já estiver no campo, ou `"Pré-visualização"` se estiver vazio) e `_contabil_resolve_componente_personalizado()`/`avalia_formula()` (as mesmas funções do cálculo de verdade) sobre instâncias de `IndicadorContabilComponente` **nunca salvas** (só construídas em memória). Cada valor de componente é formatado com o filtro `indice` (número BR simples, sem R$/%, já que um componente pode ser qualquer grandeza — moeda, ratio de outro indicador, etc., não dá pra adivinhar o formato certo por componente); o resultado final usa o `formato` escolhido no formulário. Frontend: `pidDcColetaComponentesForm()` (extraída do handler de "Salvar", reaproveitada pelos dois) monta o payload; resposta renderizada em `#dc-indicador-preview` (`.dc-ind-preview`, `dashboard-contabil.css`), escondida sempre que o modal abre de novo (não carrega sozinho, só depois de clicar "Calcular" — evita mostrar um resultado desatualizado de uma edição anterior).
|
||||
### Aba "Dashboard"
|
||||
|
||||
### Nova consulta de histórico (`_contabil_monta_historico_completo`, `views.py`)
|
||||
Mostra, **antes de gerar o relatório**, os mesmos cards de indicador e a mesma lista de observações que vão para o relatório, com um botão de olho em cada item para escondê-lo do relatório final sem apagar o dado.
|
||||
|
||||
Diferente de `_contabil_monta_historico` (capada em 2 apurações anteriores, usada só pelas regras de auditoria no `create()`), esta busca **todo** o histórico da empresa — usada só pelo cálculo de Depreciação/Amortização do relatório (EBITDA); alimentava também o gráfico de evolução do Resultado Líquido, removido numa rodada seguinte (ver "Relatório 'Gerar Dashboard'" acima). Recebe `codigo_empresa`/`competencia_atual` diretamente (não um `CabecalhoExtraido`) porque roda contra uma `ContabilApuracao` já persistida, ao contrário de `_contabil_monta_historico` (que roda durante o `create()`, antes de qualquer coisa existir no banco).
|
||||
`dcIndicadoresAtual` é buscado sob demanda só na primeira vez que a aba é aberta (e resetado para `null` em `renderRevisao()` e após qualquer criação/edição/exclusão de indicador) — evita um cálculo e uma consulta ao histórico completo toda vez que uma apuração é aberta, já que boa parte das revisões não chega a abrir essa aba. A lista de observações **não** tem fetch próprio, é derivada do payload já carregado.
|
||||
|
||||
### Ícone selecionável + flip card no relatório (rodada seguinte)
|
||||
`renderDashboardIndicadores()` agrupa as chaves pelo campo `.grupo` que vem do servidor; só a **ordem** dos grupos é fixa no frontend (`PID_DC_DASH_ORDEM_GRUPOS`, grupo desconhecido cai no fim) — é o que faz "Indicadores Personalizados" aparecer sem hardcodar nada a cada indicador criado.
|
||||
|
||||
Pedido explícito do usuário, escopado por `AskUserQuestion` só pro relatório "Gerar Dashboard" (`dashboard-contabil-relatorio.html`) — a aba "Dashboard" da tela de revisão (`dashboard-contabil.js`/`.html`) **não** ganhou flip nem ícone nesta rodada, decisão deliberada pra não competir com os botões de olho/lápis que já existem em cada card ali.
|
||||
Card clicável abre o modal "Ver fórmula" (funciona igual para indicador de sistema ou personalizado, já que os dois têm a mesma forma de metadado). Card de indicador personalizado ganha um botão de lápis a mais.
|
||||
|
||||
**Ícone volta a ser configurável por indicador** — reverte a "perda aceita conscientemente" da migração anterior (ver acima). Campo novo `IndicadorContabilDefinicao.icone` (`CharField`, `choices`, default `"barras"` — o mesmo desenho hardcoded que todo indicador usava antes desta rodada, então nenhum indicador já cadastrado muda de aparência sem uma edição manual). Migração `0063_indicadorcontabildefinicao_icone`. 11 opções curadas de ícone tipo KPI (barras/tendência de alta/tendência de baixa/percentual/pizza/atividade/cifrão/alvo/camadas/cartão/selo).
|
||||
**Botões de olho ficam `disabled` quando a apuração está concluída**, mesmo espírito do resto.
|
||||
|
||||
**O desenho (miolo de `<svg>`) de cada ícone existe em duas cópias**, mantidas em sincronia manualmente, não geradas uma a partir da outra — `_CONTABIL_ICONES_SVG` (`portal_api/views.py`, usado por `_contabil_monta_cards_indicadores()` pra montar `card["icone_svg"]` via `mark_safe()`, já que são só literais Python fixos neste arquivo) e `PID_DC_INDICADOR_ICONES` (`static/js/dashboard-contabil.js`, desenha a grade de botões do seletor no modal "Novo/Editar Indicador"). Duplicação proposital: o relatório é HTML puro servido pelo Django (sem acesso ao JS do app) e o modal de cadastro é só JS/HTML estático (a página `dashboard-contabil.html` é uma `TemplateView` sem contexto de servidor) — não dá pra ter uma fonte única sem inventar mais uma ida ao backend só pra isso. Editar/adicionar um ícone exige mexer nos dois lugares.
|
||||
`pidDcFormatIndicadorMoeda`/`Percentual`/`Indice` espelham os filtros do template em JS e **sempre transformam `null`/`undefined` em "—", nunca "R$ 0,00"**. Não reaproveitam `pidDcFormatMoeda()`, que trata `null` como `0` de propósito (usado só em valores de conta/DRE, que nunca são `None`).
|
||||
|
||||
**Seletor de ícone** (`#dc-indicador-icones`, `.dc-ind-icone-grid`/`.dc-ind-icone-btn` em `dashboard-contabil.css`): grade de botões, cada um já o próprio preview (mesmo SVG que vai aparecer no card), `.is-selecionado` marca o ativo. `dcIndicadorIconeSelecionado` (estado em memória) é inicializado por `pidDcAbrirIndicadorModal()` (`definicao.icone` ao editar, `"barras"` ao criar) e incluído no payload de salvar; `IndicadorContabilDefinicaoInputSerializer.icone` (`ChoiceField`, default `"barras"`) valida no backend.
|
||||
**Resumo do Fechamento**: editor `contenteditable` com colar/arrastar imagem (mesmo padrão de `.ag-richtext`/`.ajuda-modal__editor`, duplicado aqui de propósito — nenhum dos três é componente compartilhado; limite de 2MB por imagem). Populado **só em `renderRevisao()`**, uma vez por apuração aberta, **nunca em `renderDashboardTab()`** — resetar o `innerHTML` a cada troca de aba descartaria texto ainda não salvo. Sem toggle editar/visualizar: é sempre editável enquanto a apuração está em revisão, porque só o próprio contador vê esta tela (ao contrário do texto de "Mais informações", lido por todos e editado só pelo perfil "Inovação").
|
||||
|
||||
**Flip card no relatório** — hover revela a `descricao`/fórmula do indicador (cadastradas em "Gerenciar Indicadores"). `.dcr-card` virou só a "cena" 3D (`perspective` + `min-height:172px`, necessário porque as duas faces do card são `position:absolute` agora e não contribuem mais pra altura do elemento); `.dcr-card-inner` é quem gira (`transform:rotateY(180deg)` no hover do `.dcr-card` pai, `transition` 480ms); cada face (`.dcr-card-face--front`/`--back`, `backface-visibility:hidden`) carrega o fundo/borda/sombra/padding que antes viviam direto em `.dcr-card`. O verso mostra nome + descrição (`"Sem descrição cadastrada."` se `descricao` estiver em branco — campo é opcional no model) + a fórmula. `@media print` já zera toda `animation`/`transition` globalmente, então o PDF/impressão sempre mostra a frente do card (não existe "hover" ativo numa impressão).
|
||||
### Modal "Novo/Editar Indicador"
|
||||
|
||||
**Fórmula do verso não é mais a expressão técnica de cálculo** (ajuste na mesma rodada, depois de o usuário ver `resultado_liquido - despesas_financeiras` — as chaves internas dos componentes — no verso do EBIT e pedir um jeito de controlar o texto exibido ao cliente separado do cálculo real). Campo novo `IndicadorContabilDefinicao.formula_exibicao` (`CharField`, `blank=True`, migração `0064`) — texto **livre**, sem nenhuma validação de sintaxe (ao contrário de `formula`, nunca passa por `avalia_formula()`/`ast`), só pra exibição; editável no modal "Novo/Editar Indicador" logo abaixo do campo técnico, agora rotulado "Fórmula (cálculo interno)" pra deixar claro que só `formula_exibicao` é "Fórmula (como aparece ao cliente)". `_contabil_monta_cards_indicadores()` resolve o fallback no servidor: `definicao.formula_exibicao.strip() or definicao.formula` — indicador sem essa preferência preenchida (todo indicador criado antes desta rodada, os 11 migrados inclusive) continua mostrando a fórmula técnica até alguém preencher pela tela, nunca fica sem nenhuma fórmula visível. Nenhuma mudança em `formula` em si (continua a mesma expressão validada, usada só pro cálculo) nem em `IndicadorContabilComponente`.
|
||||
`.modal-card--wide`, com nome/descrição/formato/ícone/fórmula técnica/fórmula de exibição e uma lista dinâmica de componentes. Cada linha tem chave + tipo e um "picker" que muda conforme o tipo: `contas`/`variacao_conta` e `linha_dre` reaproveitam `.checklist-box`/`.checklist-item`/`.checklist-search` de `components.css` (necessário: uma apuração real tem 100-500 contas, sem busca a lista seria inutilizável); `indicador` vira um `<select>`; `resultado_liquido` mostra só um texto explicativo. O picker é reconstruído só quando o tipo muda, não a cada tecla da busca (que só alterna `hidden` nos itens já renderizados).
|
||||
|
||||
Verificado rodando o relatório de verdade via `Client.force_login()` num shell (`manage.py shell`, apuração `id=4`) — sem servidor de desenvolvimento nenhum aberto: a estrutura nova (`dcr-card-inner`, ícone certo por `icone_svg`), o fallback de `formula_exibicao` pra `formula` quando em branco, e o texto customizado aparecendo no lugar quando preenchido — os dois últimos testados trocando o campo de um indicador real (EBIT) e revertendo logo em seguida, sem deixar resíduo em produção. Confirmado também que "Sem descrição cadastrada" no card de um indicador que já tem `descricao` salva não é bug: o relatório é uma foto estática do momento em que foi gerado — editar a definição depois não atualiza um relatório já aberto/baixado, precisa clicar "Gerar Dashboard" de novo.
|
||||
**Scroll**: `.modal-card` (global, `components.css`) tem `max-height: calc(100vh - var(--space-5)*2)` + `overflow-y:auto`. A lista de componentes **não** tem scroll próprio — ter os dois aninhados por cima do `.checklist-box` (que já rola) deixava a área útil menor que um componente inteiro. Sobra só um scroll aninhado, o do checklist, que é genuinamente necessário.
|
||||
|
||||
**Dois ajustes de acabamento, mesma rodada**: (a) `.dcr-card__value` (valor da frente) de `1.55rem` pra `1.3rem` — valor negativo em moeda (`R$ -143.648,54`) quebrava linha com a fonte maior; (b) `.dcr-card-face__descricao` perdeu `flex:1; overflow-y:auto` (um scroll aninhado por cima do scroll que já existe em `.dcr-card-face--back`) — descrição e fórmula agora fluem juntas num único bloco, rolando como texto contínuo em vez de ficar a fórmula presa numa área separada cortada.
|
||||
**Componente "órfão" ao editar de outra apuração**: os pickers são montados a partir da apuração aberta agora, mas um componente pode ter sido configurado a partir de outra empresa/competência. Todo código/descrição salvo que não existe na apuração atual entra como item extra no topo do checklist, **já marcado** e com a nota "não encontrada nesta apuração, mantida" — senão salvar sem tocar naquele componente apagaria a referência em silêncio.
|
||||
|
||||
**A tela de revisão (`dashboard-contabil.js`) ganhou o mesmo destaque inicial do relatório**: `renderRevisao()` agora inicializa `dcContasDestaque`/`dcDreDestaque` como cópias de `dcContasColapsadas`/`dcDreColapsadas` (`new Set(dcContasColapsadas)`), em vez de `new Set()` vazio — mesmo raciocínio do ajuste do relatório (ver "Destaque replicado no relatório 'Gerar Dashboard'" acima): as linhas colapsadas por padrão **são** o último nível já visível de cada ramo, então já nascem destacadas, sem precisar de nenhum clique. Reduz a poluição visual de abrir a tela inteira na cor de grupo/total (mesmo motivo que levou ao ajuste no relatório).
|
||||
**"Calcular com esta apuração"** chama `pre-visualizar-indicador/` com o formulário **ainda não salvo** e mostra o valor de cada componente mais o resultado final, sem persistir nada. Cada componente é formatado com `indice` (número BR simples, sem R$/%) porque um componente pode ser qualquer grandeza; o resultado usa o `formato` escolhido. O painel é escondido sempre que o modal abre, para nunca mostrar um resultado desatualizado.
|
||||
|
||||
### Botão de observação virou sempre ícone + cor do destaque invertida (tema escuro)
|
||||
**Fechar pelo overlay ou "Cancelar" pede confirmação** (`pidConfirm`, `{perigoso: true}`). Salvar/excluir fecham direto — não há o que descartar depois de uma ação concluída.
|
||||
|
||||
Dois pedidos na sequência sobre a tela de revisão (Balancete/D.R.E.):
|
||||
### Lista/histórico (`#dc-list-table`)
|
||||
|
||||
**Botão de observação**: mostrar o texto da observação já preenchida direto na tabela (comportamento desde sempre) também poluía a coluna, além do "+ Observação" (já resolvido numa rodada anterior, ver acima) — o usuário pediu pra nunca mostrar texto nenhum na tabela, só o ícone, mudando de cor conforme preenchida ou não. `PID_DC_OBSERVACAO_ICONE` (constante única, sem mais um branch vazio/preenchido) é sempre o conteúdo do botão agora; `.dc-conta-observacao-btn--preenchida` (classe condicional em `dashboard-contabil.js`, tanto em `renderContas()` quanto em `renderDre()`) é a única diferença — troca a cor do ícone de `--text-muted` pra `--accent`. O texto da observação continua acessível via `title`/tooltip (`"Adicionar observação"` ou o texto em si) e pelo clique, que abre o modal de edição normalmente — só sumiu da própria célula da tabela. `.dc-conta-observacao-btn` virou sempre um botão circular 26px (antes só a variante vazia era assim; a variante "preenchida" tinha texto truncado com `max-width`/`text-overflow`, removido).
|
||||
Ordenação e filtro por coluna, client-side, mesmo mecanismo de `#ips-list-table` (Importação de Plano de Saúde), portado e renomeado com prefixo `dc-` — **não compartilhado** entre os dois arquivos JS/CSS.
|
||||
|
||||
**Cor do destaque no tema escuro invertida**: `--dc-destaque-bg` (linhas destacadas) e `--dc-row-tint-bg` (as demais) trocaram de papel — antes o destaque usava `--card-bg-hover` (mais claro) e o resto ficava transparente (mais escuro, revelando o fundo da tabela); o usuário pediu o oposto, destaque mais escuro e o resto mais claro. Agora `--dc-destaque-bg: var(--bg-canvas)` (o tom mais escuro do tema) e `--dc-row-tint-bg: var(--bg-surface-raised)` (mais claro que `--bg-canvas`/`--bg-surface`, mas ainda diferente de `--card-bg-hover` — usar o mesmo tom do hover deixaria o hover das linhas não-destacadas sem nenhum efeito visível, já que ficariam idênticas em repouso e ao passar o mouse). **Só o tema escuro mudou** — o tema claro já tinha sido invertido por pedido anterior do usuário (não-destaque colorido, destaque em branco) e continua como estava, sem relação com este pedido.
|
||||
`carregarLista()` só busca a API e guarda em `dcListaApuracoes`; `renderList()` (sem fetch) filtra, ordena e desenha — chamada por qualquer mudança de ordenação/filtro, sem round-trip.
|
||||
|
||||
### Bug real: folha genuína ficava de fora do destaque inicial
|
||||
- **Ordenação padrão**: `criado_em` decrescente (a última execução primeiro), decisão explícita do usuário, diferente do `Meta.ordering` do model (que prioriza competência).
|
||||
- **Colunas ordenáveis**: empresa (por `codigo_empresa`), competência, observações (`total_achados_pendentes`), status, criado_por, criado_em.
|
||||
- **Colunas com filtro estilo Excel** (funil, popup com busca + checklist): empresa, competência, status, criado_por. Ficam de fora `observacoes` (contagem, não dimensão de agrupamento) e `criado_em` (granularidade fina demais).
|
||||
- **`valoresDistintos()` ordena pelo valor bruto, não pelo rótulo formatado.** Importa para competência: ordenar por "MM/AAAA" agruparia por mês antes do ano ("01/2026" antes de "12/2025"), enquanto a string ISO já ordena cronologicamente por comparação simples. O popup mostra o rótulo legível, mas indexa pelo valor bruto.
|
||||
- Botão "borracha" (`#dc-list-reset-btn`) limpa os quatro filtros e volta a ordenação ao padrão.
|
||||
- **Sem paginação** (diferente de Importação de Plano de Saúde) — o histórico tende a ser curto, uma linha por empresa+competência.
|
||||
|
||||
Usuário reportou, testando a rodada anterior na D.R.E., que várias linhas-folha genuínas (`(-) DE VENDAS DE MERCADORIAS MERCADO INTERNO`, `(-) SIMPLES NACIONAL`, `DESCONTOS OBTIDOS`, várias linhas de resultado financeiro) não estavam com o destaque que deveriam ter, mesmo sendo visualmente "de baixo" quanto um grupo colapsado por padrão no mesmo nível. Causa: o destaque inicial usava só `dcContasColapsadas`/`dcDreColapsadas` (que só marca linha que **tem filho escondido**) — uma folha de verdade (sem filho nenhum, ex. `(-) SIMPLES NACIONAL`, código-fonte confirmado via `manage.py shell`: `ContabilLinhaDre.objects.get(id=503).nivel == 2`, sem nenhuma linha de `nivel=3` logo depois) nunca entra nesse conjunto, então nunca ganhava destaque, mesmo estando no mesmo nível visual que um grupo colapsado vizinho.
|
||||
Na linha concluída, o ícone de lixeira **some** (mesmo padrão do botão de reprocessar ao lado; misturar "some" e "aparece desabilitado" na mesma linha seria incoerente). O handler ainda tem `try/catch` + `pidAlert`, porque a trava do servidor continua valendo para uma lista carregada antes de outra pessoa concluir a análise.
|
||||
|
||||
Critério corrigido (`dcUltimaLevaVisivel()` em `dashboard-contabil.js`, `calculaDestaqueInicial()` na cópia irmã em `dashboard-contabil-relatorio.html`, mesmo algoritmo nas duas): reconstrói a lista de linhas **realmente visíveis** dado o colapso padrão (mesma pilha de níveis usada pra ocultar descendente de linha colapsada) e marca como destaque toda linha cuja **próxima linha visível não seja mais profunda que ela** — cobre os dois casos com uma regra só: nada foi revelado logo abaixo dela agora, seja porque está colapsada (filhos escondidos) ou porque é uma folha sem filho nenhum. Validado manualmente contra os níveis reais da apuração `id=4` (`ContabilLinhaDre` ids 490-504) antes de aplicar — o algoritmo original (`= colapsadas`) incluía só 492/496 (grupos colapsados); o corrigido inclui também 501/503 (as duas folhas citadas pelo usuário), sem incluir 500/502 (grupos abertos com algo visível abaixo, corretamente fora do destaque).
|
||||
### Tooltip no visual do Portal
|
||||
|
||||
### Flip card: texto centralizado, fonte menor, fórmula em fonte monoespaçada mais elegante
|
||||
`pidDcHoverTooltipHtml(gatilhoHtml, gatilhoClasse, texto)` monta um par gatilho+texto (`.dc-hover-tooltip`), nunca `title="..."` (o balão nativo do Chrome quebra a identidade visual, mesmo raciocínio de `pidConfirm`/`pidAlert` no lugar de `window.confirm`/`alert`). Diferente de `.info-tooltip` de `components.css` por usar `white-space: pre-line` (preserva quebras deliberadas e envolve o resto) em vez de `nowrap`.
|
||||
|
||||
Três ajustes finos de acabamento no verso do flip card do relatório, pedido explícito do usuário com uma captura de tela do flip card equivalente que ele já usa no Power BI como referência: (a) `text-align:center` em `.dcr-card-face--back` — herdado por título/descrição/rótulo/fórmula, nenhum precisou de regra própria; (b) fontes um pouco menores (`.dcr-card-face__titulo` `0.8rem→0.74rem`, `__descricao` `0.78rem→0.7rem`, `__formula-label` `0.66rem→0.62rem`, `__formula` `0.74rem→0.7rem`); (c) fórmula ganhou "JetBrains Mono" (peso 500, adicionada ao mesmo `<link>` do Google Fonts já usado por Inter/Manrope nesta página), com `"Courier New", monospace` como fallback.
|
||||
> **`position: fixed`, não `absolute`.** Os badges deste pacote vivem dentro de `.pa-table-wrap`, que tem `overflow:hidden` para arredondar o canto da tabela — qualquer coisa `absolute` que escape da tabela é cortada. Com `fixed` + `top`/`left` calculados em JS (`pidDcPosicionaTooltip()`), o balão é relativo à viewport: centralizado acima por padrão, desce quando não há espaço acima, clampado nas laterais. Delegado no `document` com `capture:true` (`mouseenter`/`focus` não borbulham), então um único par de listeners cobre todo badge atual e futuro, mesmo os recriados a cada re-render.
|
||||
|
||||
### Modal "Novo/Editar Indicador" ganhou confirmação ao fechar sem salvar
|
||||
`tabindex="0"` no gatilho + `:focus-visible` mostram o tooltip por teclado também.
|
||||
|
||||
Pedido explícito do usuário: fechar o modal clicando fora (overlay) saía direto sem perguntar nada, arriscando perder o que já estava preenchido. `pidDcFecharIndicadorModalComConfirmacao()` (nova, `async`) chama `pidConfirm("Sair sem salvar as alterações?", { perigoso: true })` antes de fechar de verdade — ligada ao clique no overlay e ao botão "Cancelar", os dois únicos caminhos de fechar iniciados pelo usuário (o modal não tem um "X" próprio). `pidDcFecharIndicadorModal()` em si continua sem confirmação nenhuma, chamada direto pelos fluxos de sucesso (salvar/excluir) — não haveria o que descartar depois de uma ação já concluída. Mesmo padrão já usado no modal de "Mais informações" (`ajuda-aplicacao.js`) e no antigo "Fechar sem salvar" do Cadastro de Regras de Plano de Saúde (ver "Modal de confirmação genérico" no `CLAUDE.md` da raiz).
|
||||
Não promovido para `components.css` por enquanto — só usado aqui, mas a mecânica é genérica se outro pacote precisar.
|
||||
|
||||
### Resumo do Fechamento — texto rico do contador antes dos indicadores
|
||||
## Relatório do cliente (`dashboard-contabil-relatorio.html` + `views.py`)
|
||||
|
||||
Pedido explícito do usuário, com um exemplo real de texto que a De Paula já manda ao cliente hoje por fora do Portal (carta com considerações/saldos/variações do fechamento) como referência do que deveria caber aqui. A aba "Indicadores" do relatório "Gerar Dashboard" (só cards de indicador até aqui) virou **"Resumo"** — nome da aba (`data-dcr-tab="indicadores"`, atributo mantido igual, só o rótulo visível mudou) e do card `dc-dash-section__titulo` na aba "Dashboard" da revisão — porque agora carrega duas coisas: o texto livre do contador (**"Resumo do Fechamento"**, sempre no topo) e, embaixo, os grupos de cards de indicador de sempre (inalterados).
|
||||
`ContabilApuracaoViewSet.dashboard()` gera um documento HTML **autocontido** — não estende o shell do Portal e **nunca usa a marca "P.I.D."**: carrega `logo-branco.png`, a identidade do escritório, porque é um documento emitido ao cliente. Ver `[[feedback_logos_documentos_vs_portal]]` na memória.
|
||||
|
||||
**Model**: `ContabilApuracao.resumo_fechamento` (`TextField`, `blank=True`, migração `0065`) — texto rico (HTML sanitizado), mesma allowlist nh3 (`RICHTEXT_ALLOWED_TAGS`/`_ATTRS`/`_SCHEMES`) de `AcessoGeral.observacoes`/`AjudaAplicacao.texto`; `CONTABIL_RESUMO_FECHAMENTO_MAX_CHARS`/`validar_tamanho_resumo_fechamento_contabil` seguem o mesmo padrão "constante+validator por campo" já usado pelos outros dois (o projeto não tem um validator de tamanho de texto rico genérico, é copiado por campo de propósito).
|
||||
> **É GET, não POST**, diferente do padrão `/gerar/` das outras ferramentas. A primeira versão era POST e o frontend abria via `fetch` + `URL.createObjectURL(blob)` + `window.open()`. Um documento carregado de uma URL `blob:` tem origem sintética, e URLs relativas geradas por `{% static %}` não resolvem de forma confiável nesse contexto — a logo do escritório simplesmente não carregava. Com GET o frontend abre a URL da API direto (`pidGerarDashboardContabil()` é só `window.open(...)`), navegação de verdade, mesma origem, sem blob nem CSRF.
|
||||
|
||||
**Backend**: `ContabilApuracaoViewSet` não tem PATCH genérico (`http_method_names` exclui `"patch"` de propósito, ver docstring da classe) — mesmo padrão de `indicadores_ocultos()`/`indicadores_selecionados()`, uma `@action` dedicada nova (`POST /api/contabil-apuracoes/{id}/resumo-fechamento/`, `resumo_fechamento()`) substitui o campo inteiro de uma vez, validado por `ContabilResumoFechamentoSerializer` (`validate_resumo_fechamento` chama `nh3.clean()`, mesmo `validate_texto`/`validate_observacoes` de `AjudaAplicacaoSerializer`/`AcessoGeralSerializer`), guardada por `_contabil_garante_em_revisao()` (não editável depois de "Concluída", igual ao resto da apuração). Campo exposto em leitura via `ContabilApuracaoDetailSerializer` (não no `ListSerializer` — só a tela de revisão precisa dele).
|
||||
**Abas** (`.dcr-tabs`, JS puro inline — não reaproveita `.pa-tabs`, que não é carregado neste documento): Balancete (inicial) → D.R.E. → Análise Vertical (só com `{% if analise_vertical_meses %}`) → Resumo. O `data-dcr-tab` do Resumo continua `"indicadores"` internamente, nome anterior ao da aba ganhar mais conteúdo.
|
||||
|
||||
**Frontend — aba "Dashboard"** (`templates/dashboard-contabil.html`/`dashboard-contabil.js`): editor `contenteditable` com colar/arrastar imagem embutida — mesmo padrão de `.ag-richtext` (`acessos-gerais.js`)/`.ajuda-modal__editor` (`ajuda-aplicacao.js`), duplicado aqui de propósito (nenhum desses três é um componente compartilhado no projeto; `PID_DC_RESUMO_FECHAMENTO_IMAGE_MAX_BYTES = 2MB`, mesmo limite dos outros dois). Populado só em `renderRevisao()` (uma vez por apuração aberta) — **nunca** em `renderDashboardTab()` (que roda toda vez que o contador troca pra essa aba): resetar o `innerHTML` a cada troca de aba descartaria texto ainda não salvo, diferente do padrão de "recarrega sempre" que os cards de indicador/observações usam ali (esses não são editados inline na mesma tela). Sem botão "Cancelar"/toggle editar-vs-visualizar (diferente de `AjudaAplicacaoSerializer`) — é sempre editável enquanto a apuração está "Em revisão" (`contenteditable` alternado via JS conforme `status`), porque só o próprio contador vê esta tela, ao contrário do texto de "Mais informações" (lido por qualquer usuário, editado só pelo perfil "Inovação").
|
||||
Na impressão (`@media print`) a barra de abas some e todas ficam visíveis, cada uma numa página (`page-break-after`); linhas recolhidas são forçadas a aparecer (`tr[hidden] { display: table-row !important }`); botões de imprimir/exportar/restaurar/voltar-ao-topo somem. **Documento impresso não esconde conteúdo atrás de aba não clicada nem de grupo recolhido.**
|
||||
|
||||
**Frontend — relatório** (`dashboard-contabil-relatorio.html`): `{{ apuracao.resumo_fechamento|safe }}` dentro de `.dcr-resumo-fechamento__texto`, condicionado a `{% if apuracao.resumo_fechamento %}` (nada renderiza se a apuração não tem resumo). **Primeiro `|safe` de template Django do projeto** — até agora todo texto rico só existia em tela SPA, injetado via `.innerHTML =` no JS, nunca por um template renderizado no servidor; seguro aqui pelo mesmo motivo de sempre (já vem sanitizado por nh3 antes de salvar, nunca cru do request). Seção com fundo/borda/sombra próprios (`.dcr-resumo-fechamento`, visual de card, diferente das outras `.dcr-secao` que são só agrupamento sem fundo) — se destaca do restante da aba antes dos grupos de indicador.
|
||||
### Estrutura de cada aba
|
||||
|
||||
Testado de ponta a ponta via `Client.force_login()` num shell: `POST .../resumo-fechamento/` com um `<script>alert(1)</script>` embutido junto de HTML válido — confirmado que o nh3 removeu o `<script>` e manteve `<p>`/`<strong>`/`<ul>`/`<li>` intactos; conferido que o texto aparece no relatório antes de "Indicadores de Resultado" e que a seção some quando `resumo_fechamento` está vazio; revertido pro valor original (vazio) na apuração real (`id=4`) ao final do teste.
|
||||
Cada aba de tabela tem a seção **"Observações do X" ACIMA da tabela**, depois a tabela. A aba Resumo tem o Resumo do Fechamento, os grupos de cards de indicador e "Todas as Observações da Análise" (as três listas + auditoria juntas, cada item com prefixo de origem: "Balancete — ", "D.R.E. — ", "Análise Vertical — ", "Auditoria — ").
|
||||
|
||||
### Análise Vertical — nova aba na revisão e no relatório (rodada 123)
|
||||
**Clicar numa observação rola até a conta/linha que ela referencia, se houver.** `dashboard()` calcula uma `ancora` por observação (setada no objeto Python, não é campo do model), igual ao `data-dcr-id` da linha correspondente, casando pela mesma chave natural. Fica `None` quando a conta não existe mais nesta apuração (observação histórica de conta que saiu do plano) — o "se houver": a observação continua aparecendo, só não vira link. Só o `<li>` com âncora ganha `data-dcr-obs-ir` + `role="button"` + `.dcr-obs-item--clicavel`.
|
||||
|
||||
Pedido explícito do usuário: a Demonstração Mensal (Análise Vertical), até então ignorada pelo parser (ver "Extração do PDF" acima), passou a ser exibida tanto na tela de revisão quanto no relatório "Gerar Dashboard" — escopo confirmado por `AskUserQuestion` antes de implementar: **aba própria** (não embutida na aba D.R.E.) e **mesmos recursos por linha que Balancete/D.R.E.** (observação inline, tri-state "validado", ocultar do relatório).
|
||||
### Árvore recolhível server-side
|
||||
|
||||
**Backend**: `ContabilLinhaAnaliseVerticalViewSet` (`GET`/`PATCH` em `/api/contabil-linhas-analise-vertical/{id}/`) é uma réplica exata de `ContabilLinhaDreViewSet` — mesmo `_contabil_garante_em_revisao()`, mesmo serializer com `valores` read-only. `ContabilApuracaoViewSet.create()` faz um terceiro `bulk_create` (depois de `ContabilConta`/`ContabilLinhaDre`) com as linhas de `resultado.extracao.linhas_analise_vertical`, e grava `analise_vertical_meses` já na criação da `ContabilApuracao` — vem pronto do parser, não precisa de nenhum cálculo adicional na view. `dashboard()` ganhou `linhas_analise_vertical`/`analise_vertical_meses`/`observacoes_analise_vertical` no contexto, reaproveitando `_contabil_arvore_contexto()` (mesma função da DRE, só que passando a lista/nivel_fn certos) — nenhuma função de árvore nova precisou ser escrita.
|
||||
Diferente da tela de revisão (SPA que re-renderiza a tabela a cada clique), aqui o HTML é gerado uma vez: `nivel`, `tem_filhos` e `colapsado_padrao` são calculados **no servidor** (`_contabil_arvore_contexto()`, mesmo algoritmo) e viram `data-dcr-nivel`/`data-dcr-tem-filhos`/`data-dcr-id`/`data-dcr-colapsado-padrao` em cada `<tr>`. `pidDcrArvore(tbodyId)` só alterna o atributo `hidden` das `<tr>` existentes, sem reconstruir HTML.
|
||||
|
||||
**Frontend — tela de revisão** (`dashboard-contabil.html`/`.js`): nova aba `data-dc-tab="analise-vertical"` (botão `#dc-av-tab-btn`, `hidden` quando `analise_vertical_meses` está vazio — relatório antigo sem essa seção não ganha uma aba com tabela vazia). `renderAnaliseVertical()` é uma réplica de `renderDre()` (mesmo algoritmo de pilha de colapso, mesmo tri-state de validado via `dcValidadoInfo()`/`dcClicarValidadoAv()` — as funções genéricas já existentes, `dcColapsoPadrao`/`dcUltimaLevaVisivel`/`dcFilhosDiretos`/`dcDescendentes`/`dcEstadoValidacaoGrupo`/`dcValidadoInfo`, já recebiam `itens`/`nivelFn` como parâmetro e foram 100% reaproveitadas sem alteração), só que cada `<td>` de valor vira **N pares** de colunas (Valor/Variação, uma dupla por mês) montadas a partir de `linha.valores` — o cabeçalho da tabela (`renderAnaliseVerticalHead()`) também é montado em JS porque o número de meses varia (normalmente 3, mas não é fixo). `pidDcFormatPercentualAnaliseVertical()` é um formatador **novo**, deliberadamente diferente de `pidDcFormatIndicadorPercentual()` — o percentual da Análise Vertical já vem "pronto" do PDF (ex. `"100.00"` quer dizer 100,00%), enquanto o dos cards de indicador é uma fração que precisa ser multiplicada por 100; reusar o errado exibiria `10000,00%`. Editor de observação inline + resumo de observações no fim da aba seguem exatamente o padrão de Balancete/D.R.E. (`dc-av-obs-*`).
|
||||
**O relatório nasce recolhido** (`colapsado_padrao = tem_filhos and nivel >= _CONTABIL_NIVEL_ABERTO_PADRAO`), ao contrário da tela de revisão. É a única tela onde `dcColapsoPadrao` ainda é padrão de abertura, e é assim de propósito: é o documento que vai ao cliente, tem só o botão de restaurar, e mudar o que o cliente vê não foi pedido.
|
||||
|
||||
**Se a apuração aberta não tem Análise Vertical mas a aba estava ativa** (ex.: usuário estava vendo essa aba de uma apuração anterior e abre uma sem essa seção) — `renderRevisao()` força a volta pra aba "Observações" nesse caso específico, pra não deixar um botão de aba escondido com o painel dele ainda visível.
|
||||
`pidDcrArvore()` devolve `{ expandeAte, resetar }`; `pidDcrObs()` devolve `{ fecharTudo }`. `expandeAte(id)` sobe a cadeia de ancestrais e reabre **só** os colapsados no caminho, não a árvore inteira. `resetar()` devolve tudo ao estado inicial do servidor. Dois dicionários (`dcrArvores`/`dcrObsControles`) casam o `tbodyId` com a árvore/painel certos, então cada botão "Limpar formatação" só afeta a própria tabela.
|
||||
|
||||
**Frontend — relatório** (`dashboard-contabil-relatorio.html`): 4ª aba `data-dcr-tab="analise-vertical"`, entre "D.R.E." e "Resumo" — todo o bloco (botão + painel) fica dentro de `{% if analise_vertical_meses %}`, então some inteiro em relatórios de apurações antigas sem essa seção; `pidDcrArvore("dcr-av-body")`/`pidDcrObs("dcr-av-body")` (chamadas incondicionais no fim do script) já toleram um `tbody` inexistente (`if (!tbody) return;`, mesma guarda que essas duas funções já tinham). Reaproveita a mesma árvore recolhível/observação inline da DRE, sem nenhuma função nova. "Observações da Análise Vertical" tem sua própria seção na aba (posição revisitada numa rodada seguinte — ver "Seção 'Observações do X' fica ACIMA da tabela" mais abaixo), e a lista consolidada "Todas as Observações da Análise" (aba "Resumo") ganhou um 3º grupo (prefixo "Análise Vertical — ...", ícone de gráfico de barras) ao lado de Balancete/D.R.E./Auditoria — a mesma extensão simples de sempre, um `{% for %}` a mais.
|
||||
O destaque de linha é replicado aqui com a mesma lógica da tela de revisão (`calculaDestaque()`/`expandidos`/`descendentes()` espelhando as funções do JS do app). **Os dois lados são mantidos em sincronia manualmente** — mudança na lógica de destaque precisa ser aplicada nos dois arquivos.
|
||||
|
||||
Dois filtros de template novos em `contabil_extras.py` — `moeda_av`/`percentual_av` — porque `ContabilLinhaAnaliseVertical.valores` grava valor/percentual como **texto** (não `Decimal`, ver acima), e os filtros `moeda`/`percentual` existentes não servem: `moeda` faria `f"{valor:,.2f}"` falhar contra uma string, e `percentual` multiplicaria por 100 (pensado pra fração, não pra um percentual "já pronto" como o desta seção).
|
||||
**Observação inline no relatório**: coluna "Observação" com ícone que só aparece quando há observação vigente **e** `mostrar_ao_cliente` — observação interna não vaza nem como ícone. Clicar abre uma `<tr class="dcr-obs-inline-row">` já presente no HTML (nasce `hidden`). A visibilidade é sincronizada a **todo** clique no corpo da tabela (inclusive os de expandir/recolher) a partir de dois fatores: se o usuário abriu aquele painel **e** se a linha-pai está visível — assim, colapsar um grupo ancestral fecha os painéis abertos dentro dele sem duplicar a lógica de pilha. **Essas linhas de observação são excluídas da lista que `pidDcrArvore()` percorre** (`filter` por `data-dcr-obs-row`): incluí-las quebraria a pilha de colapso, já que não têm `data-dcr-nivel` próprio.
|
||||
|
||||
**Validado ponta a ponta via `Client.force_login()` num shell**, contra o PDF de referência real (`792 - balancete 072026.pdf`, empresa 0792/WEITNAUER, competência 07/2026): `POST /api/contabil-apuracoes/` extraiu 154 linhas de Análise Vertical (3 meses: mai/jun/jul de 2026) além das 177 contas/163 linhas de DRE de sempre; `GET .../dashboard/` gerou o relatório com a aba "Análise Vertical" presente; `PATCH` numa linha (observação + `oculta_no_relatorio=False` + `validado=True`) persistiu corretamente e a observação apareceu no relatório gerado em seguida. Toda apuração/arquivo criados durante o teste foram apagados ao final, sem deixar resíduo em produção.
|
||||
### Análise Vertical: tabela larga
|
||||
|
||||
### Reprocessar — anexar um PDF novo pra mesma empresa/competência sem perder observações/achados (rodada 124)
|
||||
Essa tabela pode ter bem mais colunas (Descrição + 2 por mês + Observação = 8 para 3 meses) e `.dcr-page` tem `max-width:1140px`. Duas mudanças resolvem:
|
||||
- `.dcr-tabela-wrap { overflow-x: auto }` (era `overflow:hidden`, que zerava os dois eixos; `overflow-y` continua `hidden`) e `table.dcr-tabela th { white-space: nowrap }` — sem isso o navegador espremia as colunas até quebrar o cabeçalho em 3 linhas e cortar a última coluna para fora da área visível.
|
||||
- `.dcr-tabela--compacta` (só no `<table>` da Análise Vertical): fontes e paddings menores, ícones menores, e o filtro **`mes_curto`** no cabeçalho (`"mai/2026"` → `"mai/26"`), já que "— Valor"/"— Variação" repetido por mês era o maior consumidor de largura. Objetivo: o caso comum (3 meses) caber sem rolar. O `overflow-x` continua como rede de segurança para mais meses.
|
||||
|
||||
Pedido explícito do usuário: até aqui, corrigir uma apuração com o PDF errado/incompleto exigia excluir e recriar do zero (ver docstring antiga de `concluir()`), perdendo toda observação/validação/achado já registrado. Botão "Reprocessar" novo (ícone ao lado de "Abrir", na lista — só aparece em apuração "Em revisão") abre um modal só com o campo de arquivo; o PDF novo precisa ser da **mesma** `codigo_empresa`+competência (senão 400 — trocar de empresa é uma análise nova, não um reprocessamento). Escopo do que preserva/reseta confirmado com o usuário nesta rodada: contas/linhas sem mudança mantêm observação/validado como estavam; contas/linhas que mudaram voltam pra `validado=False` e ganham um alerta visual; achados **nunca são apagados nem têm status/justificativa sobrescritos**, mesmo os que não disparam mais com os dados novos (confirmado explicitamente via `AskUserQuestion` — é pra manter o histórico completo de tratativa). **Decisão sobre achados revertida na rodada 143** (ver "Achados são recriados do zero a cada reprocessamento" mais abaixo) — o comportamento atual é o oposto do descrito aqui: achado é apagado e recriado do zero a cada reprocessamento, só a parte de conta/linha (parágrafo acima) continua como decidido nesta rodada.
|
||||
### Filtros de template (`portal_api/templatetags/contabil_extras.py`)
|
||||
|
||||
**Modelos**: campo novo `alterada_reprocessamento` (`BooleanField`, default `False`) em `ContabilConta`/`ContabilLinhaDre`/`ContabilLinhaAnaliseVertical` (migração `0068`) — marca que aquela conta/linha mudou no último reprocessamento; o frontend mostra um alerta ao lado do ícone de observação enquanto for `True`. Migração `0069` acrescentou o valor de antes da mudança, só pro tooltip do badge (pedido explícito do usuário — "mostre o valor que estava antes do reprocessamento" ao passar o mouse): `valor_anterior_reprocessamento` (`DecimalField`, `null=True`) em `ContabilConta` (cópia de `saldo_atual`) e `ContabilLinhaDre` (cópia de `valor`); `valores_anterior_reprocessamento` (`JSONField`, mesmo formato de `valores`) em `ContabilLinhaAnaliseVertical`, já que ali não existe um valor único (um por mês). Os três são `read_only` no serializer — só `_contabil_sincroniza_*` grava, nunca um PATCH de cliente.
|
||||
Primeiro (e único) uso de template tags customizadas no projeto. `moeda`/`percentual`/`indice`/`competencia` no padrão brasileiro; `None` sempre vira "—", nunca "R$ 0,00"/"0,00%". `numero_bruto` devolve o valor cru só para o atributo `data-count` da animação, nunca para texto exibido. `mes_curto` para o cabeçalho da Análise Vertical.
|
||||
|
||||
**`ContabilApuracaoViewSet.reprocessar()`** (`POST /api/contabil-apuracoes/{id}/reprocessar/`, multipart `arquivo`) — bloqueado por `_contabil_garante_em_revisao()` (mesmo gate de qualquer edição; não existe "reprocessar uma apuração Concluída"). Roda o mesmo `dashboard_contabil_pipeline.processa_apuracao()` de `create()`, confere `codigo_empresa`/competência batendo com a apuração existente, troca o `arquivo` (apaga o antigo só **depois** do commit da transação, mesmo cuidado de sempre com storage não-transacional) e delega a resincronização pra 4 funções puras — cada uma com uma estratégia diferente, conforme o que precisa (ou não) persistir através de um reprocessamento:
|
||||
**`moeda_av`/`percentual_av` são separados de propósito**: `ContabilLinhaAnaliseVertical.valores` grava valor/percentual como **texto**, então `moeda` (que faria `f"{valor:,.2f}"`) falharia contra uma string, e `percentual` multiplicaria por 100 um número que já é um percentual pronto. Mesmo cuidado no JS: `pidDcFormatPercentualAnaliseVertical()` é deliberadamente diferente de `pidDcFormatIndicadorPercentual()` — usar o errado exibiria `10000,00%`.
|
||||
|
||||
- `_contabil_sincroniza_contas()`/`_contabil_sincroniza_linhas_dre()`/`_contabil_sincroniza_linhas_analise_vertical()` — **atualização no lugar (mesmo `id`), não delete+recria**: casam cada conta/linha extraída contra a existente (`codigo` pro Balancete; `(descricao, nivel)` pra DRE/Análise Vertical, mesma convenção de chave natural já usada pelo histórico de variação em `regras.py`/`_contabil_monta_historico()` — o par `(descricao, nivel)` desambigua a maioria das descrições repetidas em ramos diferentes da árvore, ex. "COMISSÕES SOBRE VENDAS" aparecendo em mais de um nível, um risco real confirmado contra o PDF de referência). Casada: atualiza os campos brutos **no mesmo registro** (`.save()`, nunca `bulk_create`/delete); se algum campo relevante mudou, força `validado=False` e `alterada_reprocessamento=True`, guardando o valor de antes em `valor_anterior_reprocessamento`/`valores_anterior_reprocessamento` (sempre lido **antes** de sobrescrever o campo com o valor novo, na mesma função), senão preserva tudo (inclusive limpando esse campo pra `None`/`[]`) como estava. `observacao`/`oculta_no_relatorio` nunca são tocados por essas funções. Sem match na nova extração: `ContabilConta.objects.create()`/equivalente, nasce com os defaults de sempre (`validado=False`, sem o alerta — não tem "antes" pra comparar). Sobra no mapa antigo sem match na nova extração: `.delete()`. Mantidas assim (não delete+recria) porque `codigo`/`(descricao, nivel)` **é** a identidade da conta/linha do ponto de vista do contador — o mesmo id continua referenciado por `ContabilObservacao` (chave natural, mas ainda assim o `id` da conta importa pra `_contabil_arvore_contexto()` no relatório) e é preciso preservar `validado`/observações de itens que não mudaram.
|
||||
- `_contabil_recria_achados()` (**rodada 143, revisão de uma decisão anterior** — ver abaixo) — **delete+recria total**, mesmo `bulk_create` de `create()`: apaga **todos** os achados da apuração e gera um conjunto novo a partir das regras rodadas sobre o PDF novo. Todo achado nasce `pendente`, mesmo que a mesma `(regra, conta)` já tivesse sido tratada antes do reprocessamento.
|
||||
Um indicador personalizado chega ao contexto **já formatado como texto** (`_contabil_formata_indicador()`), porque o Django Template Language não permite escolher um filtro por nome vindo de variável.
|
||||
|
||||
**Por que os dois grupos usam estratégias opostas**: conta/linha é dado extraído do PDF que o contador **anota** (observação/validado) — o valor de hoje precisa ser atualizado, mas a anotação de ontem sobre a mesma conta continua valendo, então "atualiza no lugar" preserva tudo que não mudou. Achado é um **apontamento derivado**, recalculado inteiramente a cada rodada das regras — não existe "achado que não mudou", ele ou dispara com os dados de agora ou não dispara; manter um achado "tratado" que não dispara mais equivalia a mostrar uma inconsistência que já não existe.
|
||||
### Visual e animações
|
||||
|
||||
**Frontend** (`dashboard-contabil.html`/`.js`): botão de ícone (refresh) ao lado de "Abrir" na lista (`data-dc-reprocessar-abrir`, escondido quando `status === "concluida"`) abre `#dc-reprocessar-modal` (mesmo campo de arquivo de "Nova Análise", reaproveitando `wireArquivoField()` que já era genérico o bastante) — `pidReprocessarApuracaoContabil(id, formData)` chama o endpoint, atualiza a lista e, se a apuração reprocessada é a que já está aberta na revisão, também re-renderiza a tela (`apuracaoAtual = atualizada; renderRevisao()`). Cada uma das 3 tabelas (Balancete/DRE/Análise Vertical) ganhou um badge de alerta (`pidDcAlteradaBadgeHtml()`, ícone de triângulo) ao lado do botão de observação, visível enquanto `alterada_reprocessamento` for `True`. **Marcar a conta/linha como validada de novo NÃO limpa o alerta** (pedido explícito do usuário, revertendo a primeira versão desta rodada, que limpava — "quando o usuário marcar como validado uma conta que foi reprocessada, não deve sumir o ícone de aviso, mas sim, ficar verde... conseguimos verificar quais itens foram reprocessados e revalidados") — o badge muda de cor conforme `validado` da própria conta/linha: `--danger` (vermelho) enquanto pendente, verde (`--validada`, mesmo hex de `.status-pill--ativo`) depois de validado; `pidAtualizarValidadoContaContabil()`/`...LinhaDreContabil()`/`...LinhaAnaliseVerticalContabil()` voltaram a enviar só `{ validado }`, sem tocar em `alterada_reprocessamento`. O campo só é limpo de verdade num próximo reprocessamento sem mudança naquela conta/linha específica (`_contabil_sincroniza_*`, backend). **Tooltip do badge mostra o valor de antes do reprocessamento**: Balancete/DRE formatam `valor_anterior_reprocessamento` direto com `pidDcFormatMoeda()`; Análise Vertical usa `pidDcValorAnteriorAvTexto()`, que junta o valor+percentual de cada mês de `valores_anterior_reprocessamento` (mesmo alinhamento posicional de `analise_vertical_meses`) numa linha por mês dentro do mesmo tooltip. Nenhum badge tem tooltip de valor quando o campo vem `null`/vazio (conta/linha nova nesta apuração, sem "antes" pra comparar).
|
||||
Fontes "Manrope" (títulos/cards/abas), "Inter" (corpo, `font-variant-numeric: tabular-nums` nas colunas de valor) e "JetBrains Mono" (fórmula do verso do card). Paleta roxo/dourado dos documentos do escritório (`#3d2178`/`#281552`/`#b4872a`), com gradiente e glow radial sutil no cabeçalho.
|
||||
|
||||
**Tooltip customizado no visual do Portal, não o balão nativo do navegador** (pedido explícito do usuário, depois de ver o balão cinza padrão do Chrome nesse badge e no de "fonte de PDF atípica" abaixo — mesmo raciocínio já aplicado a `pidConfirm()`/`pidAlert()` no lugar de `window.confirm()`/`window.alert()`, ver "Modal de confirmação genérico" no `CLAUDE.md` raiz): `pidDcHoverTooltipHtml(gatilhoHtml, gatilhoClasse, texto)` (novo, `dashboard-contabil.js`) monta um par gatilho+texto (`.dc-hover-tooltip`/`__gatilho`/`__texto`, `dashboard-contabil.css`) — mesma ideia de `.info-tooltip`/`.info-tooltip__text` (components.css), só que com `white-space: pre-line` (preserva o `\n` deliberado entre a frase e "Valor antes do reprocessamento: ..." e ainda envolve o resto normalmente) em vez de `nowrap` (que só serve pro rótulo curto "Mais informações"). Não promovido pra components.css por enquanto — só usado aqui, mas a mecânica é genérica o bastante pra virar um componente compartilhado se outro pacote precisar do mesmo tipo de tooltip mais longo. `tabindex="0"` no gatilho + `:focus-visible`/`:hover` no CSS mostram o tooltip também via teclado, não só no hover do mouse. `pidDcAlteradaBadgeHtml()`/`pidDcFontePdfAtipicaBadgeHtml()` passaram a chamar essa função em vez de escrever `title="..."` direto no `<span>`.
|
||||
> **Regra de ouro para toda animação de entrada aqui: nunca fixar `opacity:0`/`transform` como estilo estático fora de um `@keyframes`, só via `animation: nome duração easing both;`.** Isso garante que `@media print { * { animation: none !important; } }` sozinho devolve o elemento ao estado normal na impressão, sem reset explícito por seletor. Animação nova que não siga isso pode fazer a impressão sair em branco.
|
||||
|
||||
**Bug real reportado pelo usuário logo depois: o balão saía cortado** (em cima da primeira linha da lista, e na lateral perto da borda da tabela) — `.dc-hover-tooltip__texto` nasceu `position:absolute`, mas os badges deste pacote vivem dentro de `.pa-table-wrap` (perfis-acesso.css), que tem `overflow:hidden` pra arredondar o canto da tabela; qualquer coisa `absolute` que escape da própria tabela é cortada por esse `overflow`. Corrigido trocando pra `position:fixed` (relativo à viewport, não mais preso pelo `overflow` de nenhum ancestral) com `top`/`left` calculados em JS (`pidDcPosicionaTooltip()`, topo de `dashboard-contabil.js`) no `mouseenter`/`focus` do gatilho — centralizado acima por padrão, desce pra baixo quando não tem espaço acima (badge perto do topo da tela), clampado nas duas laterais pra nunca vazar da tela (badge perto da borda da tabela). Delegado no `document` com `capture:true` (`mouseenter`/`focus` não borbulham) — um único par de listeners, registrado uma vez no carregamento do script, cobre todo badge atual e futuro, mesmo os recriados a cada re-render de `renderContas()`/`renderDre()`/`renderAnaliseVertical()`/`carregarLista()`.
|
||||
**Contagem animada dos cards**: `data-count`/`data-final`/`data-format`/`data-color-rule` alimentam `pidDcrAnimaContadores()`, que anima de 0 até o valor com `requestAnimationFrame`, formatando os quadros intermediários com `toLocaleString("pt-BR")`. O texto **final** é sempre `data-final`, a string que os filtros Django já geraram — nunca um valor recalculado em JS.
|
||||
|
||||
**Validado com os 3 testes reais**: (1) reprocessar com o **mesmo** arquivo duas vezes seguidas — `validado`/`observacao` de conta/DRE/Análise Vertical preservados, `alterada_reprocessamento` continua `False` em tudo, contagem de linhas idêntica; (2) tentar reprocessar com o arquivo de **outra empresa** — 400 com mensagem explicando a diferença de empresa/competência; (3) tentar reprocessar uma apuração **Concluída** — 400 bloqueado por `_contabil_garante_em_revisao()`. As 4 funções de sincronização também foram testadas isoladamente (dry-run com `transaction.atomic()` + rollback forçado, dados fabricados cobrindo conta/linha inalterada, alterada, nova e removida, mais achado que continua disparando e achado que para de disparar) — todos os casos bateram com o comportamento esperado antes de considerar a implementação pronta. Nenhum resíduo deixado em produção.
|
||||
Cor por sinal só onde é seguro: `sign` (ROA/ROE/EBIT/EBITDA — positivo verde, negativo vermelho) e `liquidez` (verde se ≥ 1). **Kanitz, composição/grau de endividamento e IPL não ganham cor nenhuma** — não existe limiar validado para eles, e colorir como "bom"/"ruim" daria falsa segurança num número que o próprio card diz ser estimativa.
|
||||
|
||||
**Log de reprocessamentos (rodada 142)**: pedido explícito do usuário — "identificar quem reprocessou e quantos reprocessamentos já ocorreu". Model novo `ContabilApuracaoReprocessamento` (`related_name="reprocessamentos"`, `reprocessado_por` FK `SET_NULL`, `reprocessado_em` `auto_now_add`, migração `0075`) — mesmo espírito de `ContabilObservacaoEdicao`, um registro por chamada bem-sucedida. `reprocessar()` cria o registro **dentro** do `with transaction.atomic()`, logo depois de `_contabil_recria_achados()` — uma tentativa que falhar no meio do caminho (PDF de empresa errada, erro de sincronização) não deixa um registro órfão, já que a transação inteira reverte junto.
|
||||
**Cards só animam depois da aba Resumo ser aberta** (contar um número invisível não faz sentido). Isso cria um risco de impressão: se o usuário nunca abrir a aba e mandar imprimir, a contagem começaria do zero na hora da captura. O **único** listener de `beforeprint` do documento decide entre inicializar tudo já no valor final (aba nunca aberta) ou só finalizar uma contagem em andamento — nunca as duas coisas competindo.
|
||||
|
||||
**Achados são recriados do zero a cada reprocessamento (rodada 143, revisão da decisão da rodada 124)**: pedido explícito do usuário — "os apontamentos que são realizados pela própria aplicação e constam na tela de Observações devem ser refeitos [a cada reprocessamento]. Isso já ressalta para o usuário que não há mais inconsistências para validar e pode verificar apenas se surgiram novos. As observações e comentários que ele realizou nas contas devem permanecer independente do reprocessamento." Reverte por completo a decisão da rodada 124 de nunca apagar/sobrescrever `status`/`observacao_contador` de achado — o comportamento anterior deixava um apontamento "tratado" pendurado na aba Observações mesmo depois de a inconsistência sumir dos dados novos, contrariando o próprio propósito de sinalizar o que ainda precisa de atenção.
|
||||
**Flip card**: `.dcr-card` é só a cena 3D (`perspective` + `min-height`, necessário porque as duas faces são `position:absolute` e não contribuem para a altura); `.dcr-card-inner` gira no hover do pai; cada face tem `backface-visibility:hidden` e carrega o fundo/borda/sombra/padding. O verso mostra nome + descrição (`"Sem descrição cadastrada."` se vazia) + a fórmula de exibição, centralizados. Como `@media print` zera toda animação, a impressão sempre mostra a frente.
|
||||
|
||||
`_contabil_sincroniza_achados()` virou `_contabil_recria_achados()` (`views.py`) — em vez de casar por `(regra, código da conta)` e atualizar/preservar campo a campo, agora é `apuracao.achados.all().delete()` seguido de `ContabilAchado.objects.bulk_create(...)` com a lista fresca de `resultado.achados`, **exatamente o mesmo bloco de `create()`** (só que sobre uma apuração já existente). Todo achado nasce `pendente`, mesmo que a mesma combinação `(regra, conta)` já estivesse `tratado`/`ignorado` com uma justificativa escrita antes — a justificativa antiga não é preservada em lugar nenhum, some junto com o achado antigo (decisão explícita: os valores mudaram, então a tratativa escrita sobre os valores antigos não é mais válida).
|
||||
> **O relatório é uma foto estática do momento em que foi gerado.** Editar a definição de um indicador depois não atualiza um relatório já aberto ou baixado — precisa clicar "Gerar Relatório" de novo. Isso já gerou uma dúvida ("o card diz 'Sem descrição cadastrada' mas o indicador tem descrição") que não era bug.
|
||||
|
||||
**Por que isso não quebra a FK de conta/observação**: a preocupação original da rodada 124 (delete+recria quebraria a FK `ContabilAchado.conta` de um achado preservado) não se aplica mais porque não há mais achado "preservado" através do delete — todo achado é recriado do zero, então a FK é sempre resolvida fresca contra `contas_por_codigo` (o mapa de contas **já sincronizadas** por `_contabil_sincroniza_contas()`, que roda antes na mesma transação). `ContabilObservacao` (comentário do contador na conta, pedido explícito do usuário pra continuar intocado) nunca teve relação nenhuma com `ContabilAchado` — é casada por chave natural (empresa+conta), vive num model totalmente separado, e `_contabil_recria_achados()` nem toca nela.
|
||||
### Exportações
|
||||
|
||||
Validado com um script Python isolado (`transaction.atomic()` com rollback forçado, contra uma apuração real já em produção): um achado marcado manualmente como `tratado` com justificativa, seguido de uma chamada simulando `_contabil_recria_achados()` com a mesma lista de achados detectados — o achado tratado desaparece e um novo `pendente` idêntico em conteúdo (mesma regra/conta/mensagem) toma o lugar, com `id` diferente; nada persistido além da migração já aplicada em rodadas anteriores.
|
||||
- **XLSX** (`exportacao.py`, funções puras com openpyxl, recebendo dataclasses `LinhaBalanceteXlsx`/`LinhaDreXlsx`, nunca os models): um botão por seção (Balancete/D.R.E.), `<a href>` puro sem JS. Cabeçalho mesclado (título/empresa/CNPJ/competência), linha de cabeçalho de colunas com fundo roxo, dados a partir da linha 6 com `freeze_panes`. Conta sintética e linha totalizadora ganham negrito + fundo dourado claro. A hierarquia vira `Alignment(indent=nivel)` — não dá para reproduzir toggle numa planilha, então a árvore nasce totalmente expandida. Valores gravados como `float` com `number_format = '"R$" #,##0.00'`, continuam somáveis no Excel.
|
||||
- **PDF do Resumo** (`resumo_pdf.py`, pacote puro, recebe a apuração já carregada e o dict de `_contabil_dados_resumo()`): Resumo do Fechamento + Indicadores + Observações, sem Balancete/D.R.E./Análise Vertical (que já têm o caminho XLSX). Banner roxo/dourado com `logo-branco.png`. O texto rico do contador vira flowables do reportlab (`_resumo_fechamento_flowables()`, via BeautifulSoup): `_inline_markup()` reconstrói `b`/`i`/`u`/`br` aninhados na marcação que o `Paragraph` entende (`strong`→`b`, `em`→`i`), `ul`/`ol` viram `ListFlowable`, e `<img src="data:image/...">` (a única forma que o editor produz) vira um `Image` decodificado de base64 em memória, redimensionado para a largura útil. Seção pulada se o resumo estiver vazio.
|
||||
|
||||
`ContabilApuracaoListSerializer` ganhou `reprocessamentos` (nested, via `ContabilApuracaoReprocessamentoSerializer` — só `id`/`reprocessado_por_nome`/`reprocessado_em`) — não um campo `Detail`, porque o único botão "Reprocessar" da tela vive na **lista** (`data-dc-reprocessar-abrir`, ao lado de "Abrir"), nunca dentro da revisão já aberta; `get_queryset()` ganhou `prefetch_related("reprocessamentos__reprocessado_por")` só pra `action == "list"`, evitando N+1 por apuração (mesmo cuidado que `total_achados_pendentes` já deveria ter tido, mas não tinha — não corrigido aqui, fora do escopo do pedido).
|
||||
**`_contabil_dados_resumo(apuracao)`** é compartilhada por `dashboard()` e `resumo_pdf()`: calcula os grupos de indicadores e devolve `observacoes_visiveis` **sem** separar por `alvo_tipo` nem calcular `ancora` (isso é específico do relatório HTML). `dashboard()` faz a separação e as âncoras por cima.
|
||||
|
||||
**Frontend**: `abrirReprocessarModal(id)` ganhou `renderReprocessarLog(id)`, que procura a apuração em `dcListaApuracoes` (já carregada, sem chamada de API própria) e preenche uma seção nova dentro do próprio `#dc-reprocessar-modal` (`#dc-reprocessar-log`, nasce `hidden` — não aparece nada numa apuração nunca reprocessada) com a contagem (`.dc-reprocessar-log__count`, mesmo padrão visual de `.dc-obs-resumo__count`) e a lista "Nome · dd/mm/aaaa hh:mm" (mais recente primeiro, ordem que já vem do `Meta.ordering` do model). `pidDcFormatDataHora()` (novo, ao lado de `pidDcFormatData()`) é o primeiro formatador de data+hora deste arquivo — os demais (assinatura de observação, "Criado em" da lista) só mostram a data, sem hora; aqui a hora importa porque mais de um reprocessamento pode acontecer no mesmo dia. Como `carregarLista()` já roda depois de um reprocessamento bem-sucedido (pra atualizar a linha da tabela), o log fica correto da próxima vez que o modal for aberto pra mesma apuração, sem nenhuma chamada extra. Validado com um script Python ad-hoc (dentro de `transaction.atomic()` com rollback forçado, contra uma apuração real já em produção): dois registros criados, serializados na ordem certa (mais recente primeiro) e com `reprocessado_por_nome` nulo tratado como esperado; nada persistido.
|
||||
|
||||
### PDF de fonte atípica — título de seção sem acento + aviso ao contador (rodada 125)
|
||||
|
||||
**Bug real, encontrado com um PDF de cliente novo** (`1751 - Balancete 07.2026.pdf`, TAROBA INDUSTRIA HOTELEIRA LTDA): `POST /api/contabil-apuracoes/` devolvia 400 "Nenhuma linha de DRE encontrada no PDF" — o `codigo_empresa`/cabeçalho extraía normalmente, só a seção da DRE nunca era reconhecida. Causa raiz, confirmada rodando `pdfplumber` de verdade contra o arquivo (nunca supor a partir de texto colado — ver `[[feedback_pdf_parser_precisa_arquivo_real]]` na memória): a fonte embutida nesse PDF específico (instalação/versão diferente do Questor) perde o til do "Ã" ao extrair "DEMONSTRAÇÃO DO RESULTADO DO EXERCÍCIO" → sai "DEMONSTRAÇAO..." (só falta o til, resto do caractere sai certo — não é um replacement character). `parser.py` comparava esse título por igualdade exata (`texto.startswith(_TITULO_DRE)`), então a seção nunca era detectada.
|
||||
|
||||
**Correção**: `_normaliza_titulo()` (novo em `parser.py`, usa `unicodedata.normalize("NFKD", ...)` + remoção de acento) compara o título **sem acento** — `_TITULO_DRE_NORM`/`_TITULO_ANALISE_VERTICAL_NORM` calculados uma vez no import do módulo. Mesmo espírito de `_RE_PERIODO` já aceitar `Per[ií]odo` pra essa mesma classe de variação de fonte entre clientes/instalações.
|
||||
|
||||
**Segundo problema, sem correção segura**: o mesmo PDF também tem algumas descrições de conta com palavras coladas (ex. "DEPÓSITOS BANCÁRIOS A VISTA" → "...BANCÁRIOSA VISTA") — o espaçamento entre caracteres dessa fonte varia demais pra um limiar fixo de distância funcionar (`_GAP_ESPACO`, calibrado contra o PDF de referência). Investigação real, não só teórica: (1) medi a distribuição de vãos entre caracteres nesse PDF — o vão "dentro de palavra" (ex. entre "U" e "I" de "EQUIVALENTES") chega a ficar **maior** que um vão real "entre duas palavras curtas" (ex. antes de um "A" sozinho) em alguns pontos, então nenhum limiar único separa os dois casos corretamente; (2) cheguei a cogitar usar os caracteres de espaço literais do próprio PDF como sinal (esse arquivo tem bem menos espaços "de grade" que o de referência), mas descobri que a posição vertical desses espaços às vezes arredonda pra uma linha diferente da do texto real da mesma linha visual, tornando esse sinal não-confiável linha a linha. Testei baixar `_GAP_ESPACO` (ex. pra `0.3`) — conserta alguns casos mas quebra palavras que hoje saem certas (`EQUIVALENTES` vira `EQU IVALENTES`) — decisão explícita do usuário de **não** arriscar essa mudança: valores monetários nunca são afetados, só a descrição de algumas contas, então o custo de regressão supera o benefício.
|
||||
|
||||
**Solução adotada — avisar, não tentar corrigir automaticamente** (pedido explícito do usuário, depois de rejeitar a ideia inicial de um botão de lápis pra editar a descrição manualmente — suspenso por enquanto): `ContabilApuracao.fonte_pdf_atipica` (`BooleanField`, migração `0070`) é `True` quando `_normaliza_titulo()` precisou de verdade (o título bateu sem acento mas não bateria com acento) pra reconhecer a seção DRE ou Análise Vertical — sinal indireto mas real de que este PDF usa uma fonte diferente da do relatório de referência, a mesma classe de variação que já se provou capaz de grudar palavras em descrição de conta. `ResultadoExtracao.fonte_pdf_atipica` (novo campo no dataclass, `dashboard_contabil/modelos.py`) carrega o valor calculado em `extrai_balancete_dre()`; `ContabilApuracaoViewSet.create()`/`.reprocessar()` persistem no model. Exposto em `ContabilApuracaoListSerializer`/`ContabilApuracaoDetailSerializer` (`fonte_pdf_atipica`, sem rota de escrita — `http_method_names` do ViewSet nem inclui PATCH/PUT).
|
||||
|
||||
**Frontend**: `pidDcFontePdfAtipicaBadgeHtml()` (`dashboard-contabil.js`) — ícone de "i" (mesma forma de `PID_DC_OBSERVACAO_ICONE`, cor `--gold` pra diferenciar visualmente, os dois nunca aparecem lado a lado) ao lado do nome da empresa, tanto na linha da lista quanto no cabeçalho `#dc-review-empresa` da tela de revisão (que passou de `.textContent` pra `.innerHTML`, escapando `codigo_empresa`/`nome_empresa` manualmente com `pidDcEscapeHtml()` já que precisa comportar HTML agora). Tooltip customizado (`pidDcHoverTooltipHtml()`, ver "Tooltip customizado no visual do Portal" acima) explica o motivo e pede pra conferir os nomes de conta com atenção — puramente informativo, não bloqueia nem oculta nada.
|
||||
|
||||
**Validado contra os 3 arquivos reais disponíveis**: `fonte_pdf_atipica` calculado `True` só pro `1751` (o PDF com o problema), `False` pros PDFs de referência já validados (`792`, `2017`) — sem falso positivo nos dois já confirmados corretos, e a extração completa do `1751` (252 contas, 231 linhas de DRE, Análise Vertical com 3 meses) bate exatamente com o total esperado depois do fix de `_normaliza_titulo()`.
|
||||
|
||||
### Observação virou histórico por empresa+conta (rodada 126)
|
||||
|
||||
Pedido explícito do usuário: uma observação registrada num mês (o exemplo dele foi um ajuste de estoque) precisa reaparecer na análise do mês seguinte, assinada por quem escreveu e com a data, **bloqueada pra edição** por ser registro histórico, com três caminhos (manter o histórico — padrão; ocultar das próximas execuções; incluir uma observação nova) e filtro de visibilidade ao cliente em todas elas. Até aqui a observação era um campo da linha (`ContabilConta.observacao`/`oculta_no_relatorio` e equivalentes na DRE/Análise Vertical), então morria junto com a competência.
|
||||
|
||||
Três decisões de escopo confirmadas por `AskUserQuestion` **antes** de implementar, todas com a opção recomendada aceita:
|
||||
1. **Toda observação propaga por padrão** — não existe "fixar"; o que existe é o inverso, encerrar explicitamente. Evita histórico que só existe quando alguém lembra de marcar.
|
||||
2. **O histórico cobre Balancete/D.R.E./Análise Vertical** — a justificativa de tratativa de um item de auditoria (`ContabilAchado.observacao_contador`) continua presa à apuração como sempre foi. Misturar os dois fluxos aumentaria o escopo sem ganho claro. (Nota da rodada 143: essa justificativa **não** sobrevive mais a um reprocessamento — o achado inteiro é recriado do zero nesse fluxo, ver "Achados são recriados do zero a cada reprocessamento" mais abaixo; ela só é permanente enquanto a apuração não é reprocessada, diferente de `ContabilObservacao`, que atravessa competências inteiras.)
|
||||
3. **`mostrar_ao_cliente` é sempre alternável**, inclusive numa observação já travada — o bloqueio protege texto, autor e data; mostrar ou não ao cliente é decisão editorial de cada relatório, e uma marcação errada precisa ser corrigível sem reescrever o histórico.
|
||||
|
||||
**Model `ContabilObservacao`** (`portal_api/models.py`, migração `0071`): escopo `codigo_empresa` + `alvo_tipo` (`conta`/`dre`/`analise_vertical`) + `alvo_chave`, mais `alvo_rotulo` (descrição no momento em que foi escrita, só pra exibir se aquela conta sumir do plano), `apuracao_origem` (`SET_NULL`) + `competencia_origem` (cópia, pra vigência continuar resolvendo se a apuração for excluída), `texto`, `mostrar_ao_cliente` (substitui `oculta_no_relatorio`, com o sinal invertido pra bater com o rótulo que o contador vê), `criado_por`/`criado_em` e o trio `encerrada_em_competencia`/`encerrada_por`/`encerrada_em`.
|
||||
|
||||
**Chave natural, nunca FK pra linha**: `"codigo|descricao"` no Balancete, `"descricao|nivel"` na DRE/Análise Vertical (`chave_conta()`/`chave_linha()` no model, `_contabil_chave_alvo()` na view) — exatamente as chaves que `_contabil_sincroniza_*()` já usa no reprocessamento e que `regras.py` usa no histórico de variação. É o que faz a observação seguir a mesma conta de uma competência pra outra, e de brinde tira qualquer risco do reprocessamento sobre ela (antes era preciso garantir explicitamente que `observacao` não fosse tocada; agora ela nem mora lá).
|
||||
|
||||
**Bug real (rodada seguinte) — observação "vazando" pra contas irmãs com a mesma classificação**: a chave do Balancete nasceu só `codigo` (sem `descricao`) — funcionava contra os balancetes de referência usados até então, mas o Questor reaproveita o mesmo código de classificação pra várias contas analíticas de mesma natureza (confirmado pelo usuário: 6 bancos diferentes — Banco do Brasil, Inter, Itaú, Mercado Pago, PagSeguro, Sicredi — todos sob o mesmo código de "Depósitos Bancários à Vista"). Como a chave não distinguia entre eles, uma observação escrita num banco aparecia em todos os outros com o mesmo código. Corrigido trocando a chave pra `"codigo|descricao"` (`ContabilObservacao.chave_conta(codigo, descricao)`, `dcChaveObsConta()` em `dashboard-contabil.js` — mesmo formato dos dois lados) e `_contabil_sincroniza_contas()` (views.py) pra casar contas por `(codigo, descricao)` em vez de só `codigo` no reprocessamento (mesmo trade-off que `_contabil_sincroniza_linhas_dre()` já aceitava: uma conta renomeada, mesmo código, vira uma conta "nova" — a antiga é excluída e outra é criada, em vez de atualizada no lugar). Migração de dados `0073` recalcula o `alvo_chave` de toda `ContabilObservacao` já gravada (`alvo_tipo="conta"`) a partir do próprio `alvo_rotulo` (a descrição da conta congelada no momento em que a observação foi escrita, já armazenada em cada registro) — não precisou reconstruir nada a partir da apuração de origem.
|
||||
|
||||
**Vigência** (`vigentes_para()`/`vigente_em()`): aparece em toda apuração da mesma empresa com `competencia_origem <= C` e (`encerrada_em_competencia` nulo ou `>= C`). Daí saem os três caminhos pedidos: manter é não fazer nada; encerrar grava a competência aberta (a observação **continua visível nela** e some da seguinte em diante, o histórico nunca é reescrito); incluir uma nova cria outro registro, então uma conta passa a ter uma thread, não um texto único.
|
||||
|
||||
**Imutabilidade** (`ContabilObservacaoViewSet._garante_texto_editavel()`): `texto` só é aceito enquanto a apuração de origem estiver "Em revisão"; depois disso (ou numa competência posterior, onde o frontend já mostra a observação travada) a edição é recusada com 400, pedindo pra registrar uma observação nova ou encerrar a existente. `DELETE` segue a mesma regra: histórico não se apaga, se encerra. A regra do backend olha o status da apuração de origem, e a da tela olha `historica` (calculado contra a competência aberta) — as duas convergem no uso real; a diferença só apareceria se alguém editasse pela API uma observação de um mês ainda em revisão estando com outro mês aberto na tela.
|
||||
|
||||
**Endpoints** (mesma permissão de toggle único do resto da ferramenta): `GET /api/contabil-apuracoes/{id}/observacoes/` (recorte de vigência já serializado com `historica`/`encerrada` calculados contra a competência da apuração), `POST /api/contabil-observacoes/` (recebe `apuracao` + `alvo_tipo` + `alvo_id`, o id da conta/linha **desta** apuração — empresa, competência, chave e rótulo são derivados no servidor, mesmo espírito da `chave` derivada em `IndicadorContabilDefinicao`; alvo de outra apuração é 400), `PATCH` (aceita `texto` e/ou `mostrar_ao_cliente`, cada um com sua regra), `DELETE`, e as actions `encerrar`/`reativar` (as duas recebem a apuração aberta no corpo, já que é ela quem define a competência de corte). O viewset não tem `list`/`retrieve` de propósito: a leitura é sempre pelo recorte de vigência de uma apuração.
|
||||
|
||||
**Relatório "Gerar Dashboard"**: `dashboard()` monta as observações vigentes com `mostrar_ao_cliente=True` uma vez e as usa em dois lugares — penduradas em cada linha da árvore (`_contabil_arvore_contexto()` ganhou os parâmetros opcionais `observacoes_por_chave`/`chave_fn`), pro ícone/painel inline por conta, e nas listas `observacoes_contas`/`observacoes_dre`/`observacoes_analise_vertical`, que agora são listas de `ContabilObservacao` (não mais de contas/linhas). Cada item mostra a assinatura e, quando a observação vem de um mês anterior, a competência em que foi registrada.
|
||||
|
||||
**Seção "Observações do X" fica ACIMA da tabela, não abaixo** (pedido explícito do usuário, rodada seguinte, nas três abas — Balancete/D.R.E./Análise Vertical) — só trocou a ordem dos dois `.dcr-secao` dentro de cada `.dcr-tab-panel` (`dashboard-contabil-relatorio.html`), nenhuma mudança de contexto/dado. **Clicar numa observação rola até a conta/linha que ela referencia, "se houver"**: `dashboard()` calcula, pra cada observação, uma `ancora` (setada direto no objeto Python, não um campo do model — `_com_ancora()`, função local dentro de `dashboard()`) igual ao `data-dcr-id` da linha correspondente na árvore (`conta-{id}`/`linha-{id}`/`av-{id}`), casando pela mesma chave natural que `observacoes_por_tipo` já usa (`mapa_conta_por_chave`/`mapa_dre_por_chave`/`mapa_av_por_chave`, construídos a partir de `contas`/`linhas_dre`/`linhas_analise_vertical` **desta** apuração). `ancora` fica `None` quando a conta/linha não existe mais nesta apuração (observação histórica de uma conta que saiu do plano, por exemplo) — o "se houver" do pedido: a observação continua aparecendo normalmente, só sem virar link. No template, só o `<li>` com `obs.ancora` ganha `data-dcr-obs-ir="{{ obs.ancora }}"` + `role="button" tabindex="0"` + classe `.dcr-obs-item--clicavel` (cursor de ponteiro, contorno de foco); as demais continuam com a mesma aparência de sempre.
|
||||
|
||||
`pidDcrArvore(tbodyId)` passou a **devolver** `{ expandeAte, resetar }` em vez de nada — `expandeAte(id)` sobe a cadeia de ancestrais de uma linha (`ancestrais()`, olha `data-dcr-nivel` decrescente a partir do índice da linha) e reabre (`colapsadas[id] = false` + classe `is-expanded` no botão) só os que estiverem colapsados, sem mexer em mais nada da árvore (não é um "expandir tudo", só o caminho necessário até aquela linha) — devolve o `<tr>` já visível, ou `undefined` se `id` não existir. `pidDcrObsResumo(container, arvore)` (nova função) liga o clique/Enter/Espaço num `<li data-dcr-obs-ir>` a `arvore.expandeAte()` + `scrollIntoView({block:"center"})` + um flash de destaque de 1,2s (`.dcr-row-flash`, `@keyframes dcrRowFlash`, anima o `background` do `<td>` a partir do `--dcr-row-bg` que a própria linha já tem, então funciona igual numa linha comum, total ou já destacada). Chamada 3 vezes (uma por `<ul id="dcr-obs-lista-{balancete,dre,av}">`), cada uma recebendo a `arvore` retornada pelo `pidDcrArvore()` da mesma tabela — observação e conta/linha sempre vivem na mesma aba, então nunca precisa trocar de aba pra chegar lá.
|
||||
|
||||
Validado de duas formas: (1) ponta a ponta com `Client.force_login()` em transação com rollback forçado — observação com conta real ganhou `data-dcr-obs-ir` correto, observação "órfã" (chave sem conta correspondente nesta apuração) não ganhou âncora nem classe clicável, e as duas seções aparecem antes da tabela no HTML gerado; (2) **clique de verdade num Chromium headless (Playwright)**, contra o relatório real de uma apuração já em produção (empresa `2017`) — confirmou que `expandeAte()` de fato revela a linha e a página rola até ela, sem erro de JS. Essa segunda rodada de teste só aconteceu **depois** de o usuário reportar "não funciona" já em produção: a causa real não era um bug de código, e sim que `dashboard()` (a mudança que calcula `ancora`) mora em `views.py` — o `runserver` do usuário precisa reiniciar (ou o autoreload do Django precisa pegar a mudança) pra passar a computar isso; a reordenação de HTML (mudança só de template) já aparecia sem reiniciar nada, o que mascarou o diagnóstico por um tempo. Lição: uma mudança em `.py` exige reiniciar o `runserver` pra valer pro usuário testar; mudança só em `.html`/`.css` não exige.
|
||||
|
||||
**Botão "Limpar formatação" (um por tabela) e "Voltar ao topo"** (pedido explícito do usuário, mesma rodada): `resetar()` (novo, dentro do fechamento de `pidDcrArvore`) devolve `colapsadas`/`destaque`/`historico` pro estado inicial do servidor (mesmo critério de `data-dcr-colapsado-padrao` usado na primeira carga) e limpa qualquer `.dcr-row-flash` que tenha sobrado; `pidDcrObs(tbodyId)` também passou a devolver `{ fecharTudo }`, que fecha todo painel de observação inline aberto na tabela. O botão (`data-dcr-reset="dcr-{balancete,dre,av}-body"`) chama os dois de uma vez, via dois `dict`s (`dcrArvores`/`dcrObsControles`) que casam o `tbodyId` do atributo com a `arvore`/painel certos — não desfaz o que está **fora** daquela tabela (cada botão só afeta a própria seção). "Voltar ao topo" (`#dcr-scroll-top-btn`, canto inferior direito, sempre presente no DOM) aparece (`.is-visible`, `opacity`/`transform` com `transition` normal — não é um elemento que entra a partir de `hidden`/`display:none`, então não precisa do padrão `animation` do resto do documento) só depois de `window.scrollY` passar de `PID_DCR_SCROLL_TOP_LIMIAR` (320px), e faz `window.scrollTo({top:0, behavior:"smooth"})` ao clicar. Os dois somem em `@media print` (`.dcr-th-reset-btn`/`.dcr-scroll-top-btn { display: none; }`), mesmo padrão de `.dcr-print-btn`/`.dcr-export-btn`.
|
||||
|
||||
**"Limpar formatação" virou ícone dentro do cabeçalho "Observação"** (rodada seguinte, pedido explícito do usuário): nasceu como um botão com texto solto (borda, padding de pílula, `.dcr-reset-btn`) dentro do `<h2>` de cada seção — passou a ser um ícone só, dentro da própria célula `<th>Observação</th>` de cada tabela, mesmo padrão do botão "Restaurar formatação padrão" que já existe no `<thead>` de Balancete/D.R.E. da tela de revisão (`icon-btn dc-reset-formatacao-btn` dentro de `.dc-th-linha`, ver `dashboard-contabil.html`/`.css`). `.dcr-th-linha` (flex, rótulo à esquerda/ícone à direita) e `.dcr-th-reset-btn` (botão circular 20px, cor `--roxo-escuro`, sem borda) são as classes novas em `dashboard-contabil-relatorio.html`; `.dcr-reset-btn` foi removida por completo (zero consumidor restante). `data-dcr-reset` (o atributo lido pelo JS) não mudou de lugar conceitualmente — só migrou de dentro do `<h2>` pra dentro do `<th>` — então nenhuma linha de `<script>` precisou mudar.
|
||||
|
||||
**Bug real, achado testando essa mudança na Análise Vertical**: essa tabela pode ter bem mais colunas que Balancete/D.R.E. (Descrição + 2 por mês + Observação — normalmente 8 pra 3 meses), e `.dcr-page` (container do relatório inteiro) tem `max-width:1140px`. Com `.dcr-tabela-wrap { overflow: hidden }` (valor original, pensado só pra arredondar os cantos), o navegador espremia cada coluna até quebrar o texto do cabeçalho em 2-3 linhas e cortar a última coluna (Observação, com o ícone novo) pra fora da área visível — o ícone simplesmente não aparecia. Corrigido com duas mudanças em `table.dcr-tabela`/`.dcr-tabela-wrap`: `overflow-x: auto` (era `overflow: hidden`, que zerava os dois eixos — `overflow-y` continua `hidden`, mesmo comportamento vertical de sempre) permite rolagem horizontal quando a tabela precisa de mais espaço do que o container tem; `white-space: nowrap` em `table.dcr-tabela th` impede o cabeçalho de quebrar linha, fazendo a tabela realmente crescer além do container (e então rolar) em vez de espremer as colunas até ficarem ilegíveis. Balancete/D.R.E. (poucas colunas, sempre cabiam sem aperto) não mudam de aparência com isso.
|
||||
|
||||
**Análise Vertical: texto compacto pra caber sem rolagem (pedido explícito do usuário, mesma rodada)**: a rolagem horizontal resolvia o corte, mas o usuário preferiu texto/espaçamento menores pra caber tudo sem precisar arrastar, pelo menos no caso comum de 3 meses. Nova classe `dcr-tabela--compacta` (só no `<table>` da Análise Vertical, `dashboard-contabil-relatorio.html`):
|
||||
- `table.dcr-tabela.dcr-tabela--compacta th, td { padding: 5px 7px; font-size: 0.74rem; }` e `th { font-size: 0.6rem; letter-spacing: 0.01em; }` (era `9px 14px`/`0.85rem`/`0.72rem`/`0.03em`, os valores padrão que Balancete/D.R.E. continuam usando).
|
||||
- `.dcr-th-reset-btn`/`.dcr-toggle`/`.dcr-toggle-spacer`/`.dcr-obs-btn` ganham tamanhos menores (`16px`/`15px`/`15px`/`20px`) só dentro de `.dcr-tabela--compacta` — os ícones precisavam encolher junto com o texto, senão dominariam uma célula já estreita.
|
||||
- **Novo filtro `mes_curto`** (`portal_api/templatetags/contabil_extras.py`) corta o ano do mês pra 2 dígitos (`"mai/2026"` → `"mai/26"`) — usado só no cabeçalho desta tabela (`{{ mes|mes_curto|capfirst }} — Valor`/`— Variação`), já que "— Valor"/"— Variação" repetido em cada coluna por mês era o maior consumidor de largura do cabeçalho (`table.dcr-tabela th { white-space: nowrap }`, ver ajuste anterior). Não usado em nenhum outro lugar do relatório (a lista de observações, por exemplo, continua mostrando a competência por extenso via `{{ ...|competencia }}`, formato diferente — `MM/AAAA` a partir de um `date`, não de uma string "mês/ano" já formatada).
|
||||
|
||||
`overflow-x: auto` (ajuste anterior) continua como rede de segurança — uma apuração com mais de 3 meses ainda pode precisar de rolagem horizontal, já que não há como garantir que qualquer quantidade de colunas caiba num container de largura fixa só encolhendo texto até um certo ponto (ilegibilidade é o limite). O objetivo desta mudança é só o caso comum (3 meses, o normal do PDF Questor) caber inteiro sem arrastar nada.
|
||||
|
||||
**Frontend** (`dashboard-contabil.js`): as observações são carregadas à parte da apuração (`dcCarregarObservacoes()`, chamada ao abrir/criar uma análise) e indexadas por chave natural (`dcObsIndice`), porque não pertencem ao payload da apuração. O editor inline virou uma thread (`dcObsPainelHtml()`/`dcObsItemHtml()`): histórico em cima (autor, data, competência de origem, selos "Histórico"/"Encerrada"/"Editada"/"Aparece ao cliente"/"Interna" e ações de olho, ver histórico de edições (só quando `editada`), editar, encerrar/reativar, **excluir — voltou na rodada 145, ver abaixo**), campo de observação nova embaixo. A exclusão de uma observação vigente também pode passar pelo fluxo de "ocultar das próximas competências" (`encerrar`), sem apagar nada — as duas opções convivem agora (excluir é definitivo e só de quem criou; encerrar é reversível e qualquer um do time pode usar). Um handler único (`dcTrataCliqueObservacao()`) atende as três tabelas e as quatro listas de resumo, e cada mutação refaz o fetch e re-renderiza tudo (`dcRenderObservacoesTudo()`) — a mesma observação pode estar visível em mais de um lugar ao mesmo tempo. O botão da coluna "Observação" ganhou um contador (`.dc-obs-contador`), já que uma conta pode ter várias. Os chips "Todas / Visíveis ao cliente / Internas" (`dcObsFiltro`) existem nas quatro listas e compartilham a mesma variável: filtrar numa aba filtra em todas.
|
||||
|
||||
**Ícone de observação da conta/linha "mãe" também se destaca quando um descendente recolhido tem observação** (mesma rodada do bug acima, pedido explícito do usuário: "caso esteja recolhida, o usuário consegue visualizar se há ou não observações realizadas"): `dcTemObservacaoDescendente(tipo, itens, nivelFn, chaveFn, id)` (nova, reaproveita `dcDescendentes()` — todos os descendentes, não só os filhos diretos) roda pra toda conta/linha sintética (`temFilhos[i]`) nas três árvores (`renderContas()`/`renderDre()`/`renderAnaliseVertical()`) e é passada como quarto argumento de `dcObsBotaoHtml()`. O ícone da própria sintética só ganha o destaque quando ela mesma **não** tem observação própria (senão prevalece o `--preenchida` de sempre) — nova classe `.dc-conta-observacao-btn--descendente` (`dashboard-contabil.css`), cor `--gold` (mesmo tom do estado "parcial" do botão "validado", pra reaproveitar um significado visual já existente de "tem algo pendente de atenção neste grupo" sem inventar uma cor nova). Clicar no ícone continua abrindo o painel da própria conta/linha (que nasce vazio nesse caso) — é só um sinal visual de "tem observação em algum lugar dentro deste grupo recolhido", não um atalho pra ela.
|
||||
|
||||
**Coluna "Conta" (rodada seguinte)**: a aba Balancete da tela de revisão mostrava só a Classificação (`codigo`) numa coluna rotulada "Conta" — o `conta_numero` (numeração interna do Questor, já extraído pelo parser e salvo em `ContabilConta` desde sempre, ver "Models" acima) nunca tinha coluna própria, diferente do PDF original (que traz as duas: "Conta" e "S Classificação"). `dashboard-contabil.html` ganhou uma coluna nova antes da existente — hoje a tabela do Balancete tem 8 colunas: Conta (`conta_numero`) | Classificação (`codigo`) | Descrição | Saldo Anterior | Débito | Crédito | Saldo Atual | Observação. Escopo confirmado com o usuário: só a tela de revisão, não o relatório "Gerar Dashboard" (que continua só com Classificação — não precisa do número interno do Questor pro administrador da empresa). Como a posição das colunas mudou, os seletores CSS que dependiam de índice (`dashboard-contabil.css`, `.dc-contas-table td:nth-child(...)` — alinhamento numérico das colunas de valor, e o estilo apagado/`nowrap` das colunas de identificação da conta) e o colspan do editor inline de observação (`dcObsPainelHtml("conta", ...)` em `dashboard-contabil.js`, `7` → `8`) precisaram ser ajustados junto. Balancete é a única tabela com esse par Conta/Classificação — DRE e Análise Vertical não têm código de classificação nenhum (ver "Extração do PDF" acima), então não são afetadas.
|
||||
|
||||
**Histórico de edições de texto** (`ContabilObservacaoEdicao`, migração `0072`, mesma rodada da remoção do botão de excluir): pedido explícito do usuário — sem a opção de excluir, uma edição de texto precisava deixar rastro visível, pra não virar uma forma indireta de "apagar" uma observação importante reescrevendo por cima. Um registro por `PATCH` que muda `texto` de fato (`texto_novo != observacao.texto` antes de salvar, em `ContabilObservacaoViewSet.partial_update()`) — nunca por `mostrar_ao_cliente`/`encerrar`/`reativar`, que não tocam o conteúdo, e nunca quando o texto enviado é igual ao já salvo. `ContabilObservacaoSerializer` ganhou `editada` (`bool(obj.edicoes.all())`) e `edicoes` (lista aninhada, mais recente primeiro no frontend) — os dois só existem nesse serializer (usado pela tela), o relatório HTML pro cliente (`dashboard()`) não os usa, então o selo/histórico nunca aparece lá. `ContabilApuracaoViewSet.observacoes()` ganhou `.prefetch_related("edicoes__editado_por")` pra não gerar uma query por observação. Frontend: selo `.dc-obs-selo--editada` ("Editada") ao lado dos demais, e um botão de relógio (`data-dc-obs-historico`, `PID_DC_ICON_HISTORICO`) que abre `#dc-obs-historico-modal` (`pidDcAbrirHistoricoObservacao()`) — lista texto anterior (riscado) → texto novo, autor e data de cada edição; usa `obs.edicoes` já carregado junto da observação, sem chamada de API própria. Validado ponta a ponta via `Client.force_login()` dentro de uma transação com rollback forçado: criação sem edição (`editada=False`), primeira edição real de texto cria o registro e vira `editada=True`, reenviar o mesmo texto não duplica, alternar `mostrar_ao_cliente` não gera edição, e uma segunda edição real acumula um segundo registro — nada gravado em produção.
|
||||
|
||||
**Migração de dados**: a `0071` cria o model, copia cada observação preenchida das três tabelas (autor e data vêm da apuração, a melhor aproximação disponível — o modelo antigo não guardava nada disso por observação; `oculta_no_relatorio` vira `mostrar_ao_cliente` invertido) e só então remove os seis campos antigos. Em produção eram 3 observações (2 de conta, 1 de DRE), todas migradas e conferidas depois de aplicar.
|
||||
|
||||
**Botão de excluir voltou, restrito a quem criou e antes de concluir a análise (rodada 145)**: pedido explícito do usuário — "inclua o da lixeira para excluir a observação. A exclusão só poderá ser realizada pelo usuário que criou ela e só pode ser realizada antes do usuário apertar o botão de concluir análise." Reverte parte da decisão da rodada em que o botão foi tirado (ver "Histórico de edições de texto" acima) — a exclusão de verdade volta a existir, mas com uma trava de autoria que não existia antes de ter sido removida, e sem tirar o "encerrar" (que continua sendo a forma reversível/de qualquer um do time de tirar uma observação das próximas competências).
|
||||
|
||||
**Troca de ícone primeiro** (necessária antes de reintroduzir o botão): o ícone de "Encerrar" (`PID_DC_ICON_ENCERRAR`, `dashboard-contabil.js`) por coincidência **era** desenhado como o glifo padrão de lixeira (tampa+alça+corpo) — reaproveitar essa mesma forma pro botão de excluir de verdade criaria dois ícones idênticos com ações bem diferentes (uma reversível, outra não) lado a lado na mesma linha de ações. Trocado por um ícone de "arquivo" (caixa com uma linha), sem nenhuma associação com apagar; `PID_DC_ICON_LIXEIRA` (constante que já existia no arquivo, reaproveitada de `.dc-ind-comp-remover` no construtor de indicador personalizado) passou a ser a lixeira de verdade.
|
||||
|
||||
**Backend** (`views.py`): `ContabilObservacaoSerializer` ganhou `criado_por` (o id da FK, além do `criado_por_nome` que já existia) — é o que o frontend compara contra `me.id` pra decidir se mostra o botão. `ContabilObservacaoViewSet.perform_destroy()` ganhou uma segunda checagem, além de `_garante_texto_editavel()` (a mesma regra de "só enquanto Em revisão" que já valia pro texto): `if instance.criado_por_id != self.request.user.id: raise PermissionDenied(...)`. As duas regras juntas cobrem exatamente o pedido — mostrar/ocultar o botão no frontend é só UX, o servidor confere tudo de novo.
|
||||
|
||||
**Frontend**: `dcObsItemHtml()` (thread inline por conta/linha) e `dcObsResumoItemHtml()` (as 4 listas de resumo) ganharam o botão (`data-dc-obs-excluir`, `PID_DC_ICON_LIXEIRA`) condicionado a `!concluida && obs.criado_por === me.id` nos dois lugares — a mesma regra duplicada em vez de extraída pra uma função, já que são só duas linhas e cada renderer já tem seu próprio jeito de montar a lista de ações. Novo `pidExcluirObservacaoContabil(id)` (`DELETE /api/contabil-observacoes/{id}/`) e um case novo (`data-dc-obs-excluir`) em `dcTrataCliqueObservacao()` — o handler único já compartilhado pelas 3 tabelas + 4 listas de resumo, então um clique em qualquer um dos 7 lugares onde uma observação pode aparecer funciona igual. Usa `pidConfirm(..., { perigoso: true })` antes de excluir (ação destrutiva, mesmo padrão do resto do app — ver "Modal de confirmação genérico" no `CLAUDE.md` raiz), nunca `window.confirm()`.
|
||||
|
||||
Validado via `Client.force_login()` dentro de uma transação com rollback forçado, contra dados reais em produção: (1) um usuário que não criou a observação tenta excluir a de outro → 403; (2) o próprio criador tenta excluir uma observação de uma apuração já **Concluída** → 400 (mesmo `_garante_texto_editavel`); (3) o próprio criador exclui a própria observação numa apuração **Em revisão** → 204, registro realmente some do banco. Nada persistido em produção.
|
||||
|
||||
### Ordenação e filtro por coluna no histórico (`#dc-list-table`)
|
||||
|
||||
Pedido explícito do usuário pra ter a mesma experiência de `#ips-list-table` (Importação de Plano de Saúde) — mesmo mecanismo, client-side, portado 1:1 e renomeado com o prefixo `dc-`/`PID_DC_*` (não compartilhado entre os dois arquivos JS/CSS, cada tela carrega só o próprio):
|
||||
|
||||
- `carregarLista()` agora só busca a API e guarda em `dcListaApuracoes` (module-scope); `renderList()` (nova, sem fetch) filtra (`dcApuracaoPassaNosFiltros`) + ordena (`dcComparaValoresLista`, numérico quando os dois valores parecem número, senão `localeCompare` pt-BR) + desenha as `<tr>` — chamada tanto por `carregarLista()` quanto por qualquer mudança de ordenação/filtro, sem precisar de um novo round-trip à API.
|
||||
- **Ordenação padrão**: `criado_em` decrescente (`dcListSortDir = -1`) — a última execução aparece primeiro, decisão explícita do usuário (antes disso a ordem vinha só do backend, `ContabilApuracao.Meta.ordering = ["-competencia", "-criado_em"]`, que prioriza competência antes de data de criação).
|
||||
- **Colunas ordenáveis** (as 6 do histórico, `data-sort` no `<th>`): `empresa` (por `codigo_empresa`), `competencia`, `observacoes` (`total_achados_pendentes`), `status`, `criado_por` (`criado_por_nome`), `criado_em`.
|
||||
- **Colunas com filtro "estilo Excel"** (funil no `<th>`, popup com busca + checklist, reaproveita `.checklist-*` de `components.css`): `empresa`, `competencia`, `status`, `criado_por` — deixa de fora só `observacoes` (contagem, não uma dimensão de agrupamento) e `criado_em` (data/hora de criação, granularidade fina demais pra um checklist de valores distintos fazer sentido); `competencia` entrou a pedido explícito do usuário numa rodada seguinte (nasceu só ordenável, no mesmo recorte de Importação de Plano de Saúde, que deixa `criado_em` de fora do filtro — mas lá não existe uma coluna de "período" recorrente como esta, em que várias apurações de empresas diferentes tendem a cair na mesma competência). O filtro de "empresa" indexa por `codigo_empresa` (não pelo texto combinado "código - nome" da célula), mas o popup mostra o rótulo completo (`dcLabelValorFiltroLista` busca o `nome_empresa` correspondente em `dcListaApuracoes`) pra continuar identificável; o de "competencia" indexa pela data ISO bruta e mostra o rótulo `MM/AAAA` (`pidDcFormatCompetencia`). **`valoresDistintos()` ordena pelo valor bruto** (`dcComparaValoresLista`), não pelo rótulo formatado — importa justamente pra "competencia", já que ordenar pelo rótulo "MM/AAAA" agruparia por mês antes do ano (ex.: "01/2026" antes de "12/2025"), enquanto a string ISO "AAAA-MM-DD" já ordena cronologicamente certo por comparação simples.
|
||||
- Botão "borracha" (`#dc-list-reset-btn`, dentro do último `<th>` vazio, ao lado da coluna de ações) limpa os quatro filtros e volta a ordenação pro padrão (`criado_em` desc) de uma vez.
|
||||
- Sem paginação (diferente de Importação de Plano de Saúde) — não foi pedida, e o histórico desta ferramenta tende a ser mais curto (uma linha por empresa+competência).
|
||||
|
||||
### Valores monetários dos achados sem formatação BR (bug real, rodada seguinte)
|
||||
|
||||
Usuário reportou (com print da aba Observações da revisão) que os valores em R$ dentro do texto dos achados apareciam sem separador de milhar e com ponto decimal (ex.: "R$ 643545.85"), inconsistente com o padrão brasileiro (ponto de milhar, vírgula decimal) já usado em Balancete/DRE (`pidDcFormatMoeda()` no frontend) e no relatório "Gerar Dashboard" (`moeda()` em `contabil_extras.py`). Causa raiz: `regras.py` interpola `Decimal` **direto** num f-string (`f"R$ {conta.saldo_atual}"`) pra montar `AchadoDetectado.mensagem` — isso usa a formatação padrão do Python (`str(Decimal(...))`), que nunca tem separador de milhar.
|
||||
|
||||
Corrigido com um helper novo, `_moeda(valor: Decimal) -> str` (`regras.py`), duplicado localmente em vez de importado — mesmo padrão já usado (por arquivo) em `indicadores/recibo.py`/`custo_contratacao/pdf.py`/`templatetags/contabil_extras.py`, porque este pacote é Python puro (sem tocar no ORM/app registry do Django, ver docstring do módulo). Todas as 9 mensagens de achado que interpolavam `R$ {valor}` diretamente (`regra_balanceamento_ativo_passivo`, `regra_debito_credito_divergente`, `regra_saldo_negativo_caixa`, `regra_conta_transitoria_com_saldo`, `regra_conta_deveria_zerar`, `regra_saldo_sinal_invertido` — as duas variantes —, `regra_lucro_balancete_diverge_dre`, `regra_descricao_generica`) passaram a usar `_moeda()`.
|
||||
|
||||
**Não corrigido nesta rodada** (fora do escopo do pedido, mesma família de bug): `regra_variacao_atipica_dre` formata percentual com `f"{valor:.2f}%"` (ponto decimal, ex. "12.34%"), também inconsistente com o padrão BR (`12,34%`) — se o usuário pedir, é o mesmo tipo de ajuste.
|
||||
|
||||
**Só vale pra achados gerados dali em diante** — um achado já persistido em produção mantém o texto antigo (sem formatação) até a apuração ser reprocessada (`reprocessar()`/`_contabil_recria_achados()` em `views.py` recria **todo** achado do zero, inclusive `mensagem`, desde a rodada 143 — ver "Achados são recriados do zero a cada reprocessamento" abaixo) ou até uma nova apuração da mesma empresa ser criada do zero; não foi escrita nenhuma migração de dados pra reformatar o texto já gravado (parsear números dentro de frase livre por regex é arriscado — o mesmo texto tem números que não são valores, como código de conta "1.01.01.001").
|
||||
|
||||
**Testado ponta a ponta via `Client.force_login()`** dentro de uma transação com rollback forçado: listagem por vigência, criação com chave derivada no servidor, alvo de outra apuração recusado, edição de texto, bloqueio do texto quando a apuração de origem está concluída (com a visibilidade ainda alternável nesse mesmo caso), encerrar e reativar com as fronteiras de competência conferidas nos dois sentidos, herança numa competência seguinte (observação marcada como histórica) e o relatório gerado nos dois meses, incluindo a conferência de que observação interna não vaza pro relatório do cliente. Nada gravado em produção além da própria migração.
|
||||
|
||||
### Botão "Ver na tabela" nos cards de achado (rodada 146)
|
||||
|
||||
Pedido explícito do usuário a partir de um caso real: a regra `regra_variacao_atipica_dre` gera achados citando a **descrição** de uma linha (ex. "MATERIAIS E SERVIÇOS APLICADOS NA OBRA"), e a mesma descrição pode aparecer em mais de um centro de custo/grupo da Análise Vertical — não dava pra saber, só pelo texto do card, qual linha exata da tabela o achado se referia. Pedido generalizado: "isso poderia ser aplicado para todas as observações identificadas pela aplicação quais referem-se a contas específicas", não só essa regra.
|
||||
|
||||
**Alvo do achado, dois campos mutuamente exclusivos**: a maioria das regras já preenchia `ContabilAchado.conta` (FK opcional pra `ContabilConta`, casada por `codigo_conta` em `AchadoDetectado`, ver "Regras de auditoria v1" acima). `regra_variacao_atipica_dre` é a exceção — não itera `atual.contas` (Balancete), itera `atual.linhas_analise_vertical` (a seção "Demonstração Mensal (Análise Vertical)" do PDF), que não tem código de classificação nenhum, só `descricao`/`nivel`/`ordem`. Pra ela, `ContabilAchado` ganhou `linha_analise_vertical` (FK opcional pra `ContabilLinhaAnaliseVertical`, `SET_NULL`, migração `0076_contabilachado_linha_analise_vertical`) e `AchadoDetectado` ganhou `ordem_linha_analise_vertical: int | None` — `regra_variacao_atipica_dre` preenche com `linha.ordem` (o campo `ordem` já existia em `LinhaAnaliseVerticalExtraida`/`ContabilLinhaAnaliseVertical`, é a posição de leitura do PDF, única dentro da apuração — nenhum campo novo precisou ser criado só pra isso).
|
||||
|
||||
**Casamento por `ordem`, não por descrição**: tanto `ContabilApuracaoViewSet.create()` quanto `_contabil_recria_achados()` (reprocessamento) montam `av_por_ordem = {linha.ordem: linha for linha in apuracao.linhas_analise_vertical.all()}` **depois** de persistir/ressincronizar as linhas da Análise Vertical (`ContabilLinhaAnaliseVertical.objects.bulk_create(...)` em `create()`, `_contabil_sincroniza_linhas_analise_vertical()` em `reprocessar()` — a segunda sempre reescreve `antiga.ordem = linha.ordem` no registro casado, então o dicionário reflete a ordem da extração mais recente mesmo num reprocessamento) e usam isso pra resolver `achado.ordem_linha_analise_vertical → ContabilLinhaAnaliseVertical`. Evita depender de descrição (ambígua entre centros de custo, o próprio motivo desta rodada) ou de id (não existe ainda no momento em que `regras.py` roda, que é Python puro sem tocar o ORM). Validado contra dados reais de produção (apuração 26, 57 achados de `variacao_atipica_dre`): toda `ordem_linha_analise_vertical` retornada pela regra casou com uma `ContabilLinhaAnaliseVertical` de verdade via o mesmo dicionário, dentro de uma transação com rollback forçado.
|
||||
|
||||
`ContabilAchadoSerializer` expõe só o `id` de `linha_analise_vertical` (`read_only`) — o frontend já tem a linha completa cacheada em `apuracaoAtual.linhas_analise_vertical` (carregada junto da apuração), sem precisar de campos espelhados tipo `conta_codigo`/`conta_descricao`. `get_queryset()` do `ContabilApuracaoViewSet` ganhou `"achados__linha_analise_vertical"` no `prefetch_related` (mesmo padrão de `"achados__conta"`).
|
||||
|
||||
**Frontend** (`dashboard-contabil.js`): `renderAchados()` mostra um botão "Ver na tabela" (`PID_DC_ICON_LOCALIZAR`, novo ícone de pin) ao lado do "Revisar", condicionado a `achado.conta != null || achado.linha_analise_vertical != null`. O clique chama `dcIrParaLinha(tipo, id)` (`tipo` é `"conta"` ou `"av"`, roteado pelo campo presente no achado) — a função:
|
||||
|
||||
1. Acha o índice da linha alvo no array certo (`apuracaoAtual.contas`/`apuracaoAtual.linhas_analise_vertical`, via `DC_IR_PARA_CONFIG`).
|
||||
2. Calcula só os **ancestrais** dela (`dcAncestraisIds()`, a mesma pilha de níveis usada em toda a árvore desta tela — `nivel > próximo? tem filho`) e tira só esses ids de `dcContasColapsadas`/`dcAvColapsadas`, não a árvore inteira (diferente do botão "Restaurar formatação", que zera o Set inteiro) — só abre o necessário pra revelar a linha alvo, preservando o resto do estado de expansão que o contador já tinha montado.
|
||||
3. Marca `dcFocoLinha = { tipo, id }` (estado module-scope novo, sempre derivado/temporário — mesmo espírito de `dcContasDestaque`) e re-renderiza a tabela certa, que aplica a classe `.dc-conta-row--foco` só na linha marcada (`renderContas()`/`renderAnaliseVertical()` ganharam essa checagem, junto de um `data-dc-conta-row-id`/`data-dc-av-row-id` novo no `<tr>` pra dar pra selecionar a linha depois).
|
||||
4. Clica programaticamente no botão da aba certa (`#dc-tabs [data-dc-tab="balancete"|"analise-vertical"]`) — reaproveita o listener de troca de aba já existente, sem duplicar a lógica de mostrar/esconder painel.
|
||||
5. Num `requestAnimationFrame` (depois do painel já visível), `scrollIntoView({ behavior: "smooth", block: "center" })` na linha.
|
||||
6. Um `setTimeout` de 2,4s limpa `dcFocoLinha` e re-renderiza de novo, tirando a classe — o pulso da linha é sempre temporário, nunca fica "grudado".
|
||||
|
||||
`.dc-conta-row--foco` (`dashboard-contabil.css`) anima `box-shadow` (não `background-color`, que já está em disputa entre o tingimento padrão e `.dc-conta-row--destaque` — ver o comentário ao lado das duas regras) num pulso de 3 iterações (~2,1s, `@keyframes dcFocoPulse`), então o efeito aparece por cima de qualquer estado que a linha já tenha. A regra cobre `.dc-contas-table`/`.dc-dre-table`, e a Análise Vertical reaproveita porque `<table class="pa-table dc-dre-table dc-av-table">` já carrega as duas classes (mesmo reaproveitamento que `.dc-conta-row--destaque` já fazia).
|
||||
|
||||
**Por que não existe um "ir para a DRE"**: nenhuma regra hoje referencia `ContabilLinhaDre` diretamente (`regra_lucro_balancete_diverge_dre` lê `atual.linhas_dre[-1].valor`, mas o achado é ligado à conta do Balancete, `conta_lucro`, não a uma linha da DRE) — se uma regra nova precisar linkar a DRE simples no futuro, o mesmo padrão (`linha_dre` FK + `ordem_linha_dre` em `AchadoDetectado` + entrada nova em `DC_IR_PARA_CONFIG` com `tab: "dre"`) se replica sem precisar reprojetar nada.
|
||||
|
||||
**Backfill dos achados que já existiam** (migração `0077_backfill_achado_linha_analise_vertical`, mesma rodada): o campo nasceu na `0076`, então todo achado criado antes ficou nulo e **não** ganhava o botão. Na prática isso deixava o recurso invisível — das 4 apurações em produção, 3 tinham *só* achados de `variacao_atipica_dre` (a apuração que o usuário estava olhando tinha 13 de 13), e a única forma de repor o vínculo seria reprocessar, o que **exige reanexar o PDF**. A migração casa cada achado antigo com a linha certa usando a **assinatura extraída da própria mensagem** — descrição + percentual do penúltimo e do último mês (`Linha "X" da DRE foi de A% ... para B% ...`), os três conferidos contra `ContabilLinhaAnaliseVertical.valores[-2:]`. Não re-executa `regras.py` de propósito: migração que importa código de app muda de comportamento se a regra mudar depois, e esta precisa continuar auto-contida.
|
||||
|
||||
**Por que a assinatura completa, e não só a descrição**: a primeira versão do backfill casava por descrição avançando um ponteiro na ordem de leitura (os achados são criados percorrendo as linhas em ordem, então parecia suficiente). Conferido contra produção, **4 dos 78 achados apontaram pra linha errada** — exatamente as descrições repetidas em vários centros de custo (`MATERIAIS E SERVIÇOSAPLICADOS NAOBRA` aparece 4x na apuração 27), que é o caso que originou a rodada: a regra dispara na ocorrência cujos percentuais estouram o limiar, que não é necessariamente a primeira a partir do ponteiro. Com descrição+percentuais, os 78 casaram com a linha certa (conferido comparando os percentuais da linha vinculada com os citados no texto do achado: 0 divergências). O ponteiro continua no algoritmo, mas só como desempate final entre linhas que tenham descrição **e** percentuais idênticos — aí são indistinguíveis pelo dado e a ordem é o único critério que resta. Achado que não casa fica nulo de propósito (o card só não mostra o botão), nada é vinculado por aproximação.
|
||||
|
||||
### Excluir análise bloqueado depois de concluída (rodada 147)
|
||||
|
||||
Pedido explícito do usuário. `ContabilApuracaoViewSet.perform_destroy()` levanta `ValidationError` (400) quando `status == ContabilApuracao.STATUS_CONCLUIDA` — mensagem própria em vez de reusar `_contabil_garante_em_revisao()`, cuja frase fala em editar observações/achados. Fecha a última ação destrutiva fora da trava de "concluiu, não se mexe mais": reprocessar e editar observação/achado já eram bloqueados, excluir a análise inteira (a mais destrutiva das três) não era.
|
||||
|
||||
Frontend (`renderList()`): o ícone de lixeira **some** na linha concluída, mesmo padrão que o botão de reprocessar ao lado já usava — misturar "some" (reprocessar) e "aparece desabilitado" (excluir) na mesma linha de ações seria incoerente. O handler de `[data-dc-excluir]` ganhou `try/catch` + `pidAlert(e.message)`: a trava do servidor continua valendo pra uma lista carregada antes de outra pessoa concluir a análise, e nesse caso o motivo precisa aparecer na tela em vez de o clique não fazer nada.
|
||||
|
||||
**Cuidado ao testar exclusão nesta ViewSet**: `perform_destroy()` chama `instance.arquivo.delete(save=False)`, e apagar arquivo do storage **não é revertido** por `transaction.set_rollback(True)` — o padrão de teste usado no resto desta documentação protege só o banco. Validar o caminho "204" de exclusão contra uma apuração real custou o PDF anexado dela (o registro voltou pelo rollback, o arquivo não; ver CHANGELOG da rodada 147). Nenhum dado analítico depende desse arquivo (`arquivo` não é exposto em serializer nenhum e `reprocessar()` sempre grava um upload novo), mas um teste futuro desse caminho precisa de apuração descartável ou storage isolado.
|
||||
|
||||
### Tabelas abrem com tudo expandido; "+" virou "−" (rodada 148)
|
||||
|
||||
Pedido explícito do usuário: "por padrão, trazer as tabelas com a expansão de todas as contas até o último nível. O comportamento do botão de + deve virar um − e recolher os níveis para que retorne no atual padrão que abre a tabela. O botão de reverter deve reverter ao novo padrão (todas expandidas)."
|
||||
|
||||
Inverte o padrão que valia desde as primeiras rodadas (`dcColapsoPadrao()` no `renderRevisao()`): `renderRevisao()` passa `new Set()` nos três `dc*Colapsadas`, e o estado compacto virou ação sob demanda. O atributo/classe foram renomeados (`dc-expandir-tudo-btn` → `dc-recolher-grupos-btn`) porque "expandir tudo" descreveria o oposto do que o botão faz no estado inicial.
|
||||
|
||||
**Refinamento pedido logo em seguida, na mesma rodada** ("após apertar uma vez, o botão de − vira um + e retorna para o padrão anterior"): a primeira versão não era toggle — o "−" só recolhia, e voltar exigia o botão de restaurar, na outra ponta do cabeçalho. Agora o handler alterna (`colapsadas.size ? new Set() : dcColapsoPadrao(...)`) e `dcAtualizaBotaoArvore()` troca ícone/`title`/`aria-label` a cada render. Simulado contra o Balancete real da apuração 27 (564 contas): abre em "−" com 564 linhas visíveis, clique → "+" com 29, clique → "−" com 564, estável em ciclo; e recolhendo **uma** conta pelo toggle da própria linha o botão já vira "+", que devolve as 564 — o efeito de derivar a face do estado em vez de guardar um flag.
|
||||
|
||||
O destaque não precisou de nenhum ajuste: `dcUltimaLevaVisivel()` com o `Set` vazio já marca as folhas, que é o comportamento que o "+" tinha e que o usuário aprovou. Simulado contra as 4 apurações reais de produção (replicando `dcColapsoPadrao`/`dcUltimaLevaVisivel` em Python): no padrão novo todas as linhas ficam visíveis e o destaque bate com as folhas (Balancete da apuração 27: 564 visíveis, 443 destacadas = as 443 folhas), e o botão "−" reduz pra visão compacta (as mesmas 564 caem pra 29 visíveis). Efeito colateral esperado e aceito: as tabelas abrem bem mais longas (o Balancete dessa apuração passa de 29 pra 564 linhas de saída).
|
||||
|
||||
**O relatório "Gerar Dashboard" não mudou** — continua nascendo recolhido pelo `colapsado_padrao` calculado server-side (`_contabil_arvore_contexto()`, `_CONTABIL_NIVEL_ABERTO_PADRAO`). É a única tela onde `dcColapsoPadrao` ainda é padrão de abertura, e não foi tocada de propósito: é o documento que vai pro cliente, tem só o botão de restaurar (nunca teve o "+"/"−"), e mudar o que o cliente vê não foi pedido. `PID_DC_NIVEL_ABERTO_PADRAO` (JS) e `_CONTABIL_NIVEL_ABERTO_PADRAO` (Python) continuam sendo a mesma constante conceitual duplicada, agora com papéis diferentes em cada lado (sob demanda na revisão, padrão no relatório).
|
||||
## Riscos conhecidos e armadilhas
|
||||
|
||||
- **Códigos de classificação fixos** — se um cliente usar plano de contas com numeração diferente, indicadores e a regra de caixa saem errados **silenciosamente**. Revisar contra mais balancetes reais de empresas diferentes antes de confiar cegamente num valor exibido ao cliente.
|
||||
- **Descrições coladas em PDF de fonte atípica** — não têm correção segura; a ferramenta avisa por badge. Valores monetários nunca são afetados.
|
||||
- **Duas cópias mantidas à mão**: os SVGs de ícone (Python + JS) e a lógica de árvore/destaque (tela de revisão + relatório). Mudança num lado exige o outro.
|
||||
- **Duas constantes conceituais duplicadas**: `PID_DC_NIVEL_ABERTO_PADRAO` (JS) e `_CONTABIL_NIVEL_ABERTO_PADRAO` (Python), hoje com papéis diferentes (sob demanda na revisão, padrão no relatório).
|
||||
- **`total_achados_pendentes` gera N+1** na listagem de apurações.
|
||||
- **Testar exclusão nesta ViewSet destrói arquivo de verdade**: `perform_destroy()` chama `instance.arquivo.delete(save=False)`, e apagar arquivo do storage **não é revertido** por `transaction.set_rollback(True)` — o padrão de teste usado no resto desta documentação protege só o banco. Validar o caminho "204" contra uma apuração real já custou o PDF anexado dela (o registro voltou pelo rollback, o arquivo não). Nenhum dado analítico depende desse arquivo (`arquivo` não é exposto em serializer nenhum e `reprocessar()` sempre grava um upload novo), mas um teste futuro precisa de apuração descartável ou storage isolado. Ver `[[feedback_rollback_nao_desfaz_arquivo]]` na memória.
|
||||
- **Mudança em `.py` exige reiniciar o `runserver`** para o usuário conseguir testar; mudança só em `.html`/`.css`/`.js` não exige. Isso já mascarou um diagnóstico ("não funciona" que não era bug de código). Ver `[[feedback_py_edit_precisa_restart_runserver]]` na memória.
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
# Changelog — Indicador de Desempenho
|
||||
|
||||
> Histórico específico desta aplicação, extraído de `plano.md` (mesma numeração de rodada usada lá, para referência cruzada).
|
||||
> Histórico específico desta aplicação, extraído de `plano.md`.
|
||||
>
|
||||
> **Formato**: uma entrada por rodada, `### Rodada N — Título`; quando a rodada não tem número registrado, `### Título` só. **A numeração de rodada não é global** — cada aplicação conta as próprias, e o mesmo número designa trabalhos diferentes em arquivos diferentes. Ao citar uma rodada, sempre nomear o arquivo. Ver o topo de `plano.md`.
|
||||
|
||||
### Rodada 38 — Indicador de Desempenho (Geradoc)
|
||||
|
||||
|
||||
@ -31,3 +31,26 @@ Segunda ferramenta de Geradoc — substitui a apuração manual do indicador de
|
||||
- **"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).
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
# Changelog — Não Conformidades
|
||||
|
||||
> Histórico específico desta aplicação, extraído de `plano.md` (mesma numeração de rodada usada lá, para referência cruzada).
|
||||
> Histórico específico desta aplicação, extraído de `plano.md`.
|
||||
>
|
||||
> **Formato**: uma entrada por rodada, `### Rodada N — Título`; quando a rodada não tem número registrado, `### Título` só. **A numeração de rodada não é global** — cada aplicação conta as próprias, e o mesmo número designa trabalhos diferentes em arquivos diferentes. Ao citar uma rodada, sempre nomear o arquivo. Ver o topo de `plano.md`.
|
||||
|
||||
### Rodada 88 — Não Conformidades (Relatórios > Qualidade)
|
||||
|
||||
@ -45,7 +47,7 @@ Depois de testar a tela construída na rodada 88, o usuário deu uma lista concr
|
||||
|
||||
**Validado com Playwright** (instalado ad-hoc neste ambiente pra esta rodada — `npx playwright install chromium` + um script Node local, não é dependência do projeto): login, drill-down dos 4 cards, clique numa linha de ranking, expandir ocorrência e ação, alternar tratativa (marcar/reabrir) com o filtro "Tratadas" refletindo a mudança, filtro combinado Prazo+Tratativa+busca — sem erros de console/rede além do 401 esperado da checagem de sessão antes do login.
|
||||
|
||||
### 90. Tempos médios — terceira etapa (execução da ação) e ciclo completo
|
||||
### Rodada 90 — Tempos médios — terceira etapa (execução da ação) e ciclo completo
|
||||
|
||||
Na rodada 89, o terceiro KPI de tempo médio (resolução de ações) tinha sido descartado porque `Data de Finalização`/`Dias para Finalização` estavam 100% vazios no export padrão do Sigsistem (só traz ações em aberto). O usuário trouxe um export diferente — "com finalizadas" (`ocorrencias_pa_28082026-01 - COM FINALZIADAS.xlsx` + `acoes_28082026-01 - com finalizadas.xls`, em `C:\Users\Depaula\Documents\Projetos\Não Conformidades`) — que traz o histórico de ações já concluídas: 3474 linhas (vs. 287 do export normal), com `Data de Finalização` preenchida em 3187 delas.
|
||||
|
||||
@ -55,7 +57,7 @@ Antes de implementar, conferido que o `Dias para Finalização` calculado pelo S
|
||||
- Frontend: primeira versão trocou os cards por um funil visual (`Emissão → Análise → Ação aberta → Ação finalizada`, setas com a média de dias/`n` entre etapas) — **revertido no mesmo dia a pedido do usuário** ("os tempos médios, deixe como estava anteriormente, só inclua as informações do tempo para conclusão"): `.ncf-kpis`/`.ncf-kpi` continuam o card grid original de 2 cards, agora com 4 (análise, abertura, execução, ciclo completo), cada um mostrando `n` como uma segunda linha pequena abaixo do valor. A quebra por categoria (`execucao_por_categoria`) continua como tabelinha (`.pa-table`) abaixo dos cards.
|
||||
- Ver `portal_api/nao_conformidades/CLAUDE.md`, seção "Tempos médios", pro detalhamento técnico.
|
||||
|
||||
### 91. Upload travando com "Erro ao processar a solicitação." — upsert virou lote
|
||||
### Rodada 91 — Upload travando com "Erro ao processar a solicitação." — upsert virou lote
|
||||
|
||||
Usuário reportou erro genérico ao tentar reprocessar os mesmos dois arquivos "com finalizadas" da rodada 90 (`ocorrencias_pa_28082026-01 - COM FINALZIADAS.xlsx` + `acoes_28082026-01 - com finalizadas.xls`) e perguntou se seria a extensão `.xls`/`.xlsx` ou uma incompatibilidade com o ambiente Linux de produção. Nenhum dos dois: reproduzido localmente com os arquivos reais, o upload **funcionava**, só que levava ~60s — tempo o bastante pra estourar o timeout de um worker WSGI (gunicorn, 30s por padrão) em produção, ou disparar o autoreload do `manage.py runserver` em dev, derrubando a conexão no meio sem nenhum erro de dado por trás.
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
# Changelog — Importação de Plano de Saúde
|
||||
|
||||
> Histórico específico desta aplicação, extraído de `plano.md` (mesma numeração de rodada usada lá, para referência cruzada). É a aplicação com mais rodadas do projeto — cada operadora nova, cada regra de custeio e cada bug de parser tem sua própria entrada abaixo.
|
||||
> Histórico específico desta aplicação, extraído de `plano.md`. É a aplicação com mais rodadas do projeto — cada operadora nova, cada regra de custeio e cada bug de parser tem sua própria entrada abaixo.
|
||||
>
|
||||
> **Formato**: uma entrada por rodada, `### Rodada N — Título`; quando a rodada não tem número registrado, `### Título` só. **A numeração de rodada não é global** — cada aplicação conta as próprias, e o mesmo número designa trabalhos diferentes em arquivos diferentes. Ao citar uma rodada, sempre nomear o arquivo. Ver o topo de `plano.md`.
|
||||
|
||||
### Rodada 36 — Importação de Plano de Saúde (Utilitários)
|
||||
|
||||
@ -327,6 +329,16 @@ Usuário reportou (relatório "OPS - Coparticipação Analítico", competência
|
||||
|
||||
Causa: `_REAJUSTE_RE` (`operadoras/itamed/saude.py`) só reconhecia o motivo fixo "Variação de custo" — a linha do dependente ("Reajuste - Mudança de faixa" 65,73, referente a mudança de faixa etária) não batia no regex e ficava de fora da soma (492,66 + 65,73 = 558,39, batendo com o valor correto). Corrigido generalizando o regex pra casar qualquer texto depois de "Reajuste -" (`(?P<motivo>Reajuste\s*-\s*.+?)\s+(?P<valor>[\d.,]+)\s*$`), usando o motivo capturado como `rubrica` do lançamento em vez do texto fixo "Reajuste - Variação de custo" — a lógica de somar como `tipo_lancamento="mensalidade"` não mudou, só passou a valer pra qualquer motivo de reajuste, não só um.
|
||||
|
||||
### Segunda instância da ferramenta: "Importação de Plano de Saúde - De Paula" (2026-08-28)
|
||||
|
||||
Pedido do usuário (commit "Duplicação da Aplicação Importação Plano de Saúde para o RH"): uma segunda aplicação no menu Utilitários, igual à original, mas para o plano de saúde dos **próprios colaboradores do escritório**, com histórico e permissão completamente separados dos clientes.
|
||||
|
||||
- **7 models novos** (`ImportacaoPlanoSaudeDePaula` e companhia, mais `RegraCusteioPlanoSaudeDePaula` e `VinculoNomeOperadoraDePaula`), 5 grupos de rota com sufixo `-de-paula`, um template próprio e uma pasta de upload própria (`media/planos_saude_de_paula/`). Nenhuma FK cruza entre as duas aplicações, de propósito: nada vaza de um histórico para o outro, nem por um filtro esquecido.
|
||||
- **Nada do pacote `portal_api/planos_saude/` foi duplicado** — parsers, `matcher`, `pipeline`, `regras_empresa` e os helpers puros de `views.py` são os mesmos. Operadora nova ou correção de parser vale para as duas aplicações automaticamente.
|
||||
- **Frontend parametrizado em vez de copiado**: `importacao-plano-saude.js`/`.css` servem as duas telas; o template da variante injeta `window.PID_IPS_CONFIG` (appKey, os 5 prefixos de endpoint, título da ajuda) antes de carregar o JS, e a página original cai nos defaults do `Object.assign`.
|
||||
- **Primeira aplicação a nascer restrita ao perfil "Inovação"** por lidar com dado pessoal de colaborador do escritório — `seed_portal.py` força `permissoes["utilitarios"]["apps"]["importacao-plano-saude-de-paula"] = False` em todo perfil que não seja Integração e Inovação. Essa decisão virou convenção geral para aplicações novas com dado sensível (ver `CLAUDE.md` da raiz).
|
||||
- **Documentada só em 2026-09-21**, na rodada de revisão da documentação: até então a aplicação existia em produção (menu, rota, models, template de 911 linhas) sem nenhuma menção em arquivo `.md` nenhum, embora os docstrings de `models.py`/`views.py` já apontassem para uma seção do `CLAUDE.md` desta pasta que nunca tinha sido escrita. Ver "Importação de Plano de Saúde - De Paula (segunda instância da mesma ferramenta)" lá.
|
||||
|
||||
### Rodada 94 — Nome do arquivo gerado por "Nº Empresa - Nº Operadora"
|
||||
|
||||
Pedido do usuário (2026-09-15): o nome do arquivo baixado em "Gerar Arquivo" era sempre genérico (`importacao_plano_saude_<id>.zip` quando 2 tipos de lançamento juntos, `<tipo>.csv` — ex. `mensalidade.csv` — quando só 1), sem nenhuma referência à empresa/operadora daquela execução. Ficava confuso identificar, já no Downloads, duas execuções da mesma empresa com operadoras diferentes (o cenário concreto reportado).
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
> 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/planos_saude/`. Ver também as duas skills de contexto de negócio (`.claude/skills/`), divididas porque a empresa trabalha com mais de um sistema contábil e as etapas finais divergem entre eles: `importacao-plano-saude` (leitura/extração dos arquivos de operadora e regras de negócio, independente do destino — quais operadoras/empresas já estão validadas, o que falta) e `importacao-questor-plano-saude` (a etapa específica do Questor — Cadastro de Regras, planilha padrão, leiaute final). Uma terceira skill pro Contabit ainda não existe.
|
||||
|
||||
Primeira e única aplicação dentro de "Utilitários" (os placeholders "Conversor de Arquivos"/"Calculadora Fiscal" foram removidos do menu — decisão explícita do usuário, não recriar sem confirmar de novo) — importa o relatório de faturamento de uma operadora de plano de saúde/odontológico (Amil, Unimed, ...) e gera o arquivo de lançamento no leiaute fixo do Questor, mais um relatório de auditoria do que não pôde ser lançado automaticamente. Ao contrário dos módulos com tela administrável (Links & Ferramentas, Acessos Gerais, Ramais), essa ferramenta usa permissão de **toggle único** (`{"key": "importacao-plano-saude", "label": "..."}`, entrada flat em `catalogo.MODULE_APPS["utilitarios"]`, sem par visualizar/editar) — quem tem acesso pode fazer todo o fluxo (criar, revisar/editar, gerar), sem conceito de "dono" da importação (mesmo espírito compartilhado de `LinkFerramenta`/`AcessoGeral`). Por ser um app flat, não precisou de nenhum override em `seed_portal.py` (esse cuidado só existe pra pares visualizar/editar).
|
||||
Primeira aplicação dentro de "Utilitários" (os placeholders "Conversor de Arquivos"/"Calculadora Fiscal" foram removidos do menu — decisão explícita do usuário, não recriar sem confirmar de novo; a segunda é a variante "- De Paula", ver a seção logo abaixo) — importa o relatório de faturamento de uma operadora de plano de saúde/odontológico (Amil, Unimed, ...) e gera o arquivo de lançamento no leiaute fixo do Questor, mais um relatório de auditoria do que não pôde ser lançado automaticamente. Ao contrário dos módulos com tela administrável (Links & Ferramentas, Acessos Gerais, Ramais), essa ferramenta usa permissão de **toggle único** (`{"key": "importacao-plano-saude", "label": "..."}`, entrada flat em `catalogo.MODULE_APPS["utilitarios"]`, sem par visualizar/editar) — quem tem acesso pode fazer todo o fluxo (criar, revisar/editar, gerar), sem conceito de "dono" da importação (mesmo espírito compartilhado de `LinkFerramenta`/`AcessoGeral`). Por ser um app flat, não precisou de nenhum override em `seed_portal.py` (esse cuidado só existe pra pares visualizar/editar).
|
||||
|
||||
**A lógica de negócio em si não nasceu neste projeto** — veio de um pipeline Python já testado e documentado em `projects/importacao-planos-saude.skill` (arquivo `.skill`, é um zip — `SKILL.md` + `scripts/`), com um protótipo funcional em `projects/project/` (CLI `main.py`, nunca tocado pelo Portal, fica só como referência/histórico). Esse pipeline foi portado quase 1:1 para dentro do Django em **`portal_api/planos_saude/`** (pacote Python puro, sem depender do ORM):
|
||||
|
||||
@ -76,6 +76,38 @@ Dentro da regra específica, dois grupos de critério, **mutuamente exclusivos**
|
||||
|
||||
Essa divisão é calculada por `matcher._calcula_valores(valor_total, regra)` (chamada por `_aplica_regra_custeio`, que grava `valor_empresa`/`valor` **os dois juntos** a partir do mesmo `valor_total`) — note que o valor "protegido" (empresa ou empregado, conforme o grupo de critério usado) é arredondado primeiro e o outro é derivado como o complemento exato (`valor_total` menos o protegido, também arredondado), nunca os dois arredondados de forma independente, senão a soma dos dois podia ficar 1 centavo a mais/menos que o valor original (ex.: 50% de 51,69 tem que fechar em 25,84 + 25,85 = 51,69, não 25,85 + 25,85). Qual das duas regras (titular ou dependente) usar em cada `Individuo`/`LinhaSistema` é resolvido por `matcher._regra_para_pessoa(regra_por_pessoa, tipo_pessoa)` — `tipo_pessoa` 'T' cai em "titular", 'D'/'A' caem em "dependente" (mesmo critério de "D e A tratados igual" já usado no resto do leiaute) — chamada nos dois pontos de aplicação de `_casa_por_cpf`/`_casa_por_nome` antes de `_aplica_regra_custeio`.
|
||||
|
||||
## "Importação de Plano de Saúde - De Paula" (segunda instância da mesma ferramenta)
|
||||
|
||||
Existe uma **segunda aplicação** no menu Utilitários, `importacao-plano-saude-de-paula` ("Importação de Plano de Saúde - De Paula"), que é a mesma ferramenta apontada para o plano de saúde dos **próprios colaboradores do escritório**, não dos clientes. Os docstrings de `ImportacaoPlanoSaudeDePaula` (`models.py`) e o bloco de comentário das views (`views.py`, logo antes de `_garante_importacao_de_paula_em_revisao`) apontam para esta seção.
|
||||
|
||||
Por que uma cópia em vez de um campo/filtro no mesmo histórico: o dado é de natureza diferente (dado pessoal de colaborador do escritório, não dado de cliente), e a separação garante que **nada cruza** entre as duas, nem por engano, nem por um filtro esquecido numa query. Em troca, tudo que for regra de negócio é compartilhado sem duplicação.
|
||||
|
||||
**O que é duplicado** (tabelas e permissão próprias, sem nenhuma FK cruzando com as originais):
|
||||
|
||||
| Original | Variante De Paula |
|
||||
|---|---|
|
||||
| `ImportacaoPlanoSaude` | `ImportacaoPlanoSaudeDePaula` |
|
||||
| `ImportacaoPlanoSaudeArquivoOperadora` | `ImportacaoPlanoSaudeDePaulaArquivoOperadora` |
|
||||
| `ImportacaoPlanoSaudeLinha` | `ImportacaoPlanoSaudeDePaulaLinha` |
|
||||
| `ImportacaoPlanoSaudeAuditoria` | `ImportacaoPlanoSaudeDePaulaAuditoria` |
|
||||
| `ImportacaoPlanoSaudeAlteracao` | `ImportacaoPlanoSaudeDePaulaAlteracao` |
|
||||
| `RegraCusteioPlanoSaude` | `RegraCusteioPlanoSaudeDePaula` |
|
||||
| `VinculoNomeOperadora` | `VinculoNomeOperadoraDePaula` |
|
||||
| `/api/importacoes-plano-saude/` (+ `-linhas`, `-auditoria`, `-alteracoes`) | os mesmos com sufixo `-de-paula` |
|
||||
| `/api/regras-custeio-plano-saude/` | `/api/regras-custeio-plano-saude-de-paula/` |
|
||||
| `templates/importacao-plano-saude.html` | `templates/importacao-plano-saude-de-paula.html` |
|
||||
| upload em `media/planos_saude/` | upload em `media/planos_saude_de_paula/` |
|
||||
|
||||
O DE/PARA de nomes (`VinculoNomeOperadoraDePaula`) é tabela separada de propósito: um vínculo criado na aplicação de clientes **nunca** vale para a dos colaboradores, e vice-versa.
|
||||
|
||||
**O que é compartilhado** (nada disso foi copiado): todo o pacote `portal_api/planos_saude/` (parsers de operadora, `matcher`, `pipeline`, `regras_empresa`, leiaute do Questor) e os helpers puros de `views.py` (`_salva_arquivo_temporario`, `_valida_planilha_padrao`, `_valida_arquivo_operadora`, `_monta_csv_linhas_plano_saude`, `_snapshot_linha_plano_saude`). **Consequência prática: adicionar uma operadora nova, corrigir um parser ou mexer numa regra de custeio vale automaticamente para as duas aplicações** — não existe (nem deve existir) uma versão "De Paula" de nenhum arquivo do pacote.
|
||||
|
||||
**Frontend: um arquivo só, parametrizado.** `static/js/importacao-plano-saude.js` e `static/css/importacao-plano-saude.css` servem as duas páginas. O template da variante define `window.PID_IPS_CONFIG` num `<script>` inline **antes** de carregar o JS, com `appKey`, os 5 prefixos de endpoint e `tituloAjuda`; a página original não define nada, e o `Object.assign` no topo do JS cai nos defaults, que são os valores da aplicação original. Ao mexer no JS, o caminho certo é sempre `PID_IPS_CONFIG.endpointX`, nunca uma rota literal — uma rota escrita na mão faria a variante gravar no histórico errado.
|
||||
|
||||
**Divergência conhecida e deliberada**: o template da variante não tem os três elementos do resumo da regra de custeio na tela de Revisão (`ips-review-resumo-tipos`, `ips-review-resumo-mensalidade`, `ips-review-resumo-coparticipacao`), adicionados à página original depois da duplicação. O JS testa cada um antes de usar (`if (reviewResumoTipos) {...}`), então a variante simplesmente não mostra esse bloco, sem erro. Se for para igualar as duas telas, é só copiar o markup; o JS já está pronto.
|
||||
|
||||
**Permissão**: `PermissaoApp("utilitarios", "importacao-plano-saude-de-paula")`, independente da original — quem tem acesso a uma não ganha acesso à outra. Foi a **primeira aplicação a seguir a convenção de "nasce restrita ao perfil Inovação"** por lidar com dado pessoal de colaborador do escritório (`seed_portal.py` força a chave para `False` em todo perfil que não seja Integração e Inovação; ver o comentário no próprio arquivo e "Convenção pra aplicação nova que lide com dado sensível/pessoal" no `CLAUDE.md` da raiz). A aplicação original, anterior a essa decisão, continua com a visibilidade ampla de sempre.
|
||||
|
||||
## Modelos (`portal_api/models.py`)
|
||||
|
||||
- `ImportacaoPlanoSaude`: uma execução da ferramenta — `operadora`/`nome_operadora`, `tipos_lancamento` (JSONField, lista), `custeio_por_tipo` (JSONField, `{"mensalidade": {"titular": {"modo": "empresa"|"empregado"|"especifica", "limite_valor": float|None, "percentual": float|None}, "dependente": {...}}, "coparticipacao": {...}}` — ver regra de custeio acima), `regra_empresa` (CharField, blank — chave de `planos_saude.regras_empresa.REGRAS_EMPRESA` quando "mensalidade" foi custeada por uma regra especial em vez do `custeio_por_tipo["mensalidade"]` normal, ver "Regra empresa" abaixo), `planilha_padrao` (`FileField`, mesmo padrão de validator de tamanho de `LinkFerramenta.icone`, só que 15MB em vez de 2MB — é `blank=True` desde que passou a poder vir de uma busca no Questor em vez de upload, ver "Planilha padrão via Questor (SQL)" abaixo), `competencia` (DateField, null — só preenchida quando a origem da planilha padrão foi essa busca no Questor), `status` (`revisao`/`concluida`), `criado_por`, `criado_em`/`concluida_em`. **Com histórico**: decisão explícita do usuário — cada importação fica salva (quem fez, quando, arquivos), não é um fluxo descartável. O arquivo (ou arquivos) da operadora vive num model relacionado separado, ver `ImportacaoPlanoSaudeArquivoOperadora` a seguir e "Múltiplos arquivos de operadora" abaixo.
|
||||
@ -242,3 +274,17 @@ Cobre regras de custeio negociadas com uma empresa específica que não cabem no
|
||||
- Validado rodando `extrai()` + a regra + o pipeline completo (`processa_importacao`) contra o arquivo real da competência 08/2026 — ver detalhe em "Formato Excel (898, Tecnomyl)" acima.
|
||||
- **Trava de compatibilidade generalizada**: `regras_empresa.valida_regra_empresa()` recusa explicitamente (`RegraEmpresaIncompativelError`, capturada à parte em `views.py` pra devolver a mensagem certa, não o erro genérico de "formato de arquivo") se (a) nenhum tipo selecionado na importação está entre os `tipos_lancamento` da regra, (b) a operadora escolhida não usa a `chave_casamento` que a regra exige pra algum tipo coberto, ou (c) a planilha padrão anexada não tem nenhuma linha com o `codigo_empresa` esperado pela regra — trava contra aplicar a regra de uma empresa a outra por engano (vale pras duas regras, não só pra Tecnomyl como antes).
|
||||
- **"Vincular pessoa" (resolução manual de auditoria) também generalizada**: `_recalcula_familia_regra_empresa` (views.py) filtra por `linha.tipo_lancamento` (o tipo da própria linha resolvida), não mais fixo em `"mensalidade"`, e passa esse tipo como segundo argumento pra `regra["aplica"]`; `resolver()` decide se aplica esse caminho checando se `item.tipo_lancamento` está em `REGRAS_EMPRESA[chave]["tipos_lancamento"]`, não mais comparando com a string `"mensalidade"` direto.
|
||||
|
||||
## API (tabela completa)
|
||||
|
||||
| Endpoint | Método | Uso |
|
||||
|---|---|---|
|
||||
| `/api/importacoes-plano-saude/`, `/api/importacoes-plano-saude/{id}/` | GET/POST | histórico + criação; `PermissaoApp("utilitarios", "importacao-plano-saude")` (toggle único) pra todos os métodos; POST é `multipart/form-data` (planilha padrão + arquivo da operadora) e roda o pipeline de forma síncrona antes de responder |
|
||||
| `/api/importacoes-plano-saude/operadoras/` | GET | `[{key, label}]` das operadoras registradas em `planos_saude.pipeline.OPERADORAS` — alimenta o `<select>` do formulário |
|
||||
| `/api/importacoes-plano-saude/regras-empresa/` | GET | `[{key, label}]` das regras especiais registradas em `planos_saude.regras_empresa.REGRAS_EMPRESA` — alimenta o modal "Selecionar regra" do checkbox "Regra empresa" |
|
||||
| `/api/importacoes-plano-saude/{id}/gerar/` | POST | monta o CSV (ou ZIP, se mais de um tipo de lançamento) a partir das linhas já revisadas/editadas e devolve como download binário; marca a importação como `concluida` |
|
||||
| `/api/importacoes-plano-saude-linhas/`, `/api/importacoes-plano-saude-linhas/{id}/` | GET/POST/PATCH/DELETE | edição/inclusão/exclusão de uma linha da revisão (todos os campos, não só valores); mesma permissão da importação, sem conceito de "dono"; as três operações também gravam um `ImportacaoPlanoSaudeAlteracao` |
|
||||
| `/api/importacoes-plano-saude-alteracoes/{id}/reverter/` | POST | desfaz uma alteração específica (edição/inclusão/exclusão de linha) registrada na aba "Alterações" da revisão |
|
||||
| `/api/regras-custeio-plano-saude/`, `/api/regras-custeio-plano-saude/{id}/` | GET/POST/PATCH/DELETE | banco de regras de custeio por empresa+operadora (`codigo_empresa`+`operadora`, únicos juntos+`regra_empresa_chave`+`tipos_lancamento`+`custeio_por_tipo`+`observacoes`; `nome` é sempre derivado, nunca aceito do cliente) — mesma permissão de toggle único da ferramenta; lista compartilhada, sem "dono"; cadastro/edição só pela tela "Cadastro de Regras", nunca em "Nova Importação" |
|
||||
|
||||
> 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).
|
||||
|
||||
13
prd.md
13
prd.md
@ -30,9 +30,17 @@ Colaboradores do escritório, autenticados por login/senha (sessão Django). O a
|
||||
|
||||
### Utilitários (perfis com acesso liberado)
|
||||
- **Importação de Plano de Saúde** — importa o faturamento de uma operadora de plano de saúde/odontológico e gera o arquivo de lançamento no leiaute do Questor, com auditoria do que não casou automaticamente.
|
||||
- **Importação de Plano de Saúde - De Paula** — a mesma ferramenta, para o plano de saúde dos próprios colaboradores do escritório em vez do de clientes. Histórico, cadastro de regras e permissão totalmente separados dos dados de cliente, de propósito. Acesso restrito por padrão (dado pessoal de colaborador).
|
||||
|
||||
### Relatórios (perfis com acesso liberado)
|
||||
- **Não Conformidades** (Relatórios > Qualidade) — gestão contínua das ocorrências e ações do Sistema de Gestão da Qualidade (Sigsistem, ISO 9001), com reabertura automática quando algo muda desde o último tratamento e um dashboard de motivos de abertura e maior incidência. Acesso restrito por padrão.
|
||||
- **Relatório Contábil** (Relatórios > Contabilidade) — o contador anexa o PDF de Balancete + DRE e a ferramenta roda um motor de regras de auditoria, com revisão das observações antes de gerar um relatório pronto para o cliente. Acesso restrito por padrão. Fora de escopo hoje: consolidar várias empresas/competências ao mesmo tempo.
|
||||
|
||||
### Recursos visuais (todo usuário, sem item de menu)
|
||||
- **Temas Sazonais** — troca a marca P.I.D. por uma variante temática durante uma janela de datas (hoje só o Halloween), com um toggle no menu da conta para o usuário desligar por preferência ou acessibilidade. Fora da janela, tudo volta sozinho.
|
||||
|
||||
### Reservados no menu, sem tela própria ainda
|
||||
- **Portais** (Portal do Cliente, Portal Fiscal), **Relatórios** (Indicadores), **Relatórios Gerenciais** (Visão Diretoria, Visão Gerencial), **Integrações** (Questor), **Auditorias** (Consultoria Tributária, Fisco/Contábil) — já existem no catálogo de permissões (menu, controle de acesso), mas ainda não têm funcionalidade implementada. Dentro de **Relatórios**, "Qualidade" (Não Conformidades) e "Contabilidade" (Relatório Contábil) já têm funcionalidade própria — não são placeholder.
|
||||
- **Portais** (Portal do Cliente, Portal Fiscal), **Relatórios** (Indicadores), **Relatórios Gerenciais** (Visão Diretoria, Visão Gerencial), **Integrações** (Questor), **Auditorias** (Consultoria Tributária, Fisco/Contábil) — já existem no catálogo de permissões (menu, controle de acesso), mas ainda não têm funcionalidade implementada.
|
||||
|
||||
## Fora de escopo, por decisão explícita (não implementar sem confirmar de novo)
|
||||
|
||||
@ -46,4 +54,5 @@ Colaboradores do escritório, autenticados por login/senha (sessão Django). O a
|
||||
|
||||
- **Como construir / ordem de execução**: `plano.md`.
|
||||
- **Como o código funciona hoje** (arquitetura, models, endpoints, decisões técnicas): `CLAUDE.md`.
|
||||
- **Contexto de negócio por aplicação** (por quê, limitações conhecidas): skills em `.claude/skills/`.
|
||||
- **Mapa das aplicações** (uma linha por aplicação, com link para a doc de cada uma): `README.md` na raiz.
|
||||
- **Contexto de negócio por aplicação** (por quê, limitações conhecidas): as skills em `.claude/skills/`, que hoje cobrem quatro ferramentas (Importação de Plano de Saúde, a etapa específica do Questor, Indicador de Desempenho e Simulação de Custo de Contratação). As demais aplicações documentam isso no `README.md` da própria pasta.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user