From e9b9b2c81bb7d76ae1b178dd4e3c5a81fde42ed9 Mon Sep 17 00:00:00 2001 From: Neha Fathima Date: Wed, 4 Mar 2026 22:51:56 +0530 Subject: [PATCH] feat:customization in the sales invoice --- rmax_custom/api/customer.py | 106 ++++++++++ rmax_custom/hooks.py | 8 +- rmax_custom/public/js/create_customer.js | 138 ++++++++++++ rmax_custom/public/js/sales_invoice_popup.js | 210 +++++++++++++++++++ 4 files changed, 461 insertions(+), 1 deletion(-) create mode 100644 rmax_custom/api/customer.py create mode 100644 rmax_custom/public/js/create_customer.js create mode 100644 rmax_custom/public/js/sales_invoice_popup.js diff --git a/rmax_custom/api/customer.py b/rmax_custom/api/customer.py new file mode 100644 index 0000000..9e85cbc --- /dev/null +++ b/rmax_custom/api/customer.py @@ -0,0 +1,106 @@ +import frappe +from frappe import _ +from frappe.utils import cstr +from frappe.core.doctype.user_permission.user_permission import get_permitted_documents + + +def _get_default_customer_group(): + permitted = get_permitted_documents("Customer Group") + return (permitted[0] if permitted else None) or frappe.db.get_single_value("Selling Settings", "customer_group") or "All Customer Groups" + + +def _get_default_territory(): + permitted = get_permitted_documents("Territory") + return (permitted[0] if permitted else None) or frappe.db.get_single_value("Selling Settings", "territory") or "All Territories" + + +@frappe.whitelist() +def create_customer_with_address( + customer_name, + mobile_no=None, + customer_type="Individual", + email_id=None, + country=None, + default_currency=None, + tax_id=None, + commercial_registration_number=None, + address_type=None, + address_line1=None, + address_line2=None, + building_number=None, + city=None, + state=None, + pincode=None, + district=None +): + + if not customer_name: + frappe.throw(_("Customer Name is required")) + + if not mobile_no: + frappe.throw(_("Mobile No is required")) + + # Prevent duplicate + if frappe.db.exists("Customer", {"customer_name": customer_name}): + frappe.throw(_("Customer already exists")) + + # Get company defaults + if not country or not default_currency: + permitted_companies = get_permitted_documents("Company") + company = (permitted_companies[0] if permitted_companies else None) \ + or frappe.defaults.get_user_default("company") + + if not company: + frappe.throw(_("Please set a default company")) + + company_doc = frappe.get_cached_doc("Company", company) + + country = country or company_doc.country + default_currency = default_currency or company_doc.default_currency + + # Create Customer + customer = frappe.get_doc({ + "doctype": "Customer", + "customer_name": customer_name, + "customer_type": customer_type or "Individual", + "customer_group": _get_default_customer_group(), + "territory": _get_default_territory(), + "default_currency": default_currency, + "tax_id": tax_id, + "mobile_no": mobile_no, + "email_id": email_id + }) + + customer.insert(ignore_permissions=True) + + address_name = None + + address = frappe.get_doc({ + "doctype": "Address", + "address_title": customer_name, + "address_type": address_type or "Billing", + "address_line1": address_line1, + "address_line2": address_line2, + "city": city, + "state": state, + "pincode": pincode, + "country": country, + "is_primary_address": 1, + "is_shipping_address": 1 + }) + address.append("links", { + "link_doctype": "Customer", + "link_name": customer.name, + "link_title": customer.customer_name + }) + + address.insert(ignore_permissions=True) + customer.customer_primary_address = address.name + customer.save(ignore_permissions=True) + + return { + "customer": customer.name, + "address": address.name, + "message": "Customer and Address created successfully" + } + diff --git a/rmax_custom/hooks.py b/rmax_custom/hooks.py index b872a4f..975148e 100644 --- a/rmax_custom/hooks.py +++ b/rmax_custom/hooks.py @@ -29,6 +29,9 @@ app_include_js = [ "/assets/rmax_custom/js/warehouse_stock_popup.js", "/assets/rmax_custom/js/sales_invoice_pos_total_popup.js", + "/assets/rmax_custom/js/sales_invoice_popup.js", + "/assets/rmax_custom/js/create_customer.js", + ] @@ -48,7 +51,10 @@ # page_js = {"page" : "public/js/file.js"} # include js in doctype views -doctype_js = {"Purchase Receipt" : "public/js/purchase receipt.js"} +doctype_js = { + "Quotation": "rmax_custom/custom_scripts/quotation/quotation.js", + "Purchase Receipt" : "public/js/purchase receipt.js" + } # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} diff --git a/rmax_custom/public/js/create_customer.js b/rmax_custom/public/js/create_customer.js new file mode 100644 index 0000000..8f0846a --- /dev/null +++ b/rmax_custom/public/js/create_customer.js @@ -0,0 +1,138 @@ + +frappe.ui.form.on("Sales Invoice", { + refresh: function (frm) { + add_create_customer_button(frm); + + } +}); + +function add_create_customer_button(frm) { + + if (frm.doc.docstatus !== 0) return; + if (!frm.fields_dict.customer) return; + + const $field = frm.fields_dict.customer.$wrapper; + const $parent = $field.parent(); + + if ($parent.find(".create-customer-btn").length) return; + + const $btn = $(` + + `); + + $btn.on("click", function () { + open_create_customer_dialog(frm); + }); + + $field.before($btn); +} + + +function open_create_customer_dialog(frm) { + + let company = frm.doc.company || frappe.defaults.get_default("company"); + + frappe.db.get_value("Company", company, + ["country", "default_currency"], function(r) { + + let country = r.country; + let default_currency = r.default_currency; + + let d = new frappe.ui.Dialog({ + title: "Create New Customer", + fields: [ + { + fieldname: "customer_name", + fieldtype: "Data", + label: "Customer Name", + reqd: 1 + }, + { + fieldname: "mobile_no", + fieldtype: "Data", + label: "Mobile No", + reqd: 1 + }, + { + fieldname: "email_id", + fieldtype: "Data", + label: "Email ID" + }, + { fieldtype: "Section Break", label: "Address Details" }, + { + fieldname: "address_type", + fieldtype: "Select", + label: "Address Type", + options: "Billing\nShipping", + default: "Billing", + reqd: 1 + }, + { + fieldname: "address_line1", + fieldtype: "Data", + label: "Address Line 1", + reqd: 1 + }, + { + fieldname: "address_line2", + fieldtype: "Data", + label: "Address Line 2" + }, + { + fieldname: "city", + fieldtype: "Data", + label: "City/Town", + reqd: 1 + }, + { + fieldname: "country", + fieldtype: "Link", + options: "Country", + label: "Country", + default: country, + reqd: 1 + }, + + + ], + primary_action_label: "Create Customer", + primary_action(values) { + + frappe.call({ + method: "rmax_custom.api.customer.create_customer_with_address", + args: { + customer_name: values.customer_name, + mobile_no: values.mobile_no, + email_id: values.email_id || null, + address_type: values.address_type, + address_line1: values.address_line1, + address_line2: values.address_line2 || null, + city: values.city, + country: values.country, + default_currency: default_currency + }, + callback: function(r) { + if (r.message) { + + frm.set_value("customer", r.message.customer); + frm.refresh_field("customer"); + + frappe.show_alert({ + message: r.message.message, + indicator: "green" + }); + + d.hide(); + } + } + }); + } + }); + + d.show(); + }); +} \ No newline at end of file diff --git a/rmax_custom/public/js/sales_invoice_popup.js b/rmax_custom/public/js/sales_invoice_popup.js new file mode 100644 index 0000000..bdb3060 --- /dev/null +++ b/rmax_custom/public/js/sales_invoice_popup.js @@ -0,0 +1,210 @@ +frappe.ui.form.on("Sales Invoice", { + refresh: function (frm) { + if (frm.doc.docstatus === 0 || frm.doc.docstatus === 1) { + frm.add_custom_button(__("New Invoice"), function () { + window.open("/app/sales-invoice/new", "_blank"); + }); + } + set_pos_behavior(frm); + setup_enter_navigation(frm); + }, + custom_payment_mode(frm) { + set_pos_behavior(frm); + set_customer_filter(frm); + }, + onload(frm) { + set_customer_filter(frm); + }, + before_save(frm) { + if (frm.doc.docstatus !== 0) return; + if (frm.is_new()) return; + if (frm._submit_checked) return; + frappe.validated = false; + frappe.confirm( + "Do you want to Submit this Sales Invoice now?", + + function () { + frm._submit_checked = true; + frm.save('Submit'); + }, + + function () { + frm._submit_checked = true; + frm.save(); + } + ); + }, + items_add: function(frm) { + check_stock(frm); + }, + onload(frm) { + + let grid = frm.fields_dict.items.grid; + + grid.wrapper.on('keydown', 'input, select, textarea', function(e) { + + if (e.key !== "Enter") return; + + e.preventDefault(); + + let $inputs = $(this).closest('.grid-row') + .find('input, select, textarea') + .filter(':visible'); + + let index = $inputs.index(this); + + // 🔹 Same row next column + if (index < $inputs.length - 1) { + $inputs.eq(index + 1).focus(); + } + // 🔹 Last column → next row + else { + let $nextRow = $(this).closest('.grid-row').next('.grid-row'); + + if ($nextRow.length) { + $nextRow.find('input, select, textarea') + .filter(':visible') + .first() + .focus(); + } else { + grid.add_new_row(); + } + } + }); + } +}); + + +function set_pos_behavior(frm) { + if (!frm.doc.custom_payment_mode) return; + if (frm.doc.custom_payment_mode === "Cash") { + frm.set_value("is_pos", 1); + } + else if (frm.doc.custom_payment_mode === "Credit") { + frm.set_value("is_pos", 0); + } +} + + +function set_customer_filter(frm) { + if (frm.doc.custom_payment_mode === 'Credit') { + frm.set_query('customer', function () { + return { + filters: [ + ["Customer Credit Limit", "credit_limit", ">", 0] + ] + }; + }); + } else { + frm.set_query('customer', function () { + return {}; + }); + } +} + +frappe.ui.form.on("Sales Invoice Item", { + items_add: function(frm, cdt, cdn) { + check_stock(frm, cdt, cdn); + }, + item_code: function(frm, cdt, cdn) { + check_stock(frm, cdt, cdn); + }, + qty: function(frm, cdt, cdn) { + check_stock(frm, cdt, cdn); + } +}); + +function check_stock(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (!row || !row.item_code || !row.qty || !row.warehouse) return; + frappe.call({ + method: "erpnext.stock.utils.get_stock_balance", + args: { + item_code: row.item_code, + warehouse: row.warehouse + }, + callback: function(r) { + let stock = r.message || 0; + if (row.qty > stock) { + frappe.msgprint({ + title: "Stock Alert", + message: `Only ${stock} item${stock > 1 ? 's' : ''} are currently available in ${row.warehouse}.`, + indicator: "red" + }); + + frappe.model.set_value(cdt, cdn, "qty", stock); + } + } + }); +} + + +function setup_enter_navigation(frm) { + if (!frm.fields_dict.items) return; + let grid = frm.fields_dict.items.grid; + if (!grid) return; + $(document).off("keydown.pos_enter_override"); + $(document).on("keydown.pos_enter_override", function(e) { + if (e.key !== "Enter") return; + let $active = $(document.activeElement); + if (!$active.closest(".grid-row").length) return; + e.preventDefault(); + e.stopImmediatePropagation(); + + let $currentRow = $active.closest(".grid-row"); + let current_docname = $currentRow.attr("data-name"); + let current_row = grid.get_row(current_docname); + + if (!current_row) return; + + let row_index = grid.grid_rows.indexOf(current_row); + + let $inputs = $currentRow + .find("input, select, textarea") + .filter(":visible:not([readonly]):not([disabled])"); + + let index = $inputs.index(document.activeElement); + if (index < $inputs.length - 1) { + $inputs.eq(index + 1).focus().select(); + return; + } + if (row_index < grid.grid_rows.length - 1) { + + let next_row = grid.grid_rows[row_index + 1]; + next_row.activate(); + + setTimeout(() => { + let $nextInputs = $(next_row.row) + .find("input, select, textarea") + .filter(":visible:not([readonly]):not([disabled])"); + + if ($nextInputs.length) { + $nextInputs.eq(0).focus().select(); + } + }, 100); + + } else { + + grid.add_new_row(); + + setTimeout(() => { + let rows = grid.grid_rows; + let new_row = rows[rows.length - 1]; + + if (!new_row) return; + + new_row.activate(); + + let $newInputs = $(new_row.row) + .find("input, select, textarea") + .filter(":visible:not([readonly]):not([disabled])"); + + if ($newInputs.length) { + $newInputs.eq(0).focus().select(); + } + + }, 150); + } + + }); +} \ No newline at end of file