diff --git a/rmax_custom/api/material_request.py b/rmax_custom/api/material_request.py
new file mode 100644
index 0000000..556573d
--- /dev/null
+++ b/rmax_custom/api/material_request.py
@@ -0,0 +1,61 @@
+import frappe
+from frappe.utils import nowdate, flt, add_days
+from frappe import _
+
+@frappe.whitelist()
+def create_material_request(item_code, from_warehouse, to_warehouse, qty, schedule_date, material_request_type, company):
+ """Create a Material Request for Material Transfer."""
+
+ if not item_code:
+ frappe.throw(_("Item Code is required"))
+ if not from_warehouse:
+ frappe.throw(_("From Warehouse is required"))
+ if not to_warehouse:
+ frappe.throw(_("To Warehouse is required"))
+ if from_warehouse == to_warehouse:
+ frappe.throw(_("From Warehouse and To Warehouse cannot be the same"))
+ if not qty or flt(qty) <= 0:
+ frappe.throw(_("Quantity must be greater than 0"))
+ if not company:
+ frappe.throw(_("Company is required"))
+
+ # Validate item exists
+ if not frappe.db.exists("Item", item_code):
+ frappe.throw(_("Item {0} does not exist").format(item_code))
+
+ # Validate warehouses exist
+ if not frappe.db.exists("Warehouse", from_warehouse):
+ frappe.throw(_("From Warehouse {0} does not exist").format(from_warehouse))
+ if not frappe.db.exists("Warehouse", to_warehouse):
+ frappe.throw(_("To Warehouse {0} does not exist").format(to_warehouse))
+
+ # Get item details
+ item_doc = frappe.get_cached_doc("Item", item_code)
+
+ # Create Material Request
+ material_request = frappe.new_doc("Material Request")
+ material_request.transaction_date = nowdate()
+ material_request.company = company
+ material_request.material_request_type = "Material Transfer"
+
+ # Add item row
+ material_request.append("items", {
+ "item_code": item_code,
+ "item_name": item_doc.item_name,
+ "description": item_doc.description,
+ "qty": flt(qty),
+ "uom": item_doc.stock_uom,
+ "stock_uom": item_doc.stock_uom,
+ "schedule_date": schedule_date or add_days(nowdate(), 7),
+ "warehouse": to_warehouse,
+ "from_warehouse": from_warehouse,
+ "item_group": item_doc.item_group,
+ "brand": item_doc.brand
+ })
+
+ # Set missing values and insert
+ material_request.set_missing_values()
+ material_request.insert(ignore_permissions=True)
+ material_request.submit()
+
+ return material_request.name
\ No newline at end of file
diff --git a/rmax_custom/api/warehouse_stock.py b/rmax_custom/api/warehouse_stock.py
new file mode 100644
index 0000000..ec3989c
--- /dev/null
+++ b/rmax_custom/api/warehouse_stock.py
@@ -0,0 +1,88 @@
+import frappe
+from frappe import _
+from erpnext.stock.utils import get_stock_balance
+
+
+@frappe.whitelist()
+def get_item_warehouse_stock(item_code, company=None, limit=None, target_warehouse=None):
+ """
+ Get stock balance for an item across all warehouses in a company.
+ Optimized to use Bin table for faster bulk queries.
+
+ Args:
+ item_code: Item code to get stock for
+ company: Company name (optional, will use default if not provided)
+ limit: Limit number of results (optional, for pagination)
+ target_warehouse: Target warehouse to prioritize (optional)
+
+ Returns:
+ List of dictionaries with warehouse name and stock balance
+ """
+ if not item_code:
+ frappe.throw(_("Item Code is required"))
+
+ # Get company from context or use default
+ if not company:
+ company = frappe.defaults.get_user_default("company")
+
+ if not company:
+ frappe.throw(_("Please set a default company"))
+
+ # Get all warehouses for the company (non-group warehouses only)
+ warehouses = frappe.get_all(
+ "Warehouse",
+ filters={
+ "company": company,
+ "is_group": 0,
+ "disabled": 0
+ },
+ fields=["name", "warehouse_name"],
+ order_by="name"
+ )
+
+ if not warehouses:
+ return []
+
+ # Optimize: Use Bin table for faster stock queries (bulk query)
+ warehouse_names = [w.name for w in warehouses]
+
+ # Use Bin table for faster bulk query
+ bin_data = frappe.db.sql("""
+ SELECT warehouse, actual_qty
+ FROM `tabBin`
+ WHERE item_code = %s AND warehouse IN %s
+ """, (item_code, warehouse_names), as_dict=True)
+
+ # Create a dict for quick lookup
+ bin_dict = {d.warehouse: (d.actual_qty or 0.0) for d in bin_data}
+
+ # Build stock data list
+ stock_data = []
+ for warehouse in warehouses:
+ # Get stock from bin_dict (faster than individual get_stock_balance calls)
+ stock_qty = bin_dict.get(warehouse.name, 0.0)
+
+ stock_data.append({
+ "warehouse": warehouse.name,
+ "warehouse_name": warehouse.warehouse_name or warehouse.name,
+ "stock_qty": stock_qty
+ })
+
+ # Filter: Only show warehouses with stock > 0 (or target warehouse even if 0)
+ if target_warehouse:
+ filtered_stock_data = [item for item in stock_data if item["stock_qty"] > 0 or item["warehouse"] == target_warehouse]
+ else:
+ filtered_stock_data = [item for item in stock_data if item["stock_qty"] > 0]
+
+ # Sort: target warehouse first, then by stock quantity descending
+ if target_warehouse:
+ filtered_stock_data.sort(key=lambda x: (-1 if x["warehouse"] == target_warehouse else 0, -x["stock_qty"]))
+ else:
+ filtered_stock_data.sort(key=lambda x: x["stock_qty"], reverse=True)
+
+ # Apply limit if specified
+ if limit:
+ limit = int(limit)
+ return filtered_stock_data[:limit]
+
+ return filtered_stock_data
diff --git a/rmax_custom/fixtures/custom_field.json b/rmax_custom/fixtures/custom_field.json
new file mode 100644
index 0000000..89bd72c
--- /dev/null
+++ b/rmax_custom/fixtures/custom_field.json
@@ -0,0 +1,116 @@
+[
+ {
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "collapsible_depends_on": null,
+ "columns": 0,
+ "default": null,
+ "depends_on": null,
+ "description": null,
+ "docstatus": 0,
+ "doctype": "Custom Field",
+ "dt": "Quotation",
+ "fetch_from": null,
+ "fetch_if_empty": 0,
+ "fieldname": "custom_payment_mode",
+ "fieldtype": "Select",
+ "hidden": 0,
+ "hide_border": 0,
+ "hide_days": 0,
+ "hide_seconds": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_preview": 0,
+ "in_standard_filter": 0,
+ "insert_after": "valid_till",
+ "is_system_generated": 0,
+ "is_virtual": 0,
+ "label": "Payment Mode",
+ "length": 0,
+ "link_filters": null,
+ "mandatory_depends_on": null,
+ "modified": "2026-02-28 11:11:52.476754",
+ "module": null,
+ "name": "Quotation-custom_payment_mode",
+ "no_copy": 0,
+ "non_negative": 0,
+ "options": "Cash\nCredit",
+ "permlevel": 0,
+ "placeholder": null,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "print_width": null,
+ "read_only": 0,
+ "read_only_depends_on": null,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "show_dashboard": 0,
+ "sort_options": 0,
+ "translatable": 0,
+ "unique": 0,
+ "width": null
+ },
+ {
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "collapsible_depends_on": null,
+ "columns": 0,
+ "default": null,
+ "depends_on": null,
+ "description": null,
+ "docstatus": 0,
+ "doctype": "Custom Field",
+ "dt": "Sales Invoice",
+ "fetch_from": null,
+ "fetch_if_empty": 0,
+ "fieldname": "custom_payment_mode",
+ "fieldtype": "Select",
+ "hidden": 0,
+ "hide_border": 0,
+ "hide_days": 0,
+ "hide_seconds": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_preview": 0,
+ "in_standard_filter": 0,
+ "insert_after": "due_date",
+ "is_system_generated": 0,
+ "is_virtual": 0,
+ "label": "Payment Mode",
+ "length": 0,
+ "link_filters": null,
+ "mandatory_depends_on": null,
+ "modified": "2026-02-26 16:23:30.463431",
+ "module": null,
+ "name": "Sales Invoice-custom_payment_mode",
+ "no_copy": 0,
+ "non_negative": 0,
+ "options": "Cash\nCredit",
+ "permlevel": 0,
+ "placeholder": null,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "print_width": null,
+ "read_only": 0,
+ "read_only_depends_on": null,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "show_dashboard": 0,
+ "sort_options": 0,
+ "translatable": 0,
+ "unique": 0,
+ "width": null
+ }
+]
\ No newline at end of file
diff --git a/rmax_custom/hooks.py b/rmax_custom/hooks.py
index f98c63b..286e51c 100644
--- a/rmax_custom/hooks.py
+++ b/rmax_custom/hooks.py
@@ -26,7 +26,12 @@
# include js, css files in header of desk.html
# app_include_css = "/assets/rmax_custom/css/rmax_custom.css"
-# app_include_js = "/assets/rmax_custom/js/rmax_custom.js"
+app_include_js = [
+ "/assets/rmax_custom/js/warehouse_stock_popup.js",
+ "/assets/rmax_custom/js/sales_invoice_pos_total_popup.js",
+]
+
+
# include js, css files in header of web template
# web_include_css = "/assets/rmax_custom/css/rmax_custom.css"
@@ -43,7 +48,10 @@
# page_js = {"page" : "public/js/file.js"}
# include js in doctype views
-# doctype_js = {"doctype" : "public/js/doctype.js"}
+doctype_js = {
+ "Sales Invoice": "rmax_custom/custom_scripts/sales_invoice/sales_invoice.js",
+ "Quotation": "rmax_custom/custom_scripts/quotation/quotation.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"}
@@ -242,3 +250,29 @@
# "Logging DocType Name": 30 # days to retain logs
# }
+fixtures = [
+ {
+ "dt": "Custom Field",
+ "filters": [
+ [
+ "name",
+ "in",
+ [
+ # Sales Invoice
+ "Sales Invoice-custom_payment_mode",
+
+ # Sales Invoice Item
+ "Sales Invoice Item-total_vat_linewise",
+
+
+ # Quotation
+ "Quotation-custom_payment_mode",
+
+ # Quotation Item
+ "Quotation Item-total_vat_linewise",
+
+ ]
+ ]
+ ]
+ }
+]
\ No newline at end of file
diff --git a/rmax_custom/public/js/sales_invoice_pos_total_popup.js b/rmax_custom/public/js/sales_invoice_pos_total_popup.js
new file mode 100644
index 0000000..a754ea7
--- /dev/null
+++ b/rmax_custom/public/js/sales_invoice_pos_total_popup.js
@@ -0,0 +1,427 @@
+// sf_trading: Popup to enter payment amounts when is_pos is checked
+// Shows after save when the correct grand_total is available
+frappe.ui.form.on("Sales Invoice", {
+ refresh: function (frm) {
+ // Capture save action so before_save knows if user clicked Submit (skip confirm)
+ if (frm._sf_save_wrapped) return;
+ frm._sf_save_wrapped = true;
+ const orig = frm.save.bind(frm);
+ frm.save = function (save_action, callback, btn, on_error) {
+ frappe.flags._sf_save_action = save_action || "Save";
+ return orig(save_action, callback, btn, on_error).finally(function () {
+ delete frappe.flags._sf_save_action;
+ });
+ };
+ },
+ after_save: function (frm) {
+ // Prevent popup if flag is set (we're saving from popup)
+ if (frappe.flags.sf_trading_skip_payment_popup) return;
+
+ // Prevent popup if already showing
+ if (frappe.flags.sf_trading_popup_showing) return;
+
+ // Only show for POS invoices in draft state
+ if (!frm.doc.is_pos || frm.doc.docstatus !== 0) return;
+
+ // Validate required fields
+ if (!frm.doc.pos_profile || !frm.doc.grand_total || frm.doc.grand_total <= 0) return;
+
+ // Ensure form is ready
+ if (!frm.doc.name || frm.doc.name.startsWith("new-")) return;
+
+ // Show popup on every save (unless POS Profile disables it)
+ frappe.db.get_value(
+ "POS Profile",
+ frm.doc.pos_profile,
+ "disable_grand_total_to_default_mop",
+ function (r) {
+ if (r && r.message === 1) return;
+ sf_trading_show_pos_total_popup(frm);
+ }
+ );
+ },
+});
+
+function sf_trading_show_pos_total_popup(frm) {
+ // Prevent multiple popups
+ if (frappe.flags.sf_trading_popup_showing) return;
+
+ // Validate form state
+ if (!frm || !frm.doc || !frm.doc.pos_profile) {
+ console.warn("sf_trading: Cannot show popup - invalid form state");
+ return;
+ }
+
+ frappe.flags.sf_trading_popup_showing = true;
+
+ function do_show_popup() {
+ // Load payment modes from POS Profile if empty
+ if (!frm.doc.payments || frm.doc.payments.length === 0) {
+ frappe.call({
+ method: "frappe.client.get",
+ args: { doctype: "POS Profile", name: frm.doc.pos_profile },
+ callback: function (r) {
+ if (r.message && r.message.payments && r.message.payments.length > 0) {
+ frm.clear_table("payments");
+ r.message.payments.forEach(function (pay) {
+ const row = frm.add_child("payments");
+ row.mode_of_payment = pay.mode_of_payment;
+ row.default = pay.default;
+ });
+ frm.refresh_field("payments");
+ frappe.call({
+ doc: frm.doc,
+ method: "set_account_for_mode_of_payment",
+ callback: function () {
+ frm.refresh_field("payments");
+ sf_trading_render_dialog(frm);
+ },
+ error: function() {
+ frappe.flags.sf_trading_popup_showing = false;
+ frappe.msgprint(__("Error loading payment accounts. Please try again."));
+ }
+ });
+ } else {
+ frappe.flags.sf_trading_popup_showing = false;
+ frappe.msgprint(__("Add payment modes in POS Profile first"));
+ }
+ },
+ error: function() {
+ frappe.flags.sf_trading_popup_showing = false;
+ frappe.msgprint(__("Error loading POS Profile. Please try again."));
+ }
+ });
+ } else {
+ sf_trading_render_dialog(frm);
+ }
+ }
+
+ do_show_popup();
+}
+
+function sf_trading_render_dialog(frm) {
+ // Validate form state
+ if (!frm || !frm.doc) {
+ frappe.flags.sf_trading_popup_showing = false;
+ return;
+ }
+
+ const payments = frm.doc.payments || [];
+ if (payments.length === 0) {
+ frappe.flags.sf_trading_popup_showing = false;
+ return;
+ }
+
+ const invoice_total = flt(frm.doc.rounded_total || frm.doc.grand_total || 0);
+ const currency = frm.doc.currency || "";
+
+ // Validate invoice total
+ if (invoice_total <= 0) {
+ frappe.flags.sf_trading_popup_showing = false;
+ frappe.msgprint(__("Invoice total must be greater than zero."));
+ return;
+ }
+
+ const fields = [
+ {
+ fieldname: "invoice_total",
+ fieldtype: "Currency",
+ label: __("Invoice Total"),
+ default: invoice_total,
+ read_only: 1,
+ options: currency,
+ },
+ { fieldtype: "Section Break", label: __("Enter Payment Amounts") },
+ ];
+
+ payments.forEach(function (payment, idx) {
+ const mode = payment.mode_of_payment || "Payment " + (idx + 1);
+ fields.push(
+ {
+ fieldtype: "Section Break",
+ fieldname: "row_" + idx,
+ label: "",
+ hide_border: 1,
+ collapsible: 0,
+ },
+ {
+ fieldname: "pay_" + idx,
+ fieldtype: "Currency",
+ label: mode,
+ default: payment.amount || 0,
+ options: currency,
+ },
+ { fieldtype: "Column Break", fieldname: "cb_" + idx },
+ {
+ fieldtype: "Button",
+ fieldname: "fill_" + idx,
+ label: mode,
+ click: function () {
+ payments.forEach(function (_, i) {
+ d.set_value("pay_" + i, i === idx ? invoice_total : 0);
+ });
+ },
+ }
+ );
+ });
+
+ function apply_payments_and_close(vals, submit) {
+ // Prevent multiple simultaneous saves
+ if (frappe.flags.sf_trading_saving) {
+ frappe.msgprint({
+ title: __("Please Wait"),
+ message: __("Saving in progress. Please wait..."),
+ indicator: "orange",
+ });
+ return;
+ }
+
+ // Validate form state
+ if (!frm || !frm.doc || frm.doc.docstatus !== 0) {
+ frappe.msgprint({
+ title: __("Error"),
+ message: __("Cannot update payments. Form is not in draft state."),
+ indicator: "red",
+ });
+ return;
+ }
+
+ // Validate inputs
+ if (!vals) {
+ frappe.msgprint({
+ title: __("Error"),
+ message: __("Please enter payment amounts."),
+ indicator: "red",
+ });
+ return;
+ }
+
+ let total = 0;
+ // First validate total
+ payments.forEach(function (p, i) {
+ const amt = flt(vals["pay_" + i]) || 0;
+ total += amt;
+ });
+
+ if (total < invoice_total) {
+ frappe.msgprint({
+ title: __("Incomplete"),
+ message: __("{0} still to be allocated", [format_currency(invoice_total - total, currency)]),
+ indicator: "red",
+ });
+ return;
+ }
+
+ // Ensure form payments exist and match
+ const form_payments = frm.doc.payments || [];
+ if (form_payments.length === 0) {
+ frappe.msgprint({
+ title: __("Error"),
+ message: __("No payment methods found. Please refresh the form."),
+ indicator: "red",
+ });
+ return;
+ }
+
+ // Ensure conversion_rate is valid
+ const conversion_rate = flt(frm.doc.conversion_rate) || 1;
+
+ // Helper function for precision
+ const get_precision = function(fieldname, doc) {
+ try {
+ return precision(fieldname, doc) || 2;
+ } catch(e) {
+ return 2; // Default precision
+ }
+ };
+
+ // Update payments with robust matching - update ALL payments (including zero amounts)
+ let update_count = 0;
+ payments.forEach(function (p, i) {
+ const amt = flt(vals["pay_" + i]) || 0;
+ const base_amt = flt(amt * conversion_rate, get_precision("base_amount", p));
+
+ // Try multiple matching strategies for reliability
+ let form_payment = null;
+
+ // Strategy 1: Match by mode_of_payment
+ if (p.mode_of_payment) {
+ form_payment = form_payments.find(fp => fp.mode_of_payment === p.mode_of_payment);
+ }
+
+ // Strategy 2: Match by index if same length
+ if (!form_payment && i < form_payments.length && payments.length === form_payments.length) {
+ form_payment = form_payments[i];
+ }
+
+ // Strategy 3: Match by idx if available
+ if (!form_payment && p.idx) {
+ form_payment = form_payments.find(fp => fp.idx === p.idx);
+ }
+
+ // Strategy 4: Match by name if available
+ if (!form_payment && p.name) {
+ form_payment = form_payments.find(fp => fp.name === p.name);
+ }
+
+ // Update if match found - update ALL payments including zero amounts
+ if (form_payment) {
+ // Update directly on the form doc - this is synchronous
+ form_payment.amount = amt;
+ form_payment.base_amount = base_amt;
+ update_count++;
+ }
+ });
+
+ // Validate that we updated at least one payment
+ if (update_count === 0) {
+ frappe.msgprint({
+ title: __("Error"),
+ message: __("Could not match payments. Please refresh the form and try again."),
+ indicator: "red",
+ });
+ return;
+ }
+
+ // Verify payments were updated
+ const updated_payments = frm.doc.payments.filter(p => flt(p.amount) > 0);
+ if (updated_payments.length === 0) {
+ frappe.msgprint({
+ title: __("Error"),
+ message: __("No payment amounts were set. Please try again."),
+ indicator: "red",
+ });
+ return;
+ }
+
+ // Ensure form recognizes payments as changed
+ // Update the local doclist to ensure changes are tracked
+ if (frm.local_doclist && frm.local_doclist["Sales Invoice Payment"]) {
+ frm.doc.payments.forEach(function(payment) {
+ const doclist_item = frm.local_doclist["Sales Invoice Payment"].find(
+ item => item.name === payment.name || item.idx === payment.idx
+ );
+ if (doclist_item) {
+ doclist_item.amount = payment.amount;
+ doclist_item.base_amount = payment.base_amount;
+ }
+ });
+ }
+
+ // Mark form as dirty to ensure changes are saved
+ frm.dirty();
+
+ // Refresh payments field to update UI before saving
+ frm.refresh_field("payments");
+
+ // Close dialog before saving
+ d.hide();
+ frappe.flags.sf_trading_skip_payment_popup = true;
+ frappe.flags.sf_trading_popup_showing = false;
+ frappe.flags.sf_trading_saving = true;
+
+ // Use save with "Submit" action instead of savesubmit
+ const save_action = submit ? "Submit" : "Save";
+
+ // Delay to ensure refresh_field completes and form processes updates
+ setTimeout(function() {
+ // Double-check payments are in form doc before saving
+ if (!frm.doc.payments || frm.doc.payments.length === 0) {
+ frappe.msgprint({
+ title: __("Error"),
+ message: __("Payments were not updated. Please try again."),
+ indicator: "red",
+ });
+ delete frappe.flags.sf_trading_skip_payment_popup;
+ delete frappe.flags.sf_trading_saving;
+ return;
+ }
+
+ // Verify payments have amounts
+ const total_payment = frm.doc.payments.reduce((sum, p) => sum + flt(p.amount), 0);
+ if (total_payment <= 0) {
+ frappe.msgprint({
+ title: __("Error"),
+ message: __("Total payment amount must be greater than zero."),
+ indicator: "red",
+ });
+ delete frappe.flags.sf_trading_skip_payment_popup;
+ delete frappe.flags.sf_trading_saving;
+ return;
+ }
+
+ // Save - payments are already updated in frm.doc.payments
+ frm.save(save_action).then(function(r) {
+ // After save, refresh payments field to show updated values
+ // Frappe automatically refreshes the form, but we ensure payments are visible
+ setTimeout(function() {
+ frm.refresh_field("payments");
+
+ if (submit) {
+ // Reload after submit to show updated status
+ setTimeout(function() {
+ frm.reload_doc();
+ }, 200);
+ }
+ // For Save, don't reload - just refresh payments field
+ // The form refresh happens automatically, payments should be visible
+ }, 100);
+ }).catch(function(err) {
+ // Show error if save fails
+ frappe.msgprint({
+ title: __("Error"),
+ message: __("Failed to save invoice: {0}", [err.message || err]),
+ indicator: "red",
+ });
+ }).finally(function () {
+ setTimeout(function () {
+ delete frappe.flags.sf_trading_skip_payment_popup;
+ delete frappe.flags.sf_trading_saving;
+ }, 500);
+ });
+ }, 300);
+ }
+
+ const d = new frappe.ui.Dialog({
+ title: __("Enter Payment Amounts"),
+ fields: fields,
+ primary_action_label: __("Save"),
+ primary_action: function (vals) {
+ apply_payments_and_close(vals, false);
+ },
+ secondary_action_label: __("Save & Submit"),
+ secondary_action: function () {
+ const vals = d.get_values();
+ if (vals) apply_payments_and_close(vals, true);
+ },
+ onhide: function() {
+ // Reset flag when dialog is closed
+ frappe.flags.sf_trading_popup_showing = false;
+ }
+ });
+
+ d.show();
+
+ // Align button with input (same level) and field click handler
+ frappe.utils.sleep(100).then(function () {
+ // Align button with input (same level)
+ d.$wrapper.find(".section-body").css({
+ display: "flex",
+ alignItems: "flex-end",
+ });
+
+ // Field click: fill with balance only (invoice_total - sum of others)
+ payments.forEach(function (_, idx) {
+ const field = d.fields_dict["pay_" + idx];
+ if (!field || !field.$wrapper) return;
+ const $input = field.$wrapper.find("input");
+ $input.off("click.sf_fill_balance").on("click.sf_fill_balance", function () {
+ let other = 0;
+ payments.forEach(function (__, i) {
+ if (i !== idx) other += flt(d.get_value("pay_" + i)) || 0;
+ });
+ d.set_value("pay_" + idx, Math.max(0, flt(invoice_total - other, 2)));
+ });
+ });
+ });
+}
diff --git a/rmax_custom/public/js/warehouse_stock_popup.js b/rmax_custom/public/js/warehouse_stock_popup.js
new file mode 100644
index 0000000..b03b0f7
--- /dev/null
+++ b/rmax_custom/public/js/warehouse_stock_popup.js
@@ -0,0 +1,548 @@
+frappe.provide("rmax_custom");
+rmax_custom.stock_displays = {};
+
+rmax_custom.show_warehouse_stock = function(frm, item_row, load_all = false) {
+ if (!item_row || !item_row.item_code) {
+ rmax.hide_stock_display(frm);
+ return;
+ }
+ if (!frappe.meta.has_field(item_row.doctype, "warehouse")) {
+ rmax_custom.hide_stock_display(frm);
+ return;
+ }
+
+ let company = frm.doc.company;
+ if (!company) {
+ rmax_custom.hide_stock_display(frm);
+ return;
+ }
+
+ let current_row = locals[item_row.doctype][item_row.name];
+ let warehouse = current_row ? (current_row.warehouse || "") : "";
+
+ let api_args = {
+ item_code: item_row.item_code,
+ company: company,
+ target_warehouse: warehouse || null
+ };
+
+ if (!load_all) {
+ api_args.limit = 5;
+ }
+
+ // Fetch warehouse stock data
+ frappe.call({
+ method: "rmax_custom.api.warehouse_stock.get_item_warehouse_stock",
+ args: api_args,
+ callback: function(r) {
+ if (r.message && r.message.length > 0) {
+ rmax_custom.render_stock_display(frm, item_row.item_code, r.message, warehouse, item_row.name, load_all);
+ } else {
+ rmax_custom.hide_stock_display(frm);
+ }
+ },
+ error: function(r) {
+ rmax_custom.hide_stock_display(frm);
+ }
+ });
+};
+
+rmax_custom.render_stock_display = function(frm, item_code, stock_data, target_warehouse, item_row_name, is_all_loaded = false) {
+ // Get items grid
+ if (!frm.fields_dict.items || !frm.fields_dict.items.grid) {
+ return;
+ }
+
+ const grid = frm.fields_dict.items.grid;
+ const grid_wrapper = grid.wrapper;
+
+ // Remove existing stock display if any
+ rmax_custom.hide_stock_display(frm);
+
+ // Find the grid footer or create container after grid
+ let $container = grid_wrapper.find(".rmax-custom-stock-display");
+ if (!$container.length) {
+ // Create container after grid body - reduced padding
+ $container = $('
');
+ grid_wrapper.append($container);
+ }
+
+ // Data is already filtered and sorted by backend
+ // Get target warehouse name
+ let target_warehouse_name = "";
+ if (target_warehouse) {
+ let target_wh = stock_data.find(function(item) {
+ return item.warehouse === target_warehouse;
+ });
+ target_warehouse_name = target_wh ? (target_wh.warehouse_name || target_warehouse) : target_warehouse;
+ }
+
+ // If all data is loaded, show all; otherwise only show loaded data (max 5)
+ let sorted_stock_data = stock_data;
+ let visible_data, hidden_data, has_more;
+
+ if (is_all_loaded) {
+ // All data loaded - split into visible (first 5) and hidden (rest) for collapse functionality
+ visible_data = sorted_stock_data.slice(0, 5);
+ hidden_data = sorted_stock_data.slice(5);
+ has_more = hidden_data.length > 0;
+ } else {
+ // Only 5 loaded - show all of them (no hidden rows)
+ visible_data = sorted_stock_data;
+ hidden_data = [];
+ // If we got exactly 5 items, there might be more to load
+ has_more = sorted_stock_data.length >= 5;
+ }
+
+ // Generate unique ID for this display
+ let display_id = "sf_stock_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9);
+
+ // Determine button text and visibility
+ let show_toggle_button = false;
+ let button_text = "";
+ let button_action = ""; // "load_all" or "toggle_view"
+ let initial_collapsed = false; // Start collapsed (show only 5) when all loaded
+
+ if (has_more && !is_all_loaded) {
+ // Not all loaded - show "Show All" button to fetch more
+ show_toggle_button = true;
+ button_text = __("Show All");
+ button_action = "load_all";
+ } else if (is_all_loaded && sorted_stock_data.length > 5) {
+ // All loaded and more than 5 - show toggle to collapse/expand
+ show_toggle_button = true;
+ button_text = __("Show Less"); // Will toggle to "Show All" when collapsed
+ button_action = "toggle_view";
+ initial_collapsed = false; // Start expanded (show all)
+ }
+
+ let html = `
+