diff --git a/common/proto/tables_data.proto b/common/proto/tables_data.proto index e38f047b..c18be2c9 100644 --- a/common/proto/tables_data.proto +++ b/common/proto/tables_data.proto @@ -185,6 +185,9 @@ message PostTableDataResponse { // Journal created or updated by automatic accounting, when applicable. optional int64 journal_id = 5; + + // Non-blocking data-quality warnings produced while inserting the row. + repeated string warnings = 6; } message BeginTableDataImportRequest { @@ -218,9 +221,17 @@ message CommitTableDataImportRequest { string import_id = 1; } +message TableDataImportWarning { + // Zero-based position in the staged import, matching failed-row metadata. + int64 row_index = 1; + string table_name = 2; + string message = 3; +} + message CommitTableDataImportResponse { bool success = 1; int64 inserted_rows = 2; + repeated TableDataImportWarning warnings = 3; } message AbortTableDataImportRequest { diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 95136937..c84d1209 100644 Binary files a/common/src/proto/descriptor.bin and b/common/src/proto/descriptor.bin differ diff --git a/common/src/proto/komp_ac.tables_data.rs b/common/src/proto/komp_ac.tables_data.rs index 70930ffd..bb3bafae 100644 --- a/common/src/proto/komp_ac.tables_data.rs +++ b/common/src/proto/komp_ac.tables_data.rs @@ -77,6 +77,9 @@ pub struct PostTableDataResponse { /// Journal created or updated by automatic accounting, when applicable. #[prost(int64, optional, tag = "5")] pub journal_id: ::core::option::Option, + /// Non-blocking data-quality warnings produced while inserting the row. + #[prost(string, repeated, tag = "6")] + pub warnings: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct BeginTableDataImportRequest { @@ -122,12 +125,24 @@ pub struct CommitTableDataImportRequest { #[prost(string, tag = "1")] pub import_id: ::prost::alloc::string::String, } -#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct TableDataImportWarning { + /// Zero-based position in the staged import, matching failed-row metadata. + #[prost(int64, tag = "1")] + pub row_index: i64, + #[prost(string, tag = "2")] + pub table_name: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub message: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] pub struct CommitTableDataImportResponse { #[prost(bool, tag = "1")] pub success: bool, #[prost(int64, tag = "2")] pub inserted_rows: i64, + #[prost(message, repeated, tag = "3")] + pub warnings: ::prost::alloc::vec::Vec, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct AbortTableDataImportRequest { diff --git a/prod_data_parse_scripts/parse_uc_osnova.sh b/prod_data_parse_scripts/parse_uc_osnova.sh index ccb65276..7bcdecbd 100644 --- a/prod_data_parse_scripts/parse_uc_osnova.sh +++ b/prod_data_parse_scripts/parse_uc_osnova.sh @@ -60,12 +60,17 @@ process_csv() { cleaned = clean_account($5) key = key_value(cleaned) + if (key == "") { + cleaned = "\"000\"" + key = "000" + } + $5 = cleaned # Deleted: # "000" -> true # "000/" -> cleaned to "000" -> true - # "" -> true + # "" -> changed to "000" -> true # # Everything else -> false if (key == "000" || key == "") { diff --git a/web/locales/cs/main.ftl b/web/locales/cs/main.ftl index 47554207..8eb11fb6 100644 --- a/web/locales/cs/main.ftl +++ b/web/locales/cs/main.ftl @@ -616,6 +616,7 @@ import-success-message = Vloženo { $inserted -> [few] { $source_rows } připravených řádků *[other] { $source_rows } připravených řádků }. +import-success-warnings = Upozornění: { $warnings } # --- Import v průběhu ------------------------------------------------------- import-progress-heading = Importuje se do { $table } diff --git a/web/locales/en/main.ftl b/web/locales/en/main.ftl index b7f90f32..29294883 100644 --- a/web/locales/en/main.ftl +++ b/web/locales/en/main.ftl @@ -605,6 +605,7 @@ import-success-message = Inserted { $inserted -> [one] { $source_rows } prepared row *[other] { $source_rows } prepared rows }. +import-success-warnings = Warnings: { $warnings } # --- The import while it runs ---------------------------------------------- import-progress-heading = Importing into { $table } diff --git a/web/locales/sk/main.ftl b/web/locales/sk/main.ftl index eec10768..b2a3bbdd 100644 --- a/web/locales/sk/main.ftl +++ b/web/locales/sk/main.ftl @@ -614,6 +614,7 @@ import-success-message = Vložený { $inserted -> [one] { $source_rows } pripraveného riadka *[other] { $source_rows } pripravených riadkov }. +import-success-warnings = Upozornenia: { $warnings } # --- Import počas behu ------------------------------------------------------ import-progress-heading = Importuje sa do { $table } diff --git a/web/src/pages/import_export/import/logic.rs b/web/src/pages/import_export/import/logic.rs index e4cebcdc..8c2e2410 100644 --- a/web/src/pages/import_export/import/logic.rs +++ b/web/src/pages/import_export/import/logic.rs @@ -302,11 +302,12 @@ pub(crate) async fn import_progress( let response = match &snapshot.outcome { None => Html(ui::render_progress(locale, &id, &snapshot)).into_response(), - Some(Outcome::Succeeded) => Html(ui::render_success( + Some(Outcome::Succeeded { warnings }) => Html(ui::render_success( locale, snapshot.inserted, snapshot.total_rows, &snapshot.table_name, + warnings, )) .into_response(), Some(Outcome::RowFailed { @@ -445,8 +446,21 @@ async fn run_import(job: Running) { }; match committed { Ok(response) => { - let inserted = usize::try_from(response.into_inner().inserted_rows).unwrap_or(staged); - jobs.finish(&job.id, inserted, Outcome::Succeeded); + let response = response.into_inner(); + let inserted = usize::try_from(response.inserted_rows).unwrap_or(staged); + let warnings = response + .warnings + .into_iter() + .map(|warning| { + format!( + "CSV row {} ({}): {}", + warning.row_index + 2, + warning.table_name, + warning.message, + ) + }) + .collect(); + jobs.finish(&job.id, inserted, Outcome::Succeeded { warnings }); } Err(error) => { abort_import(&job, &import_id).await; diff --git a/web/src/pages/import_export/import/progress.rs b/web/src/pages/import_export/import/progress.rs index e2bba2a7..67887a98 100644 --- a/web/src/pages/import_export/import/progress.rs +++ b/web/src/pages/import_export/import/progress.rs @@ -34,7 +34,7 @@ const KEEP_FINISHED: Duration = Duration::from_secs(300); #[derive(Clone, Debug)] pub(crate) enum Outcome { /// Every staged row was committed atomically. - Succeeded, + Succeeded { warnings: Vec }, /// The backend stopped on one row. No rows from the import were committed. RowFailed { status: StatusCode, @@ -255,7 +255,7 @@ mod tests { assert!(jobs.is_running("session")); assert!(!jobs.is_running("other")); - jobs.finish(&id, 10, Outcome::Succeeded); + jobs.finish(&id, 10, Outcome::Succeeded { warnings: Vec::new() }); assert!(!jobs.is_running("session")); } @@ -267,7 +267,7 @@ mod tests { inserted: 0, total_rows: 0, elapsed: Duration::from_secs(1), - outcome: Some(Outcome::Succeeded), + outcome: Some(Outcome::Succeeded { warnings: Vec::new() }), }; assert_eq!(snapshot.percent(), 100); diff --git a/web/src/pages/import_export/import/ui.rs b/web/src/pages/import_export/import/ui.rs index a925ae90..e39178db 100644 --- a/web/src/pages/import_export/import/ui.rs +++ b/web/src/pages/import_export/import/ui.rs @@ -169,17 +169,27 @@ pub(crate) fn render_success( inserted: usize, prepared_rows: usize, table_name: &str, + warnings: &[String], ) -> String { + let mut message = tr!( + locale, + "import-success-message", + "inserted" => inserted as i64, + "source_rows" => prepared_rows as i64, + "table" => table_name.to_string(), + ); + if !warnings.is_empty() { + message.push(' '); + message.push_str(&tr!( + locale, + "import-success-warnings", + "warnings" => warnings.join("; "), + )); + } render(&ImportSuccessDialog { locale, title: &tr!(locale, "import-success-title"), - message: &tr!( - locale, - "import-success-message", - "inserted" => inserted as i64, - "source_rows" => prepared_rows as i64, - "table" => table_name.to_string(), - ), + message: &message, }) } @@ -485,7 +495,7 @@ mod tests { /// dialog the user has to actively dismiss, not a toast on its own timer. #[test] fn a_finished_import_answers_with_a_dismissible_dialog_not_a_toast() { - let html = render_success(Locale::English, 500, 500, "customers"); + let html = render_success(Locale::English, 500, 500, "customers", &[]); assert!(!html.contains("Template error"), "{html}"); assert!(html.contains(r#"role="dialog""#), "{html}"); @@ -497,4 +507,19 @@ mod tests { assert!(!html.contains("$dispatch('notify'"), "{html}"); assert!(html.contains("keydown.esc"), "{html}"); } + + #[test] + fn a_finished_import_displays_non_blocking_warnings() { + let html = render_success( + Locale::English, + 1, + 1, + "customers", + &["CSV row 2 (customers): Deleted row omitted required column 'name'".to_string()], + ); + + assert!(html.contains("Warnings:"), "{html}"); + assert!(html.contains("CSV row 2"), "{html}"); + assert!(html.contains("Deleted row omitted required column"), "{html}"); + } }