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)
`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`).
@ -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`). |
| `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-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.
@ -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)
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`):
```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.
**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.
**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).
**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`.
## 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"]`). |
| `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). |
| `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`. |

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) |
| 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 - 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) |
| 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",
"nao-conformidades.html",
"dashboard-contabil.html",
"dashboard-contabil-de-paula.html",
]
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
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
- Calendário De Paula (empresa) não tem mais versão interna — depende

View File

@ -13,13 +13,21 @@ from .models import (
ConciliacaoFornecedorVinculo,
ConciliacaoFornecedorVinculoHistorico,
ContabilAchado,
ContabilAchadoDePaula,
ContabilApuracao,
ContabilApuracaoDePaula,
ContabilApuracaoReprocessamento,
ContabilApuracaoReprocessamentoDePaula,
ContabilConta,
ContabilContaDePaula,
ContabilLinhaAnaliseVertical,
ContabilLinhaAnaliseVerticalDePaula,
ContabilLinhaDre,
ContabilLinhaDreDePaula,
ContabilObservacao,
ContabilObservacaoDePaula,
ContabilObservacaoEdicao,
ContabilObservacaoEdicaoDePaula,
Departamento,
EmpresaQuestor,
Favorito,
@ -316,6 +324,17 @@ class ContabilAchadoAdmin(admin.ModelAdmin):
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)
class ConciliacaoFornecedorAdmin(admin.ModelAdmin):
list_display = ("codigo_empresa", "nome_empresa", "data_base", "criado_por", "criado_em")

View File

@ -16,6 +16,7 @@ MODULES = [
{"key": "portais", "label": "Portais"},
{"key": "geradoc", "label": "Geradoc"},
{"key": "relatorios", "label": "Relatórios"},
{"key": "diretoria", "label": "Diretoria"},
{"key": "relatorios-gerenciais", "label": "Relatórios Gerenciais"},
{"key": "utilitarios", "label": "Utilitários"},
{"key": "integracoes", "label": "Integrações"},
@ -63,6 +64,14 @@ MODULE_APPS = {
"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": [
{"key": "visao-diretoria", "label": "Visão Diretoria"},
{"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.
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.
## 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)
- **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`)
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
@ -251,7 +278,7 @@ O miolo `<svg>` de cada um dos 11 ícones existe em `_CONTABIL_ICONES_SVG` (`vie
## 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 |
|---|---|---|
@ -522,6 +549,8 @@ Cor por sinal só onde é seguro: `sign` (ROA/ROE/EBIT/EBITDA — positivo verde
## 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.
- **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.

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).
**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).
## 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).
- `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).

View File

@ -196,6 +196,12 @@ class Command(BaseCommand):
# decisão explícita do usuário (dado de cliente, mas ferramenta
# ainda em validação), liberada depois pela tela.
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
# 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

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.")
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
contador anexa o PDF de Balancete + DRE (leiaute Questor, mesmo relatório
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
# que se usa dele fica em contas, linhas e achados.
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)
concluida_em = models.DateTimeField("Concluída em", null=True, blank=True)
# Chaves de IndicadoresFinanceiros (dashboard_contabil/indicadores.py,
@ -2065,18 +2072,43 @@ class ContabilApuracao(models.Model):
return self.leiaute != self.LEIAUTE_CONTABIT
class Meta:
verbose_name = "Apuração do Relatório Contábil"
verbose_name_plural = "Apurações do Relatório Contábil"
abstract = True
ordering = ["-competencia", "-criado_em"]
constraints = [
models.UniqueConstraint(fields=["codigo_empresa", "competencia"], name="contabil_apuracao_unica")
]
def __str__(self) -> str:
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
do usuário — "identificar quem reprocessou e quantos reprocessamentos já
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
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)
class Meta:
verbose_name = "Reprocessamento do Relatório Contábil"
verbose_name_plural = "Reprocessamentos do Relatório Contábil"
abstract = True
ordering = ["-reprocessado_em", "-id"]
def __str__(self) -> str:
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
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
de "análise" pedido pelo usuário, independente do que a auditoria
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)
# NÃO corresponde à ordem real de impressão do balancete — o Questor
# reaproveita numeração de contas antigas/canceladas, então o mesmo
@ -2158,8 +2206,7 @@ class ContabilConta(models.Model):
)
class Meta:
verbose_name = "Conta do Balancete (Relatório Contábil)"
verbose_name_plural = "Contas do Balancete (Relatório Contábil)"
abstract = True
# `id` desempata: `ordem` sozinha deixa a ordenação indefinida quando
# 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
@ -2171,12 +2218,27 @@ class ContabilConta(models.Model):
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 —
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."""
apuracao = models.ForeignKey(ContabilApuracao, on_delete=models.CASCADE, related_name="linhas_dre")
ordem = models.IntegerField("Ordem")
descricao = models.CharField("Descrição", max_length=255)
nivel = models.IntegerField("Nível de indentação", default=0)
@ -2194,16 +2256,31 @@ class ContabilLinhaDre(models.Model):
)
class Meta:
verbose_name = "Linha da DRE (Relatório Contábil)"
verbose_name_plural = "Linhas da DRE (Relatório Contábil)"
# Ver a nota em `ContabilConta.Meta`.
abstract = True
# Ver a nota em `ContabilContaBase.Meta`.
ordering = ["ordem", "id"]
def __str__(self) -> str:
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
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
@ -2213,7 +2290,6 @@ class ContabilLinhaAnaliseVertical(models.Model):
texto (não float), pra não perder precisão decimal em nenhuma conversão
— `[{"valor": "1234.56", "percentual": "12.34"}, ...]`."""
apuracao = models.ForeignKey(ContabilApuracao, on_delete=models.CASCADE, related_name="linhas_analise_vertical")
ordem = models.IntegerField("Ordem")
descricao = models.CharField("Descrição", max_length=255)
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)
class Meta:
verbose_name = "Linha da Análise Vertical (Relatório Contábil)"
verbose_name_plural = "Linhas da Análise Vertical (Relatório Contábil)"
# Ver a nota em `ContabilConta.Meta`.
abstract = True
# Ver a nota em `ContabilContaBase.Meta`.
ordering = ["ordem", "id"]
def __str__(self) -> str:
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
(`dashboard_contabil.regras`) ao criar a apuração, só muda de `status`
conforme o contador revisa (mesmo espírito de `ImportacaoPlanoSaudeAuditoria`).
@ -2275,19 +2368,14 @@ class ContabilAchado(models.Model):
(STATUS_IGNORADO, "Ignorado"),
]
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"
)
# Preenchido só por achados sem uma conta do Balancete por trás (hoje, só
# `apuracao`, `conta`, `linha_analise_vertical` e `tratado_por` ficam nas
# classes concretas. `linha_analise_vertical` é 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
# 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
# Balancete). Casado por `ordem` na view, ver `AchadoDetectado.
# 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)
severidade = models.CharField("Severidade", max_length=10, choices=SEVERIDADE_CHOICES)
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)
status = models.CharField("Status", max_length=10, choices=STATUS_CHOICES, default=STATUS_PENDENTE)
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)
# Mesmo espírito de ContabilConta.oculta_no_relatorio — só afeta a
# 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)
class Meta:
verbose_name = "Achado do Relatório Contábil"
verbose_name_plural = "Achados do Relatório Contábil"
abstract = True
ordering = ["-severidade", "id"]
def __str__(self) -> str:
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
linha da DRE ou uma linha da Análise Vertical — **não** pertence à
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
# 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)
# `SET_NULL` (não CASCADE) de propósito: o histórico é do cliente, não da
# apuração — excluir a análise de um mês não pode apagar o que o contador
# registrou sobre aquela conta. `competencia_origem` é uma cópia
# justamente pra vigência continuar resolvendo sem a apuração original.
apuracao_origem = models.ForeignKey(
ContabilApuracao, on_delete=models.SET_NULL, null=True, blank=True, related_name="observacoes"
)
# `apuracao_origem` (nas classes concretas) é `SET_NULL`, não CASCADE, de
# propósito: o histórico é do cliente, não da apuração — excluir a análise
# de um mês não pode apagar o que o contador registrou sobre aquela conta.
# `competencia_origem` é uma cópia justamente pra vigência continuar
# resolvendo sem a apuração original.
competencia_origem = models.DateField("Competência de origem")
texto = models.TextField("Observação")
# 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
# cliente quando o contador confirmar explicitamente.
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)
atualizado_em = models.DateTimeField(auto_now=True)
# "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
# 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_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)
class Meta:
verbose_name = "Observação do Relatório Contábil"
verbose_name_plural = "Observações do Relatório Contábil"
abstract = True
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:
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
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
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
@ -2470,23 +2619,44 @@ class ContabilObservacaoEdicao(models.Model):
relatório HTML gerado pro cliente (`dashboard()`) não usa esse
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_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)
class 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"
abstract = True
ordering = ["-editado_em", "-id"]
def __str__(self) -> str:
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):
"""Um indicador financeiro do Relatório Contábil — todo indicador é um
registro aqui agora, inclusive os antigos 11 "de sistema" (ROA/ROE/

View File

@ -22,13 +22,21 @@ from .models import (
CompromissoAgenda,
ConciliacaoFornecedor,
ContabilAchado,
ContabilAchadoDePaula,
ContabilApuracao,
ContabilApuracaoDePaula,
ContabilApuracaoReprocessamento,
ContabilApuracaoReprocessamentoDePaula,
ContabilConta,
ContabilContaDePaula,
ContabilLinhaAnaliseVertical,
ContabilLinhaAnaliseVerticalDePaula,
ContabilLinhaDre,
ContabilLinhaDreDePaula,
ContabilObservacao,
ContabilObservacaoDePaula,
ContabilObservacaoEdicao,
ContabilObservacaoEdicaoDePaula,
Departamento,
EmpresaQuestor,
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) -----------------------------
# O detalhe (contas + lançamentos + análise recalculada) é montado em
# `_conciliacao_detalhe()` em views.py, porque depende da análise de

View File

@ -110,6 +110,25 @@ router.register(
views.IndicadorContabilDefinicaoViewSet,
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(
"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.decorators import action, api_view, permission_classes
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.response import Response
from rest_framework.serializers import ModelSerializer
@ -74,13 +74,20 @@ from .models import (
ConciliacaoFornecedorVinculo,
ConciliacaoFornecedorVinculoHistorico,
ContabilAchado,
ContabilAchadoBase,
ContabilAchadoDePaula,
ContabilApuracao,
ContabilApuracaoReprocessamento,
ContabilApuracaoBase,
ContabilApuracaoDePaula,
ContabilConta,
ContabilContaDePaula,
ContabilLinhaAnaliseVertical,
ContabilLinhaAnaliseVerticalDePaula,
ContabilLinhaDre,
ContabilLinhaDreDePaula,
ContabilObservacao,
ContabilObservacaoEdicao,
ContabilObservacaoBase,
ContabilObservacaoDePaula,
Departamento,
Favorito,
FuncaoTelefonia,
@ -154,20 +161,29 @@ from .serializers import (
ConciliacaoFornecedorValidarSerializer,
ConciliacaoFornecedorVincularSerializer,
ContabilAchadoAjusteSerializer,
ContabilAchadoDePaulaSerializer,
ContabilAchadoOcultoSerializer,
ContabilAchadoValidarSerializer,
ContabilAchadoSerializer,
ContabilApuracaoCreateSerializer,
ContabilApuracaoDetailDePaulaSerializer,
ContabilApuracaoDetailSerializer,
ContabilApuracaoListDePaulaSerializer,
ContabilApuracaoListSerializer,
ContabilContaDePaulaSerializer,
ContabilContaSerializer,
ContabilIndicadoresOcultosSerializer,
ContabilIndicadoresSelecionadosSerializer,
ContabilResumoFechamentoSerializer,
ContabilLinhaAnaliseVerticalDePaulaSerializer,
ContabilLinhaAnaliseVerticalSerializer,
ContabilLinhaDreDePaulaSerializer,
ContabilLinhaDreSerializer,
ContabilObservacaoApuracaoDePaulaSerializer,
ContabilObservacaoApuracaoSerializer,
ContabilObservacaoCreateDePaulaSerializer,
ContabilObservacaoCreateSerializer,
ContabilObservacaoDePaulaSerializer,
ContabilObservacaoSerializer,
ContabilObservacaoUpdateSerializer,
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
primeiro) já persistidas — usado pelas regras de variação mês a mês
(`dashboard_contabil.regras`). Só uma consulta simples ao ORM, sem
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)
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")
.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]` —
histórico completo da empresa (mais recente primeiro), usado só pelo
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
usando `_contabil_monta_historico` (capado em 2), sem mudar."""
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")
.prefetch_related("contas", "linhas_dre")
)
@ -3488,13 +3515,15 @@ class _ContabilDadosIndicadores:
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"))
linhas_dre = list(apuracao.linhas_dre.order_by("ordem", "id"))
contas_atuais = {conta.codigo: conta.saldo_atual for conta in contas}
dre_atual = {linha.descricao: linha.valor for linha in linhas_dre}
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)
# só compara contra a apuração anterior imediata — mesma convenção já
# 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)]
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
+ 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
@ -3794,7 +3831,7 @@ def _contabil_dados_resumo(apuracao: ContabilApuracao) -> dict[str, Any]:
# justamente o ponto do histórico.
observacoes_visiveis = [
observacao
for observacao in ContabilObservacao.vigentes_para(
for observacao in _contabil_modelo_observacao(apuracao).vigentes_para(
apuracao.codigo_empresa, apuracao.competencia
).select_related("criado_por")
if observacao.mostrar_ao_cliente
@ -3901,7 +3938,7 @@ def _contabil_observacoes_por_chave(apuracao: ContabilApuracao, alvo_tipo: str)
por linha."""
agrupadas: dict[str, list[ContabilObservacao]] = {}
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)
.select_related("criado_por", "encerrada_por")
)
@ -4014,8 +4051,7 @@ def _contabil_sincroniza_contas(
antiga.alterada_reprocessamento = alterou
antiga.save()
else:
ContabilConta.objects.create(
apuracao=apuracao,
apuracao.contas.create(
ordem=indice,
conta_numero=extraida.conta_numero,
codigo=extraida.codigo,
@ -4058,8 +4094,7 @@ def _contabil_sincroniza_linhas_dre(
antiga.alterada_reprocessamento = alterou
antiga.save()
else:
ContabilLinhaDre.objects.create(
apuracao=apuracao,
apuracao.linhas_dre.create(
ordem=linha.ordem,
descricao=linha.descricao,
nivel=linha.nivel,
@ -4111,8 +4146,7 @@ def _contabil_sincroniza_linhas_analise_vertical(
antiga.alterada_reprocessamento = alterou
antiga.save()
else:
ContabilLinhaAnaliseVertical.objects.create(
apuracao=apuracao,
apuracao.linhas_analise_vertical.create(
ordem=linha.ordem,
descricao=linha.descricao,
nivel=linha.nivel,
@ -4150,12 +4184,22 @@ def _contabil_recria_achados(
(`ContabilObservacao`, ver o model) não vivem aqui, são casadas por chave
natural (empresa+conta) e continuam intactas através de qualquer
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()}
av_por_ordem = {linha.ordem: linha for linha in apuracao.linhas_analise_vertical.all()}
apuracao.achados.all().delete()
ContabilAchado.objects.bulk_create(
modelo_achado.objects.bulk_create(
[
ContabilAchado(
modelo_achado(
apuracao=apuracao,
conta=contas_por_codigo.get(achado.codigo_conta) if achado.codigo_conta else None,
linha_analise_vertical=(
@ -4182,15 +4226,28 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
persistido da mesma empresa e roda o motor de regras — tudo isso **antes**
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
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"]
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]:
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()
if self.action == "retrieve":
queryset = queryset.prefetch_related(
@ -4202,10 +4259,10 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
def get_serializer_class(self) -> type[ModelSerializer]:
if self.action == "list":
return ContabilApuracaoListSerializer
return ContabilApuracaoDetailSerializer
return self.serializer_lista
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
do usuário, mesma trava de "concluiu, não se mexe mais" que já valia
pra reprocessar e pra editar observação/achado
@ -4225,17 +4282,18 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
entrada.is_valid(raise_exception=True)
arquivo = entrada.validated_data["arquivo"]
conteudo = arquivo.read()
modelo_apuracao = self.queryset.model
try:
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:
return _contabil_resposta_extracao_invalida(exc)
cabecalho = resultado.extracao.cabecalho
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(
{"detail": f"Já existe uma análise para {cabecalho.nome_empresa} na competência {competencia:%m/%Y}."},
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.
try:
with transaction.atomic():
apuracao = ContabilApuracao.objects.create(
apuracao = modelo_apuracao.objects.create(
codigo_empresa=cabecalho.codigo_empresa,
nome_empresa=cabecalho.nome_empresa,
cnpj=cabecalho.cnpj,
@ -4256,9 +4314,12 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
fonte_pdf_atipica=resultado.extracao.fonte_pdf_atipica,
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,
ordem=indice,
conta_numero=conta.conta_numero,
@ -4273,9 +4334,9 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
for indice, conta in enumerate(resultado.extracao.contas)
]
)
ContabilLinhaDre.objects.bulk_create(
modelo_linha_dre.objects.bulk_create(
[
ContabilLinhaDre(
modelo_linha_dre(
apuracao=apuracao,
ordem=linha.ordem,
descricao=linha.descricao,
@ -4286,9 +4347,9 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
for linha in resultado.extracao.linhas_dre
]
)
ContabilLinhaAnaliseVertical.objects.bulk_create(
modelo_linha_av.objects.bulk_create(
[
ContabilLinhaAnaliseVertical(
modelo_linha_av(
apuracao=apuracao,
ordem=linha.ordem,
descricao=linha.descricao,
@ -4302,34 +4363,14 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
for linha in resultado.extracao.linhas_analise_vertical
]
)
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()}
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
]
)
_contabil_cria_achados(apuracao, resultado.achados)
except Exception:
return Response(
{"detail": "Não foi possível salvar a análise. Contate o setor de Inovação."},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = ContabilApuracaoDetailSerializer(apuracao)
serializer = self.serializer_detalhe(apuracao)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@action(detail=True, methods=["post"])
@ -4381,7 +4422,7 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
try:
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:
return _contabil_resposta_extracao_invalida(exc)
@ -4428,17 +4469,17 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
_contabil_recria_achados(
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:
return Response(
{"detail": "Não foi possível reprocessar a análise. Contate o setor de Inovação."},
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"
).get(pk=apuracao.pk)
return Response(ContabilApuracaoDetailSerializer(apuracao).data)
return Response(self.serializer_detalhe(apuracao).data)
@action(detail=True, methods=["post"])
def concluir(self, request: Request, pk: str | None = None) -> Response:
@ -4451,7 +4492,7 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
apuracao.status = ContabilApuracao.STATUS_CONCLUIDA
apuracao.concluida_em = timezone.now()
apuracao.save(update_fields=["status", "concluida_em"])
return Response(ContabilApuracaoDetailSerializer(apuracao).data)
return Response(self.serializer_detalhe(apuracao).data)
@action(detail=True, methods=["get"])
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)."""
apuracao = self.get_object()
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")
.prefetch_related("edicoes__editado_por")
)
serializada = ContabilObservacaoSerializer(
serializada = self.serializer_observacao(
consulta, many=True, context={"competencia": apuracao.competencia}
)
return Response(serializada.data)
@ -4574,6 +4616,7 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
"achados_com_observacao": dados_resumo["achados_com_observacao"],
"gerado_em": timezone.now(),
"logo_data_uri": _logo_relatorio_data_uri(),
"prefixo_api": self.prefixo_api,
}
html = render_to_string("dashboard-contabil-relatorio.html", contexto)
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"]
queryset = ContabilConta.objects.all()
serializer_class = ContabilContaSerializer
permissao_app = _CONTABIL_PERMISSAO
def get_permissions(self) -> list[BasePermission]:
return [PermissaoApp("relatorios", "dashboard-contabil")]
return [PermissaoApp(*self.permissao_app)]
def perform_update(self, serializer: ContabilContaSerializer) -> None:
_contabil_garante_em_revisao(serializer.instance.apuracao)
@ -4856,9 +4900,10 @@ class ContabilLinhaDreViewSet(viewsets.ModelViewSet):
http_method_names = ["get", "patch", "head", "options"]
queryset = ContabilLinhaDre.objects.all()
serializer_class = ContabilLinhaDreSerializer
permissao_app = _CONTABIL_PERMISSAO
def get_permissions(self) -> list[BasePermission]:
return [PermissaoApp("relatorios", "dashboard-contabil")]
return [PermissaoApp(*self.permissao_app)]
def perform_update(self, serializer: ContabilLinhaDreSerializer) -> None:
_contabil_garante_em_revisao(serializer.instance.apuracao)
@ -4873,9 +4918,10 @@ class ContabilLinhaAnaliseVerticalViewSet(viewsets.ModelViewSet):
http_method_names = ["get", "patch", "head", "options"]
queryset = ContabilLinhaAnaliseVertical.objects.all()
serializer_class = ContabilLinhaAnaliseVerticalSerializer
permissao_app = _CONTABIL_PERMISSAO
def get_permissions(self) -> list[BasePermission]:
return [PermissaoApp("relatorios", "dashboard-contabil")]
return [PermissaoApp(*self.permissao_app)]
def perform_update(self, serializer: ContabilLinhaAnaliseVerticalSerializer) -> None:
_contabil_garante_em_revisao(serializer.instance.apuracao)
@ -4920,21 +4966,24 @@ class ContabilObservacaoViewSet(viewsets.ModelViewSet):
http_method_names = ["post", "patch", "delete", "head", "options"]
queryset = ContabilObservacao.objects.select_related("criado_por", "encerrada_por", "apuracao_origem")
serializer_class = ContabilObservacaoSerializer
serializer_criacao = ContabilObservacaoCreateSerializer
serializer_apuracao = ContabilObservacaoApuracaoSerializer
permissao_app = _CONTABIL_PERMISSAO
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
objeto (em vez de confiar numa chave enviada pelo cliente) garante
que a chave natural gravada é sempre a real e que o alvo pertence
mesmo à apuração informada."""
modelos = {
ContabilObservacao.ALVO_CONTA: ContabilConta,
ContabilObservacao.ALVO_DRE: ContabilLinhaDre,
ContabilObservacao.ALVO_ANALISE_VERTICAL: ContabilLinhaAnaliseVertical,
relacoes = {
ContabilObservacao.ALVO_CONTA: apuracao.contas,
ContabilObservacao.ALVO_DRE: apuracao.linhas_dre,
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:
raise ValidationError({"alvo_id": "Conta ou linha não encontrada nesta análise."})
return alvo
@ -4952,13 +5001,13 @@ class ContabilObservacaoViewSet(viewsets.ModelViewSet):
)
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)
dados = entrada.validated_data
apuracao: ContabilApuracao = dados["apuracao"]
apuracao: ContabilApuracaoBase = dados["apuracao"]
_contabil_garante_em_revisao(apuracao)
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,
alvo_tipo=dados["alvo_tipo"],
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"],
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)
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.is_valid(raise_exception=True)
dados = entrada.validated_data
@ -4982,8 +5031,7 @@ class ContabilObservacaoViewSet(viewsets.ModelViewSet):
self._garante_texto_editavel(observacao)
texto_novo = dados["texto"].strip()
if texto_novo != observacao.texto:
ContabilObservacaoEdicao.objects.create(
observacao=observacao,
observacao.edicoes.create(
texto_anterior=observacao.texto,
texto_novo=texto_novo,
editado_por=request.user,
@ -4995,9 +5043,9 @@ class ContabilObservacaoViewSet(viewsets.ModelViewSet):
campos.append("mostrar_ao_cliente")
observacao.save(update_fields=[*campos, "atualizado_em"])
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)
if instance.criado_por_id != self.request.user.id:
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 é
reescrito) e some a partir da seguinte."""
observacao = self.get_object()
entrada = ContabilObservacaoApuracaoSerializer(data=request.data)
entrada = self.serializer_apuracao(data=request.data)
entrada.is_valid(raise_exception=True)
apuracao: ContabilApuracao = entrada.validated_data["apuracao"]
apuracao: ContabilApuracaoBase = entrada.validated_data["apuracao"]
_contabil_garante_em_revisao(apuracao)
observacao.encerrada_em_competencia = apuracao.competencia
observacao.encerrada_por = request.user
observacao.encerrada_em = timezone.now()
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)
@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
texto continua intocado nos dois caminhos."""
observacao = self.get_object()
entrada = ContabilObservacaoApuracaoSerializer(data=request.data)
entrada = self.serializer_apuracao(data=request.data)
entrada.is_valid(raise_exception=True)
apuracao: ContabilApuracao = entrada.validated_data["apuracao"]
apuracao: ContabilApuracaoBase = entrada.validated_data["apuracao"]
_contabil_garante_em_revisao(apuracao)
observacao.encerrada_em_competencia = None
observacao.encerrada_por = None
observacao.encerrada_em = None
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)
@ -5046,17 +5094,19 @@ class ContabilAchadoViewSet(viewsets.ModelViewSet):
http_method_names = ["get", "patch", "post", "head", "options"]
queryset = ContabilAchado.objects.all()
serializer_leitura = ContabilAchadoSerializer
permissao_app = _CONTABIL_PERMISSAO
def get_permissions(self) -> list[BasePermission]:
return [PermissaoApp("relatorios", "dashboard-contabil")]
return [PermissaoApp(*self.permissao_app)]
def get_serializer_class(self) -> type[ModelSerializer]:
if self.action in ("update", "partial_update"):
return ContabilAchadoAjusteSerializer
return ContabilAchadoSerializer
return self.serializer_leitura
def perform_update(self, serializer: ContabilAchadoAjusteSerializer) -> None:
achado: ContabilAchado = self.get_object()
achado: ContabilAchadoBase = self.get_object()
_contabil_garante_em_revisao(achado.apuracao)
achado.status = serializer.validated_data["status"]
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:
super().update(request, *args, **kwargs)
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")
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)
achado.oculto_no_relatorio = entrada.validated_data["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"])
def validar(self, request: Request, pk: str | None = None) -> Response:
@ -5110,7 +5160,7 @@ class ContabilAchadoViewSet(viewsets.ModelViewSet):
achado.tratado_por = None
achado.tratado_em = None
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:
@ -5150,7 +5200,11 @@ class IndicadorContabilDefinicaoViewSet(viewsets.ModelViewSet):
queryset = IndicadorContabilDefinicao.objects.prefetch_related("componentes").all()
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]:
if self.action in ("create", "update", "partial_update"):
@ -5246,21 +5300,73 @@ class IndicadorContabilDefinicaoViewSet(viewsets.ModelViewSet):
# em `ContabilApuracao.indicadores_selecionados`/`indicadores_ocultos`
# e a próxima tentativa de alternar QUALQUER indicador no hub daquela
# apuração falharia (a validação reenvia a lista inteira a cada
# alternância). Ver bug real, rodada 106.
for apuracao in ContabilApuracao.objects.filter(
Q(indicadores_selecionados__contains=[chave]) | Q(indicadores_ocultos__contains=[chave])
):
campos_alterados = []
if chave in apuracao.indicadores_selecionados:
apuracao.indicadores_selecionados = [
c for c in apuracao.indicadores_selecionados if c != chave
]
campos_alterados.append("indicadores_selecionados")
if chave in apuracao.indicadores_ocultos:
apuracao.indicadores_ocultos = [c for c in apuracao.indicadores_ocultos if c != chave]
campos_alterados.append("indicadores_ocultos")
if campos_alterados:
apuracao.save(update_fields=campos_alterados)
# alternância). Ver bug real, rodada 106. Varre as duas aplicações,
# 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])
):
campos_alterados = []
if chave in apuracao.indicadores_selecionados:
apuracao.indicadores_selecionados = [
c for c in apuracao.indicadores_selecionados if c != chave
]
campos_alterados.append("indicadores_selecionados")
if chave in apuracao.indicadores_ocultos:
apuracao.indicadores_ocultos = [c for c in apuracao.indicadores_ocultos if c != chave]
campos_alterados.append("indicadores_ocultos")
if 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) -----------------------------

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)
- **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 - 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)
- **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_SEVERIDADE_LABELS = { alta: "Alta", media: "Média", baixa: "Baixa" };
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);
async function pidFetchApuracoesContabil() {
return pidApiRequest("/contabil-apuracoes/");
return pidApiRequest(`${PID_DC_CONFIG.endpointApuracoes}/`);
}
async function pidFetchApuracaoContabil(id) {
return pidApiRequest(`/contabil-apuracoes/${id}/`);
return pidApiRequest(`${PID_DC_CONFIG.endpointApuracoes}/${id}/`);
}
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) {
return pidApiRequest(`/contabil-apuracoes/${id}/`, { method: "DELETE" });
return pidApiRequest(`${PID_DC_CONFIG.endpointApuracoes}/${id}/`, { method: "DELETE" });
}
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) {
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.
*/
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) ----
@ -361,14 +384,14 @@ function pidGerarDashboardContabil(id) {
// `/contabil-observacoes/`, nunca contra a linha.
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,
// competência, chave natural e rótulo a partir dele (ver
// ContabilObservacaoViewSet.create).
async function pidCriarObservacaoContabil(apuracaoId, alvoTipo, alvoId, texto, mostrarAoCliente) {
return pidApiRequest("/contabil-observacoes/", {
return pidApiRequest(`${PID_DC_CONFIG.endpointObservacoes}/`, {
method: "POST",
body: {
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
// cliente é sempre alternável, inclusive numa observação histórica.
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
// some a partir da seguinte. `reativar` desfaz.
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) {
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
@ -402,7 +425,7 @@ async function pidReativarObservacaoContabil(id, apuracaoId) {
// revisão (mesmas duas regras checadas de novo no servidor, ver
// ContabilObservacaoViewSet.perform_destroy()).
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
@ -412,19 +435,19 @@ async function pidExcluirObservacaoContabil(id) {
// um reprocessamento novo (sem mudança daquela conta/linha desta vez) limpa
// o campo de verdade, em `_contabil_sincroniza_*` (views.py).
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) {
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) {
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) {
return pidApiRequest(`/contabil-achados/${id}/`, {
return pidApiRequest(`${PID_DC_CONFIG.endpointAchados}/${id}/`, {
method: "PATCH",
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
// volta a "pendente" — ver ContabilAchadoViewSet.validar().
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") ----
async function pidBuscarIndicadoresContabil(id) {
return pidApiRequest(`/contabil-apuracoes/${id}/indicadores/`);
return pidApiRequest(`${PID_DC_CONFIG.endpointApuracoes}/${id}/indicadores/`);
}
async function pidAtualizarIndicadoresOcultosContabil(id, indicadoresOcultos) {
return pidApiRequest(`/contabil-apuracoes/${id}/indicadores-ocultos/`, {
return pidApiRequest(`${PID_DC_CONFIG.endpointApuracoes}/${id}/indicadores-ocultos/`, {
method: "POST",
body: { indicadores_ocultos: indicadoresOcultos },
});
}
async function pidAlternarOcultoAchadoContabil(id, oculto) {
return pidApiRequest(`/contabil-achados/${id}/alternar-oculto/`, {
return pidApiRequest(`${PID_DC_CONFIG.endpointAchados}/${id}/alternar-oculto/`, {
method: "POST",
body: { oculto_no_relatorio: oculto },
});
@ -478,21 +501,21 @@ async function pidBuscarDefinicoesIndicadoresContabil() {
}
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",
body: payload,
});
}
async function pidAtualizarIndicadoresSelecionadosContabil(id, indicadoresSelecionados) {
return pidApiRequest(`/contabil-apuracoes/${id}/indicadores-selecionados/`, {
return pidApiRequest(`${PID_DC_CONFIG.endpointApuracoes}/${id}/indicadores-selecionados/`, {
method: "POST",
body: { indicadores_selecionados: indicadoresSelecionados },
});
}
async function pidAtualizarResumoFechamentoContabil(id, resumoFechamento) {
return pidApiRequest(`/contabil-apuracoes/${id}/resumo-fechamento/`, {
return pidApiRequest(`${PID_DC_CONFIG.endpointApuracoes}/${id}/resumo-fechamento/`, {
method: "POST",
body: { resumo_fechamento: resumoFechamento },
});
@ -505,8 +528,8 @@ document.addEventListener("DOMContentLoaded", async () => {
const me = await pidGetMe();
if (!me) return;
const relatoriosPerms = (me.permissoes_efetivas && me.permissoes_efetivas["relatorios"]) || {};
const canUse = !!(relatoriosPerms.enabled && relatoriosPerms.apps && relatoriosPerms.apps["dashboard-contabil"]);
const moduloPerms = (me.permissoes_efetivas && me.permissoes_efetivas[PID_DC_CONFIG.moduleKey]) || {};
const canUse = !!(moduloPerms.enabled && moduloPerms.apps && moduloPerms.apps[PID_DC_CONFIG.appKey]);
const noAccessEl = document.getElementById("dc-no-access");
const contentEl = document.getElementById("dc-content");

View File

@ -57,10 +57,27 @@ function pidPerfilFromApi(p) {
nome: p.nome,
ativo: p.ativo,
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) {
return {
nome: p.nome,

View File

@ -143,6 +143,17 @@
</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">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>

View File

@ -144,6 +144,17 @@
</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">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>

View File

@ -144,6 +144,17 @@
</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">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>

View File

@ -143,6 +143,17 @@
</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">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>

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>
<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
<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>
Exportar XLSX
</a>
@ -837,7 +837,7 @@
<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>
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>
Exportar XLSX
</a>
@ -991,7 +991,7 @@
<div class="dcr-tab-panel" data-dcr-panel="indicadores" hidden>
<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>
Exportar PDF
</a>

View File

@ -144,6 +144,17 @@
</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">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>

View File

@ -144,6 +144,17 @@
</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">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>

View File

@ -144,6 +144,17 @@
</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">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>

View File

@ -144,6 +144,17 @@
</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">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>

View File

@ -144,6 +144,17 @@
</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">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>

View File

@ -144,6 +144,17 @@
</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">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>

View File

@ -143,6 +143,17 @@
</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">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>

View File

@ -147,6 +147,17 @@
</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">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>

View File

@ -144,6 +144,17 @@
</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">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>

View File

@ -143,6 +143,17 @@
</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">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>