feat(statements): add Customer list "Generate Statements" button
Register doctype_list_js for Customer and add customer_list.js, which lists customers with overdue invoices in a selection dialog (checkbox table + select-all), then calls generate_statements and opens the printable, one-page-per-customer document in a new window. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,11 @@ doctype_js = {
|
||||
"Sales Invoice": "public/js/sales_invoice.js"
|
||||
}
|
||||
|
||||
# Load on Customer list view (adds "Generate Statements" action)
|
||||
doctype_list_js = {
|
||||
"Customer": "public/js/customer_list.js"
|
||||
}
|
||||
|
||||
# Fixtures tracked in Git
|
||||
fixtures = [
|
||||
{
|
||||
|
||||
151
ns_app/public/js/customer_list.js
Normal file
151
ns_app/public/js/customer_list.js
Normal file
@@ -0,0 +1,151 @@
|
||||
// Adds a "Generate Statements" action to the Customer list. It lists customers
|
||||
// with overdue invoices, lets the user pick which ones, and opens a printable
|
||||
// (one-page-per-customer) statement document in a new window.
|
||||
|
||||
frappe.listview_settings["Customer"] = {
|
||||
onload(listview) {
|
||||
listview.page.add_inner_button(__("Generate Statements"), () => {
|
||||
open_statement_selector();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function open_statement_selector() {
|
||||
frappe.call({
|
||||
method: "ns_app.api.statements.get_customers_with_overdue_invoices",
|
||||
freeze: true,
|
||||
freeze_message: __("Finding customers with overdue invoices..."),
|
||||
callback(r) {
|
||||
const rows = r.message || [];
|
||||
if (!rows.length) {
|
||||
frappe.msgprint({
|
||||
title: __("No Overdue Customers"),
|
||||
message: __("No customers currently have overdue invoices."),
|
||||
indicator: "green"
|
||||
});
|
||||
return;
|
||||
}
|
||||
show_selection_dialog(rows);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function show_selection_dialog(rows) {
|
||||
const uid = Date.now();
|
||||
const selected = new Set(rows.map(r => r.customer)); // default: all selected
|
||||
|
||||
const body = rows.map(r => `
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="cust-check-${uid}"
|
||||
data-name="${frappe.utils.escape_html(r.customer)}" checked>
|
||||
</td>
|
||||
<td>${frappe.utils.escape_html(r.customer_name || r.customer)}</td>
|
||||
<td class="text-center">${r.overdue_count}</td>
|
||||
<td class="text-center">${r.max_days_overdue}</td>
|
||||
<td class="text-right">${format_currency(r.total_outstanding)}</td>
|
||||
</tr>`).join("");
|
||||
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Generate Customer Statements"),
|
||||
size: "large",
|
||||
fields: [{
|
||||
fieldtype: "HTML",
|
||||
fieldname: "selector",
|
||||
options: `
|
||||
<div style="max-height:50vh; overflow:auto;">
|
||||
<table class="table table-bordered table-sm" style="font-size:13px; margin:0;">
|
||||
<thead style="position:sticky; top:0; background:#f5f5f5;">
|
||||
<tr>
|
||||
<th style="width:36px;">
|
||||
<input type="checkbox" id="sel_all_${uid}" title="${__("Select all")}" checked>
|
||||
</th>
|
||||
<th>${__("Customer")}</th>
|
||||
<th class="text-center">${__("Overdue Invoices")}</th>
|
||||
<th class="text-center">${__("Max Days Overdue")}</th>
|
||||
<th class="text-right">${__("Total Outstanding")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sel_body_${uid}">${body}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="sel_count_${uid}" style="margin-top:8px; font-weight:bold;"></div>`
|
||||
}],
|
||||
primary_action_label: __("Generate Statements"),
|
||||
primary_action() { generate(); }
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
|
||||
const update_count = () => {
|
||||
const el = document.getElementById(`sel_count_${uid}`);
|
||||
if (el) el.innerText = __("{0} of {1} selected", [selected.size, rows.length]);
|
||||
};
|
||||
update_count();
|
||||
|
||||
// Row checkboxes (delegated)
|
||||
dialog.$wrapper.on("change", `.cust-check-${uid}`, function () {
|
||||
if (this.checked) selected.add(this.dataset.name);
|
||||
else selected.delete(this.dataset.name);
|
||||
|
||||
const all = dialog.$wrapper[0].querySelectorAll(`.cust-check-${uid}`);
|
||||
const selAll = document.getElementById(`sel_all_${uid}`);
|
||||
if (selAll) selAll.checked = [...all].every(c => c.checked);
|
||||
update_count();
|
||||
});
|
||||
|
||||
// Select-all
|
||||
dialog.$wrapper.on("change", `#sel_all_${uid}`, function () {
|
||||
dialog.$wrapper[0].querySelectorAll(`.cust-check-${uid}`).forEach(cb => {
|
||||
cb.checked = this.checked;
|
||||
if (this.checked) selected.add(cb.dataset.name);
|
||||
else selected.delete(cb.dataset.name);
|
||||
});
|
||||
update_count();
|
||||
});
|
||||
|
||||
function generate() {
|
||||
const customers = [...selected];
|
||||
if (!customers.length) {
|
||||
frappe.msgprint(__("Select at least one customer."));
|
||||
return;
|
||||
}
|
||||
frappe.confirm(
|
||||
__("Generate statements for {0} customer(s)?", [customers.length]),
|
||||
() => {
|
||||
frappe.call({
|
||||
method: "ns_app.api.statements.generate_statements",
|
||||
args: { customers },
|
||||
freeze: true,
|
||||
freeze_message: __("Generating statements..."),
|
||||
callback(r) {
|
||||
if (!r.message || !r.message.html) return;
|
||||
dialog.hide();
|
||||
open_print_window(r.message.html);
|
||||
const skipped = (r.message.skipped || []).length;
|
||||
if (skipped) {
|
||||
frappe.show_alert({
|
||||
message: __("Skipped {0} customer(s) with no balance.", [skipped]),
|
||||
indicator: "orange"
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function open_print_window(html) {
|
||||
const w = window.open("", "_blank");
|
||||
if (!w) {
|
||||
frappe.msgprint(__("Please allow pop-ups to view the statements."));
|
||||
return;
|
||||
}
|
||||
w.document.open();
|
||||
w.document.write(html);
|
||||
w.document.close();
|
||||
}
|
||||
Reference in New Issue
Block a user