From 7dba88639bb98244e53f3c92eb9cd4feb04e43d2 Mon Sep 17 00:00:00 2001 From: Neha Fathima Date: Sun, 1 Mar 2026 23:22:48 +0530 Subject: [PATCH 1/2] feat: Customize the sales invoice and purchase invoice --- rmax_custom/fixtures/custom_field.json | 116 ++++++ rmax_custom/hooks.py | 31 +- .../rmax_custom/custom/packed_item.json | 32 ++ rmax_custom/rmax_custom/custom/quotation.json | 281 +++++++++++++ .../rmax_custom/custom/sales_invoice.json | 394 ++++++++++++++++++ .../custom/sales_invoice_item.json | 101 +++++ .../custom_scripts/quotation/quotation.js | 15 + .../custom_scripts/quotation/quotation.py | 0 .../sales_invoice/sales_invoice.js | 121 ++++++ .../sales_invoice/sales_invoice.py | 0 10 files changed, 1090 insertions(+), 1 deletion(-) create mode 100644 rmax_custom/fixtures/custom_field.json create mode 100644 rmax_custom/rmax_custom/custom/packed_item.json create mode 100644 rmax_custom/rmax_custom/custom/quotation.json create mode 100644 rmax_custom/rmax_custom/custom/sales_invoice.json create mode 100644 rmax_custom/rmax_custom/custom/sales_invoice_item.json create mode 100644 rmax_custom/rmax_custom/custom_scripts/quotation/quotation.js create mode 100644 rmax_custom/rmax_custom/custom_scripts/quotation/quotation.py create mode 100644 rmax_custom/rmax_custom/custom_scripts/sales_invoice/sales_invoice.js create mode 100644 rmax_custom/rmax_custom/custom_scripts/sales_invoice/sales_invoice.py 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..12135d3 100644 --- a/rmax_custom/hooks.py +++ b/rmax_custom/hooks.py @@ -43,7 +43,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 +245,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/rmax_custom/custom/packed_item.json b/rmax_custom/rmax_custom/custom/packed_item.json new file mode 100644 index 0000000..e6a20db --- /dev/null +++ b/rmax_custom/rmax_custom/custom/packed_item.json @@ -0,0 +1,32 @@ +{ + "custom_fields": [], + "custom_perms": [], + "doctype": "Packed Item", + "links": [], + "property_setters": [ + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:38.303441", + "default_value": null, + "doc_type": "Packed Item", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "rate", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:38.303441", + "modified_by": "Administrator", + "module": null, + "name": "Packed Item-rate-read_only", + "owner": "Administrator", + "property": "read_only", + "property_type": "Check", + "row_name": null, + "value": "1" + } + ], + "sync_on_migrate": 1 +} \ No newline at end of file diff --git a/rmax_custom/rmax_custom/custom/quotation.json b/rmax_custom/rmax_custom/custom/quotation.json new file mode 100644 index 0000000..3930869 --- /dev/null +++ b/rmax_custom/rmax_custom/custom/quotation.json @@ -0,0 +1,281 @@ +{ + "custom_fields": [ + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "creation": "2026-02-28 11:11:52.476754", + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "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, + "idx": 10, + "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", + "modified_by": "Administrator", + "module": null, + "name": "Quotation-custom_payment_mode", + "no_copy": 0, + "non_negative": 0, + "options": "Cash\nCredit", + "owner": "Administrator", + "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": 1, + "unique": 0, + "width": null + } + ], + "custom_perms": [], + "doctype": "Quotation", + "links": [], + "property_setters": [ + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:38.512555", + "default_value": null, + "doc_type": "Quotation", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "base_rounded_total", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:38.512555", + "modified_by": "Administrator", + "module": null, + "name": "Quotation-base_rounded_total-hidden", + "owner": "Administrator", + "property": "hidden", + "property_type": "Check", + "row_name": null, + "value": "0" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:38.637672", + "default_value": null, + "doc_type": "Quotation", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "base_rounded_total", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:38.637672", + "modified_by": "Administrator", + "module": null, + "name": "Quotation-base_rounded_total-print_hide", + "owner": "Administrator", + "property": "print_hide", + "property_type": "Check", + "row_name": null, + "value": "1" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:38.653781", + "default_value": null, + "doc_type": "Quotation", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "disable_rounded_total", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:38.653781", + "modified_by": "Administrator", + "module": null, + "name": "Quotation-disable_rounded_total-default", + "owner": "Administrator", + "property": "default", + "property_type": "Text", + "row_name": null, + "value": "0" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:38.840211", + "default_value": null, + "doc_type": "Quotation", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "in_words", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:38.840211", + "modified_by": "Administrator", + "module": null, + "name": "Quotation-in_words-hidden", + "owner": "Administrator", + "property": "hidden", + "property_type": "Check", + "row_name": null, + "value": "0" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:38.844953", + "default_value": null, + "doc_type": "Quotation", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "in_words", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:38.844953", + "modified_by": "Administrator", + "module": null, + "name": "Quotation-in_words-print_hide", + "owner": "Administrator", + "property": "print_hide", + "property_type": "Check", + "row_name": null, + "value": "0" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-28 11:11:57.839398", + "default_value": null, + "doc_type": "Quotation", + "docstatus": 0, + "doctype_or_field": "DocType", + "field_name": null, + "idx": 0, + "is_system_generated": 0, + "modified": "2026-02-28 11:11:57.839398", + "modified_by": "Administrator", + "module": null, + "name": "Quotation-main-field_order", + "owner": "Administrator", + "property": "field_order", + "property_type": "Data", + "row_name": null, + "value": "[\"customer_section\", \"title\", \"naming_series\", \"quotation_to\", \"party_name\", \"customer_name\", \"column_break_7\", \"transaction_date\", \"valid_till\", \"custom_payment_mode\", \"column_break1\", \"order_type\", \"company\", \"has_unit_price_items\", \"amended_from\", \"currency_and_price_list\", \"currency\", \"conversion_rate\", \"column_break2\", \"selling_price_list\", \"price_list_currency\", \"plc_conversion_rate\", \"ignore_pricing_rule\", \"items_section\", \"scan_barcode\", \"last_scanned_warehouse\", \"items\", \"sec_break23\", \"total_qty\", \"total_net_weight\", \"column_break_28\", \"base_total\", \"base_net_total\", \"column_break_31\", \"total\", \"net_total\", \"taxes_section\", \"tax_category\", \"taxes_and_charges\", \"column_break_36\", \"shipping_rule\", \"column_break_34\", \"incoterm\", \"named_place\", \"section_break_36\", \"taxes\", \"section_break_39\", \"base_total_taxes_and_charges\", \"column_break_42\", \"total_taxes_and_charges\", \"totals\", \"base_grand_total\", \"base_rounding_adjustment\", \"base_rounded_total\", \"base_in_words\", \"column_break3\", \"grand_total\", \"rounding_adjustment\", \"rounded_total\", \"disable_rounded_total\", \"in_words\", \"section_break_44\", \"apply_discount_on\", \"base_discount_amount\", \"coupon_code\", \"column_break_46\", \"additional_discount_percentage\", \"discount_amount\", \"referral_sales_partner\", \"sec_tax_breakup\", \"other_charges_calculation\", \"bundle_items_section\", \"packed_items\", \"pricing_rule_details\", \"pricing_rules\", \"address_and_contact_tab\", \"billing_address_section\", \"customer_address\", \"address_display\", \"col_break98\", \"contact_person\", \"contact_display\", \"contact_mobile\", \"contact_email\", \"shipping_address_section\", \"shipping_address_name\", \"column_break_81\", \"shipping_address\", \"company_address_section\", \"company_address\", \"company_address_display\", \"column_break_87\", \"company_contact_person\", \"terms_tab\", \"payment_schedule_section\", \"payment_terms_template\", \"payment_schedule\", \"terms_section_break\", \"tc_name\", \"terms\", \"more_info_tab\", \"subscription_section\", \"auto_repeat\", \"update_auto_repeat_reference\", \"print_settings\", \"letter_head\", \"group_same_items\", \"column_break_73\", \"select_print_heading\", \"language\", \"lost_reasons_section\", \"lost_reasons\", \"competitors\", \"column_break_117\", \"order_lost_reason\", \"additional_info_section\", \"status\", \"customer_group\", \"territory\", \"column_break_108\", \"campaign\", \"source\", \"column_break4\", \"opportunity\", \"supplier_quotation\", \"enq_det\", \"connections_tab\"]" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:38.643166", + "default_value": null, + "doc_type": "Quotation", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "rounded_total", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:38.643166", + "modified_by": "Administrator", + "module": null, + "name": "Quotation-rounded_total-hidden", + "owner": "Administrator", + "property": "hidden", + "property_type": "Check", + "row_name": null, + "value": "0" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:38.648454", + "default_value": null, + "doc_type": "Quotation", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "rounded_total", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:38.648454", + "modified_by": "Administrator", + "module": null, + "name": "Quotation-rounded_total-print_hide", + "owner": "Administrator", + "property": "print_hide", + "property_type": "Check", + "row_name": null, + "value": "0" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:39.738395", + "default_value": null, + "doc_type": "Quotation", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "scan_barcode", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:39.738395", + "modified_by": "Administrator", + "module": null, + "name": "Quotation-scan_barcode-hidden", + "owner": "Administrator", + "property": "hidden", + "property_type": "Check", + "row_name": null, + "value": "0" + } + ], + "sync_on_migrate": 1 +} \ No newline at end of file diff --git a/rmax_custom/rmax_custom/custom/sales_invoice.json b/rmax_custom/rmax_custom/custom/sales_invoice.json new file mode 100644 index 0000000..0ca2edd --- /dev/null +++ b/rmax_custom/rmax_custom/custom/sales_invoice.json @@ -0,0 +1,394 @@ +{ + "custom_fields": [ + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "creation": "2026-02-26 16:23:30.463431", + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "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, + "idx": 14, + "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", + "modified_by": "neha@example.com", + "module": null, + "name": "Sales Invoice-custom_payment_mode", + "no_copy": 0, + "non_negative": 0, + "options": "Cash\nCredit", + "owner": "neha@example.com", + "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 + } + ], + "custom_perms": [], + "doctype": "Sales Invoice", + "links": [ + { + "creation": "2022-01-25 10:29:57.771398", + "custom": 0, + "docstatus": 0, + "group": "Reference", + "hidden": 0, + "idx": 1, + "is_child_table": 0, + "link_doctype": "POS Invoice", + "link_fieldname": "consolidated_invoice", + "modified": "2026-02-26 12:40:24.536073", + "modified_by": "Administrator", + "name": "sm57ops284", + "owner": "Administrator", + "parent": "Sales Invoice", + "parent_doctype": null, + "parentfield": "links", + "parenttype": "DocType", + "table_fieldname": null + } + ], + "property_setters": [ + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:38.320364", + "default_value": null, + "doc_type": "Sales Invoice", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "additional_discount_account", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:38.320364", + "modified_by": "Administrator", + "module": null, + "name": "Sales Invoice-additional_discount_account-hidden", + "owner": "Administrator", + "property": "hidden", + "property_type": "Check", + "row_name": null, + "value": "1" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:38.325410", + "default_value": null, + "doc_type": "Sales Invoice", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "additional_discount_account", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:38.325410", + "modified_by": "Administrator", + "module": null, + "name": "Sales Invoice-additional_discount_account-mandatory_depends_on", + "owner": "Administrator", + "property": "mandatory_depends_on", + "property_type": "Code", + "row_name": null, + "value": "" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:38.686058", + "default_value": null, + "doc_type": "Sales Invoice", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "base_rounded_total", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:38.686058", + "modified_by": "Administrator", + "module": null, + "name": "Sales Invoice-base_rounded_total-hidden", + "owner": "Administrator", + "property": "hidden", + "property_type": "Check", + "row_name": null, + "value": "0" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:38.691167", + "default_value": null, + "doc_type": "Sales Invoice", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "base_rounded_total", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:38.691167", + "modified_by": "Administrator", + "module": null, + "name": "Sales Invoice-base_rounded_total-print_hide", + "owner": "Administrator", + "property": "print_hide", + "property_type": "Check", + "row_name": null, + "value": "1" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:38.707745", + "default_value": null, + "doc_type": "Sales Invoice", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "disable_rounded_total", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:38.707745", + "modified_by": "Administrator", + "module": null, + "name": "Sales Invoice-disable_rounded_total-default", + "owner": "Administrator", + "property": "default", + "property_type": "Text", + "row_name": null, + "value": "0" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:38.859219", + "default_value": null, + "doc_type": "Sales Invoice", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "in_words", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:38.859219", + "modified_by": "Administrator", + "module": null, + "name": "Sales Invoice-in_words-hidden", + "owner": "Administrator", + "property": "hidden", + "property_type": "Check", + "row_name": null, + "value": "0" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:38.863949", + "default_value": null, + "doc_type": "Sales Invoice", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "in_words", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:38.863949", + "modified_by": "Administrator", + "module": null, + "name": "Sales Invoice-in_words-print_hide", + "owner": "Administrator", + "property": "print_hide", + "property_type": "Check", + "row_name": null, + "value": "0" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 16:23:39.047054", + "default_value": null, + "doc_type": "Sales Invoice", + "docstatus": 0, + "doctype_or_field": "DocType", + "field_name": null, + "idx": 0, + "is_system_generated": 0, + "modified": "2026-02-26 16:23:39.047054", + "modified_by": "neha@example.com", + "module": null, + "name": "Sales Invoice-main-field_order", + "owner": "neha@example.com", + "property": "field_order", + "property_type": "Data", + "row_name": null, + "value": "[\"customer_section\", \"title\", \"naming_series\", \"customer\", \"customer_name\", \"tax_id\", \"company\", \"company_tax_id\", \"column_break1\", \"posting_date\", \"posting_time\", \"set_posting_time\", \"due_date\", \"custom_payment_mode\", \"column_break_14\", \"is_pos\", \"pos_profile\", \"is_consolidated\", \"is_return\", \"return_against\", \"update_outstanding_for_self\", \"update_billed_amount_in_sales_order\", \"update_billed_amount_in_delivery_note\", \"is_debit_note\", \"amended_from\", \"accounting_dimensions_section\", \"cost_center\", \"dimension_col_break\", \"project\", \"currency_and_price_list\", \"currency\", \"conversion_rate\", \"column_break2\", \"selling_price_list\", \"price_list_currency\", \"plc_conversion_rate\", \"ignore_pricing_rule\", \"items_section\", \"scan_barcode\", \"update_stock\", \"last_scanned_warehouse\", \"column_break_39\", \"set_warehouse\", \"set_target_warehouse\", \"section_break_42\", \"items\", \"section_break_30\", \"total_qty\", \"total_net_weight\", \"column_break_32\", \"base_total\", \"base_net_total\", \"column_break_52\", \"total\", \"net_total\", \"taxes_section\", \"tax_category\", \"taxes_and_charges\", \"column_break_38\", \"shipping_rule\", \"column_break_55\", \"incoterm\", \"named_place\", \"section_break_40\", \"taxes\", \"section_break_43\", \"base_total_taxes_and_charges\", \"column_break_47\", \"total_taxes_and_charges\", \"totals\", \"base_grand_total\", \"base_rounding_adjustment\", \"base_rounded_total\", \"base_in_words\", \"column_break5\", \"grand_total\", \"rounding_adjustment\", \"use_company_roundoff_cost_center\", \"rounded_total\", \"in_words\", \"total_advance\", \"outstanding_amount\", \"disable_rounded_total\", \"section_break_49\", \"apply_discount_on\", \"base_discount_amount\", \"is_cash_or_non_trade_discount\", \"additional_discount_account\", \"column_break_51\", \"additional_discount_percentage\", \"discount_amount\", \"sec_tax_breakup\", \"other_charges_calculation\", \"pricing_rule_details\", \"pricing_rules\", \"packing_list\", \"packed_items\", \"product_bundle_help\", \"time_sheet_list\", \"timesheets\", \"section_break_104\", \"total_billing_hours\", \"column_break_106\", \"total_billing_amount\", \"payments_tab\", \"payments_section\", \"cash_bank_account\", \"payments\", \"section_break_84\", \"base_paid_amount\", \"column_break_86\", \"paid_amount\", \"section_break_88\", \"base_change_amount\", \"column_break_90\", \"change_amount\", \"account_for_change_amount\", \"advances_section\", \"allocate_advances_automatically\", \"only_include_allocated_payments\", \"get_advances\", \"advances\", \"write_off_section\", \"write_off_amount\", \"base_write_off_amount\", \"write_off_outstanding_amount_automatically\", \"column_break_74\", \"write_off_account\", \"write_off_cost_center\", \"loyalty_points_redemption\", \"redeem_loyalty_points\", \"loyalty_points\", \"loyalty_amount\", \"column_break_77\", \"loyalty_program\", \"loyalty_redemption_account\", \"loyalty_redemption_cost_center\", \"contact_and_address_tab\", \"address_and_contact\", \"customer_address\", \"address_display\", \"col_break4\", \"contact_person\", \"contact_display\", \"contact_mobile\", \"contact_email\", \"territory\", \"shipping_address_section\", \"shipping_address_name\", \"shipping_address\", \"shipping_addr_col_break\", \"dispatch_address_name\", \"dispatch_address\", \"company_address_section\", \"company_address\", \"company_address_display\", \"company_addr_col_break\", \"company_contact_person\", \"terms_tab\", \"payment_schedule_section\", \"ignore_default_payment_terms_template\", \"payment_terms_template\", \"payment_schedule\", \"terms_section_break\", \"tc_name\", \"terms\", \"more_info_tab\", \"customer_po_details\", \"po_no\", \"column_break_23\", \"po_date\", \"more_info\", \"debit_to\", \"party_account_currency\", \"is_opening\", \"column_break8\", \"unrealized_profit_loss_account\", \"against_income_account\", \"sales_team_section_break\", \"sales_partner\", \"amount_eligible_for_commission\", \"column_break10\", \"commission_rate\", \"total_commission\", \"section_break2\", \"sales_team\", \"edit_printing_settings\", \"letter_head\", \"group_same_items\", \"column_break_84\", \"select_print_heading\", \"language\", \"subscription_section\", \"subscription\", \"from_date\", \"auto_repeat\", \"column_break_140\", \"to_date\", \"update_auto_repeat_reference\", \"more_information\", \"status\", \"inter_company_invoice_reference\", \"campaign\", \"represents_company\", \"source\", \"customer_group\", \"col_break23\", \"is_internal_customer\", \"is_discounted\", \"remarks\", \"connections_tab\"]" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:38.696694", + "default_value": null, + "doc_type": "Sales Invoice", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "rounded_total", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:38.696694", + "modified_by": "Administrator", + "module": null, + "name": "Sales Invoice-rounded_total-hidden", + "owner": "Administrator", + "property": "hidden", + "property_type": "Check", + "row_name": null, + "value": "0" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:38.702230", + "default_value": null, + "doc_type": "Sales Invoice", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "rounded_total", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:38.702230", + "modified_by": "Administrator", + "module": null, + "name": "Sales Invoice-rounded_total-print_hide", + "owner": "Administrator", + "property": "print_hide", + "property_type": "Check", + "row_name": null, + "value": "0" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:39.710714", + "default_value": null, + "doc_type": "Sales Invoice", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "scan_barcode", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:39.710714", + "modified_by": "Administrator", + "module": null, + "name": "Sales Invoice-scan_barcode-hidden", + "owner": "Administrator", + "property": "hidden", + "property_type": "Check", + "row_name": null, + "value": "0" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:38.282580", + "default_value": null, + "doc_type": "Sales Invoice", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "tax_id", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:38.282580", + "modified_by": "Administrator", + "module": null, + "name": "Sales Invoice-tax_id-hidden", + "owner": "Administrator", + "property": "hidden", + "property_type": "Check", + "row_name": null, + "value": "0" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:38.287848", + "default_value": null, + "doc_type": "Sales Invoice", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "tax_id", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:38.287848", + "modified_by": "Administrator", + "module": null, + "name": "Sales Invoice-tax_id-print_hide", + "owner": "Administrator", + "property": "print_hide", + "property_type": "Check", + "row_name": null, + "value": "0" + } + ], + "sync_on_migrate": 0 +} \ No newline at end of file diff --git a/rmax_custom/rmax_custom/custom/sales_invoice_item.json b/rmax_custom/rmax_custom/custom/sales_invoice_item.json new file mode 100644 index 0000000..ac53659 --- /dev/null +++ b/rmax_custom/rmax_custom/custom/sales_invoice_item.json @@ -0,0 +1,101 @@ +{ + "custom_fields": [], + "custom_perms": [], + "doctype": "Sales Invoice Item", + "links": [], + "property_setters": [ + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:39.662148", + "default_value": null, + "doc_type": "Sales Invoice Item", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "barcode", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:39.662148", + "modified_by": "Administrator", + "module": null, + "name": "Sales Invoice Item-barcode-hidden", + "owner": "Administrator", + "property": "hidden", + "property_type": "Check", + "row_name": null, + "value": "0" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:38.309619", + "default_value": null, + "doc_type": "Sales Invoice Item", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "discount_account", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:38.309619", + "modified_by": "Administrator", + "module": null, + "name": "Sales Invoice Item-discount_account-hidden", + "owner": "Administrator", + "property": "hidden", + "property_type": "Check", + "row_name": null, + "value": "1" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:38.314983", + "default_value": null, + "doc_type": "Sales Invoice Item", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "discount_account", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:38.314983", + "modified_by": "Administrator", + "module": null, + "name": "Sales Invoice Item-discount_account-mandatory_depends_on", + "owner": "Administrator", + "property": "mandatory_depends_on", + "property_type": "Code", + "row_name": null, + "value": "" + }, + { + "_assign": null, + "_comments": null, + "_liked_by": null, + "_user_tags": null, + "creation": "2026-02-26 12:49:39.791908", + "default_value": null, + "doc_type": "Sales Invoice Item", + "docstatus": 0, + "doctype_or_field": "DocField", + "field_name": "target_warehouse", + "idx": 0, + "is_system_generated": 1, + "modified": "2026-02-26 12:49:39.791908", + "modified_by": "Administrator", + "module": null, + "name": "Sales Invoice Item-target_warehouse-hidden", + "owner": "Administrator", + "property": "hidden", + "property_type": "Check", + "row_name": null, + "value": "1" + } + ], + "sync_on_migrate": 0 +} \ No newline at end of file diff --git a/rmax_custom/rmax_custom/custom_scripts/quotation/quotation.js b/rmax_custom/rmax_custom/custom_scripts/quotation/quotation.js new file mode 100644 index 0000000..37ff376 --- /dev/null +++ b/rmax_custom/rmax_custom/custom_scripts/quotation/quotation.js @@ -0,0 +1,15 @@ +frappe.ui.form.on('Quotation', { + refresh(frm) { + setTimeout(() => { + frm.remove_custom_button(__('Sales Order'), __('Create')); + }, 10); + if (frm.doc.docstatus === 1) { + frm.add_custom_button('Sales Invoice', () => { + frappe.model.open_mapped_doc({ + method: "erpnext.selling.doctype.quotation.quotation.make_sales_invoice", + frm: frm + }); + }, 'Create'); + } + } +}); \ No newline at end of file diff --git a/rmax_custom/rmax_custom/custom_scripts/quotation/quotation.py b/rmax_custom/rmax_custom/custom_scripts/quotation/quotation.py new file mode 100644 index 0000000..e69de29 diff --git a/rmax_custom/rmax_custom/custom_scripts/sales_invoice/sales_invoice.js b/rmax_custom/rmax_custom/custom_scripts/sales_invoice/sales_invoice.js new file mode 100644 index 0000000..2321c57 --- /dev/null +++ b/rmax_custom/rmax_custom/custom_scripts/sales_invoice/sales_invoice.js @@ -0,0 +1,121 @@ +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"); + }); + } + if (frm.doc.docstatus === 1) { + frm.add_custom_button(__('Return Invoice'), function() { + frappe.call({ + method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_sales_return", + args: { + source_name: frm.doc.name + }, + callback: function(r) { + if (r.message) { + frappe.model.sync(r.message); + frappe.set_route("Form", r.message.doctype, r.message.name); + } + } + }); + + }); + } + set_pos_behavior(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); + } + +}); + + +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); + } + } + }); +} \ No newline at end of file diff --git a/rmax_custom/rmax_custom/custom_scripts/sales_invoice/sales_invoice.py b/rmax_custom/rmax_custom/custom_scripts/sales_invoice/sales_invoice.py new file mode 100644 index 0000000..e69de29 From f5ef58de1a27ceca32b5eb3879c176d2cf3a6e2a Mon Sep 17 00:00:00 2001 From: Neha Fathima Date: Wed, 4 Mar 2026 09:56:16 +0530 Subject: [PATCH 2/2] feat: Implmented the availabel stock balance and custoemr creation --- rmax_custom/api/material_request.py | 61 ++ rmax_custom/api/warehouse_stock.py | 88 +++ rmax_custom/hooks.py | 7 +- .../js/sales_invoice_pos_total_popup.js | 427 ++++++++++++++ .../public/js/warehouse_stock_popup.js | 548 ++++++++++++++++++ .../sales_invoice/sales_invoice.js | 211 ++++++- .../sales_invoice/sales_invoice.py | 67 +++ rmax_custom/rmax_custom/page/__init__.py | 0 .../page/return_invoice/__init__.py | 0 .../page/return_invoice/return_invoice.js | 7 + .../page/return_invoice/return_invoice.json | 18 + 11 files changed, 1414 insertions(+), 20 deletions(-) create mode 100644 rmax_custom/api/material_request.py create mode 100644 rmax_custom/api/warehouse_stock.py create mode 100644 rmax_custom/public/js/sales_invoice_pos_total_popup.js create mode 100644 rmax_custom/public/js/warehouse_stock_popup.js create mode 100644 rmax_custom/rmax_custom/page/__init__.py create mode 100644 rmax_custom/rmax_custom/page/return_invoice/__init__.py create mode 100644 rmax_custom/rmax_custom/page/return_invoice/return_invoice.js create mode 100644 rmax_custom/rmax_custom/page/return_invoice/return_invoice.json 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/hooks.py b/rmax_custom/hooks.py index 12135d3..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" 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 = ` +
+
+ ${__("Stock Availability - {0}", [item_code])} + ${target_warehouse ? `→ ${target_warehouse_name}` : ''} +
+ ${show_toggle_button ? ` + + ` : ''} +
+
+ + + + + + + + + + `; + + if (sorted_stock_data.length === 0) { + html += ` + + + + `; + } else { + // Render visible rows (first 5) + visible_data.forEach(function(item, index) { + let stock_color = item.stock_qty > 0 ? "#28a745" : "#6c757d"; + let stock_indicator = item.stock_qty > 0 ? "●" : "○"; + let is_target = item.warehouse === target_warehouse; + let row_bg = is_target ? "#e3f2fd" : "white"; + let row_class = `${display_id}_row`; + + html += ` + + + + + + `; + }); + + // Render hidden rows (rest, initially hidden) - only if all data is loaded + if (has_more && is_all_loaded && hidden_data.length > 0) { + hidden_data.forEach(function(item, index) { + let stock_color = item.stock_qty > 0 ? "#28a745" : "#6c757d"; + let stock_indicator = item.stock_qty > 0 ? "●" : "○"; + let is_target = false + let row_bg = is_target ? "#e3f2fd" : "white"; + let row_class = `${display_id}_row ${display_id}_hidden_row`; + + html += ` + + + + + + `; + }); + } + } + + html += ` + +
${__("Warehouse")}${__("Stock Qty")}${__("Action")}
+ ${__("No warehouses with stock available")} +
+ ${stock_indicator} + ${item.warehouse_name || item.warehouse} + ${is_target ? '(Target)' : ''} + + + ${format_number(item.stock_qty, null, {precision: 2})} + + + ${is_target ? '-' : ` + + `} +
+ ${stock_indicator} + ${item.warehouse_name || item.warehouse} + ${is_target ? '(Target)' : ''} + + + ${format_number(item.stock_qty, null, {precision: 2})} + + + ${is_target ? '-' : ` + + `} +
+
+ `; + + $container.html(html); + $container.show(); + + // Store reference + rmax_custom.stock_displays[frm.doctype + "_" + frm.docname] = $container; + + // Add toggle button handler + let $toggle_btn = $container.find(`.${display_id}_toggle_btn`); + if ($toggle_btn.length) { + let button_action = $toggle_btn.data("action"); + // Store expanded state - start expanded (show all) when all data is loaded + let is_expanded = (button_action === "toggle_view"); + + $toggle_btn.on("click", function() { + if (button_action === "load_all") { + // Fetch all warehouses when "Show All" is clicked + let item_doctype = "Sales Invoice Item"; // Default, can be extended for other doctypes + let item_row = locals[item_doctype] && locals[item_doctype][item_row_name]; + if (item_row && item_row.item_code) { + // Show loading state + $toggle_btn.prop("disabled", true).html(__("Loading...")); + + // Fetch all warehouses (load_all = true) + rmax_custom.show_warehouse_stock(frm, item_row, true); + } + } else if (button_action === "toggle_view") { + // Toggle between showing 5 and showing all + let $hidden_rows = $container.find(`.${display_id}_hidden_row`); + + if (is_expanded) { + // Collapse: hide rows beyond first 5 + $hidden_rows.hide(); + $toggle_btn.html(__("Show All")); + is_expanded = false; + } else { + // Expand: show all rows + $hidden_rows.show(); + $toggle_btn.html(__("Show Less")); + is_expanded = true; + } + } + }); + } + + // Add click handlers for request buttons + $container.find(".request-item-btn").on("click", function() { + let $btn = $(this); + let item_code = $btn.data("item-code"); + let from_warehouse = $btn.data("from-warehouse"); + let from_warehouse_name = $btn.data("from-warehouse-name"); + let to_warehouse = $btn.data("to-warehouse"); + let to_warehouse_name = $btn.data("to-warehouse-name"); + + rmax_custom.create_material_request(frm, item_code, from_warehouse, from_warehouse_name, to_warehouse, to_warehouse_name); + }); +}; + +rmax_custom.hide_stock_display = function(frm) { + let key = frm.doctype + "_" + frm.docname; + let $display = rmax_custom.stock_displays[key]; + if ($display && $display.length) { + $display.hide(); + } +}; + +rmax_custom.create_material_request = function(frm, item_code, from_warehouse, from_warehouse_name, to_warehouse, to_warehouse_name) { + // Create Material Request dialog + let dialog = new frappe.ui.Dialog({ + title: __("Create Material Transfer Request"), + fields: [ + { + fieldtype: "Data", + fieldname: "item_code", + label: __("Item Code"), + default: item_code, + read_only: 1 + }, + { + fieldtype: "Data", + fieldname: "from_warehouse", + label: __("From Warehouse"), + default: from_warehouse_name || from_warehouse, + read_only: 1 + }, + { + fieldtype: "Data", + fieldname: "to_warehouse", + label: __("To Warehouse"), + default: to_warehouse_name || to_warehouse, + read_only: 1 + }, + { + fieldtype: "Float", + fieldname: "qty", + label: __("Quantity"), + default: 1, + reqd: 1 + }, + { + fieldtype: "Date", + fieldname: "schedule_date", + label: __("Required Date"), + default: frappe.datetime.add_days(frappe.datetime.get_today(), 7), + reqd: 1 + } + ], + primary_action_label: __("Create"), + primary_action: function() { + let values = dialog.get_values(); + if (!values) { + return; + } + + // Create Material Request (always Material Transfer) + frappe.call({ + method: "rmax_custom.api.material_request.create_material_request", + args: { + item_code: values.item_code, + from_warehouse: from_warehouse, + to_warehouse: to_warehouse, + qty: values.qty, + schedule_date: values.schedule_date, + material_request_type: "Material Transfer", + company: frm.doc.company + }, + callback: function(r) { + if (r.message) { + let current_route = frappe.get_route(); + frappe.show_alert({ + message: __("Material Request {0} created & submitted", [r.message]), + indicator: "green" + }); + setTimeout(function() { + frappe.set_route(current_route); + }, 300); + } + dialog.hide(); + }, + error: function(r) { + frappe.show_alert({ + message: __("Error creating Material Request"), + indicator: "red" + }); + } + }); + } + }); + + dialog.show(); +}; + +// Hook into item_code and warehouse onchange - Only for Sales Invoice for now +// Can be extended to other doctypes in future by adding them to this array +let item_doctypes = [ + "Sales Invoice Item" +]; + +// Track currently selected row +rmax_custom.current_selected_row = null; + +item_doctypes.forEach(function(child_doctype) { + frappe.ui.form.on(child_doctype, { + item_code: function(frm, cdt, cdn) { + let item_row = locals[cdt][cdn]; + + // Track selected row + rmax_custom.current_selected_row = item_row; + + if (item_row.item_code && + frappe.meta.has_field(item_row.doctype, "warehouse") && + frm.doc.company) { + + // Show stock immediately when item_code changes + clearTimeout(item_row._rmax_stock_timeout); + item_row._rmax_stock_timeout = setTimeout(function() { + rmax_custom.show_warehouse_stock(frm, item_row); + }, 300); + } else { + rmax_custom.hide_stock_display(frm); + } + }, + + item_code_focus: function(frm, cdt, cdn) { + // When item_code field is focused/clicked, show stock for that row + let item_row = locals[cdt][cdn]; + if (item_row && item_row.item_code && + frappe.meta.has_field(item_row.doctype, "warehouse") && + frm.doc.company) { + rmax_custom.current_selected_row = item_row; + clearTimeout(item_row._rmax_custom_stock_timeout); + item_row._rmax_custom_stock_timeout = setTimeout(function() { + rmax_custom.show_warehouse_stock(frm, item_row); + }, 100); + } + }, + + warehouse: function(frm, cdt, cdn) { + let item_row = locals[cdt][cdn]; + + // Track selected row + rmax_custom.current_selected_row = item_row; + + if (item_row.item_code && + frappe.meta.has_field(item_row.doctype, "warehouse") && + frm.doc.company) { + + // Update stock display when warehouse changes + clearTimeout(item_row._rmax_custom_stock_timeout); + item_row._rmax_custom_stock_timeout = setTimeout(function() { + rmax_custom.show_warehouse_stock(frm, item_row); + }, 300); + } + }, + + // Detect when row is selected/clicked + form_render: function(frm, cdt, cdn) { + let item_row = locals[cdt][cdn]; + if (item_row && item_row.item_code) { + rmax_custom.current_selected_row = item_row; + if (frm.doc.company && frappe.meta.has_field(item_row.doctype, "warehouse")) { + clearTimeout(item_row._rmax_custom_stock_timeout); + item_row._rmax_custom_stock_timeout = setTimeout(function() { + rmax_custom.show_warehouse_stock(frm, item_row); + }, 200); + } + } + } + }); +}); + +// Listen to item_code field clicks/focus for all doctypes +function setup_item_code_field_listeners(frm) { + if (!frm.fields_dict.items || !frm.fields_dict.items.grid) { + return; + } + + const grid = frm.fields_dict.items.grid; + + // Listen for clicks on item_code field in grid rows + grid.wrapper.on("click focus", "[data-fieldname='item_code'] input, [data-fieldname='item_code'] .link-field", function() { + let $field = $(this); + let $row = $field.closest(".grid-row"); + let idx = $row.attr("data-idx"); + + if (idx && frm.doc.items) { + let item_row = frm.doc.items.find(function(item) { + return item.idx == idx; + }); + + if (item_row && item_row.item_code) { + rmax_custom.current_selected_row = item_row; + if (frm.doc.company && frappe.meta.has_field(item_row.doctype, "warehouse")) { + clearTimeout(item_row._rmax_custom_stock_timeout); + item_row._rmax_custom_stock_timeout = setTimeout(function() { + rmax_custom.show_warehouse_stock(frm, item_row); + }, 100); + } + } + } + }); +} + +// Hide stock availability when user clicks or focuses outside the items grid +function setup_hide_stock_on_click_outside(frm) { + if (!frm.fields_dict.items || !frm.fields_dict.items.grid) { + return; + } + + const grid = frm.fields_dict.items.grid; + const $grid_wrapper = grid.wrapper; + + // Remove previous handlers to avoid duplicates on refresh + $grid_wrapper.off("focusout.rmax_custom_stock"); + if (frm.wrapper) { + $(frm.wrapper).off("click.rmax_custom"); + } + + // When focus leaves the grid, hide stock if focus did not move to another element inside the grid + $grid_wrapper.on("focusout.rmax_custom_stock", function() { + setTimeout(function() { + const active = document.activeElement; + if (!active || !$grid_wrapper[0].contains(active)) { + rmax_custom.hide_stock_display(frm); + } + }, 150); + }); + + // When user clicks anywhere on the form, hide stock if click was outside the items grid + if (frm.wrapper) { + $(frm.wrapper).on("click.rmax_custom", function(e) { + if (!$grid_wrapper.length || !$grid_wrapper[0].contains(e.target)) { + rmax_custom.hide_stock_display(frm); + } + }); + } +} + +// Only enable for Sales Invoice for now +// Can be extended to other doctypes in future by adding them here +frappe.ui.form.on("Sales Invoice", { + refresh: function(frm) { + setup_item_code_field_listeners(frm); + // Always hide stock on load/refresh until user clicks an item + rmax_custom.hide_stock_display(frm); + + // Hide stock when user clicks or focuses outside the items grid + setup_hide_stock_on_click_outside(frm); + } +}); + diff --git a/rmax_custom/rmax_custom/custom_scripts/sales_invoice/sales_invoice.js b/rmax_custom/rmax_custom/custom_scripts/sales_invoice/sales_invoice.js index 2321c57..7b0f825 100644 --- a/rmax_custom/rmax_custom/custom_scripts/sales_invoice/sales_invoice.js +++ b/rmax_custom/rmax_custom/custom_scripts/sales_invoice/sales_invoice.js @@ -5,23 +5,6 @@ frappe.ui.form.on("Sales Invoice", { window.open("/app/sales-invoice/new", "_blank"); }); } - if (frm.doc.docstatus === 1) { - frm.add_custom_button(__('Return Invoice'), function() { - frappe.call({ - method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_sales_return", - args: { - source_name: frm.doc.name - }, - callback: function(r) { - if (r.message) { - frappe.model.sync(r.message); - frappe.set_route("Form", r.message.doctype, r.message.name); - } - } - }); - - }); - } set_pos_behavior(frm); }, custom_payment_mode(frm) { @@ -52,8 +35,64 @@ frappe.ui.form.on("Sales Invoice", { }, items_add: function(frm) { check_stock(frm); - } + }, + onload(frm) { + let grid = frm.fields_dict.items.grid; + grid.wrapper.off('keydown.enter_nav'); + grid.wrapper.on('keydown.enter_nav', 'input, select, textarea', function(e) { + if (e.key !== "Enter") return; + e.preventDefault(); + let $currentRow = $(this).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(this); + if (index < $inputs.length - 1) { + $inputs.eq(index + 1).focus(); + } + else { + 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(); + } + }, 50); + + } else { + grid.add_new_row(); + + setTimeout(() => { + let new_row = grid.grid_rows[grid.grid_rows.length - 1]; + + if (new_row) { + 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(); + } + } + }, 100); + } + } + }); + } }); @@ -118,4 +157,138 @@ function check_stock(frm, cdt, cdn) { } } }); -} \ No newline at end of file +} + + +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.rmax_custom.custom_scripts.sales_invoice.sales_invoice.create_customer_with_primary_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: 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(); + }); +} + + diff --git a/rmax_custom/rmax_custom/custom_scripts/sales_invoice/sales_invoice.py b/rmax_custom/rmax_custom/custom_scripts/sales_invoice/sales_invoice.py index e69de29..79109e7 100644 --- a/rmax_custom/rmax_custom/custom_scripts/sales_invoice/sales_invoice.py +++ b/rmax_custom/rmax_custom/custom_scripts/sales_invoice/sales_invoice.py @@ -0,0 +1,67 @@ +import frappe +from frappe import _ +from frappe.utils import nowdate, add_days, flt + + + + + +@frappe.whitelist() +def create_customer_with_primary_address( + customer_name, + mobile_no=None, + email_id=None, + address_type=None, + address_line1=None, + address_line2=None, + city=None, + country=None, + default_currency=None +): + + if not customer_name: + frappe.throw("Customer Name is required") + + if not mobile_no: + frappe.throw("Mobile No is required") + + if not address_line1 or not city or not country: + frappe.throw("Address details are required") + customer = frappe.get_doc({ + "doctype": "Customer", + "customer_name": customer_name, + "customer_type": "Individual", + "customer_group": "All Customer Groups", + "territory": "All Territories", + "mobile_no": mobile_no, + "email_id": email_id, + "default_currency": default_currency + }) + + customer.insert(ignore_permissions=True) + address = frappe.get_doc({ + "doctype": "Address", + "address_title": customer_name, + "address_type": address_type, + "address_line1": address_line1, + "address_line2": address_line2, + "city": city, + "country": country + }) + 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/rmax_custom/page/__init__.py b/rmax_custom/rmax_custom/page/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rmax_custom/rmax_custom/page/return_invoice/__init__.py b/rmax_custom/rmax_custom/page/return_invoice/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rmax_custom/rmax_custom/page/return_invoice/return_invoice.js b/rmax_custom/rmax_custom/page/return_invoice/return_invoice.js new file mode 100644 index 0000000..a3d8cbd --- /dev/null +++ b/rmax_custom/rmax_custom/page/return_invoice/return_invoice.js @@ -0,0 +1,7 @@ +frappe.pages['return-invoice'].on_page_load = function(wrapper) { + var page = frappe.ui.make_app_page({ + parent: wrapper, + title: 'None', + single_column: true + }); +} \ No newline at end of file diff --git a/rmax_custom/rmax_custom/page/return_invoice/return_invoice.json b/rmax_custom/rmax_custom/page/return_invoice/return_invoice.json new file mode 100644 index 0000000..c22c27f --- /dev/null +++ b/rmax_custom/rmax_custom/page/return_invoice/return_invoice.json @@ -0,0 +1,18 @@ +{ + "content": null, + "creation": "2026-03-03 10:38:51.012795", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2026-03-03 10:39:12.975907", + "modified_by": "Administrator", + "module": "Rmax Custom", + "name": "return-invoice", + "owner": "Administrator", + "page_name": "return-invoice", + "roles": [], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0 +} \ No newline at end of file