auto fill customer after customer creation for every doctype.
This commit is contained in:
@@ -1,317 +1,271 @@
|
||||
frappe.provide("ns_app.customer");
|
||||
|
||||
console.log("NS APP CUSTOMER JS LOADED");
|
||||
console.log("NS App: customer_quick_entry.js loaded");
|
||||
|
||||
$(document).ready(() => {
|
||||
// ─── 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.
|
||||
|
||||
setTimeout(() => {
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const TargetClass =
|
||||
frappe.ui.form.CustomerQuickEntryForm;
|
||||
function install_override() {
|
||||
const Base = frappe.ui.form.CustomerQuickEntryForm;
|
||||
if (!Base) return false;
|
||||
if (Base.__ns_patched) return true;
|
||||
|
||||
if (!TargetClass) {
|
||||
frappe.ui.form.CustomerQuickEntryForm = class extends Base {
|
||||
|
||||
console.error(
|
||||
"NS App: CustomerQuickEntryForm not found"
|
||||
);
|
||||
// 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");
|
||||
|
||||
return;
|
||||
}
|
||||
// Capture the typed customer name before anything mutates focus
|
||||
const customer_name = this._get_typed_name();
|
||||
|
||||
// Prevent duplicate patching
|
||||
if (TargetClass.__ns_patched) {
|
||||
console.log("NS App: prefill name =", customer_name);
|
||||
|
||||
console.log(
|
||||
"NS App: already patched"
|
||||
);
|
||||
// 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
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
"NS App: patching CustomerQuickEntryForm"
|
||||
);
|
||||
|
||||
frappe.ui.form.CustomerQuickEntryForm =
|
||||
class extends TargetClass {
|
||||
|
||||
render_dialog() {
|
||||
|
||||
console.log(
|
||||
"NS App: render_dialog intercepted"
|
||||
);
|
||||
|
||||
let customer_name = "";
|
||||
|
||||
// Route option first
|
||||
if (frappe.route_options?.name) {
|
||||
|
||||
customer_name =
|
||||
frappe.route_options.name;
|
||||
}
|
||||
|
||||
// Focused field fallback
|
||||
if (!customer_name) {
|
||||
|
||||
const active =
|
||||
document.activeElement;
|
||||
|
||||
if (
|
||||
active &&
|
||||
active.value
|
||||
) {
|
||||
|
||||
customer_name =
|
||||
active.value;
|
||||
}
|
||||
}
|
||||
|
||||
// cur_frm fallback
|
||||
if (
|
||||
!customer_name &&
|
||||
typeof cur_frm !==
|
||||
"undefined" &&
|
||||
cur_frm
|
||||
) {
|
||||
|
||||
customer_name =
|
||||
cur_frm.doc.customer ||
|
||||
cur_frm.doc.party_name ||
|
||||
"";
|
||||
}
|
||||
|
||||
console.log(
|
||||
"NS App: Captured customer name:",
|
||||
customer_name
|
||||
);
|
||||
|
||||
// DO NOT call super.render_dialog()
|
||||
// This restores the fully custom dialog
|
||||
|
||||
ns_app.customer.open_quick_entry({
|
||||
customer_name:
|
||||
customer_name,
|
||||
|
||||
callback:
|
||||
this.after_insert
|
||||
});
|
||||
// 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 };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
TargetClass.__ns_patched = true;
|
||||
_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 "";
|
||||
}
|
||||
};
|
||||
|
||||
console.log(
|
||||
"NS App: CustomerQuickEntryForm patched successfully"
|
||||
);
|
||||
frappe.ui.form.CustomerQuickEntryForm.__ns_patched = true;
|
||||
console.log("NS App: CustomerQuickEntryForm override installed ✓");
|
||||
return true;
|
||||
}
|
||||
|
||||
}, 1000);
|
||||
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: Custom Customer Quick Entry OPENED"
|
||||
);
|
||||
console.log("NS App: open_quick_entry called", opts);
|
||||
|
||||
const d = new frappe.ui.Dialog({
|
||||
|
||||
title: "New Customer",
|
||||
|
||||
size: "large",
|
||||
|
||||
size: "large",
|
||||
fields: [
|
||||
|
||||
// ───────── CUSTOMER ─────────
|
||||
// ── Customer ──────────────────────────────────────────────────
|
||||
{ fieldtype: "Section Break", label: "Customer Information" },
|
||||
|
||||
{
|
||||
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: "customer_name",
|
||||
label: "Customer Name",
|
||||
fieldtype: "Data",
|
||||
reqd: 1,
|
||||
default:
|
||||
opts.customer_name || "",
|
||||
description:
|
||||
"Enter the customer or company name"
|
||||
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: "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: "address_line1",
|
||||
label: "Address Line 1",
|
||||
fieldtype: "Data",
|
||||
reqd: 1,
|
||||
description: "Enter the street address"
|
||||
},
|
||||
|
||||
{
|
||||
fieldname: "customer_group",
|
||||
label: "Customer Group",
|
||||
fieldtype: "Link",
|
||||
options: "Customer Group",
|
||||
default: "Commercial",
|
||||
reqd: 1,
|
||||
description:
|
||||
"Select the customer group"
|
||||
fieldname: "address_line2",
|
||||
label: "Address Line 2",
|
||||
fieldtype: "Data",
|
||||
description: "Enter apartment, suite, or secondary address information"
|
||||
},
|
||||
|
||||
{
|
||||
fieldname: "custom_send_via",
|
||||
label:
|
||||
"Preferred Delivery Method",
|
||||
fieldtype: "Select",
|
||||
options:
|
||||
"mail\nemail\nfax",
|
||||
description:
|
||||
"Choose how documents should be sent to the customer"
|
||||
fieldname: "pincode",
|
||||
label: "ZIP Code",
|
||||
fieldtype: "Data",
|
||||
reqd: 1,
|
||||
description: "Enter the ZIP or postal code"
|
||||
},
|
||||
|
||||
// ───────── CONTACT ─────────
|
||||
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
label: "Primary Contact"
|
||||
fieldname: "city",
|
||||
label: "City",
|
||||
fieldtype: "Data",
|
||||
description: "Enter the city"
|
||||
},
|
||||
|
||||
{
|
||||
fieldname: "email_id",
|
||||
label: "Email Address",
|
||||
fieldtype: "Data",
|
||||
options: "Email",
|
||||
description:
|
||||
"Enter the customer's email address"
|
||||
fieldname: "state",
|
||||
label: "State",
|
||||
fieldtype: "Data",
|
||||
description: "Enter the state"
|
||||
},
|
||||
|
||||
{
|
||||
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"
|
||||
fieldname: "country",
|
||||
label: "Country",
|
||||
fieldtype: "Link",
|
||||
options: "Country",
|
||||
default: "United States",
|
||||
description: "Select the country"
|
||||
}
|
||||
],
|
||||
|
||||
primary_action_label:
|
||||
"Create Customer",
|
||||
primary_action_label: "Create Customer",
|
||||
|
||||
primary_action(values) {
|
||||
|
||||
console.log(
|
||||
"NS App: Create Customer clicked",
|
||||
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,
|
||||
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;
|
||||
}
|
||||
|
||||
console.log(
|
||||
"NS App: Customer created",
|
||||
r.message
|
||||
);
|
||||
const customer_name = r.message;
|
||||
console.log("NS App: customer created →", customer_name);
|
||||
|
||||
d.hide();
|
||||
|
||||
frappe.show_alert({
|
||||
|
||||
message:
|
||||
"Customer created via NS App",
|
||||
|
||||
message: `Customer "${customer_name}" created`,
|
||||
indicator: "green"
|
||||
});
|
||||
|
||||
if (
|
||||
opts.callback
|
||||
) {
|
||||
|
||||
opts.callback(
|
||||
r.message
|
||||
);
|
||||
// ── 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();
|
||||
}
|
||||
});
|
||||
@@ -320,166 +274,48 @@ ns_app.customer.open_quick_entry = function (opts = {}) {
|
||||
|
||||
d.show();
|
||||
|
||||
// Accessibility labels
|
||||
|
||||
// ── Accessibility ────────────────────────────────────────────────────────
|
||||
setTimeout(() => {
|
||||
|
||||
d.fields.forEach(field => {
|
||||
|
||||
const control =
|
||||
d.get_field(
|
||||
field.fieldname
|
||||
);
|
||||
|
||||
if (
|
||||
!control ||
|
||||
!control.$input
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
control.$input.attr(
|
||||
"aria-label",
|
||||
field.label ||
|
||||
field.fieldname
|
||||
);
|
||||
|
||||
control.$input.attr(
|
||||
"title",
|
||||
field.label ||
|
||||
field.fieldname
|
||||
);
|
||||
|
||||
if (field.label) {
|
||||
|
||||
control.$input.attr(
|
||||
"placeholder",
|
||||
field.label
|
||||
);
|
||||
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);
|
||||
|
||||
console.log(
|
||||
"NS App: accessibility applied"
|
||||
);
|
||||
// ── ZIP autofill ─────────────────────────────────────────────────────────
|
||||
d.fields_dict.pincode.df.onchange = () => {
|
||||
const zip = d.get_value("pincode");
|
||||
if (!zip || zip.length < 5) return;
|
||||
|
||||
}, 300);
|
||||
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(() => {});
|
||||
};
|
||||
|
||||
// ZIP autofill
|
||||
// ── 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;
|
||||
|
||||
d.fields_dict.pincode.df.onchange =
|
||||
() => {
|
||||
|
||||
const zip =
|
||||
d.get_value(
|
||||
"pincode"
|
||||
);
|
||||
|
||||
if (
|
||||
!zip ||
|
||||
zip.length < 5
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
"NS App: ZIP lookup",
|
||||
zip
|
||||
);
|
||||
|
||||
fetch(
|
||||
`https://api.zippopotam.us/us/${zip}`
|
||||
)
|
||||
.then(r =>
|
||||
r.ok
|
||||
? r.json()
|
||||
: null
|
||||
)
|
||||
.then(data => {
|
||||
|
||||
if (
|
||||
!data ||
|
||||
!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 success"
|
||||
);
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
// Enter navigation
|
||||
|
||||
d.$wrapper.on(
|
||||
"keydown",
|
||||
"input, select, textarea",
|
||||
function (e) {
|
||||
|
||||
if (
|
||||
e.key === "Enter"
|
||||
) {
|
||||
|
||||
const active =
|
||||
document.activeElement;
|
||||
|
||||
// Allow submit only
|
||||
// on primary button
|
||||
|
||||
if (
|
||||
active &&
|
||||
active.classList.contains(
|
||||
"btn-primary"
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const fields =
|
||||
d.$wrapper
|
||||
.find(
|
||||
"input, select, textarea"
|
||||
)
|
||||
.filter(
|
||||
":visible:not([disabled])"
|
||||
);
|
||||
|
||||
const index =
|
||||
fields.index(this);
|
||||
|
||||
if (
|
||||
index > -1 &&
|
||||
index + 1 <
|
||||
fields.length
|
||||
) {
|
||||
|
||||
fields
|
||||
.eq(index + 1)
|
||||
.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
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();
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user