311 lines
19 KiB
Markdown
311 lines
19 KiB
Markdown
# Voiced
|
|
|
|
An offline-first GST invoicing desktop app built by **Bongbetic**.
|
|
|
|
Voiced turns a few details into a clean, print-ready PDF invoice. It keeps a running invoice-number series in a
|
|
local database, remembers your vendor, client and bank details, and works without an account or a network
|
|
connection. The only optional network feature is the ERPNext push, which you configure yourself.
|
|
|
|
Targets: Void Linux (xbps, WebKitGTK 2.50 or newer) and Windows 10/11 (NSIS installer, WebView2).
|
|
|
|
Current release: **1.0.2**. Installers and the Void package are attached to the release on the project's Gitea
|
|
page (see "Installing").
|
|
|
|
## Features
|
|
|
|
- **First-run setup.** A four-step wizard (business, branding, bank, numbering) starts blank: no vendor, bank or tax
|
|
details are pre-filled, and a business name and state are required. **Settings, Data, Run setup again** reopens it
|
|
with your current details; invoices, clients and backups are kept, and numbering carries on unless you change the
|
|
series prefix or digits.
|
|
- **Invoices.** Automatic numbering in a persistent series (for example `INV/2026-001`) with a one-click new series,
|
|
drafts, issue and cancel. An issued invoice is immutable: it stores a snapshot of the vendor, client, GST figures
|
|
and layout. Line items are a fixed amount or rate x quantity (per second, minute, hour, session or unit), with
|
|
item presets.
|
|
- **GST.** All money and tax maths is done in integer paise. CGST+SGST, IGST or UTGST follow from the supplier's
|
|
state and the place of supply, and a mismatch is rejected. An unregistered vendor gets a plain "Invoice" with no
|
|
tax. GSTIN, PAN, IFSC, email and phone are validated; amounts are written in Indian words.
|
|
- **Templates.** 14 templates: Voiced Standard (Classic) and 13 designs measured from reference originals (Linea,
|
|
Monogram, Serenity, Crimson Grid, Tangerine Ledger, Slate Band, Teal Swoosh, Purple Pop, Citrus Split, Monolith,
|
|
Cobalt Stripe, Marble, Highlighter). Your logo fills the template's logo slot; a few templates offer an opt-in
|
|
logo position. Specs are in `docs/templates/`.
|
|
- **Page setup.** A4 or Letter, and margin presets (template, narrow 36 pt, normal 54 pt).
|
|
- **Export modes.** *Searchable* PDF (text layer kept) or *flattened* (pages rasterised at 300 DPI and rebuilt as
|
|
an image-only PDF). Issued PDFs are archived by content hash, and "Original as issued" re-exports the exact
|
|
archived bytes.
|
|
- **Fonts.** 11 bundled font families (see "Fonts and licences"). The three commercial template faces (Now, Gotham,
|
|
Open Sauce One) can be overridden with your own licensed files under Settings, Fonts.
|
|
- **Clients and presets.** Saved clients with structured addresses, state and GST category; item presets.
|
|
- **Payments.** Record payments against an issued invoice, including TDS deducted by the client. Outstanding
|
|
balance is cash plus TDS against the invoice total.
|
|
- **History.** Filterable invoice list, detail view with payments, and CSV or JSON export of the list.
|
|
- **Backup and restore.** Manual and automatic daily backups of the database, assets, PDF archive and imported fonts.
|
|
- **Export for other software.** `voiced.invoice.v1` JSON and line-item CSV for selected invoices
|
|
(see `docs/voiced-invoice-v1.md`).
|
|
- **ERPNext integration.** Push an invoice (as draft or submitted) with its PDF attached, and record payments, to an
|
|
ERPNext v15 site.
|
|
- Light and dark (Carbon g10/g100) themes.
|
|
|
|
## Tech stack
|
|
|
|
| Layer | Choice |
|
|
| ------- | ------------------------------------------------------------------------------------------- |
|
|
| Shell | [Tauri v2](https://v2.tauri.app/) (Rust core, WebKitGTK on Linux, WebView2 on Windows) |
|
|
| UI | React 19 + TypeScript + [IBM Carbon](https://carbondesignsystem.com/) (`@carbon/react`) |
|
|
| Storage | SQLite via `rusqlite` (bundled), numbered migrations |
|
|
| PDF | `@react-pdf/renderer` 4.9.0 in a Web Worker, pdf.js for preview and flattening |
|
|
| ERPNext | `reqwest` with rustls (ring provider); all HTTP runs in Rust |
|
|
|
|
## Prerequisites
|
|
|
|
- **Node.js 22** and npm
|
|
- **Rust** stable, via [rustup](https://rustup.rs/)
|
|
- Linux only: WebKitGTK 4.1, libsoup3, GTK3, librsvg, `pkg-config`, `clang`/`llvm`/`lld`
|
|
|
|
## Development
|
|
|
|
```bash
|
|
npm install
|
|
npm run app:dev # tauri dev, served from localhost:1420 (does not exercise the tauri:// protocol)
|
|
```
|
|
|
|
| Command | What it does |
|
|
| --- | --- |
|
|
| `npm run build` | `tsc --noEmit` and `vite build` (the type-check gate) |
|
|
| `npm test` | `vitest run`, about 7 minutes; run on a quiet machine because load starves the sweep tests |
|
|
| `npm run fonts:verify` | Checks the bundled fonts against `public/fonts/manifest.json` (`fonts:manifest` regenerates it) |
|
|
| `npm run logo:measure` | Regenerates `src/pdf/templates/slots.generated.ts` from the reference PDFs; must be idempotent |
|
|
| `npm run thumbnails` | Regenerates `public/templates/*.png` from the synthetic sample invoice (neutral vendor and bank, generated wordmark) |
|
|
| `node scripts/logo/make-sample-logo.mjs` | Regenerates the synthetic sample logo in `src/pdf/testing/fixtures/` |
|
|
| `npm run templates:compare` | reference vs Voiced side-by-side PNGs under `/tmp` |
|
|
| `cargo test --manifest-path src-tauri/Cargo.toml` | Rust unit tests |
|
|
| `npm run app:build` | Production build for the current platform (custom-protocol) |
|
|
|
|
The release procedure, including the manual first-run checklist, is in `docs/RELEASE.md`.
|
|
|
|
## Installing
|
|
|
|
Download the files for your platform from the release page of the project on `git.bongbetic.com` (Releases, then
|
|
`v1.0.2`).
|
|
|
|
- **Windows 10/11:** run `Voiced_1.0.2_x64-setup.exe`. It is a per-user install and is not code-signed, so Windows
|
|
SmartScreen may warn: choose "More info", then "Run anyway".
|
|
- **Void Linux:** install `voiced-1.0.2_1.x86_64.xbps` from a local repository (see "Installing on Void Linux").
|
|
|
|
On first launch the setup wizard asks for your business details, logo, bank account and invoice numbering. Nothing is
|
|
pre-filled. Uninstalling does not delete your data (see "Where data lives"); to start from scratch use
|
|
Settings, Data, Run setup again, or remove the data directory after taking a backup.
|
|
|
|
## Where data lives
|
|
|
|
The Tauri identifier is `com.bongbetic.voiced`. Voiced uses two directories, which differ only on Windows:
|
|
|
|
| What | Linux | Windows |
|
|
| --- | --- | --- |
|
|
| `app_data_dir`: database `voiced.db`, `assets/` (logos, signatures), `backups/` (`auto/`, `pre-restore-*`, pre-migration copies) | `~/.local/share/com.bongbetic.voiced/` | `%APPDATA%\com.bongbetic.voiced\` |
|
|
| `app_local_data_dir`: `archive/<sha256>.pdf` (issued PDFs), `fonts/` (fonts you import) | `~/.local/share/com.bongbetic.voiced/` | `%LOCALAPPDATA%\com.bongbetic.voiced\` |
|
|
|
|
On Linux `XDG_DATA_HOME` moves both. On Windows the archive and imported fonts stay out of the roaming profile so a
|
|
growing archive and licensed font files are not copied between machines. The window size and position are kept by
|
|
the window-state plugin in the app config directory (`~/.config/com.bongbetic.voiced/` on Linux).
|
|
|
|
## Backup and restore
|
|
|
|
Settings, Data tab.
|
|
|
|
- **Backup now** writes a zip: a consistent snapshot of `voiced.db`, `assets/`, `archive/`, `fonts/` and a
|
|
`manifest.json` with a sha256 for every file.
|
|
- **Automatic backup.** Once per calendar day, while the app is open, Voiced writes
|
|
`backups/auto/voiced-auto-YYYYMMDD.zip` and keeps the newest 14.
|
|
- **Restore** validates the zip (manifest, hashes, database integrity check) and stages it. Nothing live changes until
|
|
the next start: use the "Restart" button or close and reopen the app. On that start the current database, assets,
|
|
archive and fonts are moved to `backups/pre-restore-<timestamp>/` (a safety copy that is never pruned), then the
|
|
restored files are moved in. If anything fails the app rolls back and starts on the old data. A backup from an
|
|
older schema is migrated after restoring.
|
|
- **Backups contain secrets.** The ERPNext API secret is stored in the SQLite database, so every backup (manual,
|
|
automatic and pre-restore) holds it in plain form, together with your clients, invoices and bank details. Keep
|
|
backup files on storage you trust, do not email or upload them casually, and rotate the ERPNext API key if a
|
|
backup is lost.
|
|
|
|
## Fonts and licences
|
|
|
|
Eleven families are bundled as static TTF files in `public/fonts/<family>/`, each with its `OFL.txt`: DM Sans,
|
|
IBM Plex Mono, IBM Plex Sans, Inter, Jost, Lustria, Montserrat, Noto Sans Devanagari, Noto Sans Kannada, Open Sans
|
|
and Poppins. All are under the SIL Open Font License 1.1. `public/fonts/manifest.json` lists every file with its
|
|
hash and metrics, and `npm run fonts:verify` checks it. PDFs embed subsets, so the app never depends on system
|
|
fonts.
|
|
|
|
The Void package also installs these fonts system-wide under `/usr/share/fonts/voiced/<family>/` and each OFL text
|
|
as `/usr/share/licenses/voiced/<family-folder>-OFL.txt`.
|
|
|
|
**Now, Gotham and Open Sauce One** are not bundled. Voiced draws Now with Jost, Gotham with Montserrat and Open
|
|
Sauce One with Poppins (Marble, Citrus Split and Purple Pop use these faces). If you hold a licence
|
|
for the real fonts, import the files under Settings, Fonts (TTF, OTF or WOFF, one file per weight; WOFF2, variable
|
|
and font-collection files are refused). Importing is your confirmation that your licence covers this use. The files
|
|
are stored on this computer only, are included in backups, and issued invoices keep the fonts they were issued with.
|
|
|
|
## ERPNext integration
|
|
|
|
Settings, Integrations. Verified live against ERPNext v15.121.6 (Frappe 15.121), with and without India Compliance
|
|
15.32. v14 and v16 have not been tested.
|
|
|
|
1. **Integration user.** In ERPNext create a dedicated user (type System User) with the roles **Accounts User** and
|
|
**Sales User**. Open the user, Settings tab, API Access, Generate Keys, and copy the secret once. Accounts User
|
|
covers invoices, payments and attachments; Sales User covers customers and addresses (Accounts User alone gets
|
|
HTTP 403 on Customer). Do not use Administrator or System Manager keys. If you change roles, run
|
|
`bench clear-cache`.
|
|
2. **Rounding.** In ERPNext System Settings set Rounding Method to **Commercial Rounding**. ERPNext defaults to
|
|
Banker's Rounding while Voiced rounds half-paise up, so some totals would differ by 1 paise. Voiced detects a
|
|
mismatch after creating the draft, keeps it as a draft, never submits it, and names this fix.
|
|
3. **Connect.** Enter the site address, API key and secret and run "Test connection". Use `https://` for real
|
|
servers; plain `http://` is accepted only for `localhost`, `127.0.0.1`, `*.localhost` and `*.test`. The secret is
|
|
never shown again, and it is not sent to a different server than the one it was saved for.
|
|
4. **Company, accounts and units.** Pick the company, the output tax accounts (CGST, SGST, IGST), the bank account for
|
|
payments and the unit-of-measure mapping from the loaded option lists.
|
|
5. **Naming.** *Mirror* (default) names the ERPNext document with the Voiced number, which needs a site with API v2
|
|
naming (Frappe 15.73 or newer). *Series* lets an ERPNext naming series name the document and puts the Voiced
|
|
number in the remarks.
|
|
6. **India Compliance.** Detected by the connection test. It requires a 6 or 8 digit HSN/SAC code on each row
|
|
(checked at submit), limits document names to 16 characters (mirror mode checks this before posting), and Voiced
|
|
always sends `is_reverse_charge` as 0 with a warning.
|
|
|
|
Not covered: overseas and SEZ customers, multi-currency, reverse charge booked on RCM accounts, e-invoice and
|
|
e-waybill, TLS-terminating proxies beyond plain HTTPS, and Windows. The live test setup is in
|
|
`scripts/erpnext-e2e/README.md`.
|
|
|
|
## Building for Windows (NSIS, cross-compiled from Linux)
|
|
|
|
Tauri supports only the MSVC target. [`cargo-xwin`](https://github.com/rust-cross/cargo-xwin) cross-compiles it
|
|
and Tauri produces an **NSIS installer** (a `.msi` needs a real Windows host).
|
|
|
|
```bash
|
|
# one-time setup
|
|
rustup target add x86_64-pc-windows-msvc
|
|
cargo install --locked cargo-xwin
|
|
# plus makensis (NSIS), LLVM/LLD and clang on the host
|
|
|
|
npm run app:build:windows # embeds the small WebView2 bootstrapper (needs internet if WebView2 is missing)
|
|
npm run app:build:windows:offline # embeds the full WebView2 runtime (over 100 MB larger), for offline machines
|
|
```
|
|
|
|
Output: `src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/Voiced_<version>_x64-setup.exe`, and the portable
|
|
binary in the sibling `release/` folder. The installer is a per-user install (`installMode: currentUser`). The
|
|
frontend, fonts and pdf.js are compiled into the executable. Size budget: about 25 MB for the standard installer.
|
|
`cargo tree -i aws-lc-sys --target x86_64-pc-windows-msvc` must print nothing (the TLS stack is rustls with the ring
|
|
provider only, which cross-compiles cleanly).
|
|
|
|
Hosts without an `nsis` package (for example Void Linux):
|
|
|
|
1. Provide `makensis` and put it on `PATH`.
|
|
2. Point it at a full NSIS data directory (3.11 recommended, for `Win/RestartManager.nsh`) with `NSISDIR`.
|
|
3. Pre-seed Tauri's plugin cache so it does not have to download it:
|
|
|
|
```
|
|
mkdir -p ~/.cache/tauri/NSIS/Plugins/x86-unicode/additional
|
|
# nsis_tauri_utils.dll (SHA1 75197fee3c6a814fe035788d1c34ead39349b860)
|
|
# from https://github.com/tauri-apps/nsis-tauri-utils/releases
|
|
```
|
|
|
|
The installer has been installed and run once in a Windows 11 virtual machine; it has not been tried on physical
|
|
hardware.
|
|
|
|
## Installing on Void Linux
|
|
|
|
### Prebuilt binary package (no xbps-src needed)
|
|
|
|
```bash
|
|
./packaging/void/build-xbps.sh # -> build/void/repo/voiced-<version>_1.<arch>.xbps
|
|
echo "repository=$(pwd)/build/void/repo" | sudo tee /etc/xbps.d/99-voiced-local.conf
|
|
sudo xbps-install -Sy voiced
|
|
```
|
|
|
|
The script runs `npx tauri build --no-bundle --features custom-protocol` (using your existing `node_modules`), stages
|
|
the install tree and runs `xbps-create` and `xbps-rindex`. Launch **Voiced** from the application menu or run
|
|
`voiced`; remove it with `sudo xbps-remove voiced`.
|
|
|
|
The package installs `/usr/bin/voiced`, a desktop entry, hicolor icons, the bundled fonts under
|
|
`/usr/share/fonts/voiced/<family>/`, the OFL texts under `/usr/share/licenses/voiced/`, and `INSTALL` and `REMOVE`
|
|
action scripts that run `fc-cache -f /usr/share/fonts/voiced` after install and removal. The `voiced/` font
|
|
subdirectory avoids file conflicts with Void's `font-ibm-plex-ttf` and `noto-fonts-ttf`. It depends on
|
|
`libwebkit2gtk41>=2.50`, `fontconfig`, `xdg-utils`, `hicolor-icon-theme` and `desktop-file-utils`.
|
|
|
|
### Building through xbps-src
|
|
|
|
```bash
|
|
mkdir -p void-packages/srcpkgs/voiced
|
|
cp packaging/void/template packaging/void/INSTALL packaging/void/REMOVE void-packages/srcpkgs/voiced/
|
|
cd void-packages && ./xbps-src pkg voiced
|
|
```
|
|
|
|
`INSTALL` and `REMOVE` must sit next to the template; xbps-src packs them. The source is a tagged archive of the
|
|
private repository (see the comments at the top of the template; `checksum=SKIP` until a tarball is published).
|
|
xbps-src builds in a network-less chroot, so vendor the dependencies first: run `cargo vendor` in `src-tauri/` (with a
|
|
`.cargo/config.toml` pointing at `vendor/`) and populate the npm cache, and include the results in the source
|
|
archive. The template runs `npm ci` (not `--omit=optional`: that removes
|
|
the native rollup and esbuild binaries the Vite build needs, and `@napi-rs/canvas` is type-checked by `tsc`).
|
|
No OpenSSL is needed (rustls), and the
|
|
dependencies of the ERPNext and backup code (`reqwest`, `zip`) are pure Rust.
|
|
|
|
## Project layout
|
|
|
|
```
|
|
src/ React + Carbon frontend
|
|
views/ Onboarding, NewInvoice, InvoiceHistory, InvoiceDetail, Clients, AppSettings, SeriesSettings
|
|
components/ AppShell, TemplatePicker, PageSetupControls, PdfPreview, LogoBranding,
|
|
FontOverrideTable, DataBackupPanel, ErpnextSettingsPanel, RecordPaymentModal, ...
|
|
hooks/ Export, PDF preview, user fonts, shortcuts
|
|
lib/ Typed Tauri wrapper (api.ts), money, GST helpers, exportFlow, historyExport,
|
|
invoiceExportV1, ERPNext UI helpers
|
|
pdf/ PDF engine
|
|
model/ buildRenderModel (invoice -> render model)
|
|
engine/ geometry (A4/Letter, margins), logo slots, layout fingerprint
|
|
templates/ <family>/ with tokens.ts, plan.ts, Layout.tsx; registry.ts, catalog.ts, contract.ts
|
|
blocks/ decor/ Shared layout blocks and decor
|
|
fonts/ Font registration, roles, glyph priming
|
|
render/ renderCore, Web Worker, latest-wins client
|
|
flatten.ts assemble.ts Rasterise at 300 DPI and rebuild an image-only PDF
|
|
testing/ Goldens (layout fingerprints), self-test fixtures, synthetic sample logo
|
|
src-tauri/src/ Rust backend
|
|
db.rs Schema as numbered migrations (copies the DB aside before migrating)
|
|
gst.rs Integer-paise money and GST maths
|
|
logo.rs Logo trimming, quantising, measuring
|
|
commands/ Tauri commands: invoice, series, clients, payments, presets, settings, assets,
|
|
archive, fonts, backup, files, raw (binary IPC), erpnext, logo
|
|
integrations/erpnext/ ERPNext client, mapping, discovery, push
|
|
selftest.rs Unattended real-webview self-test
|
|
public/fonts/ Bundled TTF families, OFL texts, manifest.json (plus legacy IBM Plex web fonts)
|
|
public/templates/ Template thumbnails (rendered from synthetic sample data)
|
|
docs/ Template specs, voiced.invoice.v1, release checklist
|
|
scripts/ Font, logo, template, thumbnail and ERPNext end-to-end tooling
|
|
packaging/void/ xbps-src template, build-xbps.sh, desktop entry, INSTALL and REMOVE scripts
|
|
```
|
|
|
|
## Tests and gates
|
|
|
|
Before a release (details in `docs/RELEASE.md`): `npm run build`, `npm test`, `npm run fonts:verify`,
|
|
`cargo test --manifest-path src-tauri/Cargo.toml`, and the real-webview self-test on a debug or installed build:
|
|
|
|
```bash
|
|
npm run tauri -- build --debug --no-bundle --features custom-protocol
|
|
VOICED_SELFTEST_OUT=/tmp/voiced-e2e/report.json XDG_DATA_HOME=/tmp/voiced-e2e/data \
|
|
XDG_CONFIG_HOME=/tmp/voiced-e2e/config timeout 180 src-tauri/target/debug/voiced
|
|
```
|
|
|
|
The app is inert unless `VOICED_SELFTEST_OUT` is set; `ok: true` is expected in the report. The same checks run from
|
|
Settings, Diagnostics.
|
|
|
|
## Known limitations
|
|
|
|
- Windows and WebView2 have only been exercised in a Windows 11 virtual machine, not on physical hardware; the NSIS
|
|
installer is cross-compiled and unsigned.
|
|
- The 13 reference-based templates are re-creations of reference designs. the reference design's content licence may restrict this, so
|
|
distribute them only to the intended customer until it is confirmed.
|
|
- The SAC code for the vendor's services and the signature rule still need confirmation from a chartered accountant.
|
|
- Now, Gotham and Open Sauce One are replaced by free look-alikes unless you import licensed files.
|
|
- ERPNext: only v15 is verified; HTTP is allowed only for local hosts; see "Not covered" above.
|
|
- Not implemented (deferred): copies and watermarks, PDF/A, custom margins, user background textures, SVG logo
|
|
import, CESS, composition and SEZ invoices.
|
|
|
|
## Ownership
|
|
|
|
© Bongbetic. All rights reserved. This is a private application.
|