Inclusão relatório contábil Diretoria

This commit is contained in:
Gabriel 2026-10-02 10:07:41 -03:00
parent 236b46c65d
commit 543b1171c7
34 changed files with 2046 additions and 219 deletions

View File

@ -97,7 +97,7 @@ Ver `docs/identidade-visual/identidade-visual.md` para o inventário de cada arq
### Backend serve o frontend (mesma origem) ### Backend serve o frontend (mesma origem)
`config/urls.py` registra `path("api/", include("portal_api.urls"))` e, para cada uma das **15 páginas HTML** do frontend (`index.html` mais os 14 shells — ver a tabela em "Páginas" abaixo), uma rota `TemplateView` que resolve o arquivo em `templates/`. Dois templates não têm rota própria: `dashboard-contabil-relatorio.html` e `conciliacao-fornecedores-relatorio.html` são renderizados por uma view da aplicação correspondente (`/api/contabil-apuracoes/{id}/dashboard/` e `/api/conciliacoes-fornecedores/{id}/relatorio/`), não navegáveis pela URL de página. 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 **16 páginas HTML** do frontend (`index.html` mais os 15 shells — ver a tabela em "Páginas" abaixo), uma rota `TemplateView` que resolve o arquivo em `templates/`. Dois templates não têm rota própria: `dashboard-contabil-relatorio.html` e `conciliacao-fornecedores-relatorio.html` são renderizados por uma view da aplicação correspondente (`/api/contabil-apuracoes/{id}/dashboard/` e `/api/conciliacoes-fornecedores/{id}/relatorio/`), não navegáveis pela URL de página. 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`). 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`).
@ -185,6 +185,7 @@ Pacotes Python puros (sem ORM), um por ferramenta — cada um com seu próprio `
| `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`). | | `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`). |
| `conciliacao-fornecedores.html` | Ferramenta de Utilitários: histórico + nova conciliação (código da empresa com nome buscado no Questor + razão de fornecedores .xlsx/.csv) + detalhe com cards de alerta e tabela de fornecedores expansível, vínculo manual, validação e exportação XLSX (ver `portal_api/conciliacao_fornecedores/CLAUDE.md`). | | `conciliacao-fornecedores.html` | Ferramenta de Utilitários: histórico + nova conciliação (código da empresa com nome buscado no Questor + razão de fornecedores .xlsx/.csv) + detalhe com cards de alerta e tabela de fornecedores expansível, vínculo manual, validação e exportação XLSX (ver `portal_api/conciliacao_fornecedores/CLAUDE.md`). |
| `dashboard-contabil.html` | Aplicação de Relatórios > Contabilidade ("Relatório Contábil" na UI): histórico de análises + nova análise (upload do PDF de Balancete + DRE) + revisão de achados de auditoria (com observações por conta) + botão "Gerar Relatório" (relatório HTML autocontido, ver `portal_api/dashboard_contabil/CLAUDE.md`). | | `dashboard-contabil.html` | Aplicação de Relatórios > Contabilidade ("Relatório Contábil" na UI): histórico de análises + nova análise (upload do PDF de Balancete + DRE) + revisão de achados de auditoria (com observações por conta) + botão "Gerar Relatório" (relatório HTML autocontido, ver `portal_api/dashboard_contabil/CLAUDE.md`). |
| `dashboard-contabil-de-paula.html` | Aplicação da seção Diretoria ("Relatório Contábil - De Paula"): a mesma tela de `dashboard-contabil.html`, para as empresas do próprio escritório, com tabelas, endpoints e permissão próprios. Mesmo JS/CSS, parametrizados por `window.PID_DC_CONFIG` (ver "Relatório Contábil - De Paula" em `portal_api/dashboard_contabil/CLAUDE.md`). |
O item "Calendário De Paula" no menu **não é uma página local** — é um `<a href="#" id="calendario-depaula-btn">` (continua favoritável, já que ainda é um `<a class="nav-item">` — ver `docs/favoritos/favoritos.md`) cujo clique é interceptado em `sidebar.js` (`PID_CALENDARIO_DEPAULA_URL`) pra abrir `https://depaula-tvcorporativa.lovable.app/calendario` num modal com `<iframe>` (`#calendario-depaula-modal`, presente em todo shell) em vez de navegar — mesmo padrão do modal "Novo Chamado" de Ramais (`.ram-chamado-*` em `ramais.css`/`ramais.js`), só que genérico o bastante (`.iframe-modal-*` em `components.css`) pra existir em todo shell, não só em `ramais.html`. Funciona porque esse host (mesmo domínio do "Novo Chamado") não bloqueia ser embutido via `X-Frame-Options`/CSP, ao contrário do Asana (ver `docs/solicitacoes/solicitacoes.md`) — só foi possível confirmar isso testando de fato, não é garantia geral por domínio. Não recriar um `calendario.html` interno sem confirmar com o usuário; o antigo foi removido de propósito. O item "Calendário De Paula" no menu **não é uma página local** — é um `<a href="#" id="calendario-depaula-btn">` (continua favoritável, já que ainda é um `<a class="nav-item">` — ver `docs/favoritos/favoritos.md`) cujo clique é interceptado em `sidebar.js` (`PID_CALENDARIO_DEPAULA_URL`) pra abrir `https://depaula-tvcorporativa.lovable.app/calendario` num modal com `<iframe>` (`#calendario-depaula-modal`, presente em todo shell) em vez de navegar — mesmo padrão do modal "Novo Chamado" de Ramais (`.ram-chamado-*` em `ramais.css`/`ramais.js`), só que genérico o bastante (`.iframe-modal-*` em `components.css`) pra existir em todo shell, não só em `ramais.html`. Funciona porque esse host (mesmo domínio do "Novo Chamado") não bloqueia ser embutido via `X-Frame-Options`/CSP, ao contrário do Asana (ver `docs/solicitacoes/solicitacoes.md`) — só foi possível confirmar isso testando de fato, não é garantia geral por domínio. Não recriar um `calendario.html` interno sem confirmar com o usuário; o antigo foi removido de propósito.
@ -213,7 +214,7 @@ Padrão de guarda por página: `profiles.js`, `users-admin.js` e `widgets.js` ve
## Modelo de permissões (Perfis de Acesso) ## Modelo de permissões (Perfis de Acesso)
O catálogo de módulos/aplicações do menu vive só no backend agora (`portal_api/catalogo.py`) e é buscado uma vez por `profiles.js` via `GET /api/catalogo/` — **não editar mais um `PID_MODULES`/`PID_MODULE_APPS` hardcoded no frontend**; a edição correta é em `catalogo.py`. 11 das 14 seções do menu têm sub-aplicações configuráveis individualmente (`portais`, `geradoc`, `relatorios`, `relatorios-gerenciais`, `utilitarios`, `integracoes`, `auditorias`, `solicitacoes`, `links-ferramentas`, `ramais`, `calendario-individual`); as outras 3 (`principal`, `calendario`, `administracao`) só têm um toggle de módulo, sem filhos. Em `auditorias`, cada entrada pode ser um subgrupo com `tools` aninhadas (ex.: "Consultoria Tributária" → "Controle Simples Nacional") — categorias reais que agrupam ferramentas reais. Em `ramais` e em `links-ferramentas`, o mesmo formato de subgrupo é reaproveitado por outro motivo: cada subgrupo ali **é** uma subtela/aplicação real da seção (ex.: "Ramais"/"Telefones Externos" dentro de `ramais`; "Links & Ferramentas"/"Acessos Gerais" dentro de `links-ferramentas`), e as `tools` dentro de cada subgrupo não são aplicações de verdade — são os dois níveis de acesso (visualizar/editar) daquela subtela (ver "Padrão visualizar/editar" abaixo). Em `calendario-individual`, o único subgrupo (`calendario-individual-eventos`) tem uma **única** `tool` (`calendario-individual-criar-evento`) em vez de um par visualizar/editar — a visualização do módulo em si já é liberada a todo perfil (está em `BASE_KEYS`), só a criação/gestão de eventos corporativos e categorias é restrita (ver "Eventos Corporativos" abaixo). Em `geradoc`, `simulacao-custo-contratacao` e `indicador-desempenho` são dois apps flat (sem subgrupo), cada um com permissão de **toggle único** — mesmo espírito de `importacao-plano-saude` em Utilitários (quem tem acesso faz o fluxo inteiro, sem par visualizar/editar). O catálogo de módulos/aplicações do menu vive só no backend agora (`portal_api/catalogo.py`) e é buscado uma vez por `profiles.js` via `GET /api/catalogo/` — **não editar mais um `PID_MODULES`/`PID_MODULE_APPS` hardcoded no frontend**; a edição correta é em `catalogo.py`. 12 das 15 seções do menu têm sub-aplicações configuráveis individualmente (`portais`, `geradoc`, `relatorios`, `diretoria`, `relatorios-gerenciais`, `utilitarios`, `integracoes`, `auditorias`, `solicitacoes`, `links-ferramentas`, `ramais`, `calendario-individual`); as outras 3 (`principal`, `calendario`, `administracao`) só têm um toggle de módulo, sem filhos. Em `auditorias`, cada entrada pode ser um subgrupo com `tools` aninhadas (ex.: "Consultoria Tributária" → "Controle Simples Nacional") — categorias reais que agrupam ferramentas reais. Em `ramais` e em `links-ferramentas`, o mesmo formato de subgrupo é reaproveitado por outro motivo: cada subgrupo ali **é** uma subtela/aplicação real da seção (ex.: "Ramais"/"Telefones Externos" dentro de `ramais`; "Links & Ferramentas"/"Acessos Gerais" dentro de `links-ferramentas`), e as `tools` dentro de cada subgrupo não são aplicações de verdade — são os dois níveis de acesso (visualizar/editar) daquela subtela (ver "Padrão visualizar/editar" abaixo). Em `calendario-individual`, o único subgrupo (`calendario-individual-eventos`) tem uma **única** `tool` (`calendario-individual-criar-evento`) em vez de um par visualizar/editar — a visualização do módulo em si já é liberada a todo perfil (está em `BASE_KEYS`), só a criação/gestão de eventos corporativos e categorias é restrita (ver "Eventos Corporativos" abaixo). Em `geradoc`, `simulacao-custo-contratacao` e `indicador-desempenho` são dois apps flat (sem subgrupo), cada um com permissão de **toggle único** — mesmo espírito de `importacao-plano-saude` em Utilitários (quem tem acesso faz o fluxo inteiro, sem par visualizar/editar).
Formato de perfil no banco (`PerfilAcesso.permissoes`, `JSONField`): Formato de perfil no banco (`PerfilAcesso.permissoes`, `JSONField`):
```json ```json
@ -258,6 +259,8 @@ Isso reaproveita 100% a árvore de permissões que já existe (`renderTree()`/`r
**Cuidado com `seed_portal.py`**: o módulo continua tendo seu próprio `enabled` (visibilidade no menu) e, se estiver em `BASE_KEYS`/`SECTORAL_KEYS` etc., `catalogo.permissions_from_keys()` habilita **todos** os apps de um módulo enabled de uma vez — incluindo todo `*-editar`. Por isso `seed_portal.py` força `permissoes["links-ferramentas"]["apps"]["links-ferramentas-editar"] = False`, `permissoes["links-ferramentas"]["apps"]["acessos-gerais-editar"] = False` e `permissoes["ramais"]["apps"]["ramais-editar"] = False` (+ as outras duas subtelas administráveis de Ramais) explicitamente pra todo perfil que não seja "Integração e Inovação" (código 8, `chaves=None` → todos os apps `True` de propósito), depois de gerar o dict — sem esse override, qualquer perfil com o módulo habilitado nasceria com poder de editar tudo. Ao adicionar uma aplicação nova nesse padrão (dentro de um módulo existente ou não), replicar esse mesmo cuidado no seed. **Cuidado com `seed_portal.py`**: o módulo continua tendo seu próprio `enabled` (visibilidade no menu) e, se estiver em `BASE_KEYS`/`SECTORAL_KEYS` etc., `catalogo.permissions_from_keys()` habilita **todos** os apps de um módulo enabled de uma vez — incluindo todo `*-editar`. Por isso `seed_portal.py` força `permissoes["links-ferramentas"]["apps"]["links-ferramentas-editar"] = False`, `permissoes["links-ferramentas"]["apps"]["acessos-gerais-editar"] = False` e `permissoes["ramais"]["apps"]["ramais-editar"] = False` (+ as outras duas subtelas administráveis de Ramais) explicitamente pra todo perfil que não seja "Integração e Inovação" (código 8, `chaves=None` → todos os apps `True` de propósito), depois de gerar o dict — sem esse override, qualquer perfil com o módulo habilitado nasceria com poder de editar tudo. Ao adicionar uma aplicação nova nesse padrão (dentro de um módulo existente ou não), replicar esse mesmo cuidado no seed.
**Módulo novo no catálogo e perfis já gravados**: como `seed_portal` não roda em produção, um módulo (seção do menu) novo em `catalogo.MODULES` não entra sozinho no JSON dos perfis existentes. O servidor já trata chave ausente como desabilitada (`permissoes_efetivas()`, `Usuario.permissao_app()`), então ninguém ganha acesso por engano. Na tela de Perfis de Acesso, `pidCompletaPermissoes()` (`profiles.js`) completa módulo/app ausente como desmarcado ao carregar cada perfil, sem alterar valor existente; sem isso, abrir um perfil quebrava a tela ao ler `permissions[modulo].enabled`. A chave só passa a ser gravada quando o perfil é salvo. Primeiro caso: a seção "Diretoria".
**Convenção pra aplicação nova que lide com dado sensível/pessoal (não dado de cliente comum)**: decisão explícita do usuário (rodada em que "Importação de Plano de Saúde - De Paula" foi criada, ver `portal_api/planos_saude/CLAUDE.md`) — daqui pra frente, toda aplicação nova adicionada ao catálogo que trate dado pessoal de colaborador do próprio escritório (em vez de dado de cliente, que é o caso comum do resto do Portal) deve **nascer restrita só ao perfil "Inovação"** (código 8), mesmo padrão já usado por "Não Conformidades" (`permissoes["relatorios"]["apps"]["nao-conformidades"] = False` no bloco de overrides) — nunca herdar a visibilidade ampla padrão de `BASE_KEYS`/`SECTORAL_KEYS`. Outros perfis que precisarem ganham acesso depois, manualmente, pela tela de Perfis de Acesso. **Não retroagir** essa convenção nas aplicações de toggle único que já existiam antes desta decisão (Importação de Plano de Saúde original, Simulação de Custo de Contratação, Indicador de Desempenho) — essas continuam com a visibilidade ampla de sempre, decisão explícita do usuário de não mexer no que já está em uso. **Convenção pra aplicação nova que lide com dado sensível/pessoal (não dado de cliente comum)**: decisão explícita do usuário (rodada em que "Importação de Plano de Saúde - De Paula" foi criada, ver `portal_api/planos_saude/CLAUDE.md`) — daqui pra frente, toda aplicação nova adicionada ao catálogo que trate dado pessoal de colaborador do próprio escritório (em vez de dado de cliente, que é o caso comum do resto do Portal) deve **nascer restrita só ao perfil "Inovação"** (código 8), mesmo padrão já usado por "Não Conformidades" (`permissoes["relatorios"]["apps"]["nao-conformidades"] = False` no bloco de overrides) — nunca herdar a visibilidade ampla padrão de `BASE_KEYS`/`SECTORAL_KEYS`. Outros perfis que precisarem ganham acesso depois, manualmente, pela tela de Perfis de Acesso. **Não retroagir** essa convenção nas aplicações de toggle único que já existiam antes desta decisão (Importação de Plano de Saúde original, Simulação de Custo de Contratação, Indicador de Desempenho) — essas continuam com a visibilidade ampla de sempre, decisão explícita do usuário de não mexer no que já está em uso.
**Bug real (rodada 69) — `IntegrityError: duplicate key value violates unique constraint "portal_api_perfilacesso_pkey"` ao criar um perfil pela tela**: `seed_portal.py` semeia `PerfilAcesso` com `codigo` **explícito** (`update_or_create(codigo=dado["codigo"], ...)`, já que os 8 códigos 1–8 são referenciados por número fixo em vários lugares do código — ex.: `codigo == 8` = "Integração e Inovação"). No Postgres, um `INSERT` com PK explícita **nunca avança a sequence** por trás do `AutoField` — então a sequence ficava parada em 1 (seu valor inicial), e o primeiro perfil criado pela tela (`POST /api/perfis/`, sem PK explícita) recebia `codigo=1` do `nextval()`, colidindo com um código já usado pelo seed. `_reset_sequence(model)` (função módulo-level em `seed_portal.py`, roda `SELECT setval(pg_get_serial_sequence(...), MAX(pk))` via SQL puro do Postgres) corrige isso, chamada logo depois do loop de `PERFIS_SEED` — toda vez que `seed_portal` roda, a sequence é realinhada de novo. Se esse erro voltar a aparecer no futuro (ex.: um `loaddata`/`RunPython` de migração também inserindo `PerfilAcesso` com PK explícita sem passar por `seed_portal.py` depois), o comando pra corrigir manualmente é `python manage.py seed_portal` (idempotente, seguro rodar de novo) — não precisa de acesso direto ao banco. **Bug real (rodada 69) — `IntegrityError: duplicate key value violates unique constraint "portal_api_perfilacesso_pkey"` ao criar um perfil pela tela**: `seed_portal.py` semeia `PerfilAcesso` com `codigo` **explícito** (`update_or_create(codigo=dado["codigo"], ...)`, já que os 8 códigos 1–8 são referenciados por número fixo em vários lugares do código — ex.: `codigo == 8` = "Integração e Inovação"). No Postgres, um `INSERT` com PK explícita **nunca avança a sequence** por trás do `AutoField` — então a sequence ficava parada em 1 (seu valor inicial), e o primeiro perfil criado pela tela (`POST /api/perfis/`, sem PK explícita) recebia `codigo=1` do `nextval()`, colidindo com um código já usado pelo seed. `_reset_sequence(model)` (função módulo-level em `seed_portal.py`, roda `SELECT setval(pg_get_serial_sequence(...), MAX(pk))` via SQL puro do Postgres) corrige isso, chamada logo depois do loop de `PERFIS_SEED` — toda vez que `seed_portal` roda, a sequence é realinhada de novo. Se esse erro voltar a aparecer no futuro (ex.: um `loaddata`/`RunPython` de migração também inserindo `PerfilAcesso` com PK explícita sem passar por `seed_portal.py` depois), o comando pra corrigir manualmente é `python manage.py seed_portal` (idempotente, seguro rodar de novo) — não precisa de acesso direto ao banco.
@ -355,6 +358,8 @@ Chamado de "Dashboard Contábil" até uma rodada anterior — renomeado pra "Rel
Otimiza a conferência de balancetes hoje feita manualmente pelo Fisco/Contábil (`ITD-FISCO-7513`): o contador anexa o PDF de Balancete + DRE (modelo Questor ou Contabit, detectado sozinho, mesmo relatório hoje enviado ao cliente; no Contabit o código da empresa vem do nome do arquivo e a seção de indicadores fica oculta), a ferramenta extrai as contas e roda um motor de regras de auditoria (saldos negativos, contas transitórias/genéricas com saldo, contas que deveriam ficar zeradas, débito ≠ crédito, variações atípicas mês a mês contra o histórico já processado no Portal), com revisão de achados e observações por conta antes da conclusão (a observação fica no histórico da empresa + conta e reaparece assinada nas competências seguintes, até ser encerrada) — cada conta/linha também ganha um checkbox de "validado" (marcador informativo de que o contador já conferiu aquele item) e um editor de observação inline na própria tabela (não mais um popup), com um toggle "mostrar ao cliente" que nasce desmarcado por padrão. O botão "Gerar Relatório" gera um relatório HTML autocontido (indicadores financeiros, gráfico de evolução, DRE/Balancete agrupados, observações do contador — com a marca do escritório, não a "P.I.D." do Portal), com exportação de Balancete/DRE em XLSX a partir dele. **Ainda fora de escopo**: consolidação entre várias empresas/competências ao mesmo tempo (substituiria o BI Contábil por completo). Otimiza a conferência de balancetes hoje feita manualmente pelo Fisco/Contábil (`ITD-FISCO-7513`): o contador anexa o PDF de Balancete + DRE (modelo Questor ou Contabit, detectado sozinho, mesmo relatório hoje enviado ao cliente; no Contabit o código da empresa vem do nome do arquivo e a seção de indicadores fica oculta), a ferramenta extrai as contas e roda um motor de regras de auditoria (saldos negativos, contas transitórias/genéricas com saldo, contas que deveriam ficar zeradas, débito ≠ crédito, variações atípicas mês a mês contra o histórico já processado no Portal), com revisão de achados e observações por conta antes da conclusão (a observação fica no histórico da empresa + conta e reaparece assinada nas competências seguintes, até ser encerrada) — cada conta/linha também ganha um checkbox de "validado" (marcador informativo de que o contador já conferiu aquele item) e um editor de observação inline na própria tabela (não mais um popup), com um toggle "mostrar ao cliente" que nasce desmarcado por padrão. O botão "Gerar Relatório" gera um relatório HTML autocontido (indicadores financeiros, gráfico de evolução, DRE/Balancete agrupados, observações do contador — com a marca do escritório, não a "P.I.D." do Portal), com exportação de Balancete/DRE em XLSX a partir dele. **Ainda fora de escopo**: consolidação entre várias empresas/competências ao mesmo tempo (substituiria o BI Contábil por completo).
**Relatório Contábil - De Paula** (seção **Diretoria** do menu, `diretoria` > `dashboard-contabil-de-paula`): segunda instância para as empresas do próprio escritório. Regras, cadastro de indicadores e código compartilhados com a original (models como base abstrata + duas concretas, ViewSets e JS parametrizados); análises, histórico, observações, endpoints (`-de-paula`) e permissão separados, sem FK cruzando. Requisito do usuário: quem tem acesso só ao Relatório Contábil não acessa a De Paula em hipótese alguma. O módulo `diretoria` fica fora de `BASE_KEYS`/`SECTORAL_KEYS`, então nasce fechado para todo perfil e é liberado manualmente pela tela.
Ver `portal_api/dashboard_contabil/CLAUDE.md`. Ver `portal_api/dashboard_contabil/CLAUDE.md`.
## CSS — organização entre arquivos ## CSS — organização entre arquivos
@ -363,7 +368,7 @@ Ver `portal_api/dashboard_contabil/CLAUDE.md`.
|---|---| |---|---|
| `tokens.css` | Variáveis (`:root`, tema claro em `:root[data-theme="light"]`). | | `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`). | | `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); 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`. | | `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`, `dashboard-contabil-de-paula.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). | | `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`. | | `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`. | | `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`. |

View File

@ -44,6 +44,7 @@ Não há suíte de testes, lint ou build configurados neste projeto.
| Indicador de Desempenho | Geradoc | [portal_api/indicadores/](portal_api/indicadores/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) | | 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) | | Relatório Contábil | Relatórios > Contabilidade | [portal_api/dashboard_contabil/](portal_api/dashboard_contabil/README.md) |
| Relatório Contábil - De Paula (mesma ferramenta, para as empresas do próprio escritório) | Diretoria | [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) | | 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) | | 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) |

View File

@ -28,6 +28,7 @@ PAGINAS_FRONTEND = [
"indicador-desempenho.html", "indicador-desempenho.html",
"nao-conformidades.html", "nao-conformidades.html",
"dashboard-contabil.html", "dashboard-contabil.html",
"dashboard-contabil-de-paula.html",
] ]
urlpatterns = [ urlpatterns = [

View File

@ -599,6 +599,28 @@ Nada de acessibilidade de teclado/leitor de tela nem `prefers-reduced-motion`.
`runserver`. Todos os JS alterados passaram em `node --check`. **Não testado no `runserver`. Todos os JS alterados passaram em `node --check`. **Não testado no
navegador** (sem renderização neste ambiente). navegador** (sem renderização neste ambiente).
### 152. Seção "Diretoria" no menu e módulo novo em perfis já gravados (2026-10-02)
Parte estrutural da criação do Relatório Contábil - De Paula (detalhe da
aplicação na rodada 157 de `portal_api/dashboard_contabil/CHANGELOG.md`).
- **Seção nova "Diretoria"** (`diretoria` em `catalogo.MODULES`, entre
Relatórios e Relatórios Gerenciais, pedido do usuário), para ferramentas das
empresas do próprio escritório. Fora de `BASE_KEYS`/`SECTORAL_KEYS`: nasce
fechada para todo perfil, liberação manual pela tela. Bloco do menu
acrescentado nos 15 shells (14 existentes + `dashboard-contabil-de-paula.html`).
- **Correção em Perfis de Acesso**: a tela lia `permissions[modulo].enabled`
direto do JSON salvo, e um módulo novo não existe no JSON dos perfis já
gravados (o `seed_portal`, que acrescentava, não roda mais em produção).
Abrir qualquer perfil quebraria a tela. `pidCompletaPermissoes()`
(`profiles.js`) completa módulo/app ausente como desmarcado ao carregar, sem
alterar valor existente. Registrado no `CLAUDE.md` ("Módulo novo no catálogo
e perfis já gravados").
**Validação**: catálogo e `/api/me/` conferidos pelo cliente de teste do
Django; normalização conferida contra o JSON real do perfil 8 (nenhum valor
alterado, `diretoria` acrescentada desmarcada). **Não testado no navegador.**
## Limitações conhecidas / decisões assumidas ## Limitações conhecidas / decisões assumidas
- Calendário De Paula (empresa) não tem mais versão interna — depende - Calendário De Paula (empresa) não tem mais versão interna — depende

View File

@ -13,13 +13,21 @@ from .models import (
ConciliacaoFornecedorVinculo, ConciliacaoFornecedorVinculo,
ConciliacaoFornecedorVinculoHistorico, ConciliacaoFornecedorVinculoHistorico,
ContabilAchado, ContabilAchado,
ContabilAchadoDePaula,
ContabilApuracao, ContabilApuracao,
ContabilApuracaoDePaula,
ContabilApuracaoReprocessamento, ContabilApuracaoReprocessamento,
ContabilApuracaoReprocessamentoDePaula,
ContabilConta, ContabilConta,
ContabilContaDePaula,
ContabilLinhaAnaliseVertical, ContabilLinhaAnaliseVertical,
ContabilLinhaAnaliseVerticalDePaula,
ContabilLinhaDre, ContabilLinhaDre,
ContabilLinhaDreDePaula,
ContabilObservacao, ContabilObservacao,
ContabilObservacaoDePaula,
ContabilObservacaoEdicao, ContabilObservacaoEdicao,
ContabilObservacaoEdicaoDePaula,
Departamento, Departamento,
EmpresaQuestor, EmpresaQuestor,
Favorito, Favorito,
@ -316,6 +324,17 @@ class ContabilAchadoAdmin(admin.ModelAdmin):
search_fields = ("titulo", "mensagem") search_fields = ("titulo", "mensagem")
# Relatório Contábil - De Paula (Diretoria): mesmas telas de admin da original.
admin.site.register(ContabilApuracaoDePaula, ContabilApuracaoAdmin)
admin.site.register(ContabilApuracaoReprocessamentoDePaula, ContabilApuracaoReprocessamentoAdmin)
admin.site.register(ContabilContaDePaula, ContabilContaAdmin)
admin.site.register(ContabilLinhaDreDePaula, ContabilLinhaDreAdmin)
admin.site.register(ContabilLinhaAnaliseVerticalDePaula, ContabilLinhaAnaliseVerticalAdmin)
admin.site.register(ContabilObservacaoDePaula, ContabilObservacaoAdmin)
admin.site.register(ContabilObservacaoEdicaoDePaula, ContabilObservacaoEdicaoAdmin)
admin.site.register(ContabilAchadoDePaula, ContabilAchadoAdmin)
@admin.register(ConciliacaoFornecedor) @admin.register(ConciliacaoFornecedor)
class ConciliacaoFornecedorAdmin(admin.ModelAdmin): class ConciliacaoFornecedorAdmin(admin.ModelAdmin):
list_display = ("codigo_empresa", "nome_empresa", "data_base", "criado_por", "criado_em") list_display = ("codigo_empresa", "nome_empresa", "data_base", "criado_por", "criado_em")

View File

@ -16,6 +16,7 @@ MODULES = [
{"key": "portais", "label": "Portais"}, {"key": "portais", "label": "Portais"},
{"key": "geradoc", "label": "Geradoc"}, {"key": "geradoc", "label": "Geradoc"},
{"key": "relatorios", "label": "Relatórios"}, {"key": "relatorios", "label": "Relatórios"},
{"key": "diretoria", "label": "Diretoria"},
{"key": "relatorios-gerenciais", "label": "Relatórios Gerenciais"}, {"key": "relatorios-gerenciais", "label": "Relatórios Gerenciais"},
{"key": "utilitarios", "label": "Utilitários"}, {"key": "utilitarios", "label": "Utilitários"},
{"key": "integracoes", "label": "Integrações"}, {"key": "integracoes", "label": "Integrações"},
@ -63,6 +64,14 @@ MODULE_APPS = {
"tools": [{"key": "dashboard-contabil", "label": "Relatório Contábil"}], "tools": [{"key": "dashboard-contabil", "label": "Relatório Contábil"}],
}, },
], ],
# Ferramentas para as empresas do próprio escritório. Fora de BASE_KEYS e
# SECTORAL_KEYS de propósito: nenhum perfil ganha acesso automático, a
# liberação é sempre manual pela tela de Perfis de Acesso. O Relatório
# Contábil - De Paula tem permissão própria, independente da
# `dashboard-contabil` de Relatórios.
"diretoria": [
{"key": "dashboard-contabil-de-paula", "label": "Relatório Contábil - De Paula"},
],
"relatorios-gerenciais": [ "relatorios-gerenciais": [
{"key": "visao-diretoria", "label": "Visão Diretoria"}, {"key": "visao-diretoria", "label": "Visão Diretoria"},
{"key": "visao-gerencial", "label": "Visão Gerencial"}, {"key": "visao-gerencial", "label": "Visão Gerencial"},

View File

@ -546,3 +546,17 @@ Pedido do usuário, na mesma linha da Conciliação de Fornecedores (rodada 59 d
- Os PDFs já guardados ficam em `media/contabil/apuracoes/` (decisão do usuário: não excluir); sem a coluna, nenhum registro aponta mais para eles. Foram apagados por engano nesta rodada e restaurados do git só os versionados (1 de 12); os demais têm cópia em `Projetos\Balancetes`/Downloads. As 12 análises continuam abrindo normalmente. - Os PDFs já guardados ficam em `media/contabil/apuracoes/` (decisão do usuário: não excluir); sem a coluna, nenhum registro aponta mais para eles. Foram apagados por engano nesta rodada e restaurados do git só os versionados (1 de 12); os demais têm cópia em `Projetos\Balancetes`/Downloads. As 12 análises continuam abrindo normalmente.
Mexe em models.py/views.py/serializers.py e migração: exige reiniciar o `runserver`. Mexe em models.py/views.py/serializers.py e migração: exige reiniciar o `runserver`.
### Rodada 157 — Relatório Contábil - De Paula, na seção nova Diretoria (2026-10-02)
Pedido: duplicar o Relatório Contábil para as empresas do próprio escritório, numa seção nova do menu chamada "Diretoria", compartilhando as regras com a original, e sem que quem tem acesso só ao Relatório Contábil possa, em hipótese alguma, acessar a versão De Paula. Plano aprovado antes da execução.
Decisões confirmadas com o usuário: cadastro de indicadores **compartilhado** entre as duas (editar numa reflete na outra); seção Diretoria entre Relatórios e Relatórios Gerenciais; sem notificação de "nova ferramenta" no sino.
Mesmo modelo de isolamento da "Importação de Plano de Saúde - De Paula" (tabelas, endpoints e permissão próprios, sem FK cruzando), mas com o código **parametrizado** em vez de copiado: models viraram base abstrata + duas concretas (a original sem nenhuma alteração de banco, confirmada por `makemigrations --dry-run`), ViewSets ganharam atributos de classe e os `*DePaulaViewSet` só os trocam, helpers passaram a descobrir o model pela própria apuração, e `dashboard-contabil.js` lê os endpoints de `PID_DC_CONFIG`. Migração `0090` só cria as 8 tabelas novas. Módulo `diretoria` novo em `catalogo.py`, fora de `BASE_KEYS`/`SECTORAL_KEYS`: nasce fechado para todo perfil, liberação manual pela tela.
Validado antes de entregar: leitura da original idêntica entre o código anterior e o refatorado nas análises reais; fluxo completo de escrita idêntico nos dois (em transação desfeita no fim); mesmo fluxo na De Paula e testes de isolamento (403 cruzado por permissão, 404 para id da outra aplicação, 400 para observação apontando apuração da outra). Detalhe em "Relatório Contábil - De Paula" no `CLAUDE.md` desta pasta.
**Correção encontrada no caminho, em `profiles.js`**: a tela de Perfis de Acesso lia `permissions[modulo].enabled` direto do JSON salvo do perfil. Um módulo novo no catálogo não existe no JSON dos perfis já gravados (antes, o `seed_portal` acrescentava; em produção ele não roda mais), então abrir qualquer perfil quebraria a tela. `pidCompletaPermissoes()` completa módulo/app ausente como desmarcado ao carregar, sem alterar nenhum valor existente (conferido contra o perfil 8 real).
Mexe em models.py/views.py/serializers.py/urls.py e migração: exige reiniciar o `runserver`.

View File

@ -10,6 +10,33 @@ Ferramenta que otimiza a conferência de balancetes hoje feita manualmente pelo
**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. **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.
## Relatório Contábil - De Paula (segunda instância, seção Diretoria)
Existe uma **segunda aplicação**, `dashboard-contabil-de-paula` ("Relatório Contábil - De Paula"), na seção nova **Diretoria** do menu (entre Relatórios e Relatórios Gerenciais). É a mesma ferramenta, para as **empresas do próprio escritório**. Mesmo modelo de "Importação de Plano de Saúde - De Paula" (`portal_api/planos_saude/CLAUDE.md`): regras compartilhadas, dados e permissão separados.
**Requisito do usuário, que orienta tudo abaixo: quem tem acesso só ao Relatório Contábil não pode, em hipótese alguma, acessar o da De Paula.** São três camadas independentes, cada uma suficiente sozinha:
1. **Tabelas próprias.** Cada model tem uma versão `DePaula` (`ContabilApuracaoDePaula`, `ContabilContaDePaula`, `ContabilLinhaDreDePaula`, `ContabilLinhaAnaliseVerticalDePaula`, `ContabilAchadoDePaula`, `ContabilObservacaoDePaula`, `ContabilObservacaoEdicaoDePaula`, `ContabilApuracaoReprocessamentoDePaula`, migração `0090`), sem nenhuma FK cruzando com as originais. O histórico mês a mês e as observações por empresa + conta também ficam separados: a mesma empresa analisada nas duas aplicações tem dois históricos independentes.
2. **Endpoints próprios**, os mesmos da tabela "API" abaixo com sufixo `-de-paula` (`/api/contabil-apuracoes-de-paula/`, `/api/contabil-contas-de-paula/`, `/api/contabil-linhas-dre-de-paula/`, `/api/contabil-linhas-analise-vertical-de-paula/`, `/api/contabil-observacoes-de-paula/`, `/api/contabil-achados-de-paula/`). Cada queryset só enxerga as tabelas da De Paula, então um id da original devolve 404 ali. Os `PrimaryKeyRelatedField` de apuração dos serializers de observação (`ContabilObservacaoCreateDePaulaSerializer`/`ContabilObservacaoApuracaoDePaulaSerializer`) também usam o queryset da De Paula: apontar uma observação para uma apuração da original devolve 400.
3. **Permissão própria**: `PermissaoApp("diretoria", "dashboard-contabil-de-paula")` (`_CONTABIL_DE_PAULA_PERMISSAO` em `views.py`). Ter `relatorios > dashboard-contabil` não dá acesso, e o inverso também vale.
**Nasce fechada para todos.** O módulo `diretoria` está fora de `BASE_KEYS`/`SECTORAL_KEYS`, e nenhum perfil já gravado em produção tem a chave, que `permissoes_efetivas()` trata como desabilitada. A liberação é sempre manual pela tela de Perfis de Acesso. `seed_portal.py` força `False` em todo perfil que não seja o 8 (só vale para ambiente novo, o seed não roda em produção).
**O que é compartilhado** (nada disso foi copiado):
- todo este pacote (parsers, regras, fórmula, chaves, exportação, PDF do Resumo);
- o **cadastro de indicadores** (`IndicadorContabilDefinicao`, `/api/contabil-indicadores-definicoes/`), decisão explícita do usuário: quem tem qualquer uma das duas permissões gerencia os mesmos indicadores (`OR` das duas `PermissaoApp` em `IndicadorContabilDefinicaoViewSet`), e editar um indicador numa aplicação reflete na outra. Ao excluir um indicador, a limpeza de `indicadores_selecionados`/`indicadores_ocultos` varre as apurações das duas;
- as views, os serializers e o frontend, **parametrizados** (abaixo);
- o template do relatório do cliente (`dashboard-contabil-relatorio.html`), com os links de XLSX/PDF montados a partir de `prefixo_api` no contexto.
**Como a parametrização funciona** (diferente do Plano de Saúde, que duplicou as views: aqui são cerca de 1.000 linhas e duas cópias divergiriam):
- **Models**: cada um é uma base abstrata (`ContabilApuracaoBase`, `ContabilContaBase`, ...) com campos, constantes e métodos, mais duas concretas. As FKs ficam só nas concretas, porque cada variante aponta para os próprios models. Os `related_name` das FKs para a apuração são os mesmos nas duas (`contas`, `linhas_dre`, `linhas_analise_vertical`, `achados`, `observacoes`, `reprocessamentos`); os de `Usuario` têm sufixo `_de_paula`.
- **Views**: os ViewSets originais têm atributos de classe (`permissao_app`, `queryset`, serializers, `prefixo_api`), e os `*DePaulaViewSet` são subclasses que só trocam esses atributos. Os helpers descobrem o model pela própria apuração: `apuracao.contas.create(...)`, `apuracao.achados.model`, `_contabil_modelo_observacao(apuracao)` (= `apuracao.observacoes.model`), `type(apuracao)` para o histórico. `_contabil_monta_historico(modelo_apuracao, cabecalho)` recebe o model fixado por `functools.partial`, porque o pipeline o chama só com o cabeçalho.
- **Frontend**: `dashboard-contabil.js` lê `PID_DC_CONFIG` (`moduleKey`, `appKey` e os 6 prefixos de endpoint); `dashboard-contabil-de-paula.html` define `window.PID_DC_CONFIG` num `<script>` inline antes do JS, e a página original não define nada (cai nos defaults). O template da De Paula é uma cópia da original com título, item ativo do menu, textos e esse bloco de configuração trocados.
> **Regra para qualquer mudança futura nesta aplicação**: nenhum código de view pode citar `ContabilApuracao`/`ContabilConta`/`ContabilObservacao`/... direto para consultar ou gravar (constantes como `STATUS_CONCLUIDA`/`ALVO_CONTA` e o estático `chave_conta()` podem, são iguais nas duas). E nenhuma rota de dados pode ser escrita à mão no JS: sempre `PID_DC_CONFIG.endpointX`. Uma referência direta faria a De Paula ler ou gravar nas tabelas da original. Mudança no markup de `dashboard-contabil.html` precisa ser replicada em `dashboard-contabil-de-paula.html`.
**Validação feita antes de liberar**: (a) comparação dos endpoints de leitura da original (lista, detalhe, observações, indicadores, relatório HTML) entre o código anterior à refatoração e o atual, nas análises reais do banco: respostas idênticas; (b) fluxo completo de escrita da original (criar, duplicada, reprocessar, validar, observação com edição/encerrar/reativar, achado, indicadores, resumo, relatório, XLSX, PDF, concluir, excluir concluída, Contabit) nos dois códigos, dentro de transação desfeita no fim: resultado idêntico; (c) o mesmo fluxo na De Paula, mais os testes de isolamento: usuário só da original recebe 403 em todo endpoint da De Paula e vice-versa; id da original nos endpoints da De Paula dá 404; observação da De Paula apontando para apuração da original dá 400; observações da mesma empresa não cruzam.
## Decisões de escopo (confirmadas com o usuário) ## Decisões de escopo (confirmadas com o usuário)
- **Dois modelos de PDF, detectados sozinhos**: Questor e Contabit. O usuário nunca escolhe; `ContabilApuracao.leiaute` guarda o detectado. Ver "Leiaute Contabit" abaixo para o que muda. - **Dois modelos de PDF, detectados sozinhos**: Questor e Contabit. O usuário nunca escolhe; `ContabilApuracao.leiaute` guarda o detectado. Ver "Leiaute Contabit" abaixo para o que muda.
@ -113,7 +140,7 @@ Validado de duas formas: árvore sintética reproduzindo o caso do usuário (con
## Models (`portal_api/models.py`) ## Models (`portal_api/models.py`)
Padrão cabeçalho → linhas de detalhe → apontamentos, mesma filosofia de `IndicadorApuracao`/`IndicadorApuracaoColaborador`. Padrão cabeçalho → linhas de detalhe → apontamentos, mesma filosofia de `IndicadorApuracao`/`IndicadorApuracaoColaborador`. Cada model abaixo é, no código, uma base abstrata (`*Base`) mais duas concretas: a da original e a `*DePaula` (ver "Relatório Contábil - De Paula"). Os campos descritos aqui valem para as duas.
### Apuração e linhas ### Apuração e linhas
@ -251,7 +278,7 @@ O miolo `<svg>` de cada um dos 11 ícones existe em `_CONTABIL_ICONES_SVG` (`vie
## API ## API
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. Todos os endpoints usam `PermissaoApp("relatorios", "dashboard-contabil")`, exceto `/api/contabil-indicadores-definicoes/`, que aceita essa permissão **ou** a do Relatório Contábil - De Paula (cadastro compartilhado). Os mesmos endpoints com sufixo `-de-paula` servem a De Paula, com `PermissaoApp("diretoria", "dashboard-contabil-de-paula")` (ver "Relatório Contábil - De Paula" acima). 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.
| Endpoint | Método | Uso | | Endpoint | Método | Uso |
|---|---|---| |---|---|---|
@ -522,6 +549,8 @@ Cor por sinal só onde é seguro: `sign` (ROA/ROE/EBIT/EBITDA — positivo verde
## Riscos conhecidos e armadilhas ## Riscos conhecidos e armadilhas
- **Duas aplicações sobre o mesmo código**: referência direta a um model `Contabil*` numa view, ou rota literal no JS, faz o Relatório Contábil - De Paula ler ou gravar nas tabelas da original. Ver a regra em "Relatório Contábil - De Paula".
- **Leiaute Contabit calibrado contra um único arquivo** — ver "Leiaute Contabit": códigos das regras, passos de nível e a premissa do sinal negativo precisam ser conferidos a cada arquivo novo de outro cliente. - **Leiaute Contabit calibrado contra um único arquivo** — ver "Leiaute Contabit": códigos das regras, passos de nível e a premissa do sinal negativo precisam ser conferidos a cada arquivo novo de outro cliente.
- **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. - **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. - **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.

View File

@ -4,11 +4,14 @@
Otimiza a conferência de balancetes hoje feita manualmente pelo Fisco/Contábil: o contador anexa o PDF de Balancete + DRE de uma empresa (mesmo relatório hoje enviado ao cliente, gerado pelo Questor ou pelo Contabit; o modelo é detectado sozinho, e no Contabit o código da empresa vem do nome do arquivo, ex. "1512 - Balancete 082026.pdf"), a ferramenta extrai as contas automaticamente e roda um conjunto de checagens de auditoria (saldos negativos de caixa, contas transitórias/genéricas com saldo, contas que deveriam estar zeradas, diferença débito/crédito, lucro do balancete diferente da DRE, variações atípicas na DRE) — o contador revisa os achados, marca contas/linhas como validadas, registra observações por conta/linha (thread inline na própria tabela, com um toggle "mostrar ao cliente" que nasce desmarcado), escreve um resumo livre do fechamento (considerações/análises, texto rico com imagem) e conclui a análise. As observações não morrem com a competência: cada uma fica no histórico daquela empresa + conta, reaparecendo na análise do mês seguinte assinada por quem escreveu e com a data, travada pra edição, até o contador encerrá-la para as próximas execuções. Quando o PDF traz a seção "Demonstração Mensal (Análise Vertical)" (histórico de 3 meses, valor+variação percentual por linha), ela também é extraída e ganha sua própria aba de revisão (mesmos recursos de observação/validado/ocultar do Balancete/D.R.E.). Se o arquivo original estava errado/incompleto, o botão "Reprocessar" reanexa um PDF novo pra mesma empresa/competência sem perder nada já registrado — conta/linha sem mudança mantém observação e validação, a que mudou volta pra "não validada" com um alerta, e achado já tratado nunca é apagado ou tem sua justificativa sobrescrita. O botão "Gerar Relatório" gera um relatório HTML autocontido (resumo do fechamento, indicadores financeiros, DRE/Balancete/Análise Vertical agrupados, observações do contador) com a marca do escritório, pronto pra enviar ao administrador da empresa — a aba "Resumo" desse relatório também pode ser exportada avulsa em PDF (resumo do fechamento + indicadores + observações, sem o resto do relatório). Otimiza a conferência de balancetes hoje feita manualmente pelo Fisco/Contábil: o contador anexa o PDF de Balancete + DRE de uma empresa (mesmo relatório hoje enviado ao cliente, gerado pelo Questor ou pelo Contabit; o modelo é detectado sozinho, e no Contabit o código da empresa vem do nome do arquivo, ex. "1512 - Balancete 082026.pdf"), a ferramenta extrai as contas automaticamente e roda um conjunto de checagens de auditoria (saldos negativos de caixa, contas transitórias/genéricas com saldo, contas que deveriam estar zeradas, diferença débito/crédito, lucro do balancete diferente da DRE, variações atípicas na DRE) — o contador revisa os achados, marca contas/linhas como validadas, registra observações por conta/linha (thread inline na própria tabela, com um toggle "mostrar ao cliente" que nasce desmarcado), escreve um resumo livre do fechamento (considerações/análises, texto rico com imagem) e conclui a análise. As observações não morrem com a competência: cada uma fica no histórico daquela empresa + conta, reaparecendo na análise do mês seguinte assinada por quem escreveu e com a data, travada pra edição, até o contador encerrá-la para as próximas execuções. Quando o PDF traz a seção "Demonstração Mensal (Análise Vertical)" (histórico de 3 meses, valor+variação percentual por linha), ela também é extraída e ganha sua própria aba de revisão (mesmos recursos de observação/validado/ocultar do Balancete/D.R.E.). Se o arquivo original estava errado/incompleto, o botão "Reprocessar" reanexa um PDF novo pra mesma empresa/competência sem perder nada já registrado — conta/linha sem mudança mantém observação e validação, a que mudou volta pra "não validada" com um alerta, e achado já tratado nunca é apagado ou tem sua justificativa sobrescrita. O botão "Gerar Relatório" gera um relatório HTML autocontido (resumo do fechamento, indicadores financeiros, DRE/Balancete/Análise Vertical agrupados, observações do contador) com a marca do escritório, pronto pra enviar ao administrador da empresa — a aba "Resumo" desse relatório também pode ser exportada avulsa em PDF (resumo do fechamento + indicadores + observações, sem o resto do relatório).
**Relatório Contábil - De Paula** (seção Diretoria do menu): a mesma ferramenta para as empresas do próprio escritório. Regras e cadastro de indicadores compartilhados com a original; análises, histórico, observações e permissão totalmente separados (quem tem acesso só ao Relatório Contábil não acessa esta). Liberada manualmente por perfil em Perfis de Acesso.
**Ainda fora de escopo**: indicadores para empresas do Contabit (a seção fica oculta nesse modelo); consolidação entre várias empresas/competências ao mesmo tempo (é sempre uma apuração por vez). **Ainda fora de escopo**: indicadores para empresas do Contabit (a seção fica oculta nesse modelo); consolidação entre várias empresas/competências ao mesmo tempo (é sempre uma apuração por vez).
## Onde mexer ## Onde mexer
- `portal_api/dashboard_contabil/` — `parser.py` (extração do PDF Questor via `pdfplumber` e detecção do modelo), `parser_contabit.py` (extração do PDF Contabit), `regras.py` (motor de regras de auditoria), `indicadores.py` (indicadores financeiros do relatório), `pipeline.py` (orquestração), `modelos.py` (dataclasses), `chaves.py` (chave natural de conta/linha), `exportacao.py` (Balancete/DRE em XLSX), `resumo_pdf.py` (Resumo do Fechamento + Indicadores + Observações em PDF, avulso). - `portal_api/dashboard_contabil/` — `parser.py` (extração do PDF Questor via `pdfplumber` e detecção do modelo), `parser_contabit.py` (extração do PDF Contabit), `regras.py` (motor de regras de auditoria), `indicadores.py` (indicadores financeiros do relatório), `pipeline.py` (orquestração), `modelos.py` (dataclasses), `chaves.py` (chave natural de conta/linha), `exportacao.py` (Balancete/DRE em XLSX), `resumo_pdf.py` (Resumo do Fechamento + Indicadores + Observações em PDF, avulso).
- `ContabilApuracao`/`ContabilConta`/`ContabilLinhaDre`/`ContabilLinhaAnaliseVertical`/`ContabilAchado`/`ContabilObservacao`/`ContabilObservacaoEdicao` (`portal_api/models.py`). - `ContabilApuracao`/`ContabilConta`/`ContabilLinhaDre`/`ContabilLinhaAnaliseVertical`/`ContabilAchado`/`ContabilObservacao`/`ContabilObservacaoEdicao` (`portal_api/models.py`).
- `dashboard-contabil.html` / `static/js/dashboard-contabil.js` / `static/css/dashboard-contabil.css`. - As mesmas classes com sufixo `DePaula` para o Relatório Contábil - De Paula (cada model é uma base abstrata + duas concretas).
- `dashboard-contabil.html` / `dashboard-contabil-de-paula.html` / `static/js/dashboard-contabil.js` (parametrizado por `PID_DC_CONFIG`) / `static/css/dashboard-contabil.css`.
- `dashboard-contabil-relatorio.html` (relatório em si) / `portal_api/templatetags/contabil_extras.py` (filtros de formatação). - `dashboard-contabil-relatorio.html` (relatório em si) / `portal_api/templatetags/contabil_extras.py` (filtros de formatação).

View File

@ -196,6 +196,12 @@ class Command(BaseCommand):
# decisão explícita do usuário (dado de cliente, mas ferramenta # decisão explícita do usuário (dado de cliente, mas ferramenta
# ainda em validação), liberada depois pela tela. # ainda em validação), liberada depois pela tela.
permissoes["utilitarios"]["apps"]["conciliacao-fornecedores"] = False permissoes["utilitarios"]["apps"]["conciliacao-fornecedores"] = False
# "diretoria" (Relatório Contábil - De Paula, empresas do próprio
# escritório) já fica de fora por não estar em nenhuma lista de
# chaves acima; o override é só para continuar restrita mesmo se
# o módulo um dia entrar na lista de algum perfil.
permissoes["diretoria"]["enabled"] = False
permissoes["diretoria"]["apps"]["dashboard-contabil-de-paula"] = False
# `nome` só é gravado na criação — reexecuções não devem reverter uma # `nome` só é gravado na criação — reexecuções não devem reverter uma
# renomeação feita depois pela própria tela de Perfis de Acesso (mesmo # renomeação feita depois pela própria tela de Perfis de Acesso (mesmo
# espírito de "só inicializa na primeira criação" já usado pra # espírito de "só inicializa na primeira criação" já usado pra

View File

@ -0,0 +1,202 @@
# Generated by Django 6.0.7 on 2026-10-02 12:20
import django.db.models.deletion
import portal_api.models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0089_contabil_indicador_sem_arquivo'),
]
operations = [
migrations.CreateModel(
name='ContabilApuracaoDePaula',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('codigo_empresa', models.CharField(max_length=20, verbose_name='Código da empresa')),
('nome_empresa', models.CharField(max_length=255, verbose_name='Nome da empresa')),
('cnpj', models.CharField(blank=True, max_length=20, verbose_name='CNPJ')),
('competencia', models.DateField(verbose_name='Competência')),
('periodo_inicio', models.DateField(verbose_name='Início do período')),
('periodo_fim', models.DateField(verbose_name='Fim do período')),
('status', models.CharField(choices=[('revisao', 'Em revisão'), ('concluida', 'Concluída')], default='revisao', max_length=20, verbose_name='Status')),
('criado_em', models.DateTimeField(auto_now_add=True)),
('concluida_em', models.DateTimeField(blank=True, null=True, verbose_name='Concluída em')),
('indicadores_ocultos', models.JSONField(blank=True, default=list, verbose_name='Indicadores ocultos no relatório')),
('indicadores_selecionados', models.JSONField(blank=True, default=list, verbose_name='Indicadores não padrão selecionados')),
('resumo_fechamento', models.TextField(blank=True, validators=[portal_api.models.validar_tamanho_resumo_fechamento_contabil], verbose_name='Resumo do Fechamento')),
('analise_vertical_meses', models.JSONField(blank=True, default=list, verbose_name='Meses da Análise Vertical')),
('fonte_pdf_atipica', models.BooleanField(default=False, verbose_name='PDF de fonte atípica (risco de nomenclatura)')),
('leiaute', models.CharField(choices=[('questor', 'Questor'), ('contabit', 'Contabit')], db_default='questor', max_length=20, verbose_name='Modelo do arquivo')),
('criado_por', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='apuracoes_contabeis_de_paula', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Apuração do Relatório Contábil - De Paula',
'verbose_name_plural': 'Apurações do Relatório Contábil - De Paula',
'ordering': ['-competencia', '-criado_em'],
'abstract': False,
},
),
migrations.CreateModel(
name='ContabilApuracaoReprocessamentoDePaula',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('reprocessado_em', models.DateTimeField(auto_now_add=True, verbose_name='Reprocessado em')),
('apuracao', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reprocessamentos', to='portal_api.contabilapuracaodepaula')),
('reprocessado_por', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reprocessamentos_contabeis_de_paula', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Reprocessamento do Relatório Contábil - De Paula',
'verbose_name_plural': 'Reprocessamentos do Relatório Contábil - De Paula',
'ordering': ['-reprocessado_em', '-id'],
'abstract': False,
},
),
migrations.CreateModel(
name='ContabilContaDePaula',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ordem', models.IntegerField(default=0, verbose_name='Ordem')),
('conta_numero', models.IntegerField(blank=True, null=True, verbose_name='Número da conta')),
('codigo', models.CharField(max_length=60, verbose_name='Classificação')),
('descricao', models.CharField(max_length=255, verbose_name='Descrição')),
('tipo', models.CharField(max_length=1, verbose_name='Tipo')),
('saldo_anterior', models.DecimalField(decimal_places=2, max_digits=16)),
('debito', models.DecimalField(decimal_places=2, max_digits=16)),
('credito', models.DecimalField(decimal_places=2, max_digits=16)),
('saldo_atual', models.DecimalField(decimal_places=2, max_digits=16)),
('validado', models.BooleanField(default=False, verbose_name='Validado pelo contador')),
('alterada_reprocessamento', models.BooleanField(default=False, verbose_name='Alterada no último reprocessamento')),
('valor_anterior_reprocessamento', models.DecimalField(blank=True, decimal_places=2, max_digits=16, null=True, verbose_name='Saldo atual antes do último reprocessamento')),
('apuracao', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contas', to='portal_api.contabilapuracaodepaula')),
],
options={
'verbose_name': 'Conta do Balancete (Relatório Contábil - De Paula)',
'verbose_name_plural': 'Contas do Balancete (Relatório Contábil - De Paula)',
'ordering': ['ordem', 'id'],
'abstract': False,
},
),
migrations.CreateModel(
name='ContabilLinhaAnaliseVerticalDePaula',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ordem', models.IntegerField(verbose_name='Ordem')),
('descricao', models.CharField(max_length=255, verbose_name='Descrição')),
('nivel', models.IntegerField(default=0, verbose_name='Nível de indentação')),
('totalizador', models.BooleanField(default=False, verbose_name='Linha totalizadora')),
('valores', models.JSONField(default=list, verbose_name='Valores mensais')),
('validado', models.BooleanField(default=False, verbose_name='Validado pelo contador')),
('alterada_reprocessamento', models.BooleanField(default=False, verbose_name='Alterada no último reprocessamento')),
('valores_anterior_reprocessamento', models.JSONField(blank=True, default=list, verbose_name='Valores mensais antes do último reprocessamento')),
('apuracao', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='linhas_analise_vertical', to='portal_api.contabilapuracaodepaula')),
],
options={
'verbose_name': 'Linha da Análise Vertical (Relatório Contábil - De Paula)',
'verbose_name_plural': 'Linhas da Análise Vertical (Relatório Contábil - De Paula)',
'ordering': ['ordem', 'id'],
'abstract': False,
},
),
migrations.CreateModel(
name='ContabilAchadoDePaula',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('regra', models.CharField(max_length=60, verbose_name='Regra')),
('severidade', models.CharField(choices=[('alta', 'Alta'), ('media', 'Média'), ('baixa', 'Baixa')], max_length=10, verbose_name='Severidade')),
('titulo', models.CharField(max_length=255, verbose_name='Título')),
('mensagem', models.TextField(verbose_name='Mensagem')),
('valor_referencia', models.DecimalField(blank=True, decimal_places=2, max_digits=16, null=True)),
('status', models.CharField(choices=[('pendente', 'Pendente'), ('tratado', 'Tratado'), ('ignorado', 'Ignorado')], default='pendente', max_length=10, verbose_name='Status')),
('observacao_contador', models.TextField(blank=True, verbose_name='Observação do contador')),
('tratado_em', models.DateTimeField(blank=True, null=True, verbose_name='Tratado em')),
('oculto_no_relatorio', models.BooleanField(default=False, verbose_name='Observação oculta no relatório')),
('tratado_por', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='achados_contabeis_tratados_de_paula', to=settings.AUTH_USER_MODEL)),
('apuracao', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='achados', to='portal_api.contabilapuracaodepaula')),
('conta', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='achados', to='portal_api.contabilcontadepaula')),
('linha_analise_vertical', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='achados', to='portal_api.contabillinhaanaliseverticaldepaula')),
],
options={
'verbose_name': 'Achado do Relatório Contábil - De Paula',
'verbose_name_plural': 'Achados do Relatório Contábil - De Paula',
'ordering': ['-severidade', 'id'],
'abstract': False,
},
),
migrations.CreateModel(
name='ContabilLinhaDreDePaula',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ordem', models.IntegerField(verbose_name='Ordem')),
('descricao', models.CharField(max_length=255, verbose_name='Descrição')),
('nivel', models.IntegerField(default=0, verbose_name='Nível de indentação')),
('valor', models.DecimalField(decimal_places=2, max_digits=16)),
('totalizador', models.BooleanField(default=False, verbose_name='Linha totalizadora')),
('validado', models.BooleanField(default=False, verbose_name='Validado pelo contador')),
('alterada_reprocessamento', models.BooleanField(default=False, verbose_name='Alterada no último reprocessamento')),
('valor_anterior_reprocessamento', models.DecimalField(blank=True, decimal_places=2, max_digits=16, null=True, verbose_name='Valor antes do último reprocessamento')),
('apuracao', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='linhas_dre', to='portal_api.contabilapuracaodepaula')),
],
options={
'verbose_name': 'Linha da DRE (Relatório Contábil - De Paula)',
'verbose_name_plural': 'Linhas da DRE (Relatório Contábil - De Paula)',
'ordering': ['ordem', 'id'],
'abstract': False,
},
),
migrations.CreateModel(
name='ContabilObservacaoDePaula',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('codigo_empresa', models.CharField(max_length=20, verbose_name='Código da empresa')),
('alvo_tipo', models.CharField(choices=[('conta', 'Conta do Balancete'), ('dre', 'Linha da DRE'), ('analise_vertical', 'Linha da Análise Vertical')], max_length=20, verbose_name='Tipo do alvo')),
('alvo_chave', models.CharField(max_length=1000, verbose_name='Chave natural do alvo')),
('alvo_rotulo', models.CharField(blank=True, max_length=320, verbose_name='Rótulo do alvo')),
('competencia_origem', models.DateField(verbose_name='Competência de origem')),
('texto', models.TextField(verbose_name='Observação')),
('mostrar_ao_cliente', models.BooleanField(default=False, verbose_name='Mostrar ao cliente no relatório')),
('criado_em', models.DateTimeField(auto_now_add=True)),
('atualizado_em', models.DateTimeField(auto_now=True)),
('encerrada_em_competencia', models.DateField(blank=True, null=True, verbose_name='Encerrada na competência')),
('encerrada_em', models.DateTimeField(blank=True, null=True, verbose_name='Encerrada em')),
('apuracao_origem', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observacoes', to='portal_api.contabilapuracaodepaula')),
('criado_por', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observacoes_contabeis_de_paula', to=settings.AUTH_USER_MODEL)),
('encerrada_por', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observacoes_contabeis_encerradas_de_paula', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Observação do Relatório Contábil - De Paula',
'verbose_name_plural': 'Observações do Relatório Contábil - De Paula',
'ordering': ['competencia_origem', 'criado_em', 'id'],
'abstract': False,
},
),
migrations.CreateModel(
name='ContabilObservacaoEdicaoDePaula',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('texto_anterior', models.TextField(verbose_name='Texto anterior')),
('texto_novo', models.TextField(verbose_name='Texto novo')),
('editado_em', models.DateTimeField(auto_now_add=True, verbose_name='Editado em')),
('editado_por', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='edicoes_observacoes_contabeis_de_paula', to=settings.AUTH_USER_MODEL)),
('observacao', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='edicoes', to='portal_api.contabilobservacaodepaula')),
],
options={
'verbose_name': 'Edição de observação do Relatório Contábil - De Paula',
'verbose_name_plural': 'Edições de observação do Relatório Contábil - De Paula',
'ordering': ['-editado_em', '-id'],
'abstract': False,
},
),
migrations.AddConstraint(
model_name='contabilapuracaodepaula',
constraint=models.UniqueConstraint(fields=('codigo_empresa', 'competencia'), name='contabil_apuracao_de_paula_unica'),
),
migrations.AddIndex(
model_name='contabilobservacaodepaula',
index=models.Index(fields=['codigo_empresa', 'alvo_tipo', 'alvo_chave'], name='contabil_obs_dp_alvo_idx'),
),
]

View File

@ -1972,7 +1972,15 @@ def validar_tamanho_arquivo_contabil(arquivo: File) -> None:
raise ValidationError("O arquivo deve ter no máximo 15MB.") raise ValidationError("O arquivo deve ter no máximo 15MB.")
class ContabilApuracao(models.Model): # Relatório Contábil e Relatório Contábil - De Paula (Diretoria): cada model
# é uma base abstrata (campos e métodos) mais duas classes concretas, uma por
# aplicação, com tabelas separadas e sem nenhuma FK cruzando entre as duas. As
# FKs ficam só nas concretas, porque cada variante aponta para os próprios
# models. Ver "Relatório Contábil - De Paula" no CLAUDE.md do pacote
# `dashboard_contabil`.
class ContabilApuracaoBase(models.Model):
"""Uma análise do Relatório Contábil (Relatórios > Contabilidade): o """Uma análise do Relatório Contábil (Relatórios > Contabilidade): o
contador anexa o PDF de Balancete + DRE (leiaute Questor, mesmo relatório contador anexa o PDF de Balancete + DRE (leiaute Questor, mesmo relatório
hoje enviado ao cliente) e o pipeline (`portal_api.dashboard_contabil. hoje enviado ao cliente) e o pipeline (`portal_api.dashboard_contabil.
@ -1999,7 +2007,6 @@ class ContabilApuracao(models.Model):
# O PDF anexado não é gravado: é lido na criação/reprocessamento e tudo o # O PDF anexado não é gravado: é lido na criação/reprocessamento e tudo o
# que se usa dele fica em contas, linhas e achados. # que se usa dele fica em contas, linhas e achados.
status = models.CharField("Status", max_length=20, choices=STATUS_CHOICES, default=STATUS_REVISAO) status = models.CharField("Status", max_length=20, choices=STATUS_CHOICES, default=STATUS_REVISAO)
criado_por = models.ForeignKey(Usuario, on_delete=models.SET_NULL, null=True, related_name="apuracoes_contabeis")
criado_em = models.DateTimeField(auto_now_add=True) criado_em = models.DateTimeField(auto_now_add=True)
concluida_em = models.DateTimeField("Concluída em", null=True, blank=True) concluida_em = models.DateTimeField("Concluída em", null=True, blank=True)
# Chaves de IndicadoresFinanceiros (dashboard_contabil/indicadores.py, # Chaves de IndicadoresFinanceiros (dashboard_contabil/indicadores.py,
@ -2065,18 +2072,43 @@ class ContabilApuracao(models.Model):
return self.leiaute != self.LEIAUTE_CONTABIT return self.leiaute != self.LEIAUTE_CONTABIT
class Meta: class Meta:
verbose_name = "Apuração do Relatório Contábil" abstract = True
verbose_name_plural = "Apurações do Relatório Contábil"
ordering = ["-competencia", "-criado_em"] ordering = ["-competencia", "-criado_em"]
constraints = [
models.UniqueConstraint(fields=["codigo_empresa", "competencia"], name="contabil_apuracao_unica")
]
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.codigo_empresa} — {self.competencia:%m/%Y}" return f"{self.codigo_empresa} — {self.competencia:%m/%Y}"
class ContabilApuracaoReprocessamento(models.Model): class ContabilApuracao(ContabilApuracaoBase):
criado_por = models.ForeignKey(Usuario, on_delete=models.SET_NULL, null=True, related_name="apuracoes_contabeis")
class Meta(ContabilApuracaoBase.Meta):
verbose_name = "Apuração do Relatório Contábil"
verbose_name_plural = "Apurações do Relatório Contábil"
constraints = [
models.UniqueConstraint(fields=["codigo_empresa", "competencia"], name="contabil_apuracao_unica")
]
class ContabilApuracaoDePaula(ContabilApuracaoBase):
"""Apuração do Relatório Contábil - De Paula (Diretoria), para as empresas
do próprio escritório. Mesma estrutura da original, em tabela própria."""
criado_por = models.ForeignKey(
Usuario, on_delete=models.SET_NULL, null=True, related_name="apuracoes_contabeis_de_paula"
)
class Meta(ContabilApuracaoBase.Meta):
verbose_name = "Apuração do Relatório Contábil - De Paula"
verbose_name_plural = "Apurações do Relatório Contábil - De Paula"
constraints = [
models.UniqueConstraint(
fields=["codigo_empresa", "competencia"], name="contabil_apuracao_de_paula_unica"
)
]
class ContabilApuracaoReprocessamentoBase(models.Model):
"""Log de cada reprocessamento de uma `ContabilApuracao` (pedido explícito """Log de cada reprocessamento de uma `ContabilApuracao` (pedido explícito
do usuário — "identificar quem reprocessou e quantos reprocessamentos já do usuário — "identificar quem reprocessou e quantos reprocessamentos já
ocorreu"), mesmo espírito de `ContabilObservacaoEdicao`. Um registro por ocorreu"), mesmo espírito de `ContabilObservacaoEdicao`. Um registro por
@ -2084,29 +2116,45 @@ class ContabilApuracaoReprocessamento(models.Model):
dentro da mesma transação da resincronização, então uma tentativa que dentro da mesma transação da resincronização, então uma tentativa que
falhar no meio do caminho não deixa um registro órfão aqui.""" falhar no meio do caminho não deixa um registro órfão aqui."""
apuracao = models.ForeignKey(ContabilApuracao, on_delete=models.CASCADE, related_name="reprocessamentos")
reprocessado_por = models.ForeignKey(
Usuario, on_delete=models.SET_NULL, null=True, blank=True, related_name="reprocessamentos_contabeis"
)
reprocessado_em = models.DateTimeField("Reprocessado em", auto_now_add=True) reprocessado_em = models.DateTimeField("Reprocessado em", auto_now_add=True)
class Meta: class Meta:
verbose_name = "Reprocessamento do Relatório Contábil" abstract = True
verbose_name_plural = "Reprocessamentos do Relatório Contábil"
ordering = ["-reprocessado_em", "-id"] ordering = ["-reprocessado_em", "-id"]
def __str__(self) -> str: def __str__(self) -> str:
return f"Reprocessamento de {self.apuracao_id} em {self.reprocessado_em:%d/%m/%Y %H:%M}" return f"Reprocessamento de {self.apuracao_id} em {self.reprocessado_em:%d/%m/%Y %H:%M}"
class ContabilConta(models.Model): class ContabilApuracaoReprocessamento(ContabilApuracaoReprocessamentoBase):
apuracao = models.ForeignKey(ContabilApuracao, on_delete=models.CASCADE, related_name="reprocessamentos")
reprocessado_por = models.ForeignKey(
Usuario, on_delete=models.SET_NULL, null=True, blank=True, related_name="reprocessamentos_contabeis"
)
class Meta(ContabilApuracaoReprocessamentoBase.Meta):
verbose_name = "Reprocessamento do Relatório Contábil"
verbose_name_plural = "Reprocessamentos do Relatório Contábil"
class ContabilApuracaoReprocessamentoDePaula(ContabilApuracaoReprocessamentoBase):
apuracao = models.ForeignKey(ContabilApuracaoDePaula, on_delete=models.CASCADE, related_name="reprocessamentos")
reprocessado_por = models.ForeignKey(
Usuario, on_delete=models.SET_NULL, null=True, blank=True, related_name="reprocessamentos_contabeis_de_paula"
)
class Meta(ContabilApuracaoReprocessamentoBase.Meta):
verbose_name = "Reprocessamento do Relatório Contábil - De Paula"
verbose_name_plural = "Reprocessamentos do Relatório Contábil - De Paula"
class ContabilContaBase(models.Model):
"""Uma linha do Balancete extraída do PDF (grupo Ativo/Passivo, sintética """Uma linha do Balancete extraída do PDF (grupo Ativo/Passivo, sintética
ou analítica). `observacao` é livre e editável pelo contador via PATCH em ou analítica). `observacao` é livre e editável pelo contador via PATCH em
qualquer conta, tenha ela gerado um `ContabilAchado` ou não — é o espaço qualquer conta, tenha ela gerado um `ContabilAchado` ou não — é o espaço
de "análise" pedido pelo usuário, independente do que a auditoria de "análise" pedido pelo usuário, independente do que a auditoria
automática encontrou.""" automática encontrou."""
apuracao = models.ForeignKey(ContabilApuracao, on_delete=models.CASCADE, related_name="contas")
# `conta_numero` (a numeração interna do Questor, coluna "Conta" do PDF) # `conta_numero` (a numeração interna do Questor, coluna "Conta" do PDF)
# NÃO corresponde à ordem real de impressão do balancete — o Questor # NÃO corresponde à ordem real de impressão do balancete — o Questor
# reaproveita numeração de contas antigas/canceladas, então o mesmo # reaproveita numeração de contas antigas/canceladas, então o mesmo
@ -2158,8 +2206,7 @@ class ContabilConta(models.Model):
) )
class Meta: class Meta:
verbose_name = "Conta do Balancete (Relatório Contábil)" abstract = True
verbose_name_plural = "Contas do Balancete (Relatório Contábil)"
# `id` desempata: `ordem` sozinha deixa a ordenação indefinida quando # `id` desempata: `ordem` sozinha deixa a ordenação indefinida quando
# duas linhas empatam, e a árvore (nível de cada linha em relação à # duas linhas empatam, e a árvore (nível de cada linha em relação à
# anterior) e a chave natural da DRE/Análise Vertical dependem da # anterior) e a chave natural da DRE/Análise Vertical dependem da
@ -2171,12 +2218,27 @@ class ContabilConta(models.Model):
return f"{self.codigo} {self.descricao}" return f"{self.codigo} {self.descricao}"
class ContabilLinhaDre(models.Model): class ContabilConta(ContabilContaBase):
apuracao = models.ForeignKey(ContabilApuracao, on_delete=models.CASCADE, related_name="contas")
class Meta(ContabilContaBase.Meta):
verbose_name = "Conta do Balancete (Relatório Contábil)"
verbose_name_plural = "Contas do Balancete (Relatório Contábil)"
class ContabilContaDePaula(ContabilContaBase):
apuracao = models.ForeignKey(ContabilApuracaoDePaula, on_delete=models.CASCADE, related_name="contas")
class Meta(ContabilContaBase.Meta):
verbose_name = "Conta do Balancete (Relatório Contábil - De Paula)"
verbose_name_plural = "Contas do Balancete (Relatório Contábil - De Paula)"
class ContabilLinhaDreBase(models.Model):
"""Uma linha da Demonstração do Resultado do Exercício extraída do PDF — """Uma linha da Demonstração do Resultado do Exercício extraída do PDF —
sem código de classificação (o relatório da DRE não traz, diferente do sem código de classificação (o relatório da DRE não traz, diferente do
Balancete), só descrição/nível/valor na ordem em que aparecem.""" Balancete), só descrição/nível/valor na ordem em que aparecem."""
apuracao = models.ForeignKey(ContabilApuracao, on_delete=models.CASCADE, related_name="linhas_dre")
ordem = models.IntegerField("Ordem") ordem = models.IntegerField("Ordem")
descricao = models.CharField("Descrição", max_length=255) descricao = models.CharField("Descrição", max_length=255)
nivel = models.IntegerField("Nível de indentação", default=0) nivel = models.IntegerField("Nível de indentação", default=0)
@ -2194,16 +2256,31 @@ class ContabilLinhaDre(models.Model):
) )
class Meta: class Meta:
verbose_name = "Linha da DRE (Relatório Contábil)" abstract = True
verbose_name_plural = "Linhas da DRE (Relatório Contábil)" # Ver a nota em `ContabilContaBase.Meta`.
# Ver a nota em `ContabilConta.Meta`.
ordering = ["ordem", "id"] ordering = ["ordem", "id"]
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.descricao}" return f"{self.descricao}"
class ContabilLinhaAnaliseVertical(models.Model): class ContabilLinhaDre(ContabilLinhaDreBase):
apuracao = models.ForeignKey(ContabilApuracao, on_delete=models.CASCADE, related_name="linhas_dre")
class Meta(ContabilLinhaDreBase.Meta):
verbose_name = "Linha da DRE (Relatório Contábil)"
verbose_name_plural = "Linhas da DRE (Relatório Contábil)"
class ContabilLinhaDreDePaula(ContabilLinhaDreBase):
apuracao = models.ForeignKey(ContabilApuracaoDePaula, on_delete=models.CASCADE, related_name="linhas_dre")
class Meta(ContabilLinhaDreBase.Meta):
verbose_name = "Linha da DRE (Relatório Contábil - De Paula)"
verbose_name_plural = "Linhas da DRE (Relatório Contábil - De Paula)"
class ContabilLinhaAnaliseVerticalBase(models.Model):
"""Uma linha da seção "Demonstração Mensal (Análise Vertical)" do mesmo """Uma linha da seção "Demonstração Mensal (Análise Vertical)" do mesmo
PDF — mesma árvore/descrição/nível da DRE (`ContabilLinhaDre`), só que PDF — mesma árvore/descrição/nível da DRE (`ContabilLinhaDre`), só que
com um valor+percentual por mês em vez de um valor único, por isso com um valor+percentual por mês em vez de um valor único, por isso
@ -2213,7 +2290,6 @@ class ContabilLinhaAnaliseVertical(models.Model):
texto (não float), pra não perder precisão decimal em nenhuma conversão texto (não float), pra não perder precisão decimal em nenhuma conversão
— `[{"valor": "1234.56", "percentual": "12.34"}, ...]`.""" — `[{"valor": "1234.56", "percentual": "12.34"}, ...]`."""
apuracao = models.ForeignKey(ContabilApuracao, on_delete=models.CASCADE, related_name="linhas_analise_vertical")
ordem = models.IntegerField("Ordem") ordem = models.IntegerField("Ordem")
descricao = models.CharField("Descrição", max_length=255) descricao = models.CharField("Descrição", max_length=255)
nivel = models.IntegerField("Nível de indentação", default=0) nivel = models.IntegerField("Nível de indentação", default=0)
@ -2230,16 +2306,33 @@ class ContabilLinhaAnaliseVertical(models.Model):
valores_anterior_reprocessamento = models.JSONField("Valores mensais antes do último reprocessamento", default=list, blank=True) valores_anterior_reprocessamento = models.JSONField("Valores mensais antes do último reprocessamento", default=list, blank=True)
class Meta: class Meta:
verbose_name = "Linha da Análise Vertical (Relatório Contábil)" abstract = True
verbose_name_plural = "Linhas da Análise Vertical (Relatório Contábil)" # Ver a nota em `ContabilContaBase.Meta`.
# Ver a nota em `ContabilConta.Meta`.
ordering = ["ordem", "id"] ordering = ["ordem", "id"]
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.descricao}" return f"{self.descricao}"
class ContabilAchado(models.Model): class ContabilLinhaAnaliseVertical(ContabilLinhaAnaliseVerticalBase):
apuracao = models.ForeignKey(ContabilApuracao, on_delete=models.CASCADE, related_name="linhas_analise_vertical")
class Meta(ContabilLinhaAnaliseVerticalBase.Meta):
verbose_name = "Linha da Análise Vertical (Relatório Contábil)"
verbose_name_plural = "Linhas da Análise Vertical (Relatório Contábil)"
class ContabilLinhaAnaliseVerticalDePaula(ContabilLinhaAnaliseVerticalBase):
apuracao = models.ForeignKey(
ContabilApuracaoDePaula, on_delete=models.CASCADE, related_name="linhas_analise_vertical"
)
class Meta(ContabilLinhaAnaliseVerticalBase.Meta):
verbose_name = "Linha da Análise Vertical (Relatório Contábil - De Paula)"
verbose_name_plural = "Linhas da Análise Vertical (Relatório Contábil - De Paula)"
class ContabilAchadoBase(models.Model):
"""Um achado gerado automaticamente pelo motor de regras """Um achado gerado automaticamente pelo motor de regras
(`dashboard_contabil.regras`) ao criar a apuração, só muda de `status` (`dashboard_contabil.regras`) ao criar a apuração, só muda de `status`
conforme o contador revisa (mesmo espírito de `ImportacaoPlanoSaudeAuditoria`). conforme o contador revisa (mesmo espírito de `ImportacaoPlanoSaudeAuditoria`).
@ -2275,19 +2368,14 @@ class ContabilAchado(models.Model):
(STATUS_IGNORADO, "Ignorado"), (STATUS_IGNORADO, "Ignorado"),
] ]
apuracao = models.ForeignKey(ContabilApuracao, on_delete=models.CASCADE, related_name="achados") # `apuracao`, `conta`, `linha_analise_vertical` e `tratado_por` ficam nas
conta = models.ForeignKey( # classes concretas. `linha_analise_vertical` é preenchido só por achados
ContabilConta, on_delete=models.SET_NULL, null=True, blank=True, related_name="achados" # sem uma conta do Balancete por trás (hoje, só
)
# Preenchido só por achados sem uma conta do Balancete por trás (hoje, só
# `regra_variacao_atipica_dre`, que lê a Análise Vertical) — mesmo # `regra_variacao_atipica_dre`, que lê a Análise Vertical) — mesmo
# espírito de `conta` acima, alimenta o botão "Ver na tabela" da tela de # espírito de `conta`, alimenta o botão "Ver na tabela" da tela de
# revisão (navega até a Análise Vertical e destaca esta linha em vez do # revisão (navega até a Análise Vertical e destaca esta linha em vez do
# Balancete). Casado por `ordem` na view, ver `AchadoDetectado. # Balancete). Casado por `ordem` na view, ver `AchadoDetectado.
# ordem_linha_analise_vertical`/`_contabil_recria_achados()`. # ordem_linha_analise_vertical`/`_contabil_recria_achados()`.
linha_analise_vertical = models.ForeignKey(
ContabilLinhaAnaliseVertical, on_delete=models.SET_NULL, null=True, blank=True, related_name="achados"
)
regra = models.CharField("Regra", max_length=60) regra = models.CharField("Regra", max_length=60)
severidade = models.CharField("Severidade", max_length=10, choices=SEVERIDADE_CHOICES) severidade = models.CharField("Severidade", max_length=10, choices=SEVERIDADE_CHOICES)
titulo = models.CharField("Título", max_length=255) titulo = models.CharField("Título", max_length=255)
@ -2295,9 +2383,6 @@ class ContabilAchado(models.Model):
valor_referencia = models.DecimalField(max_digits=16, decimal_places=2, null=True, blank=True) valor_referencia = models.DecimalField(max_digits=16, decimal_places=2, null=True, blank=True)
status = models.CharField("Status", max_length=10, choices=STATUS_CHOICES, default=STATUS_PENDENTE) status = models.CharField("Status", max_length=10, choices=STATUS_CHOICES, default=STATUS_PENDENTE)
observacao_contador = models.TextField("Observação do contador", blank=True) observacao_contador = models.TextField("Observação do contador", blank=True)
tratado_por = models.ForeignKey(
Usuario, on_delete=models.SET_NULL, null=True, blank=True, related_name="achados_contabeis_tratados"
)
tratado_em = models.DateTimeField("Tratado em", null=True, blank=True) tratado_em = models.DateTimeField("Tratado em", null=True, blank=True)
# Mesmo espírito de ContabilConta.oculta_no_relatorio — só afeta a # Mesmo espírito de ContabilConta.oculta_no_relatorio — só afeta a
# seção "Observações" do relatório "Gerar Dashboard" (relevante só pra # seção "Observações" do relatório "Gerar Dashboard" (relevante só pra
@ -2306,15 +2391,48 @@ class ContabilAchado(models.Model):
oculto_no_relatorio = models.BooleanField("Observação oculta no relatório", default=False) oculto_no_relatorio = models.BooleanField("Observação oculta no relatório", default=False)
class Meta: class Meta:
verbose_name = "Achado do Relatório Contábil" abstract = True
verbose_name_plural = "Achados do Relatório Contábil"
ordering = ["-severidade", "id"] ordering = ["-severidade", "id"]
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.titulo} ({self.get_status_display()})" return f"{self.titulo} ({self.get_status_display()})"
class ContabilObservacao(models.Model): class ContabilAchado(ContabilAchadoBase):
apuracao = models.ForeignKey(ContabilApuracao, on_delete=models.CASCADE, related_name="achados")
conta = models.ForeignKey(
ContabilConta, on_delete=models.SET_NULL, null=True, blank=True, related_name="achados"
)
linha_analise_vertical = models.ForeignKey(
ContabilLinhaAnaliseVertical, on_delete=models.SET_NULL, null=True, blank=True, related_name="achados"
)
tratado_por = models.ForeignKey(
Usuario, on_delete=models.SET_NULL, null=True, blank=True, related_name="achados_contabeis_tratados"
)
class Meta(ContabilAchadoBase.Meta):
verbose_name = "Achado do Relatório Contábil"
verbose_name_plural = "Achados do Relatório Contábil"
class ContabilAchadoDePaula(ContabilAchadoBase):
apuracao = models.ForeignKey(ContabilApuracaoDePaula, on_delete=models.CASCADE, related_name="achados")
conta = models.ForeignKey(
ContabilContaDePaula, on_delete=models.SET_NULL, null=True, blank=True, related_name="achados"
)
linha_analise_vertical = models.ForeignKey(
ContabilLinhaAnaliseVerticalDePaula, on_delete=models.SET_NULL, null=True, blank=True, related_name="achados"
)
tratado_por = models.ForeignKey(
Usuario, on_delete=models.SET_NULL, null=True, blank=True, related_name="achados_contabeis_tratados_de_paula"
)
class Meta(ContabilAchadoBase.Meta):
verbose_name = "Achado do Relatório Contábil - De Paula"
verbose_name_plural = "Achados do Relatório Contábil - De Paula"
class ContabilObservacaoBase(models.Model):
"""Uma observação escrita pelo contador sobre uma conta do Balancete, uma """Uma observação escrita pelo contador sobre uma conta do Balancete, uma
linha da DRE ou uma linha da Análise Vertical — **não** pertence à linha da DRE ou uma linha da Análise Vertical — **não** pertence à
apuração, e sim à empresa + conta, atravessando competências (pedido apuração, e sim à empresa + conta, atravessando competências (pedido
@ -2373,13 +2491,11 @@ class ContabilObservacao(models.Model):
# só pra exibir o histórico quando aquela conta não existir mais na # só pra exibir o histórico quando aquela conta não existir mais na
# apuração aberta (plano de contas mudou, linha saiu do relatório). # apuração aberta (plano de contas mudou, linha saiu do relatório).
alvo_rotulo = models.CharField("Rótulo do alvo", max_length=320, blank=True) alvo_rotulo = models.CharField("Rótulo do alvo", max_length=320, blank=True)
# `SET_NULL` (não CASCADE) de propósito: o histórico é do cliente, não da # `apuracao_origem` (nas classes concretas) é `SET_NULL`, não CASCADE, de
# apuração — excluir a análise de um mês não pode apagar o que o contador # propósito: o histórico é do cliente, não da apuração — excluir a análise
# registrou sobre aquela conta. `competencia_origem` é uma cópia # de um mês não pode apagar o que o contador registrou sobre aquela conta.
# justamente pra vigência continuar resolvendo sem a apuração original. # `competencia_origem` é uma cópia justamente pra vigência continuar
apuracao_origem = models.ForeignKey( # resolvendo sem a apuração original.
ContabilApuracao, on_delete=models.SET_NULL, null=True, blank=True, related_name="observacoes"
)
competencia_origem = models.DateField("Competência de origem") competencia_origem = models.DateField("Competência de origem")
texto = models.TextField("Observação") texto = models.TextField("Observação")
# Substitui o antigo `oculta_no_relatorio` das linhas, com o sinal # Substitui o antigo `oculta_no_relatorio` das linhas, com o sinal
@ -2388,27 +2504,18 @@ class ContabilObservacao(models.Model):
# mesma decisão de sempre: uma observação nova só vai pro relatório do # mesma decisão de sempre: uma observação nova só vai pro relatório do
# cliente quando o contador confirmar explicitamente. # cliente quando o contador confirmar explicitamente.
mostrar_ao_cliente = models.BooleanField("Mostrar ao cliente no relatório", default=False) mostrar_ao_cliente = models.BooleanField("Mostrar ao cliente no relatório", default=False)
criado_por = models.ForeignKey(
Usuario, on_delete=models.SET_NULL, null=True, blank=True, related_name="observacoes_contabeis"
)
criado_em = models.DateTimeField(auto_now_add=True) criado_em = models.DateTimeField(auto_now_add=True)
atualizado_em = models.DateTimeField(auto_now=True) atualizado_em = models.DateTimeField(auto_now=True)
# "Ocultar das próximas execuções": a observação continua visível na # "Ocultar das próximas execuções": a observação continua visível na
# competência em que foi encerrada (e em todas as anteriores, o histórico # competência em que foi encerrada (e em todas as anteriores, o histórico
# nunca é reescrito) e some a partir da seguinte. # nunca é reescrito) e some a partir da seguinte. `encerrada_por` fica
# nas classes concretas.
encerrada_em_competencia = models.DateField("Encerrada na competência", null=True, blank=True) encerrada_em_competencia = models.DateField("Encerrada na competência", null=True, blank=True)
encerrada_por = models.ForeignKey(
Usuario, on_delete=models.SET_NULL, null=True, blank=True, related_name="observacoes_contabeis_encerradas"
)
encerrada_em = models.DateTimeField("Encerrada em", null=True, blank=True) encerrada_em = models.DateTimeField("Encerrada em", null=True, blank=True)
class Meta: class Meta:
verbose_name = "Observação do Relatório Contábil" abstract = True
verbose_name_plural = "Observações do Relatório Contábil"
ordering = ["competencia_origem", "criado_em", "id"] ordering = ["competencia_origem", "criado_em", "id"]
indexes = [
models.Index(fields=["codigo_empresa", "alvo_tipo", "alvo_chave"], name="contabil_obs_alvo_idx"),
]
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.codigo_empresa} {self.alvo_chave} ({self.competencia_origem:%m/%Y})" return f"{self.codigo_empresa} {self.alvo_chave} ({self.competencia_origem:%m/%Y})"
@ -2456,7 +2563,49 @@ class ContabilObservacao(models.Model):
return self.competencia_origem < competencia return self.competencia_origem < competencia
class ContabilObservacaoEdicao(models.Model): class ContabilObservacao(ContabilObservacaoBase):
apuracao_origem = models.ForeignKey(
ContabilApuracao, on_delete=models.SET_NULL, null=True, blank=True, related_name="observacoes"
)
criado_por = models.ForeignKey(
Usuario, on_delete=models.SET_NULL, null=True, blank=True, related_name="observacoes_contabeis"
)
encerrada_por = models.ForeignKey(
Usuario, on_delete=models.SET_NULL, null=True, blank=True, related_name="observacoes_contabeis_encerradas"
)
class Meta(ContabilObservacaoBase.Meta):
verbose_name = "Observação do Relatório Contábil"
verbose_name_plural = "Observações do Relatório Contábil"
indexes = [
models.Index(fields=["codigo_empresa", "alvo_tipo", "alvo_chave"], name="contabil_obs_alvo_idx"),
]
class ContabilObservacaoDePaula(ContabilObservacaoBase):
apuracao_origem = models.ForeignKey(
ContabilApuracaoDePaula, on_delete=models.SET_NULL, null=True, blank=True, related_name="observacoes"
)
criado_por = models.ForeignKey(
Usuario, on_delete=models.SET_NULL, null=True, blank=True, related_name="observacoes_contabeis_de_paula"
)
encerrada_por = models.ForeignKey(
Usuario,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="observacoes_contabeis_encerradas_de_paula",
)
class Meta(ContabilObservacaoBase.Meta):
verbose_name = "Observação do Relatório Contábil - De Paula"
verbose_name_plural = "Observações do Relatório Contábil - De Paula"
indexes = [
models.Index(fields=["codigo_empresa", "alvo_tipo", "alvo_chave"], name="contabil_obs_dp_alvo_idx"),
]
class ContabilObservacaoEdicaoBase(models.Model):
"""Log de cada edição de texto de uma `ContabilObservacao` (pedido """Log de cada edição de texto de uma `ContabilObservacao` (pedido
explícito do usuário, depois que o botão de excluir observação foi explícito do usuário, depois que o botão de excluir observação foi
removido da tela — "evitar a ocultação de observações importantes" exige removido da tela — "evitar a ocultação de observações importantes" exige
@ -2470,23 +2619,44 @@ class ContabilObservacaoEdicao(models.Model):
relatório HTML gerado pro cliente (`dashboard()`) não usa esse relatório HTML gerado pro cliente (`dashboard()`) não usa esse
serializer, então o selo/histórico nunca vaza pra lá.""" serializer, então o selo/histórico nunca vaza pra lá."""
observacao = models.ForeignKey(ContabilObservacao, on_delete=models.CASCADE, related_name="edicoes")
texto_anterior = models.TextField("Texto anterior") texto_anterior = models.TextField("Texto anterior")
texto_novo = models.TextField("Texto novo") texto_novo = models.TextField("Texto novo")
editado_por = models.ForeignKey(
Usuario, on_delete=models.SET_NULL, null=True, blank=True, related_name="edicoes_observacoes_contabeis"
)
editado_em = models.DateTimeField("Editado em", auto_now_add=True) editado_em = models.DateTimeField("Editado em", auto_now_add=True)
class Meta: class Meta:
verbose_name = "Edição de observação do Relatório Contábil" abstract = True
verbose_name_plural = "Edições de observação do Relatório Contábil"
ordering = ["-editado_em", "-id"] ordering = ["-editado_em", "-id"]
def __str__(self) -> str: def __str__(self) -> str:
return f"Edição de {self.observacao_id} em {self.editado_em:%d/%m/%Y %H:%M}" return f"Edição de {self.observacao_id} em {self.editado_em:%d/%m/%Y %H:%M}"
class ContabilObservacaoEdicao(ContabilObservacaoEdicaoBase):
observacao = models.ForeignKey(ContabilObservacao, on_delete=models.CASCADE, related_name="edicoes")
editado_por = models.ForeignKey(
Usuario, on_delete=models.SET_NULL, null=True, blank=True, related_name="edicoes_observacoes_contabeis"
)
class Meta(ContabilObservacaoEdicaoBase.Meta):
verbose_name = "Edição de observação do Relatório Contábil"
verbose_name_plural = "Edições de observação do Relatório Contábil"
class ContabilObservacaoEdicaoDePaula(ContabilObservacaoEdicaoBase):
observacao = models.ForeignKey(ContabilObservacaoDePaula, on_delete=models.CASCADE, related_name="edicoes")
editado_por = models.ForeignKey(
Usuario,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="edicoes_observacoes_contabeis_de_paula",
)
class Meta(ContabilObservacaoEdicaoBase.Meta):
verbose_name = "Edição de observação do Relatório Contábil - De Paula"
verbose_name_plural = "Edições de observação do Relatório Contábil - De Paula"
class IndicadorContabilDefinicao(models.Model): class IndicadorContabilDefinicao(models.Model):
"""Um indicador financeiro do Relatório Contábil — todo indicador é um """Um indicador financeiro do Relatório Contábil — todo indicador é um
registro aqui agora, inclusive os antigos 11 "de sistema" (ROA/ROE/ registro aqui agora, inclusive os antigos 11 "de sistema" (ROA/ROE/

View File

@ -22,13 +22,21 @@ from .models import (
CompromissoAgenda, CompromissoAgenda,
ConciliacaoFornecedor, ConciliacaoFornecedor,
ContabilAchado, ContabilAchado,
ContabilAchadoDePaula,
ContabilApuracao, ContabilApuracao,
ContabilApuracaoDePaula,
ContabilApuracaoReprocessamento, ContabilApuracaoReprocessamento,
ContabilApuracaoReprocessamentoDePaula,
ContabilConta, ContabilConta,
ContabilContaDePaula,
ContabilLinhaAnaliseVertical, ContabilLinhaAnaliseVertical,
ContabilLinhaAnaliseVerticalDePaula,
ContabilLinhaDre, ContabilLinhaDre,
ContabilLinhaDreDePaula,
ContabilObservacao, ContabilObservacao,
ContabilObservacaoDePaula,
ContabilObservacaoEdicao, ContabilObservacaoEdicao,
ContabilObservacaoEdicaoDePaula,
Departamento, Departamento,
EmpresaQuestor, EmpresaQuestor,
Favorito, Favorito,
@ -2793,6 +2801,75 @@ class ContabilApuracaoDetailSerializer(serializers.ModelSerializer):
] ]
# --- Relatório Contábil - De Paula (Diretoria) ------------------------------
# Mesmos serializers da original, apontados para as tabelas da De Paula. Os
# `PrimaryKeyRelatedField` de apuração são o ponto que importa para o
# isolamento: com o queryset da De Paula, uma requisição nos endpoints dela
# nunca resolve o id de uma apuração da original, nem o contrário.
class ContabilContaDePaulaSerializer(ContabilContaSerializer):
class Meta(ContabilContaSerializer.Meta):
model = ContabilContaDePaula
class ContabilLinhaDreDePaulaSerializer(ContabilLinhaDreSerializer):
class Meta(ContabilLinhaDreSerializer.Meta):
model = ContabilLinhaDreDePaula
class ContabilLinhaAnaliseVerticalDePaulaSerializer(ContabilLinhaAnaliseVerticalSerializer):
class Meta(ContabilLinhaAnaliseVerticalSerializer.Meta):
model = ContabilLinhaAnaliseVerticalDePaula
class ContabilObservacaoEdicaoDePaulaSerializer(ContabilObservacaoEdicaoSerializer):
class Meta(ContabilObservacaoEdicaoSerializer.Meta):
model = ContabilObservacaoEdicaoDePaula
class ContabilObservacaoDePaulaSerializer(ContabilObservacaoSerializer):
edicoes = ContabilObservacaoEdicaoDePaulaSerializer(many=True, read_only=True)
class Meta(ContabilObservacaoSerializer.Meta):
model = ContabilObservacaoDePaula
class ContabilObservacaoCreateDePaulaSerializer(ContabilObservacaoCreateSerializer):
apuracao = serializers.PrimaryKeyRelatedField(queryset=ContabilApuracaoDePaula.objects.all())
class ContabilObservacaoApuracaoDePaulaSerializer(ContabilObservacaoApuracaoSerializer):
apuracao = serializers.PrimaryKeyRelatedField(queryset=ContabilApuracaoDePaula.objects.all())
class ContabilAchadoDePaulaSerializer(ContabilAchadoSerializer):
class Meta(ContabilAchadoSerializer.Meta):
model = ContabilAchadoDePaula
class ContabilApuracaoReprocessamentoDePaulaSerializer(ContabilApuracaoReprocessamentoSerializer):
class Meta(ContabilApuracaoReprocessamentoSerializer.Meta):
model = ContabilApuracaoReprocessamentoDePaula
class ContabilApuracaoListDePaulaSerializer(ContabilApuracaoListSerializer):
reprocessamentos = ContabilApuracaoReprocessamentoDePaulaSerializer(many=True, read_only=True)
class Meta(ContabilApuracaoListSerializer.Meta):
model = ContabilApuracaoDePaula
class ContabilApuracaoDetailDePaulaSerializer(ContabilApuracaoDetailSerializer):
contas = ContabilContaDePaulaSerializer(many=True, read_only=True)
linhas_dre = ContabilLinhaDreDePaulaSerializer(many=True, read_only=True)
linhas_analise_vertical = ContabilLinhaAnaliseVerticalDePaulaSerializer(many=True, read_only=True)
achados = ContabilAchadoDePaulaSerializer(many=True, read_only=True)
class Meta(ContabilApuracaoDetailSerializer.Meta):
model = ContabilApuracaoDePaula
# --- Conciliação de Fornecedores (Utilitários) ----------------------------- # --- Conciliação de Fornecedores (Utilitários) -----------------------------
# O detalhe (contas + lançamentos + análise recalculada) é montado em # O detalhe (contas + lançamentos + análise recalculada) é montado em
# `_conciliacao_detalhe()` em views.py, porque depende da análise de # `_conciliacao_detalhe()` em views.py, porque depende da análise de

View File

@ -110,6 +110,25 @@ router.register(
views.IndicadorContabilDefinicaoViewSet, views.IndicadorContabilDefinicaoViewSet,
basename="contabil-indicador-definicao", basename="contabil-indicador-definicao",
) )
# Relatório Contábil - De Paula (Diretoria): mesmos endpoints com sufixo
# "-de-paula", tabelas e permissão próprias. O cadastro de indicadores
# (`contabil-indicadores-definicoes`) é o mesmo das duas aplicações.
router.register(
"contabil-apuracoes-de-paula", views.ContabilApuracaoDePaulaViewSet, basename="contabil-apuracao-de-paula"
)
router.register("contabil-contas-de-paula", views.ContabilContaDePaulaViewSet, basename="contabil-conta-de-paula")
router.register(
"contabil-linhas-dre-de-paula", views.ContabilLinhaDreDePaulaViewSet, basename="contabil-linha-dre-de-paula"
)
router.register(
"contabil-linhas-analise-vertical-de-paula",
views.ContabilLinhaAnaliseVerticalDePaulaViewSet,
basename="contabil-linha-analise-vertical-de-paula",
)
router.register(
"contabil-observacoes-de-paula", views.ContabilObservacaoDePaulaViewSet, basename="contabil-observacao-de-paula"
)
router.register("contabil-achados-de-paula", views.ContabilAchadoDePaulaViewSet, basename="contabil-achado-de-paula")
router.register( router.register(
"conciliacoes-fornecedores", views.ConciliacaoFornecedorViewSet, basename="conciliacao-fornecedor" "conciliacoes-fornecedores", views.ConciliacaoFornecedorViewSet, basename="conciliacao-fornecedor"

View File

@ -34,7 +34,7 @@ from django.views.decorators.csrf import ensure_csrf_cookie
from rest_framework import status, viewsets from rest_framework import status, viewsets
from rest_framework.decorators import action, api_view, permission_classes from rest_framework.decorators import action, api_view, permission_classes
from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.permissions import SAFE_METHODS, AllowAny, BasePermission, IsAuthenticated from rest_framework.permissions import OR, SAFE_METHODS, AllowAny, BasePermission, IsAuthenticated
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
@ -74,13 +74,20 @@ from .models import (
ConciliacaoFornecedorVinculo, ConciliacaoFornecedorVinculo,
ConciliacaoFornecedorVinculoHistorico, ConciliacaoFornecedorVinculoHistorico,
ContabilAchado, ContabilAchado,
ContabilAchadoBase,
ContabilAchadoDePaula,
ContabilApuracao, ContabilApuracao,
ContabilApuracaoReprocessamento, ContabilApuracaoBase,
ContabilApuracaoDePaula,
ContabilConta, ContabilConta,
ContabilContaDePaula,
ContabilLinhaAnaliseVertical, ContabilLinhaAnaliseVertical,
ContabilLinhaAnaliseVerticalDePaula,
ContabilLinhaDre, ContabilLinhaDre,
ContabilLinhaDreDePaula,
ContabilObservacao, ContabilObservacao,
ContabilObservacaoEdicao, ContabilObservacaoBase,
ContabilObservacaoDePaula,
Departamento, Departamento,
Favorito, Favorito,
FuncaoTelefonia, FuncaoTelefonia,
@ -154,20 +161,29 @@ from .serializers import (
ConciliacaoFornecedorValidarSerializer, ConciliacaoFornecedorValidarSerializer,
ConciliacaoFornecedorVincularSerializer, ConciliacaoFornecedorVincularSerializer,
ContabilAchadoAjusteSerializer, ContabilAchadoAjusteSerializer,
ContabilAchadoDePaulaSerializer,
ContabilAchadoOcultoSerializer, ContabilAchadoOcultoSerializer,
ContabilAchadoValidarSerializer, ContabilAchadoValidarSerializer,
ContabilAchadoSerializer, ContabilAchadoSerializer,
ContabilApuracaoCreateSerializer, ContabilApuracaoCreateSerializer,
ContabilApuracaoDetailDePaulaSerializer,
ContabilApuracaoDetailSerializer, ContabilApuracaoDetailSerializer,
ContabilApuracaoListDePaulaSerializer,
ContabilApuracaoListSerializer, ContabilApuracaoListSerializer,
ContabilContaDePaulaSerializer,
ContabilContaSerializer, ContabilContaSerializer,
ContabilIndicadoresOcultosSerializer, ContabilIndicadoresOcultosSerializer,
ContabilIndicadoresSelecionadosSerializer, ContabilIndicadoresSelecionadosSerializer,
ContabilResumoFechamentoSerializer, ContabilResumoFechamentoSerializer,
ContabilLinhaAnaliseVerticalDePaulaSerializer,
ContabilLinhaAnaliseVerticalSerializer, ContabilLinhaAnaliseVerticalSerializer,
ContabilLinhaDreDePaulaSerializer,
ContabilLinhaDreSerializer, ContabilLinhaDreSerializer,
ContabilObservacaoApuracaoDePaulaSerializer,
ContabilObservacaoApuracaoSerializer, ContabilObservacaoApuracaoSerializer,
ContabilObservacaoCreateDePaulaSerializer,
ContabilObservacaoCreateSerializer, ContabilObservacaoCreateSerializer,
ContabilObservacaoDePaulaSerializer,
ContabilObservacaoSerializer, ContabilObservacaoSerializer,
ContabilObservacaoUpdateSerializer, ContabilObservacaoUpdateSerializer,
DepartamentoSerializer, DepartamentoSerializer,
@ -3428,15 +3444,24 @@ def nao_conformidades_dashboard_view(request: Request) -> Response:
) )
def _contabil_monta_historico(cabecalho: ContabilCabecalhoExtraido) -> list[ContabilSnapshotHistorico]: _CONTABIL_PERMISSAO = ("relatorios", "dashboard-contabil")
_CONTABIL_DE_PAULA_PERMISSAO = ("diretoria", "dashboard-contabil-de-paula")
def _contabil_monta_historico(
modelo_apuracao: type[ContabilApuracaoBase], cabecalho: ContabilCabecalhoExtraido
) -> list[ContabilSnapshotHistorico]:
"""Histórico da mesma empresa (até 2 apurações anteriores, mais recente """Histórico da mesma empresa (até 2 apurações anteriores, mais recente
primeiro) já persistidas — usado pelas regras de variação mês a mês primeiro) já persistidas — usado pelas regras de variação mês a mês
(`dashboard_contabil.regras`). Só uma consulta simples ao ORM, sem (`dashboard_contabil.regras`). Só uma consulta simples ao ORM, sem
depender de nenhum relatório complementar anexado (ver CLAUDE.md do depender de nenhum relatório complementar anexado (ver CLAUDE.md do
pacote).""" pacote). `modelo_apuracao` é a tabela da aplicação que está processando
(original ou De Paula): o histórico de uma nunca entra na outra. O
pipeline chama com só o cabeçalho, então o chamador fixa o model com
`functools.partial`."""
competencia_atual = cabecalho.periodo_fim.replace(day=1) competencia_atual = cabecalho.periodo_fim.replace(day=1)
anteriores = ( anteriores = (
ContabilApuracao.objects.filter(codigo_empresa=cabecalho.codigo_empresa, competencia__lt=competencia_atual) modelo_apuracao.objects.filter(codigo_empresa=cabecalho.codigo_empresa, competencia__lt=competencia_atual)
.order_by("-competencia") .order_by("-competencia")
.prefetch_related("contas", "linhas_dre")[:2] .prefetch_related("contas", "linhas_dre")[:2]
) )
@ -3450,7 +3475,9 @@ def _contabil_monta_historico(cabecalho: ContabilCabecalhoExtraido) -> list[Cont
] ]
def _contabil_monta_historico_completo(codigo_empresa: str, competencia_atual: date) -> list[ContabilSnapshotHistorico]: def _contabil_monta_historico_completo(
modelo_apuracao: type[ContabilApuracaoBase], codigo_empresa: str, competencia_atual: date
) -> list[ContabilSnapshotHistorico]:
"""Mesma ideia de `_contabil_monta_historico`, mas **sem** o `[:2]` — """Mesma ideia de `_contabil_monta_historico`, mas **sem** o `[:2]` —
histórico completo da empresa (mais recente primeiro), usado só pelo histórico completo da empresa (mais recente primeiro), usado só pelo
relatório "Gerar Dashboard" (`ContabilApuracaoViewSet.dashboard`), pro relatório "Gerar Dashboard" (`ContabilApuracaoViewSet.dashboard`), pro
@ -3458,7 +3485,7 @@ def _contabil_monta_historico_completo(codigo_empresa: str, competencia_atual: d
(`dashboard_contabil.indicadores`). As regras de auditoria continuam (`dashboard_contabil.indicadores`). As regras de auditoria continuam
usando `_contabil_monta_historico` (capado em 2), sem mudar.""" usando `_contabil_monta_historico` (capado em 2), sem mudar."""
anteriores = ( anteriores = (
ContabilApuracao.objects.filter(codigo_empresa=codigo_empresa, competencia__lt=competencia_atual) modelo_apuracao.objects.filter(codigo_empresa=codigo_empresa, competencia__lt=competencia_atual)
.order_by("-competencia") .order_by("-competencia")
.prefetch_related("contas", "linhas_dre") .prefetch_related("contas", "linhas_dre")
) )
@ -3488,13 +3515,15 @@ class _ContabilDadosIndicadores:
historico_completo: list[ContabilSnapshotHistorico] historico_completo: list[ContabilSnapshotHistorico]
def _contabil_coleta_dados_indicadores(apuracao: ContabilApuracao) -> _ContabilDadosIndicadores: def _contabil_coleta_dados_indicadores(apuracao: ContabilApuracaoBase) -> _ContabilDadosIndicadores:
contas = list(apuracao.contas.order_by("ordem", "id")) contas = list(apuracao.contas.order_by("ordem", "id"))
linhas_dre = list(apuracao.linhas_dre.order_by("ordem", "id")) linhas_dre = list(apuracao.linhas_dre.order_by("ordem", "id"))
contas_atuais = {conta.codigo: conta.saldo_atual for conta in contas} contas_atuais = {conta.codigo: conta.saldo_atual for conta in contas}
dre_atual = {linha.descricao: linha.valor for linha in linhas_dre} dre_atual = {linha.descricao: linha.valor for linha in linhas_dre}
resultado_liquido = linhas_dre[-1].valor if linhas_dre else Decimal(0) resultado_liquido = linhas_dre[-1].valor if linhas_dre else Decimal(0)
historico_completo = _contabil_monta_historico_completo(apuracao.codigo_empresa, apuracao.competencia) historico_completo = _contabil_monta_historico_completo(
type(apuracao), apuracao.codigo_empresa, apuracao.competencia
)
# `variacao_conta` (componente de indicador personalizado, ver abaixo) # `variacao_conta` (componente de indicador personalizado, ver abaixo)
# só compara contra a apuração anterior imediata — mesma convenção já # só compara contra a apuração anterior imediata — mesma convenção já
# usada pra Depreciação/Amortização em `indicadores.calcula_indicadores` # usada pra Depreciação/Amortização em `indicadores.calcula_indicadores`
@ -3770,7 +3799,15 @@ def _contabil_agrupa_indicadores_cards(cards: list[dict[str, Any]]) -> list[dict
return [{"nome": nome, "cards": grupos[nome]} for nome in sorted(grupos, key=ordenacao)] return [{"nome": nome, "cards": grupos[nome]} for nome in sorted(grupos, key=ordenacao)]
def _contabil_dados_resumo(apuracao: ContabilApuracao) -> dict[str, Any]: def _contabil_modelo_observacao(apuracao: ContabilApuracaoBase) -> type[ContabilObservacaoBase]:
"""Tabela de observações da mesma aplicação da apuração (original ou De
Paula), pelo reverse de `apuracao_origem`. Observação é casada por
empresa + chave natural, não por FK, então a consulta precisa partir do
model certo para o histórico de uma aplicação nunca aparecer na outra."""
return apuracao.observacoes.model
def _contabil_dados_resumo(apuracao: ContabilApuracaoBase) -> dict[str, Any]:
"""Indicadores (já agrupados em cards) + observações visíveis ao cliente """Indicadores (já agrupados em cards) + observações visíveis ao cliente
+ achados com observação do contador — o conteúdo da aba "Resumo" do + achados com observação do contador — o conteúdo da aba "Resumo" do
relatório "Gerar Dashboard", reaproveitado tal e qual pelo PDF do Resumo relatório "Gerar Dashboard", reaproveitado tal e qual pelo PDF do Resumo
@ -3794,7 +3831,7 @@ def _contabil_dados_resumo(apuracao: ContabilApuracao) -> dict[str, Any]:
# justamente o ponto do histórico. # justamente o ponto do histórico.
observacoes_visiveis = [ observacoes_visiveis = [
observacao observacao
for observacao in ContabilObservacao.vigentes_para( for observacao in _contabil_modelo_observacao(apuracao).vigentes_para(
apuracao.codigo_empresa, apuracao.competencia apuracao.codigo_empresa, apuracao.competencia
).select_related("criado_por") ).select_related("criado_por")
if observacao.mostrar_ao_cliente if observacao.mostrar_ao_cliente
@ -3901,7 +3938,7 @@ def _contabil_observacoes_por_chave(apuracao: ContabilApuracao, alvo_tipo: str)
por linha.""" por linha."""
agrupadas: dict[str, list[ContabilObservacao]] = {} agrupadas: dict[str, list[ContabilObservacao]] = {}
consulta = ( consulta = (
ContabilObservacao.vigentes_para(apuracao.codigo_empresa, apuracao.competencia) _contabil_modelo_observacao(apuracao).vigentes_para(apuracao.codigo_empresa, apuracao.competencia)
.filter(alvo_tipo=alvo_tipo) .filter(alvo_tipo=alvo_tipo)
.select_related("criado_por", "encerrada_por") .select_related("criado_por", "encerrada_por")
) )
@ -4014,8 +4051,7 @@ def _contabil_sincroniza_contas(
antiga.alterada_reprocessamento = alterou antiga.alterada_reprocessamento = alterou
antiga.save() antiga.save()
else: else:
ContabilConta.objects.create( apuracao.contas.create(
apuracao=apuracao,
ordem=indice, ordem=indice,
conta_numero=extraida.conta_numero, conta_numero=extraida.conta_numero,
codigo=extraida.codigo, codigo=extraida.codigo,
@ -4058,8 +4094,7 @@ def _contabil_sincroniza_linhas_dre(
antiga.alterada_reprocessamento = alterou antiga.alterada_reprocessamento = alterou
antiga.save() antiga.save()
else: else:
ContabilLinhaDre.objects.create( apuracao.linhas_dre.create(
apuracao=apuracao,
ordem=linha.ordem, ordem=linha.ordem,
descricao=linha.descricao, descricao=linha.descricao,
nivel=linha.nivel, nivel=linha.nivel,
@ -4111,8 +4146,7 @@ def _contabil_sincroniza_linhas_analise_vertical(
antiga.alterada_reprocessamento = alterou antiga.alterada_reprocessamento = alterou
antiga.save() antiga.save()
else: else:
ContabilLinhaAnaliseVertical.objects.create( apuracao.linhas_analise_vertical.create(
apuracao=apuracao,
ordem=linha.ordem, ordem=linha.ordem,
descricao=linha.descricao, descricao=linha.descricao,
nivel=linha.nivel, nivel=linha.nivel,
@ -4150,12 +4184,22 @@ def _contabil_recria_achados(
(`ContabilObservacao`, ver o model) não vivem aqui, são casadas por chave (`ContabilObservacao`, ver o model) não vivem aqui, são casadas por chave
natural (empresa+conta) e continuam intactas através de qualquer natural (empresa+conta) e continuam intactas através de qualquer
reprocessamento — só o apontamento automático é recriado.""" reprocessamento — só o apontamento automático é recriado."""
apuracao.achados.all().delete()
_contabil_cria_achados(apuracao, achados_detectados)
def _contabil_cria_achados(
apuracao: ContabilApuracaoBase, achados_detectados: list[dashboard_contabil_modelos.AchadoDetectado]
) -> None:
"""`bulk_create` dos achados de uma apuração, usado na criação e no
reprocessamento. O model do achado vem do reverse `apuracao.achados`, então
serve às duas aplicações (original e De Paula) sem citar a classe."""
modelo_achado = apuracao.achados.model
contas_por_codigo = {conta.codigo: conta for conta in apuracao.contas.all()} contas_por_codigo = {conta.codigo: conta for conta in apuracao.contas.all()}
av_por_ordem = {linha.ordem: linha for linha in apuracao.linhas_analise_vertical.all()} av_por_ordem = {linha.ordem: linha for linha in apuracao.linhas_analise_vertical.all()}
apuracao.achados.all().delete() modelo_achado.objects.bulk_create(
ContabilAchado.objects.bulk_create(
[ [
ContabilAchado( modelo_achado(
apuracao=apuracao, apuracao=apuracao,
conta=contas_por_codigo.get(achado.codigo_conta) if achado.codigo_conta else None, conta=contas_por_codigo.get(achado.codigo_conta) if achado.codigo_conta else None,
linha_analise_vertical=( linha_analise_vertical=(
@ -4182,15 +4226,28 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
persistido da mesma empresa e roda o motor de regras — tudo isso **antes** persistido da mesma empresa e roda o motor de regras — tudo isso **antes**
de gravar qualquer coisa no banco, já que `codigo_empresa`/`competencia` de gravar 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 (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).""" informados pelo usuário no upload (diferente de IndicadorApuracao).
Serve também ao Relatório Contábil - De Paula (Diretoria), via
`ContabilApuracaoDePaulaViewSet`, que só troca os atributos de classe
abaixo (permissão, tabela, serializers e prefixo da API). Nada aqui pode
citar `ContabilApuracao`/`ContabilConta`/... direto: o model sai de
`self.queryset.model` ou dos reverses da própria apuração."""
http_method_names = ["get", "post", "delete", "head", "options"] http_method_names = ["get", "post", "delete", "head", "options"]
queryset = ContabilApuracao.objects.all() queryset = ContabilApuracao.objects.all()
permissao_app = _CONTABIL_PERMISSAO
serializer_lista = ContabilApuracaoListSerializer
serializer_detalhe = ContabilApuracaoDetailSerializer
serializer_observacao = ContabilObservacaoSerializer
# Prefixo da API usado pelos links do relatório HTML (exportar XLSX, PDF
# do Resumo), que precisam apontar para os endpoints da mesma aplicação.
prefixo_api = "/api/contabil-apuracoes"
def get_permissions(self) -> list[BasePermission]: def get_permissions(self) -> list[BasePermission]:
return [PermissaoApp("relatorios", "dashboard-contabil")] return [PermissaoApp(*self.permissao_app)]
def get_queryset(self) -> QuerySet[ContabilApuracao]: def get_queryset(self) -> QuerySet[ContabilApuracaoBase]:
queryset = super().get_queryset() queryset = super().get_queryset()
if self.action == "retrieve": if self.action == "retrieve":
queryset = queryset.prefetch_related( queryset = queryset.prefetch_related(
@ -4202,10 +4259,10 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
def get_serializer_class(self) -> type[ModelSerializer]: def get_serializer_class(self) -> type[ModelSerializer]:
if self.action == "list": if self.action == "list":
return ContabilApuracaoListSerializer return self.serializer_lista
return ContabilApuracaoDetailSerializer return self.serializer_detalhe
def perform_destroy(self, instance: ContabilApuracao) -> None: def perform_destroy(self, instance: ContabilApuracaoBase) -> None:
"""Análise **Concluída** não pode mais ser excluída — pedido explícito """Análise **Concluída** não pode mais ser excluída — pedido explícito
do usuário, mesma trava de "concluiu, não se mexe mais" que já valia do usuário, mesma trava de "concluiu, não se mexe mais" que já valia
pra reprocessar e pra editar observação/achado pra reprocessar e pra editar observação/achado
@ -4225,17 +4282,18 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
entrada.is_valid(raise_exception=True) entrada.is_valid(raise_exception=True)
arquivo = entrada.validated_data["arquivo"] arquivo = entrada.validated_data["arquivo"]
conteudo = arquivo.read() conteudo = arquivo.read()
modelo_apuracao = self.queryset.model
try: try:
resultado = dashboard_contabil_pipeline.processa_apuracao( resultado = dashboard_contabil_pipeline.processa_apuracao(
io.BytesIO(conteudo), _contabil_monta_historico, arquivo.name io.BytesIO(conteudo), functools.partial(_contabil_monta_historico, modelo_apuracao), arquivo.name
) )
except ContabilExtracaoInvalidaError as exc: except ContabilExtracaoInvalidaError as exc:
return _contabil_resposta_extracao_invalida(exc) return _contabil_resposta_extracao_invalida(exc)
cabecalho = resultado.extracao.cabecalho cabecalho = resultado.extracao.cabecalho
competencia = cabecalho.periodo_fim.replace(day=1) competencia = cabecalho.periodo_fim.replace(day=1)
if ContabilApuracao.objects.filter(codigo_empresa=cabecalho.codigo_empresa, competencia=competencia).exists(): if modelo_apuracao.objects.filter(codigo_empresa=cabecalho.codigo_empresa, competencia=competencia).exists():
return Response( return Response(
{"detail": f"Já existe uma análise para {cabecalho.nome_empresa} na competência {competencia:%m/%Y}."}, {"detail": f"Já existe uma análise para {cabecalho.nome_empresa} na competência {competencia:%m/%Y}."},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
@ -4244,7 +4302,7 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
# O PDF não é gravado: depois de extraído, nada mais o usa. # O PDF não é gravado: depois de extraído, nada mais o usa.
try: try:
with transaction.atomic(): with transaction.atomic():
apuracao = ContabilApuracao.objects.create( apuracao = modelo_apuracao.objects.create(
codigo_empresa=cabecalho.codigo_empresa, codigo_empresa=cabecalho.codigo_empresa,
nome_empresa=cabecalho.nome_empresa, nome_empresa=cabecalho.nome_empresa,
cnpj=cabecalho.cnpj, cnpj=cabecalho.cnpj,
@ -4256,9 +4314,12 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
fonte_pdf_atipica=resultado.extracao.fonte_pdf_atipica, fonte_pdf_atipica=resultado.extracao.fonte_pdf_atipica,
leiaute=resultado.extracao.leiaute, leiaute=resultado.extracao.leiaute,
) )
ContabilConta.objects.bulk_create( modelo_conta = apuracao.contas.model
modelo_linha_dre = apuracao.linhas_dre.model
modelo_linha_av = apuracao.linhas_analise_vertical.model
modelo_conta.objects.bulk_create(
[ [
ContabilConta( modelo_conta(
apuracao=apuracao, apuracao=apuracao,
ordem=indice, ordem=indice,
conta_numero=conta.conta_numero, conta_numero=conta.conta_numero,
@ -4273,9 +4334,9 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
for indice, conta in enumerate(resultado.extracao.contas) for indice, conta in enumerate(resultado.extracao.contas)
] ]
) )
ContabilLinhaDre.objects.bulk_create( modelo_linha_dre.objects.bulk_create(
[ [
ContabilLinhaDre( modelo_linha_dre(
apuracao=apuracao, apuracao=apuracao,
ordem=linha.ordem, ordem=linha.ordem,
descricao=linha.descricao, descricao=linha.descricao,
@ -4286,9 +4347,9 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
for linha in resultado.extracao.linhas_dre for linha in resultado.extracao.linhas_dre
] ]
) )
ContabilLinhaAnaliseVertical.objects.bulk_create( modelo_linha_av.objects.bulk_create(
[ [
ContabilLinhaAnaliseVertical( modelo_linha_av(
apuracao=apuracao, apuracao=apuracao,
ordem=linha.ordem, ordem=linha.ordem,
descricao=linha.descricao, descricao=linha.descricao,
@ -4302,34 +4363,14 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
for linha in resultado.extracao.linhas_analise_vertical for linha in resultado.extracao.linhas_analise_vertical
] ]
) )
contas_por_codigo = {conta.codigo: conta for conta in apuracao.contas.all()} _contabil_cria_achados(apuracao, resultado.achados)
av_por_ordem = {linha.ordem: linha for linha in apuracao.linhas_analise_vertical.all()}
ContabilAchado.objects.bulk_create(
[
ContabilAchado(
apuracao=apuracao,
conta=contas_por_codigo.get(achado.codigo_conta) if achado.codigo_conta else None,
linha_analise_vertical=(
av_por_ordem.get(achado.ordem_linha_analise_vertical)
if achado.ordem_linha_analise_vertical is not None
else None
),
regra=achado.regra,
severidade=achado.severidade,
titulo=achado.titulo,
mensagem=achado.mensagem,
valor_referencia=achado.valor_referencia,
)
for achado in resultado.achados
]
)
except Exception: except Exception:
return Response( return Response(
{"detail": "Não foi possível salvar a análise. Contate o setor de Inovação."}, {"detail": "Não foi possível salvar a análise. Contate o setor de Inovação."},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
serializer = ContabilApuracaoDetailSerializer(apuracao) serializer = self.serializer_detalhe(apuracao)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
@action(detail=True, methods=["post"]) @action(detail=True, methods=["post"])
@ -4381,7 +4422,7 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
try: try:
resultado = dashboard_contabil_pipeline.processa_apuracao( resultado = dashboard_contabil_pipeline.processa_apuracao(
io.BytesIO(conteudo), _contabil_monta_historico, arquivo.name io.BytesIO(conteudo), functools.partial(_contabil_monta_historico, type(apuracao)), arquivo.name
) )
except ContabilExtracaoInvalidaError as exc: except ContabilExtracaoInvalidaError as exc:
return _contabil_resposta_extracao_invalida(exc) return _contabil_resposta_extracao_invalida(exc)
@ -4428,17 +4469,17 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
_contabil_recria_achados( _contabil_recria_achados(
apuracao, resultado.achados + dashboard_contabil_regras.achados_itens_removidos(removidos) apuracao, resultado.achados + dashboard_contabil_regras.achados_itens_removidos(removidos)
) )
ContabilApuracaoReprocessamento.objects.create(apuracao=apuracao, reprocessado_por=request.user) apuracao.reprocessamentos.create(reprocessado_por=request.user)
except Exception: except Exception:
return Response( return Response(
{"detail": "Não foi possível reprocessar a análise. Contate o setor de Inovação."}, {"detail": "Não foi possível reprocessar a análise. Contate o setor de Inovação."},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
apuracao =ContabilApuracao.objects.prefetch_related( apuracao = self.queryset.model.objects.prefetch_related(
"contas", "linhas_dre", "linhas_analise_vertical", "achados__conta" "contas", "linhas_dre", "linhas_analise_vertical", "achados__conta"
).get(pk=apuracao.pk) ).get(pk=apuracao.pk)
return Response(ContabilApuracaoDetailSerializer(apuracao).data) return Response(self.serializer_detalhe(apuracao).data)
@action(detail=True, methods=["post"]) @action(detail=True, methods=["post"])
def concluir(self, request: Request, pk: str | None = None) -> Response: def concluir(self, request: Request, pk: str | None = None) -> Response:
@ -4451,7 +4492,7 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
apuracao.status = ContabilApuracao.STATUS_CONCLUIDA apuracao.status = ContabilApuracao.STATUS_CONCLUIDA
apuracao.concluida_em = timezone.now() apuracao.concluida_em = timezone.now()
apuracao.save(update_fields=["status", "concluida_em"]) apuracao.save(update_fields=["status", "concluida_em"])
return Response(ContabilApuracaoDetailSerializer(apuracao).data) return Response(self.serializer_detalhe(apuracao).data)
@action(detail=True, methods=["get"]) @action(detail=True, methods=["get"])
def observacoes(self, request: Request, pk: str | None = None) -> Response: def observacoes(self, request: Request, pk: str | None = None) -> Response:
@ -4462,11 +4503,12 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
linha (a linha é recriada a cada apuração, a observação não).""" linha (a linha é recriada a cada apuração, a observação não)."""
apuracao = self.get_object() apuracao = self.get_object()
consulta = ( consulta = (
ContabilObservacao.vigentes_para(apuracao.codigo_empresa, apuracao.competencia) _contabil_modelo_observacao(apuracao)
.vigentes_para(apuracao.codigo_empresa, apuracao.competencia)
.select_related("criado_por", "encerrada_por") .select_related("criado_por", "encerrada_por")
.prefetch_related("edicoes__editado_por") .prefetch_related("edicoes__editado_por")
) )
serializada = ContabilObservacaoSerializer( serializada = self.serializer_observacao(
consulta, many=True, context={"competencia": apuracao.competencia} consulta, many=True, context={"competencia": apuracao.competencia}
) )
return Response(serializada.data) return Response(serializada.data)
@ -4574,6 +4616,7 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
"achados_com_observacao": dados_resumo["achados_com_observacao"], "achados_com_observacao": dados_resumo["achados_com_observacao"],
"gerado_em": timezone.now(), "gerado_em": timezone.now(),
"logo_data_uri": _logo_relatorio_data_uri(), "logo_data_uri": _logo_relatorio_data_uri(),
"prefixo_api": self.prefixo_api,
} }
html = render_to_string("dashboard-contabil-relatorio.html", contexto) html = render_to_string("dashboard-contabil-relatorio.html", contexto)
return HttpResponse(html, content_type="text/html; charset=utf-8") return HttpResponse(html, content_type="text/html; charset=utf-8")
@ -4839,9 +4882,10 @@ class ContabilContaViewSet(viewsets.ModelViewSet):
http_method_names = ["get", "patch", "head", "options"] http_method_names = ["get", "patch", "head", "options"]
queryset = ContabilConta.objects.all() queryset = ContabilConta.objects.all()
serializer_class = ContabilContaSerializer serializer_class = ContabilContaSerializer
permissao_app = _CONTABIL_PERMISSAO
def get_permissions(self) -> list[BasePermission]: def get_permissions(self) -> list[BasePermission]:
return [PermissaoApp("relatorios", "dashboard-contabil")] return [PermissaoApp(*self.permissao_app)]
def perform_update(self, serializer: ContabilContaSerializer) -> None: def perform_update(self, serializer: ContabilContaSerializer) -> None:
_contabil_garante_em_revisao(serializer.instance.apuracao) _contabil_garante_em_revisao(serializer.instance.apuracao)
@ -4856,9 +4900,10 @@ class ContabilLinhaDreViewSet(viewsets.ModelViewSet):
http_method_names = ["get", "patch", "head", "options"] http_method_names = ["get", "patch", "head", "options"]
queryset = ContabilLinhaDre.objects.all() queryset = ContabilLinhaDre.objects.all()
serializer_class = ContabilLinhaDreSerializer serializer_class = ContabilLinhaDreSerializer
permissao_app = _CONTABIL_PERMISSAO
def get_permissions(self) -> list[BasePermission]: def get_permissions(self) -> list[BasePermission]:
return [PermissaoApp("relatorios", "dashboard-contabil")] return [PermissaoApp(*self.permissao_app)]
def perform_update(self, serializer: ContabilLinhaDreSerializer) -> None: def perform_update(self, serializer: ContabilLinhaDreSerializer) -> None:
_contabil_garante_em_revisao(serializer.instance.apuracao) _contabil_garante_em_revisao(serializer.instance.apuracao)
@ -4873,9 +4918,10 @@ class ContabilLinhaAnaliseVerticalViewSet(viewsets.ModelViewSet):
http_method_names = ["get", "patch", "head", "options"] http_method_names = ["get", "patch", "head", "options"]
queryset = ContabilLinhaAnaliseVertical.objects.all() queryset = ContabilLinhaAnaliseVertical.objects.all()
serializer_class = ContabilLinhaAnaliseVerticalSerializer serializer_class = ContabilLinhaAnaliseVerticalSerializer
permissao_app = _CONTABIL_PERMISSAO
def get_permissions(self) -> list[BasePermission]: def get_permissions(self) -> list[BasePermission]:
return [PermissaoApp("relatorios", "dashboard-contabil")] return [PermissaoApp(*self.permissao_app)]
def perform_update(self, serializer: ContabilLinhaAnaliseVerticalSerializer) -> None: def perform_update(self, serializer: ContabilLinhaAnaliseVerticalSerializer) -> None:
_contabil_garante_em_revisao(serializer.instance.apuracao) _contabil_garante_em_revisao(serializer.instance.apuracao)
@ -4920,21 +4966,24 @@ class ContabilObservacaoViewSet(viewsets.ModelViewSet):
http_method_names = ["post", "patch", "delete", "head", "options"] http_method_names = ["post", "patch", "delete", "head", "options"]
queryset = ContabilObservacao.objects.select_related("criado_por", "encerrada_por", "apuracao_origem") queryset = ContabilObservacao.objects.select_related("criado_por", "encerrada_por", "apuracao_origem")
serializer_class = ContabilObservacaoSerializer serializer_class = ContabilObservacaoSerializer
serializer_criacao = ContabilObservacaoCreateSerializer
serializer_apuracao = ContabilObservacaoApuracaoSerializer
permissao_app = _CONTABIL_PERMISSAO
def get_permissions(self) -> list[BasePermission]: def get_permissions(self) -> list[BasePermission]:
return [PermissaoApp("relatorios", "dashboard-contabil")] return [PermissaoApp(*self.permissao_app)]
def _resolve_alvo(self, apuracao: ContabilApuracao, alvo_tipo: str, alvo_id: int) -> Any: def _resolve_alvo(self, apuracao: ContabilApuracaoBase, alvo_tipo: str, alvo_id: int) -> Any:
"""Conta/linha **desta** apuração que recebeu a observação — pegar o """Conta/linha **desta** apuração que recebeu a observação — pegar o
objeto (em vez de confiar numa chave enviada pelo cliente) garante objeto (em vez de confiar numa chave enviada pelo cliente) garante
que a chave natural gravada é sempre a real e que o alvo pertence que a chave natural gravada é sempre a real e que o alvo pertence
mesmo à apuração informada.""" mesmo à apuração informada."""
modelos = { relacoes = {
ContabilObservacao.ALVO_CONTA: ContabilConta, ContabilObservacao.ALVO_CONTA: apuracao.contas,
ContabilObservacao.ALVO_DRE: ContabilLinhaDre, ContabilObservacao.ALVO_DRE: apuracao.linhas_dre,
ContabilObservacao.ALVO_ANALISE_VERTICAL: ContabilLinhaAnaliseVertical, ContabilObservacao.ALVO_ANALISE_VERTICAL: apuracao.linhas_analise_vertical,
} }
alvo = modelos[alvo_tipo].objects.filter(pk=alvo_id, apuracao=apuracao).first() alvo = relacoes[alvo_tipo].filter(pk=alvo_id).first()
if alvo is None: if alvo is None:
raise ValidationError({"alvo_id": "Conta ou linha não encontrada nesta análise."}) raise ValidationError({"alvo_id": "Conta ou linha não encontrada nesta análise."})
return alvo return alvo
@ -4952,13 +5001,13 @@ class ContabilObservacaoViewSet(viewsets.ModelViewSet):
) )
def create(self, request: Request, *args: Any, **kwargs: Any) -> Response: def create(self, request: Request, *args: Any, **kwargs: Any) -> Response:
entrada = ContabilObservacaoCreateSerializer(data=request.data) entrada = self.serializer_criacao(data=request.data)
entrada.is_valid(raise_exception=True) entrada.is_valid(raise_exception=True)
dados = entrada.validated_data dados = entrada.validated_data
apuracao: ContabilApuracao = dados["apuracao"] apuracao: ContabilApuracaoBase = dados["apuracao"]
_contabil_garante_em_revisao(apuracao) _contabil_garante_em_revisao(apuracao)
alvo = self._resolve_alvo(apuracao, dados["alvo_tipo"], dados["alvo_id"]) alvo = self._resolve_alvo(apuracao, dados["alvo_tipo"], dados["alvo_id"])
observacao = ContabilObservacao.objects.create( observacao = self.queryset.model.objects.create(
codigo_empresa=apuracao.codigo_empresa, codigo_empresa=apuracao.codigo_empresa,
alvo_tipo=dados["alvo_tipo"], alvo_tipo=dados["alvo_tipo"],
alvo_chave=_contabil_chave_alvo(apuracao, dados["alvo_tipo"], alvo), alvo_chave=_contabil_chave_alvo(apuracao, dados["alvo_tipo"], alvo),
@ -4969,11 +5018,11 @@ class ContabilObservacaoViewSet(viewsets.ModelViewSet):
mostrar_ao_cliente=dados["mostrar_ao_cliente"], mostrar_ao_cliente=dados["mostrar_ao_cliente"],
criado_por=request.user, criado_por=request.user,
) )
serializada = ContabilObservacaoSerializer(observacao, context={"competencia": apuracao.competencia}) serializada = self.serializer_class(observacao, context={"competencia": apuracao.competencia})
return Response(serializada.data, status=status.HTTP_201_CREATED) return Response(serializada.data, status=status.HTTP_201_CREATED)
def partial_update(self, request: Request, *args: Any, **kwargs: Any) -> Response: def partial_update(self, request: Request, *args: Any, **kwargs: Any) -> Response:
observacao: ContabilObservacao = self.get_object() observacao: ContabilObservacaoBase = self.get_object()
entrada = ContabilObservacaoUpdateSerializer(data=request.data) entrada = ContabilObservacaoUpdateSerializer(data=request.data)
entrada.is_valid(raise_exception=True) entrada.is_valid(raise_exception=True)
dados = entrada.validated_data dados = entrada.validated_data
@ -4982,8 +5031,7 @@ class ContabilObservacaoViewSet(viewsets.ModelViewSet):
self._garante_texto_editavel(observacao) self._garante_texto_editavel(observacao)
texto_novo = dados["texto"].strip() texto_novo = dados["texto"].strip()
if texto_novo != observacao.texto: if texto_novo != observacao.texto:
ContabilObservacaoEdicao.objects.create( observacao.edicoes.create(
observacao=observacao,
texto_anterior=observacao.texto, texto_anterior=observacao.texto,
texto_novo=texto_novo, texto_novo=texto_novo,
editado_por=request.user, editado_por=request.user,
@ -4995,9 +5043,9 @@ class ContabilObservacaoViewSet(viewsets.ModelViewSet):
campos.append("mostrar_ao_cliente") campos.append("mostrar_ao_cliente")
observacao.save(update_fields=[*campos, "atualizado_em"]) observacao.save(update_fields=[*campos, "atualizado_em"])
competencia = observacao.apuracao_origem.competencia if observacao.apuracao_origem else None competencia = observacao.apuracao_origem.competencia if observacao.apuracao_origem else None
return Response(ContabilObservacaoSerializer(observacao, context={"competencia": competencia}).data) return Response(self.serializer_class(observacao, context={"competencia": competencia}).data)
def perform_destroy(self, instance: ContabilObservacao) -> None: def perform_destroy(self, instance: ContabilObservacaoBase) -> None:
self._garante_texto_editavel(instance) self._garante_texto_editavel(instance)
if instance.criado_por_id != self.request.user.id: if instance.criado_por_id != self.request.user.id:
raise PermissionDenied("Só quem registrou esta observação pode excluí-la.") raise PermissionDenied("Só quem registrou esta observação pode excluí-la.")
@ -5009,15 +5057,15 @@ class ContabilObservacaoViewSet(viewsets.ModelViewSet):
competência informada (e em todas as anteriores — o histórico nunca é competência informada (e em todas as anteriores — o histórico nunca é
reescrito) e some a partir da seguinte.""" reescrito) e some a partir da seguinte."""
observacao = self.get_object() observacao = self.get_object()
entrada = ContabilObservacaoApuracaoSerializer(data=request.data) entrada = self.serializer_apuracao(data=request.data)
entrada.is_valid(raise_exception=True) entrada.is_valid(raise_exception=True)
apuracao: ContabilApuracao = entrada.validated_data["apuracao"] apuracao: ContabilApuracaoBase = entrada.validated_data["apuracao"]
_contabil_garante_em_revisao(apuracao) _contabil_garante_em_revisao(apuracao)
observacao.encerrada_em_competencia = apuracao.competencia observacao.encerrada_em_competencia = apuracao.competencia
observacao.encerrada_por = request.user observacao.encerrada_por = request.user
observacao.encerrada_em = timezone.now() observacao.encerrada_em = timezone.now()
observacao.save(update_fields=["encerrada_em_competencia", "encerrada_por", "encerrada_em", "atualizado_em"]) observacao.save(update_fields=["encerrada_em_competencia", "encerrada_por", "encerrada_em", "atualizado_em"])
serializada = ContabilObservacaoSerializer(observacao, context={"competencia": apuracao.competencia}) serializada = self.serializer_class(observacao, context={"competencia": apuracao.competencia})
return Response(serializada.data) return Response(serializada.data)
@action(detail=True, methods=["post"]) @action(detail=True, methods=["post"])
@ -5026,15 +5074,15 @@ class ContabilObservacaoViewSet(viewsets.ModelViewSet):
frente. Existe pro contador não ficar preso a um clique errado; o frente. Existe pro contador não ficar preso a um clique errado; o
texto continua intocado nos dois caminhos.""" texto continua intocado nos dois caminhos."""
observacao = self.get_object() observacao = self.get_object()
entrada = ContabilObservacaoApuracaoSerializer(data=request.data) entrada = self.serializer_apuracao(data=request.data)
entrada.is_valid(raise_exception=True) entrada.is_valid(raise_exception=True)
apuracao: ContabilApuracao = entrada.validated_data["apuracao"] apuracao: ContabilApuracaoBase = entrada.validated_data["apuracao"]
_contabil_garante_em_revisao(apuracao) _contabil_garante_em_revisao(apuracao)
observacao.encerrada_em_competencia = None observacao.encerrada_em_competencia = None
observacao.encerrada_por = None observacao.encerrada_por = None
observacao.encerrada_em = None observacao.encerrada_em = None
observacao.save(update_fields=["encerrada_em_competencia", "encerrada_por", "encerrada_em", "atualizado_em"]) observacao.save(update_fields=["encerrada_em_competencia", "encerrada_por", "encerrada_em", "atualizado_em"])
serializada = ContabilObservacaoSerializer(observacao, context={"competencia": apuracao.competencia}) serializada = self.serializer_class(observacao, context={"competencia": apuracao.competencia})
return Response(serializada.data) return Response(serializada.data)
@ -5046,17 +5094,19 @@ class ContabilAchadoViewSet(viewsets.ModelViewSet):
http_method_names = ["get", "patch", "post", "head", "options"] http_method_names = ["get", "patch", "post", "head", "options"]
queryset = ContabilAchado.objects.all() queryset = ContabilAchado.objects.all()
serializer_leitura = ContabilAchadoSerializer
permissao_app = _CONTABIL_PERMISSAO
def get_permissions(self) -> list[BasePermission]: def get_permissions(self) -> list[BasePermission]:
return [PermissaoApp("relatorios", "dashboard-contabil")] return [PermissaoApp(*self.permissao_app)]
def get_serializer_class(self) -> type[ModelSerializer]: def get_serializer_class(self) -> type[ModelSerializer]:
if self.action in ("update", "partial_update"): if self.action in ("update", "partial_update"):
return ContabilAchadoAjusteSerializer return ContabilAchadoAjusteSerializer
return ContabilAchadoSerializer return self.serializer_leitura
def perform_update(self, serializer: ContabilAchadoAjusteSerializer) -> None: def perform_update(self, serializer: ContabilAchadoAjusteSerializer) -> None:
achado: ContabilAchado = self.get_object() achado: ContabilAchadoBase = self.get_object()
_contabil_garante_em_revisao(achado.apuracao) _contabil_garante_em_revisao(achado.apuracao)
achado.status = serializer.validated_data["status"] achado.status = serializer.validated_data["status"]
achado.observacao_contador = serializer.validated_data["observacao_contador"] achado.observacao_contador = serializer.validated_data["observacao_contador"]
@ -5067,7 +5117,7 @@ class ContabilAchadoViewSet(viewsets.ModelViewSet):
def update(self, request: Request, *args: Any, **kwargs: Any) -> Response: def update(self, request: Request, *args: Any, **kwargs: Any) -> Response:
super().update(request, *args, **kwargs) super().update(request, *args, **kwargs)
achado = self.get_object() achado = self.get_object()
return Response(ContabilAchadoSerializer(achado).data) return Response(self.serializer_leitura(achado).data)
@action(detail=True, methods=["post"], url_path="alternar-oculto") @action(detail=True, methods=["post"], url_path="alternar-oculto")
def alternar_oculto(self, request: Request, pk: str | None = None) -> Response: def alternar_oculto(self, request: Request, pk: str | None = None) -> Response:
@ -5083,7 +5133,7 @@ class ContabilAchadoViewSet(viewsets.ModelViewSet):
entrada.is_valid(raise_exception=True) entrada.is_valid(raise_exception=True)
achado.oculto_no_relatorio = entrada.validated_data["oculto_no_relatorio"] achado.oculto_no_relatorio = entrada.validated_data["oculto_no_relatorio"]
achado.save(update_fields=["oculto_no_relatorio"]) achado.save(update_fields=["oculto_no_relatorio"])
return Response(ContabilAchadoSerializer(achado).data) return Response(self.serializer_leitura(achado).data)
@action(detail=True, methods=["post"]) @action(detail=True, methods=["post"])
def validar(self, request: Request, pk: str | None = None) -> Response: def validar(self, request: Request, pk: str | None = None) -> Response:
@ -5110,7 +5160,7 @@ class ContabilAchadoViewSet(viewsets.ModelViewSet):
achado.tratado_por = None achado.tratado_por = None
achado.tratado_em = None achado.tratado_em = None
achado.save(update_fields=["status", "tratado_por", "tratado_em"]) achado.save(update_fields=["status", "tratado_por", "tratado_em"])
return Response(ContabilAchadoSerializer(achado).data) return Response(self.serializer_leitura(achado).data)
def _gera_chave_indicador_contabil(nome: str) -> str: def _gera_chave_indicador_contabil(nome: str) -> str:
@ -5150,7 +5200,11 @@ class IndicadorContabilDefinicaoViewSet(viewsets.ModelViewSet):
queryset = IndicadorContabilDefinicao.objects.prefetch_related("componentes").all() queryset = IndicadorContabilDefinicao.objects.prefetch_related("componentes").all()
def get_permissions(self) -> list[BasePermission]: def get_permissions(self) -> list[BasePermission]:
return [PermissaoApp("relatorios", "dashboard-contabil")] # O cadastro de indicadores é compartilhado com o Relatório Contábil -
# De Paula (decisão do usuário): quem tem qualquer uma das duas
# aplicações gerencia os mesmos indicadores. Só o cadastro é
# compartilhado; nenhum dado de apuração passa por aqui.
return [OR(PermissaoApp(*_CONTABIL_PERMISSAO), PermissaoApp(*_CONTABIL_DE_PAULA_PERMISSAO))]
def get_serializer_class(self) -> type[ModelSerializer]: def get_serializer_class(self) -> type[ModelSerializer]:
if self.action in ("create", "update", "partial_update"): if self.action in ("create", "update", "partial_update"):
@ -5246,8 +5300,10 @@ class IndicadorContabilDefinicaoViewSet(viewsets.ModelViewSet):
# em `ContabilApuracao.indicadores_selecionados`/`indicadores_ocultos` # em `ContabilApuracao.indicadores_selecionados`/`indicadores_ocultos`
# e a próxima tentativa de alternar QUALQUER indicador no hub daquela # e a próxima tentativa de alternar QUALQUER indicador no hub daquela
# apuração falharia (a validação reenvia a lista inteira a cada # apuração falharia (a validação reenvia a lista inteira a cada
# alternância). Ver bug real, rodada 106. # alternância). Ver bug real, rodada 106. Varre as duas aplicações,
for apuracao in ContabilApuracao.objects.filter( # já que o cadastro de indicadores é compartilhado entre elas.
for modelo_apuracao in (ContabilApuracao, ContabilApuracaoDePaula):
for apuracao in modelo_apuracao.objects.filter(
Q(indicadores_selecionados__contains=[chave]) | Q(indicadores_ocultos__contains=[chave]) Q(indicadores_selecionados__contains=[chave]) | Q(indicadores_ocultos__contains=[chave])
): ):
campos_alterados = [] campos_alterados = []
@ -5263,6 +5319,56 @@ class IndicadorContabilDefinicaoViewSet(viewsets.ModelViewSet):
apuracao.save(update_fields=campos_alterados) apuracao.save(update_fields=campos_alterados)
# --- Relatório Contábil - De Paula (Diretoria) ------------------------------
# Mesmas views da original, só trocando permissão, tabelas e serializers
# (ver docstring de `ContabilApuracaoViewSet`). A permissão é própria
# (`diretoria` > `dashboard-contabil-de-paula`): ter acesso ao Relatório
# Contábil não dá acesso a nenhum destes endpoints, e cada queryset só
# enxerga as tabelas da De Paula. Ver "Relatório Contábil - De Paula" no
# CLAUDE.md do pacote `dashboard_contabil`.
class ContabilApuracaoDePaulaViewSet(ContabilApuracaoViewSet):
queryset = ContabilApuracaoDePaula.objects.all()
permissao_app = _CONTABIL_DE_PAULA_PERMISSAO
serializer_lista = ContabilApuracaoListDePaulaSerializer
serializer_detalhe = ContabilApuracaoDetailDePaulaSerializer
serializer_observacao = ContabilObservacaoDePaulaSerializer
prefixo_api = "/api/contabil-apuracoes-de-paula"
class ContabilContaDePaulaViewSet(ContabilContaViewSet):
queryset = ContabilContaDePaula.objects.all()
serializer_class = ContabilContaDePaulaSerializer
permissao_app = _CONTABIL_DE_PAULA_PERMISSAO
class ContabilLinhaDreDePaulaViewSet(ContabilLinhaDreViewSet):
queryset = ContabilLinhaDreDePaula.objects.all()
serializer_class = ContabilLinhaDreDePaulaSerializer
permissao_app = _CONTABIL_DE_PAULA_PERMISSAO
class ContabilLinhaAnaliseVerticalDePaulaViewSet(ContabilLinhaAnaliseVerticalViewSet):
queryset = ContabilLinhaAnaliseVerticalDePaula.objects.all()
serializer_class = ContabilLinhaAnaliseVerticalDePaulaSerializer
permissao_app = _CONTABIL_DE_PAULA_PERMISSAO
class ContabilObservacaoDePaulaViewSet(ContabilObservacaoViewSet):
queryset = ContabilObservacaoDePaula.objects.select_related("criado_por", "encerrada_por", "apuracao_origem")
serializer_class = ContabilObservacaoDePaulaSerializer
serializer_criacao = ContabilObservacaoCreateDePaulaSerializer
serializer_apuracao = ContabilObservacaoApuracaoDePaulaSerializer
permissao_app = _CONTABIL_DE_PAULA_PERMISSAO
class ContabilAchadoDePaulaViewSet(ContabilAchadoViewSet):
queryset = ContabilAchadoDePaula.objects.all()
serializer_leitura = ContabilAchadoDePaulaSerializer
permissao_app = _CONTABIL_DE_PAULA_PERMISSAO
# --- Conciliação de Fornecedores (Utilitários) ----------------------------- # --- Conciliação de Fornecedores (Utilitários) -----------------------------
# Ver portal_api/conciliacao_fornecedores/CLAUDE.md. Os vínculos # Ver portal_api/conciliacao_fornecedores/CLAUDE.md. Os vínculos
# (automáticos na criação, manuais depois) são gravados; status/alertas de # (automáticos na criação, manuais depois) são gravados; status/alertas de

1
prd.md
View File

@ -35,6 +35,7 @@ Colaboradores do escritório, autenticados por login/senha (sessão Django). O a
### Relatórios (perfis com acesso liberado) ### 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. - **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. - **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.
- **Relatório Contábil - De Paula** (Diretoria) — a mesma ferramenta, para as empresas do próprio escritório. Mesmas regras e indicadores; análises, histórico e permissão totalmente separados da versão de clientes, de propósito: quem tem acesso só ao Relatório Contábil não acessa esta. Nasce sem acesso para nenhum perfil, liberada manualmente.
### Recursos visuais (todo usuário, sem item de menu) ### 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. - **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.

View File

@ -1,3 +1,26 @@
// Este arquivo serve duas páginas: o Relatório Contábil (dashboard-contabil.html)
// e o Relatório Contábil - De Paula (dashboard-contabil-de-paula.html, seção
// Diretoria). A variante define window.PID_DC_CONFIG num <script> inline antes
// deste arquivo, com a permissão e os prefixos de endpoint dela; na original
// window.PID_DC_CONFIG não existe e os defaults abaixo valem. Toda rota de
// apuração/conta/linha/observação/achado passa por aqui, nunca escrita à mão:
// uma rota literal faria a variante gravar nas tabelas da outra aplicação. O
// cadastro de indicadores (/contabil-indicadores-definicoes/) é o mesmo das
// duas, por decisão do usuário.
const PID_DC_CONFIG = Object.assign(
{
moduleKey: "relatorios",
appKey: "dashboard-contabil",
endpointApuracoes: "/contabil-apuracoes",
endpointContas: "/contabil-contas",
endpointLinhasDre: "/contabil-linhas-dre",
endpointLinhasAnaliseVertical: "/contabil-linhas-analise-vertical",
endpointObservacoes: "/contabil-observacoes",
endpointAchados: "/contabil-achados",
},
window.PID_DC_CONFIG || {}
);
const PID_DC_STATUS_LABELS = { revisao: "Em revisão", concluida: "Concluída" }; const PID_DC_STATUS_LABELS = { revisao: "Em revisão", concluida: "Concluída" };
const PID_DC_SEVERIDADE_LABELS = { alta: "Alta", media: "Média", baixa: "Baixa" }; const PID_DC_SEVERIDADE_LABELS = { alta: "Alta", media: "Média", baixa: "Baixa" };
const PID_DC_ACHADO_STATUS_LABELS = { pendente: "Pendente", tratado: "Tratado", ignorado: "Ignorado" }; const PID_DC_ACHADO_STATUS_LABELS = { pendente: "Pendente", tratado: "Tratado", ignorado: "Ignorado" };
@ -315,27 +338,27 @@ document.addEventListener("click", pidDcEscondeDica, true);
window.addEventListener("scroll", pidDcEscondeDica, true); window.addEventListener("scroll", pidDcEscondeDica, true);
async function pidFetchApuracoesContabil() { async function pidFetchApuracoesContabil() {
return pidApiRequest("/contabil-apuracoes/"); return pidApiRequest(`${PID_DC_CONFIG.endpointApuracoes}/`);
} }
async function pidFetchApuracaoContabil(id) { async function pidFetchApuracaoContabil(id) {
return pidApiRequest(`/contabil-apuracoes/${id}/`); return pidApiRequest(`${PID_DC_CONFIG.endpointApuracoes}/${id}/`);
} }
async function pidCriarApuracaoContabil(formData) { async function pidCriarApuracaoContabil(formData) {
return pidApiRequest("/contabil-apuracoes/", { method: "POST", body: formData }); return pidApiRequest(`${PID_DC_CONFIG.endpointApuracoes}/`, { method: "POST", body: formData });
} }
async function pidExcluirApuracaoContabil(id) { async function pidExcluirApuracaoContabil(id) {
return pidApiRequest(`/contabil-apuracoes/${id}/`, { method: "DELETE" }); return pidApiRequest(`${PID_DC_CONFIG.endpointApuracoes}/${id}/`, { method: "DELETE" });
} }
async function pidConcluirApuracaoContabil(id) { async function pidConcluirApuracaoContabil(id) {
return pidApiRequest(`/contabil-apuracoes/${id}/concluir/`, { method: "POST" }); return pidApiRequest(`${PID_DC_CONFIG.endpointApuracoes}/${id}/concluir/`, { method: "POST" });
} }
async function pidReprocessarApuracaoContabil(id, formData) { async function pidReprocessarApuracaoContabil(id, formData) {
return pidApiRequest(`/contabil-apuracoes/${id}/reprocessar/`, { method: "POST", body: formData }); return pidApiRequest(`${PID_DC_CONFIG.endpointApuracoes}/${id}/reprocessar/`, { method: "POST", body: formData });
} }
/** /**
@ -349,7 +372,7 @@ async function pidReprocessarApuracaoContabil(id, formData) {
* navegação de verdade pra URL da API, isso deixa de ser um problema. * navegação de verdade pra URL da API, isso deixa de ser um problema.
*/ */
function pidGerarDashboardContabil(id) { function pidGerarDashboardContabil(id) {
window.open(`/api/contabil-apuracoes/${id}/dashboard/`, "_blank"); window.open(`/api${PID_DC_CONFIG.endpointApuracoes}/${id}/dashboard/`, "_blank");
} }
// ---- Observações (histórico por empresa+conta, atravessa competências) ---- // ---- Observações (histórico por empresa+conta, atravessa competências) ----
@ -361,14 +384,14 @@ function pidGerarDashboardContabil(id) {
// `/contabil-observacoes/`, nunca contra a linha. // `/contabil-observacoes/`, nunca contra a linha.
async function pidBuscarObservacoesContabil(apuracaoId) { async function pidBuscarObservacoesContabil(apuracaoId) {
return pidApiRequest(`/contabil-apuracoes/${apuracaoId}/observacoes/`); return pidApiRequest(`${PID_DC_CONFIG.endpointApuracoes}/${apuracaoId}/observacoes/`);
} }
// `alvoId` é o id da conta/linha DESTA apuração — o servidor deriva empresa, // `alvoId` é o id da conta/linha DESTA apuração — o servidor deriva empresa,
// competência, chave natural e rótulo a partir dele (ver // competência, chave natural e rótulo a partir dele (ver
// ContabilObservacaoViewSet.create). // ContabilObservacaoViewSet.create).
async function pidCriarObservacaoContabil(apuracaoId, alvoTipo, alvoId, texto, mostrarAoCliente) { async function pidCriarObservacaoContabil(apuracaoId, alvoTipo, alvoId, texto, mostrarAoCliente) {
return pidApiRequest("/contabil-observacoes/", { return pidApiRequest(`${PID_DC_CONFIG.endpointObservacoes}/`, {
method: "POST", method: "POST",
body: { body: {
apuracao: apuracaoId, apuracao: apuracaoId,
@ -384,17 +407,17 @@ async function pidCriarObservacaoContabil(apuracaoId, alvoTipo, alvoId, texto, m
// enquanto a observação ainda for da competência aberta; a visibilidade ao // enquanto a observação ainda for da competência aberta; a visibilidade ao
// cliente é sempre alternável, inclusive numa observação histórica. // cliente é sempre alternável, inclusive numa observação histórica.
async function pidAtualizarObservacaoContabil(id, payload) { async function pidAtualizarObservacaoContabil(id, payload) {
return pidApiRequest(`/contabil-observacoes/${id}/`, { method: "PATCH", body: payload }); return pidApiRequest(`${PID_DC_CONFIG.endpointObservacoes}/${id}/`, { method: "PATCH", body: payload });
} }
// "Ocultar das próximas execuções": continua visível nesta competência e // "Ocultar das próximas execuções": continua visível nesta competência e
// some a partir da seguinte. `reativar` desfaz. // some a partir da seguinte. `reativar` desfaz.
async function pidEncerrarObservacaoContabil(id, apuracaoId) { async function pidEncerrarObservacaoContabil(id, apuracaoId) {
return pidApiRequest(`/contabil-observacoes/${id}/encerrar/`, { method: "POST", body: { apuracao: apuracaoId } }); return pidApiRequest(`${PID_DC_CONFIG.endpointObservacoes}/${id}/encerrar/`, { method: "POST", body: { apuracao: apuracaoId } });
} }
async function pidReativarObservacaoContabil(id, apuracaoId) { async function pidReativarObservacaoContabil(id, apuracaoId) {
return pidApiRequest(`/contabil-observacoes/${id}/reativar/`, { method: "POST", body: { apuracao: apuracaoId } }); return pidApiRequest(`${PID_DC_CONFIG.endpointObservacoes}/${id}/reativar/`, { method: "POST", body: { apuracao: apuracaoId } });
} }
// Excluir de verdade — botão voltou a existir na tela (rodada 145), restrito // Excluir de verdade — botão voltou a existir na tela (rodada 145), restrito
@ -402,7 +425,7 @@ async function pidReativarObservacaoContabil(id, apuracaoId) {
// revisão (mesmas duas regras checadas de novo no servidor, ver // revisão (mesmas duas regras checadas de novo no servidor, ver
// ContabilObservacaoViewSet.perform_destroy()). // ContabilObservacaoViewSet.perform_destroy()).
async function pidExcluirObservacaoContabil(id) { async function pidExcluirObservacaoContabil(id) {
return pidApiRequest(`/contabil-observacoes/${id}/`, { method: "DELETE" }); return pidApiRequest(`${PID_DC_CONFIG.endpointObservacoes}/${id}/`, { method: "DELETE" });
} }
// `alterada_reprocessamento` nunca é tocado por aqui (pedido explícito do // `alterada_reprocessamento` nunca é tocado por aqui (pedido explícito do
@ -412,19 +435,19 @@ async function pidExcluirObservacaoContabil(id) {
// um reprocessamento novo (sem mudança daquela conta/linha desta vez) limpa // um reprocessamento novo (sem mudança daquela conta/linha desta vez) limpa
// o campo de verdade, em `_contabil_sincroniza_*` (views.py). // o campo de verdade, em `_contabil_sincroniza_*` (views.py).
async function pidAtualizarValidadoContaContabil(id, validado) { async function pidAtualizarValidadoContaContabil(id, validado) {
return pidApiRequest(`/contabil-contas/${id}/`, { method: "PATCH", body: { validado } }); return pidApiRequest(`${PID_DC_CONFIG.endpointContas}/${id}/`, { method: "PATCH", body: { validado } });
} }
async function pidAtualizarValidadoLinhaDreContabil(id, validado) { async function pidAtualizarValidadoLinhaDreContabil(id, validado) {
return pidApiRequest(`/contabil-linhas-dre/${id}/`, { method: "PATCH", body: { validado } }); return pidApiRequest(`${PID_DC_CONFIG.endpointLinhasDre}/${id}/`, { method: "PATCH", body: { validado } });
} }
async function pidAtualizarValidadoLinhaAnaliseVerticalContabil(id, validado) { async function pidAtualizarValidadoLinhaAnaliseVerticalContabil(id, validado) {
return pidApiRequest(`/contabil-linhas-analise-vertical/${id}/`, { method: "PATCH", body: { validado } }); return pidApiRequest(`${PID_DC_CONFIG.endpointLinhasAnaliseVertical}/${id}/`, { method: "PATCH", body: { validado } });
} }
async function pidAtualizarAchadoContabil(id, status, observacaoContador) { async function pidAtualizarAchadoContabil(id, status, observacaoContador) {
return pidApiRequest(`/contabil-achados/${id}/`, { return pidApiRequest(`${PID_DC_CONFIG.endpointAchados}/${id}/`, {
method: "PATCH", method: "PATCH",
body: { status, observacao_contador: observacaoContador }, body: { status, observacao_contador: observacaoContador },
}); });
@ -434,24 +457,24 @@ async function pidAtualizarAchadoContabil(id, status, observacaoContador) {
// justificativa escrita (o texto vai na observação da linha), desvalidar // justificativa escrita (o texto vai na observação da linha), desvalidar
// volta a "pendente" — ver ContabilAchadoViewSet.validar(). // volta a "pendente" — ver ContabilAchadoViewSet.validar().
async function pidValidarAchadoContabil(id, validado) { async function pidValidarAchadoContabil(id, validado) {
return pidApiRequest(`/contabil-achados/${id}/validar/`, { method: "POST", body: { validado } }); return pidApiRequest(`${PID_DC_CONFIG.endpointAchados}/${id}/validar/`, { method: "POST", body: { validado } });
} }
// ---- Aba "Dashboard" (pré-visualização do relatório "Gerar Dashboard") ---- // ---- Aba "Dashboard" (pré-visualização do relatório "Gerar Dashboard") ----
async function pidBuscarIndicadoresContabil(id) { async function pidBuscarIndicadoresContabil(id) {
return pidApiRequest(`/contabil-apuracoes/${id}/indicadores/`); return pidApiRequest(`${PID_DC_CONFIG.endpointApuracoes}/${id}/indicadores/`);
} }
async function pidAtualizarIndicadoresOcultosContabil(id, indicadoresOcultos) { async function pidAtualizarIndicadoresOcultosContabil(id, indicadoresOcultos) {
return pidApiRequest(`/contabil-apuracoes/${id}/indicadores-ocultos/`, { return pidApiRequest(`${PID_DC_CONFIG.endpointApuracoes}/${id}/indicadores-ocultos/`, {
method: "POST", method: "POST",
body: { indicadores_ocultos: indicadoresOcultos }, body: { indicadores_ocultos: indicadoresOcultos },
}); });
} }
async function pidAlternarOcultoAchadoContabil(id, oculto) { async function pidAlternarOcultoAchadoContabil(id, oculto) {
return pidApiRequest(`/contabil-achados/${id}/alternar-oculto/`, { return pidApiRequest(`${PID_DC_CONFIG.endpointAchados}/${id}/alternar-oculto/`, {
method: "POST", method: "POST",
body: { oculto_no_relatorio: oculto }, body: { oculto_no_relatorio: oculto },
}); });
@ -478,21 +501,21 @@ async function pidBuscarDefinicoesIndicadoresContabil() {
} }
async function pidPreVisualizarIndicadorContabil(apuracaoId, payload) { async function pidPreVisualizarIndicadorContabil(apuracaoId, payload) {
return pidApiRequest(`/contabil-apuracoes/${apuracaoId}/pre-visualizar-indicador/`, { return pidApiRequest(`${PID_DC_CONFIG.endpointApuracoes}/${apuracaoId}/pre-visualizar-indicador/`, {
method: "POST", method: "POST",
body: payload, body: payload,
}); });
} }
async function pidAtualizarIndicadoresSelecionadosContabil(id, indicadoresSelecionados) { async function pidAtualizarIndicadoresSelecionadosContabil(id, indicadoresSelecionados) {
return pidApiRequest(`/contabil-apuracoes/${id}/indicadores-selecionados/`, { return pidApiRequest(`${PID_DC_CONFIG.endpointApuracoes}/${id}/indicadores-selecionados/`, {
method: "POST", method: "POST",
body: { indicadores_selecionados: indicadoresSelecionados }, body: { indicadores_selecionados: indicadoresSelecionados },
}); });
} }
async function pidAtualizarResumoFechamentoContabil(id, resumoFechamento) { async function pidAtualizarResumoFechamentoContabil(id, resumoFechamento) {
return pidApiRequest(`/contabil-apuracoes/${id}/resumo-fechamento/`, { return pidApiRequest(`${PID_DC_CONFIG.endpointApuracoes}/${id}/resumo-fechamento/`, {
method: "POST", method: "POST",
body: { resumo_fechamento: resumoFechamento }, body: { resumo_fechamento: resumoFechamento },
}); });
@ -505,8 +528,8 @@ document.addEventListener("DOMContentLoaded", async () => {
const me = await pidGetMe(); const me = await pidGetMe();
if (!me) return; if (!me) return;
const relatoriosPerms = (me.permissoes_efetivas && me.permissoes_efetivas["relatorios"]) || {}; const moduloPerms = (me.permissoes_efetivas && me.permissoes_efetivas[PID_DC_CONFIG.moduleKey]) || {};
const canUse = !!(relatoriosPerms.enabled && relatoriosPerms.apps && relatoriosPerms.apps["dashboard-contabil"]); const canUse = !!(moduloPerms.enabled && moduloPerms.apps && moduloPerms.apps[PID_DC_CONFIG.appKey]);
const noAccessEl = document.getElementById("dc-no-access"); const noAccessEl = document.getElementById("dc-no-access");
const contentEl = document.getElementById("dc-content"); const contentEl = document.getElementById("dc-content");

View File

@ -57,10 +57,27 @@ function pidPerfilFromApi(p) {
nome: p.nome, nome: p.nome,
ativo: p.ativo, ativo: p.ativo,
gerenciaPermissoes: p.gerencia_permissoes, gerenciaPermissoes: p.gerencia_permissoes,
permissions: p.permissoes, permissions: pidCompletaPermissoes(p.permissoes),
}; };
} }
/** Completa o JSON salvo de um perfil com todo módulo/app do catálogo que
* ainda não existe nele, desmarcado. Um módulo novo no catálogo (ex.:
* "Diretoria") não está no JSON dos perfis já gravados, e como o seed não
* roda em produção ninguém o acrescenta — sem isto, abrir um perfil quebra a
* tela ao ler `permissions[modulo].enabled`. Desmarcado é o mesmo que o
* servidor já assume para chave ausente (`permissoes_efetivas()`), então
* nada muda de acesso; o perfil só passa a gravar a chave quando for salvo. */
function pidCompletaPermissoes(permissoes) {
const completo = JSON.parse(JSON.stringify(permissoes || {}));
PID_MODULES.forEach((m) => {
const atual = completo[m.key] || {};
const apps = Object.assign(pidAppsFor(m.key, false), atual.apps || {});
completo[m.key] = Object.assign({}, atual, { enabled: !!atual.enabled, apps });
});
return completo;
}
function pidPerfilToApi(p) { function pidPerfilToApi(p) {
return { return {
nome: p.nome, nome: p.nome,

View File

@ -143,6 +143,17 @@
</ul> </ul>
</li> </li>
<li data-section="diretoria" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M3 13h18" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Diretoria</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="dashboard-contabil-de-paula"><a href="dashboard-contabil-de-paula.html" class="nav-subitem">Relatório Contábil - De Paula</a></li>
</ul>
</li>
<li data-section="relatorios-gerenciais" class="nav-group"> <li data-section="relatorios-gerenciais" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle> <button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg>

View File

@ -144,6 +144,17 @@
</ul> </ul>
</li> </li>
<li data-section="diretoria" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M3 13h18" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Diretoria</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="dashboard-contabil-de-paula"><a href="dashboard-contabil-de-paula.html" class="nav-subitem">Relatório Contábil - De Paula</a></li>
</ul>
</li>
<li data-section="relatorios-gerenciais" class="nav-group"> <li data-section="relatorios-gerenciais" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle> <button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg>

View File

@ -144,6 +144,17 @@
</ul> </ul>
</li> </li>
<li data-section="diretoria" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M3 13h18" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Diretoria</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="dashboard-contabil-de-paula"><a href="dashboard-contabil-de-paula.html" class="nav-subitem">Relatório Contábil - De Paula</a></li>
</ul>
</li>
<li data-section="relatorios-gerenciais" class="nav-group"> <li data-section="relatorios-gerenciais" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle> <button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg>

View File

@ -143,6 +143,17 @@
</ul> </ul>
</li> </li>
<li data-section="diretoria" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M3 13h18" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Diretoria</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="dashboard-contabil-de-paula"><a href="dashboard-contabil-de-paula.html" class="nav-subitem">Relatório Contábil - De Paula</a></li>
</ul>
</li>
<li data-section="relatorios-gerenciais" class="nav-group"> <li data-section="relatorios-gerenciais" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle> <button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg>

View File

@ -0,0 +1,949 @@
{% load static %}
<!DOCTYPE html>
<html lang="pt-BR" data-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>P.I.D. — Relatório Contábil - De Paula</title>
<link rel="icon" type="image/svg+xml" href="{% static 'img/pid-icone-escuro.svg' %}" />
<link rel="stylesheet" href="{% static 'css/tokens.css' %}" />
<link rel="stylesheet" href="{% static 'css/base.css' %}" />
<link rel="stylesheet" href="{% static 'css/layout.css' %}" />
<link rel="stylesheet" href="{% static 'css/components.css' %}" />
<link rel="stylesheet" href="{% static 'css/perfis-acesso.css' %}" />
<link rel="stylesheet" href="{% static 'css/dashboard-contabil.css' %}" />
<script src="{% static 'js/theme.js' %}" defer></script>
</head>
<body>
<div class="app-shell" id="app-shell">
<aside class="sidebar">
<a class="sidebar__brand" href="portal.html" id="sidebar-brand-link" aria-label="Ir para a tela Principal">
<svg class="sidebar__logo sidebar__logo--full" viewBox="0 0 300 80" aria-hidden="true" focusable="false">
<g transform="translate(8,8) scale(1)">
<path d="M10 4 h34 l14 14 v40 a2 2 0 0 1 -2 2 H8 a2 2 0 0 1 -2 -2 V6 a2 2 0 0 1 2 -2 z" fill="#7B5BA8"></path>
<rect x="22" y="4" width="20" height="16" fill="#C6A24A"></rect>
<rect x="34" y="7" width="5" height="10" fill="#7B5BA8"></rect>
<rect x="12" y="26" width="40" height="30" rx="2" fill="#241C33"></rect>
<g class="pid-icon-eye">
<circle cx="24" cy="38" r="5" fill="#F2EDE3"></circle>
<circle cx="26" cy="36.2" r="1.6" fill="#241C33"></circle>
</g>
<g class="pid-icon-eye">
<circle cx="40" cy="38" r="5" fill="#F2EDE3"></circle>
<circle cx="42" cy="36.2" r="1.6" fill="#241C33"></circle>
</g>
<path d="M25 47 q7 6 14 0" stroke="#C6A24A" stroke-width="3" fill="none" stroke-linecap="round"></path>
</g>
<text x="86" y="42" font-family="'Space Grotesk',Helvetica,Arial,sans-serif" font-size="34" font-weight="700" letter-spacing="1.4" fill="#F2EDE3">P.I.D.</text>
<text x="88" y="61" font-family="'DM Mono',ui-monospace,monospace" font-size="9.5" letter-spacing="2.6" fill="#C6A24A">PORTAL INTERNO DA DE PAULA</text>
</svg>
<svg class="sidebar__logo sidebar__logo--icon" viewBox="0 0 64 64" aria-hidden="true" focusable="false">
<path d="M10 4 h34 l14 14 v40 a2 2 0 0 1 -2 2 H8 a2 2 0 0 1 -2 -2 V6 a2 2 0 0 1 2 -2 z" fill="#7B5BA8"></path>
<rect x="22" y="4" width="20" height="16" fill="#C6A24A"></rect>
<rect x="34" y="7" width="5" height="10" fill="#7B5BA8"></rect>
<rect x="12" y="26" width="40" height="30" rx="2" fill="#241C33"></rect>
<g class="pid-icon-eye">
<circle cx="24" cy="38" r="5" fill="#F2EDE3"></circle>
<circle cx="26" cy="36.2" r="1.6" fill="#241C33"></circle>
</g>
<g class="pid-icon-eye">
<circle cx="40" cy="38" r="5" fill="#F2EDE3"></circle>
<circle cx="42" cy="36.2" r="1.6" fill="#241C33"></circle>
</g>
<path d="M25 47 q7 6 14 0" stroke="#C6A24A" stroke-width="3" fill="none" stroke-linecap="round"></path>
</svg>
</a>
<nav class="sidebar__nav">
<ul class="nav-list">
<li data-section="principal">
<a href="portal.html" class="nav-item">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 11l9-7 9 7v9a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1z" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Principal</span>
</a>
</li>
<li data-section="ramais">
<a href="ramais.html" class="nav-item">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4c0 9 7 16 16 16l3-4-6-3-2 2c-3-1.5-5-3.5-6-6l2-2-3-6z" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Ramais</span>
</a>
</li>
<li data-section="calendario">
<a href="#" class="nav-item" id="calendario-depaula-btn">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 9h18M8 3v4M16 3v4" stroke-linecap="round"/></svg>
<span class="nav-item__label">Calendário De Paula</span>
</a>
</li>
<li data-section="calendario-individual">
<a href="calendario-individual.html" class="nav-item">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 9h18M8 3v4M16 3v4" stroke-linecap="round"/><path d="M9 15l2 2 4-4" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Calendário Individual</span>
</a>
</li>
<li data-section="portais" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
<span class="nav-item__label">Portais</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="portal-do-cliente"><a href="#" class="nav-subitem">Portal do Cliente</a></li>
<li data-app="portal-fiscal"><a href="#" class="nav-subitem">Portal Fiscal</a></li>
</ul>
</li>
<li data-section="geradoc" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 3h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/><path d="M14 3v4h4" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Geradoc</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="simulacao-custo-contratacao"><a href="custo-contratacao.html" class="nav-subitem">Simulação de Custo de Contratação</a></li>
<li data-app="indicador-desempenho"><a href="indicador-desempenho.html" class="nav-subitem">Indicador de Desempenho</a></li>
</ul>
</li>
<li data-section="relatorios" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 20V10M12 20V4M20 20v-7" stroke-linecap="round"/></svg>
<span class="nav-item__label">Relatórios</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="indicadores"><a href="#" class="nav-subitem">Indicadores</a></li>
<li class="nav-subgroup" data-subgroup="qualidade">
<button type="button" class="nav-subitem nav-subgroup__toggle" data-subgroup-toggle>
<span>Qualidade</span>
<svg class="nav-item__chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-subgroup__list">
<li data-app="nao-conformidades">
<a href="nao-conformidades.html" class="nav-subitem nav-subitem--tool">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/><path d="M14 2v4h4" stroke-linejoin="round"/></svg>
Não Conformidades
</a>
</li>
</ul>
</li>
<li class="nav-subgroup" data-subgroup="contabilidade">
<button type="button" class="nav-subitem nav-subgroup__toggle" data-subgroup-toggle>
<span>Contabilidade</span>
<svg class="nav-item__chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-subgroup__list">
<li data-app="dashboard-contabil">
<a href="dashboard-contabil.html" class="nav-subitem nav-subitem--tool">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/><path d="M14 2v4h4" stroke-linejoin="round"/></svg>
Relatório Contábil
</a>
</li>
</ul>
</li>
</ul>
</li>
<li data-section="diretoria" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M3 13h18" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Diretoria</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="dashboard-contabil-de-paula"><a href="dashboard-contabil-de-paula.html" class="nav-subitem is-active">Relatório Contábil - De Paula</a></li>
</ul>
</li>
<li data-section="relatorios-gerenciais" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Relatórios Gerenciais</span>
<span class="badge-restricted" data-restricted-badge hidden>Restrito</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="visao-diretoria"><a href="#" class="nav-subitem">Visão Diretoria</a></li>
<li data-app="visao-gerencial"><a href="#" class="nav-subitem">Visão Gerencial</a></li>
</ul>
</li>
<li data-section="utilitarios" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 7l3 3-8 8-4 1 1-4z" stroke-linejoin="round"/><path d="M12 9l3 3" stroke-linecap="round"/></svg>
<span class="nav-item__label">Utilitários</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li>
<li data-app="importacao-plano-saude-de-paula"><a href="importacao-plano-saude-de-paula.html" class="nav-subitem">Importação de Plano de Saúde - De Paula</a></li>
<li data-app="conciliacao-fornecedores"><a href="conciliacao-fornecedores.html" class="nav-subitem">Conciliação de Fornecedores</a></li>
</ul>
</li>
<li data-section="integracoes" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="6" cy="12" r="3"/><circle cx="18" cy="6" r="3"/><circle cx="18" cy="18" r="3"/><path d="M8.6 10.6L15.4 7.4M8.6 13.4l6.8 3.2" stroke-linecap="round"/></svg>
<span class="nav-item__label">Integrações</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="questor"><a href="#" class="nav-subitem">Questor</a></li>
</ul>
</li>
<li data-section="auditorias" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l7 4v5c0 5-3.5 8-7 9-3.5-1-7-4-7-9V7z" stroke-linejoin="round"/><path d="M9 12l2 2 4-4" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Auditorias</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li class="nav-subgroup" data-subgroup="consultoria-tributaria">
<button type="button" class="nav-subitem nav-subgroup__toggle" data-subgroup-toggle>
<span>Consultoria Tributária</span>
<svg class="nav-item__chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-subgroup__list">
<li data-app="controle-simples-nacional">
<a href="#" class="nav-subitem nav-subitem--tool">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/><path d="M14 2v4h4" stroke-linejoin="round"/></svg>
Controle Simples Nacional
</a>
</li>
</ul>
</li>
<li class="nav-subgroup" data-subgroup="fisco-contabil">
<button type="button" class="nav-subitem nav-subgroup__toggle" data-subgroup-toggle>
<span>Fisco/Contábil</span>
<svg class="nav-item__chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-subgroup__list">
<li data-app="auditoria-de-balancetes">
<a href="#" class="nav-subitem nav-subitem--tool">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/><path d="M14 2v4h4" stroke-linejoin="round"/></svg>
Auditoria de Balancetes
</a>
</li>
</ul>
</li>
</ul>
</li>
<li data-section="links-ferramentas" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1"/><path d="M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"/></svg>
<span class="nav-item__label">Links &amp; Ferramentas</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="links-ferramentas-visualizar"><a href="links-ferramentas.html" class="nav-subitem">Links &amp; Ferramentas</a></li>
<li data-app="acessos-gerais-visualizar"><a href="acessos-gerais.html" class="nav-subitem">Acessos Gerais</a></li>
</ul>
</li>
<li data-section="solicitacoes" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 10h.01M12 10h.01M16 10h.01M21 12a9 9 0 1 1-4.4-7.7L21 3l-1.2 4.5A8.96 8.96 0 0 1 21 12z"/></svg>
<span class="nav-item__label">Solicitações</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="controle-notificacoes"><a href="https://form.asana.com/?k=PcELrDLzN969CwpiRBLtFA&amp;d=1207308016523218" target="_blank" rel="noopener noreferrer" class="nav-subitem">Controle de Notificações</a></li>
<li data-app="ordem-servico"><a href="#" class="nav-subitem">Ordem de Serviço</a></li>
<li data-app="pimi"><a href="https://form.asana.com/?k=XfBww25lGUyPVrInNcLyuQ&amp;d=1207308016523218" target="_blank" rel="noopener noreferrer" class="nav-subitem">P.I.M.I.</a></li>
<li data-app="programa-indicacoes"><a href="https://form.asana.com/?k=y0mTeaRTOAoC0r0XP8ee0Q&amp;d=1207308016523218" target="_blank" rel="noopener noreferrer" class="nav-subitem">Programa de Indicações</a></li>
<li data-app="pos-vendas"><a href="https://form.asana.com/?k=jRAztLip-_BknWPsGmEcZA&amp;d=1207308016523218" target="_blank" rel="noopener noreferrer" class="nav-subitem">Pós-Vendas</a></li>
<li data-app="sugestao-inovacao"><a href="https://form.asana.com/?k=Zn8cLeT97R7FTUrsNB8Yaw&amp;d=1207308016523218" target="_blank" rel="noopener noreferrer" class="nav-subitem">Sugestão de Inovação</a></li>
</ul>
</li>
<li data-section="administracao" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1-1.6 1.7 1.7 0 0 0-1.9.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.9 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.9l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.9.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.9-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.9V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Administração</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-section="perfis-acesso"><a href="perfis-acesso.html" class="nav-subitem">Perfis de Acesso</a></li>
<li data-section="usuarios"><a href="usuarios.html" class="nav-subitem">Usuários</a></li>
</ul>
</li>
</ul>
</nav>
</aside>
<div class="main-content">
<header class="topbar">
<button type="button" class="icon-btn" id="sidebar-toggle" aria-label="Alternar menu">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h16" stroke-linecap="round"/></svg>
</button>
<h1 class="topbar__title">Relatório Contábil - De Paula</h1>
<div class="topbar__actions">
<button type="button" class="icon-btn" id="theme-toggle" aria-label="Ativar modo claro">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" stroke-linecap="round"/></svg>
</button>
<div class="notifications">
<button type="button" class="icon-btn" id="notif-bell" aria-label="Notificações">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 8a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6" stroke-linejoin="round"/><path d="M9.5 20a2.5 2.5 0 0 0 5 0"/></svg>
<span class="notif-badge" id="notif-badge" hidden>0</span>
</button>
<div class="notif-dropdown" id="notif-dropdown" hidden>
<div class="notif-dropdown__header">
<span>Notificações</span>
<div class="notif-dropdown__header-actions">
<button type="button" id="notif-history-btn">Histórico</button>
<button type="button" id="notif-clear-all">Limpar tudo</button>
</div>
</div>
<ul class="notif-list" id="notif-list"></ul>
</div>
</div>
<div class="account-menu">
<button type="button" class="persona-chip" id="persona-chip">
<span class="persona-chip__icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 20v-1a6 6 0 0 1 6-6h4a6 6 0 0 1 6 6v1" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="7" r="4"/></svg>
</span>
<span id="persona-chip-label">Usuário</span>
</button>
<div class="account-dropdown" id="account-dropdown" hidden>
<div class="account-dropdown__header">
<p class="account-dropdown__name" id="account-name"></p>
<p class="account-dropdown__profile" id="account-profile"></p>
</div>
<button type="button" class="account-dropdown__item" id="account-manage-account-btn">Gerenciar Usuário</button>
<a href="perfis-acesso.html" class="account-dropdown__item" id="account-manage-link" hidden>Gerenciar Perfis de Acesso</a>
<div class="account-dropdown__section">
<p class="account-dropdown__section-title">Tema de cores</p>
<div class="theme-swatches" id="theme-swatches"></div>
</div>
<div class="account-dropdown__section">
<p class="account-dropdown__section-title">Temas sazonais</p>
<button type="button" class="seasonal-theme-toggle" id="seasonal-theme-toggle" aria-pressed="true">
<span class="seasonal-theme-toggle__dot"></span>
<span id="seasonal-theme-toggle-label">Ativo</span>
</button>
</div>
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
</div>
</div>
</div>
</header>
<main class="page-content page-content--wide">
<div id="dc-no-access" class="no-access" hidden>
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><rect x="4" y="10" width="16" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3" stroke-linecap="round" stroke-linejoin="round"/></svg>
<h2>Sem acesso ao Relatório Contábil - De Paula</h2>
<p>Seu perfil de acesso não tem permissão para visualizar esta tela. Solicite à Inovação a liberação em Perfis de Acesso.</p>
</div>
<div id="dc-content">
<div id="dc-list-view">
<div class="dc-header">
<div>
<h2>Relatório Contábil - De Paula</h2>
<p class="dc-subtitle">Anexe o balancete + DRE em PDF (modelo Questor ou Contabit) de uma empresa do próprio escritório — a ferramenta extrai as contas, roda um conjunto de checagens automáticas e apresenta as observações para análise antes da conclusão. No modelo Contabit, o nome do arquivo precisa começar pelo código da empresa (ex.: "1512 - Balancete 082026.pdf").</p>
</div>
<button type="button" class="btn-solid" id="dc-new-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14" stroke-linecap="round"/></svg>
Nova Análise
</button>
</div>
<div class="pa-table-wrap dc-list-table-wrap">
<table class="pa-table" id="dc-list-table">
<thead>
<tr>
<th data-sort="empresa">Empresa <span class="dc-sort-icon">↕</span></th>
<th data-sort="competencia">Competência <span class="dc-sort-icon">↕</span></th>
<th data-sort="observacoes">Observações pendentes <span class="dc-sort-icon">↕</span></th>
<th data-sort="status">Status <span class="dc-sort-icon">↕</span></th>
<th data-sort="criado_por">Criado por <span class="dc-sort-icon">↕</span></th>
<th data-sort="criado_em">Data <span class="dc-sort-icon">↕</span></th>
<th>
<button type="button" class="dc-list-reset-btn" id="dc-list-reset-btn" data-dc-dica="Limpar filtros e ordenação" aria-label="Limpar filtros e ordenação">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 20H8l-5.5-5.5a2 2 0 0 1 0-2.8l9-9a2 2 0 0 1 2.8 0l6.2 6.2a2 2 0 0 1 0 2.8L13 19" stroke-linejoin="round"/><path d="M8.5 12.5l6 6" stroke-linecap="round"/></svg>
</button>
</th>
</tr>
</thead>
<tbody id="dc-list-body"></tbody>
</table>
</div>
<p class="dc-empty" id="dc-list-empty" hidden>Nenhuma análise realizada ainda.</p>
<div class="dc-list-pagination" id="dc-list-pagination"></div>
<p class="dc-list-count" id="dc-list-count"></p>
</div>
<div id="dc-form-view" hidden>
<div class="dc-header">
<div>
<h2>Nova Análise</h2>
<p class="dc-subtitle">Anexe o PDF de Balancete + DRE (mesmo relatório hoje enviado ao cliente) — empresa, CNPJ e competência são identificados automaticamente a partir do próprio arquivo.</p>
</div>
<button type="button" class="btn-outline" id="dc-form-back-btn">Voltar</button>
</div>
<div class="dc-form">
<div class="modal-field">
<label>Balancete + DRE (PDF)</label>
<div class="dc-file-field">
<label class="btn-outline dc-file-field__btn" for="dc-form-arquivo">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 16V4M12 4l4 4M12 4L8 8" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 16v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3" stroke-linecap="round" stroke-linejoin="round"/></svg>
Selecionar arquivo
</label>
<input type="file" id="dc-form-arquivo" accept=".pdf" class="dc-file-field__input" />
<span class="dc-file-field__name" id="dc-form-arquivo-name">Nenhum arquivo selecionado</span>
<button type="button" class="icon-btn dc-file-field__remove" id="dc-form-arquivo-remove" aria-label="Remover arquivo" data-dc-dica="Remover arquivo" hidden>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>
</button>
</div>
</div>
<p class="modal-error" id="dc-form-error"></p>
<div class="dc-progress" id="dc-form-progress" hidden>
<div class="dc-progress__bar"><div class="dc-progress__fill"></div></div>
<p class="dc-progress__label">Processando o arquivo, aguarde…</p>
</div>
<div class="modal-actions">
<button type="button" class="btn-outline" id="dc-form-cancel-btn">Cancelar</button>
<button type="button" class="btn-solid" id="dc-form-submit-btn">Processar</button>
</div>
</div>
</div>
<div id="dc-review-view" hidden>
<div class="dc-header">
<div>
<h2>Revisão <span class="dc-review-empresa" id="dc-review-empresa"></span></h2>
<p class="dc-subtitle" id="dc-review-subtitle"></p>
</div>
<div class="dc-header-actions">
<button type="button" class="btn-outline" id="dc-review-back-btn">Voltar</button>
<button type="button" class="btn-outline" id="dc-gerar-dashboard-btn">Gerar Relatório</button>
<button type="button" class="btn-solid" id="dc-concluir-btn">Concluir Análise</button>
</div>
</div>
<p class="dc-concluida-aviso" id="dc-concluida-aviso" hidden>Esta análise já foi concluída — as observações não podem mais ser editadas.</p>
<div class="pa-tabs" id="dc-tabs">
<button type="button" class="pa-tab is-active" data-dc-tab="achados">Observações</button>
<button type="button" class="pa-tab" data-dc-tab="balancete">Balancete</button>
<button type="button" class="pa-tab" data-dc-tab="dre">DRE</button>
<button type="button" class="pa-tab" data-dc-tab="analise-vertical" id="dc-av-tab-btn" hidden>Análise Vertical</button>
<button type="button" class="pa-tab" data-dc-tab="dashboard">Dashboard</button>
</div>
<div class="pa-tab-panel" data-dc-panel="achados">
<div class="dc-achados-resumo">
<div class="dc-achados-resumo__chart">
<div class="dc-donut" id="dc-achados-donut">
<svg class="dc-donut-svg" id="dc-achados-donut-svg" viewBox="0 0 36 36"></svg>
<div class="dc-donut__hole">
<span class="dc-donut__total" id="dc-achados-donut-total">0</span>
<span class="dc-donut__label">observações</span>
</div>
</div>
<ul class="dc-donut-legend" id="dc-achados-donut-legend"></ul>
</div>
<div class="dc-achados-categorias" id="dc-achados-categorias"></div>
</div>
<div class="dc-achados-toolbar">
<div class="dc-chip-row" id="dc-achados-severidade-chips">
<button type="button" class="dc-chip is-active" data-dc-severidade="todas">Todas</button>
<button type="button" class="dc-chip" data-dc-severidade="alta">Alta</button>
<button type="button" class="dc-chip" data-dc-severidade="media">Média</button>
<button type="button" class="dc-chip" data-dc-severidade="baixa">Baixa</button>
</div>
<div class="dc-chip-row" id="dc-achados-status-chips">
<button type="button" class="dc-chip is-active" data-dc-status="pendente">Pendentes</button>
<button type="button" class="dc-chip" data-dc-status="todos">Todos</button>
</div>
</div>
<div class="dc-regra-filtro-ativo" id="dc-regra-filtro-ativo" hidden>
Filtrando por categoria: <strong id="dc-regra-filtro-ativo-label"></strong>
<button type="button" class="dc-regra-filtro-limpar" id="dc-regra-filtro-limpar-btn">Limpar</button>
</div>
<div class="dc-achados-list" id="dc-achados-list"></div>
<p class="dc-empty" id="dc-achados-empty" hidden>Nenhuma observação para este filtro.</p>
<button type="button" class="dc-av-aviso" id="dc-av-aviso" hidden>
<span class="dc-av-aviso__texto" id="dc-av-aviso-texto"></span>
<span class="dc-av-aviso__link">Ver na Análise Vertical &rarr;</span>
</button>
</div>
<div class="pa-tab-panel" data-dc-panel="balancete" hidden>
<div class="pa-table-wrap">
<table class="pa-table dc-contas-table">
<thead id="dc-contas-thead">
<tr>
<th>
<span class="dc-th-linha dc-th-linha--inicio">
<button type="button" class="icon-btn dc-recolher-grupos-btn" data-dc-recolher-grupos="balancete" aria-label="Recolher os grupos do Balancete" data-dc-dica="Recolher os grupos (mostrar só os totais de cada nível)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/></svg>
</button>
Conta
</span>
</th>
<th>Classificação</th>
<th>Descrição</th>
<th>Saldo Anterior</th>
<th>Débito</th>
<th>Crédito</th>
<th>Saldo Atual</th>
<th>
<span class="dc-th-linha">Observação
<button type="button" class="icon-btn dc-reset-formatacao-btn" data-dc-reset-formatacao="balancete" aria-label="Restaurar formatação padrão do Balancete" data-dc-dica="Restaurar formatação padrão (tudo expandido, como ao abrir)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 2.64-6.36"/><path d="M3 3v6h6"/></svg>
</button>
</span>
</th>
</tr>
</thead>
<tbody id="dc-contas-body"></tbody>
</table>
</div>
<section class="dc-obs-resumo">
<h3 class="dc-obs-resumo__titulo">Observações do Balancete <span class="dc-obs-resumo__count" id="dc-contas-obs-count">0</span></h3>
<div class="dc-chip-row dc-obs-filtros" data-dc-obs-filtros>
<button type="button" class="dc-chip is-active" data-dc-obs-filtro="todas">Todas</button>
<button type="button" class="dc-chip" data-dc-obs-filtro="cliente">Visíveis ao cliente</button>
<button type="button" class="dc-chip" data-dc-obs-filtro="internas">Internas</button>
</div>
<div class="dc-dash-obs-list" id="dc-contas-obs-list"></div>
<p class="dc-empty" id="dc-contas-obs-empty" hidden>Nenhuma observação registrada no Balancete ainda.</p>
</section>
</div>
<div class="pa-tab-panel" data-dc-panel="dre" hidden>
<div class="pa-table-wrap">
<table class="pa-table dc-dre-table">
<thead id="dc-dre-thead">
<tr>
<th>
<span class="dc-th-linha dc-th-linha--inicio">
<button type="button" class="icon-btn dc-recolher-grupos-btn" data-dc-recolher-grupos="dre" aria-label="Recolher os grupos da DRE" data-dc-dica="Recolher os grupos (mostrar só os totais de cada nível)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/></svg>
</button>
Descrição
</span>
</th>
<th>Valor</th>
<th>
<span class="dc-th-linha">Observação
<button type="button" class="icon-btn dc-reset-formatacao-btn" data-dc-reset-formatacao="dre" aria-label="Restaurar formatação padrão da DRE" data-dc-dica="Restaurar formatação padrão (tudo expandido, como ao abrir)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 2.64-6.36"/><path d="M3 3v6h6"/></svg>
</button>
</span>
</th>
</tr>
</thead>
<tbody id="dc-dre-body"></tbody>
</table>
</div>
<section class="dc-obs-resumo">
<h3 class="dc-obs-resumo__titulo">Observações da DRE <span class="dc-obs-resumo__count" id="dc-dre-obs-count">0</span></h3>
<div class="dc-chip-row dc-obs-filtros" data-dc-obs-filtros>
<button type="button" class="dc-chip is-active" data-dc-obs-filtro="todas">Todas</button>
<button type="button" class="dc-chip" data-dc-obs-filtro="cliente">Visíveis ao cliente</button>
<button type="button" class="dc-chip" data-dc-obs-filtro="internas">Internas</button>
</div>
<div class="dc-dash-obs-list" id="dc-dre-obs-list"></div>
<p class="dc-empty" id="dc-dre-obs-empty" hidden>Nenhuma observação registrada na DRE ainda.</p>
</section>
</div>
<div class="pa-tab-panel" data-dc-panel="analise-vertical" hidden>
<div class="pa-table-wrap">
<table class="pa-table dc-dre-table dc-av-table">
<thead id="dc-av-thead">
<tr id="dc-av-head-row">
<th>Descrição</th>
</tr>
</thead>
<tbody id="dc-av-body"></tbody>
</table>
</div>
<section class="dc-obs-resumo">
<h3 class="dc-obs-resumo__titulo">Observações da Análise Vertical <span class="dc-obs-resumo__count" id="dc-av-obs-count">0</span></h3>
<div class="dc-chip-row dc-obs-filtros" data-dc-obs-filtros>
<button type="button" class="dc-chip is-active" data-dc-obs-filtro="todas">Todas</button>
<button type="button" class="dc-chip" data-dc-obs-filtro="cliente">Visíveis ao cliente</button>
<button type="button" class="dc-chip" data-dc-obs-filtro="internas">Internas</button>
</div>
<div class="dc-dash-obs-list" id="dc-av-obs-list"></div>
<p class="dc-empty" id="dc-av-obs-empty" hidden>Nenhuma observação registrada na Análise Vertical ainda.</p>
</section>
<section class="dc-obs-resumo dc-variacoes" id="dc-av-variacoes">
<h3 class="dc-obs-resumo__titulo">Variação atípica na DRE - Análise Vertical <span class="dc-obs-resumo__count" id="dc-av-variacoes-count">0</span></h3>
<p class="dc-variacoes__dica">Confira cada variação e clique em "Validar". A observação escrita aqui é a mesma da linha na tabela: fica no histórico da empresa e pode ser mostrada ao cliente.</p>
<div class="dc-variacoes__lista" id="dc-av-variacoes-list"></div>
<p class="dc-empty" id="dc-av-variacoes-empty" hidden>Nenhuma variação atípica nesta análise.</p>
</section>
</div>
<div class="pa-tab-panel" data-dc-panel="dashboard" hidden>
<p class="dc-dash-intro">
Pré-visualização do que vai para o relatório gerado pelo botão "Gerar Relatório". Use o botão de olho pra ocultar um card de indicador ou uma observação — o que estiver oculto aqui não aparece no relatório.
</p>
<section class="dc-dash-section">
<div class="dc-dash-section__header">
<h3 class="dc-dash-section__titulo">Resumo</h3>
<button type="button" class="btn-outline" id="dc-indicadores-gerenciar-btn">Gerenciar Indicadores</button>
</div>
<div class="dc-dash-resumo-fechamento">
<h4 class="dc-dash-resumo-fechamento__titulo">Resumo do Fechamento</h4>
<p class="dc-dash-resumo-fechamento__dica">Escreva aqui as considerações e análises deste fechamento — cole (Ctrl+V) ou arraste uma imagem pra incluí-la no texto. Aparece no relatório "Gerar Relatório", antes dos indicadores.</p>
<div id="dc-dash-resumo-fechamento-editor" class="dc-dash-richtext" contenteditable="true" data-placeholder="Ex.: A escrituração contábil da empresa foi atualizada até …/…/…. Considerações: o resultado do exercício foi…"></div>
<div class="dc-dash-resumo-fechamento__acoes">
<button type="button" class="btn-solid" id="dc-dash-resumo-fechamento-salvar-btn">Salvar</button>
<span class="dc-dash-resumo-fechamento__status" id="dc-dash-resumo-fechamento-status" hidden></span>
</div>
</div>
<p class="dc-dash-section__dica" id="dc-dash-indicadores-dica">Clique num card pra ver a fórmula usada.</p>
<div id="dc-dash-indicadores"></div>
</section>
<section class="dc-dash-section">
<h3 class="dc-dash-section__titulo">Observações</h3>
<div class="dc-chip-row dc-obs-filtros" data-dc-obs-filtros>
<button type="button" class="dc-chip is-active" data-dc-obs-filtro="todas">Todas</button>
<button type="button" class="dc-chip" data-dc-obs-filtro="cliente">Visíveis ao cliente</button>
<button type="button" class="dc-chip" data-dc-obs-filtro="internas">Internas</button>
</div>
<div class="dc-dash-obs-list" id="dc-dash-observacoes-list"></div>
<p class="dc-empty" id="dc-dash-observacoes-empty" hidden>Nenhuma observação registrada ainda no Balancete, na DRE ou na auditoria.</p>
</section>
</div>
</div>
</div>
</main>
</div>
</div>
<div class="modal-overlay" id="dc-reprocessar-modal" hidden>
<div class="modal-card">
<h2 class="modal-card__title">Reprocessar análise</h2>
<p class="modal-card__subtitle">Anexe um PDF novo pra mesma empresa/competência (ex.: o arquivo original estava incompleto ou com erro). Contas/linhas sem mudança mantêm observação e validação como estavam; o que mudou volta pra "não validado" e ganha um alerta.</p>
<div class="modal-field">
<label>Balancete + DRE (PDF)</label>
<div class="dc-file-field">
<label class="btn-outline dc-file-field__btn" for="dc-reprocessar-arquivo">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 16V4M12 4l4 4M12 4L8 8" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 16v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3" stroke-linecap="round" stroke-linejoin="round"/></svg>
Selecionar arquivo
</label>
<input type="file" id="dc-reprocessar-arquivo" accept=".pdf" class="dc-file-field__input" />
<span class="dc-file-field__name" id="dc-reprocessar-arquivo-name">Nenhum arquivo selecionado</span>
<button type="button" class="icon-btn dc-file-field__remove" id="dc-reprocessar-arquivo-remove" aria-label="Remover arquivo" data-dc-dica="Remover arquivo" hidden>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>
</button>
</div>
</div>
<section class="dc-reprocessar-log" id="dc-reprocessar-log" hidden>
<h3 class="dc-reprocessar-log__titulo">Reprocessamentos anteriores <span class="dc-reprocessar-log__count" id="dc-reprocessar-log-count">0</span></h3>
<ul class="dc-reprocessar-log__lista" id="dc-reprocessar-log-lista"></ul>
</section>
<p class="modal-error" id="dc-reprocessar-error"></p>
<div class="dc-progress" id="dc-reprocessar-progress" hidden>
<div class="dc-progress__bar"><div class="dc-progress__fill"></div></div>
<p class="dc-progress__label">Processando o arquivo, aguarde…</p>
</div>
<div class="modal-actions">
<button type="button" class="btn-outline" id="dc-reprocessar-cancel-btn">Cancelar</button>
<button type="button" class="btn-solid" id="dc-reprocessar-confirmar-btn">Reprocessar</button>
</div>
</div>
</div>
<div class="modal-overlay" id="dc-achado-modal" hidden>
<div class="modal-card">
<h2 class="modal-card__title" id="dc-achado-modal-titulo"></h2>
<p class="modal-card__subtitle" id="dc-achado-modal-mensagem"></p>
<div class="modal-field">
<label for="dc-achado-modal-texto">Observação (obrigatória)</label>
<textarea id="dc-achado-modal-texto" rows="5" placeholder="Justifique o tratamento desta observação…"></textarea>
</div>
<p class="modal-error" id="dc-achado-modal-error"></p>
<div class="modal-actions">
<button type="button" class="btn-outline" id="dc-achado-modal-cancel-btn">Cancelar</button>
<button type="button" class="btn-danger-outline" id="dc-achado-modal-ignorar-btn">Ignorar</button>
<button type="button" class="btn-solid" id="dc-achado-modal-tratar-btn">Marcar como tratado</button>
</div>
</div>
</div>
<div class="modal-overlay" id="dc-obs-historico-modal" hidden>
<div class="modal-card">
<h2 class="modal-card__title">Histórico de edições</h2>
<p class="modal-card__subtitle">Toda mudança de texto desta observação, da mais recente para a mais antiga.</p>
<div class="dc-obs-historico-lista" id="dc-obs-historico-lista"></div>
<div class="modal-actions">
<button type="button" class="btn-solid" id="dc-obs-historico-modal-fechar-btn">Fechar</button>
</div>
</div>
</div>
<div class="modal-overlay" id="dc-indicador-formula-modal" hidden>
<div class="modal-card">
<h2 class="modal-card__title" id="dc-indicador-formula-modal-titulo"></h2>
<p class="modal-card__subtitle" id="dc-indicador-formula-modal-descricao"></p>
<div class="dc-ind-formula-box">
<span class="dc-ind-formula-box__label">Fórmula</span>
<p id="dc-indicador-formula-modal-formula"></p>
</div>
<div class="modal-actions">
<button type="button" class="btn-solid" id="dc-indicador-formula-modal-fechar-btn">Fechar</button>
</div>
</div>
</div>
<div class="modal-overlay" id="dc-indicadores-hub-modal" hidden>
<div class="modal-card modal-card--wide">
<h2 class="modal-card__title">Gerenciar Indicadores</h2>
<p class="modal-card__subtitle">Indicadores padrão aparecem automaticamente em toda apuração. Indicadores não padrão ficam salvos aqui e só entram nesta apuração quando marcados.</p>
<div class="dc-ind-hub-secao">
<h3 class="dc-ind-hub-secao__titulo">Padrão</h3>
<div class="dc-ind-hub-lista" id="dc-ind-hub-lista-padrao"></div>
<p class="dc-empty" id="dc-ind-hub-vazio-padrao" hidden>Nenhum indicador padrão personalizado cadastrado.</p>
</div>
<div class="dc-ind-hub-secao">
<h3 class="dc-ind-hub-secao__titulo">Não padrão</h3>
<div class="dc-ind-hub-lista" id="dc-ind-hub-lista-nao-padrao"></div>
<p class="dc-empty" id="dc-ind-hub-vazio-nao-padrao" hidden>Nenhum indicador não padrão cadastrado.</p>
</div>
<div class="modal-actions">
<button type="button" class="btn-outline" id="dc-indicadores-hub-fechar-btn">Fechar</button>
<button type="button" class="btn-solid" id="dc-indicadores-hub-novo-btn">+ Novo Indicador</button>
</div>
</div>
</div>
<div class="modal-overlay modal-overlay--top" id="dc-indicador-modal" hidden>
<div class="modal-card modal-card--wide">
<h2 class="modal-card__title" id="dc-indicador-modal-titulo">Novo Indicador</h2>
<p class="modal-card__subtitle">Monte a fórmula a partir de contas do Balancete, linhas da DRE, variação entre apurações ou outros indicadores já existentes.</p>
<div class="modal-field">
<label for="dc-indicador-modal-nome">Nome</label>
<input type="text" id="dc-indicador-modal-nome" name="indicador_nome" autocomplete="off" placeholder="ex.: Margem Líquida…" />
</div>
<div class="modal-field-row">
<div class="modal-field">
<label for="dc-indicador-modal-descricao">Descrição</label>
<textarea id="dc-indicador-modal-descricao" rows="3" placeholder="O que este indicador mede, como interpretar o resultado…"></textarea>
</div>
<div class="modal-field">
<label for="dc-indicador-modal-formato">Formato de exibição</label>
<select id="dc-indicador-modal-formato">
<option value="indice">Índice</option>
<option value="percentual">Percentual (%)</option>
<option value="moeda">Moeda (R$)</option>
</select>
</div>
</div>
<label class="modal-checkbox">
<input type="checkbox" id="dc-indicador-modal-padrao" checked />
Indicador padrão (aparece automaticamente em todas as apurações — desmarque pra deixar disponível só quando selecionado em "Gerenciar Indicadores")
</label>
<div class="modal-field">
<label>Ícone (aparece no card do relatório "Gerar Relatório")</label>
<div class="dc-ind-icone-grid" id="dc-indicador-icones"></div>
</div>
<div class="dc-ind-componentes-header">
<span>Componentes</span>
<button type="button" class="btn-outline" id="dc-indicador-add-componente-btn">+ Adicionar componente</button>
</div>
<div id="dc-indicador-componentes" class="dc-ind-componentes-lista"></div>
<div class="modal-field">
<label for="dc-indicador-modal-formula">Fórmula (cálculo interno)</label>
<input type="text" id="dc-indicador-modal-formula" name="indicador_formula" autocomplete="off" placeholder="ex.: (receita - custo) / receita…" />
<p class="dc-ind-formula-hint" id="dc-indicador-modal-formula-hint">Use a chave de cada componente acima, combinadas com +, -, * , / e parênteses.</p>
</div>
<div class="modal-field">
<label for="dc-indicador-modal-formula-exibicao">Fórmula (como aparece ao cliente)</label>
<input type="text" id="dc-indicador-modal-formula-exibicao" name="indicador_formula_exibicao" autocomplete="off" placeholder="ex.: Lucro Líquido − Despesas Financeiras…" />
<p class="dc-ind-formula-hint">Texto livre, só pra exibição no relatório "Gerar Relatório" — não precisa (e não deve) usar as chaves técnicas dos componentes. Se deixar em branco, o relatório mostra a fórmula de cálculo interno acima.</p>
</div>
<button type="button" class="btn-outline" id="dc-indicador-calcular-btn">Calcular com esta apuração</button>
<div class="dc-ind-preview" id="dc-indicador-preview" hidden></div>
<p class="modal-error" id="dc-indicador-modal-error"></p>
<div class="modal-actions">
<button type="button" class="btn-danger-outline" id="dc-indicador-modal-excluir-btn" hidden>Excluir</button>
<button type="button" class="btn-outline" id="dc-indicador-modal-cancel-btn">Cancelar</button>
<button type="button" class="btn-solid" id="dc-indicador-modal-save-btn">Salvar</button>
</div>
</div>
</div>
<div class="modal-overlay" id="manage-account-modal" hidden>
<div class="modal-card" id="manage-account-modal-card">
<h2 class="modal-card__title">Gerenciar Usuário</h2>
<div class="modal-field">
<button type="button" class="btn-outline" id="manage-account-change-password-btn">Alterar senha</button>
</div>
<div class="modal-field" id="manage-account-liderados-field" hidden>
<label>Usuários sob sua liderança</label>
<div class="dual-select">
<div class="dual-select__panel">
<div class="dual-select__panel-header">
<button type="button" class="btn-solid" id="manage-account-liderados-available-link-btn" disabled>Vincular</button>
</div>
<div class="dual-select__table-wrap">
<table class="dual-select__table">
<thead>
<tr>
<th><input type="checkbox" id="manage-account-liderados-available-select-all" aria-label="Selecionar todos" /></th>
<th class="dual-select__sort" id="manage-account-liderados-available-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
<th>Departamento</th>
</tr>
<tr class="dual-select__filters">
<th></th>
<th><input type="text" id="manage-account-liderados-available-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
<th><input type="text" id="manage-account-liderados-available-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
</tr>
</thead>
<tbody id="manage-account-liderados-available-body"></tbody>
</table>
</div>
<div class="dual-select__footer" id="manage-account-liderados-available-footer"></div>
</div>
<div class="dual-select__panel">
<div class="dual-select__panel-header">
<button type="button" class="btn-danger-outline" id="manage-account-liderados-linked-unlink-btn" disabled>Desvincular</button>
</div>
<div class="dual-select__table-wrap">
<table class="dual-select__table">
<thead>
<tr>
<th><input type="checkbox" id="manage-account-liderados-linked-select-all" aria-label="Selecionar todos" /></th>
<th class="dual-select__sort" id="manage-account-liderados-linked-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
<th>Departamento</th>
</tr>
<tr class="dual-select__filters">
<th></th>
<th><input type="text" id="manage-account-liderados-linked-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
<th><input type="text" id="manage-account-liderados-linked-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
</tr>
</thead>
<tbody id="manage-account-liderados-linked-body"></tbody>
</table>
</div>
<div class="dual-select__footer" id="manage-account-liderados-linked-footer"></div>
</div>
</div>
<p class="modal-error" id="manage-account-liderados-error"></p>
</div>
<div class="modal-actions">
<button type="button" class="btn-outline" id="manage-account-close-btn">Fechar</button>
<button type="button" class="btn-solid" id="manage-account-save-btn" hidden>Salvar liderados</button>
</div>
</div>
</div>
<div class="modal-overlay" id="password-modal" hidden>
<div class="modal-card">
<h2 class="modal-card__title">Alterar senha</h2>
<div class="modal-field">
<label for="pwd-current">Senha atual</label>
<div class="password-field">
<input type="password" id="pwd-current" autocomplete="current-password" />
<button type="button" class="password-field__toggle" id="pwd-current-toggle" aria-label="Mostrar senha"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7Z" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="3"/></svg></button>
</div>
</div>
<div class="modal-field">
<label for="pwd-new">Nova senha</label>
<div class="password-field">
<input type="password" id="pwd-new" autocomplete="new-password" />
<button type="button" class="password-field__toggle" id="pwd-new-toggle" aria-label="Mostrar senha"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7Z" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="3"/></svg></button>
</div>
</div>
<div class="modal-field">
<label for="pwd-confirm">Confirmar nova senha</label>
<div class="password-field">
<input type="password" id="pwd-confirm" autocomplete="new-password" />
<button type="button" class="password-field__toggle" id="pwd-confirm-toggle" aria-label="Mostrar senha"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7Z" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="3"/></svg></button>
</div>
</div>
<p class="modal-error" id="pwd-error"></p>
<div class="modal-actions">
<button type="button" class="btn-outline" id="pwd-cancel-btn">Cancelar</button>
<button type="button" class="btn-solid" id="pwd-save-btn">Salvar</button>
</div>
</div>
</div>
<div class="modal-overlay" id="notif-history-modal" hidden>
<div class="modal-card">
<h2 class="modal-card__title">Histórico de Notificações</h2>
<ul class="notif-list" id="notif-history-list"></ul>
<div class="modal-actions">
<button type="button" class="btn-outline" id="notif-history-close-btn">Fechar</button>
</div>
</div>
</div>
<button type="button" class="dc-back-to-top-btn" id="dc-back-to-top-btn" aria-label="Voltar ao início da página" data-dc-dica="Voltar ao início da página" hidden>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5"/><path d="M5 12l7-7 7 7"/></svg>
</button>
<div class="modal-overlay" id="calendario-depaula-modal" hidden>
<div class="modal-card iframe-modal-card">
<div class="iframe-modal-header">
<h2 class="modal-card__title">Calendário De Paula</h2>
<button type="button" class="icon-btn" id="calendario-depaula-close-btn" aria-label="Fechar">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>
</button>
</div>
<iframe id="calendario-depaula-iframe" class="iframe-modal-iframe" title="Calendário De Paula"></iframe>
</div>
</div>
<script src="{% static 'js/api.js' %}"></script>
<script src="{% static 'js/confirm-modal.js' %}"></script>
<script src="{% static 'js/seasonal-theme.js' %}"></script>
<script src="{% static 'js/sidebar-brand.js' %}"></script>
<script src="{% static 'js/dual-select.js' %}"></script>
<script src="{% static 'js/profiles.js' %}"></script>
<script src="{% static 'js/auth.js' %}"></script>
<script src="{% static 'js/access.js' %}"></script>
<script src="{% static 'js/account.js' %}"></script>
<script src="{% static 'js/favorites.js' %}"></script>
<script src="{% static 'js/events.js' %}"></script>
<script src="{% static 'js/sidebar.js' %}"></script>
<script src="{% static 'js/notifications.js' %}"></script>
<script>
// Mesma tela do Relatório Contábil, apontada para as tabelas e a
// permissão da De Paula (ver PID_DC_CONFIG em dashboard-contabil.js).
window.PID_DC_CONFIG = {
moduleKey: "diretoria",
appKey: "dashboard-contabil-de-paula",
endpointApuracoes: "/contabil-apuracoes-de-paula",
endpointContas: "/contabil-contas-de-paula",
endpointLinhasDre: "/contabil-linhas-dre-de-paula",
endpointLinhasAnaliseVertical: "/contabil-linhas-analise-vertical-de-paula",
endpointObservacoes: "/contabil-observacoes-de-paula",
endpointAchados: "/contabil-achados-de-paula",
};
</script>
<script src="{% static 'js/dashboard-contabil.js' %}"></script>
</body>
</html>

View File

@ -738,7 +738,7 @@
<h2> <h2>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
Balancete Balancete
<a class="dcr-export-btn" href="/api/contabil-apuracoes/{{ apuracao.id }}/exportar-xlsx/?parte=balancete"> <a class="dcr-export-btn" href="{{ prefixo_api }}/{{ apuracao.id }}/exportar-xlsx/?parte=balancete">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg>
Exportar XLSX Exportar XLSX
</a> </a>
@ -837,7 +837,7 @@
<h2> <h2>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19V5a2 2 0 0 1 2-2h8l6 6v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2Z"/><path d="M14 3v6h6M9 13h6M9 17h6"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19V5a2 2 0 0 1 2-2h8l6 6v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2Z"/><path d="M14 3v6h6M9 13h6M9 17h6"/></svg>
D.R.E. — Demonstração do Resultado do Exercício D.R.E. — Demonstração do Resultado do Exercício
<a class="dcr-export-btn" href="/api/contabil-apuracoes/{{ apuracao.id }}/exportar-xlsx/?parte=dre"> <a class="dcr-export-btn" href="{{ prefixo_api }}/{{ apuracao.id }}/exportar-xlsx/?parte=dre">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg>
Exportar XLSX Exportar XLSX
</a> </a>
@ -991,7 +991,7 @@
<div class="dcr-tab-panel" data-dcr-panel="indicadores" hidden> <div class="dcr-tab-panel" data-dcr-panel="indicadores" hidden>
<div class="dcr-resumo-toolbar"> <div class="dcr-resumo-toolbar">
<a class="dcr-export-btn" href="/api/contabil-apuracoes/{{ apuracao.id }}/resumo-pdf/"> <a class="dcr-export-btn" href="{{ prefixo_api }}/{{ apuracao.id }}/resumo-pdf/">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><path d="M12 18v-6M9 15l3 3 3-3"/></svg> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><path d="M12 18v-6M9 15l3 3 3-3"/></svg>
Exportar PDF Exportar PDF
</a> </a>

View File

@ -144,6 +144,17 @@
</ul> </ul>
</li> </li>
<li data-section="diretoria" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M3 13h18" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Diretoria</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="dashboard-contabil-de-paula"><a href="dashboard-contabil-de-paula.html" class="nav-subitem">Relatório Contábil - De Paula</a></li>
</ul>
</li>
<li data-section="relatorios-gerenciais" class="nav-group"> <li data-section="relatorios-gerenciais" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle> <button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg>

View File

@ -144,6 +144,17 @@
</ul> </ul>
</li> </li>
<li data-section="diretoria" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M3 13h18" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Diretoria</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="dashboard-contabil-de-paula"><a href="dashboard-contabil-de-paula.html" class="nav-subitem">Relatório Contábil - De Paula</a></li>
</ul>
</li>
<li data-section="relatorios-gerenciais" class="nav-group"> <li data-section="relatorios-gerenciais" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle> <button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg>

View File

@ -144,6 +144,17 @@
</ul> </ul>
</li> </li>
<li data-section="diretoria" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M3 13h18" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Diretoria</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="dashboard-contabil-de-paula"><a href="dashboard-contabil-de-paula.html" class="nav-subitem">Relatório Contábil - De Paula</a></li>
</ul>
</li>
<li data-section="relatorios-gerenciais" class="nav-group"> <li data-section="relatorios-gerenciais" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle> <button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg>

View File

@ -144,6 +144,17 @@
</ul> </ul>
</li> </li>
<li data-section="diretoria" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M3 13h18" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Diretoria</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="dashboard-contabil-de-paula"><a href="dashboard-contabil-de-paula.html" class="nav-subitem">Relatório Contábil - De Paula</a></li>
</ul>
</li>
<li data-section="relatorios-gerenciais" class="nav-group"> <li data-section="relatorios-gerenciais" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle> <button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg>

View File

@ -144,6 +144,17 @@
</ul> </ul>
</li> </li>
<li data-section="diretoria" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M3 13h18" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Diretoria</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="dashboard-contabil-de-paula"><a href="dashboard-contabil-de-paula.html" class="nav-subitem">Relatório Contábil - De Paula</a></li>
</ul>
</li>
<li data-section="relatorios-gerenciais" class="nav-group"> <li data-section="relatorios-gerenciais" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle> <button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg>

View File

@ -144,6 +144,17 @@
</ul> </ul>
</li> </li>
<li data-section="diretoria" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M3 13h18" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Diretoria</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="dashboard-contabil-de-paula"><a href="dashboard-contabil-de-paula.html" class="nav-subitem">Relatório Contábil - De Paula</a></li>
</ul>
</li>
<li data-section="relatorios-gerenciais" class="nav-group"> <li data-section="relatorios-gerenciais" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle> <button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg>

View File

@ -143,6 +143,17 @@
</ul> </ul>
</li> </li>
<li data-section="diretoria" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M3 13h18" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Diretoria</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="dashboard-contabil-de-paula"><a href="dashboard-contabil-de-paula.html" class="nav-subitem">Relatório Contábil - De Paula</a></li>
</ul>
</li>
<li data-section="relatorios-gerenciais" class="nav-group"> <li data-section="relatorios-gerenciais" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle> <button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg>

View File

@ -147,6 +147,17 @@
</ul> </ul>
</li> </li>
<li data-section="diretoria" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M3 13h18" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Diretoria</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="dashboard-contabil-de-paula"><a href="dashboard-contabil-de-paula.html" class="nav-subitem">Relatório Contábil - De Paula</a></li>
</ul>
</li>
<li data-section="relatorios-gerenciais" class="nav-group"> <li data-section="relatorios-gerenciais" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle> <button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg>

View File

@ -144,6 +144,17 @@
</ul> </ul>
</li> </li>
<li data-section="diretoria" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M3 13h18" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Diretoria</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="dashboard-contabil-de-paula"><a href="dashboard-contabil-de-paula.html" class="nav-subitem">Relatório Contábil - De Paula</a></li>
</ul>
</li>
<li data-section="relatorios-gerenciais" class="nav-group"> <li data-section="relatorios-gerenciais" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle> <button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg>

View File

@ -143,6 +143,17 @@
</ul> </ul>
</li> </li>
<li data-section="diretoria" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M3 13h18" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Diretoria</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="dashboard-contabil-de-paula"><a href="dashboard-contabil-de-paula.html" class="nav-subitem">Relatório Contábil - De Paula</a></li>
</ul>
</li>
<li data-section="relatorios-gerenciais" class="nav-group"> <li data-section="relatorios-gerenciais" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle> <button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg>