Reestruturação das regras de conciliação
This commit is contained in:
parent
543b1171c7
commit
201bc4eff4
@ -12,7 +12,7 @@ Duas identidades visuais coexistem **de propósito** hoje: o logo cursivo "D De
|
|||||||
|
|
||||||
- `logo.png` — original, texto **preto**. Serve como fonte pra gerar as outras variantes e é usada diretamente no recibo do Indicador de Desempenho (`indicadores/recibo.py`, `LOGO_PATH`, redimensionada/recomprimida em memória pra impressão — ver `portal_api/indicadores/CLAUDE.md`).
|
- `logo.png` — original, texto **preto**. Serve como fonte pra gerar as outras variantes e é usada diretamente no recibo do Indicador de Desempenho (`indicadores/recibo.py`, `LOGO_PATH`, redimensionada/recomprimida em memória pra impressão — ver `portal_api/indicadores/CLAUDE.md`).
|
||||||
- `logo-branco.png` — usada no cabeçalho do PDF de Simulação de Custo de Contratação (`custo_contratacao/pdf.py`, banner marrom escuro, ver `portal_api/custo_contratacao/CLAUDE.md`) — até uma rodada anterior também era usada no `sidebar__brand` dos 13 shells, migrada pra marca "P.I.D." (ver abaixo; a UI do Portal e os documentos gerados usam fontes de logo independentes agora). Mesmo D colorido de `logo.png`, mas com o texto recolorido pra branco; gerada programaticamente a partir de `logo.png` (script Python com Pillow: qualquer pixel opaco quase-neutro/escuro — `max(r,g,b) < 70` e `spread(r,g,b) < 12` — virou branco; o D nunca entra nesse filtro porque mesmo na sombra mais escura do degradê ele mantém um matiz quente nitidamente não-neutro). Se o logo oficial mudar, regerar `logo-branco.png` a partir do novo `logo.png` com o mesmo filtro, não editar à mão.
|
- `logo-branco.png` — usada no cabeçalho do PDF de Simulação de Custo de Contratação (`custo_contratacao/pdf.py`, banner marrom escuro, ver `portal_api/custo_contratacao/CLAUDE.md`) — até uma rodada anterior também era usada no `sidebar__brand` dos 13 shells, migrada pra marca "P.I.D." (ver abaixo; a UI do Portal e os documentos gerados usam fontes de logo independentes agora). Mesmo D colorido de `logo.png`, mas com o texto recolorido pra branco; gerada programaticamente a partir de `logo.png` (script Python com Pillow: qualquer pixel opaco quase-neutro/escuro — `max(r,g,b) < 70` e `spread(r,g,b) < 12` — virou branco; o D nunca entra nesse filtro porque mesmo na sombra mais escura do degradê ele mantém um matiz quente nitidamente não-neutro). Se o logo oficial mudar, regerar `logo-branco.png` a partir do novo `logo.png` com o mesmo filtro, não editar à mão.
|
||||||
- `logo-branco-relatorio.png` — cópia de `logo-branco.png` só redimensionada (480 px de largura, 40 KB, Pillow LANCZOS), usada no cabeçalho do relatório da Conciliação de Fornecedores (`conciliacao-fornecedores-relatorio.html`): o original, com 6250 px, pesava mais que o PDF impresso inteiro. Se `logo-branco.png` mudar, regerar esta a partir dele.
|
- `logo-branco-relatorio.png` — cópia de `logo-branco.png` só redimensionada (480 px de largura, 40 KB, Pillow LANCZOS), usada no cabeçalho do relatório do Relatório Contábil (via `_logo_relatorio_data_uri()` em views.py): o original, com 6250 px, pesava mais que o PDF impresso inteiro. Se `logo-branco.png` mudar, regerar esta a partir dele. Foi criada para o relatório da Conciliação de Fornecedores, que desde 2026-10-02 (rodada 72 em `portal_api/conciliacao_fornecedores/CHANGELOG.md`) é uma aba do Portal, sem logo do escritório, por ser de uso interno do contador.
|
||||||
- `logo-mono.png` — versão totalmente monocromática (D **e** texto em branco/cinza claro). Não usada em nenhum consumidor hoje; existe como variante alternativa (útil se algum dia precisar de um logo "chapado" sem o dourado do D).
|
- `logo-mono.png` — versão totalmente monocromática (D **e** texto em branco/cinza claro). Não usada em nenhum consumidor hoje; existe como variante alternativa (útil se algum dia precisar de um logo "chapado" sem o dourado do D).
|
||||||
|
|
||||||
**Marca nova "P.I.D."**:
|
**Marca nova "P.I.D."**:
|
||||||
|
|||||||
@ -328,3 +328,241 @@ Pedido do usuário: o arquivo anexado ficava em `media/conciliacao_fornecedores/
|
|||||||
- Antes, a pedido do usuário, as conciliações #22 (2017) e #23 (1751) foram excluídas para refazer os testes, e os arquivos de `media/conciliacao_fornecedores/` apagados; depois o usuário disse que não precisava excluir, e o único versionado no git foi restaurado (os demais têm cópia em `Projetos\Conciliação`). A conciliação #24 (2017), criada pelo usuário durante a mudança, ficou intacta (o arquivo dela também foi apagado, sem impacto).
|
- Antes, a pedido do usuário, as conciliações #22 (2017) e #23 (1751) foram excluídas para refazer os testes, e os arquivos de `media/conciliacao_fornecedores/` apagados; depois o usuário disse que não precisava excluir, e o único versionado no git foi restaurado (os demais têm cópia em `Projetos\Conciliação`). A conciliação #24 (2017), criada pelo usuário durante a mudança, ficou intacta (o arquivo dela também foi apagado, sem impacto).
|
||||||
|
|
||||||
Mexe em models.py/views.py e migração: exige reiniciar o `runserver`.
|
Mexe em models.py/views.py e migração: exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
### Rodada 60 — Colunas do histórico enxugadas (2026-10-02)
|
||||||
|
|
||||||
|
Pedido do usuário: na tabela do histórico de conciliações, deixar só a quantidade de fornecedores e a de contas com alerta alto, sem a data-base, e mostrar quem mexeu por último em vez de quem criou.
|
||||||
|
|
||||||
|
- Coluna "Conciliados" removida (`conciliacao-fornecedores.html` e `renderHistorico()` em `conciliacao-fornecedores.js`).
|
||||||
|
- Coluna "Divergentes" renomeada para "Críticos" (mesmo valor: `resumo.por_status.divergente`). Só o rótulo dessa coluna mudou; o chip "Divergentes" do detalhe, o status `divergente` e o relatório continuam com o nome antigo.
|
||||||
|
- Coluna "Data-base" removida (a data-base continua no detalhe e no relatório).
|
||||||
|
- "Criado por" virou "Última atualização": nome e data/hora de `ultima_atualizacao` (última atualização do razão, já no payload); sem atualização, a criação (`criado_por_nome`/`criado_em`). Só a atualização do razão conta; vincular, desfazer ou validar não muda essa coluna.
|
||||||
|
|
||||||
|
Só HTML/JS: não exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
### Rodada 61 — Cards do detalhe e situação "Divergente" vira "Crítico" (2026-10-02)
|
||||||
|
|
||||||
|
Pedido do usuário: cards que digam o que falta fazer, em vez de somas que não pedem ação.
|
||||||
|
|
||||||
|
- Card 1, "Títulos em aberto" (soma de `total_em_aberto`) → **"Pagamentos a vincular"**: soma de `total_nao_vinculado` e quantidade de fornecedores; clicável, filtra pelo alerta `pagamento_sem_titulo`. A etiqueta "com pagamento não vinculado" saiu (duplicava o card).
|
||||||
|
- Card 2, "Em aberto há mais de 3 meses": o valor passa a ser a **quantidade de fornecedores pendentes de validação** na aba própria (antes, a soma de `total_vencido`), com "X de Y validado(s)" abaixo.
|
||||||
|
- Card 3, "Pagamentos sem título" (soma de `total_excedente`) → **"Críticos pendentes"**: contas com status `divergente` e `validada` falso; clicável, aplica o filtro do chip "Críticos" (as validadas já ficam escondidas por padrão).
|
||||||
|
- Barra de validação: o trilho (parte que falta) ganhou fundo verde translúcido e contorno, e a barra passou de 8 para 10px.
|
||||||
|
- Rótulo da situação `divergente`: "Divergente" → **"Crítico"** no JS (`PID_CONC_STATUS`, etiqueta da linha e chip "Críticos"), no XLSX (`_CONCILIACAO_ROTULO_STATUS` em views.py) e no relatório (`ROTULO_STATUS` e o risco "Conta em situação Crítico validada sem observação" em `relatorio.py`; textos do template). Chave interna sem mudança.
|
||||||
|
|
||||||
|
Mexe em views.py/relatorio.py: exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
### Rodada 62 — Cabeçalho do detalhe (2026-10-02)
|
||||||
|
|
||||||
|
Pedido do usuário, no cabeçalho da empresa do detalhe (`renderCabecalho()`):
|
||||||
|
|
||||||
|
- "EMPRESA XXXX" (`.conc-empresa__codigo`) no mesmo tamanho do nome (1,05rem; era 0,75rem).
|
||||||
|
- "Data-base" removida do cabeçalho (continua no relatório e no XLSX).
|
||||||
|
- "Processado por" e "Razão atualizado por" viraram uma só "Última atualização": quem atualizou o razão por último e quando; sem atualização, quem criou (mesma regra da coluna da lista, rodada 60).
|
||||||
|
|
||||||
|
Só JS/CSS: não exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
### Rodada 63 — Detalhe da validação no balão da barra (2026-10-02)
|
||||||
|
|
||||||
|
Pedido do usuário: o texto "N ainda para revisar · X% concluído" saiu de baixo da barra de validação e passou a aparecer no balão do Portal ao passar o mouse na barra (`data-conc-tooltip` + `data-conc-tooltip-sempre` em `.conc-progresso__barra`, sem `title` nativo).
|
||||||
|
|
||||||
|
Só JS: não exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
### Rodada 64 — Um balão para cada parte da barra de validação (2026-10-02)
|
||||||
|
|
||||||
|
Pedido do usuário: a parte preenchida mostra o que foi concluído e a parte vazia, o que falta.
|
||||||
|
|
||||||
|
- `.conc-progresso__barra` virou `flex` com dois filhos: `.conc-progresso__feito` (largura = %, balão "N validado(s) · X% concluído") e `.conc-progresso__falta` (`flex: 1`, transparente sobre o trilho, balão "N ainda para revisar · Y% pendente", Y = 100 − X para os dois somarem 100). Cada parte só é desenhada se tiver quantidade.
|
||||||
|
- Destaque da parte sob o mouse (pedido do usuário na sequência): a preenchida clareia (`#3fcf8e`) e o trilho apaga; a que falta fica num verde mais forte com tracejado diagonal (`repeating-linear-gradient`; o verde liso ficava igual ao verde apagado da preenchida, e o dourado testado no meio foi descartado pelo usuário) e a preenchida fica com `opacity: 0.45`. Só CSS, via `:hover`/`:has()`.
|
||||||
|
- Balão da barra mais caprichado (pedido do usuário): variante `.conc-tooltip--rico`, ativada por `data-conc-tooltip-titulo` (+ `-valor` e `-cor`) em `pidConcMostraTooltip()`: marcador de cor (liso para "Concluído", tracejado para "Pendente"), título em caixa alta, percentual em destaque à direita, "N de M fornecedores validados / ainda para revisar" abaixo, centrado na parte da barra com uma seta (`--conc-tooltip-seta`, calculada no JS; vira para cima se o balão abrir abaixo). Os demais balões da tela não mudam.
|
||||||
|
- Card "Em aberto há mais de 3 meses" (pedido do usuário, entre as opções sugeridas): o valor passa a ser a **soma dos títulos antigos dos fornecedores ainda sem revisão** (`total_vencido` das contas com `vencidos_validados` falso), com "ainda sem revisão · N fornecedor(es) a revisar · X de Y validado(s)" abaixo. Antes (rodada 61) era só a contagem de fornecedores. Na sequência, o usuário pediu o **saldo total em aberto** (soma de `total_vencido` de todos, revisados ou não), e o detalhe ficou "N fornecedor(es) a revisar · X de Y validado(s)". Por fim, o usuário esclareceu que queria o **saldo em aberto sem o recorte de 3 meses**: o card virou "Títulos em aberto" (soma de `total_em_aberto` e quantos fornecedores; "disponíveis para vincular os pagamentos", par do primeiro card), sem clique (o atalho para a aba dos títulos antigos saiu; ela continua pela aba).
|
||||||
|
- Tracejado da parte que falta (na barra e no marcador do balão) em vermelho (`--danger-rgb`), pedido do usuário; na sequência, o fundo verde saiu (na barra, o trilho fica transparente enquanto o mouse está na parte que falta; o marcador ganhou contorno vermelho): só o tracejado.
|
||||||
|
|
||||||
|
Só JS/CSS: não exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
### Rodada 65 — Chips da lista por validação (2026-10-02)
|
||||||
|
|
||||||
|
Pedido do usuário: os chips por situação (Todos/Críticos/Atenção/Conciliados/Validados) confundiam, porque misturavam situação calculada com validação.
|
||||||
|
|
||||||
|
- Chips novos (`PID_CONC_GRUPOS`, `data-conc-grupo`, `?grupo=` na URL; o antigo `?status=` é ignorado): **Pendentes** (padrão, não validadas), **Parcial** (validadas com vínculo `parcial` com `diferenca` > 0, ou seja, saldo a pagar que segue para os próximos meses), **Saldo em aberto** (validadas, sem parcial em aberto, `saldo_final` ≠ 0), **Conciliados** (validadas, sem parcial em aberto, `saldo_final` = 0) e **Validados** (todas as validadas). Parcial/Saldo em aberto/Conciliados dividem as validadas sem sobra. Interpretação da implementação: os três exigem conta validada (o pedido só diz "validados" no Saldo em aberto) e uma validada com saldo devedor (negativo) cai em "Saldo em aberto"; confirmar se o usuário disser o contrário.
|
||||||
|
- Toggle "Mostrar validadas" removido (o chip Pendentes faz o papel).
|
||||||
|
- Card "Críticos pendentes" virou um filtro de card como os outros (`filtroAlerta = "criticos_pendentes"`, com a faixa "Filtrando por"); filtro de card/etiqueta passa a valer sobre todas as contas, validadas ou não, para a lista bater com o número do card. O rótulo do filtro `pagamento_sem_titulo` virou "Pagamentos a vincular" (nome do card).
|
||||||
|
- Coluna "Validado": data e hora da validação abaixo do checkbox (`.conc-validado-data`, coluna de 90 para 130px), e a ordenação dela passou a ser por `validada_em`.
|
||||||
|
- Ordem padrão por chip (`sortPadrao()`): Pendentes por situação (críticos primeiro); os chips de validadas da validação mais recente para a mais antiga. Trocar de chip volta à ordem padrão dele.
|
||||||
|
|
||||||
|
Só HTML/JS/CSS: não exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
### Rodada 66 — Sai a coluna "Em aberto > 3 meses" da tabela de fornecedores (2026-10-02)
|
||||||
|
|
||||||
|
Pedido do usuário: o valor já é revisado na aba "Em aberto há mais de 3 meses". Coluna removida do thead e de `montarLinhasConta()`, com a chave de ordenação `total_vencido` (um `?ordem=total_vencido` antigo na URL é ignorado); `colspan` da linha expandida e das mensagens de 8 para 7. A aba própria continua com a coluna dela.
|
||||||
|
|
||||||
|
Só HTML/JS: não exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
### Rodada 67 — Sai "Saldo inicial", "Saldo residual" vira "Saldo final" (2026-10-02)
|
||||||
|
|
||||||
|
Pedido do usuário, na tabela de fornecedores: coluna "Saldo inicial" removida (thead, `montarLinhasConta()` e a chave de ordenação `saldo_inicial`) e "Saldo residual" renomeada para "Saldo final" (mesmo valor, `saldo_final`, e mesma chave de ordenação). `colspan` de 7 para 6. Só a tabela da tela: XLSX e relatório não mudam.
|
||||||
|
|
||||||
|
Só HTML/JS: não exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
### Rodada 68 — Situação "Parcial" (2026-10-02)
|
||||||
|
|
||||||
|
Pedido do usuário: conta sem alerta, mas com saldo de pagamento parcial ainda a pagar (vínculo manual que segue para o mês seguinte), aparecia como "Conciliada"; deve ser "Parcial".
|
||||||
|
|
||||||
|
- `alertas.py`: `STATUS_PARCIAL = "parcial"`, entre `atencao` e `conciliada`: sem alerta e com algum vínculo `parcial` de `diferenca` > 0. Crítico e Atenção continuam prevalecendo.
|
||||||
|
- Rótulo "Parcial" em `_CONCILIACAO_ROTULO_STATUS` (views.py: payload, XLSX e `resumo.por_status`) e `ROTULO_STATUS` (`relatorio.py`); na tela, `PID_CONC_STATUS.parcial` (rank 2, etiqueta azul `.status-pill--parcial`); no relatório, `.cfr-pill--parcial` (roxo claro) e "N parciais" no card de situação.
|
||||||
|
- Efeitos: Parcial não recebe validação automática (só `conciliada`; na prática já não recebia, porque vínculo parcial é manual); no relatório, Parcial validada não entra em "contas validadas com pendência" e a ordem do anexo passou a ser crítica, atenção, parcial, conciliada.
|
||||||
|
- Na data da mudança, 4 contas (todas da conciliação #24, nenhuma validada) tinham vínculo parcial em aberto, entre elas a ROZINI (5449166), o caso do usuário. O `resumo` gravado de cada conciliação é recalculado ao abri-la.
|
||||||
|
|
||||||
|
Mexe em alertas.py/views.py/relatorio.py: exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
Na primeira subida, o usuário teve `KeyError: 'parcial'` em `_conciliacao_calcula_resumo()`: o servidor recarregou no meio das edições (alertas.py novo, views.py antigo, sem "Parcial" em `_CONCILIACAO_ROTULO_STATUS`). Resolvido só reiniciando; com o código final, a conciliação #24 calcula 226 conciliadas, 3 parciais, 30 em atenção e 1 crítica.
|
||||||
|
|
||||||
|
- Detalhe do vínculo (botão "i", `pidConcAbreDetalheVinculo()`), pedido do usuário na sequência: pagamento parcial com saldo mostra **"Saldo a pagar R$ X"** em destaque dourado (`.conc-vinculo-detalhe__saldo`, à direita dos totais, a partir de `vinculo.diferenca`), no lugar da frase "Créditos R$ X maiores que os débitos (em aberto)". Os demais vínculos continuam com a frase de diferença.
|
||||||
|
|
||||||
|
### Rodada 69 — Linha vinculada do razão mais resumida (2026-10-02)
|
||||||
|
|
||||||
|
Pedido do usuário, na linha de lançamento já vinculado do razão expandido: "Vinculado #10" virou só **"#10"**, e o nome do tipo ("Valor exato", "Pagamento parcial"...) virou a **proporção créditos:débitos** do vínculo ("1:1", "4:1" = 4 créditos para 1 débito; contada sobre `conta.lancamentos` por `vinculo_id`, saldo anterior incluído), seguida de "automático"/"manual" como antes. O nome do tipo e a proporção por extenso ficam no balão (`data-conc-tooltip`). O modal do "i" e o bloco de pagamentos parciais não mudam.
|
||||||
|
|
||||||
|
Só JS: não exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
### Rodada 70 — Colunas "Saldo" e "Em aberto" do razão expandido (2026-10-02)
|
||||||
|
|
||||||
|
Pedido do usuário, em `linhaRazaoHtml()`:
|
||||||
|
|
||||||
|
- **Em aberto**: só a linha "Saldo a pagar do vínculo #N" (saldo de pagamento parcial, para validação posterior) mostra valor. O lançamento sem vínculo passou a mostrar "—" (antes repetia o próprio valor); o valor em aberto dele continua no fato/sugestão da coluna Situação, nos totais e na lista separada de não conciliados (`linhaLancamentoHtml()`, sem mudança).
|
||||||
|
- **Saldo** (do Questor): "—" nas linhas de vínculo fechado; continua no lançamento sem vínculo e nas linhas de vínculo `parcial` com saldo a pagar.
|
||||||
|
|
||||||
|
Só JS: não exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
### Rodada 71 — Relatório como terceira aba do detalhe (2026-10-02)
|
||||||
|
|
||||||
|
Pedido do usuário: o relatório de análise fazer parte da própria tela, como uma terceira aba ("Relatório", depois de "Conciliação" e "Em aberto há mais de 3 meses").
|
||||||
|
|
||||||
|
- Escolha da implementação: o mesmo HTML do relatório num `<iframe>` (`#conc-relatorio-frame`), não uma reescrita no tema do Portal, para não haver duas versões do relatório a manter e manter a marca do escritório e a impressão.
|
||||||
|
- `relatorio` (views.py) passa a responder `X-Frame-Options: SAMEORIGIN` (o `XFrameOptionsMiddleware` só define o cabeçalho quando ele não existe; o padrão é DENY).
|
||||||
|
- Tela: o iframe é recarregado a cada abertura da aba (`carregarRelatorio()`) e pelo botão "Atualizar relatório"; a altura acompanha o conteúdo (`ResizeObserver` no body do iframe), para não haver rolagem dupla; `?aba=relatorio` no hash. O "Imprimir" de dentro do relatório imprime só o relatório (o Ctrl+P da página imprimiria a tela inteira).
|
||||||
|
- O botão "Gerar relatório" (nova aba) continua.
|
||||||
|
|
||||||
|
Mexe em views.py: exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
### Rodada 72 — Relatório refeito no visual do Portal, só dentro da aba (2026-10-02)
|
||||||
|
|
||||||
|
Na sequência da rodada 71, o usuário decidiu: o relatório é só para validação do contador dentro do Portal, nada vai ao cliente, então não precisa de documento separado. Escolhas confirmadas com ele: refazer no visual do Portal (em vez do iframe) e manter a impressão.
|
||||||
|
|
||||||
|
- `templates/conciliacao-fornecedores-relatorio.html` reescrito como **trecho de HTML** (sem `<html>`, `<head>`, `<script>`, fontes da web nem logo do escritório), com classes `.conc-rel-*` (novas em `conciliacao-fornecedores.css`) sobre os tokens de tema, então acompanha claro/escuro. Mesmas seções e dados de antes (parecer, cards, riscos, decisões, o que falta decidir, anexo); saiu só a linha "Processado por / Arquivo" do parecer, que já está no cabeçalho do detalhe.
|
||||||
|
- View `relatorio`: sem `logo_data_uri` e sem o `X-Frame-Options: SAMEORIGIN` da rodada 71. `_logo_relatorio_data_uri()` continua, porque o Relatório Contábil usa.
|
||||||
|
- Tela: o iframe saiu; `carregarRelatorio()` busca o trecho com `fetch` e o insere em `#conc-relatorio-conteudo` (ignora a resposta se o usuário trocou de conciliação ou pediu outro relatório no meio; limpa o de outra conciliação antes de carregar). Botão **"Gerar relatório" removido** do cabeçalho do detalhe.
|
||||||
|
- Barra da aba: "Situação em ...", "Incluir anexo na impressão", "Atualizar relatório" e **"Imprimir"**. A impressão clona o relatório num `#conc-impressao` direto no body; o `@media print` esconde o resto do Portal, redefine os tokens com uma paleta clara fixa só dentro dele e mantém as regras de A4 compacto da rodada 47 (blocos abertos, anexo opcional, linha de tabela/cabeçalho de bloco/cards sem quebra, cabeçalho da tabela repetido). Um cabeçalho só de impressão traz empresa, período, data-base e data de geração.
|
||||||
|
|
||||||
|
Mexe em views.py: exige reiniciar o `runserver`. Conferido: o trecho renderiza com a conciliação #24 (6 blocos de risco, ~480 linhas de tabela). Sem conferência visual nem de impressão neste ambiente.
|
||||||
|
|
||||||
|
### Rodada 73 — Confirmação ao validar/desvalidar quando a conta muda de chip (2026-10-02)
|
||||||
|
|
||||||
|
Pedido do usuário: ao validar ou desvalidar, o fornecedor sumia da lista (ia para outro chip) sem aviso. Agora `confirmaMudancaDeChip()` pergunta antes (`pidConfirm`), citando o chip de destino ("...sairá desta lista. Ela passa a aparecer em "Saldo em aberto" (e em "Validados")" / "...Ela volta para "Pendentes""), no checkbox da linha e no botão "Marcar como validada"/"Remover validação" do detalhe. Só pergunta quando a conta sai da lista atual (`contaPassaFiltro()` com a validação nova); cancelar devolve o checkbox ao estado anterior. O destino é previsível porque validar não muda saldo nem vínculos. A validação da aba "Em aberto há mais de 3 meses" não mudou.
|
||||||
|
|
||||||
|
Só JS: não exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
### Rodada 74 — Ajuste do vínculo: parcelamento, juros ou desconto (2026-10-02)
|
||||||
|
|
||||||
|
Pedido do usuário: ao vincular um pagamento parcial, escolher entre parcelamento e juros/desconto (juros quando há mais pagamento que títulos, desconto quando há mais títulos que pagamentos), e uma coluna nos validados com um símbolo por situação, filtrável.
|
||||||
|
|
||||||
|
- **Escolha**: o confirm "Pagamento parcial" virou o modal "Diferença no vínculo" (`pidConcEscolheAjuste()`), com **Parcelamento** (como antes: vínculo `parcial`, título em aberto pelo saldo) e **Desconto** (vínculo `manual` que fecha, com a diferença). Aparece no "Vincular selecionados" e no "Adicionar ao vínculo" quando os títulos passam dos pagamentos além da tolerância. Servidor: `fechar` (opcional, padrão falso) em `ConciliacaoFornecedorVincularSerializer`, repassado a `_conciliacao_tipo_vinculo_manual()`, que não vira `parcial` com ele. Pago a maior (juros) e diferença dentro da tolerância continuam fechando direto, com o confirm reescrito ("será registrada como juros/desconto").
|
||||||
|
- **Interpretação da implementação**: a classificação não é gravada, é derivada de tipo + diferença (`pidConcAjusteVinculo()`): `parcial` > 0 = parcelamento; `manual`/`tolerancia` < 0 = juros, > 0 = desconto. Com isso, vínculos já existentes também ganham ícone, e diferença dentro da tolerância conta como juros/desconto. Parcelamento quitado por inteiro (diferença zero) deixa de ter ícone. Sem migração.
|
||||||
|
- **Coluna "Ajuste"** na tabela de fornecedores (depois de "Saldo final"), só nos chips de validadas: um ícone por ajuste presente (`pidConcAjustesHtml()`; parcelamento azul com calendário, juros vermelho com seta para cima, desconto verde com seta para baixo), com o valor no balão. **Filtro "Ajuste:"** ao lado dos chips (Todos + um chip compacto por ajuste, ícone e quantidade dentro do chip atual, nome no balão, porque a barra de filtros é fixa e de uma linha), `?ajuste=` na URL; zera ao trocar de chip; filtro de card/etiqueta o ignora. `colspan` de 6 para 7.
|
||||||
|
- Na data, a conciliação #24 tinha 3 vínculos de parcelamento (contas ainda não validadas) e nenhum de juros/desconto.
|
||||||
|
|
||||||
|
Mexe em views.py/serializers.py: exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
### Rodada 75 — Sugestões de parcela mais visuais, com a sequência (2026-10-02)
|
||||||
|
|
||||||
|
Pedido do usuário: na sugestão do título (crédito), mostrar os pagamentos de forma mais visual, com o código, e melhorar a sugestão de parcela.
|
||||||
|
|
||||||
|
- `ItemAnalise.sequencia` (novo, padrão vazio), preenchido em `_conciliacao_analisa()` (views.py).
|
||||||
|
- `_linha_item()` (alertas.py): "Seq. 37879 · 26/06/2026 · R$ 2.163,57[ · extra]" (saldo de pagamento parcial: "Saldo a pagar · ..."). Usado nas linhas da parcela, do parcelamento, da combinação exata e da quitação dos mais antigos (antes "26/06/2026 (R$ 2.163,57)").
|
||||||
|
- Parcela (lado do pagamento): "Possível parcela N de K do título:" / linha do título / "Parcela de R$ 2.163,56 em 4x, com R$ 0,01 a mais (arredondamento)." / "Vincule para confirmar." N = ordem entre os pagamentos reconhecidos como parcela daquele título. Parcelamento (lado do título): "Possível parcelamento deste título em N pagamento(s):" / uma linha por pagamento com "parcela i de K" / "Pagos R$ X de R$ Y; R$ Z ainda em aberto."
|
||||||
|
- Tela: sugestão em várias linhas abre o balão sempre (antes só quando cortada) e é montada por `pidConcSugestaoEstruturadaHtml()` (classe `.conc-tooltip--sugestao`, até 560px): cabeçalho em negrito, lançamentos em colunas com a sequência em destaque, notas em cinza. Na célula e no XLSX continua numa linha (`pidConcTextoEmLinha()`/`texto_em_linha()`).
|
||||||
|
- Na sequência, pedido do usuário: o código no balão leva a **cor da natureza** do lançamento citado, como no razão (`.conc-natureza--D` coral para pagamento, `--C` roxo para título, `--saldo` dourado para o saldo a pagar). Os citados são sempre do lado oposto ao da linha da sugestão, então a natureza sai da própria linha (tem `.conc-natureza--D` = os citados são títulos; senão, pagamentos).
|
||||||
|
- Ajustes seguintes, pedido do usuário: o valor de cada lançamento no balão ficou do tamanho da data (a classe `.conc-tooltip__valor` colidia com o percentual grande do balão da barra de validação; virou `.conc-tooltip__lanc-valor`), e a última linha do parcelamento passou a ser só o que falta ("R$ 4.327,10 ainda em aberto."; sem saldo, "Os pagamentos quitam o título."), sem o "Pagos R$ X de R$ Y".
|
||||||
|
- Conferido na conciliação #24: 101 sugestões de parcela/soma com o texto novo (ex.: ROZINI, "Possível parcela 1 de 4 do título: Seq. 35359 · 28/05/2026 · R$ 8.654,22").
|
||||||
|
|
||||||
|
Mexe em alertas.py/views.py: exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
### Rodada 76 — Vínculo com desconto/juros aparece no razão como o parcelamento (2026-10-02)
|
||||||
|
|
||||||
|
Pedido do usuário: ao escolher "Desconto" (rodada 74), o vínculo fechava e sumia do razão expandido (que começa na primeira pendência), sem mostrar a diferença. Caso real: ROZINI, vínculo manual 19102 com desconto, depois desfeito e refeito como parcelamento (19103).
|
||||||
|
|
||||||
|
- Vínculos `manual` com ajuste desconto ou juros (`pidConcAjusteVinculo()`) entram no início do razão, como os de parcelamento (`ajusteFechado` em `montarDetalheConta()`).
|
||||||
|
- Logo abaixo do último crédito do vínculo (mesma regra da linha do saldo a pagar), uma **linha da diferença** (`linhaAjusteHtml()`): "Desconto do vínculo #N" / "Juros do vínculo #N", "Pagos R$ X de R$ Y em títulos", natureza "Desconto" (verde) ou "Juros" (vermelho), valor = diferença, sem seleção, saldo e em aberto "—" (o vínculo está fechado), situação "Vínculo fechado com desconto (pago a menor)." / "...com juros/multa (pago a maior).". Fundo verde/vermelho discreto, borda tracejada, destaca junto com o vínculo no hover e não é escondida por "Ocultar conciliados" (igual à do saldo a pagar).
|
||||||
|
- Só vínculos `manual` (os automáticos antigos de `tolerancia` não ganham a linha). Interpretação: o pedido dizia "especificando que são juros" para o desconto; a linha usa o nome certo de cada caso (desconto quando os títulos passam dos pagamentos, juros quando os pagamentos passam dos títulos). Na data, a conciliação #24 tinha 2 vínculos manuais com desconto (CASIO R$ 5.065,00 e EBAZAR R$ 103,99).
|
||||||
|
|
||||||
|
Só JS/CSS: não exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
### Rodada 77 — Clicar fora recolhe; copiar texto da linha do fornecedor (2026-10-02)
|
||||||
|
|
||||||
|
Pedidos do usuário:
|
||||||
|
|
||||||
|
- **Clicar fora recolhe** os fornecedores expandidos: listener de `click` no document (`abertas.clear()` + `renderContas()`), só na aba Conciliação e com alguma expandida. Não recolhe: clique dentro de `#conc-contas-body` (linha e expansão), em `.modal-overlay` ou `.conc-tooltip`, em elemento que a ação do clique já tirou da página (re-render depois de vincular/desfazer) ou que termina uma seleção de texto. Rascunhos de observação sobrevivem (já sobreviviam ao recolher).
|
||||||
|
- **Copiar texto** da linha do fornecedor (antes, o clique expandia e o re-render desfazia a seleção): arrastar para selecionar não expande (mesma regra das linhas de lançamento), e o clique simples espera 200 ms antes de expandir, cancelado se vier um segundo clique (duplo/triplo clique selecionam palavra/linha sem expandir).
|
||||||
|
|
||||||
|
Só JS: não exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
### Rodada 78 — Desfazer vínculo remove a validação da conta (2026-10-02)
|
||||||
|
|
||||||
|
Pedido do usuário: desfazer um vínculo de uma conta já validada deve desmarcar a validação (caso real: ROZINI, validada, vínculo de parcelamento #10).
|
||||||
|
|
||||||
|
- `ConciliacaoFornecedorVinculoViewSet.destroy` (views.py): na mesma transação do desfazer, se a conta estava validada (por pessoa ou automática), zera `validada`/`validada_por`/`validada_em`; a observação fica (mesmo critério da atualização do razão). A resposta (`{conta, resumo}`) já volta com a conta "a validar", então ela sai do chip de validadas para "Pendentes" na tela.
|
||||||
|
- Tela: a pergunta "Desfazer vínculo" avisa quando a conta está validada ("A conta está validada: a validação será removida e ela volta para "Pendentes".").
|
||||||
|
- Só o desfazer. Vincular, acrescentar a um vínculo e a validação dos títulos antigos (aba própria) não mexem na validação.
|
||||||
|
|
||||||
|
Mexe em views.py: exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
### Rodada 79 — Situação "Atenção" vira "A validar" (2026-10-02)
|
||||||
|
|
||||||
|
Pedido do usuário: rótulo da situação `atencao` passa de "Atenção" para **"A validar"** na tela (`PID_CONC_STATUS`), no XLSX (`_CONCILIACAO_ROTULO_STATUS`, views.py) e no relatório (`ROTULO_STATUS` em `relatorio.py`; "N a validar" no card e "conta em situação A validar ou Crítico" no subtítulo das validadas com pendência). Chave interna (`atencao`), severidade "Média" dos alertas e cor da etiqueta sem mudança.
|
||||||
|
|
||||||
|
Ponto de atenção levado ao usuário: a situação é calculada pelos alertas e independe da validação, então uma conta validada com alerta médio/baixo mostra a etiqueta "A validar" ao lado do checkbox marcado.
|
||||||
|
|
||||||
|
Mexe em views.py/relatorio.py: exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
### Rodada 80 — Clique com texto já selecionado volta a expandir (2026-10-02)
|
||||||
|
|
||||||
|
Bug da rodada 77, apontado pelo usuário: com um texto selecionado, clicar na linha do fornecedor não expandia; o primeiro clique só desfazia a seleção. Causa: o critério era "há texto selecionado no momento do clique", e o navegador só desfaz uma seleção existente depois do clique. Troca de critério para **arraste** (`pidConcFoiArraste()`: o mouse andou mais de 4 px entre o mousedown, registrado em captura no document, e o clique). Vale para a linha do fornecedor, para o "clicar fora recolhe" e para o clique que marca/desmarca a linha de lançamento do razão (que tinha a mesma checagem). O duplo/triplo clique continua sem expandir.
|
||||||
|
|
||||||
|
Só JS: não exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
### Rodada 81 — Filtro de ajuste no cabeçalho da coluna (2026-10-02)
|
||||||
|
|
||||||
|
Pedido do usuário: o filtro "Ajuste:" (chips ao lado dos chips de grupo, rodada 74) passar para a própria coluna "Ajuste", e a dúvida se três ícones cabem na coluna.
|
||||||
|
|
||||||
|
- O cabeçalho virou o botão **"Ajuste ▾"** (`#conc-ajuste-th`), que abre um menu (`.conc-ajuste-menu`, fixo no body como o balão, porque o cabeçalho é sticky) com Todos, Parcelamento, Juros e Desconto, cada um com ícone e quantidade dentro do chip atual. Fecha ao escolher, ao clicar fora, com Esc e ao rolar. Com filtro ativo, o cabeçalho mostra o ícone do ajuste filtrado e fica na cor de destaque. O clique no cabeçalho não conta como "clicar fora" para recolher os fornecedores.
|
||||||
|
- Os chips de ajuste da barra de filtros saíram (HTML, JS e CSS `.conc-ajuste-filtro`/`.conc-chip--ajuste`).
|
||||||
|
- Coluna de 90 para 100px: três ícones (22px + 4px entre eles, ~74px) cabem com folga.
|
||||||
|
|
||||||
|
Só HTML/JS/CSS: não exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
### Rodada 82 — Atualização por acréscimo do período novo, com conferência de saldos (2026-10-02)
|
||||||
|
|
||||||
|
Pedido do usuário: anexar só o mês novo apagava o trabalho (a atualização era sempre substituição: lançamentos antes do arquivo viravam um saldo anterior sem composição, todos os vínculos eram desfeitos, validações retiradas e contas ausentes do arquivo excluídas). E: "o ideal é que ele valide o saldo inicial do novo razão, que deve bater com o saldo final do que consta atualmente".
|
||||||
|
|
||||||
|
- **Modo escolhido pela data de início do arquivo** (`_conciliacao_inicio_acrescimo()`, views.py): começa depois do início da conciliação = **acréscimo**; no início ou antes = **substituição** (a de antes, sem mudança). Arquivo que deixa buraco depois do fim atual é recusado ("falta o razão de ... a ...").
|
||||||
|
- **Conferência de saldos** (`_conciliacao_confere_saldos_acrescimo()`), antes de gravar: saldo inicial de cada conta do arquivo = saldo inicial da conta + lançamentos antes do início do arquivo (no caso comum, o saldo final). Diferença recusa a atualização inteira, listando até 15 contas ("arquivo R$ X C, conciliação R$ Y C"). O erro do modal passou a respeitar quebras de linha.
|
||||||
|
- **Acréscimo** em `_conciliacao_atualiza_conta(..., inicio_acrescimo)`: o prefixo (lançamentos antes do início e o saldo anterior original) fica intacto; só o resto é comparado com o arquivo; o "Saldo anterior" do arquivo não vira lançamento; os novos ganham `ordem` depois do prefixo; `saldo_inicial` não muda; o motor roda sobre a conta inteira com os vinculados fixos. Conta ausente do arquivo fica como está (`contas_sem_movimento`); conta nova entra inteira. Período: início da conciliação até o "Razão até" novo. A regra de validação continua: conta validada por pessoa que recebeu lançamento volta a "a validar".
|
||||||
|
- Tela: texto do modal "Atualizar razão" explica os dois modos; o resumo depois da atualização diz qual foi aplicado e quantos fornecedores ficaram sem movimento.
|
||||||
|
- **Testes** (transação desfeita, nada gravado) contra a conciliação #24: (A) outubro simulado, 114 contas com saldo: 114 lançamentos novos, 0 removidos, 632 vínculos e 22 manuais/parciais mantidos (ROZINI com o parcelamento de R$ 4.327,10), 146 contas sem movimento mantidas, 13 validações retiradas; (B) razão da própria conciliação a partir de 01/08: nada muda; (C) uma conta com R$ 10 a mais: recusado apontando só ela; (D) começo em 05/10: recusado pelo buraco. O arquivo real "Atualizado 09-2026" (só saldos, sem lançamentos, 110 contas) foi recusado: 42 contas com saldo em 31/08 diferente do da conciliação (ex.: ROZINI R$ 2.163,56 x R$ 35.712,21), sinal de que foi exportado depois de mudanças no Questor.
|
||||||
|
|
||||||
|
Mexe em views.py: exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
Depois da rodada 82, a pedido do usuário (para refazer a conciliação e testar o acréscimo com a competência 09/2026), a conciliação #24 (empresa 2017) foi excluída via shell: 260 contas, 2.609 lançamentos, 632 vínculos e 36 registros de histórico de vinculações. Era a única conciliação no banco; nenhum arquivo envolvido.
|
||||||
|
|
||||||
|
### Rodada 83 — Saldo anterior duplicado na exportação do Questor (2026-10-02)
|
||||||
|
|
||||||
|
Bug apontado pelo usuário: a CASIO (5198422) aparecia com saldo final R$ 15.586,75 C na conciliação #25 (empresa 2017, arquivo "Teste Exportação - Fornecedores - 2017 - Atualizado - teste.xlsx", criada pelo usuário depois da exclusão da #24), e o saldo certo é R$ 1.828,74 C.
|
||||||
|
|
||||||
|
- Causa: nesse arquivo, o Questor exportou o "Saldo anterior" datado de 31/03/2025 (R$ 13.758,01 C na CASIO) **seguido** dos lançamentos de 31/12/2024 a 27/03/2025, cuja soma é exatamente esse saldo anterior. O parser somava os dois. A coluna Saldo do próprio Questor também partia do saldo anterior, então a conferência "saldo recalculado x arquivo" não acusava nada.
|
||||||
|
- Medição: 82 das 376 contas do arquivo têm lançamentos até a data do saldo anterior, e em todas a soma deles é igual ao saldo anterior. Descontando o saldo anterior nessas contas, o saldo final das 376 bate com o saldo anterior em 31/08/2026 do arquivo "Atualizado 09-2026". Os outros arquivos de teste (2017 antigos e 1751) não têm o caso.
|
||||||
|
- Correção em `parser.py` (`_corrige_saldo_anterior_duplicado()`, chamada para cada conta ao fim da leitura): soma igual = saldo anterior zerado, data dele passa para a véspera do primeiro lançamento (o período passa a começar em 31/12/2024) e cada `saldo_apos` é descontado do valor duplicado; soma diferente = `ArquivoRazaoInvalidoError` pedindo nova exportação (não há como saber qual dos dois vale).
|
||||||
|
- A conciliação #25, já gravada com os saldos errados, não é corrigida sozinha: precisa ser refeita (ou atualizada por substituição com o mesmo arquivo).
|
||||||
|
|
||||||
|
Mexe em parser.py: exige reiniciar o `runserver`.
|
||||||
|
|
||||||
|
Depois da rodada 83, a pedido do usuário (para refazer com o parser corrigido), a conciliação #25 (empresa 2017, criada com o saldo anterior duplicado) foi excluída via shell. Era a única conciliação no banco; nenhum arquivo envolvido.
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -59,6 +59,9 @@ SEVERIDADE_MEDIA = "media"
|
|||||||
SEVERIDADE_BAIXA = "baixa"
|
SEVERIDADE_BAIXA = "baixa"
|
||||||
|
|
||||||
STATUS_CONCILIADA = "conciliada"
|
STATUS_CONCILIADA = "conciliada"
|
||||||
|
# Sem alerta, mas com saldo de pagamento parcial ainda a pagar (vínculo
|
||||||
|
# manual que segue para os meses seguintes); pedido do usuário.
|
||||||
|
STATUS_PARCIAL = "parcial"
|
||||||
STATUS_ATENCAO = "atencao"
|
STATUS_ATENCAO = "atencao"
|
||||||
STATUS_DIVERGENTE = "divergente"
|
STATUS_DIVERGENTE = "divergente"
|
||||||
|
|
||||||
@ -76,6 +79,7 @@ class ItemAnalise:
|
|||||||
valor: Decimal
|
valor: Decimal
|
||||||
vinculo_id: int | None
|
vinculo_id: int | None
|
||||||
descricao: str # histórico, para citar o título abatido
|
descricao: str # histórico, para citar o título abatido
|
||||||
|
sequencia: str = "" # código do lançamento no Questor, citado nas sugestões
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -242,6 +246,18 @@ def _rotulo_pagamento(debito: ItemAnalise) -> str:
|
|||||||
return f"{debito.data:%d/%m/%Y} ({_moeda(debito.valor)})"
|
return f"{debito.data:%d/%m/%Y} ({_moeda(debito.valor)})"
|
||||||
|
|
||||||
|
|
||||||
|
def _linha_item(item: ItemAnalise, extra: str = "") -> str:
|
||||||
|
"""Uma linha de lançamento numa sugestão em várias linhas: "Seq. 37879 ·
|
||||||
|
26/06/2026 · R$ 2.163,57" (pedido do usuário: ver o código de cada
|
||||||
|
lançamento). O balão da tela monta cada linha "Seq. ..." em colunas,
|
||||||
|
separando por " · "; o XLSX junta tudo numa linha (`texto_em_linha`)."""
|
||||||
|
codigo = "Saldo a pagar" if _eh_saldo_parcial(item) else f"Seq. {item.sequencia or '—'}"
|
||||||
|
partes = [codigo, f"{item.data:%d/%m/%Y}", _moeda(item.valor)]
|
||||||
|
if extra:
|
||||||
|
partes.append(extra)
|
||||||
|
return " · ".join(partes)
|
||||||
|
|
||||||
|
|
||||||
def _natureza_diferenca(diferenca: Decimal) -> str:
|
def _natureza_diferenca(diferenca: Decimal) -> str:
|
||||||
"""`diferenca` = título - pagamento (mesma convenção do vínculo)."""
|
"""`diferenca` = título - pagamento (mesma convenção do vínculo)."""
|
||||||
return "a menos (possível desconto)" if diferenca > 0 else "a mais (possível juros/multa)"
|
return "a menos (possível desconto)" if diferenca > 0 else "a mais (possível juros/multa)"
|
||||||
@ -293,11 +309,10 @@ def _analisa_pendentes(
|
|||||||
if alvo.natureza == "C":
|
if alvo.natureza == "C":
|
||||||
nome_grupo, nome_outro = "pagamentos", "pagamento(s)"
|
nome_grupo, nome_outro = "pagamentos", "pagamento(s)"
|
||||||
rotulo_alvo = _ref_titulo(alvo).removeprefix("do ")
|
rotulo_alvo = _ref_titulo(alvo).removeprefix("do ")
|
||||||
rotulos = [_rotulo_pagamento(d) for d in grupo]
|
|
||||||
else:
|
else:
|
||||||
nome_grupo, nome_outro = "títulos", "título(s)"
|
nome_grupo, nome_outro = "títulos", "título(s)"
|
||||||
rotulo_alvo = f"pagamento de {_rotulo_pagamento(alvo)}"
|
rotulo_alvo = f"pagamento de {_rotulo_pagamento(alvo)}"
|
||||||
rotulos = [_rotulo_titulo(c) for c in grupo]
|
rotulos = [_linha_item(i) for i in grupo]
|
||||||
sugestao_combinacao[alvo.id] = (
|
sugestao_combinacao[alvo.id] = (
|
||||||
"\n".join([f"Soma exata de {len(grupo)} {nome_grupo}:", *rotulos, "Vincule para confirmar."]),
|
"\n".join([f"Soma exata de {len(grupo)} {nome_grupo}:", *rotulos, "Vincule para confirmar."]),
|
||||||
[i.id for i in grupo],
|
[i.id for i in grupo],
|
||||||
@ -347,6 +362,7 @@ def _analisa_pendentes(
|
|||||||
# Pagamentos que parecem parcela de cada título (base concreta da sugestão
|
# Pagamentos que parecem parcela de cada título (base concreta da sugestão
|
||||||
# do lado do título, ao contrário da ordem de antiguidade).
|
# do lado do título, ao contrário da ordem de antiguidade).
|
||||||
parcelas_do_titulo: dict[int, list[int]] = {c.id: [] for c in creditos}
|
parcelas_do_titulo: dict[int, list[int]] = {c.id: [] for c in creditos}
|
||||||
|
qtd_parcelas_do_pagamento: dict[int, int] = {} # id do débito -> "em Nx"
|
||||||
# Títulos de uma quitação exata dos mais antigos: id -> (texto, relacionados)
|
# Títulos de uma quitação exata dos mais antigos: id -> (texto, relacionados)
|
||||||
quitacao_do_titulo: dict[int, tuple[str, list[int]]] = {}
|
quitacao_do_titulo: dict[int, tuple[str, list[int]]] = {}
|
||||||
|
|
||||||
@ -426,18 +442,20 @@ def _analisa_pendentes(
|
|||||||
if escolhida and usados == [parcela_de.id]:
|
if escolhida and usados == [parcela_de.id]:
|
||||||
qtd_parcelas, referencia, diferenca = escolhida[1]
|
qtd_parcelas, referencia, diferenca = escolhida[1]
|
||||||
parcelas_do_titulo[parcela_de.id].append(debito.id)
|
parcelas_do_titulo[parcela_de.id].append(debito.id)
|
||||||
# Quantidade de parcelas no texto (pedido do usuário).
|
qtd_parcelas_do_pagamento[debito.id] = qtd_parcelas
|
||||||
if diferenca == 0:
|
# Em linhas (pedido do usuário: mais visual, com o código): o
|
||||||
sugestao = (
|
# título, depois a parcela (número entre as reconhecidas deste
|
||||||
f"Possível parcela {_ref_titulo(parcela_de)}, em {qtd_parcelas}x de "
|
# título, quantidade e valor; arredondamento, se houver).
|
||||||
f"{_moeda(debito.valor)}. Vincule para confirmar."
|
if _eh_saldo_parcial(parcela_de):
|
||||||
)
|
cabecalho = "Possível parcela do saldo a pagar:"
|
||||||
else:
|
else:
|
||||||
|
numero = len(parcelas_do_titulo[parcela_de.id])
|
||||||
|
cabecalho = f"Possível parcela {numero} de {qtd_parcelas} do título:"
|
||||||
|
detalhe_parcela = f"Parcela de {_moeda(referencia)} em {qtd_parcelas}x"
|
||||||
|
if diferenca:
|
||||||
sentido = "a mais" if diferenca > 0 else "a menos"
|
sentido = "a mais" if diferenca > 0 else "a menos"
|
||||||
sugestao = (
|
detalhe_parcela += f", com {_moeda(diferenca)} {sentido} (arredondamento)"
|
||||||
f"Possível parcela {_ref_titulo(parcela_de)}, em {qtd_parcelas}x de "
|
sugestao = "\n".join([cabecalho, _linha_item(parcela_de), f"{detalhe_parcela}.", "Vincule para confirmar."])
|
||||||
f"{_moeda(referencia)}, com {_moeda(diferenca)} {sentido} (arredondamento). Vincule para confirmar."
|
|
||||||
)
|
|
||||||
elif (
|
elif (
|
||||||
len(titulos) >= 2
|
len(titulos) >= 2
|
||||||
and disponivel_usado == debito.valor
|
and disponivel_usado == debito.valor
|
||||||
@ -451,7 +469,7 @@ def _analisa_pendentes(
|
|||||||
sugestao = "\n".join(
|
sugestao = "\n".join(
|
||||||
[
|
[
|
||||||
f"Soma exata dos {len(titulos)} títulos mais antigos em aberto:",
|
f"Soma exata dos {len(titulos)} títulos mais antigos em aberto:",
|
||||||
*(_rotulo_titulo(c) for c in titulos),
|
*(_linha_item(c) for c in titulos),
|
||||||
"Vincule para confirmar.",
|
"Vincule para confirmar.",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@ -511,9 +529,19 @@ def _analisa_pendentes(
|
|||||||
pagos = parcelas_do_titulo[credito.id]
|
pagos = parcelas_do_titulo[credito.id]
|
||||||
soma = sum((por_id_debito[i].valor for i in pagos), Decimal("0"))
|
soma = sum((por_id_debito[i].valor for i in pagos), Decimal("0"))
|
||||||
alvo = "deste saldo" if _eh_saldo_parcial(credito) else "deste título"
|
alvo = "deste saldo" if _eh_saldo_parcial(credito) else "deste título"
|
||||||
sugestao = f"Possível parcelamento: {len(pagos)} pagamento(s) de parcela {alvo}, somando {_moeda(soma)}"
|
# Em linhas, um pagamento por linha com o código (pedido do
|
||||||
|
# usuário), e no fim quanto foi pago e quanto falta.
|
||||||
|
linhas = [f"Possível parcelamento {alvo} em {len(pagos)} pagamento(s):"]
|
||||||
|
for numero, debito_id in enumerate(pagos, start=1):
|
||||||
|
debito = por_id_debito[debito_id]
|
||||||
|
extra = "" if _eh_saldo_parcial(credito) else f"parcela {numero} de {qtd_parcelas_do_pagamento[debito_id]}"
|
||||||
|
linhas.append(_linha_item(debito, extra))
|
||||||
|
# Abaixo, só o que falta (pedido do usuário; o pago já está nas linhas).
|
||||||
restante_titulo = credito.valor - soma
|
restante_titulo = credito.valor - soma
|
||||||
sugestao += f"; {_moeda(restante_titulo)} ainda em aberto." if restante_titulo > 0 else "."
|
linhas.append(
|
||||||
|
f"{_moeda(restante_titulo)} ainda em aberto." if restante_titulo > 0 else "Os pagamentos quitam o título."
|
||||||
|
)
|
||||||
|
sugestao = "\n".join(linhas)
|
||||||
else:
|
else:
|
||||||
pagos = []
|
pagos = []
|
||||||
sugestao = ""
|
sugestao = ""
|
||||||
@ -629,6 +657,8 @@ def analisa_conta(
|
|||||||
status = STATUS_DIVERGENTE
|
status = STATUS_DIVERGENTE
|
||||||
elif severidades:
|
elif severidades:
|
||||||
status = STATUS_ATENCAO
|
status = STATUS_ATENCAO
|
||||||
|
elif any(v.tipo == TIPO_PARCIAL and v.diferenca > 0 for v in vinculos):
|
||||||
|
status = STATUS_PARCIAL
|
||||||
else:
|
else:
|
||||||
status = STATUS_CONCILIADA
|
status = STATUS_CONCILIADA
|
||||||
|
|
||||||
|
|||||||
@ -114,6 +114,39 @@ class RazaoExtraido:
|
|||||||
return max(datas) if datas else None
|
return max(datas) if datas else None
|
||||||
|
|
||||||
|
|
||||||
|
def _corrige_saldo_anterior_duplicado(conta: ContaExtraida) -> None:
|
||||||
|
"""Exportação do Questor com o "Saldo anterior" datado **depois** de
|
||||||
|
lançamentos que vêm listados logo abaixo dele (caso real, empresa 2017:
|
||||||
|
saldo anterior em 31/03/2025 seguido dos lançamentos de 31/12/2024 a
|
||||||
|
27/03/2025). O saldo anterior já é a soma desses lançamentos, então
|
||||||
|
somá-lo de novo dobrava o saldo (CASIO: R$ 15.586,75 em vez de
|
||||||
|
R$ 1.828,74), inclusive na coluna Saldo impressa pelo Questor, que também
|
||||||
|
parte dele. Nas 82 contas do arquivo com esse caso, a soma bateu exatamente
|
||||||
|
com o saldo anterior, e os saldos corrigidos das 376 contas bateram com o
|
||||||
|
saldo anterior do mês seguinte. Por isso: soma igual = saldo anterior
|
||||||
|
duplicado, vira zero, a data dele passa para a véspera do primeiro
|
||||||
|
lançamento e a coluna Saldo é corrigida; soma diferente = arquivo que não
|
||||||
|
dá para interpretar com segurança, recusado (rodada 83 do CHANGELOG)."""
|
||||||
|
if conta.data_saldo_inicial is None:
|
||||||
|
return
|
||||||
|
antes = [lanc for lanc in conta.lancamentos if lanc.data <= conta.data_saldo_inicial]
|
||||||
|
if not antes:
|
||||||
|
return
|
||||||
|
soma = sum((lanc.valor if lanc.natureza == "C" else -lanc.valor for lanc in antes), Decimal("0"))
|
||||||
|
if soma != conta.saldo_inicial:
|
||||||
|
raise ArquivoRazaoInvalidoError(
|
||||||
|
f"Conta {conta.conta_numero} ({conta.fornecedor}): o \"Saldo anterior\" está datado de "
|
||||||
|
f"{conta.data_saldo_inicial:%d/%m/%Y}, mas há lançamentos até essa data que não o explicam. "
|
||||||
|
"Exporte o razão de novo, com a data inicial do período depois do saldo anterior."
|
||||||
|
)
|
||||||
|
duplicado = conta.saldo_inicial
|
||||||
|
conta.saldo_inicial = Decimal("0")
|
||||||
|
conta.data_saldo_inicial = min(lanc.data for lanc in conta.lancamentos) - timedelta(days=1)
|
||||||
|
for lanc in conta.lancamentos:
|
||||||
|
if lanc.saldo_apos is not None:
|
||||||
|
lanc.saldo_apos -= duplicado
|
||||||
|
|
||||||
|
|
||||||
def _sem_acento(texto: str) -> str:
|
def _sem_acento(texto: str) -> str:
|
||||||
normalizado = unicodedata.normalize("NFKD", texto)
|
normalizado = unicodedata.normalize("NFKD", texto)
|
||||||
return "".join(c for c in normalizado if not unicodedata.combining(c)).upper().strip()
|
return "".join(c for c in normalizado if not unicodedata.combining(c)).upper().strip()
|
||||||
@ -285,6 +318,9 @@ def extrai_razao(conteudo: bytes, nome_arquivo: str) -> RazaoExtraido:
|
|||||||
if not contas:
|
if not contas:
|
||||||
raise ArquivoRazaoInvalidoError("Nenhuma conta encontrada no arquivo (linhas \"Conta: ...\").")
|
raise ArquivoRazaoInvalidoError("Nenhuma conta encontrada no arquivo (linhas \"Conta: ...\").")
|
||||||
|
|
||||||
|
for conta in contas:
|
||||||
|
_corrige_saldo_anterior_duplicado(conta)
|
||||||
|
|
||||||
datas = [lanc.data for conta in contas for lanc in conta.lancamentos]
|
datas = [lanc.data for conta in contas for lanc in conta.lancamentos]
|
||||||
# O início sai da linha "Saldo anterior" (datada do dia anterior ao
|
# O início sai da linha "Saldo anterior" (datada do dia anterior ao
|
||||||
# período), não do primeiro lançamento: o período pode começar sem
|
# período), não do primeiro lançamento: o período pode começar sem
|
||||||
|
|||||||
@ -43,8 +43,9 @@ SEVERIDADES = (alertas.SEVERIDADE_ALTA, alertas.SEVERIDADE_MEDIA, alertas.SEVERI
|
|||||||
ROTULO_SEVERIDADE = {alertas.SEVERIDADE_ALTA: "Alto", alertas.SEVERIDADE_MEDIA: "Médio", alertas.SEVERIDADE_BAIXA: "Baixo"}
|
ROTULO_SEVERIDADE = {alertas.SEVERIDADE_ALTA: "Alto", alertas.SEVERIDADE_MEDIA: "Médio", alertas.SEVERIDADE_BAIXA: "Baixo"}
|
||||||
ROTULO_STATUS = {
|
ROTULO_STATUS = {
|
||||||
alertas.STATUS_CONCILIADA: "Conciliada",
|
alertas.STATUS_CONCILIADA: "Conciliada",
|
||||||
alertas.STATUS_ATENCAO: "Atenção",
|
alertas.STATUS_PARCIAL: "Parcial",
|
||||||
alertas.STATUS_DIVERGENTE: "Divergente",
|
alertas.STATUS_ATENCAO: "A validar",
|
||||||
|
alertas.STATUS_DIVERGENTE: "Crítico",
|
||||||
}
|
}
|
||||||
RISCO_GERAL_ALTO = "alto"
|
RISCO_GERAL_ALTO = "alto"
|
||||||
RISCO_GERAL_MODERADO = "moderado"
|
RISCO_GERAL_MODERADO = "moderado"
|
||||||
@ -462,7 +463,7 @@ def monta_relatorio(contas: list[ContaRelatorio], data_base: date, processado_po
|
|||||||
(
|
(
|
||||||
"validada_sem_justificativa",
|
"validada_sem_justificativa",
|
||||||
alertas.SEVERIDADE_ALTA,
|
alertas.SEVERIDADE_ALTA,
|
||||||
"Conta validada com divergência e sem observação",
|
"Conta em situação Crítico validada sem observação",
|
||||||
"A decisão foi tomada, mas não há justificativa registrada para quem revisar depois.",
|
"A decisão foi tomada, mas não há justificativa registrada para quem revisar depois.",
|
||||||
False,
|
False,
|
||||||
),
|
),
|
||||||
@ -649,7 +650,9 @@ def monta_relatorio(contas: list[ContaRelatorio], data_base: date, processado_po
|
|||||||
)
|
)
|
||||||
|
|
||||||
# decisões
|
# decisões
|
||||||
if conta.validada and conta.validada_por and analise.status != alertas.STATUS_CONCILIADA:
|
# Parcial não é pendência de análise: o saldo a pagar já aparece em
|
||||||
|
# "Saldos de pagamento parcial".
|
||||||
|
if conta.validada and conta.validada_por and analise.status not in (alertas.STATUS_CONCILIADA, alertas.STATUS_PARCIAL):
|
||||||
validacoes.append(
|
validacoes.append(
|
||||||
ValidacaoComPendencia(
|
ValidacaoComPendencia(
|
||||||
conta.conta_numero,
|
conta.conta_numero,
|
||||||
@ -781,8 +784,8 @@ def monta_relatorio(contas: list[ContaRelatorio], data_base: date, processado_po
|
|||||||
if not sintese:
|
if not sintese:
|
||||||
sintese.append("Nenhum ponto de risco pendente")
|
sintese.append("Nenhum ponto de risco pendente")
|
||||||
|
|
||||||
# divergentes primeiro, depois atenção e conciliadas
|
# críticas primeiro, depois atenção, parciais e conciliadas
|
||||||
ordem_status = (alertas.STATUS_DIVERGENTE, alertas.STATUS_ATENCAO, alertas.STATUS_CONCILIADA)
|
ordem_status = (alertas.STATUS_DIVERGENTE, alertas.STATUS_ATENCAO, alertas.STATUS_PARCIAL, alertas.STATUS_CONCILIADA)
|
||||||
nao_validadas.sort(key=lambda c: (ordem_status.index(c.status), -c.valor))
|
nao_validadas.sort(key=lambda c: (ordem_status.index(c.status), -c.valor))
|
||||||
validacoes.sort(key=lambda v: (v.status != alertas.STATUS_DIVERGENTE, bool(v.observacao)))
|
validacoes.sort(key=lambda v: (v.status != alertas.STATUS_DIVERGENTE, bool(v.observacao)))
|
||||||
ajustes.sort(key=lambda a: a.ultima_acao, reverse=True)
|
ajustes.sort(key=lambda a: a.ultima_acao, reverse=True)
|
||||||
|
|||||||
@ -2966,6 +2966,10 @@ class ConciliacaoFornecedorVincularSerializer(serializers.Serializer):
|
|||||||
# 2+ para criar um vínculo; 1 já basta para acrescentar a um existente
|
# 2+ para criar um vínculo; 1 já basta para acrescentar a um existente
|
||||||
# (a exigência de débito + crédito da criação fica na view).
|
# (a exigência de débito + crédito da criação fica na view).
|
||||||
lancamentos = serializers.ListField(child=serializers.IntegerField(), min_length=1)
|
lancamentos = serializers.ListField(child=serializers.IntegerField(), min_length=1)
|
||||||
|
# Títulos maiores que os pagamentos além da tolerância: por padrão é
|
||||||
|
# pagamento parcial (parcelamento, o saldo continua em aberto); `fechar`
|
||||||
|
# = o contador escolheu "Desconto", o vínculo fecha com a diferença.
|
||||||
|
fechar = serializers.BooleanField(required=False, default=False)
|
||||||
|
|
||||||
|
|
||||||
class ConciliacaoFornecedorValidarSerializer(serializers.Serializer):
|
class ConciliacaoFornecedorValidarSerializer(serializers.Serializer):
|
||||||
|
|||||||
@ -5378,8 +5378,9 @@ class ContabilAchadoDePaulaViewSet(ContabilAchadoViewSet):
|
|||||||
_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_ATENCAO: "Atenção",
|
conciliacao_alertas.STATUS_PARCIAL: "Parcial",
|
||||||
conciliacao_alertas.STATUS_DIVERGENTE: "Divergente",
|
conciliacao_alertas.STATUS_ATENCAO: "A validar",
|
||||||
|
conciliacao_alertas.STATUS_DIVERGENTE: "Crítico",
|
||||||
}
|
}
|
||||||
_CONCILIACAO_ROTULO_SEVERIDADE = {"alta": "Alta", "media": "Média", "baixa": "Baixa"}
|
_CONCILIACAO_ROTULO_SEVERIDADE = {"alta": "Alta", "media": "Média", "baixa": "Baixa"}
|
||||||
|
|
||||||
@ -5410,6 +5411,7 @@ def _conciliacao_analisa(
|
|||||||
valor=lanc.valor,
|
valor=lanc.valor,
|
||||||
vinculo_id=lanc.vinculo_id,
|
vinculo_id=lanc.vinculo_id,
|
||||||
descricao=lanc.historico,
|
descricao=lanc.historico,
|
||||||
|
sequencia=lanc.sequencia or "",
|
||||||
)
|
)
|
||||||
for lanc in lancamentos
|
for lanc in lancamentos
|
||||||
],
|
],
|
||||||
@ -5758,18 +5760,34 @@ def _conciliacao_atualiza_conta(
|
|||||||
bloqueados: set[int],
|
bloqueados: set[int],
|
||||||
usuario: Usuario,
|
usuario: Usuario,
|
||||||
contagem: Counter[str],
|
contagem: Counter[str],
|
||||||
|
inicio_acrescimo: date | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Substitui os lançamentos de uma conta pelos do arquivo novo. Lançamento
|
"""Substitui os lançamentos de uma conta pelos do arquivo novo. Lançamento
|
||||||
igual (mesma data, sequência, natureza e valor) é mantido com o mesmo id e
|
igual (mesma data, sequência, natureza e valor) é mantido com o mesmo id e
|
||||||
o vínculo que tinha; vínculo com algum lançamento que saiu do razão é
|
o vínculo que tinha; vínculo com algum lançamento que saiu do razão é
|
||||||
desfeito inteiro (o manual fica registrado no histórico). O motor roda de
|
desfeito inteiro (o manual fica registrado no histórico). O motor roda de
|
||||||
novo só sobre o que ficou pendente, sem mexer nos vínculos mantidos nem nos
|
novo só sobre o que ficou pendente, sem mexer nos vínculos mantidos nem nos
|
||||||
lançamentos que o contador tirou de um vínculo automático (`bloqueados`)."""
|
lançamentos que o contador tirou de um vínculo automático (`bloqueados`).
|
||||||
|
|
||||||
|
`inicio_acrescimo` (arquivo que começa depois do início da conciliação):
|
||||||
|
os lançamentos antes dessa data (e o saldo anterior original) ficam como
|
||||||
|
estão; só o que vem a partir dela é comparado com o arquivo, e o saldo
|
||||||
|
anterior do arquivo não vira lançamento (já foi conferido contra o saldo
|
||||||
|
da conciliação em `_conciliacao_aplica_atualizacao`)."""
|
||||||
linhas_novas = _conciliacao_linhas_razao(conta, extraida, razao, conciliacao.data_base)
|
linhas_novas = _conciliacao_linhas_razao(conta, extraida, razao, conciliacao.data_base)
|
||||||
|
prefixo: list[ConciliacaoFornecedorLancamento] = []
|
||||||
|
if inicio_acrescimo:
|
||||||
|
prefixo = [l for l in antigos if l.eh_saldo_anterior or l.data < inicio_acrescimo]
|
||||||
|
linhas_novas = [l for l in linhas_novas if not l.eh_saldo_anterior]
|
||||||
|
base = max((l.ordem for l in prefixo), default=-1) + 1
|
||||||
|
for pos, linha in enumerate(linhas_novas):
|
||||||
|
linha.ordem = base + pos
|
||||||
|
ids_prefixo = {l.id for l in prefixo}
|
||||||
comparacao = conciliacao_atualizacao.compara(
|
comparacao = conciliacao_atualizacao.compara(
|
||||||
[
|
[
|
||||||
conciliacao_atualizacao.LancamentoComparavel(l.id, l.eh_saldo_anterior, l.data, l.sequencia, l.natureza, l.valor)
|
conciliacao_atualizacao.LancamentoComparavel(l.id, l.eh_saldo_anterior, l.data, l.sequencia, l.natureza, l.valor)
|
||||||
for l in antigos
|
for l in antigos
|
||||||
|
if l.id not in ids_prefixo
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
conciliacao_atualizacao.LancamentoComparavel(pos, l.eh_saldo_anterior, l.data, l.sequencia, l.natureza, l.valor)
|
conciliacao_atualizacao.LancamentoComparavel(pos, l.eh_saldo_anterior, l.data, l.sequencia, l.natureza, l.valor)
|
||||||
@ -5802,7 +5820,13 @@ def _conciliacao_atualiza_conta(
|
|||||||
if removidos:
|
if removidos:
|
||||||
ConciliacaoFornecedorLancamento.objects.filter(id__in=removidos).delete()
|
ConciliacaoFornecedorLancamento.objects.filter(id__in=removidos).delete()
|
||||||
|
|
||||||
final: list[ConciliacaoFornecedorLancamento] = []
|
# Acréscimo: o que fica antes do arquivo entra como está (o vínculo de um
|
||||||
|
# deles só cai se envolvia lançamento que saiu; no banco o SET_NULL já
|
||||||
|
# soltou, aqui é a cópia em memória que o motor usa).
|
||||||
|
for lanc in prefixo:
|
||||||
|
if lanc.vinculo_id in afetados:
|
||||||
|
lanc.vinculo = None
|
||||||
|
final: list[ConciliacaoFornecedorLancamento] = list(prefixo)
|
||||||
mantidos: list[ConciliacaoFornecedorLancamento] = []
|
mantidos: list[ConciliacaoFornecedorLancamento] = []
|
||||||
criar: list[ConciliacaoFornecedorLancamento] = []
|
criar: list[ConciliacaoFornecedorLancamento] = []
|
||||||
for pos, nova in enumerate(linhas_novas):
|
for pos, nova in enumerate(linhas_novas):
|
||||||
@ -5849,7 +5873,9 @@ def _conciliacao_atualiza_conta(
|
|||||||
conta.ordem = extraida.ordem
|
conta.ordem = extraida.ordem
|
||||||
conta.classificacao = extraida.classificacao
|
conta.classificacao = extraida.classificacao
|
||||||
conta.fornecedor = extraida.fornecedor[:255]
|
conta.fornecedor = extraida.fornecedor[:255]
|
||||||
conta.saldo_inicial = extraida.saldo_inicial
|
if not inicio_acrescimo:
|
||||||
|
# no acréscimo, o saldo inicial continua o do começo da conciliação
|
||||||
|
conta.saldo_inicial = extraida.saldo_inicial
|
||||||
conta.saldo_final = extraida.saldo_final_calculado
|
conta.saldo_final = extraida.saldo_final_calculado
|
||||||
conta.saldo_final_arquivo = extraida.saldo_final_arquivo
|
conta.saldo_final_arquivo = extraida.saldo_final_arquivo
|
||||||
campos = ["ordem", "classificacao", "fornecedor", "saldo_inicial", "saldo_final", "saldo_final_arquivo"]
|
campos = ["ordem", "classificacao", "fornecedor", "saldo_inicial", "saldo_final", "saldo_final_arquivo"]
|
||||||
@ -5885,6 +5911,81 @@ def _conciliacao_aplica_periodo_fim(
|
|||||||
razao.periodo_fim = periodo_fim
|
razao.periodo_fim = periodo_fim
|
||||||
|
|
||||||
|
|
||||||
|
def _conciliacao_inicio_acrescimo(
|
||||||
|
conciliacao: ConciliacaoFornecedor, razao: conciliacao_parser.RazaoExtraido
|
||||||
|
) -> date | None:
|
||||||
|
"""Arquivo que começa depois do início da conciliação é um **acréscimo**
|
||||||
|
(pedido do usuário: anexar só o mês novo sem perder o trabalho): devolve a
|
||||||
|
data de início dele. Começando no início da conciliação ou antes, é a
|
||||||
|
substituição de sempre (None). Recusa o arquivo que deixa um buraco
|
||||||
|
depois do fim atual, porque os lançamentos desse intervalo ficariam de fora."""
|
||||||
|
inicio = razao.periodo_inicio
|
||||||
|
if not (inicio and conciliacao.periodo_inicio and inicio > conciliacao.periodo_inicio):
|
||||||
|
return None
|
||||||
|
fim = conciliacao.periodo_fim
|
||||||
|
if fim and inicio > fim + timedelta(days=1):
|
||||||
|
fmt = "%d/%m/%Y"
|
||||||
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"arquivo": (
|
||||||
|
f"O arquivo começa em {inicio:{fmt}}, mas a conciliação vai até {fim:{fmt}}: "
|
||||||
|
f"falta o razão de {fim + timedelta(days=1):{fmt}} a {inicio - timedelta(days=1):{fmt}}. "
|
||||||
|
f"Exporte o razão a partir de {fim + timedelta(days=1):{fmt}}."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return inicio
|
||||||
|
|
||||||
|
|
||||||
|
def _conciliacao_saldo_texto(valor: Decimal) -> str:
|
||||||
|
"""Saldo como na tela: "R$ 1.234,56 C" (credor) / "D" (devedor)."""
|
||||||
|
texto = f"{abs(valor):,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
|
||||||
|
return f"R$ {texto} {'D' if valor < 0 else 'C'}" if valor else "R$ 0,00"
|
||||||
|
|
||||||
|
|
||||||
|
def _conciliacao_confere_saldos_acrescimo(
|
||||||
|
pares: list[tuple[conciliacao_parser.ContaExtraida, ConciliacaoFornecedorConta | None]],
|
||||||
|
lancs_por_conta: dict[int, list[ConciliacaoFornecedorLancamento]],
|
||||||
|
inicio: date,
|
||||||
|
) -> None:
|
||||||
|
"""No acréscimo, o saldo inicial de cada conta do arquivo tem de bater com
|
||||||
|
o saldo da conciliação na véspera do início dele (pedido do usuário):
|
||||||
|
saldo inicial da conciliação + lançamentos antes dessa data. No caso
|
||||||
|
comum (arquivo começa no dia seguinte ao fim atual) é o saldo final da
|
||||||
|
conta. Diferença em qualquer conta recusa a atualização inteira, antes de
|
||||||
|
gravar qualquer coisa, listando as contas."""
|
||||||
|
divergentes: list[str] = []
|
||||||
|
for extraida, conta in pares:
|
||||||
|
if conta is None:
|
||||||
|
continue
|
||||||
|
esperado = conta.saldo_inicial + sum(
|
||||||
|
(
|
||||||
|
lanc.valor if lanc.natureza == "C" else -lanc.valor
|
||||||
|
for lanc in lancs_por_conta.get(conta.id, [])
|
||||||
|
if not lanc.eh_saldo_anterior and lanc.data < inicio
|
||||||
|
),
|
||||||
|
Decimal("0"),
|
||||||
|
)
|
||||||
|
if extraida.saldo_inicial != esperado:
|
||||||
|
divergentes.append(
|
||||||
|
f"{conta.conta_numero} - {conta.fornecedor}: arquivo {_conciliacao_saldo_texto(extraida.saldo_inicial)}, "
|
||||||
|
f"conciliação {_conciliacao_saldo_texto(esperado)}"
|
||||||
|
)
|
||||||
|
if divergentes:
|
||||||
|
vespera = inicio - timedelta(days=1)
|
||||||
|
lista = "\n".join(divergentes[:15])
|
||||||
|
resto = f"\n... e mais {len(divergentes) - 15} conta(s)." if len(divergentes) > 15 else ""
|
||||||
|
raise ValidationError(
|
||||||
|
{
|
||||||
|
"arquivo": (
|
||||||
|
f"O saldo inicial do arquivo não bate com o saldo da conciliação em {vespera:%d/%m/%Y} "
|
||||||
|
f"em {len(divergentes)} conta(s). Nada foi gravado. Confira se o razão é da mesma empresa "
|
||||||
|
f"e se houve lançamento alterado no Questor nesse período:\n{lista}{resto}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _conciliacao_aplica_atualizacao(
|
def _conciliacao_aplica_atualizacao(
|
||||||
conciliacao: ConciliacaoFornecedor, razao: conciliacao_parser.RazaoExtraido, usuario: Usuario
|
conciliacao: ConciliacaoFornecedor, razao: conciliacao_parser.RazaoExtraido, usuario: Usuario
|
||||||
) -> dict[str, int]:
|
) -> dict[str, int]:
|
||||||
@ -5919,13 +6020,28 @@ def _conciliacao_aplica_atualizacao(
|
|||||||
):
|
):
|
||||||
bloqueados_por_conta.setdefault(registro.conta_id, set()).update(l["id"] for l in registro.lancamentos)
|
bloqueados_por_conta.setdefault(registro.conta_id, set()).update(l["id"] for l in registro.lancamentos)
|
||||||
|
|
||||||
contagem: Counter[str] = Counter()
|
inicio_acrescimo = _conciliacao_inicio_acrescimo(conciliacao, razao)
|
||||||
|
|
||||||
|
# Pareamento conta do arquivo -> conta da conciliação (None = nova).
|
||||||
|
pares: list[tuple[conciliacao_parser.ContaExtraida, ConciliacaoFornecedorConta | None]] = []
|
||||||
usadas: set[int] = set()
|
usadas: set[int] = set()
|
||||||
for extraida in razao.contas:
|
for extraida in razao.contas:
|
||||||
conta = por_chave.get((extraida.conta_numero, extraida.classificacao))
|
conta = por_chave.get((extraida.conta_numero, extraida.classificacao))
|
||||||
if conta is None or conta.id in usadas:
|
if conta is None or conta.id in usadas:
|
||||||
conta = next((c for c in por_numero.get(extraida.conta_numero, []) if c.id not in usadas), None)
|
conta = next((c for c in por_numero.get(extraida.conta_numero, []) if c.id not in usadas), None)
|
||||||
if conta is None:
|
if conta is not None:
|
||||||
|
usadas.add(conta.id)
|
||||||
|
pares.append((extraida, conta))
|
||||||
|
|
||||||
|
if inicio_acrescimo:
|
||||||
|
_conciliacao_confere_saldos_acrescimo(pares, lancs_por_conta, inicio_acrescimo)
|
||||||
|
|
||||||
|
contagem: Counter[str] = Counter()
|
||||||
|
if inicio_acrescimo:
|
||||||
|
contagem["acrescimo"] = 1
|
||||||
|
for extraida, conta in pares:
|
||||||
|
nova = conta is None
|
||||||
|
if nova:
|
||||||
conta = ConciliacaoFornecedorConta.objects.create(
|
conta = ConciliacaoFornecedorConta.objects.create(
|
||||||
conciliacao=conciliacao,
|
conciliacao=conciliacao,
|
||||||
ordem=extraida.ordem,
|
ordem=extraida.ordem,
|
||||||
@ -5937,7 +6053,6 @@ def _conciliacao_aplica_atualizacao(
|
|||||||
saldo_final_arquivo=extraida.saldo_final_arquivo,
|
saldo_final_arquivo=extraida.saldo_final_arquivo,
|
||||||
)
|
)
|
||||||
contagem["contas_novas"] += 1
|
contagem["contas_novas"] += 1
|
||||||
usadas.add(conta.id)
|
|
||||||
_conciliacao_atualiza_conta(
|
_conciliacao_atualiza_conta(
|
||||||
conciliacao,
|
conciliacao,
|
||||||
conta,
|
conta,
|
||||||
@ -5948,14 +6063,23 @@ def _conciliacao_aplica_atualizacao(
|
|||||||
bloqueados_por_conta.get(conta.id, set()),
|
bloqueados_por_conta.get(conta.id, set()),
|
||||||
usuario,
|
usuario,
|
||||||
contagem,
|
contagem,
|
||||||
|
# conta que aparece pela primeira vez entra inteira, com o saldo
|
||||||
|
# anterior do arquivo como lançamento (não há o que preservar)
|
||||||
|
None if nova else inicio_acrescimo,
|
||||||
)
|
)
|
||||||
sairam = [c.id for c in existentes if c.id not in usadas]
|
sairam = [c.id for c in existentes if c.id not in usadas]
|
||||||
if sairam:
|
if inicio_acrescimo:
|
||||||
ConciliacaoFornecedorConta.objects.filter(id__in=sairam).delete()
|
# No acréscimo, conta fora do arquivo é conta sem movimento no período
|
||||||
contagem["contas_removidas"] = len(sairam)
|
# novo: fica como está (não é excluída, como na substituição).
|
||||||
|
contagem["contas_sem_movimento"] = len(sairam)
|
||||||
|
else:
|
||||||
|
if sairam:
|
||||||
|
ConciliacaoFornecedorConta.objects.filter(id__in=sairam).delete()
|
||||||
|
contagem["contas_removidas"] = len(sairam)
|
||||||
|
|
||||||
conciliacao.data_base = timezone.localdate()
|
conciliacao.data_base = timezone.localdate()
|
||||||
conciliacao.periodo_inicio = razao.periodo_inicio
|
if not inicio_acrescimo:
|
||||||
|
conciliacao.periodo_inicio = razao.periodo_inicio
|
||||||
conciliacao.periodo_fim = razao.periodo_fim
|
conciliacao.periodo_fim = razao.periodo_fim
|
||||||
conciliacao.save(update_fields=["data_base", "periodo_inicio", "periodo_fim"])
|
conciliacao.save(update_fields=["data_base", "periodo_inicio", "periodo_fim"])
|
||||||
|
|
||||||
@ -6271,9 +6395,9 @@ class ConciliacaoFornecedorViewSet(viewsets.ModelViewSet):
|
|||||||
@action(detail=True, methods=["get"])
|
@action(detail=True, methods=["get"])
|
||||||
def relatorio(self, request: Request, pk: str | None = None) -> HttpResponse:
|
def relatorio(self, request: Request, pk: str | None = None) -> HttpResponse:
|
||||||
"""Relatório de análise para o contador (decisões tomadas e situações
|
"""Relatório de análise para o contador (decisões tomadas e situações
|
||||||
de risco): HTML autocontido com a marca do escritório, aberto em nova
|
de risco): trecho de HTML no visual do Portal, inserido pela tela na
|
||||||
aba pela tela (GET, mesmo motivo de `ContabilApuracaoViewSet.dashboard`).
|
aba "Relatório" do detalhe (uso interno, nada vai ao cliente). Só
|
||||||
Só leitura, pode ser gerado com a conciliação em andamento."""
|
leitura, pode ser gerado com a conciliação em andamento."""
|
||||||
conciliacao = self.get_object()
|
conciliacao = self.get_object()
|
||||||
contexto = {
|
contexto = {
|
||||||
"conciliacao": conciliacao,
|
"conciliacao": conciliacao,
|
||||||
@ -6281,24 +6405,25 @@ class ConciliacaoFornecedorViewSet(viewsets.ModelViewSet):
|
|||||||
"meses_vencido": conciliacao_alertas.MESES_VENCIDO,
|
"meses_vencido": conciliacao_alertas.MESES_VENCIDO,
|
||||||
"janela_duplicidade": conciliacao_relatorio.JANELA_DIAS_DUPLICIDADE,
|
"janela_duplicidade": conciliacao_relatorio.JANELA_DIAS_DUPLICIDADE,
|
||||||
"gerado_em": timezone.now(),
|
"gerado_em": timezone.now(),
|
||||||
"logo_data_uri": _logo_relatorio_data_uri(),
|
|
||||||
}
|
}
|
||||||
html = render_to_string("conciliacao-fornecedores-relatorio.html", contexto)
|
html = render_to_string("conciliacao-fornecedores-relatorio.html", contexto)
|
||||||
return HttpResponse(html, content_type="text/html; charset=utf-8")
|
return HttpResponse(html, content_type="text/html; charset=utf-8")
|
||||||
|
|
||||||
|
|
||||||
def _conciliacao_tipo_vinculo_manual(
|
def _conciliacao_tipo_vinculo_manual(
|
||||||
conciliacao: ConciliacaoFornecedor, lancs: list[ConciliacaoFornecedorLancamento]
|
conciliacao: ConciliacaoFornecedor, lancs: list[ConciliacaoFornecedorLancamento], fechar: bool = False
|
||||||
) -> tuple[str, Decimal]:
|
) -> tuple[str, Decimal]:
|
||||||
"""Tipo e diferença de um vínculo manual. Créditos maiores que os débitos
|
"""Tipo e diferença de um vínculo manual. Créditos maiores que os débitos
|
||||||
além da tolerância: pagamento parcial, o saldo continua em aberto. Dentro
|
além da tolerância: pagamento parcial (parcelamento), o saldo continua em
|
||||||
da tolerância (ou pago a maior), fecha como vínculo com diferença
|
aberto, salvo `fechar` (o contador escolheu "Desconto"). Dentro da
|
||||||
|
tolerância (ou pago a maior), fecha como vínculo com diferença
|
||||||
(juros/desconto). Usado ao criar e ao acrescentar lançamentos."""
|
(juros/desconto). Usado ao criar e ao acrescentar lançamentos."""
|
||||||
diferenca = sum((lanc.valor if lanc.natureza == "C" else -lanc.valor for lanc in lancs), Decimal("0"))
|
diferenca = sum((lanc.valor if lanc.natureza == "C" else -lanc.valor for lanc in lancs), Decimal("0"))
|
||||||
tolerancia = conciliacao_motor.Tolerancia(conciliacao.tolerancia_valor, conciliacao.tolerancia_percentual)
|
tolerancia = conciliacao_motor.Tolerancia(conciliacao.tolerancia_valor, conciliacao.tolerancia_percentual)
|
||||||
total_creditos = sum((lanc.valor for lanc in lancs if lanc.natureza == "C"), Decimal("0"))
|
total_creditos = sum((lanc.valor for lanc in lancs if lanc.natureza == "C"), Decimal("0"))
|
||||||
limite = Decimal(tolerancia.limite_centavos(int(total_creditos * 100))) / 100
|
limite = Decimal(tolerancia.limite_centavos(int(total_creditos * 100))) / 100
|
||||||
tipo = conciliacao_motor.TIPO_PARCIAL if diferenca > limite else conciliacao_motor.TIPO_MANUAL
|
parcial = diferenca > limite and not fechar
|
||||||
|
tipo = conciliacao_motor.TIPO_PARCIAL if parcial else conciliacao_motor.TIPO_MANUAL
|
||||||
return tipo, diferenca
|
return tipo, diferenca
|
||||||
|
|
||||||
|
|
||||||
@ -6357,7 +6482,7 @@ class ConciliacaoFornecedorContaViewSet(viewsets.GenericViewSet):
|
|||||||
raise ValidationError({"lancamentos": "Há lançamentos que já estão vinculados. Desfaça o vínculo antes."})
|
raise ValidationError({"lancamentos": "Há lançamentos que já estão vinculados. Desfaça o vínculo antes."})
|
||||||
if len(lancs) < 2 or {lanc.natureza for lanc in lancs} != {"C", "D"}:
|
if len(lancs) < 2 or {lanc.natureza for lanc in lancs} != {"C", "D"}:
|
||||||
raise ValidationError({"lancamentos": "Selecione pelo menos um débito e um crédito."})
|
raise ValidationError({"lancamentos": "Selecione pelo menos um débito e um crédito."})
|
||||||
tipo, diferenca = _conciliacao_tipo_vinculo_manual(conta.conciliacao, lancs)
|
tipo, diferenca = _conciliacao_tipo_vinculo_manual(conta.conciliacao, lancs, entrada.validated_data["fechar"])
|
||||||
vinculo = ConciliacaoFornecedorVinculo.objects.create(
|
vinculo = ConciliacaoFornecedorVinculo.objects.create(
|
||||||
conta=conta, tipo=tipo, diferenca=diferenca, criado_por=request.user
|
conta=conta, tipo=tipo, diferenca=diferenca, criado_por=request.user
|
||||||
)
|
)
|
||||||
@ -6455,6 +6580,14 @@ class ConciliacaoFornecedorVinculoViewSet(viewsets.GenericViewSet):
|
|||||||
request.user,
|
request.user,
|
||||||
)
|
)
|
||||||
vinculo.delete()
|
vinculo.delete()
|
||||||
|
# Desfazer um vínculo muda a conta que já foi conferida: a
|
||||||
|
# validação sai e ela volta a "a validar" (pedido do usuário). A
|
||||||
|
# observação fica, como na atualização do razão.
|
||||||
|
if conta.validada:
|
||||||
|
conta.validada = False
|
||||||
|
conta.validada_por = None
|
||||||
|
conta.validada_em = None
|
||||||
|
conta.save(update_fields=["validada", "validada_por", "validada_em"])
|
||||||
return _conciliacao_resposta_conta(conta)
|
return _conciliacao_resposta_conta(conta)
|
||||||
|
|
||||||
@action(detail=True, methods=["post"])
|
@action(detail=True, methods=["post"])
|
||||||
@ -6478,7 +6611,9 @@ class ConciliacaoFornecedorVinculoViewSet(viewsets.GenericViewSet):
|
|||||||
raise ValidationError({"lancamentos": "Há lançamentos que já estão vinculados. Desfaça o vínculo antes."})
|
raise ValidationError({"lancamentos": "Há lançamentos que já estão vinculados. Desfaça o vínculo antes."})
|
||||||
ConciliacaoFornecedorLancamento.objects.filter(id__in=ids).update(vinculo=vinculo)
|
ConciliacaoFornecedorLancamento.objects.filter(id__in=ids).update(vinculo=vinculo)
|
||||||
todos = list(ConciliacaoFornecedorLancamento.objects.filter(vinculo=vinculo))
|
todos = list(ConciliacaoFornecedorLancamento.objects.filter(vinculo=vinculo))
|
||||||
vinculo.tipo, vinculo.diferenca = _conciliacao_tipo_vinculo_manual(conta.conciliacao, todos)
|
vinculo.tipo, vinculo.diferenca = _conciliacao_tipo_vinculo_manual(
|
||||||
|
conta.conciliacao, todos, entrada.validated_data["fechar"]
|
||||||
|
)
|
||||||
vinculo.criado_por = request.user
|
vinculo.criado_por = request.user
|
||||||
vinculo.save(update_fields=["tipo", "diferenca", "criado_por"])
|
vinculo.save(update_fields=["tipo", "diferenca", "criado_por"])
|
||||||
_conciliacao_registra_historico(
|
_conciliacao_registra_historico(
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -405,13 +405,11 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Empresa</th>
|
<th>Empresa</th>
|
||||||
<th>Data-base</th>
|
|
||||||
<th>Período</th>
|
<th>Período</th>
|
||||||
<th class="conc-num">Fornecedores</th>
|
<th class="conc-num">Fornecedores</th>
|
||||||
<th class="conc-num">Conciliados</th>
|
<th class="conc-num">Críticos</th>
|
||||||
<th class="conc-num">Divergentes</th>
|
|
||||||
<th class="conc-num">Em aberto</th>
|
<th class="conc-num">Em aberto</th>
|
||||||
<th>Criado por</th>
|
<th>Última atualização</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -443,10 +441,6 @@
|
|||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 4v12M12 16l4-4M12 16l-4-4" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 18v1a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-1" stroke-linecap="round"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 4v12M12 16l4-4M12 16l-4-4" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 18v1a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-1" stroke-linecap="round"/></svg>
|
||||||
Exportar XLSX
|
Exportar XLSX
|
||||||
</a>
|
</a>
|
||||||
<a class="btn-solid" id="conc-relatorio-btn" href="#" target="_blank" rel="noopener">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="8" y1="13" x2="16" y2="13"/><line x1="8" y1="17" x2="13" y2="17"/></svg>
|
|
||||||
Gerar relatório
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -458,18 +452,18 @@
|
|||||||
<div class="pa-tabs conc-abas" id="conc-abas">
|
<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 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>
|
<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>
|
||||||
|
<button type="button" class="pa-tab" data-conc-aba="relatorio">Relatório</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pa-tab-panel" data-conc-painel="conciliacao">
|
<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-grupo="pendentes">Pendentes</button>
|
||||||
<button type="button" class="conc-chip" data-conc-status="divergente">Divergentes</button>
|
<button type="button" class="conc-chip" data-conc-grupo="parcial">Parcial</button>
|
||||||
<button type="button" class="conc-chip" data-conc-status="atencao">Atenção</button>
|
<button type="button" class="conc-chip" data-conc-grupo="saldo_aberto">Saldo em aberto</button>
|
||||||
<button type="button" class="conc-chip" data-conc-status="conciliada">Conciliados</button>
|
<button type="button" class="conc-chip" data-conc-grupo="conciliados">Conciliados</button>
|
||||||
<button type="button" class="conc-chip" data-conc-status="validada">Validados</button>
|
<button type="button" class="conc-chip" data-conc-grupo="validados">Validados</button>
|
||||||
</div>
|
</div>
|
||||||
<label class="conc-toggle"><input type="checkbox" id="conc-mostrar-validadas" /> Mostrar validadas <span class="conc-chip__count" id="conc-mostrar-validadas-qtd">0</span></label>
|
|
||||||
<div class="pa-search">
|
<div class="pa-search">
|
||||||
<input type="search" id="conc-contas-busca" name="busca_fornecedor" placeholder="Buscar fornecedor ou conta…" autocomplete="off" />
|
<input type="search" id="conc-contas-busca" name="busca_fornecedor" 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>
|
<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>
|
||||||
@ -484,11 +478,11 @@
|
|||||||
<th class="conc-expand-col"></th>
|
<th class="conc-expand-col"></th>
|
||||||
<th data-sort="conta_numero">Conta <span class="conc-sort-icon">↕</span></th>
|
<th data-sort="conta_numero">Conta <span class="conc-sort-icon">↕</span></th>
|
||||||
<th data-sort="fornecedor">Fornecedor <span class="conc-sort-icon">↕</span></th>
|
<th data-sort="fornecedor">Fornecedor <span class="conc-sort-icon">↕</span></th>
|
||||||
<th class="conc-num" data-sort="saldo_inicial">Saldo inicial <span class="conc-sort-icon">↕</span></th>
|
<th class="conc-num" data-sort="saldo_final">Saldo final <span class="conc-sort-icon">↕</span></th>
|
||||||
<th class="conc-num" data-sort="saldo_final">Saldo residual <span class="conc-sort-icon">↕</span></th>
|
<!-- Filtro por ajuste no próprio cabeçalho (menu `pidConcAbreMenuAjuste`) -->
|
||||||
<th class="conc-num" data-sort="total_vencido">Em aberto > 3 meses <span class="conc-sort-icon">↕</span></th>
|
<th class="conc-ajuste-col"><button type="button" class="conc-ajuste-th" id="conc-ajuste-th" aria-haspopup="menu">Ajuste <span class="conc-ajuste-th__ativo" id="conc-ajuste-th-ativo"></span><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg></button></th>
|
||||||
<th data-sort="status">Situação <span class="conc-sort-icon">↕</span></th>
|
<th data-sort="status">Situação <span class="conc-sort-icon">↕</span></th>
|
||||||
<th class="conc-validado-col" data-sort="validada">Validado <span class="conc-sort-icon">↕</span></th>
|
<th class="conc-validado-col" data-sort="validada_em">Validado <span class="conc-sort-icon">↕</span></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="conc-contas-body"></tbody>
|
<tbody id="conc-contas-body"></tbody>
|
||||||
@ -529,6 +523,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="conc-empty" id="conc-venc-vazia" hidden></p>
|
<p class="conc-empty" id="conc-venc-vazia" hidden></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Relatório de análise (trecho de HTML do servidor), gerado de
|
||||||
|
novo a cada vez que a aba é aberta -->
|
||||||
|
<div class="pa-tab-panel" data-conc-painel="relatorio" hidden>
|
||||||
|
<div class="conc-relatorio-barra">
|
||||||
|
<span class="conc-relatorio-barra__texto" id="conc-relatorio-gerado"></span>
|
||||||
|
<label class="conc-toggle"><input type="checkbox" id="conc-relatorio-anexo" /> Incluir anexo na impressão</label>
|
||||||
|
<button type="button" class="btn-ghost conc-btn-sm" id="conc-relatorio-recarregar">Atualizar relatório</button>
|
||||||
|
<button type="button" class="btn-solid conc-btn-sm" id="conc-relatorio-imprimir" disabled>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
|
||||||
|
Imprimir
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="conc-relatorio-conteudo"></div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@ -607,11 +616,11 @@
|
|||||||
<h2 class="modal-card__title">Atualizar razão</h2>
|
<h2 class="modal-card__title">Atualizar razão</h2>
|
||||||
<p class="modal-card__subtitle" id="conc-atualizar-empresa"></p>
|
<p class="modal-card__subtitle" id="conc-atualizar-empresa"></p>
|
||||||
<div class="conc-atualizar-info">
|
<div class="conc-atualizar-info">
|
||||||
<p>O arquivo novo <strong>substitui</strong> o razão desta conciliação, e a análise é refeita sobre o período dele.</p>
|
<p>O tratamento depende de onde o arquivo começa:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>O que ficou antes do início do arquivo novo passa a ser só o saldo anterior de cada fornecedor.</li>
|
<li><strong>Só o período novo</strong> (começa depois do início da conciliação, ex.: só o mês seguinte): os lançamentos são <strong>acrescentados</strong> e tudo o que já existe fica como está. O saldo inicial de cada fornecedor no arquivo precisa bater com o saldo da conciliação; se algum não bater, nada é gravado.</li>
|
||||||
<li>Vínculos, validações e observações são mantidos quando os lançamentos envolvidos continuam no arquivo novo; vínculo com lançamento que saiu do razão é desfeito.</li>
|
<li><strong>Razão desde o início</strong> (começa no início da conciliação ou antes): o arquivo <strong>substitui</strong> o razão. Vínculos e validações ficam onde os lançamentos continuam no arquivo; o que ficar antes do início dele vira só o saldo anterior.</li>
|
||||||
<li>Conta que recebeu ou perdeu lançamentos volta a "a validar" (a observação fica).</li>
|
<li>Nos dois casos, conta que recebeu ou perdeu lançamentos volta a "a validar" (a observação fica).</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="conc-atualizar-info__atual" id="conc-atualizar-periodo"></p>
|
<p class="conc-atualizar-info__atual" id="conc-atualizar-periodo"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user