Spaces:
Sleeping
Sleeping
| """SQLite persistence layer for VoiceLedger transactions.""" | |
| from __future__ import annotations | |
| import sqlite3 | |
| import tempfile | |
| from datetime import datetime | |
| from pathlib import Path | |
| from typing import Any | |
| import pandas as pd | |
| from voiceledger.config import get_database_path | |
| from voiceledger.ledger.corrections import initialize_correction_log_table | |
| from voiceledger.ledger.customers import add_credit, initialize_customers_table, record_payment | |
| from voiceledger.ledger.inventory import add_stock, initialize_inventory_table, remove_stock | |
| from voiceledger.ledger.settings import initialize_business_settings_table | |
| from voiceledger.parser.schema import Transaction | |
| SCHEMA_SQL = """ | |
| CREATE TABLE IF NOT EXISTS transactions ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| transaction_type TEXT NOT NULL, | |
| item TEXT, | |
| quantity REAL, | |
| unit_price REAL, | |
| amount REAL, | |
| customer TEXT, | |
| payment_status TEXT NOT NULL, | |
| notes TEXT NOT NULL, | |
| confidence REAL NOT NULL, | |
| created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP | |
| ); | |
| """ | |
| COLUMNS = [ | |
| "id", | |
| "transaction_type", | |
| "item", | |
| "quantity", | |
| "unit_price", | |
| "amount", | |
| "customer", | |
| "payment_status", | |
| "notes", | |
| "confidence", | |
| "created_at", | |
| ] | |
| def initialize_database(db_path: str | Path | None = None) -> Path: | |
| """Create the SQLite database and transactions table if needed.""" | |
| path = _resolve_db_path(db_path) | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| with sqlite3.connect(path) as connection: | |
| connection.execute(SCHEMA_SQL) | |
| connection.commit() | |
| initialize_customers_table(path) | |
| initialize_inventory_table(path) | |
| initialize_business_settings_table(path) | |
| initialize_correction_log_table(path) | |
| return path | |
| def add_transaction(transaction: Transaction, db_path: str | Path | None = None) -> int: | |
| """Insert a transaction and return its database id.""" | |
| path = initialize_database(db_path) | |
| payload = transaction.model_dump() | |
| with sqlite3.connect(path) as connection: | |
| cursor = connection.execute( | |
| """ | |
| INSERT INTO transactions ( | |
| transaction_type, | |
| item, | |
| quantity, | |
| unit_price, | |
| amount, | |
| customer, | |
| payment_status, | |
| notes, | |
| confidence, | |
| created_at | |
| ) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | |
| """, | |
| ( | |
| payload["transaction_type"], | |
| payload["item"], | |
| payload["quantity"], | |
| payload["unit_price"], | |
| payload["amount"], | |
| payload["customer"], | |
| payload["payment_status"], | |
| payload["notes"], | |
| payload["confidence"], | |
| datetime.now().isoformat(sep=" ", timespec="seconds"), | |
| ), | |
| ) | |
| connection.commit() | |
| transaction_id = int(cursor.lastrowid) | |
| _apply_customer_balance_update(transaction, path) | |
| _apply_inventory_update(transaction, path) | |
| return transaction_id | |
| def get_transactions(db_path: str | Path | None = None) -> pd.DataFrame: | |
| """Return all saved transactions as a Pandas DataFrame.""" | |
| path = initialize_database(db_path) | |
| with sqlite3.connect(path) as connection: | |
| rows = connection.execute( | |
| """ | |
| SELECT | |
| id, | |
| transaction_type, | |
| item, | |
| quantity, | |
| unit_price, | |
| amount, | |
| customer, | |
| payment_status, | |
| notes, | |
| confidence, | |
| created_at | |
| FROM transactions | |
| ORDER BY id DESC | |
| """ | |
| ).fetchall() | |
| records: list[dict[str, Any]] = [dict(zip(COLUMNS, row, strict=True)) for row in rows] | |
| return pd.DataFrame.from_records(records, columns=COLUMNS) | |
| def get_transaction(transaction_id: int, db_path: str | Path | None = None) -> Transaction | None: | |
| """Return one transaction by id, or None when it does not exist.""" | |
| path = initialize_database(db_path) | |
| with sqlite3.connect(path) as connection: | |
| row = connection.execute( | |
| """ | |
| SELECT | |
| transaction_type, | |
| item, | |
| quantity, | |
| unit_price, | |
| amount, | |
| customer, | |
| payment_status, | |
| notes, | |
| confidence | |
| FROM transactions | |
| WHERE id = ? | |
| """, | |
| (int(transaction_id),), | |
| ).fetchone() | |
| if row is None: | |
| return None | |
| payload = dict(zip(COLUMNS[1:-1], row, strict=True)) | |
| return Transaction.model_validate(payload) | |
| def update_transaction( | |
| transaction_id: int, | |
| transaction: Transaction, | |
| db_path: str | Path | None = None, | |
| ) -> bool: | |
| """Update a transaction and rebuild derived balances when found.""" | |
| path = initialize_database(db_path) | |
| payload = transaction.model_dump() | |
| with sqlite3.connect(path) as connection: | |
| cursor = connection.execute( | |
| """ | |
| UPDATE transactions | |
| SET | |
| transaction_type = ?, | |
| item = ?, | |
| quantity = ?, | |
| unit_price = ?, | |
| amount = ?, | |
| customer = ?, | |
| payment_status = ?, | |
| notes = ?, | |
| confidence = ? | |
| WHERE id = ? | |
| """, | |
| ( | |
| payload["transaction_type"], | |
| payload["item"], | |
| payload["quantity"], | |
| payload["unit_price"], | |
| payload["amount"], | |
| payload["customer"], | |
| payload["payment_status"], | |
| payload["notes"], | |
| payload["confidence"], | |
| int(transaction_id), | |
| ), | |
| ) | |
| connection.commit() | |
| updated = cursor.rowcount > 0 | |
| if updated: | |
| rebuild_derived_tables(path) | |
| return updated | |
| def delete_transaction(transaction_id: int, db_path: str | Path | None = None) -> bool: | |
| """Delete a transaction and rebuild derived balances when found.""" | |
| path = initialize_database(db_path) | |
| with sqlite3.connect(path) as connection: | |
| cursor = connection.execute( | |
| "DELETE FROM transactions WHERE id = ?", | |
| (int(transaction_id),), | |
| ) | |
| connection.commit() | |
| deleted = cursor.rowcount > 0 | |
| if deleted: | |
| rebuild_derived_tables(path) | |
| return deleted | |
| def export_transactions_csv( | |
| db_path: str | Path | None = None, | |
| export_path: str | Path | None = None, | |
| ) -> Path: | |
| """Export all transactions to a CSV file and return the file path.""" | |
| ledger = get_transactions(db_path) | |
| if export_path is None: | |
| export_path = Path(tempfile.gettempdir()) / "voiceledger_transactions.csv" | |
| path = Path(export_path).expanduser() | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| ledger.to_csv(path, index=False, columns=COLUMNS) | |
| return path | |
| def rebuild_derived_tables(db_path: str | Path | None = None) -> None: | |
| """Rebuild customer balances and inventory from saved transactions.""" | |
| path = initialize_database(db_path) | |
| with sqlite3.connect(path) as connection: | |
| connection.execute("DELETE FROM customers") | |
| connection.execute("DELETE FROM inventory") | |
| rows = connection.execute( | |
| """ | |
| SELECT | |
| transaction_type, | |
| item, | |
| quantity, | |
| unit_price, | |
| amount, | |
| customer, | |
| payment_status, | |
| notes, | |
| confidence | |
| FROM transactions | |
| ORDER BY id ASC | |
| """ | |
| ).fetchall() | |
| connection.commit() | |
| for row in rows: | |
| transaction = Transaction.model_validate(dict(zip(COLUMNS[1:-1], row, strict=True))) | |
| _apply_customer_balance_update(transaction, path) | |
| _apply_inventory_update(transaction, path) | |
| def _resolve_db_path(db_path: str | Path | None) -> Path: | |
| """Resolve an explicit or configured database path.""" | |
| if db_path is None: | |
| return get_database_path() | |
| return Path(db_path).expanduser() | |
| def _apply_customer_balance_update(transaction: Transaction, db_path: Path) -> None: | |
| """Apply customer balance side effects for credit-related transactions.""" | |
| if not transaction.customer or transaction.amount is None: | |
| return | |
| if transaction.transaction_type == "customer_credit": | |
| add_credit(transaction.customer, transaction.amount, db_path) | |
| elif transaction.transaction_type == "customer_payment": | |
| record_payment(transaction.customer, transaction.amount, db_path) | |
| def _apply_inventory_update(transaction: Transaction, db_path: Path) -> None: | |
| """Apply inventory side effects for stock-related transactions.""" | |
| if not transaction.item or transaction.quantity is None: | |
| return | |
| if transaction.transaction_type == "inventory_purchase": | |
| add_stock(transaction.item, transaction.quantity, db_path) | |
| elif transaction.transaction_type == "sale": | |
| remove_stock(transaction.item, transaction.quantity, db_path) | |