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
56 lines
3.9 KiB
Markdown
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.
|