Implementação dos comentários históricos
This commit is contained in:
parent
9752f7c27d
commit
66e83090b6
@ -379,7 +379,7 @@ Ver `portal_api/nao_conformidades/CLAUDE.md`.
|
|||||||
|
|
||||||
Chamado de "Dashboard Contábil" até uma rodada anterior — renomeado pra "Relatório Contábil" a pedido explícito do usuário, pra soar como um aliado do trabalho do contador em vez de mais um processo/sistema novo (rename só de rótulo visível: menu, título da página, cabeçalhos e o botão que gera o relatório; nomes técnicos internos — pasta do pacote, arquivos, classes de model, rotas — continuam `dashboard_contabil`/`dashboard-contabil.*`/`Contabil*`/`/contabil-*`, sem nenhuma mudança).
|
Chamado de "Dashboard Contábil" até uma rodada anterior — renomeado pra "Relatório Contábil" a pedido explícito do usuário, pra soar como um aliado do trabalho do contador em vez de mais um processo/sistema novo (rename só de rótulo visível: menu, título da página, cabeçalhos e o botão que gera o relatório; nomes técnicos internos — pasta do pacote, arquivos, classes de model, rotas — continuam `dashboard_contabil`/`dashboard-contabil.*`/`Contabil*`/`/contabil-*`, sem nenhuma mudança).
|
||||||
|
|
||||||
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, mesmo relatório hoje enviado ao cliente), 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 — cada conta/linha também ganha um checkbox de "validado" (marcador informativo de que o contador já conferiu aquele item) e um editor de observação inline na própria tabela (não mais um popup), com um toggle "mostrar ao cliente" que nasce desmarcado por padrão. O botão "Gerar Relatório" gera um relatório HTML autocontido (indicadores financeiros, gráfico de evolução, DRE/Balancete agrupados, observações do contador — com a marca do escritório, não a "P.I.D." do Portal), com exportação de Balancete/DRE em XLSX a partir dele. **Ainda fora de escopo**: consolidação entre várias empresas/competências ao mesmo tempo (substituiria o BI Contábil por completo).
|
Otimiza a conferência de balancetes hoje feita manualmente pelo Fisco/Contábil (`ITD-FISCO-7513`): o contador anexa o PDF de Balancete + DRE (modelo Questor, mesmo relatório hoje enviado ao cliente), 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).
|
||||||
|
|
||||||
Ver `portal_api/dashboard_contabil/CLAUDE.md`.
|
Ver `portal_api/dashboard_contabil/CLAUDE.md`.
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from .models import (
|
|||||||
ContabilConta,
|
ContabilConta,
|
||||||
ContabilLinhaAnaliseVertical,
|
ContabilLinhaAnaliseVertical,
|
||||||
ContabilLinhaDre,
|
ContabilLinhaDre,
|
||||||
|
ContabilObservacao,
|
||||||
Departamento,
|
Departamento,
|
||||||
EmpresaQuestor,
|
EmpresaQuestor,
|
||||||
Favorito,
|
Favorito,
|
||||||
@ -272,6 +273,22 @@ class ContabilLinhaAnaliseVerticalAdmin(admin.ModelAdmin):
|
|||||||
search_fields = ("descricao",)
|
search_fields = ("descricao",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ContabilObservacao)
|
||||||
|
class ContabilObservacaoAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"codigo_empresa",
|
||||||
|
"alvo_tipo",
|
||||||
|
"alvo_rotulo",
|
||||||
|
"competencia_origem",
|
||||||
|
"encerrada_em_competencia",
|
||||||
|
"mostrar_ao_cliente",
|
||||||
|
"criado_por",
|
||||||
|
)
|
||||||
|
list_filter = ("alvo_tipo", "mostrar_ao_cliente")
|
||||||
|
search_fields = ("codigo_empresa", "alvo_chave", "alvo_rotulo", "texto")
|
||||||
|
ordering = ("-competencia_origem", "codigo_empresa")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ContabilAchado)
|
@admin.register(ContabilAchado)
|
||||||
class ContabilAchadoAdmin(admin.ModelAdmin):
|
class ContabilAchadoAdmin(admin.ModelAdmin):
|
||||||
list_display = ("apuracao", "regra", "severidade", "status", "titulo")
|
list_display = ("apuracao", "regra", "severidade", "status", "titulo")
|
||||||
|
|||||||
@ -228,3 +228,15 @@ Dois ajustes de UX pedidos pelo usuário depois de ver o badge funcionando: (1)
|
|||||||
Cliente novo (`1751 - Balancete 07.2026.pdf`) deu 400 "Nenhuma linha de DRE encontrada" ao processar. Causa raiz confirmada rodando `pdfplumber` de verdade contra o arquivo: a fonte embutida nesse PDF perde o til do "Ã" ao extrair "DEMONSTRAÇÃO DO RESULTADO DO EXERCÍCIO" (sai "DEMONSTRAÇAO..."), e `parser.py` comparava esse título por igualdade exata — a seção DRE nunca era reconhecida. Corrigido com `_normaliza_titulo()` (remove acento antes de comparar), mesmo espírito de `_RE_PERIODO` já aceitar `Per[ií]odo` pra essa mesma classe de variação de fonte entre clientes/instalações do Questor.
|
Cliente novo (`1751 - Balancete 07.2026.pdf`) deu 400 "Nenhuma linha de DRE encontrada" ao processar. Causa raiz confirmada rodando `pdfplumber` de verdade contra o arquivo: a fonte embutida nesse PDF perde o til do "Ã" ao extrair "DEMONSTRAÇÃO DO RESULTADO DO EXERCÍCIO" (sai "DEMONSTRAÇAO..."), e `parser.py` comparava esse título por igualdade exata — a seção DRE nunca era reconhecida. Corrigido com `_normaliza_titulo()` (remove acento antes de comparar), mesmo espírito de `_RE_PERIODO` já aceitar `Per[ií]odo` pra essa mesma classe de variação de fonte entre clientes/instalações do Questor.
|
||||||
|
|
||||||
O mesmo PDF também tem algumas descrições de conta com palavras coladas (ex. "BANCÁRIOSA VISTA") — investigado a fundo (medição real dos vãos entre caracteres, tentativa de usar os espaços literais do PDF como sinal), mas não há correção automática segura: o espaçamento dessa fonte é inconsistente a ponto de um vão "dentro de palavra" às vezes ser maior que um vão real "entre palavras". Cheguei a propor um botão de lápis pra edição manual da descrição, mas o usuário suspendeu essa ideia e pediu algo mais simples: `ContabilApuracao.fonte_pdf_atipica` (migração `0070`) fica `True` quando a normalização de acento foi realmente necessária pra reconhecer a seção — sinal indireto de que este PDF usa fonte diferente da de referência, calculado em `extrai_balancete_dre()`/persistido por `create()`/`reprocessar()`. Frontend mostra um ícone de aviso (cor `--gold`) ao lado do nome da empresa, na lista e no cabeçalho da revisão, avisando pra conferir os nomes de conta com atenção — puramente informativo. Validado `True` só no PDF com o problema, `False` nos dois PDFs de referência já confirmados corretos (`792`, `2017`). Detalhe completo no `CLAUDE.md` desta pasta.
|
O mesmo PDF também tem algumas descrições de conta com palavras coladas (ex. "BANCÁRIOSA VISTA") — investigado a fundo (medição real dos vãos entre caracteres, tentativa de usar os espaços literais do PDF como sinal), mas não há correção automática segura: o espaçamento dessa fonte é inconsistente a ponto de um vão "dentro de palavra" às vezes ser maior que um vão real "entre palavras". Cheguei a propor um botão de lápis pra edição manual da descrição, mas o usuário suspendeu essa ideia e pediu algo mais simples: `ContabilApuracao.fonte_pdf_atipica` (migração `0070`) fica `True` quando a normalização de acento foi realmente necessária pra reconhecer a seção — sinal indireto de que este PDF usa fonte diferente da de referência, calculado em `extrai_balancete_dre()`/persistido por `create()`/`reprocessar()`. Frontend mostra um ícone de aviso (cor `--gold`) ao lado do nome da empresa, na lista e no cabeçalho da revisão, avisando pra conferir os nomes de conta com atenção — puramente informativo. Validado `True` só no PDF com o problema, `False` nos dois PDFs de referência já confirmados corretos (`792`, `2017`). Detalhe completo no `CLAUDE.md` desta pasta.
|
||||||
|
|
||||||
|
### 126. Observação virou histórico por empresa+conta, atravessando competências
|
||||||
|
|
||||||
|
Pedido explícito do usuário: uma observação registrada num mês (ex. um ajuste de estoque) precisava reaparecer na análise do mês seguinte, assinada por quem escreveu e com a data, bloqueada pra edição por ser registro histórico, com três caminhos pro contador (manter o histórico, que é o padrão; ocultar das próximas execuções; incluir uma observação nova) e filtro de visibilidade ao cliente em todas elas. Antes disso a observação era um campo da linha da apuração, então morria junto com a competência.
|
||||||
|
|
||||||
|
Estrutura alinhada com o usuário antes de implementar (três decisões confirmadas por `AskUserQuestion`): toda observação propaga por padrão (encerrar é uma ação explícita, não existe "fixar"); o histórico cobre Balancete/D.R.E./Análise Vertical, e a justificativa de tratativa dos itens de auditoria continua presa à apuração como já era; e o "mostrar ao cliente" continua alternável mesmo numa observação já travada, porque é decisão editorial de cada relatório, não parte do registro histórico.
|
||||||
|
|
||||||
|
Model novo `ContabilObservacao` (migração `0071`, que também copia as observações já existentes e remove os campos `observacao`/`oculta_no_relatorio` dos três models de linha): escopo `codigo_empresa` + chave natural do alvo (`codigo` no Balancete, `descricao|nivel` na DRE/Análise Vertical, as mesmas chaves do reprocessamento), `competencia_origem`/`encerrada_em_competencia` definindo a vigência, `criado_por`/`criado_em` como assinatura visível e `mostrar_ao_cliente` no lugar do antigo `oculta_no_relatorio`. `ContabilObservacaoViewSet` (`POST`/`PATCH`/`DELETE` + `encerrar`/`reativar`) e `GET /api/contabil-apuracoes/{id}/observacoes/` para o recorte de vigência. Como a observação não mora mais na linha, reprocessar uma apuração deixou de ter qualquer risco sobre ela.
|
||||||
|
|
||||||
|
Na tela, o editor inline virou uma thread: o histórico da conta em cima (cada item com autor, data, competência de origem, selos de "Histórico"/"Encerrada"/"Aparece ao cliente" e ações de olho, editar, excluir e encerrar) e o campo de observação nova embaixo; o botão da coluna "Observação" ganhou um contador. Os resumos por aba e a lista consolidada da aba "Dashboard" passaram a listar observações vigentes com assinatura, e ganharam os chips "Todas / Visíveis ao cliente / Internas". No relatório "Gerar Dashboard", cada observação visível aparece assinada e, quando vem de um mês anterior, com a competência em que foi registrada.
|
||||||
|
|
||||||
|
Testado ponta a ponta via `Client.force_login()` dentro de uma transação com rollback (listagem por vigência, criação com chave derivada no servidor, alvo de outra apuração recusado, edição, bloqueio do texto quando a origem está concluída, visibilidade ainda alternável nesse caso, encerrar/reativar com as fronteiras de competência, herança numa competência seguinte e o relatório nos dois meses) — nada gravado em produção além da migração em si. Detalhe completo no `CLAUDE.md` desta pasta.
|
||||||
|
|||||||
@ -349,7 +349,11 @@ Pedido explícito do usuário: até aqui, corrigir uma apuração com o PDF erra
|
|||||||
|
|
||||||
**Por que atualização no lugar em vez de delete+recria**: a alternativa óbvia (apagar tudo e rodar `bulk_create` como em `create()`) quebraria a FK de todo achado preservado que referencia uma `ContabilConta` (o `on_delete=SET_NULL` desvincularia silenciosamente a conta do achado) — mantendo o mesmo `id` por conta/linha casada, a FK nunca precisa ser tocada, e achados "sobreviventes" continuam apontando pra conta certa sem nenhum código extra de re-vinculação.
|
**Por que atualização no lugar em vez de delete+recria**: a alternativa óbvia (apagar tudo e rodar `bulk_create` como em `create()`) quebraria a FK de todo achado preservado que referencia uma `ContabilConta` (o `on_delete=SET_NULL` desvincularia silenciosamente a conta do achado) — mantendo o mesmo `id` por conta/linha casada, a FK nunca precisa ser tocada, e achados "sobreviventes" continuam apontando pra conta certa sem nenhum código extra de re-vinculação.
|
||||||
|
|
||||||
**Frontend** (`dashboard-contabil.html`/`.js`): botão de ícone (refresh) ao lado de "Abrir" na lista (`data-dc-reprocessar-abrir`, escondido quando `status === "concluida"`) abre `#dc-reprocessar-modal` (mesmo campo de arquivo de "Nova Análise", reaproveitando `wireArquivoField()` que já era genérico o bastante) — `pidReprocessarApuracaoContabil(id, formData)` chama o endpoint, atualiza a lista e, se a apuração reprocessada é a que já está aberta na revisão, também re-renderiza a tela (`apuracaoAtual = atualizada; renderRevisao()`). Cada uma das 3 tabelas (Balancete/DRE/Análise Vertical) ganhou um badge de alerta (`pidDcAlteradaBadgeHtml()`, ícone de triângulo) ao lado do botão de observação, visível enquanto `alterada_reprocessamento` for `True`. **Marcar a conta/linha como validada de novo NÃO limpa o alerta** (pedido explícito do usuário, revertendo a primeira versão desta rodada, que limpava — "quando o usuário marcar como validado uma conta que foi reprocessada, não deve sumir o ícone de aviso, mas sim, ficar verde... conseguimos verificar quais itens foram reprocessados e revalidados") — o badge muda de cor conforme `validado` da própria conta/linha: `--danger` (vermelho) enquanto pendente, verde (`--validada`, mesmo hex de `.status-pill--ativo`) depois de validado; `pidAtualizarValidadoContaContabil()`/`...LinhaDreContabil()`/`...LinhaAnaliseVerticalContabil()` voltaram a enviar só `{ validado }`, sem tocar em `alterada_reprocessamento`. O campo só é limpo de verdade num próximo reprocessamento sem mudança naquela conta/linha específica (`_contabil_sincroniza_*`, backend). **Tooltip do badge mostra o valor de antes do reprocessamento** (`title`, pedido explícito do usuário): Balancete/DRE formatam `valor_anterior_reprocessamento` direto com `pidDcFormatMoeda()`; Análise Vertical usa `pidDcValorAnteriorAvTexto()`, que junta o valor+percentual de cada mês de `valores_anterior_reprocessamento` (mesmo alinhamento posicional de `analise_vertical_meses`) numa linha por mês dentro do mesmo tooltip. Nenhum badge tem tooltip de valor quando o campo vem `null`/vazio (conta/linha nova nesta apuração, sem "antes" pra comparar).
|
**Frontend** (`dashboard-contabil.html`/`.js`): botão de ícone (refresh) ao lado de "Abrir" na lista (`data-dc-reprocessar-abrir`, escondido quando `status === "concluida"`) abre `#dc-reprocessar-modal` (mesmo campo de arquivo de "Nova Análise", reaproveitando `wireArquivoField()` que já era genérico o bastante) — `pidReprocessarApuracaoContabil(id, formData)` chama o endpoint, atualiza a lista e, se a apuração reprocessada é a que já está aberta na revisão, também re-renderiza a tela (`apuracaoAtual = atualizada; renderRevisao()`). Cada uma das 3 tabelas (Balancete/DRE/Análise Vertical) ganhou um badge de alerta (`pidDcAlteradaBadgeHtml()`, ícone de triângulo) ao lado do botão de observação, visível enquanto `alterada_reprocessamento` for `True`. **Marcar a conta/linha como validada de novo NÃO limpa o alerta** (pedido explícito do usuário, revertendo a primeira versão desta rodada, que limpava — "quando o usuário marcar como validado uma conta que foi reprocessada, não deve sumir o ícone de aviso, mas sim, ficar verde... conseguimos verificar quais itens foram reprocessados e revalidados") — o badge muda de cor conforme `validado` da própria conta/linha: `--danger` (vermelho) enquanto pendente, verde (`--validada`, mesmo hex de `.status-pill--ativo`) depois de validado; `pidAtualizarValidadoContaContabil()`/`...LinhaDreContabil()`/`...LinhaAnaliseVerticalContabil()` voltaram a enviar só `{ validado }`, sem tocar em `alterada_reprocessamento`. O campo só é limpo de verdade num próximo reprocessamento sem mudança naquela conta/linha específica (`_contabil_sincroniza_*`, backend). **Tooltip do badge mostra o valor de antes do reprocessamento**: Balancete/DRE formatam `valor_anterior_reprocessamento` direto com `pidDcFormatMoeda()`; Análise Vertical usa `pidDcValorAnteriorAvTexto()`, que junta o valor+percentual de cada mês de `valores_anterior_reprocessamento` (mesmo alinhamento posicional de `analise_vertical_meses`) numa linha por mês dentro do mesmo tooltip. Nenhum badge tem tooltip de valor quando o campo vem `null`/vazio (conta/linha nova nesta apuração, sem "antes" pra comparar).
|
||||||
|
|
||||||
|
**Tooltip customizado no visual do Portal, não o balão nativo do navegador** (pedido explícito do usuário, depois de ver o balão cinza padrão do Chrome nesse badge e no de "fonte de PDF atípica" abaixo — mesmo raciocínio já aplicado a `pidConfirm()`/`pidAlert()` no lugar de `window.confirm()`/`window.alert()`, ver "Modal de confirmação genérico" no `CLAUDE.md` raiz): `pidDcHoverTooltipHtml(gatilhoHtml, gatilhoClasse, texto)` (novo, `dashboard-contabil.js`) monta um par gatilho+texto (`.dc-hover-tooltip`/`__gatilho`/`__texto`, `dashboard-contabil.css`) — mesma ideia de `.info-tooltip`/`.info-tooltip__text` (components.css), só que com `white-space: pre-line` (preserva o `\n` deliberado entre a frase e "Valor antes do reprocessamento: ..." e ainda envolve o resto normalmente) em vez de `nowrap` (que só serve pro rótulo curto "Mais informações"). Não promovido pra components.css por enquanto — só usado aqui, mas a mecânica é genérica o bastante pra virar um componente compartilhado se outro pacote precisar do mesmo tipo de tooltip mais longo. `tabindex="0"` no gatilho + `:focus-visible`/`:hover` no CSS mostram o tooltip também via teclado, não só no hover do mouse. `pidDcAlteradaBadgeHtml()`/`pidDcFontePdfAtipicaBadgeHtml()` passaram a chamar essa função em vez de escrever `title="..."` direto no `<span>`.
|
||||||
|
|
||||||
|
**Bug real reportado pelo usuário logo depois: o balão saía cortado** (em cima da primeira linha da lista, e na lateral perto da borda da tabela) — `.dc-hover-tooltip__texto` nasceu `position:absolute`, mas os badges deste pacote vivem dentro de `.pa-table-wrap` (perfis-acesso.css), que tem `overflow:hidden` pra arredondar o canto da tabela; qualquer coisa `absolute` que escape da própria tabela é cortada por esse `overflow`. Corrigido trocando pra `position:fixed` (relativo à viewport, não mais preso pelo `overflow` de nenhum ancestral) com `top`/`left` calculados em JS (`pidDcPosicionaTooltip()`, topo de `dashboard-contabil.js`) no `mouseenter`/`focus` do gatilho — centralizado acima por padrão, desce pra baixo quando não tem espaço acima (badge perto do topo da tela), clampado nas duas laterais pra nunca vazar da tela (badge perto da borda da tabela). Delegado no `document` com `capture:true` (`mouseenter`/`focus` não borbulham) — um único par de listeners, registrado uma vez no carregamento do script, cobre todo badge atual e futuro, mesmo os recriados a cada re-render de `renderContas()`/`renderDre()`/`renderAnaliseVertical()`/`carregarLista()`.
|
||||||
|
|
||||||
**Validado com os 3 testes reais**: (1) reprocessar com o **mesmo** arquivo duas vezes seguidas — `validado`/`observacao` de conta/DRE/Análise Vertical preservados, `alterada_reprocessamento` continua `False` em tudo, contagem de linhas idêntica; (2) tentar reprocessar com o arquivo de **outra empresa** — 400 com mensagem explicando a diferença de empresa/competência; (3) tentar reprocessar uma apuração **Concluída** — 400 bloqueado por `_contabil_garante_em_revisao()`. As 4 funções de sincronização também foram testadas isoladamente (dry-run com `transaction.atomic()` + rollback forçado, dados fabricados cobrindo conta/linha inalterada, alterada, nova e removida, mais achado que continua disparando e achado que para de disparar) — todos os casos bateram com o comportamento esperado antes de considerar a implementação pronta. Nenhum resíduo deixado em produção.
|
**Validado com os 3 testes reais**: (1) reprocessar com o **mesmo** arquivo duas vezes seguidas — `validado`/`observacao` de conta/DRE/Análise Vertical preservados, `alterada_reprocessamento` continua `False` em tudo, contagem de linhas idêntica; (2) tentar reprocessar com o arquivo de **outra empresa** — 400 com mensagem explicando a diferença de empresa/competência; (3) tentar reprocessar uma apuração **Concluída** — 400 bloqueado por `_contabil_garante_em_revisao()`. As 4 funções de sincronização também foram testadas isoladamente (dry-run com `transaction.atomic()` + rollback forçado, dados fabricados cobrindo conta/linha inalterada, alterada, nova e removida, mais achado que continua disparando e achado que para de disparar) — todos os casos bateram com o comportamento esperado antes de considerar a implementação pronta. Nenhum resíduo deixado em produção.
|
||||||
|
|
||||||
@ -363,6 +367,33 @@ Pedido explícito do usuário: até aqui, corrigir uma apuração com o PDF erra
|
|||||||
|
|
||||||
**Solução adotada — avisar, não tentar corrigir automaticamente** (pedido explícito do usuário, depois de rejeitar a ideia inicial de um botão de lápis pra editar a descrição manualmente — suspenso por enquanto): `ContabilApuracao.fonte_pdf_atipica` (`BooleanField`, migração `0070`) é `True` quando `_normaliza_titulo()` precisou de verdade (o título bateu sem acento mas não bateria com acento) pra reconhecer a seção DRE ou Análise Vertical — sinal indireto mas real de que este PDF usa uma fonte diferente da do relatório de referência, a mesma classe de variação que já se provou capaz de grudar palavras em descrição de conta. `ResultadoExtracao.fonte_pdf_atipica` (novo campo no dataclass, `dashboard_contabil/modelos.py`) carrega o valor calculado em `extrai_balancete_dre()`; `ContabilApuracaoViewSet.create()`/`.reprocessar()` persistem no model. Exposto em `ContabilApuracaoListSerializer`/`ContabilApuracaoDetailSerializer` (`fonte_pdf_atipica`, sem rota de escrita — `http_method_names` do ViewSet nem inclui PATCH/PUT).
|
**Solução adotada — avisar, não tentar corrigir automaticamente** (pedido explícito do usuário, depois de rejeitar a ideia inicial de um botão de lápis pra editar a descrição manualmente — suspenso por enquanto): `ContabilApuracao.fonte_pdf_atipica` (`BooleanField`, migração `0070`) é `True` quando `_normaliza_titulo()` precisou de verdade (o título bateu sem acento mas não bateria com acento) pra reconhecer a seção DRE ou Análise Vertical — sinal indireto mas real de que este PDF usa uma fonte diferente da do relatório de referência, a mesma classe de variação que já se provou capaz de grudar palavras em descrição de conta. `ResultadoExtracao.fonte_pdf_atipica` (novo campo no dataclass, `dashboard_contabil/modelos.py`) carrega o valor calculado em `extrai_balancete_dre()`; `ContabilApuracaoViewSet.create()`/`.reprocessar()` persistem no model. Exposto em `ContabilApuracaoListSerializer`/`ContabilApuracaoDetailSerializer` (`fonte_pdf_atipica`, sem rota de escrita — `http_method_names` do ViewSet nem inclui PATCH/PUT).
|
||||||
|
|
||||||
**Frontend**: `pidDcFontePdfAtipicaBadgeHtml()` (`dashboard-contabil.js`) — ícone de "i" (mesma forma de `PID_DC_OBSERVACAO_ICONE`, cor `--gold` pra diferenciar visualmente, os dois nunca aparecem lado a lado) ao lado do nome da empresa, tanto na linha da lista quanto no cabeçalho `#dc-review-empresa` da tela de revisão (que passou de `.textContent` pra `.innerHTML`, escapando `codigo_empresa`/`nome_empresa` manualmente com `pidDcEscapeHtml()` já que precisa comportar HTML agora). Tooltip via `title` explica o motivo e pede pra conferir os nomes de conta com atenção — puramente informativo, não bloqueia nem oculta nada.
|
**Frontend**: `pidDcFontePdfAtipicaBadgeHtml()` (`dashboard-contabil.js`) — ícone de "i" (mesma forma de `PID_DC_OBSERVACAO_ICONE`, cor `--gold` pra diferenciar visualmente, os dois nunca aparecem lado a lado) ao lado do nome da empresa, tanto na linha da lista quanto no cabeçalho `#dc-review-empresa` da tela de revisão (que passou de `.textContent` pra `.innerHTML`, escapando `codigo_empresa`/`nome_empresa` manualmente com `pidDcEscapeHtml()` já que precisa comportar HTML agora). Tooltip customizado (`pidDcHoverTooltipHtml()`, ver "Tooltip customizado no visual do Portal" acima) explica o motivo e pede pra conferir os nomes de conta com atenção — puramente informativo, não bloqueia nem oculta nada.
|
||||||
|
|
||||||
**Validado contra os 3 arquivos reais disponíveis**: `fonte_pdf_atipica` calculado `True` só pro `1751` (o PDF com o problema), `False` pros PDFs de referência já validados (`792`, `2017`) — sem falso positivo nos dois já confirmados corretos, e a extração completa do `1751` (252 contas, 231 linhas de DRE, Análise Vertical com 3 meses) bate exatamente com o total esperado depois do fix de `_normaliza_titulo()`.
|
**Validado contra os 3 arquivos reais disponíveis**: `fonte_pdf_atipica` calculado `True` só pro `1751` (o PDF com o problema), `False` pros PDFs de referência já validados (`792`, `2017`) — sem falso positivo nos dois já confirmados corretos, e a extração completa do `1751` (252 contas, 231 linhas de DRE, Análise Vertical com 3 meses) bate exatamente com o total esperado depois do fix de `_normaliza_titulo()`.
|
||||||
|
|
||||||
|
### Observação virou histórico por empresa+conta (rodada 126)
|
||||||
|
|
||||||
|
Pedido explícito do usuário: uma observação registrada num mês (o exemplo dele foi um ajuste de estoque) precisa reaparecer na análise do mês seguinte, assinada por quem escreveu e com a data, **bloqueada pra edição** por ser registro histórico, com três caminhos (manter o histórico — padrão; ocultar das próximas execuções; incluir uma observação nova) e filtro de visibilidade ao cliente em todas elas. Até aqui a observação era um campo da linha (`ContabilConta.observacao`/`oculta_no_relatorio` e equivalentes na DRE/Análise Vertical), então morria junto com a competência.
|
||||||
|
|
||||||
|
Três decisões de escopo confirmadas por `AskUserQuestion` **antes** de implementar, todas com a opção recomendada aceita:
|
||||||
|
1. **Toda observação propaga por padrão** — não existe "fixar"; o que existe é o inverso, encerrar explicitamente. Evita histórico que só existe quando alguém lembra de marcar.
|
||||||
|
2. **O histórico cobre Balancete/D.R.E./Análise Vertical** — a justificativa de tratativa de um item de auditoria (`ContabilAchado.observacao_contador`) continua presa à apuração como sempre foi (ela já tem histórico próprio, nunca é apagada nem sobrescrita, ver "Models" acima). Misturar os dois fluxos aumentaria o escopo sem ganho claro.
|
||||||
|
3. **`mostrar_ao_cliente` é sempre alternável**, inclusive numa observação já travada — o bloqueio protege texto, autor e data; mostrar ou não ao cliente é decisão editorial de cada relatório, e uma marcação errada precisa ser corrigível sem reescrever o histórico.
|
||||||
|
|
||||||
|
**Model `ContabilObservacao`** (`portal_api/models.py`, migração `0071`): escopo `codigo_empresa` + `alvo_tipo` (`conta`/`dre`/`analise_vertical`) + `alvo_chave`, mais `alvo_rotulo` (descrição no momento em que foi escrita, só pra exibir se aquela conta sumir do plano), `apuracao_origem` (`SET_NULL`) + `competencia_origem` (cópia, pra vigência continuar resolvendo se a apuração for excluída), `texto`, `mostrar_ao_cliente` (substitui `oculta_no_relatorio`, com o sinal invertido pra bater com o rótulo que o contador vê), `criado_por`/`criado_em` e o trio `encerrada_em_competencia`/`encerrada_por`/`encerrada_em`.
|
||||||
|
|
||||||
|
**Chave natural, nunca FK pra linha**: `codigo` no Balancete, `"descricao|nivel"` na DRE/Análise Vertical (`chave_conta()`/`chave_linha()` no model, `_contabil_chave_alvo()` na view) — exatamente as chaves que `_contabil_sincroniza_*()` já usa no reprocessamento e que `regras.py` usa no histórico de variação. É o que faz a observação seguir a mesma conta de uma competência pra outra, e de brinde tira qualquer risco do reprocessamento sobre ela (antes era preciso garantir explicitamente que `observacao` não fosse tocada; agora ela nem mora lá).
|
||||||
|
|
||||||
|
**Vigência** (`vigentes_para()`/`vigente_em()`): aparece em toda apuração da mesma empresa com `competencia_origem <= C` e (`encerrada_em_competencia` nulo ou `>= C`). Daí saem os três caminhos pedidos: manter é não fazer nada; encerrar grava a competência aberta (a observação **continua visível nela** e some da seguinte em diante, o histórico nunca é reescrito); incluir uma nova cria outro registro, então uma conta passa a ter uma thread, não um texto único.
|
||||||
|
|
||||||
|
**Imutabilidade** (`ContabilObservacaoViewSet._garante_texto_editavel()`): `texto` só é aceito enquanto a apuração de origem estiver "Em revisão"; depois disso (ou numa competência posterior, onde o frontend já mostra a observação travada) a edição é recusada com 400, pedindo pra registrar uma observação nova ou encerrar a existente. `DELETE` segue a mesma regra: histórico não se apaga, se encerra. A regra do backend olha o status da apuração de origem, e a da tela olha `historica` (calculado contra a competência aberta) — as duas convergem no uso real; a diferença só apareceria se alguém editasse pela API uma observação de um mês ainda em revisão estando com outro mês aberto na tela.
|
||||||
|
|
||||||
|
**Endpoints** (mesma permissão de toggle único do resto da ferramenta): `GET /api/contabil-apuracoes/{id}/observacoes/` (recorte de vigência já serializado com `historica`/`encerrada` calculados contra a competência da apuração), `POST /api/contabil-observacoes/` (recebe `apuracao` + `alvo_tipo` + `alvo_id`, o id da conta/linha **desta** apuração — empresa, competência, chave e rótulo são derivados no servidor, mesmo espírito da `chave` derivada em `IndicadorContabilDefinicao`; alvo de outra apuração é 400), `PATCH` (aceita `texto` e/ou `mostrar_ao_cliente`, cada um com sua regra), `DELETE`, e as actions `encerrar`/`reativar` (as duas recebem a apuração aberta no corpo, já que é ela quem define a competência de corte). O viewset não tem `list`/`retrieve` de propósito: a leitura é sempre pelo recorte de vigência de uma apuração.
|
||||||
|
|
||||||
|
**Relatório "Gerar Dashboard"**: `dashboard()` monta as observações vigentes com `mostrar_ao_cliente=True` uma vez e as usa em dois lugares — penduradas em cada linha da árvore (`_contabil_arvore_contexto()` ganhou os parâmetros opcionais `observacoes_por_chave`/`chave_fn`), pro ícone/painel inline por conta, e nas listas `observacoes_contas`/`observacoes_dre`/`observacoes_analise_vertical`, que agora são listas de `ContabilObservacao` (não mais de contas/linhas). Cada item mostra a assinatura e, quando a observação vem de um mês anterior, a competência em que foi registrada.
|
||||||
|
|
||||||
|
**Frontend** (`dashboard-contabil.js`): as observações são carregadas à parte da apuração (`dcCarregarObservacoes()`, chamada ao abrir/criar uma análise) e indexadas por chave natural (`dcObsIndice`), porque não pertencem ao payload da apuração. O editor inline virou uma thread (`dcObsPainelHtml()`/`dcObsItemHtml()`): histórico em cima (autor, data, competência de origem, selos "Histórico"/"Encerrada"/"Aparece ao cliente"/"Interna" e ações de olho, editar, excluir, encerrar/reativar), campo de observação nova embaixo. Um handler único (`dcTrataCliqueObservacao()`) atende as três tabelas e as quatro listas de resumo, e cada mutação refaz o fetch e re-renderiza tudo (`dcRenderObservacoesTudo()`) — a mesma observação pode estar visível em mais de um lugar ao mesmo tempo. O botão da coluna "Observação" ganhou um contador (`.dc-obs-contador`), já que uma conta pode ter várias. Os chips "Todas / Visíveis ao cliente / Internas" (`dcObsFiltro`) existem nas quatro listas e compartilham a mesma variável: filtrar numa aba filtra em todas.
|
||||||
|
|
||||||
|
**Migração de dados**: a `0071` cria o model, copia cada observação preenchida das três tabelas (autor e data vêm da apuração, a melhor aproximação disponível — o modelo antigo não guardava nada disso por observação; `oculta_no_relatorio` vira `mostrar_ao_cliente` invertido) e só então remove os seis campos antigos. Em produção eram 3 observações (2 de conta, 1 de DRE), todas migradas e conferidas depois de aplicar.
|
||||||
|
|
||||||
|
**Testado ponta a ponta via `Client.force_login()`** dentro de uma transação com rollback forçado: listagem por vigência, criação com chave derivada no servidor, alvo de outra apuração recusado, edição de texto, bloqueio do texto quando a apuração de origem está concluída (com a visibilidade ainda alternável nesse mesmo caso), encerrar e reativar com as fronteiras de competência conferidas nos dois sentidos, herança numa competência seguinte (observação marcada como histórica) e o relatório gerado nos dois meses, incluindo a conferência de que observação interna não vaza pro relatório do cliente. Nada gravado em produção além da própria migração.
|
||||||
|
|||||||
@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
> Ver `CLAUDE.md` nesta mesma pasta para o detalhamento técnico (extração do PDF, regras de auditoria, models). Ver `CLAUDE.md` na raiz para arquitetura geral/transversal do Portal. Chamado de "Dashboard Contábil" até uma rodada anterior — renomeado a pedido do usuário (rótulo visível só, nomes técnicos internos continuam `dashboard_contabil`/`Contabil*`).
|
> Ver `CLAUDE.md` nesta mesma pasta para o detalhamento técnico (extração do PDF, regras de auditoria, models). Ver `CLAUDE.md` na raiz para arquitetura geral/transversal do Portal. Chamado de "Dashboard Contábil" até uma rodada anterior — renomeado a pedido do usuário (rótulo visível só, nomes técnicos internos continuam `dashboard_contabil`/`Contabil*`).
|
||||||
|
|
||||||
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 modelo Questor hoje enviado ao cliente), 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, variações atípicas mês a mês) — o contador revisa os achados, marca contas/linhas como validadas, registra observações por conta/linha (editor 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. 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, gráfico de evolução, DRE/Balancete/Análise Vertical agrupados, observações do contador) com a marca do escritório, pronto pra enviar ao administrador da empresa.
|
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 modelo Questor hoje enviado ao cliente), 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, variações atípicas mês a mês) — 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, gráfico de evolução, DRE/Balancete/Análise Vertical agrupados, observações do contador) com a marca do escritório, pronto pra enviar ao administrador da empresa.
|
||||||
|
|
||||||
**Ainda fora de escopo**: exportação em XLSX; consolidação entre várias empresas/competências ao mesmo tempo (é sempre uma apuração por vez).
|
**Ainda fora de escopo**: exportação em XLSX; consolidação entre várias empresas/competências ao mesmo tempo (é sempre uma apuração por vez).
|
||||||
|
|
||||||
## Onde mexer
|
## Onde mexer
|
||||||
|
|
||||||
- `portal_api/dashboard_contabil/` — `parser.py` (extração do PDF via `pdfplumber`), `regras.py` (motor de regras de auditoria), `indicadores.py` (indicadores financeiros do relatório), `pipeline.py` (orquestração), `modelos.py` (dataclasses).
|
- `portal_api/dashboard_contabil/` — `parser.py` (extração do PDF via `pdfplumber`), `regras.py` (motor de regras de auditoria), `indicadores.py` (indicadores financeiros do relatório), `pipeline.py` (orquestração), `modelos.py` (dataclasses).
|
||||||
- `ContabilApuracao`/`ContabilConta`/`ContabilLinhaDre`/`ContabilLinhaAnaliseVertical`/`ContabilAchado` (`portal_api/models.py`).
|
- `ContabilApuracao`/`ContabilConta`/`ContabilLinhaDre`/`ContabilLinhaAnaliseVertical`/`ContabilAchado`/`ContabilObservacao` (`portal_api/models.py`).
|
||||||
- `dashboard-contabil.html` / `static/js/dashboard-contabil.js` / `static/css/dashboard-contabil.css`.
|
- `dashboard-contabil.html` / `static/js/dashboard-contabil.js` / `static/css/dashboard-contabil.css`.
|
||||||
- `dashboard-contabil-relatorio.html` (relatório em si) / `portal_api/templatetags/contabil_extras.py` (filtros de formatação).
|
- `dashboard-contabil-relatorio.html` (relatório em si) / `portal_api/templatetags/contabil_extras.py` (filtros de formatação).
|
||||||
|
|||||||
134
portal_api/migrations/0071_contabil_observacao_historico.py
Normal file
134
portal_api/migrations/0071_contabil_observacao_historico.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
# Generated by Django 6.0.7 on 2026-09-11 19:00
|
||||||
|
#
|
||||||
|
# Observação do Relatório Contábil deixou de ser um campo da linha
|
||||||
|
# (ContabilConta/ContabilLinhaDre/ContabilLinhaAnaliseVertical, onde morria
|
||||||
|
# junto com a competência) e virou um registro próprio, por empresa+conta,
|
||||||
|
# que atravessa competências — ver `ContabilObservacao` em models.py.
|
||||||
|
#
|
||||||
|
# Ordem das operações importa: cria o model novo, copia o que já existe
|
||||||
|
# (RunPython) e só então remove os campos antigos.
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def copia_observacoes(apps, schema_editor):
|
||||||
|
"""Cada `observacao` preenchida de conta/DRE/Análise Vertical vira um
|
||||||
|
`ContabilObservacao` da competência da própria apuração. Não existe
|
||||||
|
histórico de autoria/data por observação no modelo antigo, então o autor
|
||||||
|
e a data são os da apuração (quem criou a análise, quando criou) — a
|
||||||
|
melhor aproximação disponível; `oculta_no_relatorio` vira
|
||||||
|
`mostrar_ao_cliente` com o sinal invertido."""
|
||||||
|
ContabilConta = apps.get_model("portal_api", "ContabilConta")
|
||||||
|
ContabilLinhaDre = apps.get_model("portal_api", "ContabilLinhaDre")
|
||||||
|
ContabilLinhaAnaliseVertical = apps.get_model("portal_api", "ContabilLinhaAnaliseVertical")
|
||||||
|
ContabilObservacao = apps.get_model("portal_api", "ContabilObservacao")
|
||||||
|
|
||||||
|
novas = []
|
||||||
|
datas = []
|
||||||
|
|
||||||
|
for conta in ContabilConta.objects.exclude(observacao="").select_related("apuracao"):
|
||||||
|
apuracao = conta.apuracao
|
||||||
|
novas.append(
|
||||||
|
ContabilObservacao(
|
||||||
|
codigo_empresa=apuracao.codigo_empresa,
|
||||||
|
alvo_tipo="conta",
|
||||||
|
alvo_chave=conta.codigo,
|
||||||
|
alvo_rotulo=conta.descricao,
|
||||||
|
apuracao_origem=apuracao,
|
||||||
|
competencia_origem=apuracao.competencia,
|
||||||
|
texto=conta.observacao,
|
||||||
|
mostrar_ao_cliente=not conta.oculta_no_relatorio,
|
||||||
|
criado_por=apuracao.criado_por,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
datas.append(apuracao.criado_em)
|
||||||
|
|
||||||
|
for model, alvo_tipo in (
|
||||||
|
(ContabilLinhaDre, "dre"),
|
||||||
|
(ContabilLinhaAnaliseVertical, "analise_vertical"),
|
||||||
|
):
|
||||||
|
for linha in model.objects.exclude(observacao="").select_related("apuracao"):
|
||||||
|
apuracao = linha.apuracao
|
||||||
|
novas.append(
|
||||||
|
ContabilObservacao(
|
||||||
|
codigo_empresa=apuracao.codigo_empresa,
|
||||||
|
alvo_tipo=alvo_tipo,
|
||||||
|
alvo_chave=f"{linha.descricao}|{linha.nivel}",
|
||||||
|
alvo_rotulo=linha.descricao,
|
||||||
|
apuracao_origem=apuracao,
|
||||||
|
competencia_origem=apuracao.competencia,
|
||||||
|
texto=linha.observacao,
|
||||||
|
mostrar_ao_cliente=not linha.oculta_no_relatorio,
|
||||||
|
criado_por=apuracao.criado_por,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
datas.append(apuracao.criado_em)
|
||||||
|
|
||||||
|
criadas = ContabilObservacao.objects.bulk_create(novas)
|
||||||
|
# `criado_em` é auto_now_add — só um UPDATE depois consegue gravar a data
|
||||||
|
# da apuração de origem no lugar do "agora" desta migração.
|
||||||
|
for observacao, data in zip(criadas, datas):
|
||||||
|
ContabilObservacao.objects.filter(pk=observacao.pk).update(criado_em=data)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('portal_api', '0070_contabil_fonte_pdf_atipica'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ContabilObservacao',
|
||||||
|
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=320, 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.contabilapuracao')),
|
||||||
|
('criado_por', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observacoes_contabeis', 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', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Observação do Relatório Contábil',
|
||||||
|
'verbose_name_plural': 'Observações do Relatório Contábil',
|
||||||
|
'ordering': ['competencia_origem', 'criado_em', 'id'],
|
||||||
|
'indexes': [models.Index(fields=['codigo_empresa', 'alvo_tipo', 'alvo_chave'], name='contabil_obs_alvo_idx')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RunPython(copia_observacoes, migrations.RunPython.noop),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='contabilconta',
|
||||||
|
name='observacao',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='contabilconta',
|
||||||
|
name='oculta_no_relatorio',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='contabillinhaanalisevertical',
|
||||||
|
name='observacao',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='contabillinhaanalisevertical',
|
||||||
|
name='oculta_no_relatorio',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='contabillinhadre',
|
||||||
|
name='observacao',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='contabillinhadre',
|
||||||
|
name='oculta_no_relatorio',
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -2069,14 +2069,10 @@ class ContabilConta(models.Model):
|
|||||||
debito = models.DecimalField(max_digits=16, decimal_places=2)
|
debito = models.DecimalField(max_digits=16, decimal_places=2)
|
||||||
credito = models.DecimalField(max_digits=16, decimal_places=2)
|
credito = models.DecimalField(max_digits=16, decimal_places=2)
|
||||||
saldo_atual = models.DecimalField(max_digits=16, decimal_places=2)
|
saldo_atual = models.DecimalField(max_digits=16, decimal_places=2)
|
||||||
observacao = models.TextField("Observação", blank=True)
|
# A observação do contador NÃO mora mais aqui — virou `ContabilObservacao`
|
||||||
# Não afeta a linha do Balancete em si (continua listada normalmente) —
|
# (histórico por empresa+conta que atravessa competências, ver o model
|
||||||
# só remove a observação da seção "Observações" do relatório "Gerar
|
# abaixo). Os campos `observacao`/`oculta_no_relatorio` que existiam aqui
|
||||||
# Dashboard" (ver aba "Dashboard" da tela de revisão). Default True
|
# foram migrados e removidos na migração `0071`.
|
||||||
# (oculta) — pedido explícito do usuário: uma observação nova só vai pro
|
|
||||||
# relatório do cliente depois que o contador confirmar isso explicitamente
|
|
||||||
# no editor inline da tabela, nunca por padrão.
|
|
||||||
oculta_no_relatorio = models.BooleanField("Observação oculta no relatório", default=True)
|
|
||||||
# Checkbox de "já revisei esta conta" — puramente informativo (não afeta
|
# Checkbox de "já revisei esta conta" — puramente informativo (não afeta
|
||||||
# achados/status/relatório), pedido explícito do usuário como um segundo
|
# achados/status/relatório), pedido explícito do usuário como um segundo
|
||||||
# botão ao lado do de observação, pra marcar contas já conferidas durante
|
# botão ao lado do de observação, pra marcar contas já conferidas durante
|
||||||
@ -2092,8 +2088,8 @@ class ContabilConta(models.Model):
|
|||||||
# cor, pra dar pra identificar depois quais itens já foram reprocessados
|
# cor, pra dar pra identificar depois quais itens já foram reprocessados
|
||||||
# E revalidados; só um próximo reprocessamento sem mudança nesta conta
|
# E revalidados; só um próximo reprocessamento sem mudança nesta conta
|
||||||
# específica limpa de vez. Não afeta contas sem mudança nenhuma (essas
|
# específica limpa de vez. Não afeta contas sem mudança nenhuma (essas
|
||||||
# mantêm `validado` e `observacao` como estavam, ver "Reprocessar" no
|
# mantêm `validado` como estava, ver "Reprocessar" no CLAUDE.md do
|
||||||
# CLAUDE.md do pacote).
|
# pacote; observação nunca é afetada, mora em `ContabilObservacao`).
|
||||||
alterada_reprocessamento = models.BooleanField("Alterada no último reprocessamento", default=False)
|
alterada_reprocessamento = models.BooleanField("Alterada no último reprocessamento", default=False)
|
||||||
# Cópia de `saldo_atual` de ANTES do reprocessamento que ligou
|
# Cópia de `saldo_atual` de ANTES do reprocessamento que ligou
|
||||||
# `alterada_reprocessamento` — só existe pra alimentar o tooltip do
|
# `alterada_reprocessamento` — só existe pra alimentar o tooltip do
|
||||||
@ -2126,11 +2122,7 @@ class ContabilLinhaDre(models.Model):
|
|||||||
nivel = models.IntegerField("Nível de indentação", default=0)
|
nivel = models.IntegerField("Nível de indentação", default=0)
|
||||||
valor = models.DecimalField(max_digits=16, decimal_places=2)
|
valor = models.DecimalField(max_digits=16, decimal_places=2)
|
||||||
totalizador = models.BooleanField("Linha totalizadora", default=False)
|
totalizador = models.BooleanField("Linha totalizadora", default=False)
|
||||||
observacao = models.TextField("Observação", blank=True)
|
# Observação virou `ContabilObservacao` — ver a nota em `ContabilConta`.
|
||||||
# Mesmo espírito de ContabilConta.oculta_no_relatorio — só afeta a
|
|
||||||
# seção "Observações" do relatório "Gerar Dashboard". Default True
|
|
||||||
# (oculta), mesmo motivo documentado lá.
|
|
||||||
oculta_no_relatorio = models.BooleanField("Observação oculta no relatório", default=True)
|
|
||||||
# Mesmo espírito de ContabilConta.validado.
|
# Mesmo espírito de ContabilConta.validado.
|
||||||
validado = models.BooleanField("Validado pelo contador", default=False)
|
validado = models.BooleanField("Validado pelo contador", default=False)
|
||||||
# Mesmo espírito de ContabilConta.alterada_reprocessamento.
|
# Mesmo espírito de ContabilConta.alterada_reprocessamento.
|
||||||
@ -2166,9 +2158,8 @@ class ContabilLinhaAnaliseVertical(models.Model):
|
|||||||
nivel = models.IntegerField("Nível de indentação", default=0)
|
nivel = models.IntegerField("Nível de indentação", default=0)
|
||||||
totalizador = models.BooleanField("Linha totalizadora", default=False)
|
totalizador = models.BooleanField("Linha totalizadora", default=False)
|
||||||
valores = models.JSONField("Valores mensais", default=list)
|
valores = models.JSONField("Valores mensais", default=list)
|
||||||
observacao = models.TextField("Observação", blank=True)
|
# Observação virou `ContabilObservacao` — ver a nota em `ContabilConta`.
|
||||||
# Mesmo espírito de ContabilLinhaDre.oculta_no_relatorio/.validado.
|
# Mesmo espírito de ContabilLinhaDre.validado.
|
||||||
oculta_no_relatorio = models.BooleanField("Observação oculta no relatório", default=True)
|
|
||||||
validado = models.BooleanField("Validado pelo contador", default=False)
|
validado = models.BooleanField("Validado pelo contador", default=False)
|
||||||
# Mesmo espírito de ContabilConta.alterada_reprocessamento.
|
# Mesmo espírito de ContabilConta.alterada_reprocessamento.
|
||||||
alterada_reprocessamento = models.BooleanField("Alterada no último reprocessamento", default=False)
|
alterada_reprocessamento = models.BooleanField("Alterada no último reprocessamento", default=False)
|
||||||
@ -2242,6 +2233,129 @@ class ContabilAchado(models.Model):
|
|||||||
return f"{self.titulo} ({self.get_status_display()})"
|
return f"{self.titulo} ({self.get_status_display()})"
|
||||||
|
|
||||||
|
|
||||||
|
class ContabilObservacao(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
|
||||||
|
explícito do usuário: "se eu fizer uma observação sobre o estoque do
|
||||||
|
cliente... no mês seguinte este histórico deve aparecer, informando que
|
||||||
|
fui eu quem fiz, no dia e data tal"). Substituiu os campos
|
||||||
|
`observacao`/`oculta_no_relatorio` que viviam em `ContabilConta`/
|
||||||
|
`ContabilLinhaDre`/`ContabilLinhaAnaliseVertical` (migração `0071`),
|
||||||
|
onde morriam junto com a competência.
|
||||||
|
|
||||||
|
O alvo é guardado por **chave natural**, nunca por FK a uma linha de uma
|
||||||
|
apuração específica (uma linha é recriada/ressincronizada a cada
|
||||||
|
apuração/reprocessamento): `codigo` de classificação no Balancete e
|
||||||
|
`"descricao|nivel"` na DRE/Análise Vertical — as mesmas chaves já usadas
|
||||||
|
por `_contabil_sincroniza_*()` em views.py e pelo histórico de variação
|
||||||
|
em `regras.py`. Efeito colateral bem-vindo: reprocessar uma apuração não
|
||||||
|
toca em observação nenhuma, já que elas não moram mais na linha que é
|
||||||
|
resincronizada.
|
||||||
|
|
||||||
|
**Vigência** (ver `vigentes_para()`): aparece em toda apuração da mesma
|
||||||
|
empresa com competência entre `competencia_origem` e
|
||||||
|
`encerrada_em_competencia` (inclusive nas duas pontas; `None` = vigente
|
||||||
|
pra sempre). "Manter o histórico" é o padrão (nada a fazer), "ocultar das
|
||||||
|
próximas execuções" grava `encerrada_em_competencia` com a competência
|
||||||
|
em que o contador encerrou, e "incluir uma nova observação" cria outro
|
||||||
|
registro — nunca reescreve um já existente.
|
||||||
|
|
||||||
|
**Imutabilidade**: `texto` só pode ser editado enquanto a apuração de
|
||||||
|
origem estiver "Em revisão" (ver `ContabilObservacaoViewSet`); numa
|
||||||
|
competência posterior a observação é histórica, somente leitura, com
|
||||||
|
autor/data à vista. `mostrar_ao_cliente` é a exceção deliberada, sempre
|
||||||
|
alternável (decisão confirmada com o usuário): o bloqueio protege texto,
|
||||||
|
autor e data, mas mostrar ou não ao cliente é uma decisão editorial de
|
||||||
|
cada relatório."""
|
||||||
|
|
||||||
|
ALVO_CONTA = "conta"
|
||||||
|
ALVO_DRE = "dre"
|
||||||
|
ALVO_ANALISE_VERTICAL = "analise_vertical"
|
||||||
|
ALVO_CHOICES = [
|
||||||
|
(ALVO_CONTA, "Conta do Balancete"),
|
||||||
|
(ALVO_DRE, "Linha da DRE"),
|
||||||
|
(ALVO_ANALISE_VERTICAL, "Linha da Análise Vertical"),
|
||||||
|
]
|
||||||
|
|
||||||
|
codigo_empresa = models.CharField("Código da empresa", max_length=20)
|
||||||
|
alvo_tipo = models.CharField("Tipo do alvo", max_length=20, choices=ALVO_CHOICES)
|
||||||
|
alvo_chave = models.CharField("Chave natural do alvo", max_length=320)
|
||||||
|
# Descrição da conta/linha no momento em que a observação foi escrita —
|
||||||
|
# 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"
|
||||||
|
)
|
||||||
|
competencia_origem = models.DateField("Competência de origem")
|
||||||
|
texto = models.TextField("Observação")
|
||||||
|
# Substitui o antigo `oculta_no_relatorio` das linhas, com o sinal
|
||||||
|
# invertido pra ficar igual ao rótulo que o contador vê no editor
|
||||||
|
# ("Mostrar esta observação ao cliente no relatório"). Nasce desmarcado,
|
||||||
|
# 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.
|
||||||
|
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"
|
||||||
|
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})"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def chave_conta(codigo: str) -> str:
|
||||||
|
"""Chave natural de uma conta do Balancete — mesmo critério de
|
||||||
|
`_contabil_sincroniza_contas()` (casa por `codigo` de classificação)."""
|
||||||
|
return codigo
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def chave_linha(descricao: str, nivel: int) -> str:
|
||||||
|
"""Chave natural de uma linha da DRE/Análise Vertical — o par
|
||||||
|
`(descricao, nivel)` de `_contabil_sincroniza_linhas_dre()`, que
|
||||||
|
desambigua descrições repetidas em ramos diferentes da árvore."""
|
||||||
|
return f"{descricao}|{nivel}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def vigentes_para(cls, codigo_empresa: str, competencia: date) -> models.QuerySet["ContabilObservacao"]:
|
||||||
|
return cls.objects.filter(
|
||||||
|
codigo_empresa=codigo_empresa, competencia_origem__lte=competencia
|
||||||
|
).filter(models.Q(encerrada_em_competencia__isnull=True) | models.Q(encerrada_em_competencia__gte=competencia))
|
||||||
|
|
||||||
|
def vigente_em(self, competencia: date) -> bool:
|
||||||
|
if self.competencia_origem > competencia:
|
||||||
|
return False
|
||||||
|
return self.encerrada_em_competencia is None or self.encerrada_em_competencia >= competencia
|
||||||
|
|
||||||
|
def eh_historica_em(self, competencia: date) -> bool:
|
||||||
|
"""Observação de uma competência anterior à que está aberta — texto,
|
||||||
|
autor e data ficam travados (só dá pra encerrar ou responder com uma
|
||||||
|
observação nova)."""
|
||||||
|
return self.competencia_origem < competencia
|
||||||
|
|
||||||
|
|
||||||
class IndicadorContabilDefinicao(models.Model):
|
class IndicadorContabilDefinicao(models.Model):
|
||||||
"""Um indicador financeiro do Relatório Contábil — todo indicador é um
|
"""Um indicador financeiro do Relatório Contábil — todo indicador é um
|
||||||
registro aqui agora, inclusive os antigos 11 "de sistema" (ROA/ROE/
|
registro aqui agora, inclusive os antigos 11 "de sistema" (ROA/ROE/
|
||||||
|
|||||||
@ -21,6 +21,7 @@ from .models import (
|
|||||||
ContabilConta,
|
ContabilConta,
|
||||||
ContabilLinhaAnaliseVertical,
|
ContabilLinhaAnaliseVertical,
|
||||||
ContabilLinhaDre,
|
ContabilLinhaDre,
|
||||||
|
ContabilObservacao,
|
||||||
Departamento,
|
Departamento,
|
||||||
EmpresaQuestor,
|
EmpresaQuestor,
|
||||||
Favorito,
|
Favorito,
|
||||||
@ -2246,8 +2247,6 @@ class ContabilContaSerializer(serializers.ModelSerializer):
|
|||||||
"debito",
|
"debito",
|
||||||
"credito",
|
"credito",
|
||||||
"saldo_atual",
|
"saldo_atual",
|
||||||
"observacao",
|
|
||||||
"oculta_no_relatorio",
|
|
||||||
"validado",
|
"validado",
|
||||||
"alterada_reprocessamento",
|
"alterada_reprocessamento",
|
||||||
"valor_anterior_reprocessamento",
|
"valor_anterior_reprocessamento",
|
||||||
@ -2277,8 +2276,6 @@ class ContabilLinhaDreSerializer(serializers.ModelSerializer):
|
|||||||
"nivel",
|
"nivel",
|
||||||
"valor",
|
"valor",
|
||||||
"totalizador",
|
"totalizador",
|
||||||
"observacao",
|
|
||||||
"oculta_no_relatorio",
|
|
||||||
"validado",
|
"validado",
|
||||||
"alterada_reprocessamento",
|
"alterada_reprocessamento",
|
||||||
"valor_anterior_reprocessamento",
|
"valor_anterior_reprocessamento",
|
||||||
@ -2305,8 +2302,6 @@ class ContabilLinhaAnaliseVerticalSerializer(serializers.ModelSerializer):
|
|||||||
"nivel",
|
"nivel",
|
||||||
"totalizador",
|
"totalizador",
|
||||||
"valores",
|
"valores",
|
||||||
"observacao",
|
|
||||||
"oculta_no_relatorio",
|
|
||||||
"validado",
|
"validado",
|
||||||
"alterada_reprocessamento",
|
"alterada_reprocessamento",
|
||||||
"valores_anterior_reprocessamento",
|
"valores_anterior_reprocessamento",
|
||||||
@ -2323,6 +2318,88 @@ class ContabilLinhaAnaliseVerticalSerializer(serializers.ModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ContabilObservacaoSerializer(serializers.ModelSerializer):
|
||||||
|
"""Leitura de uma observação do histórico (ver `ContabilObservacao`).
|
||||||
|
`historica`/`encerrada` são derivados da competência da apuração que
|
||||||
|
está sendo exibida, passada em `context["competencia"]` — a mesma
|
||||||
|
observação é editável na apuração que a criou e somente leitura em
|
||||||
|
qualquer competência posterior."""
|
||||||
|
|
||||||
|
criado_por_nome = serializers.CharField(source="criado_por.nome", read_only=True, default=None)
|
||||||
|
encerrada_por_nome = serializers.CharField(source="encerrada_por.nome", read_only=True, default=None)
|
||||||
|
historica = serializers.SerializerMethodField()
|
||||||
|
encerrada = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ContabilObservacao
|
||||||
|
fields = [
|
||||||
|
"id",
|
||||||
|
"codigo_empresa",
|
||||||
|
"alvo_tipo",
|
||||||
|
"alvo_chave",
|
||||||
|
"alvo_rotulo",
|
||||||
|
"apuracao_origem",
|
||||||
|
"competencia_origem",
|
||||||
|
"texto",
|
||||||
|
"mostrar_ao_cliente",
|
||||||
|
"criado_por_nome",
|
||||||
|
"criado_em",
|
||||||
|
"encerrada_em_competencia",
|
||||||
|
"encerrada_por_nome",
|
||||||
|
"encerrada_em",
|
||||||
|
"historica",
|
||||||
|
"encerrada",
|
||||||
|
]
|
||||||
|
read_only_fields = fields
|
||||||
|
|
||||||
|
def get_historica(self, obj: ContabilObservacao) -> bool:
|
||||||
|
competencia = self.context.get("competencia")
|
||||||
|
return bool(competencia and obj.eh_historica_em(competencia))
|
||||||
|
|
||||||
|
def get_encerrada(self, obj: ContabilObservacao) -> bool:
|
||||||
|
return obj.encerrada_em_competencia is not None
|
||||||
|
|
||||||
|
|
||||||
|
class ContabilObservacaoCreateSerializer(serializers.Serializer):
|
||||||
|
"""Entrada de `ContabilObservacaoViewSet.create()` — o cliente informa a
|
||||||
|
apuração aberta e **qual linha dela** recebeu a observação (`alvo_id`, o
|
||||||
|
id da `ContabilConta`/`ContabilLinhaDre`/`ContabilLinhaAnaliseVertical`);
|
||||||
|
empresa, competência, chave natural e rótulo do alvo são derivados no
|
||||||
|
servidor a partir desse objeto, nunca aceitos do cliente (mesmo espírito
|
||||||
|
da `chave` derivada em `IndicadorContabilDefinicao`)."""
|
||||||
|
|
||||||
|
apuracao = serializers.PrimaryKeyRelatedField(queryset=ContabilApuracao.objects.all())
|
||||||
|
alvo_tipo = serializers.ChoiceField(choices=[opcao[0] for opcao in ContabilObservacao.ALVO_CHOICES])
|
||||||
|
alvo_id = serializers.IntegerField()
|
||||||
|
texto = serializers.CharField(allow_blank=False)
|
||||||
|
mostrar_ao_cliente = serializers.BooleanField(required=False, default=False)
|
||||||
|
|
||||||
|
|
||||||
|
class ContabilObservacaoUpdateSerializer(serializers.Serializer):
|
||||||
|
"""PATCH de uma observação — os dois campos são opcionais e independentes:
|
||||||
|
`texto` só é aceito enquanto a observação não for histórica (ver
|
||||||
|
`ContabilObservacaoViewSet.partial_update()`), `mostrar_ao_cliente` é
|
||||||
|
sempre aceito (decisão editorial de cada relatório, não faz parte do que
|
||||||
|
o histórico congela)."""
|
||||||
|
|
||||||
|
texto = serializers.CharField(allow_blank=False, required=False)
|
||||||
|
mostrar_ao_cliente = serializers.BooleanField(required=False)
|
||||||
|
|
||||||
|
def validate(self, attrs: dict) -> dict:
|
||||||
|
if not attrs:
|
||||||
|
raise serializers.ValidationError({"detail": "Informe ao menos um campo para alterar."})
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class ContabilObservacaoApuracaoSerializer(serializers.Serializer):
|
||||||
|
"""Corpo de `ContabilObservacaoViewSet.encerrar()`/`reativar()` — a
|
||||||
|
apuração aberta é quem define a competência em que a observação deixa de
|
||||||
|
valer ("ocultar das próximas execuções"), então ela precisa vir no corpo
|
||||||
|
mesmo nessas ações que não mudam o texto."""
|
||||||
|
|
||||||
|
apuracao = serializers.PrimaryKeyRelatedField(queryset=ContabilApuracao.objects.all())
|
||||||
|
|
||||||
|
|
||||||
class ContabilAchadoSerializer(serializers.ModelSerializer):
|
class ContabilAchadoSerializer(serializers.ModelSerializer):
|
||||||
conta_codigo = serializers.CharField(source="conta.codigo", read_only=True, default=None)
|
conta_codigo = serializers.CharField(source="conta.codigo", read_only=True, default=None)
|
||||||
conta_descricao = serializers.CharField(source="conta.descricao", read_only=True, default=None)
|
conta_descricao = serializers.CharField(source="conta.descricao", read_only=True, default=None)
|
||||||
|
|||||||
@ -103,6 +103,7 @@ router.register(
|
|||||||
views.ContabilLinhaAnaliseVerticalViewSet,
|
views.ContabilLinhaAnaliseVerticalViewSet,
|
||||||
basename="contabil-linha-analise-vertical",
|
basename="contabil-linha-analise-vertical",
|
||||||
)
|
)
|
||||||
|
router.register("contabil-observacoes", views.ContabilObservacaoViewSet, basename="contabil-observacao")
|
||||||
router.register("contabil-achados", views.ContabilAchadoViewSet, basename="contabil-achado")
|
router.register("contabil-achados", views.ContabilAchadoViewSet, basename="contabil-achado")
|
||||||
router.register(
|
router.register(
|
||||||
"contabil-indicadores-definicoes",
|
"contabil-indicadores-definicoes",
|
||||||
|
|||||||
@ -57,6 +57,7 @@ from .models import (
|
|||||||
ContabilConta,
|
ContabilConta,
|
||||||
ContabilLinhaAnaliseVertical,
|
ContabilLinhaAnaliseVertical,
|
||||||
ContabilLinhaDre,
|
ContabilLinhaDre,
|
||||||
|
ContabilObservacao,
|
||||||
Departamento,
|
Departamento,
|
||||||
Favorito,
|
Favorito,
|
||||||
FuncaoTelefonia,
|
FuncaoTelefonia,
|
||||||
@ -136,6 +137,10 @@ from .serializers import (
|
|||||||
ContabilResumoFechamentoSerializer,
|
ContabilResumoFechamentoSerializer,
|
||||||
ContabilLinhaAnaliseVerticalSerializer,
|
ContabilLinhaAnaliseVerticalSerializer,
|
||||||
ContabilLinhaDreSerializer,
|
ContabilLinhaDreSerializer,
|
||||||
|
ContabilObservacaoApuracaoSerializer,
|
||||||
|
ContabilObservacaoCreateSerializer,
|
||||||
|
ContabilObservacaoSerializer,
|
||||||
|
ContabilObservacaoUpdateSerializer,
|
||||||
DepartamentoSerializer,
|
DepartamentoSerializer,
|
||||||
FavoritoSerializer,
|
FavoritoSerializer,
|
||||||
FuncaoTelefoniaSerializer,
|
FuncaoTelefoniaSerializer,
|
||||||
@ -3687,7 +3692,12 @@ _CONTABIL_NIVEL_ABERTO_PADRAO = 2
|
|||||||
|
|
||||||
|
|
||||||
def _contabil_arvore_contexto(
|
def _contabil_arvore_contexto(
|
||||||
itens: list[Any], chave: str, nivel_fn: Callable[[Any], int], px_por_nivel: int
|
itens: list[Any],
|
||||||
|
chave: str,
|
||||||
|
nivel_fn: Callable[[Any], int],
|
||||||
|
px_por_nivel: int,
|
||||||
|
observacoes_por_chave: dict[str, list[ContabilObservacao]] | None = None,
|
||||||
|
chave_fn: Callable[[Any], str] | None = None,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""Monta o contexto de uma árvore recolhível (Balancete/DRE) pro
|
"""Monta o contexto de uma árvore recolhível (Balancete/DRE) pro
|
||||||
relatório "Gerar Dashboard" — mesmo algoritmo de `dcContaNivel()`/
|
relatório "Gerar Dashboard" — mesmo algoritmo de `dcContaNivel()`/
|
||||||
@ -3696,11 +3706,19 @@ def _contabil_arvore_contexto(
|
|||||||
relatório é HTML estático renderizado uma vez, não uma tela que
|
relatório é HTML estático renderizado uma vez, não uma tela que
|
||||||
re-renderiza a cada clique — o JS do relatório só alterna `hidden` nas
|
re-renderiza a cada clique — o JS do relatório só alterna `hidden` nas
|
||||||
linhas já prontas (ver `dashboard-contabil-relatorio.html`). `colapsado_padrao`
|
linhas já prontas (ver `dashboard-contabil-relatorio.html`). `colapsado_padrao`
|
||||||
marca os itens que devem nascer recolhidos (ver `_CONTABIL_NIVEL_ABERTO_PADRAO`)."""
|
marca os itens que devem nascer recolhidos (ver `_CONTABIL_NIVEL_ABERTO_PADRAO`).
|
||||||
|
|
||||||
|
`observacoes_por_chave`+`chave_fn` (opcionais, usados pelo relatório)
|
||||||
|
penduram em cada item as observações vigentes daquela conta/linha que o
|
||||||
|
contador marcou pra mostrar ao cliente — casadas pela chave natural do
|
||||||
|
histórico (`ContabilObservacao.alvo_chave`), não por id de linha."""
|
||||||
niveis = [nivel_fn(item) for item in itens]
|
niveis = [nivel_fn(item) for item in itens]
|
||||||
contexto = []
|
contexto = []
|
||||||
for i, item in enumerate(itens):
|
for i, item in enumerate(itens):
|
||||||
tem_filhos = i + 1 < len(itens) and niveis[i + 1] > niveis[i]
|
tem_filhos = i + 1 < len(itens) and niveis[i + 1] > niveis[i]
|
||||||
|
observacoes: list[ContabilObservacao] = []
|
||||||
|
if observacoes_por_chave is not None and chave_fn is not None:
|
||||||
|
observacoes = observacoes_por_chave.get(chave_fn(item), [])
|
||||||
contexto.append(
|
contexto.append(
|
||||||
{
|
{
|
||||||
chave: item,
|
chave: item,
|
||||||
@ -3708,11 +3726,38 @@ def _contabil_arvore_contexto(
|
|||||||
"nivel_px": niveis[i] * px_por_nivel,
|
"nivel_px": niveis[i] * px_por_nivel,
|
||||||
"tem_filhos": tem_filhos,
|
"tem_filhos": tem_filhos,
|
||||||
"colapsado_padrao": tem_filhos and niveis[i] >= _CONTABIL_NIVEL_ABERTO_PADRAO,
|
"colapsado_padrao": tem_filhos and niveis[i] >= _CONTABIL_NIVEL_ABERTO_PADRAO,
|
||||||
|
"observacoes": observacoes,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return contexto
|
return contexto
|
||||||
|
|
||||||
|
|
||||||
|
def _contabil_chave_alvo(alvo_tipo: str, alvo: Any) -> str:
|
||||||
|
"""Chave natural de uma conta/linha pro histórico de observações — as
|
||||||
|
mesmas usadas por `_contabil_sincroniza_*()` no reprocessamento (`codigo`
|
||||||
|
no Balancete, `(descricao, nivel)` na DRE/Análise Vertical), pra uma
|
||||||
|
observação seguir a mesma conta de uma competência pra outra."""
|
||||||
|
if alvo_tipo == ContabilObservacao.ALVO_CONTA:
|
||||||
|
return ContabilObservacao.chave_conta(alvo.codigo)
|
||||||
|
return ContabilObservacao.chave_linha(alvo.descricao, alvo.nivel)
|
||||||
|
|
||||||
|
|
||||||
|
def _contabil_observacoes_por_chave(apuracao: ContabilApuracao, alvo_tipo: str) -> dict[str, list[ContabilObservacao]]:
|
||||||
|
"""Observações vigentes desta empresa/competência (ver
|
||||||
|
`ContabilObservacao.vigentes_para()`), agrupadas pela chave natural do
|
||||||
|
alvo — um dict pra casar com cada conta/linha da apuração sem uma query
|
||||||
|
por linha."""
|
||||||
|
agrupadas: dict[str, list[ContabilObservacao]] = {}
|
||||||
|
consulta = (
|
||||||
|
ContabilObservacao.vigentes_para(apuracao.codigo_empresa, apuracao.competencia)
|
||||||
|
.filter(alvo_tipo=alvo_tipo)
|
||||||
|
.select_related("criado_por", "encerrada_por")
|
||||||
|
)
|
||||||
|
for observacao in consulta:
|
||||||
|
agrupadas.setdefault(observacao.alvo_chave, []).append(observacao)
|
||||||
|
return agrupadas
|
||||||
|
|
||||||
|
|
||||||
def _contabil_sincroniza_contas(
|
def _contabil_sincroniza_contas(
|
||||||
apuracao: ContabilApuracao, contas_extraidas: list[dashboard_contabil_modelos.LinhaBalanceteExtraida]
|
apuracao: ContabilApuracao, contas_extraidas: list[dashboard_contabil_modelos.LinhaBalanceteExtraida]
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -3725,8 +3770,9 @@ def _contabil_sincroniza_contas(
|
|||||||
conta com algum campo divergente da versão anterior volta pra
|
conta com algum campo divergente da versão anterior volta pra
|
||||||
`validado=False` e `alterada_reprocessamento=True`, guardando o
|
`validado=False` e `alterada_reprocessamento=True`, guardando o
|
||||||
`saldo_atual` de antes em `valor_anterior_reprocessamento` (só pro
|
`saldo_atual` de antes em `valor_anterior_reprocessamento` (só pro
|
||||||
tooltip do badge no frontend). `observacao`/`oculta_no_relatorio` nunca
|
tooltip do badge no frontend). Observação nunca é afetada por aqui: ela
|
||||||
são tocados aqui."""
|
não mora mais na linha, e sim em `ContabilObservacao` (histórico por
|
||||||
|
empresa+conta)."""
|
||||||
antigas = {conta.codigo: conta for conta in apuracao.contas.all()}
|
antigas = {conta.codigo: conta for conta in apuracao.contas.all()}
|
||||||
vistos: set[str] = set()
|
vistos: set[str] = set()
|
||||||
|
|
||||||
@ -4068,8 +4114,9 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
|
|||||||
`(descricao, nivel)`, na DRE/Análise Vertical — mesma convenção já
|
`(descricao, nivel)`, na DRE/Análise Vertical — mesma convenção já
|
||||||
usada pelo histórico de variação em `regras.py`) tem seus campos
|
usada pelo histórico de variação em `regras.py`) tem seus campos
|
||||||
brutos (saldo/valor/ordem) atualizados **no mesmo registro**
|
brutos (saldo/valor/ordem) atualizados **no mesmo registro**
|
||||||
(mesmo `id`), preservando `observacao`/`oculta_no_relatorio`
|
(mesmo `id`). Observação não é preservada "por cuidado", ela
|
||||||
sempre. Se algum valor realmente mudou, `validado` volta pra
|
simplesmente não vive aqui — mora em `ContabilObservacao`, casada
|
||||||
|
por chave natural. Se algum valor realmente mudou, `validado` volta pra
|
||||||
`False` e `alterada_reprocessamento` vira `True` (frontend mostra
|
`False` e `alterada_reprocessamento` vira `True` (frontend mostra
|
||||||
um alerta); se nada mudou, os dois continuam como estavam.
|
um alerta); se nada mudou, os dois continuam como estavam.
|
||||||
- Conta/linha que só existe na versão nova é criada normalmente
|
- Conta/linha que só existe na versão nova é criada normalmente
|
||||||
@ -4175,6 +4222,22 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
|
|||||||
apuracao.save(update_fields=["status", "concluida_em"])
|
apuracao.save(update_fields=["status", "concluida_em"])
|
||||||
return Response(ContabilApuracaoDetailSerializer(apuracao).data)
|
return Response(ContabilApuracaoDetailSerializer(apuracao).data)
|
||||||
|
|
||||||
|
@action(detail=True, methods=["get"])
|
||||||
|
def observacoes(self, request: Request, pk: str | None = None) -> Response:
|
||||||
|
"""Observações vigentes nesta apuração (ver `ContabilObservacao`) —
|
||||||
|
as escritas nesta competência mais as herdadas de competências
|
||||||
|
anteriores que ainda não foram encerradas. O frontend casa cada uma
|
||||||
|
com a conta/linha pela dupla `alvo_tipo`+`alvo_chave`, não por id de
|
||||||
|
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).select_related(
|
||||||
|
"criado_por", "encerrada_por"
|
||||||
|
)
|
||||||
|
serializada = ContabilObservacaoSerializer(
|
||||||
|
consulta, many=True, context={"competencia": apuracao.competencia}
|
||||||
|
)
|
||||||
|
return Response(serializada.data)
|
||||||
|
|
||||||
@action(detail=True, methods=["get"])
|
@action(detail=True, methods=["get"])
|
||||||
def dashboard(self, request: Request, pk: str | None = None) -> HttpResponse:
|
def dashboard(self, request: Request, pk: str | None = None) -> HttpResponse:
|
||||||
"""Gera o relatório "Gerar Dashboard": um documento HTML autocontido,
|
"""Gera o relatório "Gerar Dashboard": um documento HTML autocontido,
|
||||||
@ -4206,27 +4269,70 @@ class ContabilApuracaoViewSet(viewsets.ModelViewSet):
|
|||||||
{"competencia": apuracao.competencia.strftime("%m/%Y"), "resultado_liquido": dados.resultado_liquido}
|
{"competencia": apuracao.competencia.strftime("%m/%Y"), "resultado_liquido": dados.resultado_liquido}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Cards/observações que o contador escondeu na aba "Dashboard" da
|
# Cards que o contador escondeu na aba "Dashboard" da tela de revisão
|
||||||
# tela de revisão (ver `indicadores_ocultos()`/`ContabilConta.
|
# (ver `indicadores_ocultos()`) e observação sem `mostrar_ao_cliente`
|
||||||
# oculta_no_relatorio`/etc.) não entram no relatório gerado — a
|
# não entram no relatório gerado — a linha/conta/achado em si continua
|
||||||
# linha/conta/achado em si continua listada normal no Balancete/
|
# listada normal no Balancete/DRE/Observações da revisão, só a seção de
|
||||||
# DRE/Observações da revisão, só a seção de observações/os cards de
|
# observações/os cards de indicador deste relatório é que os omitem.
|
||||||
# indicador deste relatório final é que os omitem.
|
# Observações que o contador marcou pra mostrar ao cliente, já no
|
||||||
|
# recorte de vigência desta competência (ver `ContabilObservacao`) —
|
||||||
|
# inclui as herdadas de competências anteriores ainda não encerradas,
|
||||||
|
# que é justamente o ponto do histórico.
|
||||||
|
observacoes_visiveis = [
|
||||||
|
observacao
|
||||||
|
for observacao in ContabilObservacao.vigentes_para(
|
||||||
|
apuracao.codigo_empresa, apuracao.competencia
|
||||||
|
).select_related("criado_por")
|
||||||
|
if observacao.mostrar_ao_cliente
|
||||||
|
]
|
||||||
|
observacoes_por_tipo: dict[str, dict[str, list[ContabilObservacao]]] = {
|
||||||
|
ContabilObservacao.ALVO_CONTA: {},
|
||||||
|
ContabilObservacao.ALVO_DRE: {},
|
||||||
|
ContabilObservacao.ALVO_ANALISE_VERTICAL: {},
|
||||||
|
}
|
||||||
|
for observacao in observacoes_visiveis:
|
||||||
|
observacoes_por_tipo[observacao.alvo_tipo].setdefault(observacao.alvo_chave, []).append(observacao)
|
||||||
|
|
||||||
cards = _contabil_monta_cards_indicadores(apuracao, valores_indicadores)
|
cards = _contabil_monta_cards_indicadores(apuracao, valores_indicadores)
|
||||||
contexto = {
|
contexto = {
|
||||||
"apuracao": apuracao,
|
"apuracao": apuracao,
|
||||||
"indicadores_grupos": _contabil_agrupa_indicadores_cards(cards),
|
"indicadores_grupos": _contabil_agrupa_indicadores_cards(cards),
|
||||||
"evolucao": evolucao,
|
"evolucao": evolucao,
|
||||||
"contas": _contabil_arvore_contexto(contas, "conta", lambda c: c.codigo.count("."), 18),
|
"contas": _contabil_arvore_contexto(
|
||||||
"linhas_dre": _contabil_arvore_contexto(linhas_dre, "linha", lambda l: max(0, l.nivel), 16),
|
contas,
|
||||||
|
"conta",
|
||||||
|
lambda c: c.codigo.count("."),
|
||||||
|
18,
|
||||||
|
observacoes_por_tipo[ContabilObservacao.ALVO_CONTA],
|
||||||
|
lambda c: ContabilObservacao.chave_conta(c.codigo),
|
||||||
|
),
|
||||||
|
"linhas_dre": _contabil_arvore_contexto(
|
||||||
|
linhas_dre,
|
||||||
|
"linha",
|
||||||
|
lambda l: max(0, l.nivel),
|
||||||
|
16,
|
||||||
|
observacoes_por_tipo[ContabilObservacao.ALVO_DRE],
|
||||||
|
lambda l: ContabilObservacao.chave_linha(l.descricao, l.nivel),
|
||||||
|
),
|
||||||
"analise_vertical_meses": apuracao.analise_vertical_meses,
|
"analise_vertical_meses": apuracao.analise_vertical_meses,
|
||||||
"linhas_analise_vertical": _contabil_arvore_contexto(
|
"linhas_analise_vertical": _contabil_arvore_contexto(
|
||||||
linhas_analise_vertical, "linha", lambda l: max(0, l.nivel), 16
|
linhas_analise_vertical,
|
||||||
|
"linha",
|
||||||
|
lambda l: max(0, l.nivel),
|
||||||
|
16,
|
||||||
|
observacoes_por_tipo[ContabilObservacao.ALVO_ANALISE_VERTICAL],
|
||||||
|
lambda l: ContabilObservacao.chave_linha(l.descricao, l.nivel),
|
||||||
),
|
),
|
||||||
"observacoes_contas": [conta for conta in contas if conta.observacao and not conta.oculta_no_relatorio],
|
"observacoes_contas": [
|
||||||
"observacoes_dre": [linha for linha in linhas_dre if linha.observacao and not linha.oculta_no_relatorio],
|
observacao for observacao in observacoes_visiveis if observacao.alvo_tipo == ContabilObservacao.ALVO_CONTA
|
||||||
|
],
|
||||||
|
"observacoes_dre": [
|
||||||
|
observacao for observacao in observacoes_visiveis if observacao.alvo_tipo == ContabilObservacao.ALVO_DRE
|
||||||
|
],
|
||||||
"observacoes_analise_vertical": [
|
"observacoes_analise_vertical": [
|
||||||
linha for linha in linhas_analise_vertical if linha.observacao and not linha.oculta_no_relatorio
|
observacao
|
||||||
|
for observacao in observacoes_visiveis
|
||||||
|
if observacao.alvo_tipo == ContabilObservacao.ALVO_ANALISE_VERTICAL
|
||||||
],
|
],
|
||||||
"achados_com_observacao": [
|
"achados_com_observacao": [
|
||||||
achado for achado in achados if achado.observacao_contador and not achado.oculto_no_relatorio
|
achado for achado in achados if achado.observacao_contador and not achado.oculto_no_relatorio
|
||||||
@ -4497,6 +4603,142 @@ class ContabilLinhaAnaliseVerticalViewSet(viewsets.ModelViewSet):
|
|||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
|
|
||||||
|
class ContabilObservacaoViewSet(viewsets.ModelViewSet):
|
||||||
|
"""Histórico de observações do contador (ver `ContabilObservacao`) — uma
|
||||||
|
thread por empresa+conta, que atravessa competências em vez de morrer
|
||||||
|
junto com a apuração (era um campo de `ContabilConta`/`ContabilLinhaDre`/
|
||||||
|
`ContabilLinhaAnaliseVertical` até a migração `0071`).
|
||||||
|
|
||||||
|
Não tem `list`/`retrieve` de propósito: o frontend sempre lê pelo recorte
|
||||||
|
de vigência de uma apuração (`ContabilApuracaoViewSet.observacoes()`),
|
||||||
|
nunca a tabela inteira.
|
||||||
|
|
||||||
|
O que cada método pode mexer:
|
||||||
|
- `create` escreve uma observação nova na apuração aberta (precisa estar
|
||||||
|
"Em revisão"); empresa/competência/chave do alvo saem do servidor.
|
||||||
|
- `partial_update` aceita `texto` **só enquanto a apuração de origem
|
||||||
|
estiver em revisão** (depois disso a observação é histórica, e o texto/
|
||||||
|
autor/data ficam congelados) e `mostrar_ao_cliente` sempre — decisão
|
||||||
|
confirmada com o usuário: o bloqueio protege o registro histórico, mas
|
||||||
|
mostrar ou não ao cliente é uma decisão editorial de cada relatório,
|
||||||
|
inclusive de uma competência posterior.
|
||||||
|
- `destroy` segue a mesma regra do texto (só apaga o que ainda é da
|
||||||
|
competência aberta; histórico não se apaga, se encerra).
|
||||||
|
- `encerrar`/`reativar` ligam/desligam `encerrada_em_competencia` — é o
|
||||||
|
"ocultar das próximas execuções": continua visível na competência em
|
||||||
|
que foi encerrada e some a partir da seguinte."""
|
||||||
|
|
||||||
|
http_method_names = ["post", "patch", "delete", "head", "options"]
|
||||||
|
queryset = ContabilObservacao.objects.select_related("criado_por", "encerrada_por", "apuracao_origem")
|
||||||
|
serializer_class = ContabilObservacaoSerializer
|
||||||
|
|
||||||
|
def get_permissions(self) -> list[BasePermission]:
|
||||||
|
return [PermissaoApp("relatorios", "dashboard-contabil")]
|
||||||
|
|
||||||
|
def _resolve_alvo(self, apuracao: ContabilApuracao, 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,
|
||||||
|
}
|
||||||
|
alvo = modelos[alvo_tipo].objects.filter(pk=alvo_id, apuracao=apuracao).first()
|
||||||
|
if alvo is None:
|
||||||
|
raise ValidationError({"alvo_id": "Conta ou linha não encontrada nesta análise."})
|
||||||
|
return alvo
|
||||||
|
|
||||||
|
def _garante_texto_editavel(self, observacao: ContabilObservacao) -> None:
|
||||||
|
origem = observacao.apuracao_origem
|
||||||
|
if origem is None or origem.status == ContabilApuracao.STATUS_CONCLUIDA:
|
||||||
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"detail": (
|
||||||
|
"Esta observação já faz parte do histórico, o texto não pode mais ser alterado. "
|
||||||
|
"Registre uma observação nova ou encerre esta para as próximas competências."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, request: Request, *args: Any, **kwargs: Any) -> Response:
|
||||||
|
entrada = ContabilObservacaoCreateSerializer(data=request.data)
|
||||||
|
entrada.is_valid(raise_exception=True)
|
||||||
|
dados = entrada.validated_data
|
||||||
|
apuracao: ContabilApuracao = dados["apuracao"]
|
||||||
|
_contabil_garante_em_revisao(apuracao)
|
||||||
|
alvo = self._resolve_alvo(apuracao, dados["alvo_tipo"], dados["alvo_id"])
|
||||||
|
observacao = ContabilObservacao.objects.create(
|
||||||
|
codigo_empresa=apuracao.codigo_empresa,
|
||||||
|
alvo_tipo=dados["alvo_tipo"],
|
||||||
|
alvo_chave=_contabil_chave_alvo(dados["alvo_tipo"], alvo),
|
||||||
|
alvo_rotulo=alvo.descricao,
|
||||||
|
apuracao_origem=apuracao,
|
||||||
|
competencia_origem=apuracao.competencia,
|
||||||
|
texto=dados["texto"].strip(),
|
||||||
|
mostrar_ao_cliente=dados["mostrar_ao_cliente"],
|
||||||
|
criado_por=request.user,
|
||||||
|
)
|
||||||
|
serializada = ContabilObservacaoSerializer(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()
|
||||||
|
entrada = ContabilObservacaoUpdateSerializer(data=request.data)
|
||||||
|
entrada.is_valid(raise_exception=True)
|
||||||
|
dados = entrada.validated_data
|
||||||
|
campos: list[str] = []
|
||||||
|
if "texto" in dados:
|
||||||
|
self._garante_texto_editavel(observacao)
|
||||||
|
observacao.texto = dados["texto"].strip()
|
||||||
|
campos.append("texto")
|
||||||
|
if "mostrar_ao_cliente" in dados:
|
||||||
|
observacao.mostrar_ao_cliente = dados["mostrar_ao_cliente"]
|
||||||
|
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)
|
||||||
|
|
||||||
|
def perform_destroy(self, instance: ContabilObservacao) -> None:
|
||||||
|
self._garante_texto_editavel(instance)
|
||||||
|
instance.delete()
|
||||||
|
|
||||||
|
@action(detail=True, methods=["post"])
|
||||||
|
def encerrar(self, request: Request, pk: str | None = None) -> Response:
|
||||||
|
""""Ocultar das próximas execuções": a observação continua visível na
|
||||||
|
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.is_valid(raise_exception=True)
|
||||||
|
apuracao: ContabilApuracao = 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})
|
||||||
|
return Response(serializada.data)
|
||||||
|
|
||||||
|
@action(detail=True, methods=["post"])
|
||||||
|
def reativar(self, request: Request, pk: str | None = None) -> Response:
|
||||||
|
"""Desfaz um `encerrar()` — a observação volta a valer daqui pra
|
||||||
|
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.is_valid(raise_exception=True)
|
||||||
|
apuracao: ContabilApuracao = 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})
|
||||||
|
return Response(serializada.data)
|
||||||
|
|
||||||
|
|
||||||
class ContabilAchadoViewSet(viewsets.ModelViewSet):
|
class ContabilAchadoViewSet(viewsets.ModelViewSet):
|
||||||
"""Achados de auditoria gerados automaticamente na criação da apuração —
|
"""Achados de auditoria gerados automaticamente na criação da apuração —
|
||||||
GET/PATCH (marcar como tratado/ignorado, sempre com justificativa) +
|
GET/PATCH (marcar como tratado/ignorado, sempre com justificativa) +
|
||||||
|
|||||||
@ -683,6 +683,7 @@
|
|||||||
(`.icon-btn`, `perfis-acesso.css`), mas menor, pra caber numa célula sem
|
(`.icon-btn`, `perfis-acesso.css`), mas menor, pra caber numa célula sem
|
||||||
aumentar a altura da linha. */
|
aumentar a altura da linha. */
|
||||||
.dc-conta-observacao-btn {
|
.dc-conta-observacao-btn {
|
||||||
|
position: relative; /* âncora do `.dc-obs-contador` (badge de quantidade) */
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -759,16 +760,71 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tooltip customizado no visual do Portal — os badges abaixo (alterada no
|
||||||
|
reprocessamento, fonte de PDF atípica) precisavam de um texto mais longo,
|
||||||
|
às vezes com quebra de linha própria (ex. "valor antes do
|
||||||
|
reprocessamento" numa segunda linha), do que `.info-tooltip__text`
|
||||||
|
(components.css) suporta — aquele é `white-space:nowrap`, pensado só pro
|
||||||
|
rótulo curto "Mais informações". `tabindex="0"` no gatilho + `:hover`/
|
||||||
|
`:focus-visible` mostram o tooltip também via teclado, não só no hover do
|
||||||
|
mouse. `white-space: pre-line` preserva um `\n` deliberado no texto
|
||||||
|
(quebra de linha própria) e ainda assim quebra/envolve o resto
|
||||||
|
normalmente pela largura máxima.
|
||||||
|
|
||||||
|
`position: fixed` (não `absolute` como `.info-tooltip__text`) — os badges
|
||||||
|
deste pacote vivem dentro de `.pa-table-wrap`, que tem `overflow:hidden`
|
||||||
|
pra arredondar o canto da tabela (perfis-acesso.css); um tooltip
|
||||||
|
`absolute` cortava (confirmado pelo usuário: balão cortado em cima da
|
||||||
|
primeira linha, cortado na lateral perto da borda da tabela). `fixed`
|
||||||
|
posiciona relativo à viewport, escapando desse corte — `top`/`left` são
|
||||||
|
calculados em JS (`pidDcPosicionaTooltip()`, dashboard-contabil.js, no
|
||||||
|
`mouseenter`/`focus` do gatilho) e clampados pra nunca sair da tela nem
|
||||||
|
pra cima nem pros lados; sem JS nenhum, ficaria sempre num canto fixo da
|
||||||
|
tela, errado pra qualquer badge que não esteja bem naquele ponto. */
|
||||||
|
.dc-hover-tooltip {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dc-hover-tooltip__gatilho {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dc-hover-tooltip__texto {
|
||||||
|
position: fixed;
|
||||||
|
width: max-content;
|
||||||
|
max-width: 280px;
|
||||||
|
white-space: pre-line;
|
||||||
|
text-align: left;
|
||||||
|
background: var(--bg-surface-raised);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity var(--transition-fast);
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dc-hover-tooltip__gatilho:hover + .dc-hover-tooltip__texto,
|
||||||
|
.dc-hover-tooltip__gatilho:focus-visible + .dc-hover-tooltip__texto {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* Badge de alerta — conta/linha que mudou no último reprocessamento
|
/* Badge de alerta — conta/linha que mudou no último reprocessamento
|
||||||
(`alterada_reprocessamento`, ver "Reprocessar" no CLAUDE.md do pacote).
|
(`alterada_reprocessamento`, ver "Reprocessar" no CLAUDE.md do pacote).
|
||||||
Puramente informativo (não é um botão, só um `<span>` com tooltip via
|
Puramente informativo (não é um botão, só o gatilho de um
|
||||||
`title`) — continua visível mesmo depois do contador marcar a conta/linha
|
`.dc-hover-tooltip` acima) — continua visível mesmo depois do contador
|
||||||
como validada de novo (pedido explícito do usuário: precisa dar pra
|
marcar a conta/linha como validada de novo (pedido explícito do usuário:
|
||||||
identificar quais itens já foram reprocessados E revalidados), só troca
|
precisa dar pra identificar quais itens já foram reprocessados E
|
||||||
de cor via `--validada` abaixo; só some de vez num próximo reprocessamento
|
revalidados), só troca de cor via `--validada` abaixo; só some de vez num
|
||||||
sem essa mudança específica. Cor `--danger` (pendente), igual à
|
próximo reprocessamento sem essa mudança específica. Cor `--danger`
|
||||||
severidade "alta" dos achados — chama mais atenção que o dourado já usado
|
(pendente), igual à severidade "alta" dos achados — chama mais atenção
|
||||||
pelo estado "parcial" do validado. */
|
que o dourado já usado pelo estado "parcial" do validado. */
|
||||||
.dc-conta-alterada-badge {
|
.dc-conta-alterada-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -787,9 +843,9 @@
|
|||||||
|
|
||||||
/* Aviso de "fonte de PDF atípica" (ContabilApuracao.fonte_pdf_atipica) ao
|
/* Aviso de "fonte de PDF atípica" (ContabilApuracao.fonte_pdf_atipica) ao
|
||||||
lado do nome da empresa — mesmo espírito de .dc-conta-alterada-badge
|
lado do nome da empresa — mesmo espírito de .dc-conta-alterada-badge
|
||||||
(span informativo, tooltip via `title`, não é um botão), cor `--gold`
|
(gatilho de um `.dc-hover-tooltip`, não é um botão), cor `--gold` porque
|
||||||
porque é um aviso de atenção, não um alerta de severidade alta como o
|
é um aviso de atenção, não um alerta de severidade alta como o badge de
|
||||||
badge de alteração acima. */
|
alteração acima. */
|
||||||
.dc-fonte-atipica-badge {
|
.dc-fonte-atipica-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -814,6 +870,23 @@
|
|||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Este `<td>` é sempre o único da linha (colspan cobre todas as colunas),
|
||||||
|
então ele é ao mesmo tempo "primeiro" e "último" filho pro seletor CSS —
|
||||||
|
herdaria `text-align:right` de `.pa-table td:last-child` (bug de
|
||||||
|
alinhamento já visto no painel de detalhe de outras telas) E a cor
|
||||||
|
apagada de `.dc-contas-table td:nth-child(1)` (pensada pra coluna do
|
||||||
|
código da conta, `--text-muted`), deixando o texto da thread de
|
||||||
|
observações "apagado" mesmo já sem cor própria nele. Precisa do seletor
|
||||||
|
com 2 classes (não só `.dc-obs-edit-row td`) pra empatar/ganhar em
|
||||||
|
especificidade das duas regras acima (a versão de 1 classe perdia); força
|
||||||
|
a esquerda (é texto corrido, não valor numérico) e `--text-primary`
|
||||||
|
(texto principal do tema, não um branco fixo — continua legível nos dois
|
||||||
|
temas) como cor herdada por padrão pra tudo que não define a própria. */
|
||||||
|
.pa-table .dc-obs-edit-row td {
|
||||||
|
text-align: left;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.dc-obs-edit {
|
.dc-obs-edit {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -828,6 +901,131 @@
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Thread de observações (histórico por empresa+conta) ----
|
||||||
|
O painel inline deixou de ser "um textarea com o texto da linha" e virou
|
||||||
|
uma thread: as observações vigentes daquela conta (as desta competência e
|
||||||
|
as herdadas de competências anteriores, ver `ContabilObservacao` no
|
||||||
|
backend) em cima, o campo de observação nova embaixo. Observação herdada
|
||||||
|
fica visualmente mais apagada e sem os botões de editar/excluir — é
|
||||||
|
registro histórico, só dá pra encerrar ou responder com uma nova. */
|
||||||
|
.dc-obs-thread {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dc-obs-thread__vazio {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dc-obs-item {
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: var(--space-3);
|
||||||
|
background: var(--bg-surface-raised);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dc-obs-item--historica {
|
||||||
|
border-left: 3px solid var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dc-obs-item--encerrada {
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dc-obs-item__head {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dc-obs-item__assinatura {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dc-obs-item__selos {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dc-obs-item__acoes {
|
||||||
|
margin-left: auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dc-obs-item__texto {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dc-obs-selo {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px var(--space-2);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
background: var(--card-bg-hover);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dc-obs-selo--historica {
|
||||||
|
background: rgba(var(--accent-rgb), 0.16);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dc-obs-selo--encerrada {
|
||||||
|
background: rgba(var(--gold-rgb), 0.18);
|
||||||
|
color: var(--gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dc-obs-selo--cliente {
|
||||||
|
background: rgba(var(--teal-rgb), 0.18);
|
||||||
|
color: var(--teal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dc-obs-nova {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding-top: var(--space-2);
|
||||||
|
border-top: 1px dashed var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contagem de observações vigentes ao lado do ícone da coluna "Observação" —
|
||||||
|
o texto em si nunca aparece na tabela (ver `.dc-conta-observacao-btn`),
|
||||||
|
então o número é o único sinal de "quantas coisas já foram anotadas aqui". */
|
||||||
|
.dc-obs-contador {
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
right: -4px;
|
||||||
|
min-width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
padding: 0 3px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dc-obs-filtros {
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
/* Resumo de observações no final do Balancete/DRE (pedido explícito do
|
/* Resumo de observações no final do Balancete/DRE (pedido explícito do
|
||||||
usuário) — reaproveita `.dc-dash-obs-list`/`.dc-dash-obs*` (já definidas
|
usuário) — reaproveita `.dc-dash-obs-list`/`.dc-dash-obs*` (já definidas
|
||||||
mais abaixo) pros itens da lista, só a moldura da seção é nova. */
|
mais abaixo) pros itens da lista, só a moldura da seção é nova. */
|
||||||
@ -1131,7 +1329,12 @@
|
|||||||
.dc-dash-obs__texto {
|
.dc-dash-obs__texto {
|
||||||
margin-top: var(--space-1);
|
margin-top: var(--space-1);
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
color: var(--text-secondary);
|
/* Era `--text-secondary` (cinza) — pedido explícito do usuário: no tema
|
||||||
|
escuro o texto da observação em si ficava "apagado"/difícil de ler
|
||||||
|
contra o fundo do card. `--text-primary` é o token de texto principal
|
||||||
|
(branco no escuro, escuro no claro), não um branco fixo — continua
|
||||||
|
legível nos dois temas. */
|
||||||
|
color: var(--text-primary);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -419,8 +419,9 @@
|
|||||||
.dcr-toggle-spacer { display: inline-block; width: 18px; height: 18px; flex-shrink: 0; }
|
.dcr-toggle-spacer { display: inline-block; width: 18px; height: 18px; flex-shrink: 0; }
|
||||||
|
|
||||||
/* Ícone de observação na tabela do Balancete/DRE (coluna "Observação") —
|
/* Ícone de observação na tabela do Balancete/DRE (coluna "Observação") —
|
||||||
só aparece na linha quando há mesmo uma observação visível ao cliente
|
só aparece na linha quando há mesmo uma observação vigente marcada como
|
||||||
(ver `oculta_no_relatorio` no template); clicar abre `.dcr-obs-inline-row`
|
visível ao cliente (`item.observacoes`, já filtrado na view a partir de
|
||||||
|
`ContabilObservacao.mostrar_ao_cliente`); clicar abre `.dcr-obs-inline-row`
|
||||||
logo abaixo, mesmo espírito do editor inline da tela de revisão do
|
logo abaixo, mesmo espírito do editor inline da tela de revisão do
|
||||||
Portal (`dc-conta-observacao-btn`/`dc-obs-edit-row`, dashboard-contabil.js). */
|
Portal (`dc-conta-observacao-btn`/`dc-obs-edit-row`, dashboard-contabil.js). */
|
||||||
.dcr-obs-btn {
|
.dcr-obs-btn {
|
||||||
@ -485,6 +486,9 @@
|
|||||||
}
|
}
|
||||||
.dcr-obs-item__titulo { font-weight: 700; font-size: 0.85rem; color: var(--roxo-escuro); }
|
.dcr-obs-item__titulo { font-weight: 700; font-size: 0.85rem; color: var(--roxo-escuro); }
|
||||||
.dcr-obs-item__texto { font-size: 0.85rem; margin-top: 3px; white-space: pre-wrap; color: var(--texto); }
|
.dcr-obs-item__texto { font-size: 0.85rem; margin-top: 3px; white-space: pre-wrap; color: var(--texto); }
|
||||||
|
/* Assinatura do histórico: quem escreveu, quando e (quando vem de um mês
|
||||||
|
anterior) em que competência foi registrada — ver `ContabilObservacao`. */
|
||||||
|
.dcr-obs-item__assinatura { font-size: 0.72rem; margin-top: 5px; color: var(--cinza-texto, #6b6b78); }
|
||||||
.dcr-vazio { color: var(--texto-muted); font-size: 0.85rem; font-style: italic; }
|
.dcr-vazio { color: var(--texto-muted); font-size: 0.85rem; font-style: italic; }
|
||||||
|
|
||||||
/* Resumo do Fechamento — texto rico escrito pelo contador na aba
|
/* Resumo do Fechamento — texto rico escrito pelo contador na aba
|
||||||
@ -605,20 +609,22 @@
|
|||||||
<td class="dcr-valor">{{ item.conta.credito|moeda }}</td>
|
<td class="dcr-valor">{{ item.conta.credito|moeda }}</td>
|
||||||
<td class="dcr-valor">{{ item.conta.saldo_atual|moeda }}</td>
|
<td class="dcr-valor">{{ item.conta.saldo_atual|moeda }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if item.conta.observacao and not item.conta.oculta_no_relatorio %}
|
{% if item.observacoes %}
|
||||||
<button type="button" class="dcr-obs-btn" data-dcr-obs-toggle="conta-{{ item.conta.id }}" aria-label="Ver observação">
|
<button type="button" class="dcr-obs-btn" data-dcr-obs-toggle="conta-{{ item.conta.id }}" aria-label="Ver observação">
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if item.conta.observacao and not item.conta.oculta_no_relatorio %}
|
{% if item.observacoes %}
|
||||||
<tr class="dcr-obs-inline-row" data-dcr-obs-row="conta-{{ item.conta.id }}" hidden>
|
<tr class="dcr-obs-inline-row" data-dcr-obs-row="conta-{{ item.conta.id }}" hidden>
|
||||||
<td colspan="7">
|
<td colspan="7">
|
||||||
|
{% for obs in item.observacoes %}
|
||||||
<div class="dcr-obs-inline">
|
<div class="dcr-obs-inline">
|
||||||
<span class="dcr-obs-inline__rotulo">Observação do contador</span>
|
<span class="dcr-obs-inline__rotulo">Observação do contador · {{ obs.criado_por.nome|default:"Equipe contábil" }} · {{ obs.criado_em|date:"d/m/Y" }}{% if obs.competencia_origem != apuracao.competencia %} · registrada na competência {{ obs.competencia_origem|competencia }}{% endif %}</span>
|
||||||
<p class="dcr-obs-inline__texto">{{ item.conta.observacao }}</p>
|
<p class="dcr-obs-inline__texto">{{ obs.texto }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -635,12 +641,13 @@
|
|||||||
</h2>
|
</h2>
|
||||||
{% if observacoes_contas %}
|
{% if observacoes_contas %}
|
||||||
<ul class="dcr-obs-lista">
|
<ul class="dcr-obs-lista">
|
||||||
{% for conta in observacoes_contas %}
|
{% for obs in observacoes_contas %}
|
||||||
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
|
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
|
||||||
<div class="dcr-obs-item__icone"><svg width="15" height="15" 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></div>
|
<div class="dcr-obs-item__icone"><svg width="15" height="15" 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></div>
|
||||||
<div>
|
<div>
|
||||||
<div class="dcr-obs-item__titulo">{{ conta.codigo }} — {{ conta.descricao }}</div>
|
<div class="dcr-obs-item__titulo">{{ obs.alvo_chave }} — {{ obs.alvo_rotulo }}</div>
|
||||||
<div class="dcr-obs-item__texto">{{ conta.observacao }}</div>
|
<div class="dcr-obs-item__texto">{{ obs.texto }}</div>
|
||||||
|
<div class="dcr-obs-item__assinatura">{{ obs.criado_por.nome|default:"Equipe contábil" }} · {{ obs.criado_em|date:"d/m/Y" }}{% if obs.competencia_origem != apuracao.competencia %} · competência {{ obs.competencia_origem|competencia }}{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -681,20 +688,22 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="dcr-valor">{{ item.linha.valor|moeda }}</td>
|
<td class="dcr-valor">{{ item.linha.valor|moeda }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if item.linha.observacao and not item.linha.oculta_no_relatorio %}
|
{% if item.observacoes %}
|
||||||
<button type="button" class="dcr-obs-btn" data-dcr-obs-toggle="linha-{{ item.linha.id }}" aria-label="Ver observação">
|
<button type="button" class="dcr-obs-btn" data-dcr-obs-toggle="linha-{{ item.linha.id }}" aria-label="Ver observação">
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if item.linha.observacao and not item.linha.oculta_no_relatorio %}
|
{% if item.observacoes %}
|
||||||
<tr class="dcr-obs-inline-row" data-dcr-obs-row="linha-{{ item.linha.id }}" hidden>
|
<tr class="dcr-obs-inline-row" data-dcr-obs-row="linha-{{ item.linha.id }}" hidden>
|
||||||
<td colspan="3">
|
<td colspan="3">
|
||||||
|
{% for obs in item.observacoes %}
|
||||||
<div class="dcr-obs-inline">
|
<div class="dcr-obs-inline">
|
||||||
<span class="dcr-obs-inline__rotulo">Observação do contador</span>
|
<span class="dcr-obs-inline__rotulo">Observação do contador · {{ obs.criado_por.nome|default:"Equipe contábil" }} · {{ obs.criado_em|date:"d/m/Y" }}{% if obs.competencia_origem != apuracao.competencia %} · registrada na competência {{ obs.competencia_origem|competencia }}{% endif %}</span>
|
||||||
<p class="dcr-obs-inline__texto">{{ item.linha.observacao }}</p>
|
<p class="dcr-obs-inline__texto">{{ obs.texto }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -711,12 +720,13 @@
|
|||||||
</h2>
|
</h2>
|
||||||
{% if observacoes_dre %}
|
{% if observacoes_dre %}
|
||||||
<ul class="dcr-obs-lista">
|
<ul class="dcr-obs-lista">
|
||||||
{% for linha in observacoes_dre %}
|
{% for obs in observacoes_dre %}
|
||||||
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
|
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
|
||||||
<div class="dcr-obs-item__icone"><svg width="15" height="15" 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 3v6h6"/></svg></div>
|
<div class="dcr-obs-item__icone"><svg width="15" height="15" 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 3v6h6"/></svg></div>
|
||||||
<div>
|
<div>
|
||||||
<div class="dcr-obs-item__titulo">{{ linha.descricao }}</div>
|
<div class="dcr-obs-item__titulo">{{ obs.alvo_rotulo }}</div>
|
||||||
<div class="dcr-obs-item__texto">{{ linha.observacao }}</div>
|
<div class="dcr-obs-item__texto">{{ obs.texto }}</div>
|
||||||
|
<div class="dcr-obs-item__assinatura">{{ obs.criado_por.nome|default:"Equipe contábil" }} · {{ obs.criado_em|date:"d/m/Y" }}{% if obs.competencia_origem != apuracao.competencia %} · competência {{ obs.competencia_origem|competencia }}{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -766,20 +776,22 @@
|
|||||||
<td class="dcr-valor">{{ v.percentual|percentual_av }}</td>
|
<td class="dcr-valor">{{ v.percentual|percentual_av }}</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<td>
|
<td>
|
||||||
{% if item.linha.observacao and not item.linha.oculta_no_relatorio %}
|
{% if item.observacoes %}
|
||||||
<button type="button" class="dcr-obs-btn" data-dcr-obs-toggle="av-{{ item.linha.id }}" aria-label="Ver observação">
|
<button type="button" class="dcr-obs-btn" data-dcr-obs-toggle="av-{{ item.linha.id }}" aria-label="Ver observação">
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if item.linha.observacao and not item.linha.oculta_no_relatorio %}
|
{% if item.observacoes %}
|
||||||
<tr class="dcr-obs-inline-row" data-dcr-obs-row="av-{{ item.linha.id }}" hidden>
|
<tr class="dcr-obs-inline-row" data-dcr-obs-row="av-{{ item.linha.id }}" hidden>
|
||||||
<td colspan="{{ analise_vertical_meses|length|add:2 }}">
|
<td colspan="{{ analise_vertical_meses|length|add:2 }}">
|
||||||
|
{% for obs in item.observacoes %}
|
||||||
<div class="dcr-obs-inline">
|
<div class="dcr-obs-inline">
|
||||||
<span class="dcr-obs-inline__rotulo">Observação do contador</span>
|
<span class="dcr-obs-inline__rotulo">Observação do contador · {{ obs.criado_por.nome|default:"Equipe contábil" }} · {{ obs.criado_em|date:"d/m/Y" }}{% if obs.competencia_origem != apuracao.competencia %} · registrada na competência {{ obs.competencia_origem|competencia }}{% endif %}</span>
|
||||||
<p class="dcr-obs-inline__texto">{{ item.linha.observacao }}</p>
|
<p class="dcr-obs-inline__texto">{{ obs.texto }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -796,12 +808,13 @@
|
|||||||
</h2>
|
</h2>
|
||||||
{% if observacoes_analise_vertical %}
|
{% if observacoes_analise_vertical %}
|
||||||
<ul class="dcr-obs-lista">
|
<ul class="dcr-obs-lista">
|
||||||
{% for linha in observacoes_analise_vertical %}
|
{% for obs in observacoes_analise_vertical %}
|
||||||
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
|
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
|
||||||
<div class="dcr-obs-item__icone"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M18 17V9M13 17V5M8 17v-3"/></svg></div>
|
<div class="dcr-obs-item__icone"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M18 17V9M13 17V5M8 17v-3"/></svg></div>
|
||||||
<div>
|
<div>
|
||||||
<div class="dcr-obs-item__titulo">{{ linha.descricao }}</div>
|
<div class="dcr-obs-item__titulo">{{ obs.alvo_rotulo }}</div>
|
||||||
<div class="dcr-obs-item__texto">{{ linha.observacao }}</div>
|
<div class="dcr-obs-item__texto">{{ obs.texto }}</div>
|
||||||
|
<div class="dcr-obs-item__assinatura">{{ obs.criado_por.nome|default:"Equipe contábil" }} · {{ obs.criado_em|date:"d/m/Y" }}{% if obs.competencia_origem != apuracao.competencia %} · competência {{ obs.competencia_origem|competencia }}{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -879,30 +892,33 @@
|
|||||||
</h2>
|
</h2>
|
||||||
{% if observacoes_contas or observacoes_dre or observacoes_analise_vertical or achados_com_observacao %}
|
{% if observacoes_contas or observacoes_dre or observacoes_analise_vertical or achados_com_observacao %}
|
||||||
<ul class="dcr-obs-lista">
|
<ul class="dcr-obs-lista">
|
||||||
{% for conta in observacoes_contas %}
|
{% for obs in observacoes_contas %}
|
||||||
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
|
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
|
||||||
<div class="dcr-obs-item__icone"><svg width="15" height="15" 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></div>
|
<div class="dcr-obs-item__icone"><svg width="15" height="15" 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></div>
|
||||||
<div>
|
<div>
|
||||||
<div class="dcr-obs-item__titulo">Balancete — {{ conta.codigo }} {{ conta.descricao }}</div>
|
<div class="dcr-obs-item__titulo">Balancete — {{ obs.alvo_chave }} {{ obs.alvo_rotulo }}</div>
|
||||||
<div class="dcr-obs-item__texto">{{ conta.observacao }}</div>
|
<div class="dcr-obs-item__texto">{{ obs.texto }}</div>
|
||||||
|
<div class="dcr-obs-item__assinatura">{{ obs.criado_por.nome|default:"Equipe contábil" }} · {{ obs.criado_em|date:"d/m/Y" }}{% if obs.competencia_origem != apuracao.competencia %} · competência {{ obs.competencia_origem|competencia }}{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for linha in observacoes_dre %}
|
{% for obs in observacoes_dre %}
|
||||||
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
|
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
|
||||||
<div class="dcr-obs-item__icone"><svg width="15" height="15" 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 3v6h6"/></svg></div>
|
<div class="dcr-obs-item__icone"><svg width="15" height="15" 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 3v6h6"/></svg></div>
|
||||||
<div>
|
<div>
|
||||||
<div class="dcr-obs-item__titulo">D.R.E. — {{ linha.descricao }}</div>
|
<div class="dcr-obs-item__titulo">D.R.E. — {{ obs.alvo_rotulo }}</div>
|
||||||
<div class="dcr-obs-item__texto">{{ linha.observacao }}</div>
|
<div class="dcr-obs-item__texto">{{ obs.texto }}</div>
|
||||||
|
<div class="dcr-obs-item__assinatura">{{ obs.criado_por.nome|default:"Equipe contábil" }} · {{ obs.criado_em|date:"d/m/Y" }}{% if obs.competencia_origem != apuracao.competencia %} · competência {{ obs.competencia_origem|competencia }}{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for linha in observacoes_analise_vertical %}
|
{% for obs in observacoes_analise_vertical %}
|
||||||
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
|
<li class="dcr-obs-item" style="--i:{{ forloop.counter0 }}">
|
||||||
<div class="dcr-obs-item__icone"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M18 17V9M13 17V5M8 17v-3"/></svg></div>
|
<div class="dcr-obs-item__icone"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M18 17V9M13 17V5M8 17v-3"/></svg></div>
|
||||||
<div>
|
<div>
|
||||||
<div class="dcr-obs-item__titulo">Análise Vertical — {{ linha.descricao }}</div>
|
<div class="dcr-obs-item__titulo">Análise Vertical — {{ obs.alvo_rotulo }}</div>
|
||||||
<div class="dcr-obs-item__texto">{{ linha.observacao }}</div>
|
<div class="dcr-obs-item__texto">{{ obs.texto }}</div>
|
||||||
|
<div class="dcr-obs-item__assinatura">{{ obs.criado_por.nome|default:"Equipe contábil" }} · {{ obs.criado_em|date:"d/m/Y" }}{% if obs.competencia_origem != apuracao.competencia %} · competência {{ obs.competencia_origem|competencia }}{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -1116,10 +1132,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- Balancete/D.R.E.: painel de observação inline ----
|
// ---- Balancete/D.R.E.: painel de observação inline ----
|
||||||
// Ícone ao lado da descrição (só aparece quando a conta/linha tem uma
|
// Ícone ao lado da descrição (só aparece quando a conta/linha tem
|
||||||
// observação que o contador marcou como visível ao cliente, ver
|
// observação vigente marcada como visível ao cliente, ver
|
||||||
// `oculta_no_relatorio` — as ocultas nem chegam a este HTML) — clicar
|
// `ContabilObservacao.mostrar_ao_cliente` — as internas nem chegam a
|
||||||
// abre a linha logo abaixo com o texto, mesmo comportamento do editor
|
// este HTML) — clicar abre a linha logo abaixo com o texto (pode ter
|
||||||
|
// mais de uma, cada uma assinada), mesmo comportamento do painel
|
||||||
// inline da tela de revisão do Portal. Fica escondida por padrão
|
// inline da tela de revisão do Portal. Fica escondida por padrão
|
||||||
// (`hidden` no HTML) e some de novo se a linha-pai for escondida por
|
// (`hidden` no HTML) e some de novo se a linha-pai for escondida por
|
||||||
// um ancestral colapsado (`sincroniza()`, chamada depois de qualquer
|
// um ancestral colapsado (`sincroniza()`, chamada depois de qualquer
|
||||||
|
|||||||
@ -470,6 +470,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<section class="dc-obs-resumo">
|
<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>
|
<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>
|
<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>
|
<p class="dc-empty" id="dc-contas-obs-empty" hidden>Nenhuma observação registrada no Balancete ainda.</p>
|
||||||
</section>
|
</section>
|
||||||
@ -496,6 +501,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<section class="dc-obs-resumo">
|
<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>
|
<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>
|
<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>
|
<p class="dc-empty" id="dc-dre-obs-empty" hidden>Nenhuma observação registrada na DRE ainda.</p>
|
||||||
</section>
|
</section>
|
||||||
@ -514,6 +524,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<section class="dc-obs-resumo">
|
<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>
|
<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>
|
<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>
|
<p class="dc-empty" id="dc-av-obs-empty" hidden>Nenhuma observação registrada na Análise Vertical ainda.</p>
|
||||||
</section>
|
</section>
|
||||||
@ -546,6 +561,11 @@
|
|||||||
|
|
||||||
<section class="dc-dash-section">
|
<section class="dc-dash-section">
|
||||||
<h3 class="dc-dash-section__titulo">Observações</h3>
|
<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>
|
<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>
|
<p class="dc-empty" id="dc-dash-observacoes-empty" hidden>Nenhuma observação registrada ainda no Balancete, na DRE ou na auditoria.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user