display what accouting would create

This commit is contained in:
Priec
2026-08-12 18:57:10 +02:00
parent 719bae0cd2
commit cdaa1b2b7d
11 changed files with 668 additions and 22 deletions

View File

@@ -13,7 +13,7 @@
use crate::schema::{ColumnCatalog, ColumnDraft, columns_from_rows};
use super::draft::TableDraft;
use super::draft::{ACCOUNTING_FIELD_TYPE, TableDraft};
/// The `profile_name` option meaning "create a new profile too".
pub(crate) const NEW_PROFILE: &str = "__new__";
@@ -172,6 +172,75 @@ impl AddTablePageState {
}
}
/// The "Columns" list: every declared column, each followed by the columns
/// it expands into.
///
/// The expansion is flattened here rather than nested in the template so
/// the markup stays one loop over one list, and so what a definition row
/// brings is decided in one place.
pub(crate) fn column_rows(&self) -> Vec<ColumnRow> {
let columns = &self.draft.columns;
let last_index = columns.added.len().saturating_sub(1);
let mut rows = Vec::new();
for (index, column) in columns.added.iter().enumerate() {
let mut tags = Vec::new();
if column.quantity_ledger {
tags.push("quantity ledger".to_string());
}
if !column.option_label().is_empty() {
tags.push(column.option_label());
}
rows.push(ColumnRow {
index: Some(index),
first: index == 0,
last: index == last_index,
name: column.name.clone(),
data_type: column.data_type.clone(),
indexable: columns.is_indexable(index),
indexed: column.indexed,
tags,
});
for generated in columns.generated_columns_of(index) {
let mut tags = vec![format!("generated by {}", column.data_type)];
if generated.inherits_currency {
tags.push(format!("{}, {}", column.currency, column.money_mode.label()));
}
rows.push(ColumnRow {
index: None,
first: false,
last: false,
name: generated.name.clone(),
data_type: generated.data_type.clone(),
indexable: false,
indexed: false,
tags,
});
}
// The account foreign key is a system column rather than a
// generated user column, so the catalog does not report it; the
// same explanation as in `TableDraft::preview_rows`.
if column.data_type == ACCOUNTING_FIELD_TYPE {
rows.push(ColumnRow {
index: None,
first: false,
last: false,
name: "account_id".to_string(),
data_type: "BIGINT".to_string(),
indexable: false,
indexed: true,
tags: vec![
"system column".to_string(),
"written as account".to_string(),
],
});
}
}
rows
}
/// Row-display candidates: `id` first, then every column, matching the
/// client's candidate list.
pub(crate) fn row_display_candidates(&self) -> Vec<RowDisplayCandidate> {
@@ -203,6 +272,23 @@ impl AddTablePageState {
}
}
/// One line of the "Columns" list: either a column the user declared, or one
/// the server will generate from the definition row above it.
pub(crate) struct ColumnRow {
/// Where the column sits in the draft, for the buttons that act on it.
/// `None` for a generated column, which is not the user's to act on: it
/// moves and is removed with the definition row it came from.
pub index: Option<usize>,
/// Whether it can move any further up, and any further down.
pub first: bool,
pub last: bool,
pub name: String,
pub data_type: String,
pub indexable: bool,
pub indexed: bool,
pub tags: Vec<String>,
}
pub(crate) struct RowDisplayCandidate {
pub index: usize,
pub name: String,
@@ -288,6 +374,57 @@ mod tests {
assert_eq!(form.to_draft().row_display_columns, vec!["number"]);
}
/// The column list shows a definition row with the columns it expands
/// into. Only the definition row is the user's to act on: the generated
/// ones carry no index, so they get no buttons.
#[test]
fn the_column_list_shows_what_a_definition_row_expands_into() {
let mut page = AddTablePageState {
nav: crate::ui::Nav::default(),
profiles: Vec::new(),
draft: posted_form().to_draft(),
status: None,
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,
money_mode: MoneyMode::Exact,
currency: "EUR".to_string(),
});
let rows = page.column_rows();
assert_eq!(
rows.iter().map(|row| row.name.as_str()).collect::<Vec<_>>(),
[
"number",
"total",
"accounting",
"name",
"tax_point_date",
"debit",
"credit",
"account_id",
]
);
let row = |name: &str| rows.iter().find(|row| row.name == name).unwrap();
// Only the three declared columns can be moved, indexed or removed.
assert_eq!(
rows.iter().filter(|row| row.index.is_some()).count(),
3,
"only declared columns carry an index"
);
assert!(row("number").first);
assert!(row("accounting").last);
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()));
}
#[test]
fn the_draft_never_trusts_the_posted_table_list() {
// `existing_profile_tables` is what duplicate-name checks read, so it