currency per column in table definition
This commit is contained in:
2
client
2
client
Submodule client updated: e3eef67d20...d76107b4c4
@@ -60,6 +60,16 @@ const CANONICAL_TYPES: &[&str] = &[
|
||||
"bigint",
|
||||
];
|
||||
|
||||
/// Whether a column of this type declares a currency.
|
||||
///
|
||||
/// Both MONEY and ACCOUNTING do, which is why this is a named predicate rather
|
||||
/// than an inline comparison: written by hand, the ACCOUNTING half is easy to
|
||||
/// forget, and forgetting it is silent — the currency is still stored and sent,
|
||||
/// it just stops being validated or displayed.
|
||||
pub(crate) fn carries_currency(field_type: &str) -> bool {
|
||||
field_type.eq_ignore_ascii_case("money") || field_type.eq_ignore_ascii_case("accounting")
|
||||
}
|
||||
|
||||
/// Types a quantity-ledger column may use.
|
||||
fn quantity_ledger_type_allowed(field_type: &str) -> bool {
|
||||
matches!(field_type, "int" | "bigint" | "money")
|
||||
@@ -146,8 +156,8 @@ pub(crate) struct ColumnDefinition {
|
||||
impl ColumnDefinition {
|
||||
/// The `option` cell of the preview, mirroring the client's preview table.
|
||||
pub(crate) fn option_label(&self) -> String {
|
||||
let is_money = self.data_type.eq_ignore_ascii_case("money");
|
||||
match (self.indexed, is_money) {
|
||||
let has_currency = carries_currency(&self.data_type);
|
||||
match (self.indexed, has_currency) {
|
||||
(true, true) => format!("indexed, {}, {}", self.currency, self.money_mode.label()),
|
||||
(true, false) => "indexed".to_string(),
|
||||
(false, true) => format!("{}, {}", self.currency, self.money_mode.label()),
|
||||
@@ -219,9 +229,8 @@ impl TableDraft {
|
||||
|
||||
// ---- pending-column input -------------------------------------------
|
||||
|
||||
pub(crate) fn is_money_column_input(&self) -> bool {
|
||||
let value = self.column_type_input.trim();
|
||||
value.eq_ignore_ascii_case("money") || value.eq_ignore_ascii_case("accounting")
|
||||
pub(crate) fn pending_column_carries_currency(&self) -> bool {
|
||||
carries_currency(self.column_type_input.trim())
|
||||
}
|
||||
|
||||
pub(crate) fn is_temporal_column_input(&self) -> bool {
|
||||
@@ -273,8 +282,9 @@ impl TableDraft {
|
||||
self.is_gtin_column_input()
|
||||
}
|
||||
|
||||
pub(crate) fn show_rounding(&self) -> bool {
|
||||
self.is_money_column_input()
|
||||
/// Currency and rounding both apply only to a money column.
|
||||
pub(crate) fn show_money_options(&self) -> bool {
|
||||
self.pending_column_carries_currency()
|
||||
}
|
||||
|
||||
// ---- mutations -------------------------------------------------------
|
||||
@@ -315,9 +325,8 @@ impl TableDraft {
|
||||
);
|
||||
}
|
||||
|
||||
let is_money = column_type.eq_ignore_ascii_case("money")
|
||||
|| column_type.eq_ignore_ascii_case("accounting");
|
||||
let currency = if is_money {
|
||||
let has_currency = carries_currency(&column_type);
|
||||
let currency = if has_currency {
|
||||
normalize_currency_input(&self.column_currency_input)?
|
||||
} else {
|
||||
String::new()
|
||||
@@ -330,7 +339,7 @@ impl TableDraft {
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("yes"),
|
||||
quantity_ledger,
|
||||
money_mode: if is_money {
|
||||
money_mode: if has_currency {
|
||||
MoneyMode::from_input(&self.column_rounding_input)
|
||||
} else {
|
||||
MoneyMode::Exact
|
||||
@@ -554,6 +563,20 @@ impl TableDraft {
|
||||
if let Some(error) = validate_field_type(&column.data_type) {
|
||||
return Err(format!("Column `{}`: {error}", column.name));
|
||||
}
|
||||
// The same rule the server enforces: required for a money column,
|
||||
// forbidden for every other type. `add_column_from_inputs` already
|
||||
// applies it, but a draft rebuilt from a posted form has not been
|
||||
// through that path.
|
||||
if carries_currency(&column.data_type) {
|
||||
if let Err(error) = normalize_currency_input(&column.currency) {
|
||||
return Err(format!("Column `{}`: {error}", column.name));
|
||||
}
|
||||
} else if !column.currency.trim().is_empty() {
|
||||
return Err(format!(
|
||||
"Column `{}`: only MONEY and ACCOUNTING columns may declare a currency.",
|
||||
column.name
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -810,6 +833,40 @@ mod tests {
|
||||
assert_eq!(draft.into_request().unwrap().accounting_currency, "EUR");
|
||||
}
|
||||
|
||||
/// `add_column_from_inputs` enforces this, but a draft rebuilt from a
|
||||
/// posted form skips that path, so `validate` has to enforce it too.
|
||||
#[test]
|
||||
fn a_rebuilt_draft_is_still_held_to_the_currency_rule() {
|
||||
let mut draft = draft_with_column("total", "money");
|
||||
draft.columns[0].currency = String::new();
|
||||
assert!(draft.validate().is_err());
|
||||
|
||||
draft.columns[0].currency = "XYZ".to_string();
|
||||
assert!(draft.validate().is_err());
|
||||
|
||||
draft.columns[0].currency = "EUR".to_string();
|
||||
assert!(draft.validate().is_ok());
|
||||
|
||||
// Forbidden on everything else, exactly as the server has it.
|
||||
let mut draft = draft_with_column("note", "text");
|
||||
draft.columns[0].currency = "EUR".to_string();
|
||||
assert!(draft.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_accounting_column_shows_its_currency_too() {
|
||||
let mut draft = TableDraft::new();
|
||||
draft.column_type_input = "accounting".to_string();
|
||||
draft.column_currency_input = "czk".to_string();
|
||||
draft.add_column_from_inputs().unwrap();
|
||||
|
||||
assert_eq!(draft.columns[0].currency, "CZK");
|
||||
assert_eq!(draft.columns[0].option_label(), "CZK, exact");
|
||||
|
||||
draft.toggle_column_indexed(0);
|
||||
assert_eq!(draft.columns[0].option_label(), "indexed, CZK, exact");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_profile_sends_no_accounting_currency() {
|
||||
let draft = draft_with_column("total", "int");
|
||||
@@ -927,3 +984,4 @@ mod tests {
|
||||
assert_eq!(draft.into_request().unwrap().indexes, vec!["number"]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
</label>
|
||||
{% endif %}
|
||||
|
||||
{% if page.draft.show_rounding() %}
|
||||
{% if page.draft.show_money_options() %}
|
||||
<label>Currency
|
||||
<input name="column_currency_input" value="{{ page.draft.column_currency_input }}" list="currency-codes"
|
||||
maxlength="3" placeholder="EUR">
|
||||
|
||||
Reference in New Issue
Block a user