check / check (push) Waiting to run
- core: asset tags (generated PREFIX-0001 or the human's own; unique ignoring case; never reused by another asset) and serial numbers (rules.md B-24); a journaled link from an asset to the entry that acquired it (posted, unreversed, debiting the cost account); fixed_asset_register(as_of), reconciled to the GL (unposted depreciation, unregistered cost) - store: asset.tagged, asset.serial_set, asset.acquisition_linked journaled, replayed, projected (schema v9); differs_from now also compares control roles and tag history - config: [books] asset_tag_prefix, default the company's initials without legal-form words - import: Avery 5160/8160 label sheets with QR codes (geometry vendored from Avery, docs/spec/sources/avery-5160.md); the register as XLSX and PDF - mcp: register_asset tags (and links) automatically; books.label_asset, books.asset_labels; asset tools take a tag; books.show asset / assets page / register report; export fixed_asset_register (json, csv, xlsx, pdf) and in the CPA package - viewer: Assets page (register, reconciliation), asset page (identity, schedule posted vs not, entries, documents), Fixed asset register report, global search by tag/serial/description, journal text, needs-attention items - fix: verify compared snapshot amounts as strings; a zero total summed in hash-map order could print "0" or "0.00", so agreeing books failed verify at random. Now by value (regression test) - tests: spec-blind suites for tags, register, prefix, store, MCP; label and register PDF/XLSX tests; viewer tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
150 lines
4.4 KiB
Typst
150 lines
4.4 KiB
Typst
// The fixed asset register: every asset, its cost, depreciation, and book
|
|
// value, then the totals and the reconciliation to the general ledger.
|
|
//
|
|
// Everything arrives as data in sys.inputs.data (JSON), laid out by
|
|
// ledger-import::asset_register, with amounts already split into exact
|
|
// parts. The template only typesets: no arithmetic, and no text is ever
|
|
// parsed as markup.
|
|
//
|
|
// Shape: { company, title, period, footer, columns: [10 headings], empty,
|
|
// lines: [{ tag, description, serial, in_service, cost, salvage,
|
|
// life, accumulated, book_value, status, disposed }],
|
|
// totals: [cost, accumulated, book_value],
|
|
// reconciliation: [{ label, amount, difference }] }
|
|
// where every amount is { digits: "1,234.56", negative }.
|
|
|
|
#let d = json(bytes(sys.inputs.data))
|
|
#let ink = luma(25)
|
|
#let quiet = luma(110)
|
|
#let hair = 0.5pt + luma(160)
|
|
#let firm = 0.6pt + ink
|
|
|
|
#set document(title: d.company + " — " + d.title, author: d.company)
|
|
#set text(font: "Inter", size: 8pt, fill: ink, number-width: "tabular", hyphenate: false)
|
|
#set page(
|
|
paper: "us-letter",
|
|
flipped: true,
|
|
margin: (x: 0.6in, top: 0.7in, bottom: 0.7in),
|
|
header: context {
|
|
if counter(page).get().first() > 1 {
|
|
set text(7.5pt, fill: quiet)
|
|
[#d.company · #d.title #h(1fr) #d.period]
|
|
}
|
|
},
|
|
footer: context {
|
|
set text(7.5pt, fill: quiet)
|
|
grid(
|
|
columns: (1fr, auto, 1fr),
|
|
[],
|
|
d.footer,
|
|
align(right)[Page #counter(page).display() of #counter(page).final().first()],
|
|
)
|
|
},
|
|
)
|
|
|
|
#align(center)[
|
|
#text(15pt, fill: luma(45), d.company) \
|
|
#v(4pt)
|
|
#text(10.5pt, weight: "bold", d.title) \
|
|
#v(2pt)
|
|
#d.period
|
|
]
|
|
#v(12pt)
|
|
|
|
// An amount: negatives in parentheses, the closing one's width reserved on
|
|
// every row so decimal points line up; "$" on totals.
|
|
#let amount(a, currency: false) = {
|
|
let open = if a.negative { [(] } else { [] }
|
|
let close = if a.negative { [)] } else { hide[)] }
|
|
let sign = if currency [\$] else []
|
|
[#open#sign#a.digits#close]
|
|
}
|
|
|
|
// Columns: 4-8 are numbers (right-aligned).
|
|
#let numeric = (4, 5, 6, 7, 8)
|
|
#let ncols = d.columns.len()
|
|
#let full(body, ..args) = table.cell(colspan: ncols, ..args, body)
|
|
|
|
#table(
|
|
// Number columns are fixed widths (headings wrap); text takes the rest.
|
|
columns: (auto, 1.4fr, auto, auto, 6.4em, 5.6em, 6em, 7.4em, 6.4em, 1fr),
|
|
stroke: none,
|
|
inset: (x: 3.5pt, y: 3.4pt),
|
|
align: (col, row) => (if col in numeric { right } else { left }) + (if row == 0 { bottom } else { horizon }),
|
|
table.header(
|
|
..d.columns.map(c => table.cell(
|
|
stroke: (top: firm, bottom: hair),
|
|
text(6.5pt, weight: "bold", tracking: 0.04em, upper(c)),
|
|
)),
|
|
),
|
|
..if d.lines.len() == 0 {
|
|
(full(emph(d.empty)),)
|
|
} else {
|
|
d
|
|
.lines
|
|
.map(l => {
|
|
let t(s) = if l.disposed { text(fill: quiet, s) } else { s }
|
|
let a(x) = t(amount(x))
|
|
(
|
|
t(l.tag),
|
|
t(l.description),
|
|
t(l.serial),
|
|
t(l.in_service),
|
|
a(l.cost),
|
|
a(l.salvage),
|
|
t(l.life),
|
|
a(l.accumulated),
|
|
a(l.book_value),
|
|
t(l.status),
|
|
)
|
|
})
|
|
.flatten()
|
|
},
|
|
// Totals of the assets held: rule above, double rule beneath.
|
|
..{
|
|
let s = (top: firm)
|
|
let tot(x) = table.cell(stroke: s, text(weight: "bold", amount(x, currency: true)))
|
|
let blank = table.cell(stroke: s, [])
|
|
(
|
|
table.cell(stroke: s, colspan: 4, text(weight: "bold", "Total, assets held")),
|
|
tot(d.totals.at(0)),
|
|
blank,
|
|
blank,
|
|
tot(d.totals.at(1)),
|
|
tot(d.totals.at(2)),
|
|
blank,
|
|
)
|
|
},
|
|
full(
|
|
inset: (x: 0pt, y: 0pt),
|
|
stack(spacing: 1.6pt, line(length: 100%, stroke: firm), line(length: 100%, stroke: firm)),
|
|
),
|
|
)
|
|
|
|
#v(14pt)
|
|
#block(breakable: false, {
|
|
text(weight: "semibold", "Reconciliation to the books")
|
|
v(2pt)
|
|
table(
|
|
columns: (auto, 9em),
|
|
stroke: none,
|
|
inset: (x: 4pt, y: 3.4pt),
|
|
align: (left + horizon, right + horizon),
|
|
..d
|
|
.reconciliation
|
|
.map(x => {
|
|
if x.difference {
|
|
let s = (top: hair)
|
|
(
|
|
table.cell(stroke: s, text(weight: "bold", x.label)),
|
|
table.cell(stroke: s, text(weight: "bold", amount(x.amount, currency: true))),
|
|
table.cell(colspan: 2, inset: 2pt, []),
|
|
)
|
|
} else {
|
|
(pad(left: 1.4em, x.label), amount(x.amount))
|
|
}
|
|
})
|
|
.flatten(),
|
|
)
|
|
})
|