Asset descriptions can change: books.label_asset description, journaled as asset.described (with the old one), replayed, projected, shown in the journal
check / check (push) Waiting to run
check / check (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
1 parent
2e14c702c0
commit
49c49aef33
6 files changed
+127
-3
No files matched your search
@@ -1620,6 +1620,42 @@ impl Ledger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Change an asset's description (what it is, as the register and its
|
||||||
|
/// label print it). Its accounting doesn't change.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// GOAL: keep the register's words right when they go stale (a
|
||||||
|
/// corrected entry, a better name).
|
||||||
|
/// SOURCE: DECISION (Stephen, 2026-10-15).
|
||||||
|
/// SCENARIOS:
|
||||||
|
/// 1. IF the asset is unknown, or the description is blank: SHOULD
|
||||||
|
/// refuse, changing nothing.
|
||||||
|
/// ```
|
||||||
|
pub fn set_asset_description(
|
||||||
|
&mut self,
|
||||||
|
id: AssetId,
|
||||||
|
description: &str,
|
||||||
|
) -> Result<(), LedgerError> {
|
||||||
|
self.check_asset_description(id, description)?;
|
||||||
|
self.assets.get_mut(&id).expect("checked").description = description.to_string();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The validation behind [`Self::set_asset_description`].
|
||||||
|
pub fn check_asset_description(
|
||||||
|
&self,
|
||||||
|
id: AssetId,
|
||||||
|
description: &str,
|
||||||
|
) -> Result<(), LedgerError> {
|
||||||
|
if !self.assets.contains_key(&id) {
|
||||||
|
return Err(LedgerError::UnknownAsset(id));
|
||||||
|
}
|
||||||
|
if description.trim().is_empty() {
|
||||||
|
return Err(LedgerError::EmptyAssetDescription);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// The asset whose current tag is `tag`, ignoring case.
|
/// The asset whose current tag is `tag`, ignoring case.
|
||||||
pub fn asset_by_tag(&self, tag: &str) -> Option<&Asset> {
|
pub fn asset_by_tag(&self, tag: &str) -> Option<&Asset> {
|
||||||
let id = self.asset_tags.get(&assets::tag_key(tag))?;
|
let id = self.asset_tags.get(&assets::tag_key(tag))?;
|
||||||
|
|||||||
@@ -267,6 +267,15 @@ pub fn describe(b: &Books, line: &JournalLine) -> Option<Event> {
|
|||||||
};
|
};
|
||||||
ev(icon::BARCODE, text, asset_id.map(Route::Asset))
|
ev(icon::BARCODE, text, asset_id.map(Route::Asset))
|
||||||
}
|
}
|
||||||
|
"asset.described" => ev(
|
||||||
|
icon::PENCIL_SIMPLE,
|
||||||
|
format!(
|
||||||
|
"Renamed asset {} to {}",
|
||||||
|
s(p, "previous_description"),
|
||||||
|
s(p, "description")
|
||||||
|
),
|
||||||
|
asset_id.map(Route::Asset),
|
||||||
|
),
|
||||||
"asset.acquisition_linked" => {
|
"asset.acquisition_linked" => {
|
||||||
let entry = p.get("entry_id").and_then(|v| v.as_u64()).unwrap_or(0);
|
let entry = p.get("entry_id").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||||
Some(Event {
|
Some(Event {
|
||||||
|
|||||||
@@ -2561,7 +2561,7 @@ impl BooksServer {
|
|||||||
|
|
||||||
#[tool(
|
#[tool(
|
||||||
name = "books.label_asset",
|
name = "books.label_asset",
|
||||||
description = "Link a fixed asset to the entry that acquired it (acquisition_entry), or set or change its tag (the label on the thing: letters, digits, - _ . /, at most 32; unique ignoring case and never reused by another asset, even after a retag or disposal) and its manufacturer serial number. The asset is its id or current tag. Journaled. Print labels with books.asset_labels."
|
description = "Change a fixed asset's description, link it to the entry that acquired it (acquisition_entry), or set or change its tag (the label on the thing: letters, digits, - _ . /, at most 32; unique ignoring case and never reused by another asset, even after a retag or disposal) and its manufacturer serial number. The asset is its id or current tag. Journaled. Print labels with books.asset_labels."
|
||||||
)]
|
)]
|
||||||
async fn label_asset(
|
async fn label_asset(
|
||||||
&self,
|
&self,
|
||||||
@@ -2574,11 +2574,23 @@ impl BooksServer {
|
|||||||
if clear && p.serial.is_some() {
|
if clear && p.serial.is_some() {
|
||||||
return Err("pass serial or clear_serial, not both".into());
|
return Err("pass serial or clear_serial, not both".into());
|
||||||
}
|
}
|
||||||
if p.tag.is_none() && p.serial.is_none() && !clear && p.acquisition_entry.is_none() {
|
if p.tag.is_none()
|
||||||
|
&& p.serial.is_none()
|
||||||
|
&& !clear
|
||||||
|
&& p.acquisition_entry.is_none()
|
||||||
|
&& p.description.is_none()
|
||||||
|
{
|
||||||
return Err(
|
return Err(
|
||||||
"nothing to change: pass tag, serial, clear_serial, or acquisition_entry".into(),
|
"nothing to change: pass tag, serial, clear_serial, description, or acquisition_entry"
|
||||||
|
.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if let Some(d) = &p.description {
|
||||||
|
store
|
||||||
|
.ledger()
|
||||||
|
.check_asset_description(id, d)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
if let Some(entry) = p.acquisition_entry {
|
if let Some(entry) = p.acquisition_entry {
|
||||||
store
|
store
|
||||||
.ledger()
|
.ledger()
|
||||||
@@ -2612,12 +2624,18 @@ impl BooksServer {
|
|||||||
.set_asset_acquisition(id, entry)
|
.set_asset_acquisition(id, entry)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
}
|
}
|
||||||
|
if let Some(d) = &p.description {
|
||||||
|
store
|
||||||
|
.set_asset_description(id, d)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
let a = store.ledger().get_asset(id).expect("resolved");
|
let a = store.ledger().get_asset(id).expect("resolved");
|
||||||
Ok(Json(LabelAssetOut {
|
Ok(Json(LabelAssetOut {
|
||||||
asset_id: id,
|
asset_id: id,
|
||||||
tag: a.tag.clone(),
|
tag: a.tag.clone(),
|
||||||
serial: a.serial.clone(),
|
serial: a.serial.clone(),
|
||||||
acquisition_entry: a.acquired_by,
|
acquisition_entry: a.acquired_by,
|
||||||
|
description: a.description.clone(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1537,6 +1537,9 @@ pub struct LabelAssetParams {
|
|||||||
pub serial: Option<String>,
|
pub serial: Option<String>,
|
||||||
/// True to clear the serial number.
|
/// True to clear the serial number.
|
||||||
pub clear_serial: Option<bool>,
|
pub clear_serial: Option<bool>,
|
||||||
|
/// A new description (what it is, as the register and label print it).
|
||||||
|
/// Omit to keep it.
|
||||||
|
pub description: Option<String>,
|
||||||
/// Link the posted entry that put it on the books (purchase or
|
/// Link the posted entry that put it on the books (purchase or
|
||||||
/// contribution; it must debit the asset's cost account and not be
|
/// contribution; it must debit the asset's cost account and not be
|
||||||
/// reversed). Use after correcting that entry, to link the replacement.
|
/// reversed). Use after correcting that entry, to link the replacement.
|
||||||
@@ -1554,6 +1557,8 @@ pub struct LabelAssetOut {
|
|||||||
pub serial: Option<String>,
|
pub serial: Option<String>,
|
||||||
/// The entry that acquired it, if linked.
|
/// The entry that acquired it, if linked.
|
||||||
pub acquisition_entry: Option<u64>,
|
pub acquisition_entry: Option<u64>,
|
||||||
|
/// Its description now.
|
||||||
|
pub description: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parameters for `books.asset_labels`.
|
/// Parameters for `books.asset_labels`.
|
||||||
|
|||||||
@@ -725,6 +725,28 @@ impl Store {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Change an asset's description (see `Ledger::set_asset_description`).
|
||||||
|
/// Journaled as `asset.described`, with the description it had.
|
||||||
|
pub fn set_asset_description(
|
||||||
|
&mut self,
|
||||||
|
id: AssetId,
|
||||||
|
description: &str,
|
||||||
|
) -> Result<(), StoreError> {
|
||||||
|
self.ledger.check_asset_description(id, description)?;
|
||||||
|
let previous = self
|
||||||
|
.ledger
|
||||||
|
.get_asset(id)
|
||||||
|
.map(|a| a.description.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let line = self.append_journal(
|
||||||
|
"asset.described",
|
||||||
|
json!({"asset_id": id, "description": description, "previous_description": previous}),
|
||||||
|
)?;
|
||||||
|
self.ledger.set_asset_description(id, description)?;
|
||||||
|
apply_line_to_sqlite(&self.conn, &line)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Link an asset to the entry that put it on the books (see
|
/// Link an asset to the entry that put it on the books (see
|
||||||
/// `Ledger::set_asset_acquisition`). Journaled as
|
/// `Ledger::set_asset_acquisition`). Journaled as
|
||||||
/// `asset.acquisition_linked`.
|
/// `asset.acquisition_linked`.
|
||||||
@@ -3430,6 +3452,9 @@ pub(crate) fn replay_line_into_ledger(
|
|||||||
"asset.tagged" => {
|
"asset.tagged" => {
|
||||||
ledger.set_asset_tag(get_u64(p, "asset_id")?, get_str(p, "tag")?)?;
|
ledger.set_asset_tag(get_u64(p, "asset_id")?, get_str(p, "tag")?)?;
|
||||||
}
|
}
|
||||||
|
"asset.described" => {
|
||||||
|
ledger.set_asset_description(get_u64(p, "asset_id")?, get_str(p, "description")?)?;
|
||||||
|
}
|
||||||
"asset.acquisition_linked" => {
|
"asset.acquisition_linked" => {
|
||||||
ledger.set_asset_acquisition(get_u64(p, "asset_id")?, get_u64(p, "entry_id")?)?;
|
ledger.set_asset_acquisition(get_u64(p, "asset_id")?, get_u64(p, "entry_id")?)?;
|
||||||
}
|
}
|
||||||
@@ -3904,6 +3929,12 @@ fn apply_line_rows(conn: &Connection, line: &JournalLine) -> Result<(), StoreErr
|
|||||||
params![id, tag],
|
params![id, tag],
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
"asset.described" => {
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE assets SET description = ? WHERE id = ?",
|
||||||
|
params![get_str(p, "description")?, get_u64(p, "asset_id")? as i64],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
"asset.acquisition_linked" => {
|
"asset.acquisition_linked" => {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE assets SET acquisition_entry = ? WHERE id = ?",
|
"UPDATE assets SET acquisition_entry = ? WHERE id = ?",
|
||||||
|
|||||||
@@ -499,3 +499,28 @@ fn verify_compares_amounts_by_value() {
|
|||||||
Store::open(&dir).unwrap().verify().unwrap();
|
Store::open(&dir).unwrap().verify().unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A description change is journaled with the old one, replays on reopen
|
||||||
|
/// and from the journal alone, and verifies; a blank one is refused and
|
||||||
|
/// journals nothing.
|
||||||
|
#[test]
|
||||||
|
fn a_description_change_replays_and_verifies() {
|
||||||
|
let (dir, mut s, _, x, _) = fixture("described");
|
||||||
|
let before = journal(&dir).len();
|
||||||
|
assert!(s.set_asset_description(x, " ").is_err());
|
||||||
|
assert_eq!(journal(&dir).len(), before, "a refusal journals nothing");
|
||||||
|
s.set_asset_description(x, "Laptop, contributed jointly")
|
||||||
|
.unwrap();
|
||||||
|
let (kind, p) = journal(&dir).pop().unwrap();
|
||||||
|
assert_eq!(kind, "asset.described");
|
||||||
|
assert_eq!(p["description"], "Laptop, contributed jointly");
|
||||||
|
s.verify().unwrap();
|
||||||
|
drop(s);
|
||||||
|
std::fs::remove_file(dir.join(DB_FILE)).unwrap();
|
||||||
|
let s = Store::open(&dir).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
s.ledger().get_asset(x).unwrap().description,
|
||||||
|
"Laptop, contributed jointly"
|
||||||
|
);
|
||||||
|
s.verify().unwrap();
|
||||||
|
}
|
||||||
Reference in new issue
Block a user