import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields MULTITENANT_DOCTYPES = [ "SB Organization", "SB Incident", "SB Idea", "SB Order", "SB Process", "SB Cycle", "SB KPI Input", "Managements", "Department", "Branch" ] def ensure_group_company_doctype(): """Ensure SB Group Company doctype exists.""" if frappe.db.exists("DocType", "SB Group Company"): return doc = frappe.get_doc({ "doctype": "DocType", "name": "SB Group Company", "module": "Sidrah Business", "custom": 1, "is_submittable": 0, "issingle": 0, "istable": 0, "quick_entry": 1, "track_changes": 1, "autoname": "field:company_name", "fields": [ { "fieldname": "company_name", "fieldtype": "Data", "label": "Company Name", "reqd": 1, "unique": 1, "in_list_view": 1, }, { "fieldname": "acronym", "fieldtype": "Data", "label": "Acronym", "in_list_view": 1, }, { "fieldname": "code", "fieldtype": "Data", "label": "Company Code", "unique": 1, }, { "fieldname": "status", "fieldtype": "Select", "label": "Status", "options": "Active\nInactive", "default": "Active", "in_list_view": 1, }, { "fieldname": "owner_user", "fieldtype": "Link", "label": "Group Co Owner / Admin", "options": "User", }, { "fieldname": "notes", "fieldtype": "Small Text", "label": "Notes", }, { "fieldname": "custom_is_deleted", "fieldtype": "Check", "label": "Is Deleted", "default": "0", "read_only": 1, "hidden": 1, }, { "fieldname": "custom_deleted_by", "fieldtype": "Link", "label": "Deleted By", "options": "User", "read_only": 1, "hidden": 1, }, { "fieldname": "custom_deleted_on", "fieldtype": "Datetime", "label": "Deleted On", "read_only": 1, "hidden": 1, }, { "fieldname": "custom_deleted_reason", "fieldtype": "Small Text", "label": "Deleted Reason", "read_only": 1, "hidden": 1, }, ], "permissions": [ { "role": "System Manager", "read": 1, "write": 1, "create": 1, "delete": 1 }, { "role": "Administrator", "read": 1, "write": 1, "create": 1, "delete": 1 } ] }) doc.insert(ignore_permissions=True) frappe.db.commit() DOCTYPES_INSERT_AFTER = { "SB Organization": "organization_name", "SB Incident": "organization", "SB Idea": "organization", "SB Order": "organization", "SB Process": "organization", "SB Cycle": "organization", "SB KPI Input": "organization", "Managements": "organization", "Department": "custom_organization", "Branch": "organization", } def sync_user_group_company_permission(user_doc, method=None): """ Synchronizes SB Group Company User Permissions for a user. Rule: - Skip Administrator, Guest, Platform Admin, and CEO (unrestricted across system). - For regular users (Auditors, Managers, Employees): Ensure User Permission exists with for_value = custom_group_company. Remove any permissions with a different for_value. - For Admins / CEOs: Remove any restricting SB Group Company user permissions so they have full access. """ user_name = user_doc.name if hasattr(user_doc, "name") else str(user_doc) if user_name in ("Administrator", "Guest"): return from sidrah_business.soft_delete import is_platform_admin is_admin = is_platform_admin(user_name) company = getattr(user_doc, "custom_group_company", None) if company is None and frappe.db.exists("User", user_name): company = frappe.db.get_value("User", user_name, "custom_group_company") # Fetch all existing User Permission records for allow = 'SB Group Company' for this user existing_perms = frappe.get_all( "User Permission", filters={"user": user_name, "allow": "SB Group Company"}, fields=["name", "for_value"] ) if is_admin: # Admins and CEOs should not have restricting user permissions for perm in existing_perms: frappe.delete_doc("User Permission", perm.name, ignore_permissions=True) return # Regular users (Auditors, Managers, Employees): create user permission for their company if company: has_correct = False for perm in existing_perms: if perm.for_value == company: has_correct = True else: frappe.delete_doc("User Permission", perm.name, ignore_permissions=True) if not has_correct: new_perm = frappe.get_doc({ "doctype": "User Permission", "user": user_name, "allow": "SB Group Company", "for_value": company, "is_default": 1, "apply_to_all_doctypes": 1, }) new_perm.insert(ignore_permissions=True) else: for perm in existing_perms: frappe.delete_doc("User Permission", perm.name, ignore_permissions=True) # Synchronize category to Frappe Has Role category = getattr(user_doc, "custom_category", None) if category is None and frappe.db.exists("User", user_name): category = frappe.db.get_value("User", user_name, "custom_category") if category: category_map = { 'Admin': ['SB Admin', 'System Manager'], 'Business Owner': ['SB Business Owner', 'Business Owner', 'SB Owner'], 'Manager': ['SB Manager'], 'Auditor': ['SB Auditor'], 'X-Auditor': ['SB X-Auditor', 'SB Auditor'], 'Employee': ['SB Employee'], 'CEO': ['Ceo', 'SB Admin', 'System Manager'], } target_roles = category_map.get(category, []) user_roles = frappe.get_roles(user_name) for r in target_roles: if not frappe.db.exists("Has Role", {"parent": user_name, "role": r}) and frappe.db.exists("Role", r): frappe.get_doc({ "doctype": "Has Role", "parent": user_name, "parenttype": "User", "parentfield": "roles", "role": r }).insert(ignore_permissions=True) # Auto pick Group Co Owner / Admin User if category in ('Admin', 'CEO', 'Business Owner') and company and frappe.db.exists("SB Group Company", company): gc_owner = frappe.db.get_value("SB Group Company", company, "owner_user") if not gc_owner: frappe.db.set_value("SB Group Company", company, "owner_user", user_name, update_modified=False) def sync_all_user_group_company_permissions(): """Batch-sync SB Group Company User Permissions for all users.""" users = frappe.get_all("User", filters={"name": ["not in", ["Administrator", "Guest"]]}, fields=["name", "custom_group_company"]) for u in users: sync_user_group_company_permission(u) frappe.db.commit() def setup_multitenancy_schema(): """Creates SB Group Company doctype and adds group_company custom fields to target doctypes.""" ensure_group_company_doctype() # Create default Group Company if none exists default_co = "Default Group Company" if not frappe.db.exists("SB Group Company", default_co): co_doc = frappe.new_doc("SB Group Company") co_doc.company_name = default_co co_doc.acronym = "DGC" co_doc.code = "DEFAULT" co_doc.status = "Active" co_doc.insert(ignore_permissions=True) frappe.db.commit() custom_fields = {} for dt in MULTITENANT_DOCTYPES: if frappe.db.exists("DocType", dt): custom_fields[dt] = [{ "fieldname": "group_company", "label": "Group Company", "fieldtype": "Link", "options": "SB Group Company", "in_list_view": 0, "in_standard_filter": 1, "hidden": 1, "insert_after": DOCTYPES_INSERT_AFTER.get(dt, "organization"), }] # Add custom_group_company and custom_manager fields on User doctype custom_fields["User"] = [ { "fieldname": "custom_group_company", "label": "Group Company", "fieldtype": "Link", "options": "SB Group Company", "in_list_view": 1, "in_standard_filter": 1, "insert_after": "custom_category", }, { "fieldname": "custom_manager", "label": "Reports To / Manager", "fieldtype": "Link", "options": "User", "in_list_view": 1, "in_standard_filter": 1, "insert_after": "custom_group_company", } ] create_custom_fields(custom_fields, ignore_validate=True) # Ensure existing Custom Field records have hidden=1 and right insert_after for dt, insert_after in DOCTYPES_INSERT_AFTER.items(): cf_name = frappe.db.get_value("Custom Field", {"dt": dt, "fieldname": "group_company"}) if cf_name: frappe.db.set_value("Custom Field", cf_name, { "insert_after": insert_after, "hidden": 1, "in_list_view": 0, }, update_modified=False) frappe.clear_cache(doctype=dt) user_cf = frappe.db.get_value("Custom Field", {"dt": "User", "fieldname": "custom_group_company"}) if user_cf: frappe.db.set_value("Custom Field", user_cf, "insert_after", "custom_category", update_modified=False) user_mgr_cf = frappe.db.get_value("Custom Field", {"dt": "User", "fieldname": "custom_manager"}) if user_mgr_cf: frappe.db.set_value("Custom Field", user_mgr_cf, "insert_after", "custom_group_company", update_modified=False) cat_cf = frappe.db.get_value("Custom Field", {"dt": "User", "fieldname": "custom_category"}) if cat_cf: frappe.db.set_value("Custom Field", cat_cf, "options", "\nAdmin\nBusiness Owner\nCEO\nChairman\nAuditor\nX-Auditor\nManager\nEmployee", update_modified=False) frappe.clear_cache(doctype="User") # Sync Group Company owner_users as SB Admin companies = frappe.get_all("SB Group Company", filters={"owner_user": ["is", "set"], "custom_is_deleted": 0}, fields=["name", "owner_user"]) for comp in companies: if comp.owner_user and frappe.db.exists("User", comp.owner_user): owner_doc = frappe.get_doc("User", comp.owner_user) roles = [r.role for r in owner_doc.roles] if "SB Admin" not in roles: owner_doc.add_roles("SB Admin") if not owner_doc.custom_group_company: frappe.db.set_value("User", comp.owner_user, "custom_group_company", comp.name, update_modified=False) # Backfill existing records to default Group Company if empty orgs = frappe.db.get_all("SB Organization", filters={"group_company": ["is", "not set"]}) for o in orgs: frappe.db.set_value("SB Organization", o.name, "group_company", default_co, update_modified=False) users = frappe.db.get_all("User", filters={"custom_group_company": ["is", "not set"]}) for u in users: if u.name != "Administrator": frappe.db.set_value("User", u.name, "custom_group_company", default_co, update_modified=False) # Backfill documents from organization's group_company for dt in ["SB Incident", "SB Idea", "SB Order", "SB Process", "SB Cycle", "SB KPI Input", "Managements", "Department", "Branch"]: if frappe.db.exists("DocType", dt): docs = frappe.db.get_all(dt, filters={"group_company": ["is", "not set"]}) for d in docs: org = None if frappe.db.has_column(dt, "organization"): org = frappe.db.get_value(dt, d.name, "organization") elif frappe.db.has_column(dt, "custom_organization"): org = frappe.db.get_value(dt, d.name, "custom_organization") co = frappe.db.get_value("SB Organization", org, "group_company") if org else default_co frappe.db.set_value(dt, d.name, "group_company", co or default_co, update_modified=False) # Synchronize User Permissions for all non-default group companies sync_all_user_group_company_permissions() frappe.db.commit() print("Multi-tenancy schema, insert_after positioning, & user permissions sync completed successfully.")