From f7535a6fd5abb0323e32a436a46febb082fb6246 Mon Sep 17 00:00:00 2001 From: Priec Date: Thu, 3 Sep 2026 08:34:05 +0200 Subject: [PATCH] fixed issues with the import --- common/proto/tables_data.proto | 11 +++++ common/src/proto/descriptor.bin | Bin 239828 -> 240541 bytes common/src/proto/komp_ac.tables_data.rs | 17 +++++++- prod_data_parse_scripts/parse_uc_osnova.sh | 7 ++- web/locales/cs/main.ftl | 1 + web/locales/en/main.ftl | 1 + web/locales/sk/main.ftl | 1 + web/src/pages/import_export/import/logic.rs | 20 +++++++-- .../pages/import_export/import/progress.rs | 6 +-- web/src/pages/import_export/import/ui.rs | 41 ++++++++++++++---- 10 files changed, 89 insertions(+), 16 deletions(-) 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 95136937575e7fbb17b4ff57111b586ddf01f1c1..c84d1209b21208910df9d1d01f01230cc29abde4 100644 GIT binary patch delta 8119 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 delta 7423 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, + /// 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}"); + } }