Implementado (3 features)
**Agrupamento de faltantes contíguos** (`detector_sequencia.rs`) - Nova função pública `agrupar_contiguos(&[u64]) -> Vec<(u64, u64)>` com 4 testes - Na tela de resultado, faltantes agora aparecem como `• 100–104 (5 notas)` em vez de 5 linhas separadas **Estatísticas de completude** (`resultado.rs`) - Cada série exibe: `Série 001 — 10 faltante(s) — 990/1000 notas (99.0% completo):` **Copiar para clipboard** (`resultado.rs`) - Botão `📋 Copiar` ao lado de cada série nos faltantes — copia todos os números (não só a página atual) - Botão `📋 Copiar` nas duplicatas — copia no formato `1234 (3x), 5678 (2x)` Total: **42 → 46 testes**, todos passando.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use crate::domain::{
|
||||
entities::{
|
||||
chave_serie::ChaveSerie,
|
||||
nota::Nota,
|
||||
resultado_analise::{ResultadoAnalise, ResultadoPreAnalise},
|
||||
},
|
||||
@@ -15,30 +16,31 @@ use std::collections::HashMap;
|
||||
/// O caller deve verificar se algum intervalo excede `LIMITE_FALTANTES` e,
|
||||
/// se sim, exibir confirmação ao usuário antes de chamar `expandir_analise`.
|
||||
pub fn pre_analisar(notas: &[Nota]) -> ResultadoPreAnalise {
|
||||
// Agrupar por série
|
||||
let mut por_serie: HashMap<String, Vec<&Nota>> = HashMap::new();
|
||||
// Agrupar por (serie, documento_tipo)
|
||||
let mut por_chave: HashMap<ChaveSerie, Vec<&Nota>> = HashMap::new();
|
||||
for nota in notas {
|
||||
por_serie.entry(nota.serie.clone()).or_default().push(nota);
|
||||
let chave = ChaveSerie::new(nota.serie.clone(), nota.documento_tipo.clone());
|
||||
por_chave.entry(chave).or_default().push(nota);
|
||||
}
|
||||
|
||||
let mut intervalos_por_serie = HashMap::new();
|
||||
let mut soma_total = Decimal::ZERO;
|
||||
let mut soma_por_serie: HashMap<String, Decimal> = HashMap::new();
|
||||
let mut total_por_serie: HashMap<String, usize> = HashMap::new();
|
||||
let mut soma_por_serie: HashMap<ChaveSerie, Decimal> = HashMap::new();
|
||||
let mut total_por_serie: HashMap<ChaveSerie, usize> = HashMap::new();
|
||||
|
||||
for (serie, notas_serie) in &por_serie {
|
||||
for (chave, notas_grupo) in &por_chave {
|
||||
// Somar valores
|
||||
for nota in notas_serie.iter() {
|
||||
for nota in notas_grupo.iter() {
|
||||
if let Some(v) = nota.valor {
|
||||
soma_total += v;
|
||||
*soma_por_serie.entry(serie.clone()).or_insert(Decimal::ZERO) += v;
|
||||
*soma_por_serie.entry(chave.clone()).or_insert(Decimal::ZERO) += v;
|
||||
}
|
||||
}
|
||||
*total_por_serie.entry(serie.clone()).or_insert(0) += notas_serie.len();
|
||||
*total_por_serie.entry(chave.clone()).or_insert(0) += notas_grupo.len();
|
||||
|
||||
// Calcular intervalo de faltantes
|
||||
if let Some(intervalo) = calcular_intervalo(notas_serie) {
|
||||
intervalos_por_serie.insert(serie.clone(), intervalo);
|
||||
if let Some(intervalo) = calcular_intervalo(notas_grupo) {
|
||||
intervalos_por_serie.insert(chave.clone(), intervalo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,29 +55,30 @@ pub fn pre_analisar(notas: &[Nota]) -> ResultadoPreAnalise {
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifica se alguma série excede o limite de faltantes.
|
||||
/// Retorna lista de séries que precisam de confirmação.
|
||||
pub fn series_com_intervalo_excessivo(pre: &ResultadoPreAnalise) -> Vec<(String, u64)> {
|
||||
/// Verifica se algum grupo excede o limite de faltantes.
|
||||
/// Retorna lista de chaves que precisam de confirmação.
|
||||
pub fn series_com_intervalo_excessivo(pre: &ResultadoPreAnalise) -> Vec<(ChaveSerie, u64)> {
|
||||
pre.intervalos_por_serie
|
||||
.iter()
|
||||
.filter(|(_, iv)| iv.excede_limite(LIMITE_FALTANTES))
|
||||
.map(|(serie, iv)| (serie.clone(), iv.contagem_faltantes))
|
||||
.map(|(chave, iv)| (chave.clone(), iv.contagem_faltantes))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Expande a pré-análise para o resultado completo, materializando a lista de faltantes.
|
||||
/// Deve ser chamado após confirmação do usuário (ou quando nenhum intervalo excede o limite).
|
||||
pub fn expandir_analise(pre: ResultadoPreAnalise, notas: &[Nota]) -> ResultadoAnalise {
|
||||
let mut por_serie: HashMap<String, Vec<&Nota>> = HashMap::new();
|
||||
let mut por_chave: HashMap<ChaveSerie, Vec<&Nota>> = HashMap::new();
|
||||
for nota in notas {
|
||||
por_serie.entry(nota.serie.clone()).or_default().push(nota);
|
||||
let chave = ChaveSerie::new(nota.serie.clone(), nota.documento_tipo.clone());
|
||||
por_chave.entry(chave).or_default().push(nota);
|
||||
}
|
||||
|
||||
let mut faltantes_por_serie = HashMap::new();
|
||||
|
||||
for (serie, notas_serie) in &por_serie {
|
||||
let faltantes = detectar_faltantes(notas_serie);
|
||||
faltantes_por_serie.insert(serie.clone(), faltantes);
|
||||
for (chave, notas_grupo) in &por_chave {
|
||||
let faltantes = detectar_faltantes(notas_grupo);
|
||||
faltantes_por_serie.insert(chave.clone(), faltantes);
|
||||
}
|
||||
|
||||
ResultadoAnalise {
|
||||
@@ -100,7 +103,11 @@ mod tests {
|
||||
}
|
||||
|
||||
fn nota(numero: u64, serie: &str, valor: Option<rust_decimal::Decimal>) -> Nota {
|
||||
Nota::new(numero, serie.to_string(), valor, None)
|
||||
Nota::new(numero, serie.to_string(), None, valor, None)
|
||||
}
|
||||
|
||||
fn nota_com_tipo(numero: u64, serie: &str, tipo: &str) -> Nota {
|
||||
Nota::new(numero, serie.to_string(), Some(tipo.to_string()), None, None)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -111,7 +118,8 @@ mod tests {
|
||||
nota(4, "001", Some(dec!(50.00))),
|
||||
];
|
||||
let resultado = executar_analise(¬as);
|
||||
assert_eq!(resultado.faltantes_por_serie["001"], vec![3u64]);
|
||||
let chave = ChaveSerie::new("001".to_string(), None);
|
||||
assert_eq!(resultado.faltantes_por_serie[&chave], vec![3u64]);
|
||||
assert_eq!(resultado.soma_total, dec!(350.00));
|
||||
}
|
||||
|
||||
@@ -124,8 +132,10 @@ mod tests {
|
||||
nota(2, "002", None),
|
||||
];
|
||||
let resultado = executar_analise(¬as);
|
||||
assert_eq!(resultado.faltantes_por_serie["001"], vec![2u64]);
|
||||
assert!(resultado.faltantes_por_serie["002"].is_empty());
|
||||
let chave001 = ChaveSerie::new("001".to_string(), None);
|
||||
let chave002 = ChaveSerie::new("002".to_string(), None);
|
||||
assert_eq!(resultado.faltantes_por_serie[&chave001], vec![2u64]);
|
||||
assert!(resultado.faltantes_por_serie[&chave002].is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -134,6 +144,23 @@ mod tests {
|
||||
let pre = pre_analisar(¬as);
|
||||
let excessivos = series_com_intervalo_excessivo(&pre);
|
||||
assert_eq!(excessivos.len(), 1);
|
||||
assert_eq!(excessivos[0].0, "001");
|
||||
assert_eq!(excessivos[0].0, ChaveSerie::new("001".to_string(), None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analise_nfe_nfce_mesma_serie_grupos_separados() {
|
||||
// NFE série 001: 1, 3 → faltante 2
|
||||
// NFCE série 001: 2, 5 → faltante 3 e 4
|
||||
let notas = vec![
|
||||
nota_com_tipo(1, "001", "NFE"),
|
||||
nota_com_tipo(3, "001", "NFE"),
|
||||
nota_com_tipo(2, "001", "NFCE"),
|
||||
nota_com_tipo(5, "001", "NFCE"),
|
||||
];
|
||||
let resultado = executar_analise(¬as);
|
||||
let chave_nfe = ChaveSerie::new("001".to_string(), Some("NFE".to_string()));
|
||||
let chave_nfce = ChaveSerie::new("001".to_string(), Some("NFCE".to_string()));
|
||||
assert_eq!(resultado.faltantes_por_serie[&chave_nfe], vec![2u64]);
|
||||
assert_eq!(resultado.faltantes_por_serie[&chave_nfce], vec![3u64, 4u64]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user