conversions5

This commit is contained in:
Filipriec
2026-08-23 15:50:03 +02:00
parent af7016e4c7
commit 0c33d1fa5c
20 changed files with 1197 additions and 845 deletions

View File

@@ -7,21 +7,21 @@ use axum::{
routing::get,
};
mod authz;
mod i18n;
mod pages;
mod schema;
mod services;
mod ui;
mod authz;
mod i18n;
// The server's system column vocabulary, read out of `common` the same way the
// generated protos are: this crate compiles that source tree directly instead
// of depending on the crate. Only `is_system_column` is used here.
#[path = "../../common/src/system_column.rs"]
#[allow(dead_code)]
mod system_column;
#[path = "../../common/src/grpc_error.rs"]
#[allow(dead_code)]
mod grpc_error;
#[path = "../../common/src/system_column.rs"]
#[allow(dead_code)]
mod system_column;
mod analytics {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
@@ -78,12 +78,17 @@ mod definitions {
"/../common/src/proto/komp_ac.ecb.rs"
));
}
pub mod exchange_rates {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../common/src/proto/komp_ac.exchange_rates.rs"
));
}
}
use auth::auth_service_client::AuthServiceClient;
use definitions::{
ecb,
table_definition::table_definition_client::TableDefinitionClient,
ecb, table_definition::table_definition_client::TableDefinitionClient,
table_script::table_script_client::TableScriptClient,
table_structure::table_structure_service_client::TableStructureServiceClient,
table_validation::table_validation_service_client::TableValidationServiceClient,

View File

@@ -13,10 +13,8 @@
use crate::{
definitions::table_definition::{GeneratedColumnAlias, PostTableDefinitionRequest},
schema::{ColumnCatalog, ColumnDraft, proto_columns, validate_identifier, validate_table_name},
{i18n::Locale, tr},
schema::{
ColumnCatalog, ColumnDraft, proto_columns, validate_identifier, validate_table_name,
},
};
/// The compound type that also brings a system column with it. Which columns
@@ -68,6 +66,8 @@ pub(crate) struct TableDraft {
pub creating_new_profile: bool,
pub global: bool,
pub accounting_currency: String,
pub rate_source_id: String,
pub foreign_currencies: String,
pub table_name: String,
@@ -98,6 +98,7 @@ impl TableDraft {
pub(crate) fn new() -> Self {
Self {
accounting_currency: "EUR".to_string(),
rate_source_id: "ecb".to_string(),
columns: ColumnDraft::new(ColumnCatalog::default()),
..Self::default()
}
@@ -116,11 +117,7 @@ impl TableDraft {
// ---- mutations -------------------------------------------------------
/// Removes one column, and drops it from the display columns with it.
pub(crate) fn remove_column(
&mut self,
locale: Locale,
index: usize,
) -> Result<String, String> {
pub(crate) fn remove_column(&mut self, locale: Locale, index: usize) -> Result<String, String> {
let removed = self.columns.remove(locale, index)?;
self.row_display_columns
.retain(|display| display != &removed.name);
@@ -364,9 +361,7 @@ impl TableDraft {
);
for (source, alias) in &renames {
if let Some(error) =
validate_identifier(locale, alias, "label-column-alias", true)
{
if let Some(error) = validate_identifier(locale, alias, "label-column-alias", true) {
return Err(error);
}
if taken.iter().filter(|name| *name == alias).count() > 1 {
@@ -574,6 +569,21 @@ impl TableDraft {
} else {
String::new()
},
rate_source_id: if self.creating_new_profile && !self.global {
self.rate_source_id.trim().to_ascii_lowercase()
} else {
String::new()
},
foreign_currencies: if self.creating_new_profile && !self.global {
self.foreign_currencies
.split(',')
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_ascii_uppercase)
.collect()
} else {
Vec::new()
},
row_display_columns: self.row_display_columns.clone(),
global: self.global,
// The generated columns are named in the same request that creates
@@ -634,7 +644,13 @@ mod tests {
draft.accounting_currency = "eur".to_string();
assert!(draft.validate(crate::i18n::Locale::default()).is_ok());
assert_eq!(draft.into_request(crate::i18n::Locale::default()).unwrap().accounting_currency, "EUR");
assert_eq!(
draft
.into_request(crate::i18n::Locale::default())
.unwrap()
.accounting_currency,
"EUR"
);
}
/// Every column an ACCOUNTING row generates may be aliased, including the
@@ -740,16 +756,28 @@ mod tests {
source: "debit".to_string(),
alias: "Md".to_string(),
}];
assert!(draft.validate(crate::i18n::Locale::default()).is_err(), "an alias is a column name");
assert!(
draft.validate(crate::i18n::Locale::default()).is_err(),
"an alias is a column name"
);
draft.generated_aliases[0].alias = "note".to_string();
assert!(draft.validate(crate::i18n::Locale::default()).is_err(), "a declared column holds the name");
assert!(
draft.validate(crate::i18n::Locale::default()).is_err(),
"a declared column holds the name"
);
draft.generated_aliases[0].alias = "credit".to_string();
assert!(draft.validate(crate::i18n::Locale::default()).is_err(), "another generated column does");
assert!(
draft.validate(crate::i18n::Locale::default()).is_err(),
"another generated column does"
);
draft.generated_aliases[0].alias = "id".to_string();
assert!(draft.validate(crate::i18n::Locale::default()).is_err(), "a system column does");
assert!(
draft.validate(crate::i18n::Locale::default()).is_err(),
"a system column does"
);
draft.generated_aliases[0].alias = "md".to_string();
assert!(draft.validate(crate::i18n::Locale::default()).is_ok());
@@ -791,7 +819,9 @@ mod tests {
#[test]
fn a_table_with_no_columns_is_refused() {
let mut draft = draft_with_column("total", "int");
draft.remove_column(crate::i18n::Locale::default(), 0).unwrap();
draft
.remove_column(crate::i18n::Locale::default(), 0)
.unwrap();
assert!(draft.validate(crate::i18n::Locale::default()).is_err());
}
@@ -850,9 +880,20 @@ mod tests {
// never the definition row itself.
assert_eq!(
draft.row_display_column_names(),
["number", "name", "tax_point_date", "debit", "credit", "account"]
[
"number",
"name",
"tax_point_date",
"debit",
"credit",
"account"
]
);
assert!(
!draft
.row_display_column_names()
.contains(&"accounting".to_string())
);
assert!(!draft.row_display_column_names().contains(&"accounting".to_string()));
// `name` is the second candidate, and choosing it names the generated
// column rather than the row it came from.
@@ -879,7 +920,9 @@ mod tests {
draft.toggle_row_display_candidate(1);
assert_eq!(draft.row_display_columns, vec!["number"]);
draft.remove_column(crate::i18n::Locale::default(), 0).unwrap();
draft
.remove_column(crate::i18n::Locale::default(), 0)
.unwrap();
assert!(draft.row_display_columns.is_empty());
}
@@ -919,7 +962,9 @@ mod tests {
draft.toggle_row_display_candidate(1); // number
draft.toggle_row_display_candidate(2); // issued_on
draft.move_column(crate::i18n::Locale::default(), 0, 1).unwrap();
draft
.move_column(crate::i18n::Locale::default(), 0, 1)
.unwrap();
assert_eq!(
draft
@@ -993,7 +1038,13 @@ mod tests {
let mut draft = draft_with_column("number", "text");
draft.columns.toggle_indexed(0);
assert_eq!(draft.into_request(crate::i18n::Locale::default()).unwrap().indexes, vec!["number"]);
assert_eq!(
draft
.into_request(crate::i18n::Locale::default())
.unwrap()
.indexes,
vec!["number"]
);
}
/// The request the builder sends is one the server will take: a link
@@ -1090,6 +1141,10 @@ mod tests {
source: "name".to_string(),
alias: "row_revision".to_string(),
}];
assert!(draft.validate_generated_aliases(crate::i18n::Locale::default()).is_err());
assert!(
draft
.validate_generated_aliases(crate::i18n::Locale::default())
.is_err()
);
}
}

View File

@@ -37,6 +37,10 @@ pub(crate) struct BuilderForm {
#[serde(default)]
pub accounting_currency: String,
#[serde(default)]
pub rate_source_id: String,
#[serde(default)]
pub foreign_currencies: String,
#[serde(default)]
pub global: bool,
#[serde(default)]
pub table_name: String,
@@ -163,6 +167,8 @@ impl BuilderForm {
profile_name_input: self.profile_name_input.clone(),
creating_new_profile,
accounting_currency: self.accounting_currency.clone(),
rate_source_id: self.rate_source_id.clone(),
foreign_currencies: self.foreign_currencies.clone(),
global: self.global,
table_name: self.table_name.clone(),
columns,
@@ -233,7 +239,11 @@ impl AddTablePageState {
tags.push("quantity ledger".to_string());
}
if !column.currency.is_empty() {
tags.push(format!("{}, {}", column.currency, column.money_mode.label()));
tags.push(format!(
"{}, {}",
column.currency,
column.money_mode.label()
));
}
rows.push(ColumnRow {
index: Some(index),
@@ -266,7 +276,11 @@ impl AddTablePageState {
for generated in generated_columns {
let mut tags = vec![format!("generated by {}", column.data_type)];
if generated.inherits_currency {
tags.push(format!("{}, {}", column.currency, column.money_mode.label()));
tags.push(format!(
"{}, {}",
column.currency,
column.money_mode.label()
));
}
rows.push(ColumnRow {
index: None,
@@ -502,15 +516,18 @@ mod tests {
error: None,
};
page.draft.columns.catalog = crate::schema::tests::catalog();
page.draft.columns.added.push(crate::schema::ColumnDefinition {
name: "accounting".to_string(),
data_type: "accounting".to_string(),
indexed: false,
quantity_ledger: false,
required: false,
money_mode: MoneyMode::Exact,
currency: "EUR".to_string(),
});
page.draft
.columns
.added
.push(crate::schema::ColumnDefinition {
name: "accounting".to_string(),
data_type: "accounting".to_string(),
indexed: false,
quantity_ledger: false,
required: false,
money_mode: MoneyMode::Exact,
currency: "EUR".to_string(),
});
let rows = page.column_rows();
assert_eq!(
@@ -537,9 +554,16 @@ mod tests {
);
assert!(row("number").first);
assert!(row("accounting").last);
assert!(!row("accounting").indexable, "a definition row is no column");
assert!(
!row("accounting").indexable,
"a definition row is no column"
);
assert!(row("debit").tags.contains(&"EUR, exact".to_string()));
assert!(row("name").tags.contains(&"generated by accounting".to_string()));
assert!(
row("name")
.tags
.contains(&"generated by accounting".to_string())
);
}
/// The generated columns of an ACCOUNTING row are the user's to alias, and
@@ -586,7 +610,11 @@ mod tests {
assert_eq!(row("account").alias_source.as_deref(), Some("account"));
assert_eq!(row("account").alias, "ucet");
assert!(row("account_id").alias_source.is_none());
assert!(row("account_id").tags.contains(&"written as ucet".to_string()));
assert!(
row("account_id")
.tags
.contains(&"written as ucet".to_string())
);
assert!(
row("accounting").alias_source.is_none(),
"a declared column is named where it is described"

View File

@@ -88,8 +88,19 @@
#}
<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>
</label>
<label>Initial foreign currencies
<input name="foreign_currencies" value="{{ page.draft.foreign_currencies }}"
placeholder="CZK, USD">
</label>
{% else %}
<input type="hidden" name="accounting_currency" value="{{ page.draft.accounting_currency }}">
<input type="hidden" name="rate_source_id" value="{{ page.draft.rate_source_id }}">
<input type="hidden" name="foreign_currencies" value="{{ page.draft.foreign_currencies }}">
{% endif %}
{#