Files
ns_erpnext_app/ns_app/public/js/customer_statements.js
Norman King 326adf865a fix(statements): restore Customer list button (merge listview_settings)
The list button was registered by reassigning
frappe.listview_settings['Customer'] in a globally-loaded script, but
ERPNext's own Customer list_js (loaded when the list opens) overwrote it,
so the button never appeared. Register it via doctype_list_js instead —
which Frappe appends after the doctype's own list_js — and merge into the
existing settings (wrapping onload, preserving ERPNext's add_fields)
rather than reassigning. The form button and shared ns_statements helpers
stay in customer_statements.js.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 20:36:21 -04:00

201 lines
8.0 KiB
JavaScript

// Customer Statements: generate printable, one-page-per-customer account
// statements formatted for a window envelope. Two entry points share the same
// generate/print helpers — a multi-select action on the Customer list and a
// single-customer button on the Customer form. Loaded globally so both the
// list view and the form can reach the shared `ns_statements` helpers.
frappe.provide("ns_statements");
// The Customer list button is registered separately in customer_list.js
// (a doctype_list_js) so it merges with — rather than overwrites — ERPNext's
// own listview_settings["Customer"]. Shared helpers live here on ns_statements.
// ── Entry point: Customer form ───────────────────────────────────────────────
frappe.ui.form.on("Customer", {
refresh(frm) {
if (frm.is_new()) return;
frm.add_custom_button(__("Generate Statement"), () => {
ns_statements.generate_for_customer(frm.doc.name);
});
}
});
// ── Shared: call the backend and open the printable document ─────────────────
ns_statements.run = function (customers, skip_late_fee) {
frappe.call({
method: "ns_app.api.statements.generate_statements",
args: { customers, skip_late_fee: skip_late_fee ? 1 : 0 },
freeze: true,
freeze_message: __("Generating statements..."),
callback(r) {
if (!r.message || !r.message.html) return;
ns_statements.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"
});
}
}
});
};
ns_statements.open_print_window = function (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();
};
// ── List flow: pick customers with overdue invoices, then generate ───────────
ns_statements.pick_and_generate = function () {
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;
}
ns_statements._selection_dialog(rows);
}
});
};
ns_statements._selection_dialog = function (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:45vh; 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>`
},
{
fieldtype: "Check",
fieldname: "generate_late_fee",
label: __("Generate late payment fee"),
default: 1,
description: __("Bills a late-fee invoice (once per customer this month) for overdue balances.")
}
],
primary_action_label: __("Generate Statements"),
primary_action() {
const customers = [...selected];
if (!customers.length) {
frappe.msgprint(__("Select at least one customer."));
return;
}
const gen_fee = dialog.get_value("generate_late_fee");
const proceed = () => {
dialog.hide();
ns_statements.run(customers, !gen_fee);
};
if (gen_fee) {
frappe.confirm(
__("Generate statements for {0} customer(s)? A late-fee invoice will be raised (once per customer this month) for any overdue balances.", [customers.length]),
proceed
);
} else {
proceed();
}
}
});
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();
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();
});
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();
});
};
// ── Form flow: single customer, with the same fee toggle ─────────────────────
ns_statements.generate_for_customer = function (customer) {
const d = new frappe.ui.Dialog({
title: __("Generate Statement"),
fields: [
{
fieldtype: "HTML",
options: `<p>${__("Generate an account statement for <b>{0}</b>.", [frappe.utils.escape_html(customer)])}</p>`
},
{
fieldtype: "Check",
fieldname: "generate_late_fee",
label: __("Generate late payment fee"),
default: 1,
description: __("Bills a late-fee invoice (once this month) for overdue balances.")
}
],
primary_action_label: __("Generate"),
primary_action(values) {
d.hide();
ns_statements.run([customer], !values.generate_late_fee);
}
});
d.show();
};