From 89f56527cad50cc53af2e0002e596af19b5260be Mon Sep 17 00:00:00 2001 From: MostafaKadry Date: Tue, 9 Jun 2026 11:40:39 +0300 Subject: [PATCH 1/2] feat: add District and Governorate doctype with custom fields --- pos_next/pos_next/custom/customer.json | 137 ++++++++++++++++++ .../pos_next/doctype/district/__init__.py | 0 .../pos_next/doctype/district/district.js | 8 + .../pos_next/doctype/district/district.json | 63 ++++++++ .../pos_next/doctype/district/district.py | 9 ++ .../doctype/district/test_district.py | 9 ++ .../pos_next/doctype/governorate/__init__.py | 0 .../doctype/governorate/governorate.js | 8 + .../doctype/governorate/governorate.json | 46 ++++++ .../doctype/governorate/governorate.py | 9 ++ .../doctype/governorate/test_governorate.py | 9 ++ 11 files changed, 298 insertions(+) create mode 100644 pos_next/pos_next/custom/customer.json create mode 100644 pos_next/pos_next/doctype/district/__init__.py create mode 100644 pos_next/pos_next/doctype/district/district.js create mode 100644 pos_next/pos_next/doctype/district/district.json create mode 100644 pos_next/pos_next/doctype/district/district.py create mode 100644 pos_next/pos_next/doctype/district/test_district.py create mode 100644 pos_next/pos_next/doctype/governorate/__init__.py create mode 100644 pos_next/pos_next/doctype/governorate/governorate.js create mode 100644 pos_next/pos_next/doctype/governorate/governorate.json create mode 100644 pos_next/pos_next/doctype/governorate/governorate.py create mode 100644 pos_next/pos_next/doctype/governorate/test_governorate.py diff --git a/pos_next/pos_next/custom/customer.json b/pos_next/pos_next/custom/customer.json new file mode 100644 index 000000000..ba3c426ed --- /dev/null +++ b/pos_next/pos_next/custom/customer.json @@ -0,0 +1,137 @@ +{ + "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-06-09 11:35:04.696593", + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "dt": "Customer", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_district", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "idx": 47, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 0, + "insert_after": "custom_governorate", + "is_system_generated": 0, + "is_virtual": 0, + "label": "District", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2026-06-09 11:35:04.696593", + "modified_by": "Administrator", + "module": null, + "name": "Customer-custom_district", + "no_copy": 0, + "non_negative": 0, + "options": "District", + "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": 0, + "unique": 0, + "width": null + }, + { + "_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-06-09 10:57:06.280375", + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "dt": "Customer", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_governorate", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "idx": 47, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 0, + "insert_after": "primary_address", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Governorate", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2026-06-09 10:57:06.280375", + "modified_by": "Administrator", + "module": null, + "name": "Customer-custom_governorate", + "no_copy": 0, + "non_negative": 0, + "options": "Governorate", + "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": 0, + "unique": 0, + "width": null + } + ], + "custom_perms": [], + "doctype": "Customer", + "links": [], + "property_setters": [], + "sync_on_migrate": 1 +} \ No newline at end of file diff --git a/pos_next/pos_next/doctype/district/__init__.py b/pos_next/pos_next/doctype/district/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pos_next/pos_next/doctype/district/district.js b/pos_next/pos_next/doctype/district/district.js new file mode 100644 index 000000000..bf6bfed54 --- /dev/null +++ b/pos_next/pos_next/doctype/district/district.js @@ -0,0 +1,8 @@ +// Copyright (c) 2026, BrainWise and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("District", { +// refresh(frm) { + +// }, +// }); diff --git a/pos_next/pos_next/doctype/district/district.json b/pos_next/pos_next/doctype/district/district.json new file mode 100644 index 000000000..28e237f6d --- /dev/null +++ b/pos_next/pos_next/doctype/district/district.json @@ -0,0 +1,63 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-06-09 10:55:40.584980", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "governorate", + "column_break_drcm", + "district" + ], + "fields": [ + { + "fieldname": "district", + "fieldtype": "Data", + "in_list_view": 1, + "label": "District", + "reqd": 1 + }, + { + "fieldname": "governorate", + "fieldtype": "Link", + "in_filter": 1, + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "Governorate", + "options": "Governorate", + "reqd": 1 + }, + { + "fieldname": "column_break_drcm", + "fieldtype": "Column Break" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-06-09 11:02:01.069029", + "modified_by": "Administrator", + "module": "POS Next", + "name": "District", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/pos_next/pos_next/doctype/district/district.py b/pos_next/pos_next/doctype/district/district.py new file mode 100644 index 000000000..0f49a3460 --- /dev/null +++ b/pos_next/pos_next/doctype/district/district.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, BrainWise and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class District(Document): + pass diff --git a/pos_next/pos_next/doctype/district/test_district.py b/pos_next/pos_next/doctype/district/test_district.py new file mode 100644 index 000000000..979b3b9e2 --- /dev/null +++ b/pos_next/pos_next/doctype/district/test_district.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, BrainWise and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestDistrict(FrappeTestCase): + pass diff --git a/pos_next/pos_next/doctype/governorate/__init__.py b/pos_next/pos_next/doctype/governorate/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pos_next/pos_next/doctype/governorate/governorate.js b/pos_next/pos_next/doctype/governorate/governorate.js new file mode 100644 index 000000000..e4f1b3f7e --- /dev/null +++ b/pos_next/pos_next/doctype/governorate/governorate.js @@ -0,0 +1,8 @@ +// Copyright (c) 2026, BrainWise and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Governorate", { +// refresh(frm) { + +// }, +// }); diff --git a/pos_next/pos_next/doctype/governorate/governorate.json b/pos_next/pos_next/doctype/governorate/governorate.json new file mode 100644 index 000000000..7a720c0c0 --- /dev/null +++ b/pos_next/pos_next/doctype/governorate/governorate.json @@ -0,0 +1,46 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-06-09 10:54:57.009043", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "governorate" + ], + "fields": [ + { + "fieldname": "governorate", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Governorate", + "reqd": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-06-09 10:56:00.364650", + "modified_by": "Administrator", + "module": "POS Next", + "name": "Governorate", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/pos_next/pos_next/doctype/governorate/governorate.py b/pos_next/pos_next/doctype/governorate/governorate.py new file mode 100644 index 000000000..bcb14a4da --- /dev/null +++ b/pos_next/pos_next/doctype/governorate/governorate.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, BrainWise and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class Governorate(Document): + pass diff --git a/pos_next/pos_next/doctype/governorate/test_governorate.py b/pos_next/pos_next/doctype/governorate/test_governorate.py new file mode 100644 index 000000000..eebff73f0 --- /dev/null +++ b/pos_next/pos_next/doctype/governorate/test_governorate.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, BrainWise and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestGovernorate(FrappeTestCase): + pass From af58a4d7895d9f580a338fbfd98c62c2bea78e93 Mon Sep 17 00:00:00 2001 From: MostafaKadry Date: Tue, 9 Jun 2026 12:54:10 +0300 Subject: [PATCH 2/2] feat: add custom governorate and district fields to customer creation and management --- .../components/sale/CreateCustomerDialog.vue | 117 +++++++++++++++++- pos_next/api/customers.py | 6 + pos_next/hooks.py | 2 +- .../pos_next/doctype/district/district.json | 4 +- .../doctype/governorate/governorate.json | 15 ++- pos_next/public/js/customer.js | 20 +++ pos_next/translations/ar.csv | 7 +- 7 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 pos_next/public/js/customer.js diff --git a/POS/src/components/sale/CreateCustomerDialog.vue b/POS/src/components/sale/CreateCustomerDialog.vue index cba4978d8..ff13eca08 100644 --- a/POS/src/components/sale/CreateCustomerDialog.vue +++ b/POS/src/components/sale/CreateCustomerDialog.vue @@ -130,6 +130,41 @@ + + +
+ + +
+ + +
+ + +
@@ -228,6 +263,8 @@ const countrySearchRef = ref(null) const customerGroups = ref([]) const territories = ref([]) +const governorates = ref([]) +const districts = ref([]) const customerData = ref({ customer_name: "", @@ -235,6 +272,8 @@ const customerData = ref({ email_id: "", customer_group: "", territory: "", + custom_governorate: "", + custom_district: "", }) // ============================================================================= @@ -339,6 +378,8 @@ const createCustomerResource = createResource({ email_id: customerData.value.email_id || "", customer_group: customerData.value.customer_group || "", territory: customerData.value.territory || "", + custom_governorate: customerData.value.custom_governorate || "", + custom_district: customerData.value.custom_district || "", pos_profile: props.posProfile, }), onSuccess: (data) => { @@ -363,6 +404,8 @@ const updateCustomerResource = createResource({ territory: customerData.value.territory || "", mobile_no: customerData.value.mobile_no || "", email_id: customerData.value.email_id || "", + custom_governorate: customerData.value.custom_governorate || "", + custom_district: customerData.value.custom_district || "", }, }), onSuccess: (data) => { @@ -427,6 +470,50 @@ const territoriesResource = createListResource("Territory", (names) => { } }) +const governoratesResource = createListResource("Governorate", (names) => { + governorates.value = names +}) + + +const customerLocationResource = createResource({ + url: "frappe.client.get_value", + makeParams: () => ({ + doctype: "Customer", + filters: { name: props.customer?.name }, + fieldname: ["custom_governorate", "custom_district"], + }), + auto: false, + onSuccess: (data) => { + customerData.value.custom_governorate = data?.custom_governorate || "" + customerData.value.custom_district = data?.custom_district || "" + }, + onError: (err) => log.error("Error loading customer location", err), +}) + + +const districtsResource = createResource({ + url: "frappe.client.get_list", + makeParams: () => ({ + doctype: "District", + fields: ["name", "district"], + filters: { governorate: customerData.value.custom_governorate }, + limit_page_length: 0, + order_by: "district asc", + }), + auto: false, + onSuccess: (data) => { + districts.value = data || [] + // Drop the selected district if it no longer belongs to the governorate + if ( + customerData.value.custom_district && + !districts.value.some((d) => d.name === customerData.value.custom_district) + ) { + customerData.value.custom_district = "" + } + }, + onError: (err) => log.error("Error loading Districts", err), +}) + const posProfileResource = createResource({ url: "frappe.client.get_value", makeParams: () => ({ @@ -458,7 +545,17 @@ const loadDialogData = async () => { } // Load form options - await Promise.all([territoriesResource.reload(), customerGroupsResource.reload()]) + await Promise.all([ + territoriesResource.reload(), + customerGroupsResource.reload(), + governoratesResource.reload(), + ]) + if (isEditMode.value && props.customer?.name) { + await customerLocationResource.reload() + } + if (customerData.value.custom_governorate) { + await districtsResource.reload() + } checkPermissions() // Set country from POS Profile @@ -504,7 +601,10 @@ const resetForm = () => { territories.value, (list) => list.find((n) => n === "All Territories"), ), + custom_governorate: "", + custom_district: "", }) + districts.value = [] selectedCountryCode.value = "" phoneNumber.value = "" } @@ -531,6 +631,8 @@ watch( territories.value.find((n) => n === "All Territories") || territories.value[0] || "" + customerData.value.custom_governorate = customer.custom_governorate || "" + customerData.value.custom_district = customer.custom_district || "" // Handle mobile_no with country code if (customer.mobile_no) { customerData.value.mobile_no = customer.mobile_no @@ -564,6 +666,19 @@ watch(selectedCountryCode, async (newVal, oldVal) => { updateTerritoryFromCountry() }) + +watch( + () => customerData.value.custom_governorate, + (governorate) => { + if (governorate) { + districtsResource.reload() + } else { + districts.value = [] + customerData.value.custom_district = "" + } + } +) + watch(showCountryDropdown, async (isOpen) => { if (isOpen) { await nextTick() diff --git a/pos_next/api/customers.py b/pos_next/api/customers.py index 08f96d084..fbc664a59 100644 --- a/pos_next/api/customers.py +++ b/pos_next/api/customers.py @@ -81,6 +81,8 @@ def create_customer( territory=None, company=None, pos_profile=None, + custom_governorate=None, + custom_district=None, ): """ Create a new customer from POS. @@ -93,6 +95,8 @@ def create_customer( territory (str): Territory (default: from Selling Settings) company (str): Company (optional, used to auto-assign loyalty program) pos_profile (str): POS Profile (optional, preferred for context-aware loyalty assignment) + custom_governorate (str): Governorate (optional) + custom_district (str): District (optional, must belong to the governorate) Returns: dict: Created customer document @@ -135,6 +139,8 @@ def create_customer( "mobile_no": mobile_no or "", "email_id": email_id or "", "loyalty_program": loyalty_program, + "custom_governorate": custom_governorate or None, + "custom_district": custom_district or None, } ) diff --git a/pos_next/hooks.py b/pos_next/hooks.py index a08c97b63..e72e35b84 100644 --- a/pos_next/hooks.py +++ b/pos_next/hooks.py @@ -48,7 +48,7 @@ # page_js = {"page" : "public/js/file.js"} # include js in doctype views -# doctype_js = {"doctype" : "public/js/doctype.js"} +doctype_js = {"Customer": "public/js/customer.js"} # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} diff --git a/pos_next/pos_next/doctype/district/district.json b/pos_next/pos_next/doctype/district/district.json index 28e237f6d..de1f44f5b 100644 --- a/pos_next/pos_next/doctype/district/district.json +++ b/pos_next/pos_next/doctype/district/district.json @@ -1,6 +1,7 @@ { "actions": [], "allow_rename": 1, + "autoname": "format:{governorate}-{district}", "creation": "2026-06-09 10:55:40.584980", "doctype": "DocType", "engine": "InnoDB", @@ -36,10 +37,11 @@ "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2026-06-09 11:02:01.069029", + "modified": "2026-06-09 12:50:24.717788", "modified_by": "Administrator", "module": "POS Next", "name": "District", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { diff --git a/pos_next/pos_next/doctype/governorate/governorate.json b/pos_next/pos_next/doctype/governorate/governorate.json index 7a720c0c0..dc672cf68 100644 --- a/pos_next/pos_next/doctype/governorate/governorate.json +++ b/pos_next/pos_next/doctype/governorate/governorate.json @@ -1,6 +1,7 @@ { "actions": [], "allow_rename": 1, + "autoname": "field:governorate", "creation": "2026-06-09 10:54:57.009043", "doctype": "DocType", "engine": "InnoDB", @@ -11,18 +12,23 @@ { "fieldname": "governorate", "fieldtype": "Data", + "in_filter": 1, "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, "label": "Governorate", - "reqd": 1 + "reqd": 1, + "unique": 1 } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2026-06-09 10:56:00.364650", + "modified": "2026-06-09 12:45:46.769004", "modified_by": "Administrator", "module": "POS Next", "name": "Governorate", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -40,7 +46,10 @@ ], "row_format": "Dynamic", "rows_threshold_for_grid_search": 20, + "show_title_field_in_link": 1, "sort_field": "modified", "sort_order": "DESC", - "states": [] + "states": [], + "title_field": "governorate", + "translated_doctype": 1 } \ No newline at end of file diff --git a/pos_next/public/js/customer.js b/pos_next/public/js/customer.js new file mode 100644 index 000000000..38d37196f --- /dev/null +++ b/pos_next/public/js/customer.js @@ -0,0 +1,20 @@ +// Copyright (c) 2026, BrainWise and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Customer", { + refresh(frm) { + frm.set_query("custom_district", () => { + return { + filters: { + governorate: frm.doc.custom_governorate || "", + }, + } + }) + }, + + custom_governorate(frm) { + if (frm.doc.custom_district) { + frm.set_value("custom_district", null) + } + }, +}) diff --git a/pos_next/translations/ar.csv b/pos_next/translations/ar.csv index f89e6390a..c99f44df1 100644 --- a/pos_next/translations/ar.csv +++ b/pos_next/translations/ar.csv @@ -1559,4 +1559,9 @@ Points applied: {0}. Please pay remaining {1} with {2},تم خصم النقاط: "This offline receipt is no longer in browser storage. Sync the invoice, then print from history.","هذا الإيصال المحفوظ دون اتصال لم يعد موجودًا في تخزين المتصفح. قم بمزامنة الفاتورة أولًا، ثم اطبعها من السجل.","" "Invoice {0} saved offline and sent to printer — will sync when online","تم حفظ الفاتورة {0} دون اتصال وإرسالها إلى الطابعة — وستتم مزامنتها عند عودة الاتصال","" "Invoice {0} saved offline but print failed — open Print from the success dialog","تم حفظ الفاتورة {0} دون اتصال، لكن تعذّرت الطباعة — افتح خيار الطباعة من نافذة النجاح","" -"Popup blocked — check your browser settings.","تم حظر النافذة المنبثقة — يُرجى التحقق من إعدادات المتصفح.","" \ No newline at end of file +"Popup blocked — check your browser settings.","تم حظر النافذة المنبثقة — يُرجى التحقق من إعدادات المتصفح.","" +"Governorate","المحافظة","" +"Select Governorate","اختر المحافظة","" +"District","الحي","" +"Select District","اختر الحي","" +"Select a governorate first","اختر المحافظة أولاً",""