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.
|
||||
pub fn asset_by_tag(&self, tag: &str) -> Option<&Asset> {
|
||||
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))
|
||||
}
|
||||
"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" => {
|
||||
let entry = p.get("entry_id").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
Some(Event {
|
||||
|
||||
@@ -2561,7 +2561,7 @@ impl BooksServer {
|
||||
|
||||
#[tool(
|
||||
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(
|
||||
&self,
|
||||
@@ -2574,11 +2574,23 @@ impl BooksServer {
|
||||
if clear && p.serial.is_some() {
|
||||
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(
|
||||
"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 {
|
||||
store
|
||||
.ledger()
|
||||
@@ -2612,12 +2624,18 @@ impl BooksServer {
|
||||
.set_asset_acquisition(id, entry)
|
||||
.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");
|
||||
Ok(Json(LabelAssetOut {
|
||||
asset_id: id,
|
||||
tag: a.tag.clone(),
|
||||
serial: a.serial.clone(),
|
||||
acquisition_entry: a.acquired_by,
|
||||
description: a.description.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -1537,6 +1537,9 @@ pub struct LabelAssetParams {
|
||||
pub serial: Option<String>,
|
||||
/// True to clear the serial number.
|
||||
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
|
||||
/// contribution; it must debit the asset's cost account and not be
|
||||
/// reversed). Use after correcting that entry, to link the replacement.
|
||||
@@ -1554,6 +1557,8 @@ pub struct LabelAssetOut {
|
||||
pub serial: Option<String>,
|
||||
/// The entry that acquired it, if linked.
|
||||
pub acquisition_entry: Option<u64>,
|
||||
/// Its description now.
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// Parameters for `books.asset_labels`.
|
||||
|
||||
@@ -725,6 +725,28 @@ impl Store {
|
||||
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
|
||||
/// `Ledger::set_asset_acquisition`). Journaled as
|
||||
/// `asset.acquisition_linked`.
|
||||
@@ -3430,6 +3452,9 @@ pub(crate) fn replay_line_into_ledger(
|
||||
"asset.tagged" => {
|
||||
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" => {
|
||||
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],
|
||||
)?;
|
||||
}
|
||||
"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" => {
|
||||
conn.execute(
|
||||
"UPDATE assets SET acquisition_entry = ? WHERE id = ?",
|
||||
|
||||
@@ -499,3 +499,28 @@ fn verify_compares_amounts_by_value() {
|
||||
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