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: ` +
+ + + + + + + + + + + ${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)?", [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(); +}