diff --git a/POS/src/components/sale/CreateCustomerDialog.vue b/POS/src/components/sale/CreateCustomerDialog.vue
index 571b5f4bc..1d52e912d 100644
--- a/POS/src/components/sale/CreateCustomerDialog.vue
+++ b/POS/src/components/sale/CreateCustomerDialog.vue
@@ -165,6 +165,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
@@ -280,6 +315,9 @@ const countrySearchRef = ref(null);
const customerGroups = ref([]);
const territories = ref([]);
+const governorates = ref([]);
+const districts = ref([]);
+
const customerData = ref({
customer_name: "",
@@ -287,6 +325,8 @@ const customerData = ref({
email_id: "",
customer_group: "",
territory: "",
+ custom_governorate: "",
+ custom_district: "",
});
// =============================================================================
@@ -398,6 +438,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) => {
@@ -422,6 +464,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) => {
@@ -484,6 +528,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: () => ({
@@ -515,7 +603,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
@@ -559,10 +657,13 @@ const resetForm = () => {
territory: pickDefault(settings.territory, territories.value, (list) =>
list.find((n) => n === "All Territories")
),
+ custom_governorate: "",
+ custom_district: "",
});
+ districts.value = [];
selectedCountryCode.value = "";
phoneNumber.value = "";
-};
+}
// =============================================================================
// Watchers
@@ -585,8 +686,10 @@ watch(
customerData.value.territory =
customer.territory ||
territories.value.find((n) => n === "All Territories") ||
- territories.value[0] ||
- "";
+ 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;
@@ -620,6 +723,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 1f7b56511..635713e95 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
@@ -136,6 +140,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 0a0f26de7..d51ab641e 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/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..de1f44f5b
--- /dev/null
+++ b/pos_next/pos_next/doctype/district/district.json
@@ -0,0 +1,65 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "format:{governorate}-{district}",
+ "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 12:50:24.717788",
+ "modified_by": "Administrator",
+ "module": "POS Next",
+ "name": "District",
+ "naming_rule": "Expression (old style)",
+ "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..dc672cf68
--- /dev/null
+++ b/pos_next/pos_next/doctype/governorate/governorate.json
@@ -0,0 +1,55 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:governorate",
+ "creation": "2026-06-09 10:54:57.009043",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "governorate"
+ ],
+ "fields": [
+ {
+ "fieldname": "governorate",
+ "fieldtype": "Data",
+ "in_filter": 1,
+ "in_list_view": 1,
+ "in_preview": 1,
+ "in_standard_filter": 1,
+ "label": "Governorate",
+ "reqd": 1,
+ "unique": 1
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2026-06-09 12:45:46.769004",
+ "modified_by": "Administrator",
+ "module": "POS Next",
+ "name": "Governorate",
+ "naming_rule": "By fieldname",
+ "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,
+ "show_title_field_in_link": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "title_field": "governorate",
+ "translated_doctype": 1
+}
\ 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
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 ebd48f310..c99f44df1 100644
--- a/pos_next/translations/ar.csv
+++ b/pos_next/translations/ar.csv
@@ -1560,3 +1560,8 @@ Points applied: {0}. Please pay remaining {1} with {2},تم خصم النقاط:
"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.","تم حظر النافذة المنبثقة — يُرجى التحقق من إعدادات المتصفح.",""
+"Governorate","المحافظة",""
+"Select Governorate","اختر المحافظة",""
+"District","الحي",""
+"Select District","اختر الحي",""
+"Select a governorate first","اختر المحافظة أولاً",""