frappe.provide("ns_app.customer"); console.log("NS App: customer_quick_entry.js loaded"); // ─── Install override ──────────────────────────────────────────────────────── // Poll until frappe.ui.form.make_quick_entry exists (ERPNext bundle settled), // then wrap make_quick_entry so we re-assert our class at every Customer call. (function () { "use strict"; function install_override() { const Base = frappe.ui.form.CustomerQuickEntryForm; if (!Base) return false; if (Base.__ns_patched) return true; frappe.ui.form.CustomerQuickEntryForm = class extends Base { // render_dialog is called by QuickEntryForm.setup() after the // constructor runs. At this point this.after_insert is already // set by the base constructor. We show our custom dialog instead // of ERPNext's, but we PRESERVE this.after_insert so the link // field callback chain stays intact. render_dialog() { console.log("NS App: render_dialog intercepted"); // Capture the typed customer name before anything mutates focus const customer_name = this._get_typed_name(); console.log("NS App: prefill name =", customer_name); // Open our custom dialog, passing: // - the prefilled name // - this.after_insert as the callback so ERPNext's link // field gets notified when the customer is created ns_app.customer.open_quick_entry({ customer_name: customer_name, after_insert: this.after_insert // ← this is the key }); // We intentionally do NOT call super.render_dialog(). // ERPNext's dialog is replaced entirely by ours. // But we must call this.dialog = something so that // QuickEntryForm.setup() doesn't crash on teardown. // A minimal placeholder dialog satisfies that contract. if (!this.dialog) { this.dialog = { hide: () => {}, get_field: () => null }; } } _get_typed_name() { // 1. The link field control that triggered quick entry if (frappe.ui.form.cur_field) { const v = frappe.ui.form.cur_field.get_value?.(); if (v) return v; } // 2. Whatever input had focus when the dialog opened const el = document.activeElement; if (el?.value) return el.value; // 3. Current form doc if (typeof cur_frm !== "undefined" && cur_frm?.doc) { return cur_frm.doc.customer || cur_frm.doc.party_name || ""; } return ""; } }; frappe.ui.form.CustomerQuickEntryForm.__ns_patched = true; console.log("NS App: CustomerQuickEntryForm override installed ✓"); return true; } function patch_make_quick_entry() { const orig = frappe.ui.form.make_quick_entry; if (!orig || orig.__ns_patched) return; frappe.ui.form.make_quick_entry = function (doctype, after_insert, init_callback, doc, force) { if (doctype === "Customer") { install_override(); // re-assert in case anything clobbered it } return orig.apply(this, arguments); }; frappe.ui.form.make_quick_entry.__ns_patched = true; console.log("NS App: make_quick_entry patched ✓"); } let attempts = 0; const poller = setInterval(() => { if (++attempts > 100) { clearInterval(poller); console.error("NS App: gave up waiting for frappe.ui.form.make_quick_entry"); return; } if (frappe.ui.form?.make_quick_entry) { clearInterval(poller); install_override(); patch_make_quick_entry(); } }, 100); })(); // ─── Custom dialog ─────────────────────────────────────────────────────────── // opts: // customer_name {string} prefill value // after_insert {function} ERPNext's link field callback — MUST be called // with the new customer name on success ns_app.customer.open_quick_entry = function (opts = {}) { console.log("NS App: open_quick_entry called", opts); const d = new frappe.ui.Dialog({ title: "New Customer", size: "large", fields: [ // ── Customer ────────────────────────────────────────────────── { fieldtype: "Section Break", label: "Customer Information" }, { fieldname: "customer_name", label: "Customer Name", fieldtype: "Data", reqd: 1, default: opts.customer_name || "", description: "Enter the customer or company name" }, { fieldname: "customer_type", label: "Customer Type", fieldtype: "Select", options: "Company\nIndividual", default: "Company", reqd: 1, description: "Select whether this customer is a company or individual" }, { fieldname: "customer_group", label: "Customer Group", fieldtype: "Link", options: "Customer Group", default: "Commercial", reqd: 1, description: "Select the customer group" }, { fieldname: "custom_send_via", label: "Preferred Delivery Method", fieldtype: "Select", options: "mail\nemail\nfax", description: "Choose how documents should be sent to the customer" }, // ── Contact ─────────────────────────────────────────────────── { fieldtype: "Section Break", label: "Primary Contact" }, { fieldname: "email_id", label: "Email Address", fieldtype: "Data", options: "Email", description: "Enter the customer's email address" }, { fieldname: "mobile_no", label: "Mobile Phone Number", fieldtype: "Data", reqd: 1, description: "Enter the customer's mobile phone number" }, // ── Address ─────────────────────────────────────────────────── { fieldtype: "Section Break", label: "Address Information" }, { fieldname: "address_line1", label: "Address Line 1", fieldtype: "Data", reqd: 1, description: "Enter the street address" }, { fieldname: "address_line2", label: "Address Line 2", fieldtype: "Data", description: "Enter apartment, suite, or secondary address information" }, { fieldname: "pincode", label: "ZIP Code", fieldtype: "Data", reqd: 1, description: "Enter the ZIP or postal code" }, { fieldname: "city", label: "City", fieldtype: "Data", description: "Enter the city" }, { fieldname: "state", label: "State", fieldtype: "Data", description: "Enter the state" }, { fieldname: "country", label: "Country", fieldtype: "Link", options: "Country", default: "United States", description: "Select the country" } ], primary_action_label: "Create Customer", primary_action(values) { console.log("NS App: submitting customer creation", values); d.disable_primary_action(); frappe.call({ method: "ns_app.api.customer.create_customer_full", args: values, callback(r) { if (!r.message) { console.error("NS App: create_customer_full returned empty"); d.enable_primary_action(); return; } const customer_name = r.message; console.log("NS App: customer created →", customer_name); d.hide(); frappe.show_alert({ message: `Customer "${customer_name}" created`, indicator: "green" }); // ── Hand control back to ERPNext ───────────────────── // after_insert is ERPNext's link field callback. // Calling it with the new customer name does everything: // - populates the Customer field on the originating form // - triggers the field's onchange/fetch logic // - does NOT require any routing from our side // This is the ONLY correct way to resume the originating // document flow without racing the backend transaction. if (typeof opts.after_insert === "function") { console.log("NS App: calling after_insert with", customer_name); // ERPNext's callback expects an object with a .name property, // not a plain string — { name: "cu-00741" } opts.after_insert({ name: customer_name }); } else { // Fallback: no callback was passed (e.g. dialog opened // standalone). Just reload to a new Customer form. console.warn("NS App: no after_insert callback — navigating to customer"); frappe.set_route("Form", "Customer", customer_name); } }, always() { d.enable_primary_action(); } }); } }); d.show(); // ── Accessibility ──────────────────────────────────────────────────────── setTimeout(() => { d.fields.forEach(f => { const ctrl = d.get_field(f.fieldname); if (!ctrl?.$input) return; ctrl.$input .attr("aria-label", f.label || f.fieldname) .attr("title", f.label || f.fieldname); if (f.label && !ctrl.$input.attr("placeholder")) { ctrl.$input.attr("placeholder", f.label); } }); }, 100); // ── ZIP autofill ───────────────────────────────────────────────────────── d.fields_dict.pincode.df.onchange = () => { const zip = d.get_value("pincode"); if (!zip || zip.length < 5) return; fetch(`https://api.zippopotam.us/us/${zip}`) .then(r => r.ok ? r.json() : null) .then(data => { if (!data?.places?.length) return; const p = data.places[0]; d.set_value("city", p["place name"]); d.set_value("state", p["state"]); d.set_value("country", data.country); console.log("NS App: ZIP autofill →", p["place name"], p["state"]); }) .catch(() => {}); }; // ── Enter → next field ─────────────────────────────────────────────────── d.$wrapper.on("keydown", "input, select, textarea", function (e) { if (e.key !== "Enter") return; if (document.activeElement?.classList.contains("btn-primary")) return; e.preventDefault(); const fields = d.$wrapper .find("input, select, textarea") .filter(":visible:not([disabled])"); const i = fields.index(this); if (i > -1 && i + 1 < fields.length) fields.eq(i + 1).focus(); }); };