diff --git a/ns_app/hooks.py b/ns_app/hooks.py
index c4fe7c6..089f50a 100644
--- a/ns_app/hooks.py
+++ b/ns_app/hooks.py
@@ -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 = [
{
diff --git a/ns_app/public/js/customer_list.js b/ns_app/public/js/customer_list.js
new file mode 100644
index 0000000..16292aa
--- /dev/null
+++ b/ns_app/public/js/customer_list.js
@@ -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 => `
+
+ |
+
+ |
+ ${frappe.utils.escape_html(r.customer_name || r.customer)} |
+ ${r.overdue_count} |
+ ${r.max_days_overdue} |
+ ${format_currency(r.total_outstanding)} |
+
`).join("");
+
+ const dialog = new frappe.ui.Dialog({
+ title: __("Generate Customer Statements"),
+ size: "large",
+ fields: [{
+ fieldtype: "HTML",
+ fieldname: "selector",
+ options: `
+
+ `
+ }],
+ 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();
+}