Reestruturação Conciliação de Fornecedores
This commit is contained in:
parent
9ff4329758
commit
4ab224d9b3
@ -345,7 +345,7 @@ Ver `portal_api/nao_conformidades/CLAUDE.md`.
|
|||||||
|
|
||||||
## Conciliação de Fornecedores (Utilitários)
|
## Conciliação de Fornecedores (Utilitários)
|
||||||
|
|
||||||
O contador informa o código da empresa (nome buscado no Questor) e anexa o razão contábil de fornecedores exportado do Questor; a ferramenta vincula pagamentos e títulos de cada fornecedor por valor e data (inclusive combinações e parcelas) e aponta o que está pendente: títulos em aberto há mais de 2 meses, pagamentos sem título (divergência), diferenças de juros/desconto e conta transitória com saldo. Vínculo manual, validação por conta, histórico salvo e exportação XLSX. Restrita ao perfil Inovação.
|
O contador informa o código da empresa (nome buscado no Questor) e anexa o razão contábil de fornecedores exportado do Questor; a ferramenta vincula pagamentos e títulos de cada fornecedor por valor e data (inclusive combinações e parcelas) e aponta o que está pendente: pagamentos sem título (divergência), diferenças de juros/desconto e conta transitória com saldo; conta só com títulos em aberto é conciliada, e os títulos em aberto há mais de 3 meses são revisados numa aba à parte, com validação própria por fornecedor. Vínculo manual, validação por conta, histórico salvo e exportação XLSX. Restrita ao perfil Inovação.
|
||||||
|
|
||||||
Ver `portal_api/conciliacao_fornecedores/CLAUDE.md`.
|
Ver `portal_api/conciliacao_fornecedores/CLAUDE.md`.
|
||||||
|
|
||||||
|
|||||||
@ -160,3 +160,18 @@ Pergunta do usuário (BRASPRESS): o pagamento de R$ 234,40 era exatamente a soma
|
|||||||
### Rodada 33 — Demais fornecedores apagados com um expandido (2026-09-28)
|
### Rodada 33 — Demais fornecedores apagados com um expandido (2026-09-28)
|
||||||
|
|
||||||
Pedido do usuário: ao expandir um fornecedor, deixar os demais "apagados" para destacar o que está em análise. Só CSS (`conciliacao-fornecedores.css`): enquanto houver alguma `tr.conc-row.is-aberta` em `#conc-contas-body` (seletor `:has()`), as linhas de fornecedor não expandidas ganham o fundo `--bg-canvas` (o tom mais escuro do tema, mesmo das linhas em destaque do Relatório Contábil), fora do hover, que continua com o `--card-bg-hover` de sempre. A primeira versão só apagava por opacidade (0.35); o usuário pediu esse tom de fundo e, em seguida, que o texto continuasse apagado como antes: o fundo vai no `tr` e a opacidade 0.35 no `td` (com fundo transparente), pra opacidade não clarear o fundo junto. Com mais de um expandido, todos os expandidos ficam em destaque.
|
Pedido do usuário: ao expandir um fornecedor, deixar os demais "apagados" para destacar o que está em análise. Só CSS (`conciliacao-fornecedores.css`): enquanto houver alguma `tr.conc-row.is-aberta` em `#conc-contas-body` (seletor `:has()`), as linhas de fornecedor não expandidas ganham o fundo `--bg-canvas` (o tom mais escuro do tema, mesmo das linhas em destaque do Relatório Contábil), fora do hover, que continua com o `--card-bg-hover` de sempre. A primeira versão só apagava por opacidade (0.35); o usuário pediu esse tom de fundo e, em seguida, que o texto continuasse apagado como antes: o fundo vai no `tr` e a opacidade 0.35 no `td` (com fundo transparente), pra opacidade não clarear o fundo junto. Com mais de um expandido, todos os expandidos ficam em destaque.
|
||||||
|
|
||||||
|
### Rodada 34 — Só títulos em aberto é conciliada; tela à parte para os títulos antigos (2026-09-28)
|
||||||
|
|
||||||
|
Pedido do usuário (AUDIOFRAHM: 1 título de R$ 4.853,89 em aberto há mais de 3 meses, nenhum pagamento, conta em "Atenção"): (1) se há só saldo em aberto, sem pagamentos para conciliar, considerar conciliada automaticamente; (2) uma tela à parte para validar os títulos em aberto há mais de 3 meses; (3) essa situação não é bloqueio para análise, então não deve deixar a conta em "Atenção".
|
||||||
|
|
||||||
|
Decisões confirmadas com o usuário: validação da tela nova **por fornecedor** (não por título); chip "Pendentes de pagamento" **removido**; conciliações já salvas também recebem a validação automática, **ao abrir**.
|
||||||
|
|
||||||
|
O que mudou:
|
||||||
|
- `alertas.py`: o alerta `credito_vencido` saiu (os títulos antigos continuam na categoria `vencido` e em `total_vencido`); `STATUS_PENDENTE` saiu, e sem alerta a conta é `conciliada`, mesmo com títulos em aberto.
|
||||||
|
- Model: `vencidos_validados`/`vencidos_validados_por`/`vencidos_validados_em`/`vencidos_observacao` em `ConciliacaoFornecedorConta` (migração `0086_conciliacao_vencidos_validados`, aplicada). Endpoint novo `POST /conciliacoes-fornecedores-contas/{id}/validar-vencidos/`. Payload da conta com esses campos e `qtd_vencidos`; `resumo` com `contas_vencidas` (agora por `total_vencido > 0`) e `contas_vencidas_validadas`.
|
||||||
|
- `retrieve` reaplica a validação automática em todas as contas e regrava o `resumo` se mudou.
|
||||||
|
- XLSX: duas colunas novas no Resumo ("Títulos > 3 meses validados", vazia para quem não tem título antigo, e a observação).
|
||||||
|
- Tela: abas "Conciliação" / "Em aberto há mais de 3 meses" no detalhe; o card de vencidos abre a aba nova; a linha de fornecedor sem alerta diz "N título(s) em aberto, sem pagamento a conciliar".
|
||||||
|
|
||||||
|
Efeito na conciliação 20 (empresa 2017, testado dentro de transação desfeita): antes 146 conciliadas / 52 pendentes / 61 atenção / 1 divergente, 149 validadas; depois 233 conciliadas / 26 atenção / 1 divergente, 234 validadas (as 87 conciliadas com título em aberto, todas validadas automaticamente). 57 fornecedores com título antigo na aba nova. `validar-vencidos/` testado sem alterar a validação da conciliação; segunda abertura não sincroniza mais nada. A tela não foi renderizada neste ambiente.
|
||||||
|
|||||||
@ -48,22 +48,24 @@ As sugestões saem de uma **distribuição** dos pagamentos não vinculados sobr
|
|||||||
|
|
||||||
| Categoria | Significado (fato) | Alerta |
|
| Categoria | Significado (fato) | Alerta |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `em_aberto` | título sem pagamento vinculado (ou saldo de um pagamento parcial vinculado) | nenhum (conta fica "Pendente de pagamento") |
|
| `em_aberto` | título sem pagamento vinculado (ou saldo de um pagamento parcial vinculado) | nenhum |
|
||||||
| `vencido` | idem, há mais de 3 meses até a data-base | média (`credito_vencido`, valor real) |
|
| `vencido` | idem, há mais de 3 meses até a data-base | **nenhum** (desde a rodada 34 de `CHANGELOG.md` nesta pasta: "não é bloqueio para análise"; vai para `total_vencido` e para a aba "Em aberto há mais de 3 meses") |
|
||||||
| `nao_vinculado` | pagamento ainda não vinculado a um título | média (`pagamento_sem_titulo`, "N pagamentos ainda não vinculados, somando R$ X") |
|
| `nao_vinculado` | pagamento ainda não vinculado a um título | média (`pagamento_sem_titulo`, "N pagamentos ainda não vinculados, somando R$ X") |
|
||||||
| `excedente` | pagamento além de todos os títulos em aberto | **alta (divergência)** |
|
| `excedente` | pagamento além de todos os títulos em aberto | **alta (divergência)** |
|
||||||
|
|
||||||
Consequência nos totais: `total_em_aberto` (card "Títulos em aberto") é a soma real dos títulos sem baixa e pode passar do saldo contábil da conta quando há pagamentos a vincular. O card mostra os dois (`total_nao_vinculado` = "em pagamentos a vincular"); o saldo contábil continua na coluna "Saldo residual".
|
Consequência nos totais: `total_em_aberto` (card "Títulos em aberto") é a soma real dos títulos sem baixa e pode passar do saldo contábil da conta quando há pagamentos a vincular. O card mostra os dois (`total_nao_vinculado` = "em pagamentos a vincular"); o saldo contábil continua na coluna "Saldo residual".
|
||||||
|
|
||||||
Alertas de conta: `saldo_divergente_arquivo`, `conta_transitoria`, `saldo_devedor` (alta); `debito_excedente` (alta); `credito_vencido`, `pagamento_sem_titulo` (média); `diferenca_vinculo` (baixa, vínculo automático por tolerância ou manual com diferença; diferença negativa = pago a maior, possível juros/multa; positiva = pago a menor, possível desconto).
|
Alertas de conta: `saldo_divergente_arquivo`, `conta_transitoria`, `saldo_devedor` (alta); `debito_excedente` (alta); `pagamento_sem_titulo` (média); `diferenca_vinculo` (baixa, vínculo automático por tolerância ou manual com diferença; diferença negativa = pago a maior, possível juros/multa; positiva = pago a menor, possível desconto).
|
||||||
|
|
||||||
Status: `divergente` (algum alerta alto) > `atencao` (médio/baixo) > `pendente` (só títulos recentes em aberto) > `conciliada`.
|
Status: `divergente` (algum alerta alto) > `atencao` (médio/baixo) > `conciliada` (nenhum alerta). **Só títulos em aberto, sem pagamento a conciliar, é `conciliada`** (pedido do usuário, rodada 34): o que falta ali é o pagamento acontecer, não uma análise. A situação "Pendente de pagamento" (`pendente`) deixou de existir, e o chip dela saiu da tela; o `resumo` gravado de uma conciliação antiga ainda pode ter a chave `pendente` até ela ser aberta de novo.
|
||||||
|
|
||||||
**Validação é uma marcação à parte e nunca muda a situação** (pedido do usuário): uma conta em "Atenção" validada continua "Atenção", com o checkbox marcado. Conta "Conciliada" já nasce validada (`_conciliacao_valida_conciliadas()` na criação). A validação automática é a que tem `validada_por` nulo (`validada_automatica` no payload): depois de vincular/desfazer, `_conciliacao_sincroniza_validacao_automatica()` a aplica se a conta ficou conciliada e a retira se deixou de estar; a validação feita por uma pessoa (endpoint `validar/`) nunca é mexida por essa regra.
|
**Validação é uma marcação à parte e nunca muda a situação** (pedido do usuário): uma conta em "Atenção" validada continua "Atenção", com o checkbox marcado. Conta "Conciliada" já nasce validada (`_conciliacao_valida_conciliadas()` na criação). A validação automática é a que tem `validada_por` nulo (`validada_automatica` no payload): depois de vincular/desfazer, `_conciliacao_sincroniza_validacao_automatica()` a aplica se a conta ficou conciliada e a retira se deixou de estar; a validação feita por uma pessoa (endpoint `validar/`) nunca é mexida por essa regra. **Ao abrir uma conciliação** (`retrieve`), a mesma sincronização roda em todas as contas e o `resumo` é regravado se mudou (pedido do usuário, para as conciliações anteriores à regra nova de "conciliada" também receberem a validação automática).
|
||||||
|
|
||||||
|
**Revisão dos títulos antigos é outra validação, por fornecedor** (pedido do usuário): `vencidos_validados`/`vencidos_validados_por`/`vencidos_validados_em`/`vencidos_observacao` na conta (migração `0086`), gravados por `validar-vencidos/`. Independente de `validada`: validar uma não mexe na outra, e a validação automática nunca toca nesses campos. Não é zerada se os títulos antigos mudarem (ex.: um deles for vinculado depois); se o fornecedor deixar de ter título antigo, só sai da aba.
|
||||||
|
|
||||||
## Models (`portal_api/models.py`)
|
## Models (`portal_api/models.py`)
|
||||||
|
|
||||||
`ConciliacaoFornecedor` (cabeçalho: empresa, data-base, período, arquivo, tolerâncias, `resumo` JSON) → `ConciliacaoFornecedorConta` (saldos, `validada`/`validada_por`/`validada_em`, `observacao`) → `ConciliacaoFornecedorVinculo` (`tipo` exato/combinacao/saldo_zerado/tolerancia/manual/**parcial**, `diferenca` = créditos − débitos, `criado_por` só no manual) e `ConciliacaoFornecedorLancamento` (`vinculo` FK `SET_NULL`: excluir o vínculo devolve os lançamentos para pendente). `ConciliacaoFornecedorVinculoHistorico` (por conta): cada ação manual sobre vínculo (`acao` vinculou/acrescentou/desfez, este último inclusive para vínculo automático), com `usuario`, `criado_em`, `tipo_rotulo`/`vinculo_automatico` e **uma cópia dos lançamentos envolvidos** em `lancamentos` (JSON com seq., data, natureza, valor e `acrescentado`), porque o vínculo desfeito é apagado. Gravado por `_conciliacao_registra_historico()` dentro da mesma transação da ação. Vínculos criados pelo motor na importação **não** entram (não são ação de ninguém); ações anteriores à rodada 23 não têm registro.
|
`ConciliacaoFornecedor` (cabeçalho: empresa, data-base, período, arquivo, tolerâncias, `resumo` JSON) → `ConciliacaoFornecedorConta` (saldos, `validada`/`validada_por`/`validada_em`, `observacao`, e a validação à parte dos títulos antigos `vencidos_validados`/`_por`/`_em`/`vencidos_observacao`) → `ConciliacaoFornecedorVinculo` (`tipo` exato/combinacao/saldo_zerado/tolerancia/manual/**parcial**, `diferenca` = créditos − débitos, `criado_por` só no manual) e `ConciliacaoFornecedorLancamento` (`vinculo` FK `SET_NULL`: excluir o vínculo devolve os lançamentos para pendente). `ConciliacaoFornecedorVinculoHistorico` (por conta): cada ação manual sobre vínculo (`acao` vinculou/acrescentou/desfez, este último inclusive para vínculo automático), com `usuario`, `criado_em`, `tipo_rotulo`/`vinculo_automatico` e **uma cópia dos lançamentos envolvidos** em `lancamentos` (JSON com seq., data, natureza, valor e `acrescentado`), porque o vínculo desfeito é apagado. Gravado por `_conciliacao_registra_historico()` dentro da mesma transação da ação. Vínculos criados pelo motor na importação **não** entram (não são ação de ninguém); ações anteriores à rodada 23 não têm registro.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
@ -72,11 +74,12 @@ Todas com `PermissaoApp("utilitarios", "conciliacao-fornecedores")`.
|
|||||||
| Endpoint | Método | Uso |
|
| Endpoint | Método | Uso |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `/api/conciliacoes-fornecedores/` | GET/POST | histórico (`?codigo_empresa=` filtra) / criação multipart (só `codigo_empresa` + `arquivo`; data-base e tolerâncias são definidas no servidor) |
|
| `/api/conciliacoes-fornecedores/` | GET/POST | histórico (`?codigo_empresa=` filtra) / criação multipart (só `codigo_empresa` + `arquivo`; data-base e tolerâncias são definidas no servidor) |
|
||||||
| `/api/conciliacoes-fornecedores/{id}/` | GET/DELETE | detalhe completo (contas + lançamentos + vínculos + análise, ~1 MB para 2.500 lançamentos) / exclusão (apaga o arquivo depois do commit) |
|
| `/api/conciliacoes-fornecedores/{id}/` | GET/DELETE | detalhe completo (contas + lançamentos + vínculos + análise, ~1 MB para 2.500 lançamentos; reaplica a validação automática e regrava o `resumo` se mudou) / exclusão (apaga o arquivo depois do commit) |
|
||||||
| `/api/conciliacoes-fornecedores/nome-empresa/?codigo_empresa=` | GET | nome no Questor (`null` se não achou ou Questor fora) |
|
| `/api/conciliacoes-fornecedores/nome-empresa/?codigo_empresa=` | GET | nome no Questor (`null` se não achou ou Questor fora) |
|
||||||
| `/api/conciliacoes-fornecedores/{id}/exportar-xlsx/` | GET | planilha com abas Resumo, Pendências e Alertas |
|
| `/api/conciliacoes-fornecedores/{id}/exportar-xlsx/` | GET | planilha com abas Resumo, Pendências e Alertas |
|
||||||
| `/api/conciliacoes-fornecedores-contas/{id}/vincular/` | POST | `{lancamentos: [ids]}`: vínculo manual; exige itens sem vínculo, da própria conta, com pelo menos um débito e um crédito. Créditos maiores que débitos **além da tolerância** da conciliação viram `tipo="parcial"` (pagamento parcial: a `diferenca` continua em aberto, entra na distribuição como um título com a data do crédito mais antigo do vínculo e conta para o aging); dentro da tolerância, ou pago a maior, `tipo="manual"` (fecha, com alerta de diferença) |
|
| `/api/conciliacoes-fornecedores-contas/{id}/vincular/` | POST | `{lancamentos: [ids]}`: vínculo manual; exige itens sem vínculo, da própria conta, com pelo menos um débito e um crédito. Créditos maiores que débitos **além da tolerância** da conciliação viram `tipo="parcial"` (pagamento parcial: a `diferenca` continua em aberto, entra na distribuição como um título com a data do crédito mais antigo do vínculo e conta para o aging); dentro da tolerância, ou pago a maior, `tipo="manual"` (fecha, com alerta de diferença) |
|
||||||
| `/api/conciliacoes-fornecedores-contas/{id}/validar/` | POST | `{validada, observacao}` |
|
| `/api/conciliacoes-fornecedores-contas/{id}/validar/` | POST | `{validada, observacao}` |
|
||||||
|
| `/api/conciliacoes-fornecedores-contas/{id}/validar-vencidos/` | POST | `{validada, observacao?}`: validação e observação dos títulos em aberto há mais de 3 meses do fornecedor (aba própria), independentes de `validar/` |
|
||||||
| `/api/conciliacoes-fornecedores-contas/{id}/historico-vinculos/` | GET | histórico das ações manuais sobre vínculos da conta, mais recente primeiro (botão "i" do razão) |
|
| `/api/conciliacoes-fornecedores-contas/{id}/historico-vinculos/` | GET | histórico das ações manuais sobre vínculos da conta, mais recente primeiro (botão "i" do razão) |
|
||||||
| `/api/conciliacoes-fornecedores-vinculos/{id}/` | DELETE | desfaz o vínculo |
|
| `/api/conciliacoes-fornecedores-vinculos/{id}/` | DELETE | desfaz o vínculo |
|
||||||
| `/api/conciliacoes-fornecedores-vinculos/{id}/adicionar/` | POST | `{lancamentos: [ids]}` (1 ou mais): acrescenta lançamentos pendentes da mesma conta a um vínculo **manual ou parcial** (automático é 400), sem desfazer. Tipo e diferença recalculados por `_conciliacao_tipo_vinculo_manual()`, a mesma regra da criação (pode continuar parcial, fechar ou virar pago a maior); `criado_por` passa a ser quem acrescentou |
|
| `/api/conciliacoes-fornecedores-vinculos/{id}/adicionar/` | POST | `{lancamentos: [ids]}` (1 ou mais): acrescenta lançamentos pendentes da mesma conta a um vínculo **manual ou parcial** (automático é 400), sem desfazer. Tipo e diferença recalculados por `_conciliacao_tipo_vinculo_manual()`, a mesma regra da criação (pode continuar parcial, fechar ou virar pago a maior); `criado_por` passa a ser quem acrescentou |
|
||||||
@ -87,9 +90,10 @@ Os três endpoints de ajuste devolvem `{conta, resumo}`: a conta reanalisada e o
|
|||||||
|
|
||||||
Duas vistas na mesma página. **Histórico**: formulário de nova conciliação (o código da empresa busca o nome no Questor com debounce; o nome do arquivo com prefixo numérico sugere o código) + tabela do histórico. **Detalhe** (`#conciliacao-<id>` na URL), desenhado para ser leve (pedido do usuário, validado antes num protótipo):
|
Duas vistas na mesma página. **Histórico**: formulário de nova conciliação (o código da empresa busca o nome no Questor com debounce; o nome do arquivo com prefixo numérico sugere o código) + tabela do histórico. **Detalhe** (`#conciliacao-<id>` na URL), desenhado para ser leve (pedido do usuário, validado antes num protótipo):
|
||||||
- cabeçalho da empresa baixo, código e nome na mesma linha;
|
- cabeçalho da empresa baixo, código e nome na mesma linha;
|
||||||
- **uma faixa de indicadores**: saldo em aberto, em aberto há mais de 3 meses (clica e filtra), pagamentos sem título (divergência, clica e filtra) e uma **barra de progresso da validação** ("146 de 260");
|
- **uma faixa de indicadores**: saldo em aberto, em aberto há mais de 3 meses (clica e abre a aba própria, com quantos fornecedores já foram validados nela), pagamentos sem título (divergência, clica e filtra) e uma **barra de progresso da validação** ("146 de 260");
|
||||||
- os apontamentos menores (diferença de juros/desconto, pagamento sem título de mesmo valor, conta transitória) como **etiquetas** clicáveis numa linha, só quando existem;
|
- os apontamentos menores (diferença de juros/desconto, pagamento sem título de mesmo valor, conta transitória) como **etiquetas** clicáveis numa linha, só quando existem;
|
||||||
- **os números por situação ficam só nos chips** (Todos/Divergentes/Atenção/Pendentes/Conciliados/Validados), que também filtram: não duplicar em card;
|
- **duas abas** (`.pa-tabs`, `mostrarAba()`): "Conciliação" (tudo abaixo) e **"Em aberto há mais de 3 meses"** (contador de fornecedores ainda a validar): fornecedores com `total_vencido > 0`, chips A validar (padrão)/Validados/Todos, busca, tabela ordenável (conta, fornecedor, quantidade de títulos, mais antigo com dias em aberto, valor, checkbox "Validado"); expandir mostra os títulos antigos (`titulosVencidos()`: lançamentos com `situacao.categoria === "vencido"` mais saldos de pagamento parcial vencidos) com dias em aberto até a data-base, observação e "Marcar como validado". Validado some da lista com fade (mesma ideia de `removerConta()`), e a expansão recolhe. Mesmo visual de linhas apagadas da aba principal;
|
||||||
|
- **os números por situação ficam só nos chips** (Todos/Divergentes/Atenção/Conciliados/Validados), que também filtram: não duplicar em card;
|
||||||
- **contas validadas escondidas por padrão** ("Mostrar validadas" ao lado da busca; o chip "Validados" mostra sempre), para a lista mostrar o que falta revisar; depois de qualquer ajuste numa conta (validar, vincular, desfazer), `aplicarRespostaConta()` reaplica o filtro (`contaPassaFiltro()`, a mesma regra de `contasFiltradas()`) e, se a conta não couber mais, ela sai na hora com um fade (`removerConta()`), sem esperar o próximo render da lista;
|
- **contas validadas escondidas por padrão** ("Mostrar validadas" ao lado da busca; o chip "Validados" mostra sempre), para a lista mostrar o que falta revisar; depois de qualquer ajuste numa conta (validar, vincular, desfazer), `aplicarRespostaConta()` reaplica o filtro (`contaPassaFiltro()`, a mesma regra de `contasFiltradas()`) e, se a conta não couber mais, ela sai na hora com um fade (`removerConta()`), sem esperar o próximo render da lista;
|
||||||
- filtros e cabeçalho da tabela **fixos ao rolar** (`position: sticky` abaixo da `.topbar`; `.conc-table-wrap` desliga o `overflow:hidden` de `.pa-table-wrap`, que anularia o sticky);
|
- filtros e cabeçalho da tabela **fixos ao rolar** (`position: sticky` abaixo da `.topbar`; `.conc-table-wrap` desliga o `overflow:hidden` de `.pa-table-wrap`, que anularia o sticky);
|
||||||
- linhas compactas, com o fornecedor e o primeiro alerta cortados com reticências e a situação só como etiqueta.
|
- linhas compactas, com o fornecedor e o primeiro alerta cortados com reticências e a situação só como etiqueta.
|
||||||
|
|||||||
@ -6,7 +6,7 @@ Ferramenta para o contador conciliar as contas de fornecedores de uma empresa. E
|
|||||||
|
|
||||||
- vincula, por valor e data, os pagamentos (débitos) aos títulos (créditos) de cada fornecedor, inclusive vários pagamentos somando um título e parcelas;
|
- vincula, por valor e data, os pagamentos (débitos) aos títulos (créditos) de cada fornecedor, inclusive vários pagamentos somando um título e parcelas;
|
||||||
- mostra, por fornecedor, conta, saldo inicial, saldo residual e a situação (conciliada, pendente de pagamento, atenção, divergente, validada);
|
- mostra, por fornecedor, conta, saldo inicial, saldo residual e a situação (conciliada, pendente de pagamento, atenção, divergente, validada);
|
||||||
- alerta títulos em aberto há mais de 2 meses, pagamentos sem título (divergência), diferenças pequenas (possível juros/desconto) e conta transitória com saldo;
|
- separa numa tela própria os títulos em aberto há mais de 3 meses, com validação por fornecedor (não bloqueiam a conciliação); alerta pagamentos sem título (divergência), diferenças pequenas (possível juros/desconto) e conta transitória com saldo;
|
||||||
- deixa o contador desfazer e criar vínculos, registrar observação e validar a conta;
|
- deixa o contador desfazer e criar vínculos, registrar observação e validar a conta;
|
||||||
- guarda o histórico e exporta o resultado em XLSX.
|
- guarda o histórico e exporta o resultado em XLSX.
|
||||||
|
|
||||||
|
|||||||
@ -46,7 +46,6 @@ SEVERIDADE_MEDIA = "media"
|
|||||||
SEVERIDADE_BAIXA = "baixa"
|
SEVERIDADE_BAIXA = "baixa"
|
||||||
|
|
||||||
STATUS_CONCILIADA = "conciliada"
|
STATUS_CONCILIADA = "conciliada"
|
||||||
STATUS_PENDENTE = "pendente"
|
|
||||||
STATUS_ATENCAO = "atencao"
|
STATUS_ATENCAO = "atencao"
|
||||||
STATUS_DIVERGENTE = "divergente"
|
STATUS_DIVERGENTE = "divergente"
|
||||||
|
|
||||||
@ -424,15 +423,10 @@ def analisa_conta(
|
|||||||
f"{quantidade} pagamento(s) sem título em aberto correspondente, somando {_moeda(total_excedente)}.",
|
f"{quantidade} pagamento(s) sem título em aberto correspondente, somando {_moeda(total_excedente)}.",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if total_vencido > 0:
|
# Título em aberto há mais de `MESES_VENCIDO` meses não é alerta da
|
||||||
quantidade = sum(1 for s in situacoes.values() if s.categoria == CATEGORIA_VENCIDO)
|
# conciliação (pedido do usuário: "não é bloqueio para análise"): fica em
|
||||||
alertas.append(
|
# `total_vencido` e na categoria `vencido` de cada título, revisado numa
|
||||||
AlertaConta(
|
# tela à parte, com validação própria por fornecedor.
|
||||||
"credito_vencido",
|
|
||||||
SEVERIDADE_MEDIA,
|
|
||||||
f"{quantidade} título(s) em aberto há mais de {MESES_VENCIDO} meses, somando {_moeda(total_vencido)}.",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if total_nao_vinculado > 0:
|
if total_nao_vinculado > 0:
|
||||||
quantidade = sum(1 for s in situacoes.values() if s.categoria == CATEGORIA_NAO_VINCULADO)
|
quantidade = sum(1 for s in situacoes.values() if s.categoria == CATEGORIA_NAO_VINCULADO)
|
||||||
alertas.append(
|
alertas.append(
|
||||||
@ -453,13 +447,14 @@ def analisa_conta(
|
|||||||
AlertaConta("diferenca_vinculo", SEVERIDADE_BAIXA, f"{origem} com diferença de {_moeda(vinculo.diferenca)}: {natureza}.")
|
AlertaConta("diferenca_vinculo", SEVERIDADE_BAIXA, f"{origem} com diferença de {_moeda(vinculo.diferenca)}: {natureza}.")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Só títulos em aberto, sem pagamento a conciliar, é conta conciliada
|
||||||
|
# (pedido do usuário; antes era "Pendente de pagamento", que deixou de
|
||||||
|
# existir): o que falta ali é o pagamento acontecer, não uma análise.
|
||||||
severidades = {a.severidade for a in alertas}
|
severidades = {a.severidade for a in alertas}
|
||||||
if SEVERIDADE_ALTA in severidades:
|
if SEVERIDADE_ALTA in severidades:
|
||||||
status = STATUS_DIVERGENTE
|
status = STATUS_DIVERGENTE
|
||||||
elif severidades:
|
elif severidades:
|
||||||
status = STATUS_ATENCAO
|
status = STATUS_ATENCAO
|
||||||
elif total_aberto > 0:
|
|
||||||
status = STATUS_PENDENTE
|
|
||||||
else:
|
else:
|
||||||
status = STATUS_CONCILIADA
|
status = STATUS_CONCILIADA
|
||||||
|
|
||||||
|
|||||||
@ -38,6 +38,9 @@ class ContaXlsx:
|
|||||||
situacao: str
|
situacao: str
|
||||||
validada: bool
|
validada: bool
|
||||||
observacao: str
|
observacao: str
|
||||||
|
# revisão dos títulos em aberto há mais de 3 meses (tela à parte)
|
||||||
|
vencidos_validados: bool = False
|
||||||
|
vencidos_observacao: str = ""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -114,6 +117,8 @@ def gera_xlsx(
|
|||||||
"Situação",
|
"Situação",
|
||||||
"Validada",
|
"Validada",
|
||||||
"Observação",
|
"Observação",
|
||||||
|
"Títulos > 3 meses validados",
|
||||||
|
"Observação (títulos > 3 meses)",
|
||||||
]
|
]
|
||||||
linha = _cabecalho(ws, "Conciliação de Fornecedores - Resumo", empresa, subtitulo, colunas)
|
linha = _cabecalho(ws, "Conciliação de Fornecedores - Resumo", empresa, subtitulo, colunas)
|
||||||
for conta in contas:
|
for conta in contas:
|
||||||
@ -129,13 +134,15 @@ def gera_xlsx(
|
|||||||
conta.situacao,
|
conta.situacao,
|
||||||
"Sim" if conta.validada else "Não",
|
"Sim" if conta.validada else "Não",
|
||||||
conta.observacao,
|
conta.observacao,
|
||||||
|
("Sim" if conta.vencidos_validados else "Não") if conta.total_vencido else "",
|
||||||
|
conta.vencidos_observacao,
|
||||||
]
|
]
|
||||||
for coluna, valor in enumerate(valores, start=1):
|
for coluna, valor in enumerate(valores, start=1):
|
||||||
celula = ws.cell(row=linha, column=coluna, value=valor)
|
celula = ws.cell(row=linha, column=coluna, value=valor)
|
||||||
if 4 <= coluna <= 8:
|
if 4 <= coluna <= 8:
|
||||||
celula.number_format = FORMATO_MOEDA
|
celula.number_format = FORMATO_MOEDA
|
||||||
linha += 1
|
linha += 1
|
||||||
_larguras(ws, [12, 18, 45, 16, 16, 16, 18, 18, 20, 10, 50])
|
_larguras(ws, [12, 18, 45, 16, 16, 16, 18, 18, 20, 10, 50, 14, 50])
|
||||||
|
|
||||||
ws = wb.create_sheet("Pendências")
|
ws = wb.create_sheet("Pendências")
|
||||||
colunas = ["Conta", "Fornecedor", "Data", "Seqüência", "Histórico", "Natureza", "Valor", "Valor em aberto", "Situação"]
|
colunas = ["Conta", "Fornecedor", "Data", "Seqüência", "Histórico", "Natureza", "Valor", "Valor em aberto", "Situação"]
|
||||||
|
|||||||
35
portal_api/migrations/0086_conciliacao_vencidos_validados.py
Normal file
35
portal_api/migrations/0086_conciliacao_vencidos_validados.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Generated by Django 6.0.7 on 2026-09-28 19:27
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('portal_api', '0085_conciliacao_vinculo_sequencia'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='conciliacaofornecedorconta',
|
||||||
|
name='vencidos_observacao',
|
||||||
|
field=models.TextField(blank=True, verbose_name='Observação sobre os títulos antigos'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='conciliacaofornecedorconta',
|
||||||
|
name='vencidos_validados',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Títulos antigos validados'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='conciliacaofornecedorconta',
|
||||||
|
name='vencidos_validados_em',
|
||||||
|
field=models.DateTimeField(blank=True, null=True, verbose_name='Títulos antigos validados em'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='conciliacaofornecedorconta',
|
||||||
|
name='vencidos_validados_por',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -2750,6 +2750,13 @@ class ConciliacaoFornecedorConta(models.Model):
|
|||||||
validada_por = models.ForeignKey(Usuario, on_delete=models.SET_NULL, null=True, blank=True, related_name="+")
|
validada_por = models.ForeignKey(Usuario, on_delete=models.SET_NULL, null=True, blank=True, related_name="+")
|
||||||
validada_em = models.DateTimeField("Validada em", null=True, blank=True)
|
validada_em = models.DateTimeField("Validada em", null=True, blank=True)
|
||||||
observacao = models.TextField("Observação do contador", blank=True)
|
observacao = models.TextField("Observação do contador", blank=True)
|
||||||
|
# Revisão dos títulos em aberto há mais de 3 meses (tela à parte, pedido
|
||||||
|
# do usuário): validação própria por fornecedor, independente da
|
||||||
|
# validação da conciliação acima.
|
||||||
|
vencidos_validados = models.BooleanField("Títulos antigos validados", default=False)
|
||||||
|
vencidos_validados_por = models.ForeignKey(Usuario, on_delete=models.SET_NULL, null=True, blank=True, related_name="+")
|
||||||
|
vencidos_validados_em = models.DateTimeField("Títulos antigos validados em", null=True, blank=True)
|
||||||
|
vencidos_observacao = models.TextField("Observação sobre os títulos antigos", blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Conta (Conciliação de fornecedores)"
|
verbose_name = "Conta (Conciliação de fornecedores)"
|
||||||
|
|||||||
@ -5253,7 +5253,6 @@ class IndicadorContabilDefinicaoViewSet(viewsets.ModelViewSet):
|
|||||||
_CONCILIACAO_PERMISSAO = ("utilitarios", "conciliacao-fornecedores")
|
_CONCILIACAO_PERMISSAO = ("utilitarios", "conciliacao-fornecedores")
|
||||||
_CONCILIACAO_ROTULO_STATUS = {
|
_CONCILIACAO_ROTULO_STATUS = {
|
||||||
conciliacao_alertas.STATUS_CONCILIADA: "Conciliada",
|
conciliacao_alertas.STATUS_CONCILIADA: "Conciliada",
|
||||||
conciliacao_alertas.STATUS_PENDENTE: "Pendente de pagamento",
|
|
||||||
conciliacao_alertas.STATUS_ATENCAO: "Atenção",
|
conciliacao_alertas.STATUS_ATENCAO: "Atenção",
|
||||||
conciliacao_alertas.STATUS_DIVERGENTE: "Divergente",
|
conciliacao_alertas.STATUS_DIVERGENTE: "Divergente",
|
||||||
}
|
}
|
||||||
@ -5368,6 +5367,11 @@ def _conciliacao_conta_payload(
|
|||||||
"validada_automatica": conta.validada and conta.validada_por_id is None,
|
"validada_automatica": conta.validada and conta.validada_por_id is None,
|
||||||
"validada_em": conta.validada_em.isoformat() if conta.validada_em else None,
|
"validada_em": conta.validada_em.isoformat() if conta.validada_em else None,
|
||||||
"observacao": conta.observacao,
|
"observacao": conta.observacao,
|
||||||
|
"vencidos_validados": conta.vencidos_validados,
|
||||||
|
"vencidos_validados_por_nome": conta.vencidos_validados_por.nome if conta.vencidos_validados_por else None,
|
||||||
|
"vencidos_validados_em": conta.vencidos_validados_em.isoformat() if conta.vencidos_validados_em else None,
|
||||||
|
"vencidos_observacao": conta.vencidos_observacao,
|
||||||
|
"qtd_vencidos": sum(1 for s in analise.situacoes.values() if s.categoria == conciliacao_alertas.CATEGORIA_VENCIDO),
|
||||||
"status": analise.status,
|
"status": analise.status,
|
||||||
"status_rotulo": _CONCILIACAO_ROTULO_STATUS[analise.status],
|
"status_rotulo": _CONCILIACAO_ROTULO_STATUS[analise.status],
|
||||||
"transitoria": conciliacao_alertas.eh_conta_transitoria(conta.fornecedor),
|
"transitoria": conciliacao_alertas.eh_conta_transitoria(conta.fornecedor),
|
||||||
@ -5416,7 +5420,7 @@ def _conciliacao_carrega(
|
|||||||
"""Contas + lançamentos + vínculos + análise, com 3 consultas no total
|
"""Contas + lançamentos + vínculos + análise, com 3 consultas no total
|
||||||
(não uma por conta)."""
|
(não uma por conta)."""
|
||||||
if contas is None:
|
if contas is None:
|
||||||
contas = list(conciliacao.contas.select_related("validada_por").order_by("ordem", "id"))
|
contas = list(conciliacao.contas.select_related("validada_por", "vencidos_validados_por").order_by("ordem", "id"))
|
||||||
ids = [c.id for c in contas]
|
ids = [c.id for c in contas]
|
||||||
lancs_por_conta: dict[int, list[ConciliacaoFornecedorLancamento]] = {i: [] for i in ids}
|
lancs_por_conta: dict[int, list[ConciliacaoFornecedorLancamento]] = {i: [] for i in ids}
|
||||||
for lanc in ConciliacaoFornecedorLancamento.objects.filter(conta_id__in=ids).order_by("ordem", "id"):
|
for lanc in ConciliacaoFornecedorLancamento.objects.filter(conta_id__in=ids).order_by("ordem", "id"):
|
||||||
@ -5438,11 +5442,14 @@ def _conciliacao_carrega(
|
|||||||
def _conciliacao_calcula_resumo(carregado: list[_ConciliacaoContaCarregada]) -> dict[str, Any]:
|
def _conciliacao_calcula_resumo(carregado: list[_ConciliacaoContaCarregada]) -> dict[str, Any]:
|
||||||
por_status = {chave: 0 for chave in _CONCILIACAO_ROTULO_STATUS}
|
por_status = {chave: 0 for chave in _CONCILIACAO_ROTULO_STATUS}
|
||||||
total_aberto = total_vencido = total_excedente = total_nao_vinculado = Decimal("0")
|
total_aberto = total_vencido = total_excedente = total_nao_vinculado = Decimal("0")
|
||||||
validadas = 0
|
validadas = contas_vencidas = contas_vencidas_validadas = 0
|
||||||
contas_por_alerta = {"credito_vencido": 0, "diferenca_vinculo": 0, "pagamento_sem_titulo": 0, "conta_transitoria": 0}
|
contas_por_alerta = {"diferenca_vinculo": 0, "pagamento_sem_titulo": 0, "conta_transitoria": 0}
|
||||||
for conta, _, _, analise in carregado:
|
for conta, _, _, analise in carregado:
|
||||||
por_status[analise.status] += 1
|
por_status[analise.status] += 1
|
||||||
validadas += conta.validada
|
validadas += conta.validada
|
||||||
|
if analise.total_vencido > 0:
|
||||||
|
contas_vencidas += 1
|
||||||
|
contas_vencidas_validadas += conta.vencidos_validados
|
||||||
total_aberto += analise.total_em_aberto
|
total_aberto += analise.total_em_aberto
|
||||||
total_vencido += analise.total_vencido
|
total_vencido += analise.total_vencido
|
||||||
total_excedente += analise.total_excedente
|
total_excedente += analise.total_excedente
|
||||||
@ -5458,7 +5465,8 @@ def _conciliacao_calcula_resumo(carregado: list[_ConciliacaoContaCarregada]) ->
|
|||||||
"total_vencido": str(total_vencido),
|
"total_vencido": str(total_vencido),
|
||||||
"total_excedente": str(total_excedente),
|
"total_excedente": str(total_excedente),
|
||||||
"total_nao_vinculado": str(total_nao_vinculado),
|
"total_nao_vinculado": str(total_nao_vinculado),
|
||||||
"contas_vencidas": contas_por_alerta["credito_vencido"],
|
"contas_vencidas": contas_vencidas,
|
||||||
|
"contas_vencidas_validadas": contas_vencidas_validadas,
|
||||||
"contas_com_diferenca": contas_por_alerta["diferenca_vinculo"],
|
"contas_com_diferenca": contas_por_alerta["diferenca_vinculo"],
|
||||||
"contas_pagamento_sem_titulo": contas_por_alerta["pagamento_sem_titulo"],
|
"contas_pagamento_sem_titulo": contas_por_alerta["pagamento_sem_titulo"],
|
||||||
"contas_transitorias": contas_por_alerta["conta_transitoria"],
|
"contas_transitorias": contas_por_alerta["conta_transitoria"],
|
||||||
@ -5490,7 +5498,7 @@ def _conciliacao_resposta_conta(conta: ConciliacaoFornecedorConta, sincroniza_va
|
|||||||
vincular/desfazer, reaplica a validação automática; depois de o
|
vincular/desfazer, reaplica a validação automática; depois de o
|
||||||
contador marcar/desmarcar à mão (`sincroniza_validacao=False`), não."""
|
contador marcar/desmarcar à mão (`sincroniza_validacao=False`), não."""
|
||||||
conciliacao = conta.conciliacao
|
conciliacao = conta.conciliacao
|
||||||
conta = ConciliacaoFornecedorConta.objects.select_related("validada_por").get(pk=conta.pk)
|
conta = ConciliacaoFornecedorConta.objects.select_related("validada_por", "vencidos_validados_por").get(pk=conta.pk)
|
||||||
[carregada] = _conciliacao_carrega(conciliacao, [conta])
|
[carregada] = _conciliacao_carrega(conciliacao, [conta])
|
||||||
if sincroniza_validacao and _conciliacao_sincroniza_validacao_automatica(conta, carregada[3]):
|
if sincroniza_validacao and _conciliacao_sincroniza_validacao_automatica(conta, carregada[3]):
|
||||||
conta.save(update_fields=["validada", "validada_por", "validada_em"])
|
conta.save(update_fields=["validada", "validada_por", "validada_em"])
|
||||||
@ -5590,6 +5598,8 @@ def _conciliacao_gera_xlsx(conciliacao: ConciliacaoFornecedor) -> bytes:
|
|||||||
situacao=_CONCILIACAO_ROTULO_STATUS[analise.status],
|
situacao=_CONCILIACAO_ROTULO_STATUS[analise.status],
|
||||||
validada=conta.validada,
|
validada=conta.validada,
|
||||||
observacao=conta.observacao,
|
observacao=conta.observacao,
|
||||||
|
vencidos_validados=conta.vencidos_validados,
|
||||||
|
vencidos_observacao=conta.vencidos_observacao,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
for vinc in vincs:
|
for vinc in vincs:
|
||||||
@ -5672,8 +5682,19 @@ class ConciliacaoFornecedorViewSet(viewsets.ModelViewSet):
|
|||||||
def retrieve(self, request: Request, *args: Any, **kwargs: Any) -> Response:
|
def retrieve(self, request: Request, *args: Any, **kwargs: Any) -> Response:
|
||||||
conciliacao = self.get_object()
|
conciliacao = self.get_object()
|
||||||
carregado = _conciliacao_carrega(conciliacao)
|
carregado = _conciliacao_carrega(conciliacao)
|
||||||
|
# A regra de situação pode mudar depois da importação (ex.: "só
|
||||||
|
# títulos em aberto" passou a ser conciliada): ao abrir, reaplica a
|
||||||
|
# validação automática, como na criação (pedido do usuário). A feita
|
||||||
|
# por uma pessoa nunca é mexida.
|
||||||
|
alteradas = [conta for conta, _, _, analise in carregado if _conciliacao_sincroniza_validacao_automatica(conta, analise)]
|
||||||
|
resumo = _conciliacao_calcula_resumo(carregado)
|
||||||
|
if alteradas:
|
||||||
|
ConciliacaoFornecedorConta.objects.bulk_update(alteradas, ["validada", "validada_por", "validada_em"])
|
||||||
|
if alteradas or conciliacao.resumo != resumo:
|
||||||
|
conciliacao.resumo = resumo
|
||||||
|
conciliacao.save(update_fields=["resumo"])
|
||||||
dados = dict(ConciliacaoFornecedorListSerializer(conciliacao).data)
|
dados = dict(ConciliacaoFornecedorListSerializer(conciliacao).data)
|
||||||
dados["resumo"] = _conciliacao_calcula_resumo(carregado)
|
dados["resumo"] = resumo
|
||||||
dados["contas"] = [_conciliacao_conta_payload(*item) for item in carregado]
|
dados["contas"] = [_conciliacao_conta_payload(*item) for item in carregado]
|
||||||
return Response(dados)
|
return Response(dados)
|
||||||
|
|
||||||
@ -5799,7 +5820,7 @@ class ConciliacaoFornecedorContaViewSet(viewsets.GenericViewSet):
|
|||||||
"""Ajustes manuais numa conta: vincular lançamentos pendentes e marcar
|
"""Ajustes manuais numa conta: vincular lançamentos pendentes e marcar
|
||||||
a conta como validada (com observação)."""
|
a conta como validada (com observação)."""
|
||||||
|
|
||||||
queryset = ConciliacaoFornecedorConta.objects.select_related("conciliacao", "validada_por")
|
queryset = ConciliacaoFornecedorConta.objects.select_related("conciliacao", "validada_por", "vencidos_validados_por")
|
||||||
|
|
||||||
def get_permissions(self) -> list[BasePermission]:
|
def get_permissions(self) -> list[BasePermission]:
|
||||||
return [PermissaoApp(*_CONCILIACAO_PERMISSAO)]
|
return [PermissaoApp(*_CONCILIACAO_PERMISSAO)]
|
||||||
@ -5877,6 +5898,23 @@ class ConciliacaoFornecedorContaViewSet(viewsets.GenericViewSet):
|
|||||||
conta.save(update_fields=["validada", "validada_por", "validada_em", "observacao"])
|
conta.save(update_fields=["validada", "validada_por", "validada_em", "observacao"])
|
||||||
return _conciliacao_resposta_conta(conta, sincroniza_validacao=False)
|
return _conciliacao_resposta_conta(conta, sincroniza_validacao=False)
|
||||||
|
|
||||||
|
@action(detail=True, methods=["post"], url_path="validar-vencidos")
|
||||||
|
def validar_vencidos(self, request: Request, pk: str | None = None) -> Response:
|
||||||
|
"""Validação dos títulos em aberto há mais de 3 meses do fornecedor
|
||||||
|
(tela à parte), independente da validação da conciliação. Mesmo
|
||||||
|
corpo de `validar/`: `{validada, observacao?}`."""
|
||||||
|
conta = self.get_object()
|
||||||
|
entrada = ConciliacaoFornecedorValidarSerializer(data=request.data)
|
||||||
|
entrada.is_valid(raise_exception=True)
|
||||||
|
dados = entrada.validated_data
|
||||||
|
conta.vencidos_validados = dados["validada"]
|
||||||
|
conta.vencidos_validados_por = request.user if conta.vencidos_validados else None
|
||||||
|
conta.vencidos_validados_em = timezone.now() if conta.vencidos_validados else None
|
||||||
|
if "observacao" in dados:
|
||||||
|
conta.vencidos_observacao = dados["observacao"].strip()
|
||||||
|
conta.save(update_fields=["vencidos_validados", "vencidos_validados_por", "vencidos_validados_em", "vencidos_observacao"])
|
||||||
|
return _conciliacao_resposta_conta(conta, sincroniza_validacao=False)
|
||||||
|
|
||||||
|
|
||||||
class ConciliacaoFornecedorVinculoViewSet(viewsets.GenericViewSet):
|
class ConciliacaoFornecedorVinculoViewSet(viewsets.GenericViewSet):
|
||||||
"""`DELETE` desfaz o vínculo: os lançamentos voltam a pendente.
|
"""`DELETE` desfaz o vínculo: os lançamentos voltam a pendente.
|
||||||
|
|||||||
@ -413,15 +413,36 @@ button.conc-kpi.is-active {
|
|||||||
hover, pra `.pa-table tr:hover td` continuar dando o feedback de sempre.
|
hover, pra `.pa-table tr:hover td` continuar dando o feedback de sempre.
|
||||||
O conteúdo também fica apagado: o fundo vai no `tr` e a opacidade no `td`
|
O conteúdo também fica apagado: o fundo vai no `tr` e a opacidade no `td`
|
||||||
(com fundo transparente), senão a opacidade clarearia o fundo junto. */
|
(com fundo transparente), senão a opacidade clarearia o fundo junto. */
|
||||||
#conc-contas-body:has(> tr.conc-row.is-aberta) > tr.conc-row:not(.is-aberta):not(:hover) {
|
:is(#conc-contas-body, #conc-venc-body):has(> tr.conc-row.is-aberta) > tr.conc-row:not(.is-aberta):not(:hover) {
|
||||||
background: var(--bg-canvas);
|
background: var(--bg-canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
#conc-contas-body:has(> tr.conc-row.is-aberta) > tr.conc-row:not(.is-aberta):not(:hover) > td {
|
:is(#conc-contas-body, #conc-venc-body):has(> tr.conc-row.is-aberta) > tr.conc-row:not(.is-aberta):not(:hover) > td {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
opacity: 0.35;
|
opacity: 0.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------- Abas do detalhe ---------------- */
|
||||||
|
|
||||||
|
.conc-abas {
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conc-abas .conc-chip__count {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conc-venc-intro {
|
||||||
|
margin: var(--space-2) 0 var(--space-3);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conc-venc-dias {
|
||||||
|
font-size: 0.74rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
.conc-expand-btn {
|
.conc-expand-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -13,15 +13,18 @@
|
|||||||
|
|
||||||
const PID_CONC_MOEDA = new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" });
|
const PID_CONC_MOEDA = new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" });
|
||||||
|
|
||||||
|
// "Pendente de pagamento" deixou de existir: só títulos em aberto, sem
|
||||||
|
// pagamento a conciliar, é conciliada (pedido do usuário).
|
||||||
const PID_CONC_STATUS = {
|
const PID_CONC_STATUS = {
|
||||||
divergente: { rotulo: "Divergente", pill: "status-pill--danger", rank: 0 },
|
divergente: { rotulo: "Divergente", pill: "status-pill--danger", rank: 0 },
|
||||||
atencao: { rotulo: "Atenção", pill: "status-pill--warning", rank: 1 },
|
atencao: { rotulo: "Atenção", pill: "status-pill--warning", rank: 1 },
|
||||||
pendente: { rotulo: "Pendente de pagamento", pill: "status-pill--neutral", rank: 2 },
|
conciliada: { rotulo: "Conciliada", pill: "status-pill--ok", rank: 2 },
|
||||||
conciliada: { rotulo: "Conciliada", pill: "status-pill--ok", rank: 3 },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PID_CONC_CHEVRON =
|
||||||
|
'<span class="conc-expand-btn"><svg class="conc-expand-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg></span>';
|
||||||
|
|
||||||
const PID_CONC_ROTULO_ALERTA = {
|
const PID_CONC_ROTULO_ALERTA = {
|
||||||
credito_vencido: "Títulos em aberto há mais de 3 meses",
|
|
||||||
debito_excedente: "Pagamentos sem título em aberto",
|
debito_excedente: "Pagamentos sem título em aberto",
|
||||||
pagamento_sem_titulo: "Pagamentos sem título de mesmo valor",
|
pagamento_sem_titulo: "Pagamentos sem título de mesmo valor",
|
||||||
diferenca_vinculo: "Diferenças de juros/desconto",
|
diferenca_vinculo: "Diferenças de juros/desconto",
|
||||||
@ -273,7 +276,7 @@ function pidConcTextoValidacao(conta) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pidConcStatusHtml(status) {
|
function pidConcStatusHtml(status) {
|
||||||
const info = PID_CONC_STATUS[status] || PID_CONC_STATUS.pendente;
|
const info = PID_CONC_STATUS[status] || PID_CONC_STATUS.conciliada;
|
||||||
return `<span class="status-pill ${info.pill}">${info.rotulo}</span>`;
|
return `<span class="status-pill ${info.pill}">${info.rotulo}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -531,6 +534,11 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
history.replaceState(null, "", `#conciliacao-${id}`);
|
history.replaceState(null, "", `#conciliacao-${id}`);
|
||||||
abertas.clear();
|
abertas.clear();
|
||||||
estadoConta.clear();
|
estadoConta.clear();
|
||||||
|
vencAbertas.clear();
|
||||||
|
vencFiltro = "pendentes";
|
||||||
|
vencBusca.value = "";
|
||||||
|
detalhe = null; // mostrarAba não redesenha nada até o detalhe novo chegar
|
||||||
|
mostrarAba("conciliacao");
|
||||||
filtroStatus = "";
|
filtroStatus = "";
|
||||||
filtroAlerta = "";
|
filtroAlerta = "";
|
||||||
contasBusca.value = "";
|
contasBusca.value = "";
|
||||||
@ -575,7 +583,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
porStatus[c.status] = (porStatus[c.status] || 0) + 1;
|
porStatus[c.status] = (porStatus[c.status] || 0) + 1;
|
||||||
});
|
});
|
||||||
const soma = (lista, campo) => lista.reduce((total, c) => total + (Number(c[campo]) || 0), 0);
|
const soma = (lista, campo) => lista.reduce((total, c) => total + (Number(c[campo]) || 0), 0);
|
||||||
const vencidas = contasComAlerta("credito_vencido");
|
const vencidas = contasVencidas();
|
||||||
|
const vencidasValidadas = vencidas.filter((c) => c.vencidos_validados).length;
|
||||||
const excedentes = contas.filter((c) => Number(c.total_excedente) > 0);
|
const excedentes = contas.filter((c) => Number(c.total_excedente) > 0);
|
||||||
const validados = contas.filter((c) => c.validada).length;
|
const validados = contas.filter((c) => c.validada).length;
|
||||||
const pct = contas.length ? Math.round((validados / contas.length) * 100) : 0;
|
const pct = contas.length ? Math.round((validados / contas.length) * 100) : 0;
|
||||||
@ -615,7 +624,11 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
"",
|
"",
|
||||||
""
|
""
|
||||||
)}
|
)}
|
||||||
${kpi("Em aberto há mais de 3 meses", pidConcMoeda(soma(vencidas, "total_vencido")), `${vencidas.length} fornecedor(es)`, "conc-kpi--warning", "credito_vencido")}
|
<button type="button" class="conc-kpi conc-kpi--warning${abaAtual === "vencidos" ? " is-active" : ""}" data-conc-abrir-vencidos>
|
||||||
|
<span class="conc-kpi__rotulo">Em aberto há mais de 3 meses</span>
|
||||||
|
<span class="conc-kpi__valor">${pidConcMoeda(soma(vencidas, "total_vencido"))}</span>
|
||||||
|
<span class="conc-kpi__detalhe">${vencidas.length} fornecedor(es) · ${vencidasValidadas} validado(s)</span>
|
||||||
|
</button>
|
||||||
${kpi("Pagamentos sem título", pidConcMoeda(soma(excedentes, "total_excedente")), `${excedentes.length} fornecedor(es) · divergência`, "conc-kpi--danger", "debito_excedente")}
|
${kpi("Pagamentos sem título", pidConcMoeda(soma(excedentes, "total_excedente")), `${excedentes.length} fornecedor(es) · divergência`, "conc-kpi--danger", "debito_excedente")}
|
||||||
<div class="conc-progresso">
|
<div class="conc-progresso">
|
||||||
<div class="conc-progresso__topo"><span class="conc-kpi__rotulo">Validação</span><span><strong>${validados}</strong> de ${contas.length} fornecedores</span></div>
|
<div class="conc-progresso__topo"><span class="conc-kpi__rotulo">Validação</span><span><strong>${validados}</strong> de ${contas.length} fornecedores</span></div>
|
||||||
@ -630,10 +643,15 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
const alerta = el.dataset.concAlerta;
|
const alerta = el.dataset.concAlerta;
|
||||||
filtroAlerta = filtroAlerta === alerta ? "" : alerta;
|
filtroAlerta = filtroAlerta === alerta ? "" : alerta;
|
||||||
filtroStatus = "";
|
filtroStatus = "";
|
||||||
|
mostrarAba("conciliacao");
|
||||||
renderCards();
|
renderCards();
|
||||||
renderContas();
|
renderContas();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
cardsEl.querySelector("[data-conc-abrir-vencidos]").addEventListener("click", () => {
|
||||||
|
mostrarAba(abaAtual === "vencidos" ? "conciliacao" : "vencidos");
|
||||||
|
});
|
||||||
|
abaVencidosQtd.textContent = vencidas.length - vencidasValidadas;
|
||||||
|
|
||||||
filtroStatusBtns.forEach((btn) => {
|
filtroStatusBtns.forEach((btn) => {
|
||||||
const status = btn.dataset.concStatus;
|
const status = btn.dataset.concStatus;
|
||||||
@ -733,7 +751,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resumoAlertas(conta) {
|
function resumoAlertas(conta) {
|
||||||
if (!conta.alertas.length) return conta.qtd_pendentes ? `${conta.qtd_pendentes} lançamento(s) pendente(s)` : "";
|
// sem alerta, o que sobra pendente são só títulos (pagamento não vinculado é alerta)
|
||||||
|
if (!conta.alertas.length) return conta.qtd_pendentes ? `${conta.qtd_pendentes} título(s) em aberto, sem pagamento a conciliar` : "";
|
||||||
// só o primeiro alerta, cortado com reticências: o resto está na expansão
|
// só o primeiro alerta, cortado com reticências: o resto está na expansão
|
||||||
return conta.alertas[0].mensagem;
|
return conta.alertas[0].mensagem;
|
||||||
}
|
}
|
||||||
@ -746,7 +765,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
const saldoFinal = Number(conta.saldo_final);
|
const saldoFinal = Number(conta.saldo_final);
|
||||||
const vencido = Number(conta.total_vencido);
|
const vencido = Number(conta.total_vencido);
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td><span class="conc-expand-btn"><svg class="conc-expand-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg></span></td>
|
<td>${PID_CONC_CHEVRON}</td>
|
||||||
<td class="conc-conta-numero">${pidConcEscape(conta.conta_numero)}</td>
|
<td class="conc-conta-numero">${pidConcEscape(conta.conta_numero)}</td>
|
||||||
<td><span class="conc-fornecedor__nome">${pidConcEscape(conta.fornecedor)}</span><span class="conc-fornecedor__alerta">${pidConcEscape(resumoAlertas(conta))}</span></td>
|
<td><span class="conc-fornecedor__nome">${pidConcEscape(conta.fornecedor)}</span><span class="conc-fornecedor__alerta">${pidConcEscape(resumoAlertas(conta))}</span></td>
|
||||||
<td class="conc-num conc-valor${Number(conta.saldo_inicial) === 0 ? " conc-valor--zero" : Number(conta.saldo_inicial) < 0 ? " conc-valor--devedor" : ""}">${pidConcSaldo(conta.saldo_inicial)}</td>
|
<td class="conc-num conc-valor${Number(conta.saldo_inicial) === 0 ? " conc-valor--zero" : Number(conta.saldo_inicial) < 0 ? " conc-valor--devedor" : ""}">${pidConcSaldo(conta.saldo_inicial)}</td>
|
||||||
@ -1281,6 +1300,281 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
return painel;
|
return painel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================================================================
|
||||||
|
// Tela "Em aberto há mais de 3 meses"
|
||||||
|
// ======================================================================
|
||||||
|
// Pedido do usuário: título antigo sem pagamento não é bloqueio para a
|
||||||
|
// análise (não deixa mais a conta em "Atenção"), mas precisa de revisão
|
||||||
|
// própria. Esta aba lista os fornecedores com esses títulos, com
|
||||||
|
// validação e observação por fornecedor (`validar-vencidos/`), separadas
|
||||||
|
// da validação da conciliação.
|
||||||
|
const abaBtns = Array.from(document.querySelectorAll("[data-conc-aba]"));
|
||||||
|
const abaPaineis = Array.from(document.querySelectorAll("[data-conc-painel]"));
|
||||||
|
const abaVencidosQtd = document.getElementById("conc-aba-vencidos-qtd");
|
||||||
|
const vencBody = document.getElementById("conc-venc-body");
|
||||||
|
const vencVazia = document.getElementById("conc-venc-vazia");
|
||||||
|
const vencBusca = document.getElementById("conc-venc-busca");
|
||||||
|
const vencFiltroBtns = Array.from(document.querySelectorAll("[data-conc-venc]"));
|
||||||
|
const vencSortHeaders = Array.from(document.querySelectorAll("#conc-venc-table th[data-venc-sort]"));
|
||||||
|
let abaAtual = "conciliacao";
|
||||||
|
let vencFiltro = "pendentes";
|
||||||
|
let vencSortKey = "total_vencido";
|
||||||
|
let vencSortDir = -1;
|
||||||
|
const vencAbertas = new Set();
|
||||||
|
|
||||||
|
function mostrarAba(aba) {
|
||||||
|
abaAtual = aba;
|
||||||
|
abaBtns.forEach((btn) => btn.classList.toggle("is-active", btn.dataset.concAba === aba));
|
||||||
|
abaPaineis.forEach((painel) => { painel.hidden = painel.dataset.concPainel !== aba; });
|
||||||
|
if (!detalhe) return;
|
||||||
|
renderCards();
|
||||||
|
if (aba === "vencidos") renderVencidos();
|
||||||
|
else renderContas();
|
||||||
|
}
|
||||||
|
|
||||||
|
abaBtns.forEach((btn) => btn.addEventListener("click", () => mostrarAba(btn.dataset.concAba)));
|
||||||
|
|
||||||
|
function contasVencidas() {
|
||||||
|
return detalhe.contas.filter((c) => Number(c.total_vencido) > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Títulos da conta em aberto há mais de 3 meses, do mais antigo ao mais
|
||||||
|
* recente: lançamentos sem vínculo e saldos de pagamento parcial. */
|
||||||
|
function titulosVencidos(conta) {
|
||||||
|
const itens = conta.lancamentos
|
||||||
|
.filter((l) => l.situacao && l.situacao.categoria === "vencido")
|
||||||
|
.map((l) => ({ lanc: l, valor: l.situacao.valor_em_aberto }));
|
||||||
|
conta.vinculos.forEach((v) => {
|
||||||
|
if (!v.situacao || v.situacao.categoria !== "vencido") return;
|
||||||
|
const titulo = conta.lancamentos
|
||||||
|
.filter((l) => l.vinculo_id === v.id && l.natureza === "C")
|
||||||
|
.sort((a, b) => a.data.localeCompare(b.data))[0];
|
||||||
|
if (titulo) {
|
||||||
|
itens.push({ lanc: { ...titulo, historico: `${titulo.historico} (saldo do pagamento parcial)` }, valor: v.situacao.valor_em_aberto });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return itens.sort((a, b) => a.lanc.data.localeCompare(b.lanc.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
function diasEmAberto(iso) {
|
||||||
|
const dias = Math.round((Date.parse(detalhe.data_base) - Date.parse(iso.slice(0, 10))) / 86400000);
|
||||||
|
return `${dias} dias`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function textoValidacaoVencidos(conta) {
|
||||||
|
if (!conta.vencidos_validados) return "";
|
||||||
|
return `Validado por ${conta.vencidos_validados_por_nome || "—"} em ${pidConcDataHora(conta.vencidos_validados_em)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function vencPassaFiltro(c) {
|
||||||
|
if (Number(c.total_vencido) <= 0) return false;
|
||||||
|
if (vencFiltro === "pendentes" && c.vencidos_validados) return false;
|
||||||
|
if (vencFiltro === "validados" && !c.vencidos_validados) return false;
|
||||||
|
const termo = vencBusca.value.trim().toLowerCase();
|
||||||
|
return !termo || `${c.conta_numero} ${c.fornecedor}`.toLowerCase().includes(termo);
|
||||||
|
}
|
||||||
|
|
||||||
|
const vencSortValor = {
|
||||||
|
conta_numero: (c) => Number(c.conta_numero),
|
||||||
|
fornecedor: (c) => c.fornecedor,
|
||||||
|
qtd_vencidos: (c) => c.qtd_vencidos,
|
||||||
|
mais_antigo: (c) => (titulosVencidos(c)[0] || { lanc: { data: "" } }).lanc.data,
|
||||||
|
total_vencido: (c) => Number(c.total_vencido),
|
||||||
|
vencidos_validados: (c) => (c.vencidos_validados ? 1 : 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
vencSortHeaders.forEach((th) => {
|
||||||
|
th.addEventListener("click", () => {
|
||||||
|
const chave = th.dataset.vencSort;
|
||||||
|
vencSortDir = vencSortKey === chave ? vencSortDir * -1 : 1;
|
||||||
|
vencSortKey = chave;
|
||||||
|
renderVencidos();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
vencFiltroBtns.forEach((btn) => {
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
vencFiltro = btn.dataset.concVenc;
|
||||||
|
renderVencidos();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
vencBusca.addEventListener("input", renderVencidos);
|
||||||
|
|
||||||
|
function atualizarContagemVencidos() {
|
||||||
|
const todas = contasVencidas();
|
||||||
|
const validadas = todas.filter((c) => c.vencidos_validados).length;
|
||||||
|
const contagem = { pendentes: todas.length - validadas, validados: validadas, "": todas.length };
|
||||||
|
vencFiltroBtns.forEach((btn) => {
|
||||||
|
const chave = btn.dataset.concVenc;
|
||||||
|
btn.innerHTML = `${btn.textContent.replace(/\s*\d+$/, "")} <span class="conc-chip__count">${contagem[chave]}</span>`;
|
||||||
|
btn.classList.toggle("is-active", vencFiltro === chave);
|
||||||
|
});
|
||||||
|
vencSortHeaders.forEach((h) => {
|
||||||
|
h.classList.toggle("is-sort-asc", h.dataset.vencSort === vencSortKey && vencSortDir === 1);
|
||||||
|
h.classList.toggle("is-sort-desc", h.dataset.vencSort === vencSortKey && vencSortDir === -1);
|
||||||
|
});
|
||||||
|
return todas.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mensagemVencidosVazia(totalComVencidos) {
|
||||||
|
if (!totalComVencidos) return "Nenhum título em aberto há mais de 3 meses nesta conciliação.";
|
||||||
|
if (vencFiltro === "pendentes" && !vencBusca.value.trim()) return "Todos os fornecedores com títulos antigos já foram validados.";
|
||||||
|
return "Nenhum fornecedor para este filtro.";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderVencidos() {
|
||||||
|
const total = atualizarContagemVencidos();
|
||||||
|
const extrator = vencSortValor[vencSortKey];
|
||||||
|
const lista = detalhe.contas
|
||||||
|
.filter(vencPassaFiltro)
|
||||||
|
.sort((a, b) => pidConcCompara(extrator(a), extrator(b)) * vencSortDir);
|
||||||
|
vencBody.innerHTML = "";
|
||||||
|
vencVazia.hidden = lista.length > 0;
|
||||||
|
vencVazia.textContent = mensagemVencidosVazia(total);
|
||||||
|
const fragmento = document.createDocumentFragment();
|
||||||
|
lista.forEach((conta) => montarLinhasVencidos(conta).forEach((tr) => fragmento.appendChild(tr)));
|
||||||
|
vencBody.appendChild(fragmento);
|
||||||
|
}
|
||||||
|
|
||||||
|
function montarLinhasVencidos(conta) {
|
||||||
|
const aberta = vencAbertas.has(conta.id);
|
||||||
|
const titulos = titulosVencidos(conta);
|
||||||
|
const maisAntigo = titulos[0] ? titulos[0].lanc.data : null;
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
tr.className = `conc-row${aberta ? " is-aberta" : ""}`;
|
||||||
|
tr.dataset.vencContaId = conta.id;
|
||||||
|
const subtitulo = textoValidacaoVencidos(conta) || conta.vencidos_observacao || "";
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>${PID_CONC_CHEVRON}</td>
|
||||||
|
<td class="conc-conta-numero">${pidConcEscape(conta.conta_numero)}</td>
|
||||||
|
<td><span class="conc-fornecedor__nome">${pidConcEscape(conta.fornecedor)}</span><span class="conc-fornecedor__alerta">${pidConcEscape(subtitulo)}</span></td>
|
||||||
|
<td class="conc-num">${titulos.length}</td>
|
||||||
|
<td>${pidConcData(maisAntigo)}${maisAntigo ? ` <span class="conc-venc-dias">· ${diasEmAberto(maisAntigo)}</span>` : ""}</td>
|
||||||
|
<td class="conc-num conc-valor conc-valor--vencido">${pidConcMoeda(conta.total_vencido)}</td>
|
||||||
|
<td class="conc-validado-col"><input type="checkbox" class="conc-validado-check" data-venc-check ${conta.vencidos_validados ? "checked" : ""} aria-label="Confirmar que os títulos antigos do fornecedor foram validados" /></td>`;
|
||||||
|
const check = tr.querySelector("[data-venc-check]");
|
||||||
|
check.addEventListener("click", (evento) => evento.stopPropagation());
|
||||||
|
check.addEventListener("change", async () => {
|
||||||
|
check.disabled = true;
|
||||||
|
try {
|
||||||
|
await salvarVencidos(conta, check.checked);
|
||||||
|
} catch (erro) {
|
||||||
|
check.checked = !check.checked;
|
||||||
|
check.disabled = false;
|
||||||
|
pidAlert(erro.message, { titulo: "Não foi possível salvar" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tr.addEventListener("click", () => {
|
||||||
|
if (vencAbertas.has(conta.id)) vencAbertas.delete(conta.id);
|
||||||
|
else vencAbertas.add(conta.id);
|
||||||
|
substituirVencidos(conta);
|
||||||
|
});
|
||||||
|
if (!aberta) return [tr];
|
||||||
|
|
||||||
|
const trDetalhe = document.createElement("tr");
|
||||||
|
trDetalhe.className = "conc-detail-row";
|
||||||
|
trDetalhe.dataset.vencDetalheId = conta.id;
|
||||||
|
const td = document.createElement("td");
|
||||||
|
td.colSpan = 7;
|
||||||
|
td.className = "conc-detail-cell";
|
||||||
|
td.appendChild(montarDetalheVencidos(conta, titulos));
|
||||||
|
trDetalhe.appendChild(td);
|
||||||
|
return [tr, trDetalhe];
|
||||||
|
}
|
||||||
|
|
||||||
|
function montarDetalheVencidos(conta, titulos) {
|
||||||
|
const painel = document.createElement("div");
|
||||||
|
painel.className = "conc-detail";
|
||||||
|
painel.innerHTML = `
|
||||||
|
<div>
|
||||||
|
<div class="conc-detail__secao-titulo"><h4>Títulos em aberto há mais de 3 meses (${titulos.length})</h4></div>
|
||||||
|
<table class="conc-subtabela">
|
||||||
|
<thead><tr><th>Data</th><th>Seq.</th><th>Histórico</th><th class="conc-num">Valor em aberto</th><th class="conc-num">Em aberto há</th></tr></thead>
|
||||||
|
<tbody>${titulos
|
||||||
|
.map(
|
||||||
|
(t) => `
|
||||||
|
<tr>
|
||||||
|
<td>${pidConcData(t.lanc.data)}</td>
|
||||||
|
<td>${t.lanc.eh_saldo_anterior ? "—" : pidConcEscape(t.lanc.sequencia || "—")}</td>
|
||||||
|
${pidConcHistoricoHtml(t.lanc)}
|
||||||
|
<td class="conc-num conc-valor">${pidConcMoeda(t.valor)}</td>
|
||||||
|
<td class="conc-num">${diasEmAberto(t.lanc.data)}</td>
|
||||||
|
</tr>`
|
||||||
|
)
|
||||||
|
.join("")}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="conc-validacao">
|
||||||
|
<label class="conc-detail__secao-titulo"><h4>Observação sobre os títulos antigos</h4></label>
|
||||||
|
<textarea data-venc-observacao placeholder="Registre a análise (ex.: títulos já pagos, aguardando baixa no Questor).">${pidConcEscape(conta.vencidos_observacao)}</textarea>
|
||||||
|
<div class="conc-detail__acoes">
|
||||||
|
<button type="button" class="btn-outline conc-btn-sm" data-venc-salvar-obs>Salvar observação</button>
|
||||||
|
<button type="button" class="${conta.vencidos_validados ? "btn-outline" : "btn-solid"} conc-btn-sm" data-venc-validar>${conta.vencidos_validados ? "Remover validação" : "Marcar como validado"}</button>
|
||||||
|
${conta.vencidos_validados ? `<span class="conc-validacao__info">${pidConcEscape(textoValidacaoVencidos(conta))}</span>` : ""}
|
||||||
|
<button type="button" class="btn-outline conc-btn-sm conc-recolher-btn" data-venc-recolher>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 15l6-6 6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
|
Recolher
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
const observacaoEl = painel.querySelector("[data-venc-observacao]");
|
||||||
|
const salvar = async (validada) => {
|
||||||
|
try {
|
||||||
|
await salvarVencidos(conta, validada, observacaoEl.value);
|
||||||
|
} catch (erro) {
|
||||||
|
pidAlert(erro.message, { titulo: "Não foi possível salvar" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
painel.querySelector("[data-venc-salvar-obs]").addEventListener("click", () => salvar(conta.vencidos_validados));
|
||||||
|
painel.querySelector("[data-venc-validar]").addEventListener("click", () => salvar(!conta.vencidos_validados));
|
||||||
|
painel.querySelector("[data-venc-recolher]").addEventListener("click", () => {
|
||||||
|
vencAbertas.delete(conta.id);
|
||||||
|
substituirVencidos(conta);
|
||||||
|
const linha = vencBody.querySelector(`tr[data-venc-conta-id="${conta.id}"]`);
|
||||||
|
if (linha && linha.getBoundingClientRect().top < 120) linha.scrollIntoView({ block: "center" });
|
||||||
|
});
|
||||||
|
return painel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function substituirVencidos(conta) {
|
||||||
|
const antiga = vencBody.querySelector(`tr[data-venc-conta-id="${conta.id}"]`);
|
||||||
|
if (!antiga) return;
|
||||||
|
const detalheAntigo = vencBody.querySelector(`tr[data-venc-detalhe-id="${conta.id}"]`);
|
||||||
|
antiga.replaceWith(...montarLinhasVencidos(conta));
|
||||||
|
if (detalheAntigo) detalheAntigo.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removerVencidos(contaId) {
|
||||||
|
const linhas = vencBody.querySelectorAll(`tr[data-venc-conta-id="${contaId}"], tr[data-venc-detalhe-id="${contaId}"]`);
|
||||||
|
linhas.forEach((tr) => tr.classList.add("is-saindo"));
|
||||||
|
window.setTimeout(() => {
|
||||||
|
linhas.forEach((tr) => tr.remove());
|
||||||
|
if (!vencBody.querySelector("tr[data-venc-conta-id]")) {
|
||||||
|
vencVazia.hidden = false;
|
||||||
|
vencVazia.textContent = mensagemVencidosVazia(contasVencidas().length);
|
||||||
|
}
|
||||||
|
}, 160);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Grava a validação/observação dos títulos antigos e atualiza a linha
|
||||||
|
* (sai da lista com fade se deixar de caber no filtro, como na conciliação). */
|
||||||
|
async function salvarVencidos(conta, validada, observacao) {
|
||||||
|
const corpo = { validada };
|
||||||
|
if (observacao !== undefined) corpo.observacao = observacao;
|
||||||
|
const resposta = await pidApiRequest(`/conciliacoes-fornecedores-contas/${conta.id}/validar-vencidos/`, {
|
||||||
|
method: "POST",
|
||||||
|
body: corpo,
|
||||||
|
});
|
||||||
|
const indice = detalhe.contas.findIndex((c) => c.id === resposta.conta.id);
|
||||||
|
const eraValidada = indice >= 0 && detalhe.contas[indice].vencidos_validados;
|
||||||
|
if (indice >= 0) detalhe.contas[indice] = resposta.conta;
|
||||||
|
if (!eraValidada && resposta.conta.vencidos_validados) vencAbertas.delete(resposta.conta.id);
|
||||||
|
detalhe.resumo = resposta.resumo;
|
||||||
|
renderCards();
|
||||||
|
atualizarContagemVencidos();
|
||||||
|
if (vencPassaFiltro(resposta.conta)) substituirVencidos(resposta.conta);
|
||||||
|
else removerVencidos(resposta.conta.id);
|
||||||
|
}
|
||||||
|
|
||||||
// ======================================================================
|
// ======================================================================
|
||||||
// Início
|
// Início
|
||||||
// ======================================================================
|
// ======================================================================
|
||||||
|
|||||||
@ -416,12 +416,19 @@
|
|||||||
<div class="conc-cards" id="conc-cards"></div>
|
<div class="conc-cards" id="conc-cards"></div>
|
||||||
<div class="conc-alertas-gerais" id="conc-alertas-gerais"></div>
|
<div class="conc-alertas-gerais" id="conc-alertas-gerais"></div>
|
||||||
|
|
||||||
|
<!-- Duas telas no detalhe: a conciliação e a revisão dos títulos
|
||||||
|
em aberto há mais de 3 meses (validação própria por fornecedor) -->
|
||||||
|
<div class="pa-tabs conc-abas" id="conc-abas">
|
||||||
|
<button type="button" class="pa-tab is-active" data-conc-aba="conciliacao">Conciliação</button>
|
||||||
|
<button type="button" class="pa-tab" data-conc-aba="vencidos">Em aberto há mais de 3 meses <span class="conc-chip__count" id="conc-aba-vencidos-qtd">0</span></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pa-tab-panel" data-conc-painel="conciliacao">
|
||||||
<div class="conc-toolbar conc-toolbar--fixa">
|
<div class="conc-toolbar conc-toolbar--fixa">
|
||||||
<div class="conc-chip-row" id="conc-filtro-status">
|
<div class="conc-chip-row" id="conc-filtro-status">
|
||||||
<button type="button" class="conc-chip is-active" data-conc-status="">Todos</button>
|
<button type="button" class="conc-chip is-active" data-conc-status="">Todos</button>
|
||||||
<button type="button" class="conc-chip" data-conc-status="divergente">Divergentes</button>
|
<button type="button" class="conc-chip" data-conc-status="divergente">Divergentes</button>
|
||||||
<button type="button" class="conc-chip" data-conc-status="atencao">Atenção</button>
|
<button type="button" class="conc-chip" data-conc-status="atencao">Atenção</button>
|
||||||
<button type="button" class="conc-chip" data-conc-status="pendente">Pendentes de pagamento</button>
|
|
||||||
<button type="button" class="conc-chip" data-conc-status="conciliada">Conciliados</button>
|
<button type="button" class="conc-chip" data-conc-status="conciliada">Conciliados</button>
|
||||||
<button type="button" class="conc-chip" data-conc-status="validada">Validados</button>
|
<button type="button" class="conc-chip" data-conc-status="validada">Validados</button>
|
||||||
</div>
|
</div>
|
||||||
@ -451,6 +458,40 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<p class="conc-empty" id="conc-contas-vazia" hidden>Nenhum fornecedor para este filtro.</p>
|
<p class="conc-empty" id="conc-contas-vazia" hidden>Nenhum fornecedor para este filtro.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pa-tab-panel" data-conc-painel="vencidos" hidden>
|
||||||
|
<div class="conc-toolbar conc-toolbar--fixa">
|
||||||
|
<div class="conc-chip-row" id="conc-venc-filtro">
|
||||||
|
<button type="button" class="conc-chip is-active" data-conc-venc="pendentes">A validar</button>
|
||||||
|
<button type="button" class="conc-chip" data-conc-venc="validados">Validados</button>
|
||||||
|
<button type="button" class="conc-chip" data-conc-venc="">Todos</button>
|
||||||
|
</div>
|
||||||
|
<div class="pa-search">
|
||||||
|
<input type="search" id="conc-venc-busca" placeholder="Buscar fornecedor ou conta..." autocomplete="off" />
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5" stroke-linecap="round"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="conc-venc-intro">Títulos sem pagamento vinculado há mais de 3 meses até a data-base. Não bloqueiam a conciliação: a validação desta tela é separada da validação do fornecedor.</p>
|
||||||
|
|
||||||
|
<div class="pa-table-wrap conc-table-wrap">
|
||||||
|
<table class="pa-table conc-table" id="conc-venc-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="conc-expand-col"></th>
|
||||||
|
<th data-venc-sort="conta_numero">Conta <span class="conc-sort-icon">↕</span></th>
|
||||||
|
<th data-venc-sort="fornecedor">Fornecedor <span class="conc-sort-icon">↕</span></th>
|
||||||
|
<th class="conc-num" data-venc-sort="qtd_vencidos">Títulos <span class="conc-sort-icon">↕</span></th>
|
||||||
|
<th data-venc-sort="mais_antigo">Mais antigo <span class="conc-sort-icon">↕</span></th>
|
||||||
|
<th class="conc-num" data-venc-sort="total_vencido">Valor > 3 meses <span class="conc-sort-icon">↕</span></th>
|
||||||
|
<th class="conc-validado-col" data-venc-sort="vencidos_validados">Validado <span class="conc-sort-icon">↕</span></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="conc-venc-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p class="conc-empty" id="conc-venc-vazia" hidden></p>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user