From 774c9a3ce0c073ff444e18b612af51048bcf2599 Mon Sep 17 00:00:00 2001 From: Priec Date: Thu, 13 Aug 2026 11:41:28 +0200 Subject: [PATCH] ecb conversions with corrections2 --- common/proto/accounting.proto | 46 +++++- common/proto/ecb.proto | 105 ++++++++----- common/proto/tables_data.proto | 21 +++ common/src/proto/descriptor.bin | Bin 172926 -> 178601 bytes common/src/proto/komp_ac.accounting.rs | 133 ++++++++++++++-- common/src/proto/komp_ac.ecb.rs | 160 +++++++++++++------ common/src/proto/komp_ac.tables_data.rs | 200 ++++++++++++++++++++++++ server | 2 +- web/src/lib.rs | 14 +- 9 files changed, 569 insertions(+), 112 deletions(-) diff --git a/common/proto/accounting.proto b/common/proto/accounting.proto index ae298c70..faf26c40 100644 --- a/common/proto/accounting.proto +++ b/common/proto/accounting.proto @@ -1,6 +1,8 @@ syntax = "proto3"; package komp_ac.accounting; +import "ecb.proto"; + // Mutable informational journals. A journal may contain only debits, only // credits, or any non-zero balance. Balance is reported but never enforced. // Accounting periods close date ranges inside a profile and freeze account sums @@ -36,6 +38,10 @@ service Accounting { // Soft-delete one line while retaining it for audit display. rpc SoftDeleteJournalLine(SoftDeleteJournalLineRequest) returns (Journal); + // Atomically supersede active lines and append corrected replacements. The + // originals and their conversion evidence remain immutable audit history. + rpc CorrectJournal(CorrectJournalRequest) returns (Journal); + // Create an open accounting period for a profile. Periods may be any length // (month, half-year, year). Overlapping ranges for the same profile are rejected. rpc ConfigureAccountingPeriod(ConfigureAccountingPeriodRequest) @@ -151,12 +157,6 @@ message PostJournalRequest { // the latest closed or approved accounting boundary for the profile. string accounting_date = 7; - // Convert this posting with the profile's own rate for the publication date - // instead of the ECB reference rate. False, the default, always uses ECB, even - // where a custom rate for that date exists. True requires one to have been - // entered: a missing rate is refused rather than silently converted at the - // official rate. Ignored when nothing needs converting. - bool use_custom_rate = 8; } message JournalLineInput { @@ -165,6 +165,9 @@ message JournalLineInput { string account = 2; string amount = 3; string description = 4; + // Empty uses previous publication and ECB. Set this per line when the + // accountant needs a different rule, a saved custom rate, or a one-off rate. + optional komp_ac.ecb.ExchangeRateSelection exchange_rate_selection = 5; } message CloseJournalRequest { @@ -181,8 +184,8 @@ message GetJournalRequest { string profile_name = 1; int64 journal_id = 2; - // False by default. True is reserved for a future Casbin permission granted - // only to superadmin and is rejected until that permission is implemented. + // False returns the live journal. True also returns superseded/deleted lines + // so an authorized journal reader can inspect the correction trail. bool include_deleted = 3; } @@ -276,6 +279,24 @@ message SoftDeleteJournalLineRequest { int64 journal_line_id = 3; } +message CorrectJournalRequest { + string profile_name = 1; + int64 journal_id = 2; + // One or more line replacements committed as a single correction. Batching + // lets both sides of a closed balanced journal be corrected together. + repeated JournalLineCorrection corrections = 3; +} + +message JournalLineCorrection { + int64 journal_line_id = 1; + // Full replacement line. Its amount is expressed in currency. + JournalLineInput replacement = 2; + string currency = 3; + // Required independently of any exceptional exchange-rate reason and stored + // permanently beside this original/replacement pair. + string correction_reason = 4; +} + message JournalLine { int64 id = 1; int32 line_number = 2; @@ -290,6 +311,15 @@ message JournalLine { string source_table_name = 10; int64 source_record_id = 11; int64 source_row_revision = 12; + // Populated on a replacement created by CorrectJournal. + int64 supersedes_line_id = 13; + string correction_reason = 14; + string corrected_by_user_id = 15; + // Original foreign-currency input and immutable conversion evidence. Empty/ + // zero when the line did not require conversion. + string original_amount = 16; + string original_currency = 17; + int64 conversion_evidence_id = 18; } message Journal { diff --git a/common/proto/ecb.proto b/common/proto/ecb.proto index f2f905cf..4e3a2da9 100644 --- a/common/proto/ecb.proto +++ b/common/proto/ecb.proto @@ -2,6 +2,39 @@ syntax = "proto3"; package komp_ac.ecb; +// How the publication date is selected from the accounting basis date. +enum ExchangeRateDateRule { + EXCHANGE_RATE_DATE_RULE_UNSPECIFIED = 0; + // Latest TARGET publication strictly before the basis date. + EXCHANGE_RATE_DATE_RULE_PREVIOUS_PUBLICATION = 1; + // Latest TARGET publication on or before the basis date. + EXCHANGE_RATE_DATE_RULE_ON_OR_BEFORE_DATE = 2; + // The exact publication date supplied in specific_rate_date. + EXCHANGE_RATE_DATE_RULE_SPECIFIC_PUBLICATION_DATE = 3; +} + +enum ExchangeRateSource { + EXCHANGE_RATE_SOURCE_UNSPECIFIED = 0; + EXCHANGE_RATE_SOURCE_ECB = 1; + // A reusable rate from the profile's custom_exchange_rates table. + EXCHANGE_RATE_SOURCE_SAVED_CUSTOM = 2; + // A one-off rate recorded only in this posting's immutable evidence. + EXCHANGE_RATE_SOURCE_MANUAL = 3; +} + +// Accountant-controlled conversion treatment. Exceptional choices remain +// explicit and auditable rather than being inferred from a legal label. +message ExchangeRateSelection { + ExchangeRateDateRule date_rule = 1; + ExchangeRateSource source = 2; + // Required for SPECIFIC_PUBLICATION_DATE and MANUAL; YYYY-MM-DD. + optional string specific_rate_date = 3; + // Required only for MANUAL. Exact foreign-currency units per EUR. + optional string manual_units_per_eur = 4; + // Required for SPECIFIC_PUBLICATION_DATE and MANUAL. + string reason = 5; +} + // Read-only access to ECB conversion previews and audit evidence. // // This service exists for manual visual verification before and after posting. @@ -93,34 +126,21 @@ message GetEcbPipelineStatusResponse { optional string statements_postable_through = 10; } -// Conversion basis rule used to select an ECB publication. -enum EcbConversionContext { - ECB_CONVERSION_CONTEXT_UNSPECIFIED = 0; - ECB_CONVERSION_CONTEXT_ORDINARY_TRANSACTION = 1; - ECB_CONVERSION_CONTEXT_FINANCIAL_STATEMENT = 2; - ECB_CONVERSION_CONTEXT_DECISIVE_DATE = 3; -} - // Exact inputs for a conversion preview. Decimal values are strings so the // client never loses precision through binary floating point. message PreviewEcbConversionRequest { string original_amount = 1; string original_currency = 2; - EcbConversionContext conversion_context = 3; - // ISO calendar date (YYYY-MM-DD) from which the applicable publication date - // is derived. Its meaning is selected by conversion_context: transaction, - // statement, or decisive date. - string conversion_basis_date = 4; + // ISO calendar date (YYYY-MM-DD) of the accounting case. + string conversion_basis_date = 3; - // Required. Profile whose rates apply. Only consulted when use_custom_rate is - // set, but always required so a preview names the books it describes. - string profile_name = 5; + // Required. Profile whose rates apply. Consulted when the selection requests + // a saved custom rate, and always required so the preview names its books. + string profile_name = 4; - // Preview the profile's own rate for the publication date rather than the ECB - // reference rate. Must match what the posting will ask for, or the preview - // describes a conversion that will not happen. - bool use_custom_rate = 6; + // Empty uses previous publication and ECB. + optional ExchangeRateSelection exchange_rate_selection = 5; } // The conversion and immutable local ECB observation that would be used now. @@ -129,26 +149,31 @@ message PreviewEcbConversionResponse { string original_amount = 1; string original_currency = 2; string eur_amount = 3; - EcbConversionContext conversion_context = 4; - string conversion_basis_date = 5; - string determination_method = 6; - string rounding_method = 7; - optional string rate_date = 8; - optional string units_per_eur = 9; - optional int64 rate_observation_id = 10; - optional string observation_hash = 11; - optional string source_payload_hash = 12; - optional string rate_fetched_at = 13; - optional int64 import_batch_id = 14; - optional string source_endpoint = 15; + string conversion_basis_date = 4; + string determination_method = 5; + string rounding_method = 6; + optional string rate_date = 7; + optional string units_per_eur = 8; + optional int64 rate_observation_id = 9; + optional string observation_hash = 10; + optional string source_payload_hash = 11; + optional string rate_fetched_at = 12; + optional int64 import_batch_id = 13; + optional string source_endpoint = 14; // Present only when determination_method is custom_rate. rate_date and // units_per_eur then describe the hand-entered row identified here, and the // ECB observation fields above are empty. - optional int64 custom_rate_table_definition_id = 16; - optional int64 custom_rate_record_id = 17; - optional int64 custom_rate_row_revision = 18; - optional string custom_rate_note = 19; + optional int64 custom_rate_table_definition_id = 15; + optional int64 custom_rate_record_id = 16; + optional int64 custom_rate_row_revision = 17; + optional string custom_rate_note = 18; + + // Accountant-supplied justification for an exceptional publication date or + // one-off manual rate. Empty for the normal/default rule. + ExchangeRateDateRule applied_date_rule = 19; + ExchangeRateSource applied_source = 20; + string selection_reason = 21; } // Identify one logical money cell whose conversion history should be inspected. @@ -203,11 +228,15 @@ message EcbConversionEvidence { optional int64 custom_rate_record_id = 20; optional int64 custom_rate_row_revision = 21; optional string custom_rate_note = 22; + + // Immutable audit data for an accountant-controlled exception. + string selection_reason = 23; + optional string selected_by_user_id = 24; } message ListEcbConversionEvidenceResponse { - // True when at least one evidence row for this cell used a rate, whether it - // came from ECB or from a hand-entered custom row. + // True when at least one evidence row for this cell used an ECB, saved + // custom, or one-off manual rate. bool has_exchange = 1; // Newest-first immutable history for this page. diff --git a/common/proto/tables_data.proto b/common/proto/tables_data.proto index 39abd93a..ba453903 100644 --- a/common/proto/tables_data.proto +++ b/common/proto/tables_data.proto @@ -3,6 +3,7 @@ syntax = "proto3"; package komp_ac.tables_data; import "common.proto"; +import "ecb.proto"; import "google/protobuf/struct.proto"; // Read and write row data for user-defined tables inside profiles (schemas). @@ -23,6 +24,11 @@ service TablesData { // - If the physical table is missing but the definition exists, returns INTERNAL rpc PostTableData(PostTableDataRequest) returns (PostTableDataResponse); + // Insert an ACCOUNTING-enabled source row with an explicit exchange-rate + // treatment for the journal line created from it. + rpc PostAccountingTableData(PostAccountingTableDataRequest) + returns (PostTableDataResponse); + // Insert multiple rows by applying PostTableData behavior to each row. // // Behavior: @@ -46,6 +52,8 @@ service TablesData { // - Binds values with correct SQL types; rejects invalid formats/ranges // - Updates the row and returns the id; queues search indexing (best effort) rpc PutTableData(PutTableDataRequest) returns (PutTableDataResponse); + rpc PutAccountingTableData(PutAccountingTableDataRequest) + returns (PutTableDataResponse); // Performs a PUT after the user explicitly accepted changing a referenced version. rpc PutTableDataConfirmed(PutTableDataConfirmedRequest) returns (PutTableDataResponse); @@ -139,6 +147,11 @@ message PostTableDataRequest { map data = 3; } +message PostAccountingTableDataRequest { + PostTableDataRequest row = 1; + komp_ac.ecb.ExchangeRateSelection exchange_rate_selection = 2; +} + // Insert response. message PostTableDataResponse { // True if the insert succeeded. @@ -210,6 +223,14 @@ message PutTableDataRequest { // Required. Revision returned when this row was loaded or last saved. int64 expected_revision = 5; + +} + +message PutAccountingTableDataRequest { + PutTableDataRequest update = 1; + komp_ac.ecb.ExchangeRateSelection exchange_rate_selection = 2; + bool confirmed = 3; + repeated string expected_affected_profiles = 4; } message PutTableDataConfirmedRequest { diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 2d5576ba722bd24c7719729ed5bae3f38d81ed2d..20521138e46403cde7cb030a9cdc74a3ce85423a 100644 GIT binary patch delta 32318 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 V<2;{#3sO8H5X$pN#B^Wg{y*R86qo=2 delta 27239 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, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct CloseJournalRequest { @@ -96,8 +95,8 @@ pub struct GetJournalRequest { pub profile_name: ::prost::alloc::string::String, #[prost(int64, tag = "2")] pub journal_id: i64, - /// False by default. True is reserved for a future Casbin permission granted - /// only to superadmin and is rejected until that permission is implemented. + /// False returns the live journal. True also returns superseded/deleted lines + /// so an authorized journal reader can inspect the correction trail. #[prost(bool, tag = "3")] pub include_deleted: bool, } @@ -229,6 +228,31 @@ pub struct SoftDeleteJournalLineRequest { #[prost(int64, tag = "3")] pub journal_line_id: i64, } +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CorrectJournalRequest { + #[prost(string, tag = "1")] + pub profile_name: ::prost::alloc::string::String, + #[prost(int64, tag = "2")] + pub journal_id: i64, + /// One or more line replacements committed as a single correction. Batching + /// lets both sides of a closed balanced journal be corrected together. + #[prost(message, repeated, tag = "3")] + pub corrections: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct JournalLineCorrection { + #[prost(int64, tag = "1")] + pub journal_line_id: i64, + /// Full replacement line. Its amount is expressed in currency. + #[prost(message, optional, tag = "2")] + pub replacement: ::core::option::Option, + #[prost(string, tag = "3")] + pub currency: ::prost::alloc::string::String, + /// Required independently of any exceptional exchange-rate reason and stored + /// permanently beside this original/replacement pair. + #[prost(string, tag = "4")] + pub correction_reason: ::prost::alloc::string::String, +} #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct JournalLine { #[prost(int64, tag = "1")] @@ -256,6 +280,21 @@ pub struct JournalLine { pub source_record_id: i64, #[prost(int64, tag = "12")] pub source_row_revision: i64, + /// Populated on a replacement created by CorrectJournal. + #[prost(int64, tag = "13")] + pub supersedes_line_id: i64, + #[prost(string, tag = "14")] + pub correction_reason: ::prost::alloc::string::String, + #[prost(string, tag = "15")] + pub corrected_by_user_id: ::prost::alloc::string::String, + /// Original foreign-currency input and immutable conversion evidence. Empty/ + /// zero when the line did not require conversion. + #[prost(string, tag = "16")] + pub original_amount: ::prost::alloc::string::String, + #[prost(string, tag = "17")] + pub original_currency: ::prost::alloc::string::String, + #[prost(int64, tag = "18")] + pub conversion_evidence_id: i64, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct Journal { @@ -1027,6 +1066,31 @@ pub mod accounting_client { ); self.inner.unary(req, path, codec).await } + /// Atomically supersede active lines and append corrected replacements. The + /// originals and their conversion evidence remain immutable audit history. + pub async fn correct_journal( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.accounting.Accounting/CorrectJournal", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("komp_ac.accounting.Accounting", "CorrectJournal"), + ); + self.inner.unary(req, path, codec).await + } /// Create an open accounting period for a profile. Periods may be any length /// (month, half-year, year). Overlapping ranges for the same profile are rejected. pub async fn configure_accounting_period( @@ -1456,6 +1520,12 @@ pub mod accounting_server { &self, request: tonic::Request, ) -> std::result::Result, tonic::Status>; + /// Atomically supersede active lines and append corrected replacements. The + /// originals and their conversion evidence remain immutable audit history. + async fn correct_journal( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; /// Create an open accounting period for a profile. Periods may be any length /// (month, half-year, year). Overlapping ranges for the same profile are rejected. async fn configure_accounting_period( @@ -2037,6 +2107,51 @@ pub mod accounting_server { }; Box::pin(fut) } + "/komp_ac.accounting.Accounting/CorrectJournal" => { + #[allow(non_camel_case_types)] + struct CorrectJournalSvc(pub Arc); + impl< + T: Accounting, + > tonic::server::UnaryService + for CorrectJournalSvc { + type Response = super::Journal; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::correct_journal(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = CorrectJournalSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } "/komp_ac.accounting.Accounting/ConfigureAccountingPeriod" => { #[allow(non_camel_case_types)] struct ConfigureAccountingPeriodSvc(pub Arc); diff --git a/common/src/proto/komp_ac.ecb.rs b/common/src/proto/komp_ac.ecb.rs index 03cd6f6a..c801661a 100644 --- a/common/src/proto/komp_ac.ecb.rs +++ b/common/src/proto/komp_ac.ecb.rs @@ -1,4 +1,22 @@ // This file is @generated by prost-build. +/// Accountant-controlled conversion treatment. Exceptional choices remain +/// explicit and auditable rather than being inferred from a legal label. +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ExchangeRateSelection { + #[prost(enumeration = "ExchangeRateDateRule", tag = "1")] + pub date_rule: i32, + #[prost(enumeration = "ExchangeRateSource", tag = "2")] + pub source: i32, + /// Required for SPECIFIC_PUBLICATION_DATE and MANUAL; YYYY-MM-DD. + #[prost(string, optional, tag = "3")] + pub specific_rate_date: ::core::option::Option<::prost::alloc::string::String>, + /// Required only for MANUAL. Exact foreign-currency units per EUR. + #[prost(string, optional, tag = "4")] + pub manual_units_per_eur: ::core::option::Option<::prost::alloc::string::String>, + /// Required for SPECIFIC_PUBLICATION_DATE and MANUAL. + #[prost(string, tag = "5")] + pub reason: ::prost::alloc::string::String, +} #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetEcbPipelineStatusRequest { /// Optional. How many recent import attempts to return. Zero uses 20; the @@ -94,22 +112,16 @@ pub struct PreviewEcbConversionRequest { pub original_amount: ::prost::alloc::string::String, #[prost(string, tag = "2")] pub original_currency: ::prost::alloc::string::String, - #[prost(enumeration = "EcbConversionContext", tag = "3")] - pub conversion_context: i32, - /// ISO calendar date (YYYY-MM-DD) from which the applicable publication date - /// is derived. Its meaning is selected by conversion_context: transaction, - /// statement, or decisive date. - #[prost(string, tag = "4")] + /// ISO calendar date (YYYY-MM-DD) of the accounting case. + #[prost(string, tag = "3")] pub conversion_basis_date: ::prost::alloc::string::String, - /// Required. Profile whose rates apply. Only consulted when use_custom_rate is - /// set, but always required so a preview names the books it describes. - #[prost(string, tag = "5")] + /// Required. Profile whose rates apply. Consulted when the selection requests + /// a saved custom rate, and always required so the preview names its books. + #[prost(string, tag = "4")] pub profile_name: ::prost::alloc::string::String, - /// Preview the profile's own rate for the publication date rather than the ECB - /// reference rate. Must match what the posting will ask for, or the preview - /// describes a conversion that will not happen. - #[prost(bool, tag = "6")] - pub use_custom_rate: bool, + /// Empty uses previous publication and ECB. + #[prost(message, optional, tag = "5")] + pub exchange_rate_selection: ::core::option::Option, } /// The conversion and immutable local ECB observation that would be used now. /// No rounding is performed and no conversion evidence is persisted. @@ -121,41 +133,47 @@ pub struct PreviewEcbConversionResponse { pub original_currency: ::prost::alloc::string::String, #[prost(string, tag = "3")] pub eur_amount: ::prost::alloc::string::String, - #[prost(enumeration = "EcbConversionContext", tag = "4")] - pub conversion_context: i32, - #[prost(string, tag = "5")] + #[prost(string, tag = "4")] pub conversion_basis_date: ::prost::alloc::string::String, - #[prost(string, tag = "6")] + #[prost(string, tag = "5")] pub determination_method: ::prost::alloc::string::String, - #[prost(string, tag = "7")] + #[prost(string, tag = "6")] pub rounding_method: ::prost::alloc::string::String, - #[prost(string, optional, tag = "8")] + #[prost(string, optional, tag = "7")] pub rate_date: ::core::option::Option<::prost::alloc::string::String>, - #[prost(string, optional, tag = "9")] + #[prost(string, optional, tag = "8")] pub units_per_eur: ::core::option::Option<::prost::alloc::string::String>, - #[prost(int64, optional, tag = "10")] + #[prost(int64, optional, tag = "9")] pub rate_observation_id: ::core::option::Option, - #[prost(string, optional, tag = "11")] + #[prost(string, optional, tag = "10")] pub observation_hash: ::core::option::Option<::prost::alloc::string::String>, - #[prost(string, optional, tag = "12")] + #[prost(string, optional, tag = "11")] pub source_payload_hash: ::core::option::Option<::prost::alloc::string::String>, - #[prost(string, optional, tag = "13")] + #[prost(string, optional, tag = "12")] pub rate_fetched_at: ::core::option::Option<::prost::alloc::string::String>, - #[prost(int64, optional, tag = "14")] + #[prost(int64, optional, tag = "13")] pub import_batch_id: ::core::option::Option, - #[prost(string, optional, tag = "15")] + #[prost(string, optional, tag = "14")] pub source_endpoint: ::core::option::Option<::prost::alloc::string::String>, /// Present only when determination_method is custom_rate. rate_date and /// units_per_eur then describe the hand-entered row identified here, and the /// ECB observation fields above are empty. - #[prost(int64, optional, tag = "16")] + #[prost(int64, optional, tag = "15")] pub custom_rate_table_definition_id: ::core::option::Option, - #[prost(int64, optional, tag = "17")] + #[prost(int64, optional, tag = "16")] pub custom_rate_record_id: ::core::option::Option, - #[prost(int64, optional, tag = "18")] + #[prost(int64, optional, tag = "17")] pub custom_rate_row_revision: ::core::option::Option, - #[prost(string, optional, tag = "19")] + #[prost(string, optional, tag = "18")] pub custom_rate_note: ::core::option::Option<::prost::alloc::string::String>, + /// Accountant-supplied justification for an exceptional publication date or + /// one-off manual rate. Empty for the normal/default rule. + #[prost(enumeration = "ExchangeRateDateRule", tag = "19")] + pub applied_date_rule: i32, + #[prost(enumeration = "ExchangeRateSource", tag = "20")] + pub applied_source: i32, + #[prost(string, tag = "21")] + pub selection_reason: ::prost::alloc::string::String, } /// Identify one logical money cell whose conversion history should be inspected. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] @@ -231,11 +249,16 @@ pub struct EcbConversionEvidence { pub custom_rate_row_revision: ::core::option::Option, #[prost(string, optional, tag = "22")] pub custom_rate_note: ::core::option::Option<::prost::alloc::string::String>, + /// Immutable audit data for an accountant-controlled exception. + #[prost(string, tag = "23")] + pub selection_reason: ::prost::alloc::string::String, + #[prost(string, optional, tag = "24")] + pub selected_by_user_id: ::core::option::Option<::prost::alloc::string::String>, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct ListEcbConversionEvidenceResponse { - /// True when at least one evidence row for this cell used a rate, whether it - /// came from ECB or from a hand-entered custom row. + /// True when at least one evidence row for this cell used an ECB, saved + /// custom, or one-off manual rate. #[prost(bool, tag = "1")] pub has_exchange: bool, /// Newest-first immutable history for this page. @@ -245,39 +268,78 @@ pub struct ListEcbConversionEvidenceResponse { #[prost(bool, tag = "3")] pub has_more: bool, } -/// Conversion basis rule used to select an ECB publication. +/// How the publication date is selected from the accounting basis date. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] -pub enum EcbConversionContext { +pub enum ExchangeRateDateRule { Unspecified = 0, - OrdinaryTransaction = 1, - FinancialStatement = 2, - DecisiveDate = 3, + /// Latest TARGET publication strictly before the basis date. + PreviousPublication = 1, + /// Latest TARGET publication on or before the basis date. + OnOrBeforeDate = 2, + /// The exact publication date supplied in specific_rate_date. + SpecificPublicationDate = 3, } -impl EcbConversionContext { +impl ExchangeRateDateRule { /// String value of the enum field names used in the ProtoBuf definition. /// /// The values are not transformed in any way and thus are considered stable /// (if the ProtoBuf definition does not change) and safe for programmatic use. pub fn as_str_name(&self) -> &'static str { match self { - Self::Unspecified => "ECB_CONVERSION_CONTEXT_UNSPECIFIED", - Self::OrdinaryTransaction => "ECB_CONVERSION_CONTEXT_ORDINARY_TRANSACTION", - Self::FinancialStatement => "ECB_CONVERSION_CONTEXT_FINANCIAL_STATEMENT", - Self::DecisiveDate => "ECB_CONVERSION_CONTEXT_DECISIVE_DATE", + Self::Unspecified => "EXCHANGE_RATE_DATE_RULE_UNSPECIFIED", + Self::PreviousPublication => "EXCHANGE_RATE_DATE_RULE_PREVIOUS_PUBLICATION", + Self::OnOrBeforeDate => "EXCHANGE_RATE_DATE_RULE_ON_OR_BEFORE_DATE", + Self::SpecificPublicationDate => { + "EXCHANGE_RATE_DATE_RULE_SPECIFIC_PUBLICATION_DATE" + } } } /// Creates an enum from field names used in the ProtoBuf definition. pub fn from_str_name(value: &str) -> ::core::option::Option { match value { - "ECB_CONVERSION_CONTEXT_UNSPECIFIED" => Some(Self::Unspecified), - "ECB_CONVERSION_CONTEXT_ORDINARY_TRANSACTION" => { - Some(Self::OrdinaryTransaction) + "EXCHANGE_RATE_DATE_RULE_UNSPECIFIED" => Some(Self::Unspecified), + "EXCHANGE_RATE_DATE_RULE_PREVIOUS_PUBLICATION" => { + Some(Self::PreviousPublication) } - "ECB_CONVERSION_CONTEXT_FINANCIAL_STATEMENT" => { - Some(Self::FinancialStatement) + "EXCHANGE_RATE_DATE_RULE_ON_OR_BEFORE_DATE" => Some(Self::OnOrBeforeDate), + "EXCHANGE_RATE_DATE_RULE_SPECIFIC_PUBLICATION_DATE" => { + Some(Self::SpecificPublicationDate) } - "ECB_CONVERSION_CONTEXT_DECISIVE_DATE" => Some(Self::DecisiveDate), + _ => None, + } + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ExchangeRateSource { + Unspecified = 0, + Ecb = 1, + /// A reusable rate from the profile's custom_exchange_rates table. + SavedCustom = 2, + /// A one-off rate recorded only in this posting's immutable evidence. + Manual = 3, +} +impl ExchangeRateSource { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "EXCHANGE_RATE_SOURCE_UNSPECIFIED", + Self::Ecb => "EXCHANGE_RATE_SOURCE_ECB", + Self::SavedCustom => "EXCHANGE_RATE_SOURCE_SAVED_CUSTOM", + Self::Manual => "EXCHANGE_RATE_SOURCE_MANUAL", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "EXCHANGE_RATE_SOURCE_UNSPECIFIED" => Some(Self::Unspecified), + "EXCHANGE_RATE_SOURCE_ECB" => Some(Self::Ecb), + "EXCHANGE_RATE_SOURCE_SAVED_CUSTOM" => Some(Self::SavedCustom), + "EXCHANGE_RATE_SOURCE_MANUAL" => Some(Self::Manual), _ => None, } } diff --git a/common/src/proto/komp_ac.tables_data.rs b/common/src/proto/komp_ac.tables_data.rs index c552f9be..4475787c 100644 --- a/common/src/proto/komp_ac.tables_data.rs +++ b/common/src/proto/komp_ac.tables_data.rs @@ -48,6 +48,15 @@ pub struct PostTableDataRequest { ::prost_types::Value, >, } +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PostAccountingTableDataRequest { + #[prost(message, optional, tag = "1")] + pub row: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub exchange_rate_selection: ::core::option::Option< + super::ecb::ExchangeRateSelection, + >, +} /// Insert response. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct PostTableDataResponse { @@ -134,6 +143,21 @@ pub struct PutTableDataRequest { pub expected_revision: i64, } #[derive(Clone, PartialEq, ::prost::Message)] +pub struct PutAccountingTableDataRequest { + #[prost(message, optional, tag = "1")] + pub update: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub exchange_rate_selection: ::core::option::Option< + super::ecb::ExchangeRateSelection, + >, + #[prost(bool, tag = "3")] + pub confirmed: bool, + #[prost(string, repeated, tag = "4")] + pub expected_affected_profiles: ::prost::alloc::vec::Vec< + ::prost::alloc::string::String, + >, +} +#[derive(Clone, PartialEq, ::prost::Message)] pub struct PutTableDataConfirmedRequest { #[prost(message, optional, tag = "1")] pub update: ::core::option::Option, @@ -517,6 +541,37 @@ pub mod tables_data_client { ); self.inner.unary(req, path, codec).await } + /// Insert an ACCOUNTING-enabled source row with an explicit exchange-rate + /// treatment for the journal line created from it. + pub async fn post_accounting_table_data( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.tables_data.TablesData/PostAccountingTableData", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.tables_data.TablesData", + "PostAccountingTableData", + ), + ); + self.inner.unary(req, path, codec).await + } /// Insert multiple rows by applying PostTableData behavior to each row. /// /// Behavior: @@ -594,6 +649,35 @@ pub mod tables_data_client { ); self.inner.unary(req, path, codec).await } + pub async fn put_accounting_table_data( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.tables_data.TablesData/PutAccountingTableData", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.tables_data.TablesData", + "PutAccountingTableData", + ), + ); + self.inner.unary(req, path, codec).await + } /// Performs a PUT after the user explicitly accepted changing a referenced version. pub async fn put_table_data_confirmed( &mut self, @@ -888,6 +972,15 @@ pub mod tables_data_server { tonic::Response, tonic::Status, >; + /// Insert an ACCOUNTING-enabled source row with an explicit exchange-rate + /// treatment for the journal line created from it. + async fn post_accounting_table_data( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; /// Insert multiple rows by applying PostTableData behavior to each row. /// /// Behavior: @@ -924,6 +1017,13 @@ pub mod tables_data_server { tonic::Response, tonic::Status, >; + async fn put_accounting_table_data( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; /// Performs a PUT after the user explicitly accepted changing a referenced version. async fn put_table_data_confirmed( &self, @@ -1145,6 +1245,57 @@ pub mod tables_data_server { }; Box::pin(fut) } + "/komp_ac.tables_data.TablesData/PostAccountingTableData" => { + #[allow(non_camel_case_types)] + struct PostAccountingTableDataSvc(pub Arc); + impl< + T: TablesData, + > tonic::server::UnaryService + for PostAccountingTableDataSvc { + type Response = super::PostTableDataResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + super::PostAccountingTableDataRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::post_accounting_table_data( + &inner, + request, + ) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = PostAccountingTableDataSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } "/komp_ac.tables_data.TablesData/PostTableDataBulk" => { #[allow(non_camel_case_types)] struct PostTableDataBulkSvc(pub Arc); @@ -1236,6 +1387,55 @@ pub mod tables_data_server { }; Box::pin(fut) } + "/komp_ac.tables_data.TablesData/PutAccountingTableData" => { + #[allow(non_camel_case_types)] + struct PutAccountingTableDataSvc(pub Arc); + impl< + T: TablesData, + > tonic::server::UnaryService + for PutAccountingTableDataSvc { + type Response = super::PutTableDataResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::put_accounting_table_data( + &inner, + request, + ) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = PutAccountingTableDataSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } "/komp_ac.tables_data.TablesData/PutTableDataConfirmed" => { #[allow(non_camel_case_types)] struct PutTableDataConfirmedSvc(pub Arc); diff --git a/server b/server index 09b145f9..2e88284d 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 09b145f9e37e84e349012852aff2581a0145c23e +Subproject commit 2e88284dd3a721ebe6e187f17b5c6c3ccadcc505 diff --git a/web/src/lib.rs b/web/src/lib.rs index aa54fd25..447d91b8 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -68,17 +68,17 @@ mod definitions { "/../common/src/proto/komp_ac.tables_data.rs" )); } -} - -pub(crate) mod ecb { - include!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/../common/src/proto/komp_ac.ecb.rs" - )); + pub mod ecb { + include!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../common/src/proto/komp_ac.ecb.rs" + )); + } } use auth::auth_service_client::AuthServiceClient; use definitions::{ + ecb, table_definition::table_definition_client::TableDefinitionClient, table_script::table_script_client::TableScriptClient, table_structure::table_structure_service_client::TableStructureServiceClient,