diff --git a/ns_app/hooks.py b/ns_app/hooks.py
index bcaad81..1802a5b 100644
--- a/ns_app/hooks.py
+++ b/ns_app/hooks.py
@@ -7,7 +7,8 @@ app_license = "MIT"
# Load on every page
app_include_js = [
- "/assets/ns_app/js/customer_quick_entry.js"
+ "/assets/ns_app/js/customer_quick_entry.js",
+ "/assets/ns_app/js/customer_statements.js"
]
# Load on Sales Invoice form
@@ -15,11 +16,6 @@ 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"
-}
-
# Ensure custom fields exist after every migrate
after_migrate = "ns_app.setup.after_migrate"
diff --git a/ns_app/public/js/customer_list.js b/ns_app/public/js/customer_list.js
deleted file mode 100644
index e0faf0a..0000000
--- a/ns_app/public/js/customer_list.js
+++ /dev/null
@@ -1,151 +0,0 @@
-// 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)? A late-fee invoice will be raised (once per customer this month) for any overdue balances.", [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();
-}
diff --git a/ns_app/public/js/customer_statements.js b/ns_app/public/js/customer_statements.js
new file mode 100644
index 0000000..f417586
--- /dev/null
+++ b/ns_app/public/js/customer_statements.js
@@ -0,0 +1,205 @@
+// 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");
+
+// ── Entry point: Customer list ───────────────────────────────────────────────
+frappe.listview_settings["Customer"] = {
+ onload(listview) {
+ listview.page.add_inner_button(__("Generate Statements"), () => {
+ ns_statements.pick_and_generate();
+ });
+ }
+};
+
+// ── 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 => `
+
+ |
+
+ |
+ ${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: `
+
+ `
+ },
+ {
+ 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: `${__("Generate an account statement for {0}.", [frappe.utils.escape_html(customer)])}
`
+ },
+ {
+ 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();
+};