Reestruturação das regras de conciliação

This commit is contained in:
Gabriel 2026-10-02 17:55:50 -03:00
parent 543b1171c7
commit 201bc4eff4
12 changed files with 2563 additions and 933 deletions

View File

@ -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-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).
**Marca nova "P.I.D."**:

View File

@ -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).
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

View File

@ -59,6 +59,9 @@ SEVERIDADE_MEDIA = "media"
SEVERIDADE_BAIXA = "baixa"
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_DIVERGENTE = "divergente"
@ -76,6 +79,7 @@ class ItemAnalise:
valor: Decimal
vinculo_id: int | None
descricao: str # histórico, para citar o título abatido
sequencia: str = "" # código do lançamento no Questor, citado nas sugestões
@dataclass
@ -242,6 +246,18 @@ def _rotulo_pagamento(debito: ItemAnalise) -> str:
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:
"""`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)"
@ -293,11 +309,10 @@ def _analisa_pendentes(
if alvo.natureza == "C":
nome_grupo, nome_outro = "pagamentos", "pagamento(s)"
rotulo_alvo = _ref_titulo(alvo).removeprefix("do ")
rotulos = [_rotulo_pagamento(d) for d in grupo]
else:
nome_grupo, nome_outro = "títulos", "título(s)"
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] = (
"\n".join([f"Soma exata de {len(grupo)} {nome_grupo}:", *rotulos, "Vincule para confirmar."]),
[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
# 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}
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)
quitacao_do_titulo: dict[int, tuple[str, list[int]]] = {}
@ -426,18 +442,20 @@ def _analisa_pendentes(
if escolhida and usados == [parcela_de.id]:
qtd_parcelas, referencia, diferenca = escolhida[1]
parcelas_do_titulo[parcela_de.id].append(debito.id)
# Quantidade de parcelas no texto (pedido do usuário).
if diferenca == 0:
sugestao = (
f"Possível parcela {_ref_titulo(parcela_de)}, em {qtd_parcelas}x de "
f"{_moeda(debito.valor)}. Vincule para confirmar."
)
qtd_parcelas_do_pagamento[debito.id] = qtd_parcelas
# Em linhas (pedido do usuário: mais visual, com o código): o
# título, depois a parcela (número entre as reconhecidas deste
# título, quantidade e valor; arredondamento, se houver).
if _eh_saldo_parcial(parcela_de):
cabecalho = "Possível parcela do saldo a pagar:"
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"
sugestao = (
f"Possível parcela {_ref_titulo(parcela_de)}, em {qtd_parcelas}x de "
f"{_moeda(referencia)}, com {_moeda(diferenca)} {sentido} (arredondamento). Vincule para confirmar."
)
detalhe_parcela += f", com {_moeda(diferenca)} {sentido} (arredondamento)"
sugestao = "\n".join([cabecalho, _linha_item(parcela_de), f"{detalhe_parcela}.", "Vincule para confirmar."])
elif (
len(titulos) >= 2
and disponivel_usado == debito.valor
@ -451,7 +469,7 @@ def _analisa_pendentes(
sugestao = "\n".join(
[
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.",
]
)
@ -511,9 +529,19 @@ def _analisa_pendentes(
pagos = parcelas_do_titulo[credito.id]
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"
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
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:
pagos = []
sugestao = ""
@ -629,6 +657,8 @@ def analisa_conta(
status = STATUS_DIVERGENTE
elif severidades:
status = STATUS_ATENCAO
elif any(v.tipo == TIPO_PARCIAL and v.diferenca > 0 for v in vinculos):
status = STATUS_PARCIAL
else:
status = STATUS_CONCILIADA

View File

@ -114,6 +114,39 @@ class RazaoExtraido:
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:
normalizado = unicodedata.normalize("NFKD", texto)
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:
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]
# 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

View File

@ -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_STATUS = {
alertas.STATUS_CONCILIADA: "Conciliada",
alertas.STATUS_ATENCAO: "Atenção",
alertas.STATUS_DIVERGENTE: "Divergente",
alertas.STATUS_PARCIAL: "Parcial",
alertas.STATUS_ATENCAO: "A validar",
alertas.STATUS_DIVERGENTE: "Crítico",
}
RISCO_GERAL_ALTO = "alto"
RISCO_GERAL_MODERADO = "moderado"
@ -462,7 +463,7 @@ def monta_relatorio(contas: list[ContaRelatorio], data_base: date, processado_po
(
"validada_sem_justificativa",
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.",
False,
),
@ -649,7 +650,9 @@ def monta_relatorio(contas: list[ContaRelatorio], data_base: date, processado_po
)
# 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(
ValidacaoComPendencia(
conta.conta_numero,
@ -781,8 +784,8 @@ def monta_relatorio(contas: list[ContaRelatorio], data_base: date, processado_po
if not sintese:
sintese.append("Nenhum ponto de risco pendente")
# divergentes primeiro, depois atenção e conciliadas
ordem_status = (alertas.STATUS_DIVERGENTE, alertas.STATUS_ATENCAO, alertas.STATUS_CONCILIADA)
# críticas primeiro, depois atenção, parciais e conciliadas
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))
validacoes.sort(key=lambda v: (v.status != alertas.STATUS_DIVERGENTE, bool(v.observacao)))
ajustes.sort(key=lambda a: a.ultima_acao, reverse=True)

View File

@ -2966,6 +2966,10 @@ class ConciliacaoFornecedorVincularSerializer(serializers.Serializer):
# 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).
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):

View File

@ -5378,8 +5378,9 @@ class ContabilAchadoDePaulaViewSet(ContabilAchadoViewSet):
_CONCILIACAO_PERMISSAO = ("utilitarios", "conciliacao-fornecedores")
_CONCILIACAO_ROTULO_STATUS = {
conciliacao_alertas.STATUS_CONCILIADA: "Conciliada",
conciliacao_alertas.STATUS_ATENCAO: "Atenção",
conciliacao_alertas.STATUS_DIVERGENTE: "Divergente",
conciliacao_alertas.STATUS_PARCIAL: "Parcial",
conciliacao_alertas.STATUS_ATENCAO: "A validar",
conciliacao_alertas.STATUS_DIVERGENTE: "Crítico",
}
_CONCILIACAO_ROTULO_SEVERIDADE = {"alta": "Alta", "media": "Média", "baixa": "Baixa"}
@ -5410,6 +5411,7 @@ def _conciliacao_analisa(
valor=lanc.valor,
vinculo_id=lanc.vinculo_id,
descricao=lanc.historico,
sequencia=lanc.sequencia or "",
)
for lanc in lancamentos
],
@ -5758,18 +5760,34 @@ def _conciliacao_atualiza_conta(
bloqueados: set[int],
usuario: Usuario,
contagem: Counter[str],
inicio_acrescimo: date | None = None,
) -> None:
"""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
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
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)
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(
[
conciliacao_atualizacao.LancamentoComparavel(l.id, l.eh_saldo_anterior, l.data, l.sequencia, l.natureza, l.valor)
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)
@ -5802,7 +5820,13 @@ def _conciliacao_atualiza_conta(
if removidos:
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] = []
criar: list[ConciliacaoFornecedorLancamento] = []
for pos, nova in enumerate(linhas_novas):
@ -5849,6 +5873,8 @@ def _conciliacao_atualiza_conta(
conta.ordem = extraida.ordem
conta.classificacao = extraida.classificacao
conta.fornecedor = extraida.fornecedor[:255]
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_arquivo = extraida.saldo_final_arquivo
@ -5885,6 +5911,81 @@ def _conciliacao_aplica_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(
conciliacao: ConciliacaoFornecedor, razao: conciliacao_parser.RazaoExtraido, usuario: Usuario
) -> 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)
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()
for extraida in razao.contas:
conta = por_chave.get((extraida.conta_numero, extraida.classificacao))
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)
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(
conciliacao=conciliacao,
ordem=extraida.ordem,
@ -5937,7 +6053,6 @@ def _conciliacao_aplica_atualizacao(
saldo_final_arquivo=extraida.saldo_final_arquivo,
)
contagem["contas_novas"] += 1
usadas.add(conta.id)
_conciliacao_atualiza_conta(
conciliacao,
conta,
@ -5948,13 +6063,22 @@ def _conciliacao_aplica_atualizacao(
bloqueados_por_conta.get(conta.id, set()),
usuario,
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]
if inicio_acrescimo:
# No acréscimo, conta fora do arquivo é conta sem movimento no período
# 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()
if not inicio_acrescimo:
conciliacao.periodo_inicio = razao.periodo_inicio
conciliacao.periodo_fim = razao.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"])
def relatorio(self, request: Request, pk: str | None = None) -> HttpResponse:
"""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
aba pela tela (GET, mesmo motivo de `ContabilApuracaoViewSet.dashboard`).
Só leitura, pode ser gerado com a conciliação em andamento."""
de risco): trecho de HTML no visual do Portal, inserido pela tela na
aba "Relatório" do detalhe (uso interno, nada vai ao cliente). Só
leitura, pode ser gerado com a conciliação em andamento."""
conciliacao = self.get_object()
contexto = {
"conciliacao": conciliacao,
@ -6281,24 +6405,25 @@ class ConciliacaoFornecedorViewSet(viewsets.ModelViewSet):
"meses_vencido": conciliacao_alertas.MESES_VENCIDO,
"janela_duplicidade": conciliacao_relatorio.JANELA_DIAS_DUPLICIDADE,
"gerado_em": timezone.now(),
"logo_data_uri": _logo_relatorio_data_uri(),
}
html = render_to_string("conciliacao-fornecedores-relatorio.html", contexto)
return HttpResponse(html, content_type="text/html; charset=utf-8")
def _conciliacao_tipo_vinculo_manual(
conciliacao: ConciliacaoFornecedor, lancs: list[ConciliacaoFornecedorLancamento]
conciliacao: ConciliacaoFornecedor, lancs: list[ConciliacaoFornecedorLancamento], fechar: bool = False
) -> tuple[str, Decimal]:
"""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
da tolerância (ou pago a maior), fecha como vínculo com diferença
além da tolerância: pagamento parcial (parcelamento), o saldo continua em
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."""
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)
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
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
@ -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."})
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."})
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(
conta=conta, tipo=tipo, diferenca=diferenca, criado_por=request.user
)
@ -6455,6 +6580,14 @@ class ConciliacaoFornecedorVinculoViewSet(viewsets.GenericViewSet):
request.user,
)
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)
@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."})
ConciliacaoFornecedorLancamento.objects.filter(id__in=ids).update(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.save(update_fields=["tipo", "diferenca", "criado_por"])
_conciliacao_registra_historico(

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,490 +1,130 @@
{% load static %}
{% load contabil_extras %}
<!doctype html>
<html lang="pt-br">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Conciliação de Fornecedores · {{ conciliacao.codigo_empresa }} {{ conciliacao.nome_empresa }}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800&display=swap" rel="stylesheet">
<style>
/* Documento gerado: paleta fixa do escritório (mesma do relatório do
Relatório Contábil), sem tema claro/escuro do Portal. */
:root {
--roxo-escuro: #3d2178;
--roxo-mais-escuro: #281552;
--roxo-claro: #efe8ff;
--roxo-medio: #7a5bc0;
--dourado: #b4872a;
--dourado-claro: #f6ecd4;
--dourado-vivo: #d9ac48;
--texto: #211d2b;
--texto-muted: #6b6478;
--borda: #e6e1f0;
--bg-canvas: #f5f3fa;
--sucesso-bg: #dcf5e3;
--sucesso-texto: #1a7a3d;
--falha-bg: #fbe2e2;
--falha-texto: #b3261e;
--aviso-bg: #fdf0d8;
--aviso-texto: #9a6a0a;
--neutro-bg: #eeecf3;
--sombra: 0 1px 2px rgba(40, 21, 82, 0.04), 0 8px 24px -12px rgba(40, 21, 82, 0.18);
}
* { box-sizing: border-box; }
body {
margin: 0;
background:
radial-gradient(circle at 15% -10%, rgba(122, 91, 192, 0.14), transparent 42%),
radial-gradient(circle at 100% 0%, rgba(180, 135, 42, 0.10), transparent 38%),
var(--bg-canvas);
color: var(--texto);
font-family: "Inter", "Segoe UI", Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
h1, h2, h3, .cfr-card__valor, .cfr-parecer__nivel { font-family: "Manrope", "Inter", sans-serif; }
@keyframes cfrFadeSlideUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.cfr-page { max-width: 1140px; margin: 0 auto; padding: 0 24px 56px; }
/* ---- Cabeçalho ---- */
.cfr-header {
background: linear-gradient(135deg, var(--roxo-escuro) 0%, var(--roxo-mais-escuro) 100%);
color: #fff;
padding: 28px;
margin: 0 -24px 24px;
display: flex;
align-items: center;
gap: 22px;
flex-wrap: wrap;
border-bottom: 3px solid var(--dourado);
}
.cfr-header__brand { display: flex; align-items: center; gap: 18px; }
.cfr-header img { height: 52px; width: auto; }
.cfr-header h1 { margin: 0; font-size: 1.35rem; font-weight: 800; }
.cfr-header p { margin: 4px 0 0; color: var(--dourado-claro); font-size: 0.86rem; }
.cfr-print-btn {
margin-left: auto;
background: linear-gradient(135deg, var(--dourado-vivo), var(--dourado));
color: var(--roxo-mais-escuro);
border: none;
border-radius: 8px;
padding: 11px 20px;
font: inherit;
font-size: 0.86rem;
font-weight: 700;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
transition: transform 160ms ease;
}
.cfr-print-btn:hover { transform: translateY(-2px); }
.cfr-print-btn:active { transform: scale(0.97); }
/* ---- Aviso de conciliação em andamento ---- */
.cfr-aviso {
display: flex;
gap: 12px;
align-items: flex-start;
background: var(--aviso-bg);
color: var(--aviso-texto);
border: 1px solid #f0d9a8;
border-radius: 12px;
padding: 12px 16px;
margin-bottom: 20px;
font-weight: 600;
}
.cfr-aviso svg { flex-shrink: 0; margin-top: 2px; }
/* ---- Parecer ---- */
.cfr-parecer {
display: grid;
grid-template-columns: 240px 1fr;
gap: 20px;
background: #fff;
border: 1px solid var(--borda);
border-radius: 16px;
padding: 20px;
box-shadow: var(--sombra);
margin-bottom: 20px;
animation: cfrFadeSlideUp 360ms ease both;
}
.cfr-parecer__risco {
border-radius: 12px;
padding: 18px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
}
.cfr-parecer__risco--alto { background: var(--falha-bg); color: var(--falha-texto); }
.cfr-parecer__risco--moderado { background: var(--aviso-bg); color: var(--aviso-texto); }
.cfr-parecer__risco--baixo { background: var(--sucesso-bg); color: var(--sucesso-texto); }
.cfr-parecer__rotulo { font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; }
.cfr-parecer__nivel { font-size: 1.9rem; font-weight: 800; line-height: 1.1; }
.cfr-parecer__nota { font-size: 0.74rem; opacity: 0.85; }
.cfr-parecer__sintese { margin: 0 0 12px; padding-left: 18px; font-size: 0.92rem; }
.cfr-parecer__sintese li { margin-bottom: 2px; }
.cfr-parecer__meta { color: var(--texto-muted); font-size: 0.8rem; display: flex; flex-wrap: wrap; gap: 4px 18px; }
.cfr-parecer__meta strong { color: var(--texto); font-weight: 600; }
/* ---- Cards de números ---- */
.cfr-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 12px; margin-bottom: 28px; }
.cfr-card {
background: #fff;
border: 1px solid var(--borda);
border-radius: 14px;
padding: 14px 16px;
box-shadow: var(--sombra);
animation: cfrFadeSlideUp 360ms ease 60ms both;
}
.cfr-card__label { font-size: 0.7rem; color: var(--texto-muted); text-transform: uppercase; letter-spacing: 0.04em; font-weight: 600; }
.cfr-card__valor { font-size: 1.2rem; font-weight: 800; color: var(--roxo-escuro); margin-top: 4px; font-variant-numeric: tabular-nums; }
.cfr-card__nota { font-size: 0.72rem; color: var(--texto-muted); margin-top: 2px; }
.cfr-barra { height: 6px; background: var(--neutro-bg); border-radius: 999px; overflow: hidden; margin-top: 8px; }
.cfr-barra span { display: block; height: 100%; background: linear-gradient(90deg, var(--roxo-medio), var(--roxo-escuro)); border-radius: 999px; }
/* ---- Seções ---- */
.cfr-secao { margin-bottom: 32px; }
.cfr-secao > h2 {
display: flex;
align-items: center;
gap: 10px;
margin: 0 0 6px;
color: var(--roxo-escuro);
font-size: 1.1rem;
font-weight: 800;
}
.cfr-secao > h2 .cfr-num {
width: 26px;
height: 26px;
border-radius: 50%;
background: var(--roxo-escuro);
color: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
}
.cfr-secao__desc { margin: 0 0 14px; color: var(--texto-muted); font-size: 0.84rem; }
.cfr-subtitulo { margin: 20px 0 8px; font-size: 0.92rem; font-weight: 700; color: var(--texto); }
.cfr-subtitulo small { color: var(--texto-muted); font-weight: 500; }
.cfr-vazio {
background: #fff;
border: 1px dashed var(--borda);
border-radius: 12px;
padding: 14px 16px;
color: var(--texto-muted);
font-size: 0.84rem;
}
/* ---- Blocos de risco ---- */
.cfr-risco {
background: #fff;
border: 1px solid var(--borda);
border-left: 4px solid var(--borda);
border-radius: 12px;
margin-bottom: 10px;
box-shadow: var(--sombra);
overflow: hidden;
}
.cfr-risco--alta { border-left-color: var(--falha-texto); }
.cfr-risco--media { border-left-color: var(--dourado); }
.cfr-risco--baixa { border-left-color: var(--roxo-medio); }
.cfr-risco > summary {
list-style: none;
cursor: pointer;
padding: 12px 16px;
display: grid;
grid-template-columns: auto 1fr auto;
gap: 4px 12px;
align-items: center;
}
.cfr-risco > summary::-webkit-details-marker { display: none; }
.cfr-risco > summary:hover { background: #faf8fd; }
.cfr-risco__titulo { font-weight: 700; }
.cfr-risco__porque { grid-column: 2 / 4; color: var(--texto-muted); font-size: 0.8rem; }
.cfr-risco__resumo { text-align: right; font-size: 0.8rem; color: var(--texto-muted); white-space: nowrap; }
.cfr-risco__resumo strong { color: var(--texto); font-variant-numeric: tabular-nums; }
.cfr-risco__corpo { border-top: 1px solid var(--borda); padding: 12px 16px 16px; }
.cfr-risco__extra { font-size: 0.84rem; margin: 0 0 10px; }
.cfr-pill {
display: inline-flex;
align-items: center;
padding: 2px 9px;
border-radius: 999px;
font-size: 0.68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.03em;
white-space: nowrap;
}
.cfr-pill--alta, .cfr-pill--divergente, .cfr-pill--pendente { background: var(--falha-bg); color: var(--falha-texto); }
.cfr-pill--media, .cfr-pill--atencao { background: var(--aviso-bg); color: var(--aviso-texto); }
.cfr-pill--baixa { background: var(--roxo-claro); color: var(--roxo-escuro); }
.cfr-pill--conciliada, .cfr-pill--ok { background: var(--sucesso-bg); color: var(--sucesso-texto); }
.cfr-pill--neutro { background: var(--neutro-bg); color: var(--texto-muted); }
/* ---- Tabelas ---- */
.cfr-tabela-wrap { background: #fff; border: 1px solid var(--borda); border-radius: 12px; overflow-x: auto; }
table.cfr-tabela { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
table.cfr-tabela th, table.cfr-tabela td { padding: 7px 10px; border-bottom: 1px solid var(--borda); text-align: left; vertical-align: top; }
table.cfr-tabela tr:last-child td { border-bottom: none; }
table.cfr-tabela th {
background: var(--roxo-claro);
color: var(--roxo-escuro);
font-size: 0.66rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.03em;
white-space: nowrap;
}
table.cfr-tabela td.cfr-valor, table.cfr-tabela th.cfr-valor { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
table.cfr-tabela td.cfr-conta { white-space: nowrap; color: var(--texto-muted); }
table.cfr-tabela td.cfr-centro, table.cfr-tabela th.cfr-centro { text-align: center; }
.cfr-justificativa { font-size: 0.78rem; }
.cfr-justificativa em { color: var(--texto-muted); }
.cfr-sem-just { color: var(--falha-texto); font-weight: 600; font-size: 0.76rem; }
.cfr-lista-alertas { margin: 0; padding-left: 16px; }
.cfr-muted { color: var(--texto-muted); }
/* ---- Concentração ---- */
.cfr-conc-barra { display: flex; align-items: center; gap: 8px; min-width: 160px; }
.cfr-conc-barra .cfr-barra { flex: 1; margin: 0; }
.cfr-conc-barra .cfr-barra span { background: linear-gradient(90deg, var(--dourado-vivo), var(--dourado)); }
/* ---- Anexo ---- */
.cfr-anexo > summary { cursor: pointer; font-weight: 700; color: var(--roxo-escuro); margin-bottom: 10px; }
.cfr-footer { color: var(--texto-muted); font-size: 0.74rem; text-align: center; margin-top: 28px; }
.cfr-so-impressao { display: none; }
@media (max-width: 720px) {
.cfr-parecer { grid-template-columns: 1fr; }
.cfr-header { padding: 22px 20px; }
.cfr-risco > summary { grid-template-columns: auto 1fr; }
.cfr-risco__resumo { grid-column: 1 / 3; text-align: left; }
.cfr-risco__porque { grid-column: 1 / 3; }
}
/* Opção de impressão (só na tela): incluir ou não o anexo. */
.cfr-print-opcoes { display: flex; align-items: center; gap: 14px; margin-left: auto; }
.cfr-print-opcoes .cfr-print-btn { margin-left: 0; }
.cfr-print-check { display: inline-flex; align-items: center; gap: 6px; font-size: 0.8rem; color: var(--dourado-claro); cursor: pointer; }
.cfr-print-check input { accent-color: var(--dourado-vivo); }
/* Impressão compacta: fundo branco, sem sombra nem gradiente, fonte e
espaçamento menores. Blocos grandes podem quebrar entre páginas (um
`break-inside: avoid` no bloco inteiro empurrava o bloco para a página
seguinte e deixava a anterior quase em branco); o que não quebra é a
linha de tabela, o cabeçalho de cada bloco (fica junto do conteúdo) e
os cards. O cabeçalho da tabela se repete em cada página. */
@page { size: A4; margin: 12mm 10mm; }
@media print {
* { animation: none !important; transition: none !important; box-shadow: none !important; }
html, body { background: #fff !important; font-size: 10px; line-height: 1.35; }
/* Fonte do sistema na impressão: as fontes da web embutidas no PDF
eram a maior parte do peso do arquivo. */
body, h1, h2, h3, .cfr-card__valor, .cfr-parecer__nivel { font-family: Arial, "Segoe UI", sans-serif; }
table.cfr-tabela th { white-space: normal; }
.cfr-nowrap { white-space: nowrap; }
.cfr-page { max-width: none; padding: 0; }
.cfr-print-opcoes { display: none; }
.cfr-header {
margin: 0 0 10px;
padding: 10px 14px;
background: var(--roxo-escuro);
border-bottom-width: 2px;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.cfr-header img { height: 32px; }
.cfr-header h1 { font-size: 1.15rem; }
.cfr-header p { font-size: 0.85rem; }
.cfr-aviso { padding: 6px 10px; margin-bottom: 10px; border-radius: 6px; }
.cfr-parecer, .cfr-parecer__risco, .cfr-pill, table.cfr-tabela th, .cfr-barra span, .cfr-aviso {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.cfr-parecer { grid-template-columns: 150px 1fr; gap: 12px; padding: 10px; margin-bottom: 10px; border-radius: 8px; break-inside: avoid; }
.cfr-parecer__risco { padding: 10px; border-radius: 6px; }
.cfr-parecer__nivel { font-size: 1.4rem; }
.cfr-parecer__sintese { margin-bottom: 6px; font-size: 1rem; }
.cfr-cards { grid-template-columns: repeat(5, 1fr); gap: 6px; margin-bottom: 14px; }
.cfr-card { padding: 7px 9px; border-radius: 6px; break-inside: avoid; }
.cfr-card__valor { font-size: 1.1rem; }
.cfr-secao { margin-bottom: 14px; }
.cfr-secao > h2 { font-size: 1.25rem; margin-bottom: 2px; break-after: avoid; }
.cfr-secao > h2 .cfr-num { width: 18px; height: 18px; font-size: 0.8rem; }
.cfr-secao__desc { margin-bottom: 8px; }
.cfr-subtitulo { margin: 10px 0 4px; font-size: 1.05rem; break-after: avoid; }
.cfr-vazio { padding: 6px 10px; border-radius: 6px; }
.cfr-risco { border-radius: 6px; margin-bottom: 8px; overflow: visible; }
.cfr-risco > summary { padding: 6px 10px; break-inside: avoid; break-after: avoid; }
.cfr-risco__corpo { padding: 6px 10px 8px; }
.cfr-tabela-wrap { overflow: visible; border-radius: 6px; }
table.cfr-tabela { font-size: 0.95rem; }
table.cfr-tabela th, table.cfr-tabela td { padding: 3px 6px; }
table.cfr-tabela th { font-size: 0.8rem; }
table.cfr-tabela thead { display: table-header-group; }
table.cfr-tabela tr { break-inside: avoid; }
.cfr-anexo > summary { break-after: avoid; }
body:not(.cfr-imprime-anexo) .cfr-anexo-secao { display: none; }
.cfr-so-tela { display: none; }
.cfr-so-impressao { display: block; }
.cfr-footer { margin-top: 12px; }
}
</style>
</head>
<body>
<div class="cfr-page">
<header class="cfr-header">
<div class="cfr-header__brand">
{# cópia reduzida de logo-branco.png (6250 px de largura pesava mais que o PDF inteiro) #}
<img src="{% if logo_data_uri %}{{ logo_data_uri }}{% else %}{% static 'img/logo-branco-relatorio.png' %}{% endif %}" alt="De Paula Contadores">
<div>
<h1>Conciliação de Fornecedores</h1>
<p>
{% comment %}
Relatório de análise da Conciliação de Fornecedores: trecho de HTML
inserido na aba "Relatório" do detalhe (não é mais página própria; rodada
72 do CHANGELOG). Uso interno do contador, nada vai ao cliente: visual do
Portal (tokens de tema, classes `.conc-rel-*` em conciliacao-fornecedores.css),
sem logo do escritório. A impressão clona este trecho para fora do layout
(`imprimirRelatorio()` no JS) e usa uma paleta clara fixa.
{% endcomment %}
<div class="conc-rel">
<div class="conc-rel-so-impressao conc-rel-cabecalho-impressao">
<strong>Conciliação de Fornecedores · Relatório de análise</strong>
<span>
{{ conciliacao.codigo_empresa }} · {{ conciliacao.nome_empresa|default:"Empresa não encontrada no Questor" }}
· Razão de {% if conciliacao.periodo_inicio %}{{ conciliacao.periodo_inicio|date:"d/m/Y" }} a {{ conciliacao.periodo_fim|date:"d/m/Y" }}{% else %}período sem lançamentos{% endif %}
· Data-base {{ conciliacao.data_base|date:"d/m/Y" }}
</p>
· Data-base {{ conciliacao.data_base|date:"d/m/Y" }} · Gerado em {{ gerado_em|date:"d/m/Y H:i" }}
</span>
</div>
</div>
<div class="cfr-print-opcoes">
<label class="cfr-print-check">
<input type="checkbox" id="cfr-imprime-anexo">
Incluir anexo na impressão
</label>
<button type="button" class="cfr-print-btn" onclick="window.print()">
<svg width="16" height="16" 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>
</header>
{% if not relatorio.concluida %}
<div class="cfr-aviso" role="note">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
<span>Conciliação em andamento: {{ relatorio.contas_nao_validadas|length }} de {{ relatorio.total_contas }} contas ainda sem validação. Este relatório mostra a situação no momento em que foi gerado.</span>
<div class="conc-rel-aviso" role="note">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
<span>Conciliação em andamento: {{ relatorio.contas_nao_validadas|length }} de {{ relatorio.total_contas }} contas ainda sem validação. O relatório mostra a situação no momento em que foi gerado.</span>
</div>
{% endif %}
<section class="cfr-parecer" aria-label="Parecer geral">
<div class="cfr-parecer__risco cfr-parecer__risco--{{ relatorio.risco_geral }}">
<span class="cfr-parecer__rotulo">Risco geral</span>
<span class="cfr-parecer__nivel">{{ relatorio.risco_geral_rotulo }}</span>
<span class="cfr-parecer__nota">Considera só os pontos ainda sem justificativa.</span>
<section class="conc-rel-parecer" aria-label="Parecer geral">
<div class="conc-rel-parecer__risco conc-rel-parecer__risco--{{ relatorio.risco_geral }}">
<span class="conc-rel-parecer__rotulo">Risco geral</span>
<span class="conc-rel-parecer__nivel">{{ relatorio.risco_geral_rotulo }}</span>
<span class="conc-rel-parecer__nota">Considera só os pontos ainda sem justificativa.</span>
</div>
<div>
<ul class="cfr-parecer__sintese">
<ul class="conc-rel-parecer__sintese">
{% for frase in relatorio.sintese %}<li>{{ frase }}.</li>{% endfor %}
</ul>
<div class="cfr-parecer__meta">
<span>Processado por <strong>{{ conciliacao.criado_por.nome|default:"—" }}</strong> em {{ conciliacao.criado_em|date:"d/m/Y H:i" }}</span>
{% with ultima=conciliacao.atualizacoes.first %}{% if ultima %}
<span>Razão atualizado por <strong>{{ ultima.usuario.nome|default:"—" }}</strong> em {{ ultima.criado_em|date:"d/m/Y H:i" }}</span>
{% endif %}{% endwith %}
<span>Arquivo <strong>{{ conciliacao.nome_arquivo|default:"—" }}</strong></span>
<div class="conc-rel-parecer__meta">
<span>Participaram <strong>{{ relatorio.responsaveis|join:", "|default:"—" }}</strong></span>
</div>
</div>
</section>
<div class="cfr-cards">
<div class="cfr-card">
<div class="cfr-card__label">Saldo de fornecedores</div>
<div class="cfr-card__valor">{{ relatorio.saldo_total|moeda }}</div>
<div class="cfr-card__nota">{{ relatorio.total_contas }} contas no razão</div>
<div class="conc-rel-cards">
<div class="conc-rel-card">
<div class="conc-rel-card__label">Saldo de fornecedores</div>
<div class="conc-rel-card__valor">{{ relatorio.saldo_total|moeda }}</div>
<div class="conc-rel-card__nota">{{ relatorio.total_contas }} contas no razão</div>
</div>
<div class="cfr-card">
<div class="cfr-card__label">Títulos em aberto</div>
<div class="cfr-card__valor">{{ relatorio.total_em_aberto|moeda }}</div>
{% if relatorio.total_nao_vinculado %}<div class="cfr-card__nota">{{ relatorio.total_nao_vinculado|moeda }} em pagamentos a vincular</div>{% endif %}
<div class="conc-rel-card">
<div class="conc-rel-card__label">Títulos em aberto</div>
<div class="conc-rel-card__valor">{{ relatorio.total_em_aberto|moeda }}</div>
{% if relatorio.total_nao_vinculado %}<div class="conc-rel-card__nota">{{ relatorio.total_nao_vinculado|moeda }} em pagamentos a vincular</div>{% endif %}
</div>
<div class="cfr-card">
<div class="cfr-card__label">Há mais de {{ meses_vencido }} meses</div>
<div class="cfr-card__valor">{{ relatorio.total_vencido|moeda }}</div>
<div class="cfr-card__nota">{{ relatorio.vencidos_revisados|length }} de {{ relatorio.qtd_fornecedores_vencidos }} fornecedores revisados</div>
<div class="conc-rel-card">
<div class="conc-rel-card__label">Há mais de {{ meses_vencido }} meses</div>
<div class="conc-rel-card__valor">{{ relatorio.total_vencido|moeda }}</div>
<div class="conc-rel-card__nota">{{ relatorio.vencidos_revisados|length }} de {{ relatorio.qtd_fornecedores_vencidos }} fornecedores revisados</div>
</div>
<div class="cfr-card">
<div class="cfr-card__label">Pagamentos sem título</div>
<div class="cfr-card__valor">{{ relatorio.total_excedente|moeda }}</div>
<div class="cfr-card__nota">além de todos os títulos em aberto</div>
<div class="conc-rel-card">
<div class="conc-rel-card__label">Pagamentos sem título</div>
<div class="conc-rel-card__valor">{{ relatorio.total_excedente|moeda }}</div>
<div class="conc-rel-card__nota">além de todos os títulos em aberto</div>
</div>
<div class="cfr-card">
<div class="cfr-card__label">Validação</div>
<div class="cfr-card__valor">{{ relatorio.contas_validadas }} de {{ relatorio.total_contas }}</div>
<div class="cfr-barra" aria-hidden="true"><span style="width: {{ relatorio.percentual_validado }}%"></span></div>
<div class="cfr-card__nota">{{ relatorio.por_status.divergente }} divergentes · {{ relatorio.por_status.atencao }} em atenção · {{ relatorio.por_status.conciliada }} conciliadas</div>
<div class="conc-rel-card">
<div class="conc-rel-card__label">Validação</div>
<div class="conc-rel-card__valor">{{ relatorio.contas_validadas }} de {{ relatorio.total_contas }}</div>
<div class="conc-rel-barra" aria-hidden="true"><span style="width: {{ relatorio.percentual_validado }}%"></span></div>
<div class="conc-rel-card__nota">{{ relatorio.por_status.divergente }} críticas · {{ relatorio.por_status.atencao }} a validar · {{ relatorio.por_status.parcial }} parciais · {{ relatorio.por_status.conciliada }} conciliadas</div>
</div>
</div>
<!-- 1. Situações de risco -->
<section class="cfr-secao">
<h2><span class="cfr-num">1</span> Situações de risco</h2>
<p class="cfr-secao__desc">Cada ponto traz o valor envolvido e por que merece atenção. Quando a conta foi validada com observação, a justificativa aparece ao lado.</p>
<section class="conc-rel-secao">
<h2><span class="conc-rel-num">1</span> Situações de risco</h2>
<p class="conc-rel-secao__desc">Cada ponto traz o valor envolvido e por que merece atenção. Quando a conta foi validada com observação, a justificativa aparece ao lado.</p>
{% for risco in relatorio.riscos %}
<details class="cfr-risco cfr-risco--{{ risco.severidade }}"{% if risco.severidade == "alta" and risco.qtd_sem_justificativa %} open{% endif %}>
<details class="conc-rel-risco conc-rel-risco--{{ risco.severidade }}"{% if risco.severidade == "alta" and risco.qtd_sem_justificativa %} open{% endif %}>
<summary>
<span class="cfr-pill cfr-pill--{{ risco.severidade }}">{{ risco.severidade_rotulo }}</span>
<span class="cfr-risco__titulo">{{ risco.titulo }}</span>
<span class="cfr-risco__resumo">
<span class="conc-rel-pill conc-rel-pill--{{ risco.severidade }}">{{ risco.severidade_rotulo }}</span>
<span class="conc-rel-risco__titulo">{{ risco.titulo }}</span>
<span class="conc-rel-risco__resumo">
{{ risco.itens|length }} {% if risco.itens|length == 1 %}item{% else %}itens{% endif %}
{% if risco.valor_total %}· <strong>{{ risco.valor_total|moeda }}</strong>{% endif %}
{% if not risco.informativo %}· {% if risco.qtd_sem_justificativa %}{{ risco.qtd_sem_justificativa }} sem justificativa{% else %}todos justificados{% endif %}{% endif %}
</span>
<span class="cfr-risco__porque">{{ risco.porque }}{% if risco.informativo %} Informativo: não entra no risco geral.{% endif %}</span>
<span class="conc-rel-risco__porque">{{ risco.porque }}{% if risco.informativo %} Informativo: não entra no risco geral.{% endif %}</span>
</summary>
<div class="cfr-risco__corpo">
<div class="conc-rel-risco__corpo">
{% if risco.chave == "juros_descontos" %}
<p class="cfr-risco__extra">Total pago a maior (possível juros/multa): <strong>{{ relatorio.total_juros|moeda }}</strong> · Total pago a menor (possível desconto): <strong>{{ relatorio.total_descontos|moeda }}</strong></p>
<p class="conc-rel-risco__extra">Total pago a maior (possível juros/multa): <strong>{{ relatorio.total_juros|moeda }}</strong> · Total pago a menor (possível desconto): <strong>{{ relatorio.total_descontos|moeda }}</strong></p>
{% endif %}
{% if risco.chave == "vencidos" and relatorio.faixas_vencidos %}
<div class="cfr-tabela-wrap" style="margin-bottom: 12px;">
<table class="cfr-tabela">
<thead><tr><th>Faixa</th><th class="cfr-valor">Títulos</th><th class="cfr-valor">Fornecedores</th><th class="cfr-valor">Sem revisão</th><th class="cfr-valor">Valor</th></tr></thead>
<div class="conc-rel-tabela-wrap conc-rel-tabela-wrap--espaco">
<table class="conc-rel-tabela">
<thead><tr><th>Faixa</th><th class="conc-rel-valor">Títulos</th><th class="conc-rel-valor">Fornecedores</th><th class="conc-rel-valor">Sem revisão</th><th class="conc-rel-valor">Valor</th></tr></thead>
<tbody>
{% for faixa in relatorio.faixas_vencidos %}
<tr>
<td>{{ faixa.rotulo }}</td>
<td class="cfr-valor">{{ faixa.qtd_titulos }}</td>
<td class="cfr-valor">{{ faixa.qtd_fornecedores }}</td>
<td class="cfr-valor">{% if faixa.qtd_sem_revisao %}<span class="cfr-sem-just">{{ faixa.qtd_sem_revisao }}</span>{% else %}0{% endif %}</td>
<td class="cfr-valor">{{ faixa.valor|moeda }}</td>
<td class="conc-rel-valor">{{ faixa.qtd_titulos }}</td>
<td class="conc-rel-valor">{{ faixa.qtd_fornecedores }}</td>
<td class="conc-rel-valor">{% if faixa.qtd_sem_revisao %}<span class="conc-rel-sem-just">{{ faixa.qtd_sem_revisao }}</span>{% else %}0{% endif %}</td>
<td class="conc-rel-valor">{{ faixa.valor|moeda }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
<div class="cfr-tabela-wrap">
<table class="cfr-tabela">
<div class="conc-rel-tabela-wrap">
<table class="conc-rel-tabela">
<thead>
<tr>
<th>Conta</th><th>Fornecedor</th><th>Situação</th><th class="cfr-valor">Valor</th>
<th>Conta</th><th>Fornecedor</th><th>Situação</th><th class="conc-rel-valor">Valor</th>
{% if not risco.informativo %}<th>Justificativa</th>{% endif %}
</tr>
</thead>
<tbody>
{% for item in risco.itens %}
<tr>
<td class="cfr-conta">{{ item.conta_numero }}</td>
<td class="conc-rel-conta">{{ item.conta_numero }}</td>
<td>{{ item.fornecedor }}</td>
<td>{{ item.detalhe }}</td>
<td class="cfr-valor">{% if item.valor is not None %}{{ item.valor|moeda }}{% else %}—{% endif %}</td>
<td class="conc-rel-valor">{% if item.valor is not None %}{{ item.valor|moeda }}{% else %}—{% endif %}</td>
{% if not risco.informativo %}
<td class="cfr-justificativa">{% if item.justificado %}{{ item.justificativa|linebreaksbr }}{% else %}<span class="cfr-sem-just">Sem justificativa</span>{% endif %}</td>
<td class="conc-rel-justificativa">{% if item.justificado %}{{ item.justificativa|linebreaksbr }}{% else %}<span class="conc-rel-sem-just">Sem justificativa</span>{% endif %}</td>
{% endif %}
</tr>
{% endfor %}
@ -494,21 +134,21 @@
</div>
</details>
{% empty %}
<p class="cfr-vazio">Nenhuma situação de risco encontrada nesta conciliação.</p>
<p class="conc-rel-vazio">Nenhuma situação de risco encontrada nesta conciliação.</p>
{% endfor %}
{% if relatorio.concentracao %}
<h3 class="cfr-subtitulo">Concentração do saldo em aberto <small>· os {{ relatorio.concentracao|length }} maiores fornecedores, onde um erro pesa mais</small></h3>
<div class="cfr-tabela-wrap">
<table class="cfr-tabela">
<thead><tr><th>Conta</th><th>Fornecedor</th><th class="cfr-valor">Em aberto</th><th>Participação</th></tr></thead>
<h3 class="conc-rel-subtitulo">Concentração do saldo em aberto <small>· os {{ relatorio.concentracao|length }} maiores fornecedores, onde um erro pesa mais</small></h3>
<div class="conc-rel-tabela-wrap">
<table class="conc-rel-tabela">
<thead><tr><th>Conta</th><th>Fornecedor</th><th class="conc-rel-valor">Em aberto</th><th>Participação</th></tr></thead>
<tbody>
{% for item in relatorio.concentracao %}
<tr>
<td class="cfr-conta">{{ item.conta_numero }}</td>
<td class="conc-rel-conta">{{ item.conta_numero }}</td>
<td>{{ item.fornecedor }}</td>
<td class="cfr-valor">{{ item.valor|moeda }}</td>
<td><div class="cfr-conc-barra"><div class="cfr-barra"><span style="width: {% widthratio item.valor relatorio.total_em_aberto 100 %}%"></span></div><span>{{ item.participacao|percentual }}</span></div></td>
<td class="conc-rel-valor">{{ item.valor|moeda }}</td>
<td><div class="conc-rel-participacao"><div class="conc-rel-barra conc-rel-barra--ouro"><span style="width: {% widthratio item.valor relatorio.total_em_aberto 100 %}%"></span></div><span>{{ item.participacao|percentual }}</span></div></td>
</tr>
{% endfor %}
</tbody>
@ -518,70 +158,70 @@
</section>
<!-- 2. Decisões tomadas -->
<section class="cfr-secao">
<h2><span class="cfr-num">2</span> Decisões tomadas</h2>
<p class="cfr-secao__desc">O que foi decidido na conciliação, por quem e com qual justificativa.</p>
<section class="conc-rel-secao">
<h2><span class="conc-rel-num">2</span> Decisões tomadas</h2>
<p class="conc-rel-secao__desc">O que foi decidido na conciliação, por quem e com qual justificativa.</p>
<h3 class="cfr-subtitulo">Contas validadas com pendência <small>· conta em Atenção ou Divergente validada por uma pessoa</small></h3>
<h3 class="conc-rel-subtitulo">Contas validadas com pendência <small>· conta em situação A validar ou Crítico validada por uma pessoa</small></h3>
{% if relatorio.validacoes_com_pendencia %}
<div class="cfr-tabela-wrap">
<table class="cfr-tabela">
<div class="conc-rel-tabela-wrap">
<table class="conc-rel-tabela">
<thead><tr><th>Conta</th><th>Fornecedor</th><th>Situação</th><th>Pendências</th><th>Validada por</th><th>Justificativa</th></tr></thead>
<tbody>
{% for v in relatorio.validacoes_com_pendencia %}
<tr>
<td class="cfr-conta">{{ v.conta_numero }}</td>
<td class="conc-rel-conta">{{ v.conta_numero }}</td>
<td>{{ v.fornecedor }}</td>
<td><span class="cfr-pill cfr-pill--{{ v.status }}">{{ v.status_rotulo }}</span></td>
<td><ul class="cfr-lista-alertas">{% for a in v.alertas %}<li>{{ a }}</li>{% endfor %}</ul></td>
<td>{{ v.validada_por }}<br><span class="cfr-muted cfr-nowrap">{{ v.validada_em|date:"d/m/Y H:i" }}</span></td>
<td class="cfr-justificativa">{% if v.observacao %}{{ v.observacao|linebreaksbr }}{% else %}<span class="cfr-sem-just">Sem justificativa</span>{% endif %}</td>
<td><span class="conc-rel-pill conc-rel-pill--{{ v.status }}">{{ v.status_rotulo }}</span></td>
<td><ul class="conc-rel-lista-alertas">{% for a in v.alertas %}<li>{{ a }}</li>{% endfor %}</ul></td>
<td>{{ v.validada_por }}<br><span class="conc-rel-muted conc-rel-nowrap">{{ v.validada_em|date:"d/m/Y H:i" }}</span></td>
<td class="conc-rel-justificativa">{% if v.observacao %}{{ v.observacao|linebreaksbr }}{% else %}<span class="conc-rel-sem-just">Sem justificativa</span>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="cfr-vazio">Nenhuma conta com pendência foi validada.</p>
<p class="conc-rel-vazio">Nenhuma conta com pendência foi validada.</p>
{% endif %}
<h3 class="cfr-subtitulo">Ajustes manuais nos vínculos <small>· pelo histórico de vinculações de cada conta</small></h3>
<h3 class="conc-rel-subtitulo">Ajustes manuais nos vínculos <small>· pelo histórico de vinculações de cada conta</small></h3>
{% if relatorio.ajustes %}
<div class="cfr-tabela-wrap">
<table class="cfr-tabela">
<thead><tr><th>Conta</th><th style="width: 34%">Fornecedor</th><th class="cfr-centro">Criados</th><th class="cfr-centro">Acrésc.</th><th class="cfr-centro">Autom. desfeitos</th><th class="cfr-centro">Manuais desfeitos</th><th>Responsáveis</th><th>Última ação</th></tr></thead>
<div class="conc-rel-tabela-wrap">
<table class="conc-rel-tabela">
<thead><tr><th>Conta</th><th class="conc-rel-col-fornecedor">Fornecedor</th><th class="conc-rel-centro">Criados</th><th class="conc-rel-centro">Acrésc.</th><th class="conc-rel-centro">Autom. desfeitos</th><th class="conc-rel-centro">Manuais desfeitos</th><th>Responsáveis</th><th>Última ação</th></tr></thead>
<tbody>
{% for a in relatorio.ajustes %}
<tr>
<td class="cfr-conta">{{ a.conta_numero }}</td>
<td class="conc-rel-conta">{{ a.conta_numero }}</td>
<td>{{ a.fornecedor }}</td>
<td class="cfr-centro">{{ a.vinculou }}</td>
<td class="cfr-centro">{{ a.acrescentou }}</td>
<td class="cfr-centro">{% if a.desfez_automatico %}<strong>{{ a.desfez_automatico }}</strong>{% else %}0{% endif %}</td>
<td class="cfr-centro">{{ a.desfez_manual }}</td>
<td class="conc-rel-centro">{{ a.vinculou }}</td>
<td class="conc-rel-centro">{{ a.acrescentou }}</td>
<td class="conc-rel-centro">{% if a.desfez_automatico %}<strong>{{ a.desfez_automatico }}</strong>{% else %}0{% endif %}</td>
<td class="conc-rel-centro">{{ a.desfez_manual }}</td>
<td>{{ a.usuarios|join:", "|default:"—" }}</td>
<td class="cfr-muted cfr-nowrap">{{ a.ultima_acao|date:"d/m/Y H:i" }}</td>
<td class="conc-rel-muted conc-rel-nowrap">{{ a.ultima_acao|date:"d/m/Y H:i" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="cfr-vazio">Nenhum ajuste manual: todos os vínculos são os automáticos da importação.</p>
<p class="conc-rel-vazio">Nenhum ajuste manual: todos os vínculos são os automáticos da importação.</p>
{% endif %}
{% if relatorio.saldos_parciais %}
<h3 class="cfr-subtitulo">Pagamentos parciais com saldo a pagar <small>· vínculos manuais em que o título não foi quitado por inteiro</small></h3>
<div class="cfr-tabela-wrap">
<table class="cfr-tabela">
<thead><tr><th>Conta</th><th>Fornecedor</th><th>Título de</th><th class="cfr-valor">Saldo a pagar</th><th>Vinculado por</th></tr></thead>
<h3 class="conc-rel-subtitulo">Pagamentos parciais com saldo a pagar <small>· vínculos manuais em que o título não foi quitado por inteiro</small></h3>
<div class="conc-rel-tabela-wrap">
<table class="conc-rel-tabela">
<thead><tr><th>Conta</th><th>Fornecedor</th><th>Título de</th><th class="conc-rel-valor">Saldo a pagar</th><th>Vinculado por</th></tr></thead>
<tbody>
{% for s in relatorio.saldos_parciais %}
<tr>
<td class="cfr-conta">{{ s.conta_numero }}</td>
<td class="conc-rel-conta">{{ s.conta_numero }}</td>
<td>{{ s.fornecedor }}</td>
<td>{{ s.data_titulo|date:"d/m/Y"|default:"—" }}</td>
<td class="cfr-valor">{{ s.valor|moeda }}</td>
<td class="conc-rel-valor">{{ s.valor|moeda }}</td>
<td>{{ s.criado_por|default:"—" }}</td>
</tr>
{% endfor %}
@ -590,100 +230,100 @@
</div>
{% endif %}
<h3 class="cfr-subtitulo">Títulos antigos revisados <small>· aba "Em aberto há mais de {{ meses_vencido }} meses"</small></h3>
<h3 class="conc-rel-subtitulo">Títulos antigos revisados <small>· aba "Em aberto há mais de {{ meses_vencido }} meses"</small></h3>
{% if relatorio.vencidos_revisados %}
<div class="cfr-tabela-wrap">
<table class="cfr-tabela">
<thead><tr><th>Conta</th><th>Fornecedor</th><th class="cfr-centro">Títulos</th><th>Mais antigo</th><th class="cfr-valor">Valor</th><th>Revisado por</th><th>Observação</th></tr></thead>
<div class="conc-rel-tabela-wrap">
<table class="conc-rel-tabela">
<thead><tr><th>Conta</th><th>Fornecedor</th><th class="conc-rel-centro">Títulos</th><th>Mais antigo</th><th class="conc-rel-valor">Valor</th><th>Revisado por</th><th>Observação</th></tr></thead>
<tbody>
{% for v in relatorio.vencidos_revisados %}
<tr>
<td class="cfr-conta">{{ v.conta_numero }}</td>
<td class="conc-rel-conta">{{ v.conta_numero }}</td>
<td>{{ v.fornecedor }}</td>
<td class="cfr-centro">{{ v.qtd_titulos }}</td>
<td class="conc-rel-centro">{{ v.qtd_titulos }}</td>
<td>{{ v.mais_antigo|date:"d/m/Y" }}</td>
<td class="cfr-valor">{{ v.valor|moeda }}</td>
<td>{{ v.revisado_por|default:"—" }}<br><span class="cfr-muted cfr-nowrap">{{ v.revisado_em|date:"d/m/Y H:i" }}</span></td>
<td class="cfr-justificativa">{% if v.observacao %}{{ v.observacao|linebreaksbr }}{% else %}<em>Sem observação</em>{% endif %}</td>
<td class="conc-rel-valor">{{ v.valor|moeda }}</td>
<td>{{ v.revisado_por|default:"—" }}<br><span class="conc-rel-muted conc-rel-nowrap">{{ v.revisado_em|date:"d/m/Y H:i" }}</span></td>
<td class="conc-rel-justificativa">{% if v.observacao %}{{ v.observacao|linebreaksbr }}{% else %}<em>Sem observação</em>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="cfr-vazio">Nenhum fornecedor com títulos antigos foi revisado ainda.</p>
<p class="conc-rel-vazio">Nenhum fornecedor com títulos antigos foi revisado ainda.</p>
{% endif %}
</section>
<!-- 3. O que ainda falta decidir -->
<section class="cfr-secao">
<h2><span class="cfr-num">3</span> O que ainda falta decidir</h2>
<p class="cfr-secao__desc">Contas e fornecedores sem nenhuma decisão registrada.</p>
<section class="conc-rel-secao">
<h2><span class="conc-rel-num">3</span> O que ainda falta decidir</h2>
<p class="conc-rel-secao__desc">Contas e fornecedores sem nenhuma decisão registrada.</p>
<h3 class="cfr-subtitulo">Contas sem validação <small>· {{ relatorio.contas_nao_validadas|length }}</small></h3>
<h3 class="conc-rel-subtitulo">Contas sem validação <small>· {{ relatorio.contas_nao_validadas|length }}</small></h3>
{% if relatorio.contas_nao_validadas %}
<div class="cfr-tabela-wrap">
<table class="cfr-tabela">
<thead><tr><th>Conta</th><th>Fornecedor</th><th>Situação</th><th>Motivo principal</th><th class="cfr-valor">Pagamentos pendentes</th></tr></thead>
<div class="conc-rel-tabela-wrap">
<table class="conc-rel-tabela">
<thead><tr><th>Conta</th><th>Fornecedor</th><th>Situação</th><th>Motivo principal</th><th class="conc-rel-valor">Pagamentos pendentes</th></tr></thead>
<tbody>
{% for c in relatorio.contas_nao_validadas %}
<tr>
<td class="cfr-conta">{{ c.conta_numero }}</td>
<td class="conc-rel-conta">{{ c.conta_numero }}</td>
<td>{{ c.fornecedor }}</td>
<td><span class="cfr-pill cfr-pill--{{ c.status }}">{{ c.status_rotulo }}</span></td>
<td><span class="conc-rel-pill conc-rel-pill--{{ c.status }}">{{ c.status_rotulo }}</span></td>
<td>{{ c.motivo }}</td>
<td class="cfr-valor">{% if c.valor %}{{ c.valor|moeda }}{% else %}—{% endif %}</td>
<td class="conc-rel-valor">{% if c.valor %}{{ c.valor|moeda }}{% else %}—{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="cfr-vazio">Todas as contas foram validadas.</p>
<p class="conc-rel-vazio">Todas as contas foram validadas.</p>
{% endif %}
<h3 class="cfr-subtitulo">Títulos antigos sem revisão <small>· {{ relatorio.vencidos_nao_revisados|length }} fornecedores</small></h3>
<h3 class="conc-rel-subtitulo">Títulos antigos sem revisão <small>· {{ relatorio.vencidos_nao_revisados|length }} fornecedores</small></h3>
{% if relatorio.vencidos_nao_revisados %}
<p class="cfr-vazio cfr-so-impressao">Listados no item 1, "Títulos em aberto há mais de {{ meses_vencido }} meses", como "Sem justificativa".</p>
<div class="cfr-tabela-wrap cfr-so-tela">
<table class="cfr-tabela">
<thead><tr><th>Conta</th><th>Fornecedor</th><th class="cfr-centro">Títulos</th><th>Mais antigo</th><th class="cfr-valor">Valor</th></tr></thead>
<p class="conc-rel-vazio conc-rel-so-impressao">Listados no item 1, "Títulos em aberto há mais de {{ meses_vencido }} meses", como "Sem justificativa".</p>
<div class="conc-rel-tabela-wrap conc-rel-so-tela">
<table class="conc-rel-tabela">
<thead><tr><th>Conta</th><th>Fornecedor</th><th class="conc-rel-centro">Títulos</th><th>Mais antigo</th><th class="conc-rel-valor">Valor</th></tr></thead>
<tbody>
{% for v in relatorio.vencidos_nao_revisados %}
<tr>
<td class="cfr-conta">{{ v.conta_numero }}</td>
<td class="conc-rel-conta">{{ v.conta_numero }}</td>
<td>{{ v.fornecedor }}</td>
<td class="cfr-centro">{{ v.qtd_titulos }}</td>
<td class="conc-rel-centro">{{ v.qtd_titulos }}</td>
<td>{{ v.mais_antigo|date:"d/m/Y" }}</td>
<td class="cfr-valor">{{ v.valor|moeda }}</td>
<td class="conc-rel-valor">{{ v.valor|moeda }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="cfr-vazio">Todos os fornecedores com títulos antigos foram revisados.</p>
<p class="conc-rel-vazio">Todos os fornecedores com títulos antigos foram revisados.</p>
{% endif %}
</section>
<!-- 4. Anexo (fora da impressão, salvo se marcado no cabeçalho) -->
<section class="cfr-secao cfr-anexo-secao">
<details class="cfr-anexo">
<!-- 4. Anexo (fora da impressão, salvo se marcado na barra da aba) -->
<section class="conc-rel-secao conc-rel-anexo-secao">
<details class="conc-rel-anexo">
<summary>Anexo: todos os fornecedores ({{ relatorio.anexo|length }})</summary>
<div class="cfr-tabela-wrap">
<table class="cfr-tabela">
<thead><tr><th>Conta</th><th>Fornecedor</th><th class="cfr-valor">Saldo inicial</th><th class="cfr-valor">Saldo final</th><th class="cfr-valor">Em aberto</th><th class="cfr-valor">Há mais de {{ meses_vencido }} meses</th><th>Situação</th><th>Validação</th></tr></thead>
<div class="conc-rel-tabela-wrap">
<table class="conc-rel-tabela">
<thead><tr><th>Conta</th><th>Fornecedor</th><th class="conc-rel-valor">Saldo inicial</th><th class="conc-rel-valor">Saldo final</th><th class="conc-rel-valor">Em aberto</th><th class="conc-rel-valor">Há mais de {{ meses_vencido }} meses</th><th>Situação</th><th>Validação</th></tr></thead>
<tbody>
{% for l in relatorio.anexo %}
<tr>
<td class="cfr-conta">{{ l.conta_numero }}</td>
<td class="conc-rel-conta">{{ l.conta_numero }}</td>
<td>{{ l.fornecedor }}</td>
<td class="cfr-valor">{{ l.saldo_inicial|moeda }}</td>
<td class="cfr-valor">{{ l.saldo_final|moeda }}</td>
<td class="cfr-valor">{{ l.total_em_aberto|moeda }}</td>
<td class="cfr-valor">{% if l.total_vencido %}{{ l.total_vencido|moeda }}{% else %}—{% endif %}</td>
<td><span class="cfr-pill cfr-pill--{{ l.status }}">{{ l.status_rotulo }}</span></td>
<td>{% if l.validacao %}{{ l.validacao }}{% else %}<span class="cfr-pill cfr-pill--neutro">Pendente</span>{% endif %}</td>
<td class="conc-rel-valor">{{ l.saldo_inicial|moeda }}</td>
<td class="conc-rel-valor">{{ l.saldo_final|moeda }}</td>
<td class="conc-rel-valor">{{ l.total_em_aberto|moeda }}</td>
<td class="conc-rel-valor">{% if l.total_vencido %}{{ l.total_vencido|moeda }}{% else %}—{% endif %}</td>
<td><span class="conc-rel-pill conc-rel-pill--{{ l.status }}">{{ l.status_rotulo }}</span></td>
<td>{% if l.validacao %}{{ l.validacao }}{% else %}<span class="conc-rel-pill conc-rel-pill--neutro">Pendente</span>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
@ -692,27 +332,8 @@
</details>
</section>
<p class="cfr-footer">
<p class="conc-rel-rodape">
Gerado em {{ gerado_em|date:"d/m/Y H:i" }} pelo Portal De Paula. Documento interno de análise do contador.
Os vínculos entre pagamentos e títulos são feitos por valor e data; possível duplicidade considera pagamentos de mesmo valor com até {{ janela_duplicidade }} dias entre eles.
</p>
</div>
<script>
document.getElementById("cfr-imprime-anexo").addEventListener("change", (e) => {
document.body.classList.toggle("cfr-imprime-anexo", e.target.checked);
});
// Impressão mostra todos os blocos abertos (o anexo só se marcado).
window.addEventListener("beforeprint", () => {
document.querySelectorAll("details").forEach((d) => {
d.dataset.cfrAberto = d.open ? "1" : "";
d.open = true;
});
});
window.addEventListener("afterprint", () => {
document.querySelectorAll("details").forEach((d) => {
d.open = d.dataset.cfrAberto === "1";
});
});
</script>
</body>
</html>
</div>

View File

@ -405,13 +405,11 @@
<thead>
<tr>
<th>Empresa</th>
<th>Data-base</th>
<th>Período</th>
<th class="conc-num">Fornecedores</th>
<th class="conc-num">Conciliados</th>
<th class="conc-num">Divergentes</th>
<th class="conc-num">Críticos</th>
<th class="conc-num">Em aberto</th>
<th>Criado por</th>
<th>Última atualização</th>
<th></th>
</tr>
</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>
Exportar XLSX
</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>
@ -458,18 +452,18 @@
<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>
<button type="button" class="pa-tab" data-conc-aba="relatorio">Relatório</button>
</div>
<div class="pa-tab-panel" data-conc-painel="conciliacao">
<div class="conc-toolbar conc-toolbar--fixa">
<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" 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="conciliada">Conciliados</button>
<button type="button" class="conc-chip" data-conc-status="validada">Validados</button>
<button type="button" class="conc-chip is-active" data-conc-grupo="pendentes">Pendentes</button>
<button type="button" class="conc-chip" data-conc-grupo="parcial">Parcial</button>
<button type="button" class="conc-chip" data-conc-grupo="saldo_aberto">Saldo em aberto</button>
<button type="button" class="conc-chip" data-conc-grupo="conciliados">Conciliados</button>
<button type="button" class="conc-chip" data-conc-grupo="validados">Validados</button>
</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">
<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>
@ -484,11 +478,11 @@
<th class="conc-expand-col"></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 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 residual <span class="conc-sort-icon">↕</span></th>
<th class="conc-num" data-sort="total_vencido">Em aberto &gt; 3 meses <span class="conc-sort-icon">↕</span></th>
<th class="conc-num" data-sort="saldo_final">Saldo final <span class="conc-sort-icon">↕</span></th>
<!-- Filtro por ajuste no próprio cabeçalho (menu `pidConcAbreMenuAjuste`) -->
<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 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>
</thead>
<tbody id="conc-contas-body"></tbody>
@ -529,6 +523,21 @@
</div>
<p class="conc-empty" id="conc-venc-vazia" hidden></p>
</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>
</div>
</main>
@ -607,11 +616,11 @@
<h2 class="modal-card__title">Atualizar razão</h2>
<p class="modal-card__subtitle" id="conc-atualizar-empresa"></p>
<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>
<li>O que ficou antes do início do arquivo novo passa a ser só o saldo anterior de cada fornecedor.</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>Conta que recebeu ou perdeu lançamentos volta a "a validar" (a observação fica).</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><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>Nos dois casos, conta que recebeu ou perdeu lançamentos volta a "a validar" (a observação fica).</li>
</ul>
<p class="conc-atualizar-info__atual" id="conc-atualizar-periodo"></p>
</div>