From 75600fdbeb883d7777aa9498263f8717fb347b9f Mon Sep 17 00:00:00 2001 From: Ty Reynolds Date: Mon, 9 Feb 2026 15:03:17 -0500 Subject: [PATCH] Custom Quick entry version 1 done. Zip API done and Customer backend added. --- ns_app/__init__.py | 1 + ns_app/api/customer.py | 81 +++++++++ ns_app/hooks.py | 2 +- ns_app/public/js/customer_quick_entry.js | 200 +++++++++++++++++++++++ setup.py | 12 ++ 5 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 ns_app/api/customer.py create mode 100644 ns_app/public/js/customer_quick_entry.js diff --git a/ns_app/__init__.py b/ns_app/__init__.py index e69de29..a68927d 100644 --- a/ns_app/__init__.py +++ b/ns_app/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" \ No newline at end of file diff --git a/ns_app/api/customer.py b/ns_app/api/customer.py new file mode 100644 index 0000000..932dfb8 --- /dev/null +++ b/ns_app/api/customer.py @@ -0,0 +1,81 @@ +import frappe +from frappe import _ + +@frappe.whitelist() +def create_customer_full(**data): + frappe.only_for("System Manager", "Sales User", "Sales Manager") + + required = [ + "customer_name", + "customer_type", + "customer_group", + "mobile_no", + "address_line1", + "pincode", + "country" + ] + + for field in required: + if not data.get(field): + frappe.throw(_(f"Missing required field: {field}")) + + # Prevent duplicates + if frappe.db.exists("Customer", {"customer_name": data["customer_name"]}): + frappe.throw(_("Customer already exists")) + + try: + frappe.db.begin() + + customer = frappe.get_doc({ + "doctype": "Customer", + "customer_name": data["customer_name"], + "customer_type": data["customer_type"], + "customer_group": data["customer_group"], + "territory": "United States", + "custom_auto_pay_status": "Active" if data.get("custom_auto_pay_enabled") else "Disabled", + "custom_send_via": data.get("custom_send_via"), + }).insert(ignore_permissions=True) + + contact = frappe.get_doc({ + "doctype": "Contact", + "first_name": data["customer_name"] + if data["customer_type"] == "Individual" + else data["customer_name"], + "email_ids": [{ + "email_id": data.get("email_id"), + "is_primary": 1 + }] if data.get("email_id") else [], + "phone_nos": [{ + "phone": data["mobile_no"], + "is_primary_phone": 1 + }], + "links": [{ + "link_doctype": "Customer", + "link_name": customer.name + }] + }).insert(ignore_permissions=True) + + address = frappe.get_doc({ + "doctype": "Address", + "address_title": customer.name, + "address_type": "Billing", + "address_line1": data["address_line1"], + "address_line2": data.get("address_line2"), + "city": data.get("city"), + "state": data.get("state"), + "pincode": data["pincode"], + "country": data["country"], + "links": [{ + "link_doctype": "Customer", + "link_name": customer.name + }] + }).insert(ignore_permissions=True) + + frappe.db.commit() + + return customer.name + + except Exception: + frappe.db.rollback() + frappe.log_error(frappe.get_traceback(), "NS App: Create Customer Failed") + raise diff --git a/ns_app/hooks.py b/ns_app/hooks.py index 7635494..c4fe7c6 100644 --- a/ns_app/hooks.py +++ b/ns_app/hooks.py @@ -7,7 +7,7 @@ app_license = "MIT" # Load on every page app_include_js = [ - "/assets/ns_erpnext_app/js/custom.js" + "/assets/ns_app/js/customer_quick_entry.js" ] # Load on Sales Invoice form diff --git a/ns_app/public/js/customer_quick_entry.js b/ns_app/public/js/customer_quick_entry.js new file mode 100644 index 0000000..ba61dfd --- /dev/null +++ b/ns_app/public/js/customer_quick_entry.js @@ -0,0 +1,200 @@ +frappe.provide("ns_app.customer"); + +// Preserve original quick entry +const _make_quick_entry = frappe.ui.form.make_quick_entry; + +// Override +frappe.ui.form.make_quick_entry = function (doctype, after_insert) { + if (doctype === "Customer") { + console.log("NS App: Intercepted Customer Quick Entry"); + ns_app.customer.open_quick_entry({ + callback: after_insert + }); + return; + } + + return _make_quick_entry.apply(this, arguments); +}; + +ns_app.customer.open_quick_entry = function (opts = {}) { + console.log("NS App: Custom Customer Quick Entry OPENED"); + + const d = new frappe.ui.Dialog({ + title: "New Customer", + size: "large", + + fields: [ + // ───────── CUSTOMER ───────── + { fieldtype: "Section Break", label: "Customer" }, + + { + fieldname: "customer_name", + label: "Customer Name", + fieldtype: "Data", + reqd: 1 + }, + { + fieldname: "customer_type", + label: "Customer Type", + fieldtype: "Select", + options: "Company\nIndividual", + default: "Company", + reqd: 1 + }, + { + fieldname: "customer_group", + label: "Customer Group", + fieldtype: "Link", + options: "Customer Group", + default: "Commercial", + reqd: 1 + }, + { + fieldname: "custom_auto_pay_enabled", + label: "Auto Pay Enabled", + fieldtype: "Check", + default: 0 + }, + { + fieldname: "custom_send_via", + label: "Send Via", + fieldtype: "Select", + options: "Mail\nEmail\nFax" + }, + + // ───────── CONTACT ───────── + { fieldtype: "Section Break", label: "Primary Contact" }, + + { + fieldname: "email_id", + label: "Email", + fieldtype: "Data", + options: "Email" + }, + { + fieldname: "mobile_no", + label: "Mobile", + fieldtype: "Data", + reqd: 1 + }, + + // ───────── ADDRESS ───────── + { fieldtype: "Section Break", label: "Address" }, + + { + fieldname: "address_line1", + label: "Address Line 1", + fieldtype: "Data", + reqd: 1 + }, + { + fieldname: "address_line2", + label: "Address Line 2", + fieldtype: "Data" + }, + { + fieldname: "pincode", + label: "ZIP Code", + fieldtype: "Data", + reqd: 1 + }, + { + fieldname: "city", + label: "City", + fieldtype: "Data" + }, + { + fieldname: "state", + label: "State", + fieldtype: "Data" + }, + { + fieldname: "country", + label: "Country", + fieldtype: "Link", + options: "Country", + default: "United States" + } + ], + + primary_action_label: "Create Customer", + primary_action(values) { + console.log("NS App: Create Customer clicked", values); + + d.disable_primary_action(); + + frappe.call({ + method: "ns_app.api.customer.create_customer_full", + args: values, + callback(r) { + console.log("NS App: Customer created", r.message); + + d.hide(); + frappe.show_alert({ + message: "Customer created via NS App", + indicator: "green" + }); + + if (opts.callback) { + opts.callback(r.message); + } + }, + always() { + d.enable_primary_action(); + } + }); + } + }); + + // ZIP auto-fill + 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(() => {}); + }; + + d.show(); + + // Prevent Enter from submitting unless primary button is focused + d.$wrapper.on("keydown", "input, select, textarea", function (e) { + if (e.key === "Enter") { + const active = document.activeElement; + + // Allow Enter ONLY if primary action button is focused + if ( + active && + active.classList.contains("btn-primary") + ) { + return; + } + + e.preventDefault(); + + // Move to next field + 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(); + } + } + }); + +}; diff --git a/setup.py b/setup.py index e69de29..921efe4 100644 --- a/setup.py +++ b/setup.py @@ -0,0 +1,12 @@ +from setuptools import setup, find_packages + +setup( + name="ns_app", + version="0.0.1", + description="NS Innovations ERPNext Custom App", + author="NS Innovations", + author_email="ty@nsinnovations.net", + packages=find_packages(), + include_package_data=True, + zip_safe=False, +)