Release 1.2.0: Reset all data #3

Merged
xavierk merged 7 commits from release/1.2.0 into main 2026-10-06 07:14:35 +00:00
5 changed files with 74 additions and 13 deletions
Showing only changes of commit 93a4ece886 - Show all commits
+6 -1
View File
@@ -161,7 +161,12 @@ mod tests {
#[test]
fn units_beyond_the_original_five_are_stored_as_given() {
let conn = open_in_memory().unwrap();
for (typed, stored) in [("km", "km"), ("SQFT", "sqft"), (" bundle ", "bundle"), ("sq.ft/day", "sq.ft/day")] {
for (typed, stored) in [
("km", "km"),
("SQFT", "sqft"),
(" bundle ", "bundle"),
("sq.ft/day", "sq.ft/day"),
] {
let mut p = preset(typed);
p.mode = "rate".into();
p.unit = typed.into();
+42 -5
View File
@@ -1108,7 +1108,17 @@ CREATE INDEX IF NOT EXISTS idx_invoices_created ON invoices(created_at DESC);
fn m12_rebuilds_item_presets_keeping_rows_and_allowing_new_units() {
let mut conn = Connection::open_in_memory().unwrap();
let v11 = vec![
M::up(SCHEMA), M::up(M2), M::up(M3), M::up(M4), M::up(M5), M::up(M6), M::up(M7), M::up(M8), M::up(M9), M::up(M10), M::up(M11),
M::up(SCHEMA),
M::up(M2),
M::up(M3),
M::up(M4),
M::up(M5),
M::up(M6),
M::up(M7),
M::up(M8),
M::up(M9),
M::up(M10),
M::up(M11),
];
Migrations::new(v11).to_latest(&mut conn).unwrap();
assert_eq!(user_version(&conn), 11);
@@ -1134,16 +1144,43 @@ CREATE INDEX IF NOT EXISTS idx_invoices_created ON invoices(created_at DESC);
assert_eq!(
rows,
vec![
(1, "Studio".into(), "998397".into(), "rate".into(), "hour".into(), 150050, 3, "then".into(), Some("ITEM-1".into())),
(2, "Fee".into(), "".into(), "fixed".into(), "unit".into(), 500000, 0, "then".into(), None),
(
1,
"Studio".into(),
"998397".into(),
"rate".into(),
"hour".into(),
150050,
3,
"then".into(),
Some("ITEM-1".into())
),
(
2,
"Fee".into(),
"".into(),
"fixed".into(),
"unit".into(),
500000,
0,
"then".into(),
None
),
]
);
// New units and custom text are allowed; an empty or over-long unit is not; ids keep counting up.
conn.execute("INSERT INTO item_presets (description, unit, created_at) VALUES ('Travel', 'km', 'now')", []).unwrap();
conn.execute("INSERT INTO item_presets (description, unit, created_at) VALUES ('Parcel', 'bundle', 'now')", []).unwrap();
assert!(conn.execute("INSERT INTO item_presets (description, unit, created_at) VALUES ('X', '', 'now')", []).is_err());
assert!(conn
.execute(
"INSERT INTO item_presets (description, unit, created_at) VALUES ('X', '', 'now')",
[]
)
.is_err());
assert!(conn.execute("INSERT INTO item_presets (description, unit, created_at) VALUES ('X', 'thirteen char', 'now')", []).is_err());
let last: i64 = conn.query_row("SELECT MAX(id) FROM item_presets", [], |r| r.get(0)).unwrap();
let last: i64 = conn
.query_row("SELECT MAX(id) FROM item_presets", [], |r| r.get(0))
.unwrap();
assert_eq!(last, 4);
}
+4 -1
View File
@@ -67,7 +67,10 @@ impl NamingMode {
/// Voiced line unit to ERPNext UOM. Whole-number UOMs such as "Nos" reject fractional quantities,
/// which is why hours, minutes, kilometres and the like map to their own UOMs (see `units::KNOWN`).
pub fn default_uom_map() -> BTreeMap<String, String> {
crate::units::KNOWN.iter().map(|(id, uom)| (id.to_string(), uom.to_string())).collect()
crate::units::KNOWN
.iter()
.map(|(id, uom)| (id.to_string(), uom.to_string()))
.collect()
}
/// Everything stored for the integration, secret included. Backend-only.
+13 -4
View File
@@ -31,7 +31,10 @@ pub fn is_known(unit: &str) -> bool {
/// The ERPNext UOM that suits a built-in unit.
pub fn default_uom(unit: &str) -> Option<&'static str> {
KNOWN.iter().find(|(id, _)| *id == unit).map(|(_, uom)| *uom)
KNOWN
.iter()
.find(|(id, _)| *id == unit)
.map(|(_, uom)| *uom)
}
fn custom_problem(text: &str) -> Option<String> {
@@ -40,11 +43,14 @@ fn custom_problem(text: &str) -> Option<String> {
return Some("a unit is required".into());
}
if t.chars().count() > MAX_CUSTOM_LEN {
return Some(format!("a custom unit has at most {MAX_CUSTOM_LEN} characters"));
return Some(format!(
"a custom unit has at most {MAX_CUSTOM_LEN} characters"
));
}
let mut chars = t.chars();
let first_ok = chars.next().is_some_and(|c| c.is_ascii_alphanumeric());
if !first_ok || !chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, ' ' | '.' | '/' | '-')) {
if !first_ok || !chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, ' ' | '.' | '/' | '-'))
{
return Some("a unit may use letters, digits, spaces, . / and - only".into());
}
None
@@ -113,7 +119,10 @@ mod tests {
#[test]
fn fractional_units_do_not_map_to_whole_number_uoms() {
for id in ["second", "minute", "hour", "day", "week", "month", "km", "m", "sqft", "sqm", "kg", "litre"] {
for id in [
"second", "minute", "hour", "day", "week", "month", "km", "m", "sqft", "sqm", "kg",
"litre",
] {
assert_ne!(default_uom(id), Some("Nos"), "{id}");
}
}
+8 -1
View File
@@ -39,8 +39,15 @@ export default function QuantityInput({ id, value, unit, onChange, disabled, onV
const parsed = parseQuantity(text, unit);
const clock = knownUnit(unit)?.clock;
const validity = useRef(onValidity);
useEffect(() => {
validity.current = onValidity;
useEffect(() => validity.current?.(parsed.ok), [parsed.ok]);
});
// Reported while this box exists; a box that goes away (its line was removed) stops counting as invalid.
useEffect(() => {
const report = validity.current;
report?.(parsed.ok);
return () => report?.(true);
}, [parsed.ok]);
const helper = parsed.ok && text.includes(":") ? `= ${formatQuantity(parsed.value, "unit")} ${knownUnit(unit)?.short ?? ""}`.trim() : undefined;