conversions6
This commit is contained in:
2
client
2
client
Submodule client updated: 097aba8b61...b36ef1decb
@@ -165,7 +165,7 @@ message JournalLineInput {
|
||||
string account = 2;
|
||||
string amount = 3;
|
||||
string description = 4;
|
||||
// Empty uses previous publication and ECB. Set this per line when the
|
||||
// Empty uses the profile provider's previous publication. Set this per line when the
|
||||
// accountant needs a different rule, a saved custom rate, or a one-off rate.
|
||||
optional komp_ac.exchange_rates.ExchangeRateSelection exchange_rate_selection = 5;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ message PreviewDirectConversionResponse {
|
||||
optional string foreign_currency = 10;
|
||||
optional string foreign_units = 11;
|
||||
string source_id = 12;
|
||||
optional int64 rate_observation_id = 13;
|
||||
optional int64 official_observation_id = 13;
|
||||
optional string observation_hash = 14;
|
||||
optional string source_payload_hash = 15;
|
||||
optional string rate_fetched_at = 16;
|
||||
@@ -96,7 +96,7 @@ message ConversionEvidence {
|
||||
string rounding_method = 13;
|
||||
optional string rate_date = 14;
|
||||
string source_id = 15;
|
||||
optional int64 rate_observation_id = 16;
|
||||
optional int64 official_observation_id = 16;
|
||||
optional string observation_hash = 17;
|
||||
optional string source_payload_hash = 18;
|
||||
optional string rate_fetched_at = 19;
|
||||
|
||||
@@ -105,7 +105,7 @@ message PostTableDefinitionRequest {
|
||||
string accounting_currency = 8;
|
||||
|
||||
// Compiled direct-rate provider selected when the profile is created.
|
||||
// Currently only "ecb" is available, and it requires EUR accounting.
|
||||
// The server validates this identifier against its compiled provider registry.
|
||||
string rate_source_id = 11;
|
||||
|
||||
// Append-only foreign currencies initially enabled for this profile.
|
||||
|
||||
Binary file not shown.
@@ -68,7 +68,7 @@ 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
|
||||
/// Empty uses the profile provider's previous publication. 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<
|
||||
|
||||
@@ -52,7 +52,7 @@ pub struct PreviewDirectConversionResponse {
|
||||
#[prost(string, tag = "12")]
|
||||
pub source_id: ::prost::alloc::string::String,
|
||||
#[prost(int64, optional, tag = "13")]
|
||||
pub rate_observation_id: ::core::option::Option<i64>,
|
||||
pub official_observation_id: ::core::option::Option<i64>,
|
||||
#[prost(string, optional, tag = "14")]
|
||||
pub observation_hash: ::core::option::Option<::prost::alloc::string::String>,
|
||||
#[prost(string, optional, tag = "15")]
|
||||
@@ -126,7 +126,7 @@ pub struct ConversionEvidence {
|
||||
#[prost(string, tag = "15")]
|
||||
pub source_id: ::prost::alloc::string::String,
|
||||
#[prost(int64, optional, tag = "16")]
|
||||
pub rate_observation_id: ::core::option::Option<i64>,
|
||||
pub official_observation_id: ::core::option::Option<i64>,
|
||||
#[prost(string, optional, tag = "17")]
|
||||
pub observation_hash: ::core::option::Option<::prost::alloc::string::String>,
|
||||
#[prost(string, optional, tag = "18")]
|
||||
|
||||
@@ -52,7 +52,7 @@ pub struct PostTableDefinitionRequest {
|
||||
#[prost(string, tag = "8")]
|
||||
pub accounting_currency: ::prost::alloc::string::String,
|
||||
/// Compiled direct-rate provider selected when the profile is created.
|
||||
/// Currently only "ecb" is available, and it requires EUR accounting.
|
||||
/// The server validates this identifier against its compiled provider registry.
|
||||
#[prost(string, tag = "11")]
|
||||
pub rate_source_id: ::prost::alloc::string::String,
|
||||
/// Append-only foreign currencies initially enabled for this profile.
|
||||
|
||||
2
server
2
server
Submodule server updated: b929f654ed...d21be00ac2
@@ -12,7 +12,7 @@ pub(crate) const READ: &str = "read";
|
||||
/// Every ECB object, which is what the pipeline status is checked against:
|
||||
/// one importer feeds every profile, so reading its health is not a
|
||||
/// per-profile question.
|
||||
pub(crate) const ALL_ECB: &str = "ecb:*";
|
||||
pub(crate) const ALL_EXCHANGE_RATES: &str = "exchange-rates:*";
|
||||
|
||||
/// The two bulk-transfer actions, mirroring
|
||||
/// `server/src/auth/rbac/objects.rs`. Taking a whole table out as a file is
|
||||
@@ -44,8 +44,8 @@ pub(crate) fn can_manage(snapshot: &AuthorizationSnapshot, area: &str) -> bool {
|
||||
|
||||
/// Whether the caller may see the reference-rate pipeline. Mirrors the
|
||||
/// server's own check in `server/src/ecb/grpc.rs`.
|
||||
pub(crate) fn can_read_ecb(snapshot: &AuthorizationSnapshot) -> bool {
|
||||
permits(snapshot, ALL_ECB, READ)
|
||||
pub(crate) fn can_read_exchange_rates(snapshot: &AuthorizationSnapshot) -> bool {
|
||||
permits(snapshot, ALL_EXCHANGE_RATES, READ)
|
||||
}
|
||||
|
||||
/// Whether the caller holds a transfer action on any table at all — what the
|
||||
|
||||
@@ -97,8 +97,8 @@ impl TableDraft {
|
||||
/// thing that knows it.
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
accounting_currency: "EUR".to_string(),
|
||||
rate_source_id: "ecb".to_string(),
|
||||
accounting_currency: String::new(),
|
||||
rate_source_id: String::new(),
|
||||
columns: ColumnDraft::new(ColumnCatalog::default()),
|
||||
..Self::default()
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ pub(crate) async fn load_admin_page(
|
||||
can_manage_validations: crate::authz::can_manage(&authorization, crate::authz::STRUCT_VALIDATION),
|
||||
can_import: crate::authz::can_transfer_anything(&authorization, crate::authz::IMPORT),
|
||||
can_export: crate::authz::can_transfer_anything(&authorization, crate::authz::EXPORT),
|
||||
can_ecb: crate::authz::can_read_ecb(&authorization),
|
||||
can_exchange_rates: crate::authz::can_read_exchange_rates(&authorization),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ pub(crate) struct AdminPageState {
|
||||
/// Whether the exchange-rate pipeline is visible to this caller. Not a
|
||||
/// structural area: it is granted through the ECB object, like the
|
||||
/// conversions it reports on.
|
||||
pub can_ecb: bool,
|
||||
pub can_exchange_rates: bool,
|
||||
}
|
||||
|
||||
impl AdminPageState {
|
||||
|
||||
@@ -64,7 +64,7 @@ mod tests {
|
||||
can_permissions: true,
|
||||
can_import: false,
|
||||
can_export: false,
|
||||
can_ecb: false,
|
||||
can_exchange_rates: false,
|
||||
active: "admin",
|
||||
};
|
||||
let page = AdminPageState {
|
||||
@@ -80,7 +80,7 @@ mod tests {
|
||||
can_manage_validations: true,
|
||||
can_import: true,
|
||||
can_export: true,
|
||||
can_ecb: true,
|
||||
can_exchange_rates: true,
|
||||
};
|
||||
let html = render_page(&page);
|
||||
for route in [
|
||||
@@ -126,7 +126,7 @@ mod tests {
|
||||
can_manage_validations: true,
|
||||
can_import: true,
|
||||
can_export: true,
|
||||
can_ecb: true,
|
||||
can_exchange_rates: true,
|
||||
};
|
||||
|
||||
let html = render_workspace(&page);
|
||||
@@ -163,7 +163,7 @@ mod tests {
|
||||
can_manage_validations: true,
|
||||
can_import: true,
|
||||
can_export: true,
|
||||
can_ecb: true,
|
||||
can_exchange_rates: true,
|
||||
};
|
||||
|
||||
let html = render_workspace(&page);
|
||||
@@ -203,7 +203,7 @@ mod tests {
|
||||
can_manage_validations: true,
|
||||
can_import: true,
|
||||
can_export: true,
|
||||
can_ecb: true,
|
||||
can_exchange_rates: true,
|
||||
};
|
||||
|
||||
let html = render_workspace(&page);
|
||||
@@ -242,7 +242,7 @@ mod tests {
|
||||
can_manage_validations: true,
|
||||
can_import: true,
|
||||
can_export: true,
|
||||
can_ecb: true,
|
||||
can_exchange_rates: true,
|
||||
};
|
||||
|
||||
let html = render_workspace(&page);
|
||||
|
||||
@@ -37,7 +37,7 @@ pub(crate) async fn load_ecb_page(
|
||||
})?
|
||||
.into_inner();
|
||||
|
||||
if !crate::authz::can_read_ecb(&authorization) {
|
||||
if !crate::authz::can_read_exchange_rates(&authorization) {
|
||||
return Err(LoadError::Forbidden);
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ pub(crate) struct RoleOption {
|
||||
/// The objects of one profile, or the wildcards that cover every profile.
|
||||
pub(crate) struct ObjectGroup {
|
||||
pub title: String,
|
||||
/// The `data:*` / `journal:*` / `ecb:*` group, which is the one whose
|
||||
/// The `data:*` / `journal:*` / `exchange-rates:*` group, which is the one whose
|
||||
/// shortcuts mean "everything, including what is created later".
|
||||
pub global: bool,
|
||||
pub rows: Vec<ObjectRow>,
|
||||
@@ -228,7 +228,7 @@ mod tests {
|
||||
cells: cells(&ACTIONS, &["read"]),
|
||||
},
|
||||
ObjectRow {
|
||||
object: "ecb:acme".to_string(),
|
||||
object: "exchange-rates:acme".to_string(),
|
||||
label: "ECB rates".to_string(),
|
||||
note: String::new(),
|
||||
wildcard: true,
|
||||
@@ -259,12 +259,12 @@ mod tests {
|
||||
"data:acme/*|delete",
|
||||
"data:acme/*|export",
|
||||
"data:acme/*|import",
|
||||
"ecb:acme|read",
|
||||
"exchange-rates:acme|read",
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
group.read_pairs(),
|
||||
vec!["data:acme/*|read", "ecb:acme|read"]
|
||||
vec!["data:acme/*|read", "exchange-rates:acme|read"]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ mod tests {
|
||||
can_permissions: true,
|
||||
can_import: false,
|
||||
can_export: false,
|
||||
can_ecb: true,
|
||||
can_exchange_rates: true,
|
||||
active: "permissions",
|
||||
},
|
||||
tabs: Tabs {
|
||||
|
||||
@@ -82,10 +82,10 @@ pub(crate) fn starter_grants(
|
||||
"read" => Ok(vec![
|
||||
("data:*", "read"),
|
||||
("journal:*", "read"),
|
||||
("ecb:*", "read"),
|
||||
("exchange-rates:*", "read"),
|
||||
]),
|
||||
"full" => {
|
||||
let mut grants = vec![("ecb:*", "read")];
|
||||
let mut grants = vec![("exchange-rates:*", "read")];
|
||||
for object in ["data:*", "journal:*"] {
|
||||
for action in ["read", "insert", "update", "delete"] {
|
||||
grants.push((object, action));
|
||||
@@ -120,7 +120,7 @@ mod tests {
|
||||
assert!(
|
||||
grants
|
||||
.iter()
|
||||
.all(|(object, action)| *object != "ecb:*" || *action == "read"),
|
||||
.all(|(object, action)| *object != "exchange-rates:*" || *action == "read"),
|
||||
"{level} granted a write on ECB rates"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ mod tests {
|
||||
can_permissions: true,
|
||||
can_import: false,
|
||||
can_export: false,
|
||||
can_ecb: true,
|
||||
can_exchange_rates: true,
|
||||
active: "permissions",
|
||||
},
|
||||
tabs: Tabs {
|
||||
|
||||
@@ -38,7 +38,7 @@ mod tests {
|
||||
can_permissions: true,
|
||||
can_import: false,
|
||||
can_export: false,
|
||||
can_ecb: true,
|
||||
can_exchange_rates: true,
|
||||
active: "permissions",
|
||||
},
|
||||
tabs: Tabs {
|
||||
|
||||
@@ -112,7 +112,7 @@ pub(crate) struct Nav {
|
||||
pub can_permissions: bool,
|
||||
pub can_import: bool,
|
||||
pub can_export: bool,
|
||||
pub can_ecb: bool,
|
||||
pub can_exchange_rates: bool,
|
||||
pub active: &'static str,
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ impl Nav {
|
||||
crate::authz::can_transfer_anything(authorization, crate::authz::IMPORT);
|
||||
nav.can_export =
|
||||
crate::authz::can_transfer_anything(authorization, crate::authz::EXPORT);
|
||||
nav.can_ecb = crate::authz::can_read_ecb(authorization);
|
||||
nav.can_exchange_rates = crate::authz::can_read_exchange_rates(authorization);
|
||||
nav
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ impl Nav {
|
||||
can_permissions: false,
|
||||
can_import: false,
|
||||
can_export: false,
|
||||
can_ecb: false,
|
||||
can_exchange_rates: false,
|
||||
active,
|
||||
}
|
||||
}
|
||||
@@ -268,7 +268,7 @@ impl Default for Nav {
|
||||
can_permissions: false,
|
||||
can_import: false,
|
||||
can_export: false,
|
||||
can_ecb: false,
|
||||
can_exchange_rates: false,
|
||||
active: "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,18 +80,12 @@
|
||||
{% if page.draft.show_accounting_currency() %}
|
||||
<label>{{ nav.tr("builder-accounting-currency") }}
|
||||
<input name="accounting_currency" value="{{ page.draft.accounting_currency }}" list="currency-codes"
|
||||
maxlength="3" placeholder="EUR">
|
||||
{#
|
||||
The list stays complete: EUR-only books are the server's current
|
||||
limitation, not the shape of the field. When it accepts more, this
|
||||
hint goes and nothing else here has to change.
|
||||
#}
|
||||
maxlength="3" placeholder="USD">
|
||||
<small>{{ nav.tr("builder-accounting-currency-hint") }}</small>
|
||||
</label>
|
||||
<label>Rate source
|
||||
<select name="rate_source_id">
|
||||
<option value="ecb" {% if page.draft.rate_source_id == "ecb" %}selected{% endif %}>ECB</option>
|
||||
</select>
|
||||
<input name="rate_source_id" value="{{ page.draft.rate_source_id }}"
|
||||
placeholder="provider-id">
|
||||
</label>
|
||||
<label>Initial foreign currencies
|
||||
<input name="foreign_currencies" value="{{ page.draft.foreign_currencies }}"
|
||||
@@ -212,7 +206,7 @@
|
||||
{% if page.draft.columns.show_money_options() %}
|
||||
<label>{{ nav.tr("td-currency") }}
|
||||
<input name="column_currency_input" value="{{ page.draft.columns.currency_input }}" list="currency-codes"
|
||||
maxlength="3" placeholder="EUR">
|
||||
maxlength="3" placeholder="USD">
|
||||
</label>
|
||||
<label>{{ nav.tr("td-rounding") }}
|
||||
<select name="column_rounding_input">
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
{% if page.can_manage_validations %}<a href="/admin/validation/sets/new">{{ nav.tr("admin-add-rule") }}</a>{% endif %}
|
||||
{% if page.can_import %}<a href="/admin/import">{{ nav.tr("admin-import") }}</a>{% endif %}
|
||||
{% if page.can_export %}<a href="/admin/export">{{ nav.tr("admin-export") }}</a>{% endif %}
|
||||
{% if page.can_ecb %}<a href="/admin/ecb">{{ nav.tr("admin-exchange-rates") }}</a>{% endif %}
|
||||
{% if page.can_exchange_rates %}<a href="/admin/ecb">{{ nav.tr("admin-exchange-rates") }}</a>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
<div id="admin-workspace">{% include "pages/admin/admin/workspace.html" %}</div>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
{% if nav.can_admin %}<li><a href="/admin" class="{% if nav.active == "admin" %}font-bold text-primary dark:text-primary-dark{% else %}font-medium text-on-surface dark:text-on-surface-dark dark:hover:text-primary-dark{% endif %} underline-offset-2 hover:text-primary focus:outline-hidden focus:underline" {% if nav.active == "admin" %}aria-current="page"{% endif %}>{{ nav.tr("nav-admin") }}</a></li>{% endif %}
|
||||
{% if nav.can_permissions %}<li><a href="/permissions" class="{% if nav.active == "permissions" %}font-bold text-primary dark:text-primary-dark{% else %}font-medium text-on-surface dark:text-on-surface-dark dark:hover:text-primary-dark{% endif %} underline-offset-2 hover:text-primary focus:outline-hidden focus:underline" {% if nav.active == "permissions" %}aria-current="page"{% endif %}>{{ nav.tr("nav-permissions") }}</a></li>{% endif %}
|
||||
<li><a href="/" class="{% if nav.active == "analytics" %}font-bold text-primary dark:text-primary-dark{% else %}font-medium text-on-surface dark:text-on-surface-dark dark:hover:text-primary-dark{% endif %} underline-offset-2 hover:text-primary focus:outline-hidden focus:underline" {% if nav.active == "analytics" %}aria-current="page"{% endif %}>{{ nav.tr("nav-analytics") }}</a></li>
|
||||
{% if nav.can_ecb %}<li><a href="/admin/ecb" class="{% if nav.active == "ecb" %}font-bold text-primary dark:text-primary-dark{% else %}font-medium text-on-surface dark:text-on-surface-dark dark:hover:text-primary-dark{% endif %} underline-offset-2 hover:text-primary focus:outline-hidden focus:underline" {% if nav.active == "ecb" %}aria-current="page"{% endif %}>{{ nav.tr("nav-rates") }}</a></li>{% endif %}
|
||||
{% if nav.can_exchange_rates %}<li><a href="/admin/ecb" class="{% if nav.active == "ecb" %}font-bold text-primary dark:text-primary-dark{% else %}font-medium text-on-surface dark:text-on-surface-dark dark:hover:text-primary-dark{% endif %} underline-offset-2 hover:text-primary focus:outline-hidden focus:underline" {% if nav.active == "ecb" %}aria-current="page"{% endif %}>{{ nav.tr("nav-rates") }}</a></li>{% endif %}
|
||||
{% if nav.can_import %}<li><a href="/admin/import" class="font-medium text-on-surface underline-offset-2 hover:text-primary focus:outline-hidden focus:underline dark:text-on-surface-dark dark:hover:text-primary-dark">{{ nav.tr("nav-import") }}</a></li>{% endif %}
|
||||
{% if nav.can_export %}<li><a href="/admin/export" class="font-medium text-on-surface underline-offset-2 hover:text-primary focus:outline-hidden focus:underline dark:text-on-surface-dark dark:hover:text-primary-dark">{{ nav.tr("nav-export") }}</a></li>{% endif %}
|
||||
{% if nav.authenticated %}
|
||||
@@ -51,7 +51,7 @@
|
||||
{% if nav.can_admin %}<li class="py-4"><a href="/admin" class="w-full text-lg {% if nav.active == "admin" %}font-bold text-primary dark:text-primary-dark{% else %}font-medium text-on-surface dark:text-on-surface-dark{% endif %} focus:underline" {% if nav.active == "admin" %}aria-current="page"{% endif %}>{{ nav.tr("nav-admin") }}</a></li>{% endif %}
|
||||
{% if nav.can_permissions %}<li class="py-4"><a href="/permissions" class="w-full text-lg {% if nav.active == "permissions" %}font-bold text-primary dark:text-primary-dark{% else %}font-medium text-on-surface dark:text-on-surface-dark{% endif %} focus:underline" {% if nav.active == "permissions" %}aria-current="page"{% endif %}>{{ nav.tr("nav-permissions") }}</a></li>{% endif %}
|
||||
<li class="py-4"><a href="/" class="w-full text-lg {% if nav.active == "analytics" %}font-bold text-primary dark:text-primary-dark{% else %}font-medium text-on-surface dark:text-on-surface-dark{% endif %} focus:underline" {% if nav.active == "analytics" %}aria-current="page"{% endif %}>{{ nav.tr("nav-analytics") }}</a></li>
|
||||
{% if nav.can_ecb %}<li class="py-4"><a href="/admin/ecb" class="w-full text-lg {% if nav.active == "ecb" %}font-bold text-primary dark:text-primary-dark{% else %}font-medium text-on-surface dark:text-on-surface-dark{% endif %} focus:underline" {% if nav.active == "ecb" %}aria-current="page"{% endif %}>{{ nav.tr("nav-rates") }}</a></li>{% endif %}
|
||||
{% if nav.can_exchange_rates %}<li class="py-4"><a href="/admin/ecb" class="w-full text-lg {% if nav.active == "ecb" %}font-bold text-primary dark:text-primary-dark{% else %}font-medium text-on-surface dark:text-on-surface-dark{% endif %} focus:underline" {% if nav.active == "ecb" %}aria-current="page"{% endif %}>{{ nav.tr("nav-rates") }}</a></li>{% endif %}
|
||||
{% if nav.can_import %}<li class="py-4"><a href="/admin/import" class="w-full text-lg font-medium text-on-surface focus:underline dark:text-on-surface-dark">{{ nav.tr("nav-import") }}</a></li>{% endif %}
|
||||
{% if nav.can_export %}<li class="py-4"><a href="/admin/export" class="w-full text-lg font-medium text-on-surface focus:underline dark:text-on-surface-dark">{{ nav.tr("nav-export") }}</a></li>{% endif %}
|
||||
{% if nav.authenticated %}
|
||||
|
||||
Reference in New Issue
Block a user