Commit inicial - upload de todos os arquivos da pasta
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-07-25
|
||||
@@ -0,0 +1,75 @@
|
||||
## Context
|
||||
|
||||
LerNotaFiscal persiste hoje uma data completa (`purchase_date TEXT NOT NULL`, ISO `YYYY-MM-DD`) em `fiscal_documents`, e `purchase_date TEXT` (nullable) em `detected_documents` (staging). O acesso a dados é `sqlite3` puro sem ORM/migrations (`app/database.py`): o schema vive numa string `SCHEMA` rodada via `executescript()` em toda abertura de conexão, e o único precedente de evolução de schema é aditivo (`ALTER TABLE ... ADD COLUMN`, guardado por `PRAGMA table_info`, usado para `categoria_id`). Nunca houve remoção/rebuild de coluna neste projeto.
|
||||
|
||||
O campo de data hoje passa por: extração IA (`app/ai_extraction.py`, prompt pedindo `"data_compra"` completo) ou fallback OCR/regex (`lernotafiscal/extraction.py`, `DATE_RE` dd/mm/aaaa); normalização central em `app/dates.py` (`resolve_purchase_date`, com fallback "1º dia do mês corrente" quando ilegível); persistência/CRUD (`create_fiscal`, `update_fiscal`, `confirm_batch`, `update_staged`); agregação (`monthly_totals` via `substr(purchase_date,1,7)`); filtro/ordenação por intervalo (`_fiscal_where`, `FISCAL_SORT_COLUMNS`); e exibição (`format_br_date`, coluna "Data" em `dashboard.html`/`documents_list.html`, `<input type="date">` em `staging.html`/`document_form.html`).
|
||||
|
||||
Esta mudança substitui esse conceito por competência (mês/ano), refletindo que o dia nunca foi de fato relevante para o negócio (o Dashboard já converte para mês na exibição, e o fallback já assume "mês corrente" quando a data é ilegível).
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Persistir `mes` (1-12) e `ano` (ex. 2026) como colunas nativas inteiras em `fiscal_documents` (obrigatórias) e `detected_documents` (opcionais, como `purchase_date` era), substituindo `purchase_date` por completo.
|
||||
- Exigir confirmação explícita de "Competência: Mês/Ano" via dois `<select>` (Mês: Jan-Dez; Ano: 2026-2030) na tela de revisão, antes de permitir confirmar o lote — mesmo em documentos onde a IA/OCR já sugeriu um valor.
|
||||
- Preservar a capacidade de filtrar/ordenar/agrupar por período no Dashboard e em `/documents`, agora em granularidade de mês em vez de dia.
|
||||
- Migrar dados existentes (backfill `mes`/`ano` a partir do `purchase_date` atual) sem perda de informação de competência.
|
||||
|
||||
**Non-Goals:**
|
||||
- Não se propõe manter o dia da compra em lugar nenhum (nem como campo oculto/auditoria) — é descartado deliberadamente.
|
||||
- Não se propõe suportar competências fora do intervalo 2026-2030 no seletor (ver Open Questions sobre extensibilidade do range de anos).
|
||||
- Não se propõe alterar a lógica de categorização (`categorization.py`) — confirmado que não usa data/competência.
|
||||
- Não se propõe migrar `lernotafiscal/db.py` (módulo legado, não usado pelo fluxo atual além da constante `PREVIEW_DIR`).
|
||||
|
||||
## Decisions
|
||||
|
||||
**1. Duas colunas inteiras (`mes INTEGER`, `ano INTEGER`), não uma única coluna `competencia TEXT` (`YYYY-MM`).**
|
||||
Racional: o pedido explícito é "criar campos: mes e ano", e os `<select>` da UI (mês por nome, ano por valor) mapeiam naturalmente para dois valores inteiros independentes, evitando parsing de string em toda leitura. Comparações/ordenação também ficam mais simples com inteiros (`ORDER BY ano, mes`) do que com `substr`/`LIKE` em texto. Alternativa considerada: coluna única `competencia TEXT` no formato `YYYY-MM` (mais parecida com o `substr(purchase_date,1,7)` já usado hoje) — rejeitada por divergir do pedido explícito e por exigir parsing de string em toda query de filtro/ordenação por ano isolado.
|
||||
|
||||
**2. IA e OCR continuam extraindo uma data completa do texto/imagem; o descarte do dia acontece na camada de normalização, não no prompt/regex.**
|
||||
Racional: documentos fiscais reais quase sempre exibem uma data completa (dd/mm/aaaa) — pedir ao modelo de IA para "raciocinar" apenas sobre mês/ano a partir do texto bruto é uma tarefa estritamente mais difícil e menos confiável do que extrair a data completa (already validado hoje por `_is_plausible_purchase_date`) e simplesmente descartar o dia depois. O mesmo vale para o regex `DATE_RE` (dd/mm/aaaa), que já funciona bem. Portanto: `app/ai_extraction.py` e `lernotafiscal/extraction.py` mantêm a extração de data completa como está; a função de normalização (`app/dates.py`) é que passa a devolver `(mes, ano)` em vez de uma string de data, descartando o componente de dia. Alternativa considerada: mudar o prompt da IA para pedir diretamente `mes_compra`/`ano_compra` — rejeitada por risco de o modelo confundir nomes de mês/abreviações ou é induzido a "inventar" quando o texto está truncado, perdendo a checagem de plausibilidade que hoje já opera sobre data completa.
|
||||
|
||||
**3. `app/dates.py` ganha `resolve_competencia(date_raw, reference=None) -> tuple[int, int]`, substituindo `resolve_purchase_date`.**
|
||||
Racional: mantém o mesmo ponto único de normalização/fallback já estabelecido (usado tanto no upload quanto no CRUD manual), apenas trocando o tipo de retorno. O fallback "1º dia do mês corrente" vira "mês/ano corrente" — mesma semântica, um passo mais simples (não precisa mais inventar um dia fictício). `parse_date` é ajustado para aceitar os formatos existentes (ISO, `dd/mm/aaaa`) e devolver `(mes, ano)`; `format_br_date` é substituído por `format_competencia(mes, ano) -> str` (ex. `"jul/2026"`), reaproveitando a lista de abreviações de mês que já existe em `_MESES` (`dashboard_routes.py`) — essa lista é promovida para `app/dates.py` como fonte única, e `dashboard_routes.py`/`_mes_label` passam a importá-la de lá em vez de duplicar.
|
||||
|
||||
**4. Migração de schema via rebuild de tabela (não `ALTER TABLE ... DROP COLUMN`), para não depender da versão do SQLite.**
|
||||
Racional: `DROP COLUMN` só existe a partir do SQLite 3.35.0 (2021); o projeto não fixa/verifica versão mínima de SQLite hoje, e usar rebuild (criar tabela nova com o schema final, copiar dados com `INSERT INTO nova_tabela SELECT ...` computando `mes`/`ano` a partir de `purchase_date`, `DROP TABLE` antiga, `ALTER TABLE ... RENAME TO`, recriar índices) é portável para qualquer versão e é a técnica recomendada pela própria documentação do SQLite para mudanças estruturais. A migração roda dentro de `init_db()`, guardada por uma checagem `PRAGMA table_info` (mesma convenção já usada para `categoria_id`): se `purchase_date` ainda existir em `fiscal_documents`/`detected_documents`, executa o rebuild uma única vez. Alternativa considerada: tentar `ALTER TABLE ... DROP COLUMN purchase_date` diretamente — rejeitada pelo risco de quebrar em instalações com SQLite mais antigo (ex. bibliotecas do sistema operacional legadas empacotadas com Python).
|
||||
|
||||
**5. Índice `idx_fiscal_date` é substituído por `idx_fiscal_competencia ON fiscal_documents(ano, mes)`.**
|
||||
Racional: mesma finalidade (acelerar filtro/ordenação por período), agora alinhado às colunas nativas; ordem `(ano, mes)` favorece tanto filtro por ano isolado quanto por intervalo completo de competência.
|
||||
|
||||
**6. Filtro por intervalo (`start`/`end`) em `/documents` é substituído por filtro de competência usando a expressão `(ano * 12 + mes)` como chave comparável, sem nova coluna derivada.**
|
||||
Racional: preserva a capacidade de "listar de Jan/2026 até Jun/2026" com uma única expressão SQL indexável de forma equivalente (`ano*12+mes BETWEEN ? AND ?`), sem precisar manter uma terceira coluna redundante sincronizada com `mes`/`ano`. A UI de filtro em `documents_list.html` passa a ter dois pares de `<select>` Mês/Ano ("De" e "Até") em vez de dois `<input type="date">`. `FISCAL_SORT_COLUMNS` passa a expor `ano`/`mes` (ordenação primária por `ano, mes`) em vez de `purchase_date`. Alternativa considerada: manter apenas filtro por ano (sem intervalo) — rejeitada por reduzir uma funcionalidade hoje existente sem necessidade.
|
||||
|
||||
**7. `monthly_totals` agrupa diretamente por `ano, mes` (SQL `GROUP BY ano, mes ORDER BY ano, mes`), eliminando o `substr(purchase_date, 1, 7)`.**
|
||||
Racional: elimina dependência de formatação de string para agregação, mais robusto e mais rápido (usa o novo índice). O label exibido (`_mes_label`) passa a montar a partir das colunas nativas via `format_competencia`.
|
||||
|
||||
**8. Formato de exibição segue exatamente o exemplo dado pelo usuário: mês abreviado capitalizado + ano com 2 dígitos (ex. `"Jun/26"`, `"Jul/26"`), substituindo o formato hoje usado por `_mes_label` (`"jul/2026"`, minúsculo/4 dígitos).**
|
||||
Racional: o pedido original especifica esse formato de forma explícita e com dois exemplos consistentes entre si, então é tratado como requisito e não como abreviação informal. `format_competencia(mes, ano) -> str` monta a string a partir da lista `_MESES` (capitalizada, ex. `["Jan","Fev","Mar",...]`) e do ano truncado para 2 dígitos (`ano % 100`, formatado com zero à esquerda). `_mes_label`/`dashboard.html` passam a usar este novo formato, uma mudança visual em relação ao Dashboard atual.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[Risk]** Rebuild de tabela em `init_db()` roda em toda conexão até a checagem `PRAGMA table_info` confirmar que a migração já ocorreu; se o processo for interrompido no meio do rebuild (`INSERT ... SELECT` + `DROP` + `RENAME`), o banco pode ficar em estado inconsistente (tabela temporária órfã). → **Mitigação**: envolver os passos do rebuild em uma única transação (`BEGIN`/`COMMIT`), e usar nomes de tabela temporária previsíveis (`fiscal_documents_new`) verificados/limpos no início da rotina caso uma execução anterior tenha falhado a meio caminho.
|
||||
- **[Risk]** Perda de granularidade: filtros/relatórios que hoje poderiam (em teoria) restringir por dia específico deixam de ser possíveis. → **Mitigação**: aceito como consequência intencional da mudança (já sinalizado como **BREAKING** no proposal); nenhum uso atual do produto depende de granularidade diária (Dashboard e listagem já operam em nível de mês/intervalo).
|
||||
- **[Risk]** `scripts/migrate_notas.py` e testes (`tests/test_app.py`, `tests/test_ingestion.py`, `tests/test_categorization.py`) chamam `create_fiscal`/`update_fiscal`/`resolve_purchase_date` com `purchase_date=...` diretamente; ficarão quebrados até serem atualizados. → **Mitigação**: listado explicitamente nas tasks; não há uso em produção desses scripts fora de execução manual, então não bloqueiam o deploy da aplicação principal, mas devem ser corrigidos antes de considerar a mudança completa.
|
||||
- **[Trade-off]** Descartar o dia na normalização (Decisão 2) em vez de mudar o prompt da IA mantém mais código de extração inalterado, mas significa que a "plausibilidade" (`_is_plausible_purchase_date`) continua avaliando uma data completa que depois é truncada — a validação de mês/ano futuro/passado precisa ser revisada para operar em termos de mês/ano (ex. "mês/ano não pode ser mais que 3 meses no futuro" em vez de "3 dias").
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Adicionar rebuild de `fiscal_documents` e `detected_documents` em `app/database.py`/`init_db()`: criar tabelas `_new` com `mes INTEGER`/`ano INTEGER` (NOT NULL em `fiscal_documents`, nullable em `detected_documents`), copiar dados computando `mes = CAST(substr(purchase_date,6,2) AS INTEGER)`, `ano = CAST(substr(purchase_date,1,4) AS INTEGER)`, remover a tabela antiga e renomear; recriar `idx_fiscal_competencia`; tudo guardado por `PRAGMA table_info` (só roda se `purchase_date` ainda existir) e dentro de uma transação.
|
||||
2. Atualizar `app/dates.py` (`resolve_competencia`, `format_competencia`, ajuste de `_is_plausible_purchase_date` para mês/ano) e promover `_MESES` para lá.
|
||||
3. Atualizar `app/database.py`: `insert_detected`, `update_staged`, `confirm_batch`, `create_fiscal`, `update_fiscal`, `_fiscal_where`, `FISCAL_SORT_COLUMNS`, `monthly_totals`, `category_totals` para usar `mes`/`ano`.
|
||||
4. Atualizar `app/ingestion.py` (`_candidate_to_raw`) e dataclasses (`RawExtraction`, `DetectedDocumentCandidate`) apenas na nomeação/tipo de campo repassado a `resolve_competencia`, sem mudar a extração de data em si (Decisão 2).
|
||||
5. Atualizar `app/routes/upload_routes.py` e `app/templates/staging.html`: novos `<select>` Mês/Ano por linha, tornando a escolha de competência um passo obrigatório antes de habilitar a confirmação do lote.
|
||||
6. Atualizar `app/routes/documents_routes.py` e `app/templates/document_form.html` (mesmo padrão de `<select>`), e `app/templates/documents_list.html`/`dashboard.html` (filtros e exibição por competência).
|
||||
7. Atualizar `app/routes/dashboard_routes.py` (`monthly_totals`, `_mes_label` → `format_competencia`).
|
||||
8. Atualizar `scripts/migrate_notas.py` e os testes afetados (`tests/test_app.py`, `tests/test_ingestion.py`, `tests/test_categorization.py`) para o novo schema/campos.
|
||||
9. Rollback: como o rebuild remove `purchase_date` permanentemente, o rollback de código sem rollback de dados perde a competência de mês/ano recém-adotada; recomenda-se backup do arquivo SQLite antes do deploy desta mudança em produção, e reverter apenas em ambiente onde a perda de granularidade de dia (já ocorrida) é aceitável.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- O intervalo fixo de anos no seletor (2026-2030) deve ser estático no template ou calculado dinamicamente (ex. ano atual até ano atual + 4) para não exigir alteração de código todo ano? Assumido estático por ora, conforme literalmente pedido, com nota para revisão futura.
|
||||
|
||||
## Resolved
|
||||
|
||||
- **Escopo de "filtros em todas as rotas"**: confirmado com o usuário — significa apenas adaptar os filtros/agregações **existentes** (Dashboard por mês, listagem `/documents` por intervalo) para usar competência nativa. Nenhum filtro de período novo é adicionado ao Dashboard (que continua filtrando só por categoria).
|
||||
- **Alterações não commitadas no working tree**: usuário não utiliza controle de versão git neste projeto; desprezado como preocupação — não há necessidade de reconciliar/commitar nada antes da implementação.
|
||||
@@ -0,0 +1,36 @@
|
||||
## Why
|
||||
|
||||
Hoje o LerNotaFiscal extrai e persiste uma data completa (`purchase_date`, `YYYY-MM-DD`) do documento fiscal via OCR/IA, mas o valor real de negócio para o usuário é apenas o **mês de referência do gasto** (competência), não o dia exato. A extração de dia é frequentemente a fonte de baixa confiança/ilegibilidade no OCR (regex `DATE_RE` e checagem `_is_plausible_purchase_date`), o dashboard já converte a data para "mês/ano" para exibição (`_mes_label`, `substr(purchase_date, 1, 7)`), e o formulário manual já reforça a convenção "1º dia do mês corrente" como fallback. Trocar o campo de data por competência (mês/ano) simplifica a extração, elimina uma fonte de erro/ilegibilidade desnecessária e alinha o dado armazenado com o que o Dashboard e os relatórios realmente precisam.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **BREAKING**: Remover a coluna `purchase_date` (data completa) de `fiscal_documents` e `detected_documents`, substituindo por dois campos: `mes` (inteiro 1-12) e `ano` (inteiro, ex. 2026).
|
||||
- Atualizar a extração via IA (`app/ai_extraction.py`) e o fallback OCR (`lernotafiscal/extraction.py`) para não mais extrair dia — apenas identificar mês/ano do documento quando possível (ex. a partir da mesma data encontrada no texto, descartando o dia).
|
||||
- Adicionar uma etapa de confirmação obrigatória antes do botão "Enviar/Revisar": perguntar explicitamente a **Competência: Mês/Ano** de cada documento na tela de revisão (`staging.html`), com dois campos `<select>` — Mês (Jan, Fev, Mar, Abr, Mai, Jun, Jul, Ago, Set, Out, Nov, Dez) e Ano (2026, 2027, 2028, 2029, 2030) — pré-selecionados com o valor detectado quando disponível, permitindo correção manual antes de confirmar o lote.
|
||||
- Aplicar o mesmo par de selects (Mês/Ano) no formulário manual de documento (`document_form.html`), substituindo o `<input type="date">` atual.
|
||||
- Atualizar `confirm_batch`/`create_fiscal`/`update_fiscal` para gravar `mes`/`ano` em vez de `purchase_date`, com `mes` e `ano` obrigatórios (mesma garantia de não-nulo que `purchase_date` tinha).
|
||||
- Atualizar todas as agregações e filtros que hoje dependem de `purchase_date`:
|
||||
- `monthly_totals` (Dashboard: "Gastos por mês") passa a agrupar diretamente por `ano`/`mes` em vez de `substr(purchase_date, 1, 7)`.
|
||||
- `category_totals`, `_fiscal_where`, `FISCAL_SORT_COLUMNS` e a listagem `/documents` (`documents_list.html`) passam a filtrar/ordenar por `ano`/`mes` em vez de intervalo de datas (`start`/`end` do tipo date).
|
||||
- Exibição de "Competência" (ex. `Jun/2026`) substitui a coluna/célula "Data" no Dashboard e em `/documents`.
|
||||
- Atualizar scripts e testes que hoje gravam/leem `purchase_date` diretamente (`scripts/migrate_notas.py`, `tests/test_app.py`, `tests/test_ingestion.py`, `tests/test_categorization.py`) para o novo par de campos.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `document-competencia`: Cobre a captura, revisão, persistência, exibição e filtragem da competência (mês/ano) de um documento fiscal, substituindo o conceito de data completa de compra em todo o fluxo (upload → revisão → confirmação → dashboard → listagem/filtros).
|
||||
|
||||
### Modified Capabilities
|
||||
(nenhuma — não há specs existentes em `openspec/specs/`; o item acima é tratado como nova capability, que substitui integralmente o comportamento de data hoje implícito no código)
|
||||
|
||||
## Impact
|
||||
|
||||
- **Banco de dados** (`app/database.py`): remoção de `purchase_date` (e do índice `idx_fiscal_date`) de `fiscal_documents` e `detected_documents`; novas colunas `mes INTEGER NOT NULL` e `ano INTEGER NOT NULL` (com índice equivalente `idx_fiscal_competencia` em `(ano, mes)`); rebuild de tabela (SQLite não remove coluna com `ALTER TABLE` em todas as versões-alvo) com backfill dos dados existentes a partir do `purchase_date` atual antes de descartá-lo.
|
||||
- **Extração** (`app/ai_extraction.py`, `lernotafiscal/extraction.py`, `app/ingestion.py`): prompt de IA e regex OCR passam a reportar mês/ano em vez de dia completo; `RawExtraction`/`DetectedDocumentCandidate` trocam `purchase_date_raw`/`purchase_date` por `mes_raw`/`ano_raw`.
|
||||
- **Normalização** (`app/dates.py`): `resolve_purchase_date`/`parse_date`/`format_br_date` são substituídos ou adaptados para resolver/validar/formatar competência (`mes`/`ano` → "Jun/2026"), reaproveitando a lógica de fallback "mês corrente" já existente.
|
||||
- **Rotas e templates de upload/revisão** (`app/routes/upload_routes.py`, `app/templates/staging.html`): novo par de `<select>` Mês/Ano por linha, com pergunta explícita de competência antes de habilitar "Enviar"/confirmar lote.
|
||||
- **Rotas e templates de CRUD manual** (`app/routes/documents_routes.py`, `app/templates/document_form.html`): mesmo par de `<select>` Mês/Ano substituindo `<input type="date">`.
|
||||
- **Dashboard** (`app/routes/dashboard_routes.py`, `app/templates/dashboard.html`): `monthly_totals` e exibição de "Últimos documentos" passam a usar `ano`/`mes` nativos; `_mes_label` deixa de derivar de string e passa a formatar diretamente a partir das colunas.
|
||||
- **Listagem/filtros** (`app/routes/documents_routes.py`, `app/templates/documents_list.html`): filtros `start`/`end` (datas) são substituídos por filtro de competência (mês/ano ou intervalo de competência); ordenação por `purchase_date` substituída por ordenação por `(ano, mes)`.
|
||||
- **Scripts e testes**: `scripts/migrate_notas.py`, `tests/test_app.py`, `tests/test_ingestion.py`, `tests/test_categorization.py` precisam ser atualizados para o novo schema/campos.
|
||||
- **Compatibilidade**: mudança é destrutiva para dados existentes no formato antigo — exige migração/backfill de `purchase_date` para `mes`/`ano` antes da remoção da coluna; documentos sem data reconhecível hoje (fallback "1º dia do mês corrente") migram diretamente para o mês/ano correspondente.
|
||||
@@ -0,0 +1,81 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Persistência de Competência (Mês/Ano)
|
||||
O sistema SHALL persistir a competência de um documento fiscal como dois campos inteiros, `mes` (1-12) e `ano` (ex. 2026), em vez de uma data completa de compra. Em `fiscal_documents`, `mes` e `ano` SHALL ser obrigatórios (não nulos). Em `detected_documents` (staging), `mes` e `ano` SHALL permanecer opcionais enquanto o documento não tiver sido revisado/confirmado.
|
||||
|
||||
#### Scenario: Documento confirmado exige competência
|
||||
- **WHEN** um documento em staging é promovido para `fiscal_documents` via confirmação de lote
|
||||
- **THEN** o registro criado possui `mes` e `ano` preenchidos com valores válidos (mes entre 1 e 12, ano numérico)
|
||||
|
||||
#### Scenario: Documento em staging pode estar sem competência
|
||||
- **WHEN** um documento acabou de ser detectado por OCR/IA e ainda não foi revisado
|
||||
- **THEN** `mes` e/ou `ano` podem estar nulos em `detected_documents` até que o usuário revise e preencha a competência
|
||||
|
||||
### Requirement: Extração Não Persiste Mais Data Completa
|
||||
O sistema SHALL deixar de persistir o dia da compra em qualquer tabela. A extração via IA ou OCR pode continuar identificando uma data completa no texto/imagem do documento, mas o dia SHALL ser descartado antes da persistência, mantendo-se apenas mês e ano.
|
||||
|
||||
#### Scenario: Data completa detectada no documento
|
||||
- **WHEN** a extração (IA ou OCR) identifica uma data completa (dia/mês/ano) no documento
|
||||
- **THEN** o sistema armazena apenas o mês e o ano correspondentes, descartando o dia
|
||||
|
||||
### Requirement: Confirmação Obrigatória de Competência Antes do Envio
|
||||
Na tela de revisão pós-upload, o sistema SHALL exigir que o usuário confirme explicitamente a Competência (Mês/Ano) de cada documento, através de dois campos de seleção — Mês (Jan a Dez) e Ano (2026 a 2030) — antes de permitir a confirmação do lote.
|
||||
|
||||
#### Scenario: Competência pré-selecionada a partir da extração
|
||||
- **WHEN** a extração identificou um mês/ano válido para o documento
|
||||
- **THEN** os seletores de Mês e Ano na tela de revisão vêm pré-selecionados com esses valores, permitindo correção manual pelo usuário
|
||||
|
||||
#### Scenario: Competência ausente bloqueia confirmação do lote
|
||||
- **WHEN** um ou mais documentos do lote não têm Mês e Ano selecionados
|
||||
- **THEN** o sistema não permite confirmar o lote até que a competência de todos os documentos pendentes seja preenchida
|
||||
|
||||
### Requirement: Seleção de Competência no Cadastro Manual
|
||||
O formulário manual de criação/edição de documento fiscal (`document_form.html`) SHALL substituir o campo de data por dois seletores — Mês (Jan a Dez) e Ano (2026 a 2030) — para definir a competência do documento.
|
||||
|
||||
#### Scenario: Criação manual de documento
|
||||
- **WHEN** o usuário cria um novo documento fiscal manualmente
|
||||
- **THEN** o formulário exige a seleção de Mês e Ano em vez de uma data completa
|
||||
|
||||
#### Scenario: Edição de documento existente
|
||||
- **WHEN** o usuário edita um documento fiscal existente
|
||||
- **THEN** os seletores de Mês e Ano vêm pré-preenchidos com a competência atual do documento, podendo ser alterados
|
||||
|
||||
### Requirement: Exibição de Competência no Dashboard e Listagens
|
||||
O Dashboard e a listagem de documentos (`/documents`) SHALL exibir a competência de cada documento no formato "Mês abreviado/Ano com 2 dígitos" (ex. "Jun/26", "Jul/26") no lugar da antiga coluna "Data".
|
||||
|
||||
#### Scenario: Tabela "Últimos documentos" no Dashboard
|
||||
- **WHEN** o usuário visualiza o Dashboard
|
||||
- **THEN** cada linha da tabela "Últimos documentos" exibe a competência (mês/ano) do documento, em vez da data completa
|
||||
|
||||
#### Scenario: Coluna na listagem de documentos
|
||||
- **WHEN** o usuário visualiza a listagem `/documents`
|
||||
- **THEN** a coluna antes chamada "Data" exibe a competência (mês/ano) de cada documento, e continua ordenável
|
||||
|
||||
### Requirement: Filtro por Competência na Listagem de Documentos
|
||||
A listagem `/documents` SHALL permitir filtrar documentos por intervalo de competência (Mês/Ano inicial e Mês/Ano final), substituindo o filtro anterior por intervalo de datas.
|
||||
|
||||
#### Scenario: Filtro por intervalo de competência
|
||||
- **WHEN** o usuário seleciona uma competência inicial (ex. Jan/2026) e uma competência final (ex. Jun/2026) nos filtros da listagem
|
||||
- **THEN** apenas documentos cuja competência esteja dentro desse intervalo (inclusive) são exibidos
|
||||
|
||||
#### Scenario: Ordenação por competência
|
||||
- **WHEN** o usuário ordena a listagem pela coluna de competência
|
||||
- **THEN** os documentos são ordenados por ano e mês (crescente ou decrescente conforme selecionado)
|
||||
|
||||
### Requirement: Agregação Mensal do Dashboard por Competência Nativa
|
||||
O gráfico "Gastos por mês" do Dashboard SHALL agrupar os totais diretamente pelas colunas `ano`/`mes` de `fiscal_documents`, em vez de derivar o mês a partir de uma string de data.
|
||||
|
||||
#### Scenario: Totais mensais agrupados por competência
|
||||
- **WHEN** o Dashboard calcula os totais mensais para exibição
|
||||
- **THEN** o agrupamento é feito pelas colunas `ano` e `mes`, e o rótulo exibido (ex. "Jun/26") é formatado a partir desses valores nativos
|
||||
|
||||
### Requirement: Migração de Dados Existentes para Competência
|
||||
Ao atualizar para esta mudança, o sistema SHALL migrar automaticamente todos os registros existentes de `fiscal_documents` e `detected_documents`, preenchendo `mes` e `ano` a partir da data de compra anteriormente armazenada, antes de remover o campo de data.
|
||||
|
||||
#### Scenario: Migração automática na inicialização
|
||||
- **WHEN** a aplicação inicializa contra um banco de dados que ainda contém o campo de data completa
|
||||
- **THEN** o sistema preenche `mes` e `ano` de cada registro a partir da data existente e remove o campo de data, sem exigir intervenção manual do usuário
|
||||
|
||||
#### Scenario: Migração já aplicada não é repetida
|
||||
- **WHEN** a aplicação inicializa contra um banco de dados que já foi migrado (campo de data já removido)
|
||||
- **THEN** o sistema não tenta migrar novamente e opera normalmente com `mes`/`ano`
|
||||
@@ -0,0 +1,65 @@
|
||||
## 1. Migração de Schema (Banco de Dados)
|
||||
|
||||
- [x] 1.1 Em `app/database.py`, implementar rebuild de `fiscal_documents` (tabela `_new` com `mes INTEGER NOT NULL`, `ano INTEGER NOT NULL`, demais colunas iguais, sem `purchase_date`), copiando dados existentes com `mes = CAST(substr(purchase_date,6,2) AS INTEGER)` e `ano = CAST(substr(purchase_date,1,4) AS INTEGER)`, dentro de uma transação, guardado por checagem `PRAGMA table_info` (só roda se `purchase_date` ainda existir).
|
||||
- [x] 1.2 Repetir o mesmo rebuild para `detected_documents`, com `mes INTEGER` e `ano INTEGER` nullable (mantendo o mesmo comportamento opcional que `purchase_date` tinha).
|
||||
- [x] 1.3 Substituir o índice `idx_fiscal_date` por `idx_fiscal_competencia ON fiscal_documents(ano, mes)`.
|
||||
- [x] 1.4 Testar a migração rodando `init_db()` contra uma cópia do banco atual (com dados de `purchase_date` já existentes) e validar que `mes`/`ano` foram preenchidos corretamente e que rodar `init_db()` de novo não falha nem duplica/recria o rebuild.
|
||||
|
||||
## 2. Normalização de Competência (`app/dates.py`)
|
||||
|
||||
- [x] 2.1 Promover a lista `_MESES` (hoje em `dashboard_routes.py`) para `app/dates.py` como fonte única de abreviações de mês.
|
||||
- [x] 2.2 Implementar `resolve_competencia(date_raw, reference=None) -> tuple[int, int]`, substituindo `resolve_purchase_date`, com fallback para mês/ano corrente quando o valor for inválido/ausente.
|
||||
- [x] 2.3 Implementar `format_competencia(mes: int, ano: int) -> str` no formato `"Jun/26"` (mês abreviado capitalizado + ano com 2 dígitos), substituindo `format_br_date`, e atualizar o filtro Jinja registrado em `app/templating.py` (`brdate` → filtro de competência).
|
||||
- [x] 2.4 Ajustar `_is_plausible_purchase_date` (ou equivalente) para validar plausibilidade em termos de mês/ano (ex. não mais que alguns meses no futuro, ano não muito no passado), em vez de dias.
|
||||
|
||||
## 3. Extração (IA e OCR)
|
||||
|
||||
- [x] 3.1 Confirmar que `app/ai_extraction.py` e `lernotafiscal/extraction.py` continuam extraindo a data completa do documento sem alteração de prompt/regex (Decisão de design: descarte do dia acontece na normalização, não na extração).
|
||||
- [x] 3.2 Renomear/ajustar `RawExtraction.purchase_date_raw` e `DetectedDocumentCandidate.purchase_date` apenas na camada de repasse (`app/ingestion.py:_candidate_to_raw`), garantindo que o valor bruto siga até `resolve_competencia` sem mudança de comportamento de extração.
|
||||
|
||||
## 4. Persistência e Consultas (`app/database.py`)
|
||||
|
||||
- [x] 4.1 Atualizar `insert_detected` e `update_staged` para gravar `mes`/`ano` (via `resolve_competencia`) em vez de `purchase_date`.
|
||||
- [x] 4.2 Atualizar `confirm_batch` para exigir `mes`/`ano` válidos (em vez de `purchase_date` truthy) antes de promover um documento de staging para `fiscal_documents`.
|
||||
- [x] 4.3 Atualizar `create_fiscal` e `update_fiscal` para receber `mes`/`ano` como parâmetros obrigatórios em vez de `purchase_date`.
|
||||
- [x] 4.4 Atualizar `_fiscal_where` para filtrar por intervalo de competência usando a expressão `(ano * 12 + mes) BETWEEN ? AND ?`, substituindo o filtro por `purchase_date >= / <=`.
|
||||
- [x] 4.5 Atualizar `FISCAL_SORT_COLUMNS` para expor ordenação por `(ano, mes)` no lugar de `purchase_date`.
|
||||
- [x] 4.6 Atualizar `monthly_totals` para agrupar diretamente por `ano, mes` (`GROUP BY ano, mes ORDER BY ano, mes`), eliminando `substr(purchase_date, 1, 7)`.
|
||||
- [x] 4.7 Atualizar `category_totals` para filtrar por competência usando a mesma expressão `(ano * 12 + mes)`.
|
||||
|
||||
## 5. Fluxo de Upload e Revisão (Staging)
|
||||
|
||||
- [x] 5.1 Em `app/routes/upload_routes.py`, atualizar o handler de upload (`POST /upload`) para gravar `mes`/`ano` (via `resolve_competencia`) em vez de `purchase_date` ao inserir em `detected_documents`.
|
||||
- [x] 5.2 Atualizar `app/templates/staging.html`: substituir o `<input type="date" name="purchase_date">` por dois `<select>` — Mês (Jan-Dez) e Ano (2026-2030) — pré-selecionados com o valor detectado quando disponível.
|
||||
- [x] 5.3 Atualizar o handler `POST /import/{batch_id}/update/{detected_id}` para receber e persistir `mes`/`ano` do formulário de revisão.
|
||||
- [x] 5.4 Garantir que o cálculo de `summary["pendentes"]` (usado para bloquear o botão de confirmação do lote) passe a considerar `mes`/`ano` ausentes/inválidos como pendência, em vez de `purchase_date`.
|
||||
|
||||
## 6. Cadastro Manual de Documento
|
||||
|
||||
- [x] 6.1 Atualizar `app/templates/document_form.html`: substituir o `<input type="date" name="purchase_date">` pelos mesmos dois `<select>` de Mês/Ano usados na revisão, pré-preenchidos na edição.
|
||||
- [x] 6.2 Atualizar os handlers de criação/edição em `app/routes/documents_routes.py` (`/documents/new`, `/documents/{id}/edit`) para ler `mes`/`ano` do formulário e repassar a `create_fiscal`/`update_fiscal`.
|
||||
|
||||
## 7. Listagem e Filtros (`/documents`)
|
||||
|
||||
- [x] 7.1 Atualizar `app/templates/documents_list.html`: substituir os filtros `<input type="date" name="start">`/`name="end"` por dois pares de `<select>` Mês/Ano ("De" e "Até").
|
||||
- [x] 7.2 Atualizar o handler `GET /documents` em `app/routes/documents_routes.py` para ler os novos parâmetros de filtro de competência e repassá-los a `list_fiscal`/`fiscal_summary`.
|
||||
- [x] 7.3 Atualizar a coluna "Data" em `documents_list.html` para exibir a competência formatada (`format_competencia`) e manter a ordenação por competência (`sort_url` apontando para `ano`/`mes`).
|
||||
|
||||
## 8. Dashboard
|
||||
|
||||
- [x] 8.1 Atualizar `app/routes/dashboard_routes.py`: `_mes_label` passa a usar `format_competencia`/`_MESES` centralizados em `app/dates.py`, consumindo `ano`/`mes` nativos vindos de `monthly_totals`.
|
||||
- [x] 8.2 Atualizar `app/templates/dashboard.html`: tabela "Últimos documentos" exibe a competência (`format_competencia`) no lugar de `d.purchase_date | brdate`.
|
||||
|
||||
## 9. Scripts e Testes
|
||||
|
||||
- [x] 9.1 Atualizar `scripts/migrate_notas.py` para gravar `mes`/`ano` em vez de `purchase_date` ao chamar `create_fiscal`.
|
||||
- [x] 9.2 Atualizar `tests/test_app.py` (usos de `resolve_purchase_date`, `create_fiscal`, `update_fiscal` com `purchase_date=...`) para o novo par de campos.
|
||||
- [x] 9.3 Atualizar `tests/test_ingestion.py` (`docs[0].purchase_date`) para verificar `mes`/`ano` em vez de data completa.
|
||||
- [x] 9.4 Atualizar `tests/test_categorization.py` (`db.create_fiscal(..., purchase_date=...)`) para o novo schema.
|
||||
- [x] 9.5 Rodar a suíte de testes completa e corrigir quaisquer quebras remanescentes relacionadas a `purchase_date`.
|
||||
|
||||
## 10. Validação Manual
|
||||
|
||||
- [x] 10.1 Rodar a aplicação localmente, fazer upload de um documento de teste, confirmar que a tela de revisão exige seleção de Mês/Ano antes de habilitar a confirmação do lote, e validar que o documento aparece corretamente no Dashboard e em `/documents` após confirmado.
|
||||
- [x] 10.2 Validar cadastro manual (criar/editar) de um documento fiscal usando os novos seletores de Mês/Ano.
|
||||
- [x] 10.3 Validar o filtro por intervalo de competência em `/documents` e a ordenação pela coluna de competência.
|
||||
Reference in New Issue
Block a user