The CPA package's general-journal.iif now holds every posted entry through as_of (closing entries left out, D-36), written as plain ASCII with CR LF, DOCNUM the entry id, QuickBooks Desktop's field limits and subaccount types honoured, and every character written differently listed in the export's warnings and the README (grouped by substitution, TINs masked). The 1099 worksheet stops at as_of too. PDFs print CJK, Arabic, Hebrew, Thai and Devanagari (Noto fallbacks, OFL), and so does the viewer. has_full_tin reads Unicode separators. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
92 lines
3.8 KiB
Typst
92 lines
3.8 KiB
Typst
// Asset label sheets: one tag per label, with its QR code.
|
|
//
|
|
// Everything arrives as data in sys.inputs.data (JSON), built by
|
|
// ledger-import::labels: the sheet geometry (in points, from
|
|
// docs/spec/sources/avery-5160.md), and one entry per sheet position. The
|
|
// template only typesets: no label text is ever parsed as markup, and every
|
|
// label is drawn inside a clipped box the size of its cell, so nothing can
|
|
// print past its edge.
|
|
//
|
|
// Shape: { company, geometry: { page_width, page_height, label_width,
|
|
// label_height, left, top, pitch_x, pitch_y, columns, rows, inset },
|
|
// pages: [[null | { tag, description, serial: null | str, qr: svg }]] }
|
|
// Each inner list is one sheet, positions in reading order (left to right,
|
|
// then top to bottom); null is a position left blank.
|
|
|
|
#let d = json(bytes(sys.inputs.data))
|
|
#let g = d.geometry
|
|
#let pt1 = 1pt
|
|
#let ink = luma(15)
|
|
#let quiet = luma(90)
|
|
|
|
#set document(title: d.company + " — Asset labels", author: d.company)
|
|
#set text(font: ("Inter", "Noto Sans CJK SC", "Noto Sans Arabic UI", "Noto Sans Hebrew", "Noto Sans Thai", "Noto Sans Devanagari"), fill: ink, number-width: "tabular", hyphenate: false)
|
|
#set par(leading: 0.28em, spacing: 0pt)
|
|
#set page(width: g.page_width * pt1, height: g.page_height * pt1, margin: 0pt)
|
|
|
|
// The inner box every label's content lives in.
|
|
#let inset = g.inset * pt1
|
|
#let cell-w = g.label_width * pt1
|
|
#let cell-h = g.label_height * pt1
|
|
// The QR code (its 4-module quiet zone included) fills the label's height.
|
|
#let qr-size = cell-h - 2 * inset
|
|
#let gap = 4pt
|
|
#let text-w = cell-w - 2 * inset - qr-size - gap
|
|
|
|
// The largest of `sizes` at which `s` fits in `lines` lines of `width`;
|
|
// at the smallest size, the longest prefix that fits, ending in "…".
|
|
// Text is set as a string, so it is always literal.
|
|
#let fit(s, width, sizes, lines, weight: "regular", fill: ink) = context {
|
|
let set-at(size, body) = block(width: width, text(size: size, weight: weight, fill: fill, body))
|
|
let limit(size) = measure(set-at(size, ("X\n" * lines).trim())).height + 0.01pt
|
|
// Fits: wraps into `lines` lines, and no word is wider than the box
|
|
// (an unbreakable word would overflow sideways, not down).
|
|
let fits(size, body) = (
|
|
measure(set-at(size, body)).height <= limit(size)
|
|
and body.split(" ").all(w => measure(text(size: size, weight: weight, w)).width <= width)
|
|
)
|
|
let chosen = sizes.find(size => fits(size, s))
|
|
if chosen != none {
|
|
set-at(chosen, s)
|
|
} else {
|
|
let size = sizes.last()
|
|
let cs = s.clusters()
|
|
// Binary search for the longest prefix that fits with the ellipsis.
|
|
let lo = 0
|
|
let hi = cs.len()
|
|
while lo < hi {
|
|
let mid = calc.quo(lo + hi + 1, 2)
|
|
if fits(size, cs.slice(0, mid).join().trim() + "…") { lo = mid } else { hi = mid - 1 }
|
|
}
|
|
let prefix = if lo == 0 { "" } else { cs.slice(0, lo).join().trim() }
|
|
set-at(size, prefix + "…")
|
|
}
|
|
}
|
|
|
|
#let label(l) = box(width: cell-w, height: cell-h, clip: true, inset: inset, {
|
|
grid(
|
|
columns: (qr-size, gap, text-w),
|
|
rows: (qr-size,),
|
|
image(bytes(l.qr), format: "svg", width: qr-size, height: qr-size),
|
|
[],
|
|
align(horizon, stack(
|
|
spacing: 2.2pt,
|
|
fit(d.company, text-w, (6pt, 5.5pt, 5pt), 1, fill: quiet),
|
|
fit(l.tag, text-w, (15pt, 13pt, 11pt, 9pt, 8pt, 7pt, 6pt), 1, weight: "bold"),
|
|
fit(l.description, text-w, (7.5pt, 7pt, 6.5pt, 6pt), 2),
|
|
if l.serial != none { fit("S/N " + l.serial, text-w, (6.5pt, 6pt, 5.5pt, 5pt), 1, fill: quiet) },
|
|
)),
|
|
)
|
|
})
|
|
|
|
#for (i, sheet) in d.pages.enumerate() {
|
|
if i > 0 { pagebreak() }
|
|
for (p, l) in sheet.enumerate() {
|
|
if l != none {
|
|
let col = calc.rem(p, g.columns)
|
|
let row = calc.quo(p, g.columns)
|
|
place(top + left, dx: (g.left + col * g.pitch_x) * pt1, dy: (g.top + row * g.pitch_y) * pt1, label(l))
|
|
}
|
|
}
|
|
}
|