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 2d5576ba..20521138 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.accounting.rs b/common/src/proto/komp_ac.accounting.rs index f0278312..f7b8353a 100644 --- a/common/src/proto/komp_ac.accounting.rs +++ b/common/src/proto/komp_ac.accounting.rs @@ -56,13 +56,6 @@ pub struct PostJournalRequest { /// the latest closed or approved accounting boundary for the profile. #[prost(string, tag = "7")] pub accounting_date: ::prost::alloc::string::String, - /// 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. - #[prost(bool, tag = "8")] - pub use_custom_rate: bool, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct JournalLineInput { @@ -75,6 +68,12 @@ pub struct JournalLineInput { pub amount: ::prost::alloc::string::String, #[prost(string, tag = "4")] pub description: ::prost::alloc::string::String, + /// 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. + #[prost(message, optional, tag = "5")] + pub exchange_rate_selection: ::core::option::Option< + super::ecb::ExchangeRateSelection, + >, } #[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,