Add payments with TDS, invoice detail view and filterable History (Phase E2)
- Migration M8 adds payments (cash, TDS, mode, reference). Payment status is derived, never stored: unpaid, partially paid, paid, or overdue. Overpayment is rejected, TDS counts toward settling, and only issued invoices accept payments. list_invoices and get_invoice carry the paid, TDS and balance summary. - New invoice detail view: archived-PDF preview, payments list, Record payment modal, Export, Duplicate (as a draft), Re-render and Cancel. An invoice with payments cannot be cancelled until they are deleted. - History gets search, status, financial-year, month and client filters, sortable columns, pagination, footer totals and an FY summary. Cancelled invoices are excluded from totals. The per-row archive-status calls are gone; archiving is on the detail page. - Rust tests for payments and the list summary; vitest for fiscal-year, money, filter and duplicate helpers. PDF output and fingerprint goldens are unchanged. The new UI has not been run in a webview yet.
This commit is contained in:
@@ -3,8 +3,9 @@ use super::series::{validate_series_format, MAX_NUMBER_LEN};
|
||||
use super::settings::{map_bank, map_settings, SETTINGS_COLS};
|
||||
use crate::db::format_number;
|
||||
use crate::gst::{self, TaxType};
|
||||
use super::payments::{derive_summary, summary_for, today_ist};
|
||||
use crate::models::{
|
||||
BankAccount, DraftSummary, Invoice, InvoiceInput, InvoiceItem, InvoiceSummary, Settings,
|
||||
BankAccount, DraftSummary, Invoice, InvoiceInput, InvoiceItem, InvoiceSummary, PaymentSummary, Settings,
|
||||
};
|
||||
use crate::AppState;
|
||||
use chrono::NaiveDate;
|
||||
@@ -59,6 +60,8 @@ fn map_invoice(row: &Row) -> rusqlite::Result<Invoice> {
|
||||
archived_fingerprint: row.get(35)?,
|
||||
archived_at: row.get(36)?,
|
||||
items: Vec::new(),
|
||||
payment: PaymentSummary::default(),
|
||||
total_paise: 0,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -90,6 +93,8 @@ fn fetch_invoice(conn: &Connection, id: i64) -> rusqlite::Result<Invoice> {
|
||||
map_invoice,
|
||||
)?;
|
||||
invoice.items = fetch_items(conn, id)?;
|
||||
invoice.total_paise = gst::rupees_to_paise(invoice.total);
|
||||
invoice.payment = summary_for(conn, id, &invoice.status, invoice.total, &invoice.due_date)?;
|
||||
Ok(invoice)
|
||||
}
|
||||
|
||||
@@ -460,6 +465,12 @@ pub fn issue_invoice(
|
||||
|
||||
/// Issued invoices are never deleted: cancelling keeps the row and its number.
|
||||
pub fn cancel_invoice_impl(conn: &mut Connection, id: i64, reason: &str) -> Result<Invoice, String> {
|
||||
let payments: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM payments WHERE invoice_id = ?1", params![id], |r| r.get(0))
|
||||
.map_err(|e| e.to_string())?;
|
||||
if payments > 0 {
|
||||
return Err("This invoice has payments recorded. Delete them before cancelling it.".into());
|
||||
}
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let changed = conn
|
||||
.execute(
|
||||
@@ -491,23 +502,42 @@ pub fn cancel_invoice(state: State<AppState>, id: i64, reason: String) -> Result
|
||||
#[tauri::command]
|
||||
pub fn list_invoices(state: State<AppState>) -> Result<Vec<InvoiceSummary>, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
list_invoices_impl(&conn)
|
||||
}
|
||||
|
||||
/// One query: invoices left-joined to their payment sums. Status derivation happens in Rust
|
||||
/// (`derive_summary`) so list and detail agree.
|
||||
pub fn list_invoices_impl(conn: &Connection) -> Result<Vec<InvoiceSummary>, String> {
|
||||
let today = today_ist();
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT id, number, invoice_date, due_date, client_name, total, status, created_at
|
||||
FROM invoices ORDER BY id DESC",
|
||||
"SELECT i.id, i.number, i.invoice_date, i.due_date, i.client_name, i.total, i.status,
|
||||
i.created_at, i.po_number, COALESCE(p.paid, 0), COALESCE(p.tds, 0)
|
||||
FROM invoices i
|
||||
LEFT JOIN (SELECT invoice_id, SUM(amount_paise) AS paid, SUM(tds_paise) AS tds
|
||||
FROM payments GROUP BY invoice_id) p ON p.invoice_id = i.id
|
||||
ORDER BY i.id DESC",
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map([], |row| {
|
||||
let total: f64 = row.get(5)?;
|
||||
let status: String = row.get(6)?;
|
||||
let due_date: String = row.get(3)?;
|
||||
let total_paise = gst::rupees_to_paise(total);
|
||||
let payment = derive_summary(&status, total_paise, row.get(9)?, row.get(10)?, &due_date, &today);
|
||||
Ok(InvoiceSummary {
|
||||
id: row.get(0)?,
|
||||
number: row.get(1)?,
|
||||
invoice_date: row.get(2)?,
|
||||
due_date: row.get(3)?,
|
||||
due_date,
|
||||
client_name: row.get(4)?,
|
||||
total: row.get(5)?,
|
||||
status: row.get(6)?,
|
||||
total,
|
||||
status,
|
||||
created_at: row.get(7)?,
|
||||
po_number: row.get(8)?,
|
||||
total_paise,
|
||||
payment,
|
||||
})
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
@@ -516,10 +546,14 @@ pub fn list_invoices(state: State<AppState>) -> Result<Vec<InvoiceSummary>, Stri
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
pub fn get_invoice_impl(conn: &Connection, id: i64) -> Result<Invoice, String> {
|
||||
fetch_invoice(conn, id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_invoice(state: State<AppState>, id: i64) -> Result<Invoice, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
fetch_invoice(&conn, id).map_err(|e| e.to_string())
|
||||
get_invoice_impl(&conn, id)
|
||||
}
|
||||
|
||||
pub fn save_draft_impl(
|
||||
|
||||
Reference in New Issue
Block a user