Initial commit: Voiced invoice desktop app
Offline-first invoicing app for Test Vendor, built by Bongbetic. - Tauri v2 (Rust) + React/TypeScript + IBM Carbon Design System UI - SQLite (rusqlite) storage with a persistent invoice-number series - Guided first-run setup, pre-filled vendor/bank details - Fixed-amount or rate x quantity line items (per sec/min/hour/session/unit) - GST as CGST+SGST or IGST, Indian amount-in-words - Optional signature, with a computer-generated note as fallback - Print-ready PDF export via @react-pdf/renderer - Windows cross-compile via cargo-xwin (NSIS installer) Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
use rusqlite::{params, Connection};
|
||||
use std::path::Path;
|
||||
|
||||
/// The full schema. Every statement is idempotent so it doubles as the migration step.
|
||||
const SCHEMA: &str = r#"
|
||||
CREATE TABLE IF NOT EXISTS app_settings (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
vendor_name TEXT NOT NULL DEFAULT '',
|
||||
vendor_address TEXT NOT NULL DEFAULT '',
|
||||
vendor_email TEXT NOT NULL DEFAULT '',
|
||||
vendor_phone TEXT NOT NULL DEFAULT '',
|
||||
vendor_pan TEXT NOT NULL DEFAULT '',
|
||||
vendor_gstin TEXT NOT NULL DEFAULT '',
|
||||
vendor_state_code TEXT NOT NULL DEFAULT '29',
|
||||
logo_path TEXT,
|
||||
signature_path TEXT,
|
||||
default_bank_id INTEGER,
|
||||
default_tax_rate REAL NOT NULL DEFAULT 18,
|
||||
default_tax_type TEXT NOT NULL DEFAULT 'cgst_sgst',
|
||||
payment_terms_days INTEGER NOT NULL DEFAULT 30,
|
||||
currency TEXT NOT NULL DEFAULT 'INR',
|
||||
onboarded INTEGER NOT NULL DEFAULT 0,
|
||||
theme TEXT NOT NULL DEFAULT 'g10'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bank_accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
label TEXT NOT NULL DEFAULT '',
|
||||
bank_name TEXT NOT NULL DEFAULT '',
|
||||
account_name TEXT NOT NULL DEFAULT '',
|
||||
account_no TEXT NOT NULL DEFAULT '',
|
||||
branch TEXT NOT NULL DEFAULT '',
|
||||
ifsc TEXT NOT NULL DEFAULT '',
|
||||
is_default INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS invoice_series (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
prefix TEXT NOT NULL,
|
||||
padding INTEGER NOT NULL DEFAULT 3,
|
||||
next_number INTEGER NOT NULL DEFAULT 1,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS clients (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
address TEXT NOT NULL DEFAULT '',
|
||||
gstin TEXT NOT NULL DEFAULT '',
|
||||
state_code TEXT NOT NULL DEFAULT '',
|
||||
po_number TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS invoices (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
number TEXT NOT NULL UNIQUE,
|
||||
series_id INTEGER,
|
||||
invoice_date TEXT NOT NULL,
|
||||
due_date TEXT NOT NULL DEFAULT '',
|
||||
client_id INTEGER,
|
||||
client_name TEXT NOT NULL DEFAULT '',
|
||||
client_address TEXT NOT NULL DEFAULT '',
|
||||
client_gstin TEXT NOT NULL DEFAULT '',
|
||||
po_number TEXT NOT NULL DEFAULT '',
|
||||
place_of_supply_state_code TEXT NOT NULL DEFAULT '',
|
||||
subtotal REAL NOT NULL DEFAULT 0,
|
||||
discount REAL NOT NULL DEFAULT 0,
|
||||
tax_type TEXT NOT NULL DEFAULT 'none',
|
||||
tax_rate REAL NOT NULL DEFAULT 0,
|
||||
cgst_amount REAL NOT NULL DEFAULT 0,
|
||||
sgst_amount REAL NOT NULL DEFAULT 0,
|
||||
igst_amount REAL NOT NULL DEFAULT 0,
|
||||
total REAL NOT NULL DEFAULT 0,
|
||||
amount_in_words TEXT NOT NULL DEFAULT '',
|
||||
bank_account_id INTEGER,
|
||||
bank_snapshot TEXT NOT NULL DEFAULT '',
|
||||
signature_path TEXT,
|
||||
notes TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'issued',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS invoice_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
invoice_id INTEGER NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
mode TEXT NOT NULL DEFAULT 'fixed',
|
||||
rate REAL NOT NULL DEFAULT 0,
|
||||
unit TEXT NOT NULL DEFAULT 'unit',
|
||||
quantity REAL NOT NULL DEFAULT 1,
|
||||
amount REAL NOT NULL DEFAULT 0,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_invoice_items_invoice ON invoice_items(invoice_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_created ON invoices(created_at DESC);
|
||||
"#;
|
||||
|
||||
pub fn open(path: &Path) -> rusqlite::Result<Connection> {
|
||||
let conn = Connection::open(path)?;
|
||||
conn.pragma_update(None, "journal_mode", "WAL")?;
|
||||
conn.pragma_update(None, "foreign_keys", "ON")?;
|
||||
conn.execute_batch(SCHEMA)?;
|
||||
seed(&conn)?;
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
/// Insert the first-run defaults the very first time the app is opened.
|
||||
/// Vendor details are pre-filled from the owner's existing invoice.
|
||||
fn seed(conn: &Connection) -> rusqlite::Result<()> {
|
||||
let settings_count: i64 =
|
||||
conn.query_row("SELECT COUNT(*) FROM app_settings", [], |r| r.get(0))?;
|
||||
if settings_count == 0 {
|
||||
conn.execute(
|
||||
r#"INSERT INTO app_settings
|
||||
(id, vendor_name, vendor_address, vendor_email, vendor_phone,
|
||||
vendor_pan, vendor_gstin, vendor_state_code, default_tax_rate,
|
||||
default_tax_type, payment_terms_days, currency, onboarded, theme)
|
||||
VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, '29', 18, 'cgst_sgst', 30, 'INR', 0, 'g10')"#,
|
||||
params![
|
||||
"Test Vendor",
|
||||
"1 Test Road, Bangalore - 560001",
|
||||
"vendor@example.com",
|
||||
"9000000000",
|
||||
"ABCDE1234F",
|
||||
"",
|
||||
],
|
||||
)?;
|
||||
}
|
||||
|
||||
let bank_count: i64 =
|
||||
conn.query_row("SELECT COUNT(*) FROM bank_accounts", [], |r| r.get(0))?;
|
||||
if bank_count == 0 {
|
||||
conn.execute(
|
||||
r#"INSERT INTO bank_accounts
|
||||
(label, bank_name, account_name, account_no, branch, ifsc, is_default)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, 1)"#,
|
||||
params![
|
||||
"Primary",
|
||||
"State Bank of India",
|
||||
"Test Vendor",
|
||||
"000000000000",
|
||||
"000001",
|
||||
"TEST0000001",
|
||||
],
|
||||
)?;
|
||||
let bank_id = conn.last_insert_rowid();
|
||||
conn.execute(
|
||||
"UPDATE app_settings SET default_bank_id = ?1 WHERE id = 1",
|
||||
params![bank_id],
|
||||
)?;
|
||||
}
|
||||
|
||||
let series_count: i64 =
|
||||
conn.query_row("SELECT COUNT(*) FROM invoice_series", [], |r| r.get(0))?;
|
||||
if series_count == 0 {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let year = chrono::Local::now().format("%Y").to_string();
|
||||
conn.execute(
|
||||
r#"INSERT INTO invoice_series (prefix, padding, next_number, is_active, created_at)
|
||||
VALUES (?1, 3, 1, 1, ?2)"#,
|
||||
params![format!("INV/{}-", year), now],
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Format an invoice number from a series definition, e.g. "INV/2026-" + 1 -> "INV/2026-001".
|
||||
pub fn format_number(prefix: &str, padding: i64, number: i64) -> String {
|
||||
format!("{}{:0width$}", prefix, number, width = padding.max(1) as usize)
|
||||
}
|
||||
Reference in New Issue
Block a user