Files
Voiced/docs/voiced-invoice-v1.md
T
xavierk 3aa0d33a01 Add units of measure and fractional quantities to rate lines
Quantities can now be time, count, distance, area, weight or volume, or a custom unit, instead of only
second/minute/hour/session/unit.

- Registry in src/lib/units.ts (mirrored in src-tauri/src/units.rs): grouped built-in units plus a custom unit of 1-12
  characters. A rate line's unit is validated when an invoice or a preset is saved; migration M12 drops the old unit
  allow-list from item_presets and keeps every row.
- Time is typed as h:mm: "4:30" hours counts as 4.5, "4:20" as 4.333... (kept exact, so rate x quantity rounds once, the
  same in TypeScript and Rust). Other units take plain decimals up to 3 places. A bad quantity blocks issuing.
- The PDF quantity column shows the unit ("4.5 hr", "12.75 km"); the rate keeps "per hour" / "/hr". RenderItem carries
  priceText, perText and rateShort, so no template parses the rate text with a regex over five hard-coded units.
- Quantity columns are sized from the widest word once a quantity with its unit is wider than 72 pt, so one long
  custom unit cannot squeeze the description (the serenity and citrus-split tables overflowed in the new fixture).
  Classic's quantity column is now content-sized.
- ERPNext: the default UOM map comes from the registry, with fractional-capable UOMs for time, distance, area, weight
  and volume. A custom unit is sent as Nos. A quantity that is not exact to 3 decimals (4:20 hours) is still refused for
  push, as before.
- Goldens and template thumbnails regenerated; the template harness has a "units" fixture with the longest texts.

Claude-Session: https://claude.ai/code/session_01PZypiWDfMkDTeEPeXjRhW5
2026-10-06 09:35:20 +05:30

56 lines
3.9 KiB
Markdown

# voiced.invoice.v1
A neutral export of issued invoices for software Voiced has no direct integration with. Choose invoices in the
History list (checkboxes) or open one, then use "Export selected (voiced.invoice.v1 JSON)" or "(line items CSV)".
The code is `src/lib/invoiceExportV1.ts`; `src/lib/__golden__/invoice-v1.golden.json` is a full example.
## Rules
- **Money is exact.** Every amount appears twice: `<name>_paise` (an integer) and `<name>` (a decimal string with two
places, e.g. `"7310.10"`). Nothing is a float. Rates (percent) are decimal strings such as `"9"` or `"6.25"`.
- **Deterministic.** The same invoices give the same bytes: fixed key order, no timestamps, input order kept.
- **JSON file** is an array with one object per invoice; each object carries `"schema": "voiced.invoice.v1"`.
- Cancelled invoices are included with `"cancelled": true` (their figures are kept; filter them out of sums).
- An unregistered supplier's plain "Invoice" has `document_type: "invoice"`, all tax heads zero and no supplier GSTIN.
## Invoice object
| Key | Meaning |
| --- | --- |
| `schema`, `number`, `issue_date`, `due_date` | `YYYY-MM-DD` dates; `due_date` is null when unset |
| `document_type` | `tax_invoice` or `invoice` |
| `status`, `cancelled`, `cancelled_at`, `cancel_reason` | `status` is `issued` or `cancelled` |
| `currency`, `po_number` | always `INR` |
| `supplier` | `name`, `address`, `gstin` (null if none), `pan`, `gst_registration`, `state {code, name}`; frozen at issue |
| `client` | `name`, `gstin`, `gst_category`, `address {text, line1, line2, city, pincode, state}`, `place_of_supply {code, name}` |
| `items[]` | `line`, `description`, `hsn_sac`, `quantity` (decimal string), `unit`, `rate`, `taxable_value` (each with `_paise`) |
| `tax` | `cgst`, `sgst`, `utgst`, `igst`, each `{rate, amount_paise, amount}`; heads that do not apply are zero |
| `totals` | `subtotal`, `discount`, `taxable_value`, `tax`, `total` (each with `_paise`), `amount_in_words` |
| `reverse_charge`, `notes` | |
| `payments[]` | `date`, `amount` (cash), `tds`, `mode`, `reference` (amounts with `_paise`) |
Notes on the fields:
- **Items.** `taxable_value` of a line is the line amount before the invoice-level discount. `totals.taxable_value` is
`subtotal - discount`, the base the tax is charged on. A fixed-amount line has `quantity "1"` and `unit null`.
- **Units.** On a rate line `unit` is a built-in id (`unit`, `piece`, `set`, `session`, `second`, `minute`, `hour`, `day`,
`week`, `month`, `km`, `m`, `sqft`, `sqm`, `kg`, `litre`) or the custom text typed on the invoice (at most 12 characters:
letters, digits, spaces and `. / -`). `quantity` may be fractional; a time typed as `4:20` hours is stored as the exact
repeating decimal.
- **UTGST.** When the supplier is in a union territory without a legislature (state codes 04, 26, 31, 35, 38) the second
head is reported under `utgst` and `sgst` is zero.
- **Client details.** The name, GSTIN, printed address and place of supply are frozen on the invoice. The structured
address parts and `gst_category` come from the client's saved record at export time (null or derived from the GSTIN
when the invoice has no saved client).
- **Supplier snapshot.** Only name, address, GSTIN, PAN, registration and state are exported; logo paths, e-mail and
phone are not.
## Line-items CSV
One row per line item, UTF-8 with a byte order mark and CRLF line ends; text cells that a spreadsheet would read as a
formula get a leading apostrophe. Columns: `invoice_number, issue_date, due_date, document_type, status, cancelled,
currency, supplier_name, supplier_gstin, client_name, client_gstin, client_gst_category, place_of_supply_code,
place_of_supply, reverse_charge, line, description, hsn_sac, quantity, unit, rate, taxable_value, invoice_discount,
invoice_cgst, invoice_sgst, invoice_utgst, invoice_igst, invoice_total`. The `invoice_*` amounts are filled on the first
row of each invoice only, so summing a column never counts an invoice twice.