conversions7

This commit is contained in:
Filipriec
2026-08-23 22:28:53 +02:00
parent 6666f7a5b1
commit 8dacb981bf
15 changed files with 75 additions and 47 deletions

2
client

Submodule client updated: b36ef1decb...9dbcd82f37

View File

@@ -22,6 +22,10 @@ message ExchangeRateSelection {
optional string specific_rate_date = 3; optional string specific_rate_date = 3;
optional string manual_foreign_units = 4; optional string manual_foreign_units = 4;
string reason = 5; string reason = 5;
// Omit to use the profile's current default source for the currency.
// An explicit different source must be enabled for the same direct pair and
// is recorded as an audited per-event override.
optional string rate_source_id = 6;
} }
service ExchangeRateService { service ExchangeRateService {
@@ -31,7 +35,7 @@ service ExchangeRateService {
returns (ListConversionEvidenceResponse); returns (ListConversionEvidenceResponse);
rpc GetProfileExchangeRateSettings(GetProfileExchangeRateSettingsRequest) rpc GetProfileExchangeRateSettings(GetProfileExchangeRateSettingsRequest)
returns (ProfileExchangeRateSettings); returns (ProfileExchangeRateSettings);
rpc AddProfileForeignCurrency(AddProfileForeignCurrencyRequest) rpc AddProfileCurrencySource(AddProfileCurrencySourceRequest)
returns (ProfileForeignCurrency); returns (ProfileForeignCurrency);
} }
@@ -121,6 +125,12 @@ message GetProfileExchangeRateSettingsRequest { string profile_name = 1; }
message ProfileForeignCurrency { message ProfileForeignCurrency {
string currency = 1; string currency = 1;
string default_source_id = 2;
repeated ProfileCurrencySource sources = 3;
}
message ProfileCurrencySource {
string source_id = 1;
bool coverage_complete = 2; bool coverage_complete = 2;
optional string verified_from_date = 3; optional string verified_from_date = 3;
optional string verified_through_date = 4; optional string verified_through_date = 4;
@@ -129,11 +139,13 @@ message ProfileForeignCurrency {
message ProfileExchangeRateSettings { message ProfileExchangeRateSettings {
string profile_name = 1; string profile_name = 1;
string accounting_currency = 2; string accounting_currency = 2;
string rate_source_id = 3; repeated string enabled_source_ids = 3;
repeated ProfileForeignCurrency foreign_currencies = 4; repeated ProfileForeignCurrency foreign_currencies = 4;
} }
message AddProfileForeignCurrencyRequest { message AddProfileCurrencySourceRequest {
string profile_name = 1; string profile_name = 1;
string currency = 2; string currency = 2;
string source_id = 3;
bool make_default = 4;
} }

View File

@@ -104,8 +104,8 @@ message PostTableDefinitionRequest {
// to this one when they reach the ledger. // to this one when they reach the ledger.
string accounting_currency = 8; string accounting_currency = 8;
// Compiled direct-rate provider selected when the profile is created. // Initial compiled direct-rate provider. It is enabled for each initial
// The server validates this identifier against its compiled provider registry. // foreign currency below; more provider/currency bindings may be appended later.
string rate_source_id = 11; string rate_source_id = 11;
// Append-only foreign currencies initially enabled for this profile. // Append-only foreign currencies initially enabled for this profile.

Binary file not shown.

View File

@@ -11,6 +11,11 @@ pub struct ExchangeRateSelection {
pub manual_foreign_units: ::core::option::Option<::prost::alloc::string::String>, pub manual_foreign_units: ::core::option::Option<::prost::alloc::string::String>,
#[prost(string, tag = "5")] #[prost(string, tag = "5")]
pub reason: ::prost::alloc::string::String, pub reason: ::prost::alloc::string::String,
/// Omit to use the profile's current default source for the currency.
/// An explicit different source must be enabled for the same direct pair and
/// is recorded as an audited per-event override.
#[prost(string, optional, tag = "6")]
pub rate_source_id: ::core::option::Option<::prost::alloc::string::String>,
} }
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct PreviewDirectConversionRequest { pub struct PreviewDirectConversionRequest {
@@ -166,10 +171,19 @@ pub struct GetProfileExchangeRateSettingsRequest {
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String, pub profile_name: ::prost::alloc::string::String,
} }
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProfileForeignCurrency { pub struct ProfileForeignCurrency {
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
pub currency: ::prost::alloc::string::String, pub currency: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub default_source_id: ::prost::alloc::string::String,
#[prost(message, repeated, tag = "3")]
pub sources: ::prost::alloc::vec::Vec<ProfileCurrencySource>,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct ProfileCurrencySource {
#[prost(string, tag = "1")]
pub source_id: ::prost::alloc::string::String,
#[prost(bool, tag = "2")] #[prost(bool, tag = "2")]
pub coverage_complete: bool, pub coverage_complete: bool,
#[prost(string, optional, tag = "3")] #[prost(string, optional, tag = "3")]
@@ -183,17 +197,21 @@ pub struct ProfileExchangeRateSettings {
pub profile_name: ::prost::alloc::string::String, pub profile_name: ::prost::alloc::string::String,
#[prost(string, tag = "2")] #[prost(string, tag = "2")]
pub accounting_currency: ::prost::alloc::string::String, pub accounting_currency: ::prost::alloc::string::String,
#[prost(string, tag = "3")] #[prost(string, repeated, tag = "3")]
pub rate_source_id: ::prost::alloc::string::String, pub enabled_source_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
#[prost(message, repeated, tag = "4")] #[prost(message, repeated, tag = "4")]
pub foreign_currencies: ::prost::alloc::vec::Vec<ProfileForeignCurrency>, pub foreign_currencies: ::prost::alloc::vec::Vec<ProfileForeignCurrency>,
} }
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct AddProfileForeignCurrencyRequest { pub struct AddProfileCurrencySourceRequest {
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String, pub profile_name: ::prost::alloc::string::String,
#[prost(string, tag = "2")] #[prost(string, tag = "2")]
pub currency: ::prost::alloc::string::String, pub currency: ::prost::alloc::string::String,
#[prost(string, tag = "3")]
pub source_id: ::prost::alloc::string::String,
#[prost(bool, tag = "4")]
pub make_default: bool,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)] #[repr(i32)]
@@ -445,9 +463,9 @@ pub mod exchange_rate_service_client {
); );
self.inner.unary(req, path, codec).await self.inner.unary(req, path, codec).await
} }
pub async fn add_profile_foreign_currency( pub async fn add_profile_currency_source(
&mut self, &mut self,
request: impl tonic::IntoRequest<super::AddProfileForeignCurrencyRequest>, request: impl tonic::IntoRequest<super::AddProfileCurrencySourceRequest>,
) -> std::result::Result< ) -> std::result::Result<
tonic::Response<super::ProfileForeignCurrency>, tonic::Response<super::ProfileForeignCurrency>,
tonic::Status, tonic::Status,
@@ -462,14 +480,14 @@ pub mod exchange_rate_service_client {
})?; })?;
let codec = tonic_prost::ProstCodec::default(); let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static( let path = http::uri::PathAndQuery::from_static(
"/komp_ac.exchange_rates.ExchangeRateService/AddProfileForeignCurrency", "/komp_ac.exchange_rates.ExchangeRateService/AddProfileCurrencySource",
); );
let mut req = request.into_request(); let mut req = request.into_request();
req.extensions_mut() req.extensions_mut()
.insert( .insert(
GrpcMethod::new( GrpcMethod::new(
"komp_ac.exchange_rates.ExchangeRateService", "komp_ac.exchange_rates.ExchangeRateService",
"AddProfileForeignCurrency", "AddProfileCurrencySource",
), ),
); );
self.inner.unary(req, path, codec).await self.inner.unary(req, path, codec).await
@@ -510,9 +528,9 @@ pub mod exchange_rate_service_server {
tonic::Response<super::ProfileExchangeRateSettings>, tonic::Response<super::ProfileExchangeRateSettings>,
tonic::Status, tonic::Status,
>; >;
async fn add_profile_foreign_currency( async fn add_profile_currency_source(
&self, &self,
request: tonic::Request<super::AddProfileForeignCurrencyRequest>, request: tonic::Request<super::AddProfileCurrencySourceRequest>,
) -> std::result::Result< ) -> std::result::Result<
tonic::Response<super::ProfileForeignCurrency>, tonic::Response<super::ProfileForeignCurrency>,
tonic::Status, tonic::Status,
@@ -750,16 +768,15 @@ pub mod exchange_rate_service_server {
}; };
Box::pin(fut) Box::pin(fut)
} }
"/komp_ac.exchange_rates.ExchangeRateService/AddProfileForeignCurrency" => { "/komp_ac.exchange_rates.ExchangeRateService/AddProfileCurrencySource" => {
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
struct AddProfileForeignCurrencySvc<T: ExchangeRateService>( struct AddProfileCurrencySourceSvc<T: ExchangeRateService>(
pub Arc<T>, pub Arc<T>,
); );
impl< impl<
T: ExchangeRateService, T: ExchangeRateService,
> tonic::server::UnaryService< > tonic::server::UnaryService<super::AddProfileCurrencySourceRequest>
super::AddProfileForeignCurrencyRequest, for AddProfileCurrencySourceSvc<T> {
> for AddProfileForeignCurrencySvc<T> {
type Response = super::ProfileForeignCurrency; type Response = super::ProfileForeignCurrency;
type Future = BoxFuture< type Future = BoxFuture<
tonic::Response<Self::Response>, tonic::Response<Self::Response>,
@@ -768,12 +785,12 @@ pub mod exchange_rate_service_server {
fn call( fn call(
&mut self, &mut self,
request: tonic::Request< request: tonic::Request<
super::AddProfileForeignCurrencyRequest, super::AddProfileCurrencySourceRequest,
>, >,
) -> Self::Future { ) -> Self::Future {
let inner = Arc::clone(&self.0); let inner = Arc::clone(&self.0);
let fut = async move { let fut = async move {
<T as ExchangeRateService>::add_profile_foreign_currency( <T as ExchangeRateService>::add_profile_currency_source(
&inner, &inner,
request, request,
) )
@@ -788,7 +805,7 @@ pub mod exchange_rate_service_server {
let max_encoding_message_size = self.max_encoding_message_size; let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone(); let inner = self.inner.clone();
let fut = async move { let fut = async move {
let method = AddProfileForeignCurrencySvc(inner); let method = AddProfileCurrencySourceSvc(inner);
let codec = tonic_prost::ProstCodec::default(); let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec) let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config( .apply_compression_config(

View File

@@ -51,8 +51,8 @@ pub struct PostTableDefinitionRequest {
/// to this one when they reach the ledger. /// to this one when they reach the ledger.
#[prost(string, tag = "8")] #[prost(string, tag = "8")]
pub accounting_currency: ::prost::alloc::string::String, pub accounting_currency: ::prost::alloc::string::String,
/// Compiled direct-rate provider selected when the profile is created. /// Initial compiled direct-rate provider. It is enabled for each initial
/// The server validates this identifier against its compiled provider registry. /// foreign currency below; more provider/currency bindings may be appended later.
#[prost(string, tag = "11")] #[prost(string, tag = "11")]
pub rate_source_id: ::prost::alloc::string::String, pub rate_source_id: ::prost::alloc::string::String,
/// Append-only foreign currencies initially enabled for this profile. /// Append-only foreign currencies initially enabled for this profile.

2
server

Submodule server updated: d21be00ac2...0a8cf005d8

View File

@@ -507,7 +507,7 @@ grants-note-all-tables = Každá tabulka v každém profilu včetně těch, kter
grants-label-all-journals = Všechny deníky grants-label-all-journals = Všechny deníky
grants-note-all-journals = Účetní deník každého profilu. grants-note-all-journals = Účetní deník každého profilu.
grants-label-all-rates = Všechny devizové kurzy grants-label-all-rates = Všechny devizové kurzy
grants-note-all-rates = Kurzy ECB importuje server, takže je lze pouze číst. grants-note-all-rates = Oficiální kurzy importuje server, takže je lze pouze číst.
grants-note-profile-tables = Každá tabulka v tomto profilu včetně těch, které přibudou později. grants-note-profile-tables = Každá tabulka v tomto profilu včetně těch, které přibudou později.
grants-label-journal = Deník grants-label-journal = Deník
grants-note-journal = Účetní deník tohoto profilu. grants-note-journal = Účetní deník tohoto profilu.
@@ -755,7 +755,7 @@ builder-choose-profile = Vyberte profil
builder-new-profile = Nový profil… builder-new-profile = Nový profil…
builder-new-profile-name = Název nového profilu builder-new-profile-name = Název nového profilu
builder-accounting-currency = Účetní měna builder-accounting-currency = Účetní měna
builder-accounting-currency-hint = Účetnictví může být vedeno v jakékoli měně, pro kterou ECB zveřejňuje referenční kurz. Jiná měna než EUR se v případě potřeby přepočítá přes EUR, protože kurzy ECB jsou vždy vyjádřeny vůči euru. builder-accounting-currency-hint = Vybraný počáteční zdroj kurzů musí zveřejňovat přímý kurz mezi touto měnou a každou počáteční cizí měnou. Další zdroje a měny lze přidat později.
builder-table-name = Název tabulky builder-table-name = Název tabulky
builder-add-column-heading = Přidat sloupec builder-add-column-heading = Přidat sloupec
builder-add-column-button = Přidat sloupec builder-add-column-button = Přidat sloupec

View File

@@ -500,7 +500,7 @@ grants-note-all-tables = Every table in every profile, including ones added late
grants-label-all-journals = All journals grants-label-all-journals = All journals
grants-note-all-journals = Every profile's accounting journal. grants-note-all-journals = Every profile's accounting journal.
grants-label-all-rates = All exchange rates grants-label-all-rates = All exchange rates
grants-note-all-rates = ECB rates are imported by the server, so they can only be read. grants-note-all-rates = Official rates are imported by the server, so they can only be read.
grants-note-profile-tables = Every table in this profile, including ones added later. grants-note-profile-tables = Every table in this profile, including ones added later.
grants-label-journal = Journal grants-label-journal = Journal
grants-note-journal = This profile's accounting journal. grants-note-journal = This profile's accounting journal.
@@ -743,7 +743,7 @@ builder-choose-profile = Choose a profile
builder-new-profile = New profile… builder-new-profile = New profile…
builder-new-profile-name = New profile name builder-new-profile-name = New profile name
builder-accounting-currency = Accounting currency builder-accounting-currency = Accounting currency
builder-accounting-currency-hint = Books can be kept in any currency ECB publishes a reference rate for. A currency other than EUR is converted through EUR when needed, since ECB rates are always quoted against the euro. builder-accounting-currency-hint = The selected initial rate source must publish a direct quote between this currency and every initial foreign currency. Additional sources and currencies can be added later.
builder-table-name = Table name builder-table-name = Table name
builder-add-column-heading = Add a column builder-add-column-heading = Add a column
builder-add-column-button = Add column builder-add-column-button = Add column

View File

@@ -507,7 +507,7 @@ grants-note-all-tables = Každá tabuľka v každom profile vrátane tých, ktor
grants-label-all-journals = Všetky denníky grants-label-all-journals = Všetky denníky
grants-note-all-journals = Účtovný denník každého profilu. grants-note-all-journals = Účtovný denník každého profilu.
grants-label-all-rates = Všetky devízové kurzy grants-label-all-rates = Všetky devízové kurzy
grants-note-all-rates = Kurzy ECB importuje server, takže ich možno len čítať. grants-note-all-rates = Oficiálne kurzy importuje server, takže ich možno len čítať.
grants-note-profile-tables = Každá tabuľka v tomto profile vrátane tých, ktoré pribudnú neskôr. grants-note-profile-tables = Každá tabuľka v tomto profile vrátane tých, ktoré pribudnú neskôr.
grants-label-journal = Denník grants-label-journal = Denník
grants-note-journal = Účtovný denník tohto profilu. grants-note-journal = Účtovný denník tohto profilu.
@@ -753,7 +753,7 @@ builder-choose-profile = Vyberte profil
builder-new-profile = Nový profil… builder-new-profile = Nový profil…
builder-new-profile-name = Názov nového profilu builder-new-profile-name = Názov nového profilu
builder-accounting-currency = Účtovná mena builder-accounting-currency = Účtovná mena
builder-accounting-currency-hint = Účtovníctvo môže byť vedené v akejkoľvek mene, pre ktorú ECB zverejňuje referenčný kurz. Iná mena ako EUR sa v prípade potreby prepočíta cez EUR, keďže kurzy ECB sú vždy vyjadrené voči euru. builder-accounting-currency-hint = Vybraný počiatočný zdroj kurzov musí zverejňovať priamy kurz medzi touto menou a každou počiatočnou cudzou menou. Ďalšie zdroje a meny možno pridať neskôr.
builder-table-name = Názov tabuľky builder-table-name = Názov tabuľky
builder-add-column-heading = Pridať stĺpec builder-add-column-heading = Pridať stĺpec
builder-add-column-button = Pridať stĺpec builder-add-column-button = Pridať stĺpec

View File

@@ -9,9 +9,8 @@ pub(crate) const STRUCT_USER: &str = "struct:user";
pub(crate) const MANAGE: &str = "manage"; pub(crate) const MANAGE: &str = "manage";
pub(crate) const READ: &str = "read"; pub(crate) const READ: &str = "read";
/// Every ECB object, which is what the pipeline status is checked against: /// Every exchange-rate object. Provider pipeline health is a global rather
/// one importer feeds every profile, so reading its health is not a /// than per-profile concern.
/// per-profile question.
pub(crate) const ALL_EXCHANGE_RATES: &str = "exchange-rates:*"; pub(crate) const ALL_EXCHANGE_RATES: &str = "exchange-rates:*";
/// The two bulk-transfer actions, mirroring /// The two bulk-transfer actions, mirroring
@@ -43,7 +42,7 @@ pub(crate) fn can_manage(snapshot: &AuthorizationSnapshot, area: &str) -> bool {
} }
/// Whether the caller may see the reference-rate pipeline. Mirrors the /// Whether the caller may see the reference-rate pipeline. Mirrors the
/// server's own check in `server/src/ecb/grpc.rs`. /// server's exchange-rate authorization check.
pub(crate) fn can_read_exchange_rates(snapshot: &AuthorizationSnapshot) -> bool { pub(crate) fn can_read_exchange_rates(snapshot: &AuthorizationSnapshot) -> bool {
permits(snapshot, ALL_EXCHANGE_RATES, READ) permits(snapshot, ALL_EXCHANGE_RATES, READ)
} }

View File

@@ -26,7 +26,7 @@ pub(crate) struct AdminPageState {
pub can_import: bool, pub can_import: bool,
pub can_export: bool, pub can_export: bool,
/// Whether the exchange-rate pipeline is visible to this caller. Not a /// Whether the exchange-rate pipeline is visible to this caller. Not a
/// structural area: it is granted through the ECB object, like the /// structural area: it is granted through the exchange-rate object, like the
/// conversions it reports on. /// conversions it reports on.
pub can_exchange_rates: bool, pub can_exchange_rates: bool,
} }

View File

@@ -181,7 +181,7 @@ fn describe(locale: Locale, object: &GrantableObject) -> (String, String) {
tr!(locale, "grants-label-all-journals"), tr!(locale, "grants-label-all-journals"),
tr!(locale, "grants-note-all-journals"), tr!(locale, "grants-note-all-journals"),
), ),
"global_ecb" => ( "global_exchange_rates" => (
tr!(locale, "grants-label-all-rates"), tr!(locale, "grants-label-all-rates"),
tr!(locale, "grants-note-all-rates"), tr!(locale, "grants-note-all-rates"),
), ),
@@ -193,7 +193,7 @@ fn describe(locale: Locale, object: &GrantableObject) -> (String, String) {
tr!(locale, "grants-label-journal"), tr!(locale, "grants-label-journal"),
tr!(locale, "grants-note-journal"), tr!(locale, "grants-note-journal"),
), ),
"ecb" => ( "exchange_rates" => (
tr!(locale, "grants-label-rates"), tr!(locale, "grants-label-rates"),
tr!(locale, "grants-note-rates"), tr!(locale, "grants-note-rates"),
), ),

View File

@@ -229,7 +229,7 @@ mod tests {
}, },
ObjectRow { ObjectRow {
object: "exchange-rates:acme".to_string(), object: "exchange-rates:acme".to_string(),
label: "ECB rates".to_string(), label: "Exchange rates".to_string(),
note: String::new(), note: String::new(),
wildcard: true, wildcard: true,
cells: cells(&["read"], &[]), cells: cells(&["read"], &[]),
@@ -249,7 +249,7 @@ mod tests {
fn a_profile_shortcut_grants_the_wildcards_and_never_an_action_the_server_refuses() { fn a_profile_shortcut_grants_the_wildcards_and_never_an_action_the_server_refuses() {
let group = group(); let group = group();
// The table row is covered by data:acme/*, so it is not granted again, // The table row is covered by data:acme/*, so it is not granted again,
// and ECB rates only ever offer read. // and official rates only ever offer read.
assert_eq!( assert_eq!(
group.all_pairs(), group.all_pairs(),
vec![ vec![

View File

@@ -71,8 +71,8 @@ impl RoleRow {
/// ///
/// These are wildcard objects on purpose: they keep covering profiles and /// These are wildcard objects on purpose: they keep covering profiles and
/// tables added later, which is what "everything" has to mean for a role /// tables added later, which is what "everything" has to mean for a role
/// created before the data exists. ECB rates are written by the server, so they /// created before the data exists. Official rates are written by the server,
/// are readable and nothing more. /// so they are readable and nothing more.
pub(crate) fn starter_grants( pub(crate) fn starter_grants(
locale: Locale, locale: Locale,
access: &str, access: &str,
@@ -110,7 +110,7 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn starter_access_never_hands_out_a_write_on_ecb_rates() { fn starter_access_never_hands_out_a_write_on_exchange_rates() {
let locale = Locale::default(); let locale = Locale::default();
assert!(starter_grants(locale, "none").unwrap().is_empty()); assert!(starter_grants(locale, "none").unwrap().is_empty());
assert!(starter_grants(locale, "nonsense").is_err()); assert!(starter_grants(locale, "nonsense").is_err());
@@ -121,12 +121,12 @@ mod tests {
grants grants
.iter() .iter()
.all(|(object, action)| *object != "exchange-rates:*" || *action == "read"), .all(|(object, action)| *object != "exchange-rates:*" || *action == "read"),
"{level} granted a write on ECB rates" "{level} granted a write on exchange rates"
); );
} }
assert_eq!(starter_grants(locale, "read").unwrap().len(), 3); assert_eq!(starter_grants(locale, "read").unwrap().len(), 3);
// Four row actions on two wildcard objects, ECB read, and the two // Four row actions on two wildcard objects, rate read, and the two
// transfer actions, which apply to tables alone. // transfer actions, which apply to tables alone.
assert_eq!(starter_grants(locale, "full").unwrap().len(), 11); assert_eq!(starter_grants(locale, "full").unwrap().len(), 11);
} }