feat(mcp): payments on terms, bills due, and the agings' tie-out (P4b S6)
books.receive_payment and books.pay_bill take applications, each document's cash with an optional discount and reason, and a customer payment may carry the processor's fee; the result names every document it settled and what reached the account. books.bills_due lists what to pay and the running total by due date. The agings take detail and return the control account's tie-out, and books.export writes ar_aging/ap_aging. The handlers live in tools/settlements.rs (the EM's split, so S4.4's documents.rs stays free); documents.rs keeps the documents, the void and the control check. docs/mcp-interface.md documents every changed tool and the new one; docs/architecture.md names the group; docs/testing.md records the tests and P12's mutation targets. Old names (document_id, cash_account) still work; the output keeps remaining/status for one document. A fee's processor is named in the description, not tagged as the entry's vendor: ledger tags are entry-wide and tagging would move the customer's party totals and 1099-NEC. The handbook's invoices-and-bills section gains the payment, discount, fee and bounce-replacement procedures (S4.5 can extend it). Over the wire: one_payment_settles_two_invoices, late_discount_needs_a_reason, a_fee_posts_gross_to_receivables_and_net_to_the_bank, card_paid_bill_is_not_1099_nec, multi_document_payment_counts_once_for_1099_and_party_totals, overpayment_refused_with_the_excess, bills_due_orders_by_due_and_discount and the_aging_export_ties_out. Terms cannot yet be journaled on a document (S4.3) and a bill line's account is not an MCP field (S4.4), so bill_on_terms_with_discount, equipment_bill_payment_is_investing_cash and the discount half of bills_due are core tests until those land; the unit's As-built block records this.
This commit is contained in:
1 parent
78a2385440
commit
57f04dbb0c
17 files changed
+1467
-231
No files matched your search
@@ -848,7 +848,7 @@ impl BooksServer {
|
||||
#[tool(
|
||||
name = "books.export",
|
||||
title = "Export a report",
|
||||
description = "Export a report. Reports: profit_and_loss, balance_sheet, cash_flow (direct method, with its reconciliation and noncash schedules; needs cash_accounts), trial_balance (as_of), members_equity (multi-member LLC; members from the settings), tax_summary, general_ledger (by account: running balances, splits), general_journal (each entry posted in the period; alias journal), fixed_asset_register (as_of: every asset with tag, serial, cost, depreciation, book value, reconciled to the books). The file is handed over (run its download to save it; never its bytes). Formats: pdf and xlsx for the statements, tax_summary and fixed_asset_register; xlsx, csv, json for every report; iif for general_journal or journal (QuickBooks Desktop: plain ASCII text, CR LF; every posted entry from the first through as_of (start and end are refused), period closes left out, DOCNUM the entry id; names, numbers and memos fit QuickBooks' limits; warnings says what is written differently from the books, memos grouped, to tell the human). Dates: start/end for period reports (omitted: the fiscal year to date), as_of for balance_sheet, trial_balance, fixed_asset_register and iif (omitted: today).",
|
||||
description = "Export a report. Reports: profit_and_loss, balance_sheet, cash_flow (direct method, with its reconciliation and noncash schedules; needs cash_accounts), trial_balance (as_of), members_equity (multi-member LLC; members from the settings), tax_summary, general_ledger (by account: running balances, splits), general_journal (each entry posted in the period; alias journal), fixed_asset_register (as_of: every asset with tag, serial, cost, depreciation, book value, reconciled to the books), ar_aging and ap_aging (as_of: one row per open invoice or bill, with a tie-out line to its control account). The file is handed over (run its download to save it; never its bytes). Formats: pdf and xlsx for the statements, tax_summary, fixed_asset_register and the agings; xlsx, csv, json for every report; iif for general_journal or journal (QuickBooks Desktop: plain ASCII text, CR LF; every posted entry from the first through as_of (start and end are refused), period closes left out, DOCNUM the entry id; names, numbers and memos fit QuickBooks' limits; warnings says what is written differently from the books, memos grouped, to tell the human). Dates: start/end for period reports (omitted: the fiscal year to date), as_of for balance_sheet, trial_balance, fixed_asset_register, ar_aging, ap_aging and iif (omitted: today).",
|
||||
annotations(read_only_hint = true, open_world_hint = false)
|
||||
)]
|
||||
async fn export(&self, params: Parameters<ExportParams>) -> Result<Json<ExportOut>, String> {
|
||||
@@ -911,9 +911,11 @@ impl BooksServer {
|
||||
"fixed_asset_register" => {
|
||||
self.export_fixed_asset_register(&format, p.as_of.as_deref())?
|
||||
}
|
||||
"ar_aging" => self.export_aging(&format, true, p.as_of.as_deref())?,
|
||||
"ap_aging" => self.export_aging(&format, false, p.as_of.as_deref())?,
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"bad report {:?}: expected general_journal, general_ledger, trial_balance, profit_and_loss, balance_sheet, cash_flow, members_equity, tax_summary, or fixed_asset_register",
|
||||
"bad report {:?}: expected general_journal, general_ledger, trial_balance, profit_and_loss, balance_sheet, cash_flow, members_equity, tax_summary, fixed_asset_register, ar_aging, or ap_aging",
|
||||
p.report
|
||||
));
|
||||
}
|
||||
@@ -1205,14 +1207,64 @@ impl BooksServer {
|
||||
.map_err(|e| e.to_string())?;
|
||||
report::members_equity(&me)
|
||||
}
|
||||
"ar_aging" => {
|
||||
ledger_package::aging_statement(ledger, true, self.as_of(p.as_of.as_deref())?)
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
"ap_aging" => {
|
||||
ledger_package::aging_statement(ledger, false, self.as_of(p.as_of.as_deref())?)
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"bad report {report:?}: expected profit_and_loss, balance_sheet, cash_flow, trial_balance, members_equity, or tax_summary"
|
||||
"bad report {report:?}: expected profit_and_loss, balance_sheet, cash_flow, trial_balance, members_equity, tax_summary, ar_aging, or ap_aging"
|
||||
));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// The receivables or payables aging as CSV or JSON (plan P4b S6,
|
||||
/// 8.3.10): one row per open document, then the tie-out line; the
|
||||
/// same CSV the CPA package carries.
|
||||
fn export_aging(
|
||||
&self,
|
||||
format: &str,
|
||||
invoices: bool,
|
||||
as_of: Option<&str>,
|
||||
) -> Result<String, String> {
|
||||
let as_of = self.as_of(as_of)?;
|
||||
let store = self.lock()?;
|
||||
if format == "json" {
|
||||
let docs = store
|
||||
.ledger()
|
||||
.aging_documents(as_of, invoices)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let rows: Vec<serde_json::Value> = docs
|
||||
.iter()
|
||||
.map(|d| {
|
||||
serde_json::json!({
|
||||
"number": match d.doc {
|
||||
ledger_core::DocRef::Invoice(id) => store.ledger().get_invoice(id).and_then(|i| i.number.clone()),
|
||||
ledger_core::DocRef::Bill(id) => store.ledger().get_bill(id).and_then(|b| b.vendor_ref.clone()),
|
||||
},
|
||||
"party": d.party,
|
||||
"date": d.date.to_string(),
|
||||
"due_date": d.due_date.to_string(),
|
||||
"terms": d.terms,
|
||||
"total": money(d.total),
|
||||
"paid": money(d.paid),
|
||||
"discounted": money(d.discounted),
|
||||
"outstanding": money(d.outstanding),
|
||||
"bucket": d.bucket,
|
||||
"days_overdue": d.days_overdue,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
return serde_json::to_string_pretty(&rows).map_err(|e| e.to_string());
|
||||
}
|
||||
ledger_package::aging_csv(store.ledger(), invoices, as_of)
|
||||
}
|
||||
|
||||
/// Render a report to XLSX bytes via ledger-import.
|
||||
fn export_xlsx(&self, report: &str, p: &ExportParams) -> Result<Vec<u8>, String> {
|
||||
use ledger_import::{XlsxReport, report_to_xlsx};
|
||||
|
||||
Reference in new issue
Block a user