From f0652a2848562240fd7c50c7dc0ad56189062f32 Mon Sep 17 00:00:00 2001 From: roshmol Date: Mon, 1 Jun 2026 05:46:35 +0000 Subject: [PATCH 1/2] feat:update branch configuration --- .../branch_configuration.js | 57 +++++ .../branch_configuration.py | 241 ++++++++++++++++++ 2 files changed, 298 insertions(+) create mode 100644 sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.js create mode 100644 sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.py diff --git a/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.js b/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.js new file mode 100644 index 0000000..55e191c --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.js @@ -0,0 +1,57 @@ + +// Copyright (c) 2026, Enfono and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Branch Configuration", { + refresh(frm) { + set_child_filters(frm); + }, + company(frm) { + set_child_filters(frm); + + // Clear warehouse + cost center + account fields when company changes + // (they may belong to the old company) + if (frm.doc.warehouse && frm.doc.warehouse.length) { + frm.clear_table("warehouse"); + frm.refresh_field("warehouse"); + } + if (frm.doc.cost_center && frm.doc.cost_center.length) { + frm.clear_table("cost_center"); + frm.refresh_field("cost_center"); + } + // MoPs are global, not company-bound — no need to clear on company change. + } +}); + +function set_child_filters(frm) { + frm.set_query("warehouse", "warehouse", function () { + return { + filters: { + company: frm.doc.company, + is_group: 0 + } + }; + }); + + frm.set_query("cost_center", "cost_center", function () { + return { + filters: { + company: frm.doc.company, + is_group: 0 + } + }; + }); + + frm.set_query("mode_of_payment", "mode_of_payment", function () { + return { filters: { type: ["in", ["Cash", "Bank"]] } }; + }); + + // Filter roles shown in user child table + frm.set_query("role", "user", function () { + return { + filters: { + name: ["in", ["Branch User", "Sales Manager", "Damage User", "Stock User"]] + } + }; + }); +} \ No newline at end of file diff --git a/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.py b/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.py new file mode 100644 index 0000000..69c6708 --- /dev/null +++ b/sf_trading/sf_trading/doctype/branch_configuration/branch_configuration.py @@ -0,0 +1,241 @@ +# User permissions are created for company, warehouse and cost center. +# Company and first warehouse are set as is_default=1 so Frappe uses them as +# the user's Session Defaults instead of falling back to global defaults. +# The role assigned in the User child table is auto-assigned to the user. +# A Module Profile matching the role is also applied automatically. + +import frappe +from frappe.model.document import Document + +BRANCH_USER_ROLE = "Branch User" + + +class BranchConfiguration(Document): + + def validate(self): + # Ensure warehouse and cost center belong to the selected company + if self.company: + for w in self.warehouse: + if w.warehouse: + wh_company = frappe.db.get_value("Warehouse", w.warehouse, "company") + if wh_company and wh_company != self.company: + frappe.throw( + f"Warehouse {w.warehouse} belongs to company {wh_company}, " + f"not {self.company}. Please select a warehouse from the correct company." + ) + + for c in self.cost_center: + if c.cost_center: + cc_company = frappe.db.get_value("Cost Center", c.cost_center, "company") + if cc_company and cc_company != self.company: + frappe.throw( + f"Cost Center {c.cost_center} belongs to company {cc_company}, " + f"not {self.company}. Please select a cost center from the correct company." + ) + + def before_save(self): + if self.is_new(): + return + + old_doc = self.get_doc_before_save() + + old_users = {d.user for d in old_doc.user} + new_users = {d.user for d in self.user} + + removed_users = old_users - new_users + + for user in removed_users: + # Delete company permission + if old_doc.get("company"): + delete_permission(user, "Company", old_doc.company) + + for w in old_doc.warehouse: + delete_permission(user, "Warehouse", w.warehouse) + + for c in old_doc.cost_center: + delete_permission(user, "Cost Center", c.cost_center) + + # Remove role if user is not in any other Branch Configuration + old_role = None + for old_u in old_doc.user: + if old_u.user == user: + old_role = old_u.get("role") or BRANCH_USER_ROLE + break + _maybe_remove_role(user, old_role or BRANCH_USER_ROLE, exclude_branch=self.name) + + # Handle company change — remove old company permission for remaining users + old_company = old_doc.get("company") + new_company = self.get("company") + if old_company and old_company != new_company: + for u in self.user: + delete_permission(u.user, "Company", old_company) + + def on_update(self): + self.create_permissions() + + def create_permissions(self): + for u in self.user: + # Create company permission (marked as default so Session Defaults picks it) + if self.company: + create_permission(u.user, "Company", self.company, is_default=1) + + for idx, w in enumerate(self.warehouse): + # First warehouse is the default + create_permission(u.user, "Warehouse", w.warehouse, is_default=1 if idx == 0 else 0) + + for idx, c in enumerate(self.cost_center): + # First cost center is the default + create_permission(u.user, "Cost Center", c.cost_center, is_default=1 if idx == 0 else 0) + + # Also grant access to the company's default cost center (used in tax templates) + # without marking it as default — the branch cost center stays as the user's default + if self.company: + company_default_cc = frappe.db.get_value("Company", self.company, "cost_center") + if company_default_cc: + create_permission(u.user, "Cost Center", company_default_cc, is_default=0) + + # Auto-assign the selected role + selected_role = u.get("role") or BRANCH_USER_ROLE + _assign_role(u.user, selected_role) + + # Auto-set Module Profile based on role + _set_module_profile_for_role(u.user, selected_role) + + +def create_permission(user, allow, value, is_default=0): + if not value: + return + + existing = frappe.db.exists("User Permission", { + "user": user, + "allow": allow, + "for_value": value + }) + + if existing: + # Update is_default if needed — but only if no other default exists + if is_default and not _has_existing_default(user, allow, exclude_value=value): + frappe.db.set_value("User Permission", existing, "is_default", 1) + else: + # If we want to set default but one already exists, don't override it + if is_default and _has_existing_default(user, allow): + is_default = 0 + + doc = frappe.new_doc("User Permission") + doc.user = user + doc.allow = allow + doc.for_value = value + doc.is_default = is_default + doc.apply_to_all_doctypes = 1 + doc.insert(ignore_permissions=True) + + +def _has_existing_default(user, allow, exclude_value=None): + """Check if user already has a default User Permission for this allow type.""" + filters = { + "user": user, + "allow": allow, + "is_default": 1, + } + if exclude_value: + filters["for_value"] = ["!=", exclude_value] + + return frappe.db.exists("User Permission", filters) + + +def delete_permission(user, allow, value): + if not value: + return + + perms = frappe.get_all( + "User Permission", + filters={ + "user": user, + "allow": allow, + "for_value": value + }, + pluck="name" + ) + + for p in perms: + frappe.delete_doc("User Permission", p, ignore_permissions=True) + + +def _ensure_system_user(user): + """Website Users cannot hold desk roles. Upgrade to System User so role takes effect.""" + current_type = frappe.db.get_value("User", user, "user_type") + if current_type and current_type != "System User": + frappe.db.set_value("User", user, "user_type", "System User") + + +def _assign_role(user, role): + """Assign the specified role if not already assigned. Uses direct DB for reliability.""" + if not role or not frappe.db.exists("Role", role): + return + + # Desk roles require System User user_type; Website Users silently lose role assignments. + _ensure_system_user(user) + + # Check if role already assigned + if frappe.db.exists("Has Role", {"parent": user, "role": role}): + return + + # Direct insert — more reliable than user_doc.add_roles() which can fail + # during Branch Configuration save context + frappe.get_doc({ + "doctype": "Has Role", + "parent": user, + "parenttype": "User", + "parentfield": "roles", + "role": role, + }).insert(ignore_permissions=True) + + +def _set_module_profile_for_role(user, role): + """Set Module Profile based on assigned role.""" + role_profile_map = { + "Branch User": "Branch User", + "Damage User": "Damage User", + "Sales Manager": "Sales Manager", + "Stock User": "Stock User", + } + + profile_name = role_profile_map.get(role) + if not profile_name: + return + + if not frappe.db.exists("Module Profile", profile_name): + frappe.msgprint( + f"Module Profile {profile_name} does not exist. " + f"Please create it in Setup > Module Profile.", + indicator="orange", + alert=True + ) + return + + current = frappe.db.get_value("User", user, "module_profile") + if current != profile_name: + frappe.db.set_value("User", user, "module_profile", profile_name) + + +def _maybe_remove_role(user, role, exclude_branch=None): + """Remove role if user is not in any other Branch Configuration with the same role.""" + if not role: + return + + other_configs = frappe.get_all( + "Branch Configuration User", + filters={"user": user}, + fields=["parent", "role"], + ) + + # Check if user has this same role in any other Branch Configuration + has_role_elsewhere = any( + c.parent != exclude_branch and (c.get("role") or BRANCH_USER_ROLE) == role + for c in other_configs + ) + + if not has_role_elsewhere: + user_doc = frappe.get_doc("User", user) + if role in [r.role for r in user_doc.roles]: + user_doc.remove_roles(role) \ No newline at end of file From cb78faf75d9a3554b8dbe09f3c8871779f902664 Mon Sep 17 00:00:00 2001 From: roshmol Date: Mon, 1 Jun 2026 11:49:56 +0000 Subject: [PATCH 2/2] feat:update item order view --- sf_trading/fixtures/custom_field.json | 64 +++++++++++------ sf_trading/fixtures/property_setter.json | 29 ++++---- sf_trading/fixtures/report.json | 90 ++++++++++++++++++++++++ sf_trading/sf_trading/custom_search.py | 64 +++++++++++++++++ 4 files changed, 214 insertions(+), 33 deletions(-) create mode 100644 sf_trading/fixtures/report.json create mode 100644 sf_trading/sf_trading/custom_search.py diff --git a/sf_trading/fixtures/custom_field.json b/sf_trading/fixtures/custom_field.json index 54f4af6..cd8a7ff 100644 --- a/sf_trading/fixtures/custom_field.json +++ b/sf_trading/fixtures/custom_field.json @@ -7,32 +7,54 @@ "collapsible_depends_on": null, "columns": 0, "default": null, - "depends_on": "eval:doc.is_internal_customer && doc.represents_company", + "depends_on": null, "description": null, "docstatus": 0, "doctype": "Custom Field", - "dt": "Sales Invoice", + "dt": "Customer", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "inter_company_branch", - "fieldtype": "Link", + "fieldname": "custom_commercial_registration_number", + "fieldtype": "Data", "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": "represents_company", + "insert_after": "custom_vat_registration_number", "is_system_generated": 0, - "label": "Inter Company Branch", - "module": "Sf Trading", - "name": "Sales Invoice-inter_company_branch", + "is_virtual": 0, + "label": "Commercial Registration Number", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2026-01-21 22:02:44.991168", + "module": null, + "name": "Customer-custom_commercial_registration_number", "no_copy": 0, - "options": "Inter Company Branch", + "non_negative": 0, + "options": null, "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 + "unique": 0, + "width": null }, { "allow_in_quick_entry": 0, @@ -42,15 +64,15 @@ "collapsible_depends_on": null, "columns": 0, "default": null, - "depends_on": null, + "depends_on": "eval:doc.is_internal_customer && doc.represents_company", "description": null, "docstatus": 0, "doctype": "Custom Field", - "dt": "Customer", + "dt": "Sales Invoice", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_commercial_registration_number", - "fieldtype": "Data", + "fieldname": "inter_company_branch", + "fieldtype": "Link", "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -61,22 +83,22 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_vat_registration_number", + "insert_after": "represents_company", "is_system_generated": 0, "is_virtual": 0, - "label": "Commercial Registration Number", + "label": "Inter Company Branch", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2026-01-21 22:02:44.991168", - "module": null, - "name": "Customer-custom_commercial_registration_number", + "modified": "2026-05-04 17:38:13.257201", + "module": "Sf Trading", + "name": "Sales Invoice-inter_company_branch", "no_copy": 0, "non_negative": 0, - "options": null, + "options": "Inter Company Branch", "permlevel": 0, "placeholder": null, - "precision": "", + "precision": null, "print_hide": 0, "print_hide_if_no_value": 0, "print_width": null, diff --git a/sf_trading/fixtures/property_setter.json b/sf_trading/fixtures/property_setter.json index 5570cf0..62db3bc 100644 --- a/sf_trading/fixtures/property_setter.json +++ b/sf_trading/fixtures/property_setter.json @@ -1,13 +1,18 @@ [ - { - "doc_type": "Sales Invoice Item", - "doctype": "Property Setter", - "doctype_or_field": "DocField", - "field_name": "barcode", - "module": "Sf Trading", - "name": "Sales Invoice Item-barcode-in_list_view", - "property": "in_list_view", - "property_type": "Check", - "value": "1" - } -] + { + "default_value": null, + "doc_type": "Sales Invoice Item", + "docstatus": 0, + "doctype": "Property Setter", + "doctype_or_field": "DocField", + "field_name": "barcode", + "is_system_generated": 0, + "modified": "2026-05-04 17:38:15.245578", + "module": "Sf Trading", + "name": "Sales Invoice Item-barcode-in_list_view", + "property": "in_list_view", + "property_type": "Check", + "row_name": null, + "value": "1" + } +] \ No newline at end of file diff --git a/sf_trading/fixtures/report.json b/sf_trading/fixtures/report.json new file mode 100644 index 0000000..3774fc2 --- /dev/null +++ b/sf_trading/fixtures/report.json @@ -0,0 +1,90 @@ +[ + { + "add_total_row": 1, + "add_translate_data": 0, + "columns": [], + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "is_standard": "Yes", + "javascript": null, + "json": null, + "letter_head": null, + "modified": "2025-01-21 22:00:00", + "module": "Sf Trading", + "name": "DCR Detailed", + "prepared_report": 0, + "query": null, + "ref_doctype": "Sales Invoice", + "reference_report": null, + "report_name": "DCR Detailed", + "report_script": null, + "report_type": "Script Report", + "roles": [ + { + "parent": "DCR Detailed", + "parentfield": "roles", + "parenttype": "Report", + "role": "Accounts Manager" + }, + { + "parent": "DCR Detailed", + "parentfield": "roles", + "parenttype": "Report", + "role": "Accounts User" + }, + { + "parent": "DCR Detailed", + "parentfield": "roles", + "parenttype": "Report", + "role": "System Manager" + } + ], + "timeout": 0 + }, + { + "add_total_row": 0, + "add_translate_data": 0, + "columns": [], + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "is_standard": "Yes", + "javascript": null, + "json": null, + "letter_head": null, + "modified": "2026-02-11 23:38:16.427918", + "module": "Accounts", + "name": "DCR Report", + "prepared_report": 0, + "query": null, + "ref_doctype": "GL Entry", + "reference_report": null, + "report_name": "DCR Report", + "report_script": null, + "report_type": "Script Report", + "roles": [ + { + "parent": "DCR Report", + "parentfield": "roles", + "parenttype": "Report", + "role": "Accounts User" + }, + { + "parent": "DCR Report", + "parentfield": "roles", + "parenttype": "Report", + "role": "Accounts Manager" + }, + { + "parent": "DCR Report", + "parentfield": "roles", + "parenttype": "Report", + "role": "Auditor" + } + ], + "timeout": 0 + } +] \ No newline at end of file diff --git a/sf_trading/sf_trading/custom_search.py b/sf_trading/sf_trading/custom_search.py new file mode 100644 index 0000000..9a794c6 --- /dev/null +++ b/sf_trading/sf_trading/custom_search.py @@ -0,0 +1,64 @@ +import frappe +import re + +def natural_sort_key(item_name): + """Sort key that handles numeric parts correctly for size ordering.""" + parts = re.split(r'(\d+\.?\d*)', str(item_name)) + result = [] + for part in parts: + try: + result.append(float(part)) + except ValueError: + result.append(part.lower()) + return result + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def item_search_sorted(doctype, txt, searchfield, start, page_len, filters): + """Custom item search that sorts results by size (natural/numeric order).""" + + conditions = "" + if txt: + conditions = """ + AND ( + item.item_code LIKE %(txt)s + OR item.item_name LIKE %(txt)s + OR item.description LIKE %(txt)s + OR EXISTS ( + SELECT 1 FROM `tabItem Barcode` ib + WHERE ib.parent = item.item_code + AND ib.barcode LIKE %(txt)s + ) + ) + """ + + results = frappe.db.sql( + f""" + SELECT + item.item_code, + item.item_name, + item.item_group, + item.description, + item.stock_uom + FROM `tabItem` item + WHERE + item.disabled = 0 + AND item.has_variants = 0 + {conditions} + LIMIT %(page_len)s OFFSET %(start)s + """, + { + "txt": f"%{txt}%", + "start": start, + "page_len": page_len, + }, + as_dict=True + ) + + # Sort results by natural/numeric order of item_name + results.sort(key=lambda r: natural_sort_key(r.get("item_name", ""))) + + return [ + (r.item_code, r.item_name, r.item_group or "", r.description or "") + for r in results + ] \ No newline at end of file