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","اختر المحافظة أولاً",""