diff --git a/pos_next/company_isolation.py b/pos_next/company_isolation.py index 3473074c..8d3204b8 100644 --- a/pos_next/company_isolation.py +++ b/pos_next/company_isolation.py @@ -1,7 +1,14 @@ import frappe + def get_user_companies(user=None): - """Return company names the current user is allowed to access.""" + """Return company names the current user is allowed to access. + + Used by `validations.item_query` and any other code that wants to scope a + query to the user's companies. Empty list means "no company restriction + derivable for this user" — callers decide whether that should fall through + to stock permission handling or block the query. + """ user = user or frappe.session.user if user == "Administrator": @@ -30,84 +37,3 @@ def get_user_companies(user=None): companies.add(employee_company) return sorted(companies) - - -def _build_company_condition(doctype, user=None): - user = user or frappe.session.user - if user == "Administrator": - return "" - - companies = get_user_companies(user) - if not companies: - return "1=0" - - companies_sql = ", ".join(frappe.db.escape(company) for company in companies) - return f"`tab{doctype}`.`custom_company` IN ({companies_sql})" - - -def _has_company_permission(doc, user=None): - user = user or frappe.session.user - if user == "Administrator": - return True - - companies = set(get_user_companies(user)) - if not companies: - return False - - return doc.get("custom_company") in companies - - -def customer_permission_query_conditions(user): - return _build_company_condition("Customer", user) - - -def supplier_permission_query_conditions(user): - return _build_company_condition("Supplier", user) - - -def item_group_permission_query_conditions(user): - return _build_company_condition("Item Group", user) - - -def customer_group_permission_query_conditions(user): - return _build_company_condition("Customer Group", user) - - -def supplier_group_permission_query_conditions(user): - return _build_company_condition("Supplier Group", user) - - -def brand_permission_query_conditions(user): - return _build_company_condition("Brand", user) - - -def price_list_permission_query_conditions(user): - return _build_company_condition("Price List", user) - - -def customer_has_permission(doc, user=None, permission_type=None): - return _has_company_permission(doc, user) - - -def supplier_has_permission(doc, user=None, permission_type=None): - return _has_company_permission(doc, user) - - -def item_group_has_permission(doc, user=None, permission_type=None): - return _has_company_permission(doc, user) - - -def customer_group_has_permission(doc, user=None, permission_type=None): - return _has_company_permission(doc, user) - - -def supplier_group_has_permission(doc, user=None, permission_type=None): - return _has_company_permission(doc, user) - - -def brand_has_permission(doc, user=None, permission_type=None): - return _has_company_permission(doc, user) - - -def price_list_has_permission(doc, user=None, permission_type=None): - return _has_company_permission(doc, user) diff --git a/pos_next/hooks.py b/pos_next/hooks.py index 10095ba1..91c2804d 100644 --- a/pos_next/hooks.py +++ b/pos_next/hooks.py @@ -137,29 +137,6 @@ # notification_config = "pos_next.notifications.get_notification_config" # Permissions -# ----------- -# Permissions evaluated in scripted ways - -permission_query_conditions = { - "Customer": "pos_next.company_isolation.customer_permission_query_conditions", - "Supplier": "pos_next.company_isolation.supplier_permission_query_conditions", - "Item Group": "pos_next.company_isolation.item_group_permission_query_conditions", - "Customer Group": "pos_next.company_isolation.customer_group_permission_query_conditions", - "Supplier Group": "pos_next.company_isolation.supplier_group_permission_query_conditions", - "Brand": "pos_next.company_isolation.brand_permission_query_conditions", - "Price List": "pos_next.company_isolation.price_list_permission_query_conditions", -} - -has_permission = { - "Customer": "pos_next.company_isolation.customer_has_permission", - "Supplier": "pos_next.company_isolation.supplier_has_permission", - "Item Group": "pos_next.company_isolation.item_group_has_permission", - "Customer Group": "pos_next.company_isolation.customer_group_has_permission", - "Supplier Group": "pos_next.company_isolation.supplier_group_has_permission", - "Brand": "pos_next.company_isolation.brand_has_permission", - "Price List": "pos_next.company_isolation.price_list_has_permission", -} - # Standard Queries # ---------------- # Custom query for company-aware item filtering diff --git a/pos_next/pos_next/custom/brand.json b/pos_next/pos_next/custom/brand.json index 3a57e6ea..8cf62848 100644 --- a/pos_next/pos_next/custom/brand.json +++ b/pos_next/pos_next/custom/brand.json @@ -10,7 +10,7 @@ "module": "POS Next", "name": "Brand-custom_company", "options": "Company", - "reqd": 1 + "reqd": 0 } ], "custom_perms": [], diff --git a/pos_next/pos_next/custom/customer.json b/pos_next/pos_next/custom/customer.json index 31949149..77646095 100644 --- a/pos_next/pos_next/custom/customer.json +++ b/pos_next/pos_next/custom/customer.json @@ -10,7 +10,7 @@ "module": "POS Next", "name": "Customer-custom_company", "options": "Company", - "reqd": 1 + "reqd": 0 } ], "custom_perms": [], diff --git a/pos_next/pos_next/custom/customer_group.json b/pos_next/pos_next/custom/customer_group.json index d784ac5d..f8298e43 100644 --- a/pos_next/pos_next/custom/customer_group.json +++ b/pos_next/pos_next/custom/customer_group.json @@ -10,7 +10,7 @@ "module": "POS Next", "name": "Customer Group-custom_company", "options": "Company", - "reqd": 1 + "reqd": 0 } ], "custom_perms": [], diff --git a/pos_next/pos_next/custom/item.json b/pos_next/pos_next/custom/item.json index 7140d6fa..a583213c 100644 --- a/pos_next/pos_next/custom/item.json +++ b/pos_next/pos_next/custom/item.json @@ -56,7 +56,7 @@ "read_only": 0, "read_only_depends_on": null, "report_hide": 0, - "reqd": 1, + "reqd": 0, "search_index": 0, "show_dashboard": 0, "sort_options": 0, diff --git a/pos_next/pos_next/custom/item_group.json b/pos_next/pos_next/custom/item_group.json index d3a6c6c2..7a7850ab 100644 --- a/pos_next/pos_next/custom/item_group.json +++ b/pos_next/pos_next/custom/item_group.json @@ -10,7 +10,7 @@ "module": "POS Next", "name": "Item Group-custom_company", "options": "Company", - "reqd": 1 + "reqd": 0 } ], "custom_perms": [], diff --git a/pos_next/pos_next/custom/price_list.json b/pos_next/pos_next/custom/price_list.json index c1cc8fa6..d43aa3b8 100644 --- a/pos_next/pos_next/custom/price_list.json +++ b/pos_next/pos_next/custom/price_list.json @@ -10,7 +10,7 @@ "module": "POS Next", "name": "Price List-custom_company", "options": "Company", - "reqd": 1 + "reqd": 0 } ], "custom_perms": [], diff --git a/pos_next/pos_next/custom/supplier.json b/pos_next/pos_next/custom/supplier.json index a62d586c..6cf7f2f3 100644 --- a/pos_next/pos_next/custom/supplier.json +++ b/pos_next/pos_next/custom/supplier.json @@ -10,7 +10,7 @@ "module": "POS Next", "name": "Supplier-custom_company", "options": "Company", - "reqd": 1 + "reqd": 0 } ], "custom_perms": [], diff --git a/pos_next/pos_next/custom/supplier_group.json b/pos_next/pos_next/custom/supplier_group.json index 67b8eb78..35cb2465 100644 --- a/pos_next/pos_next/custom/supplier_group.json +++ b/pos_next/pos_next/custom/supplier_group.json @@ -10,7 +10,7 @@ "module": "POS Next", "name": "Supplier Group-custom_company", "options": "Company", - "reqd": 1 + "reqd": 0 } ], "custom_perms": [], diff --git a/pos_next/test_company_isolation.py b/pos_next/test_company_isolation.py deleted file mode 100644 index a13d31e9..00000000 --- a/pos_next/test_company_isolation.py +++ /dev/null @@ -1,101 +0,0 @@ -import json -import sys -import unittest -from pathlib import Path -from types import SimpleNamespace -from unittest.mock import patch - -try: - import frappe # noqa: F401 -except ModuleNotFoundError: - sys.modules["frappe"] = SimpleNamespace( - defaults=SimpleNamespace(get_user_default=lambda *args, **kwargs: None), - get_all=lambda *args, **kwargs: [], - db=SimpleNamespace( - get_value=lambda *args, **kwargs: None, - escape=lambda value: f"'{value}'", - ), - session=SimpleNamespace(user="Guest"), - ) - -from pos_next import company_isolation - - -class TestCompanyIsolation(unittest.TestCase): - def test_get_user_companies_combines_defaults_permissions_and_employee_company(self): - fake_frappe = SimpleNamespace( - defaults=SimpleNamespace(get_user_default=lambda *args, **kwargs: "Company A"), - get_all=lambda *args, **kwargs: ["Company B", "Company A"], - db=SimpleNamespace(get_value=lambda *args, **kwargs: "Company C"), - session=SimpleNamespace(user="demo@example.com"), - ) - - with patch.object(company_isolation, "frappe", fake_frappe): - companies = company_isolation.get_user_companies("demo@example.com") - - self.assertEqual(companies, ["Company A", "Company B", "Company C"]) - - def test_permission_query_conditions_restrict_by_custom_company(self): - fake_frappe = SimpleNamespace( - db=SimpleNamespace(escape=lambda value: f"'{value}'"), - session=SimpleNamespace(user="demo@example.com"), - ) - - with patch( - "pos_next.company_isolation.get_user_companies", - return_value=["Brainwise"], - ), patch.object(company_isolation, "frappe", fake_frappe): - condition = company_isolation.customer_permission_query_conditions( - "demo@example.com" - ) - - self.assertEqual(condition, "`tabCustomer`.`custom_company` IN ('Brainwise')") - - def test_permission_query_conditions_return_false_condition_when_no_company(self): - with patch( - "pos_next.company_isolation.get_user_companies", - return_value=[], - ): - condition = company_isolation.supplier_permission_query_conditions( - "demo@example.com" - ) - - self.assertEqual(condition, "1=0") - - def test_has_permission_checks_document_company(self): - doc = {"custom_company": "Company A"} - with patch( - "pos_next.company_isolation.get_user_companies", - return_value=["Company A"], - ): - self.assertTrue(company_isolation.brand_has_permission(doc, "demo@example.com")) - - with patch( - "pos_next.company_isolation.get_user_companies", - return_value=["Company B"], - ): - self.assertFalse(company_isolation.brand_has_permission(doc, "demo@example.com")) - - -class TestCustomCompanyFieldConfiguration(unittest.TestCase): - def test_custom_company_fields_are_required_without_default_values(self): - custom_dir = Path(__file__).resolve().parent / "pos_next" / "custom" - files_to_check = [ - "item.json", - "customer.json", - "supplier.json", - "item_group.json", - "customer_group.json", - "supplier_group.json", - "brand.json", - "price_list.json", - ] - - for file_name in files_to_check: - with self.subTest(file_name=file_name): - content = json.loads((custom_dir / file_name).read_text()) - field = next( - row for row in content.get("custom_fields", []) if row.get("fieldname") == "custom_company" - ) - self.assertEqual(field.get("reqd"), 1) - self.assertIn(field.get("default"), (None, "")) diff --git a/pos_next/validations.py b/pos_next/validations.py index 88398757..cab6b11c 100644 --- a/pos_next/validations.py +++ b/pos_next/validations.py @@ -40,17 +40,16 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): company = filters.get("company") if filters else None if company: - # Show only items for the selected company - conditions.append("custom_company = %s") + conditions.append("(custom_company = %s OR custom_company IS NULL OR custom_company = '')") values.append(company) else: user_companies = get_user_companies() if user_companies: placeholders = ", ".join(["%s"] * len(user_companies)) - conditions.append(f"custom_company IN ({placeholders})") + conditions.append( + f"(custom_company IN ({placeholders}) OR custom_company IS NULL OR custom_company = '')" + ) values.extend(user_companies) - else: - conditions.append("1 = 0") query = f""" SELECT name, item_name, item_group