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: ` -
- - - - - - - - - - - ${body} -
- - ${__("Customer")}${__("Overdue Invoices")}${__("Max Days Overdue")}${__("Total Outstanding")}
-
-
` - }], - 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: ` +
+ + + + + + + + + + + ${body} +
+ + ${__("Customer")}${__("Overdue Invoices")}${__("Max Days Overdue")}${__("Total Outstanding")}
+
+
` + }, + { + 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(); +};