Suggestion: V14/V15 Workspace override via hooks should be added!

Hi all,
I am trying to override Payroll - Salary_payout json but even after copying to my custom app under Workspaces it does not load the changes…
I wonder if was possible to have something under Hooks.py like override_module_workspace = { ‘Payroll’: ‘custom_app.module_override.salary_payout.json’}

This way changes would be loaded.

:slight_smile:

Hi all,
Until Frappe team adds or finds a solution for extending current workspace i have come with the following work solution …

First you need to add under hooks.py override_module_workspace = { ‘Payroll’: 'customapp.workspace_overrides.salary_payout.salary_payout.json"}

After copy original salary_payout.json to you customapp/workspace_overrides
Inside your copied salary_payout paste from your Custom app the Cards you want to add or just create on the file…
My case i need to add Custom Reports - Report1, Report 2, Report 3

Also if you copied from your Custom app the list of reports from the context copy your and append to the salary_payout …
Mine custom report content had {"id":"MzU3sonXYo","type":"card","data":{"card_name":"Recibos de Sal\u00e1rio","col":4}

On the Override file i just added as the last record of the Content
“[{"id":"jvTZ8RvO42","type":"header","data":{"text":"<span class=\"h4\">Your Shortcuts","col":12}},{"id":"hNVIisuaFR","type":"shortcut","data":{"shortcut_name":"Salary Slip","col":3}},{"id":"6XUIWHJ3jI","type":"shortcut","data":{"shortcut_name":"Payroll Entry","col":3}},{"id":"yjIBi3GMoo","type":"shortcut","data":{"shortcut_name":"Salary Register","col":3}},{"id":"ORKhwX-uqw","type":"spacer","data":{"col":12}},{"id":"gGURwviUAZ","type":"header","data":{"text":"<span class=\"h4\">Transactions & Reports","col":12}},{"id":"m7ibJXxzpl","type":"card","data":{"card_name":"Masters","col":4}},{"id":"U-jv2v4nCv","type":"card","data":{"card_name":"Payroll","col":4}},{"id":"LG69O3ku4y","type":"card","data":{"card_name":"Incentives","col":4}},{"id":"kOuItimoNm","type":"card","data":{"card_name":"Accounting","col":4}},{"id":"UJqBhPqNZd","type":"card","data":{"card_name":"Accounting Reports","col":4}},{"id":"eNZuk6i-jy","type":"card","data":{"card_name":"Payroll Reports","col":4}},{"id":"ll91Zs2cbx","type":"card","data":{"card_name":"Deduction Reports","col":4}},{"id":"Rtbe3KmbBf","type":"card","data":{"card_name":"Relat\u00f3rios RH","col":4}},{"id":"MzU3sonXYo","type":"card","data":{"card_name":"Recibos de Sal\u00e1rio","col":4}}]”

Now that the files are ready… lets hack desktop.py
Create your customdesktop.py under Customapp.overrides and paste this…

from functools import wraps
from json import dumps, loads

import frappe
from frappe import DoesNotExistError, ValidationError, _, _dict
from frappe.boot import get_allowed_pages, get_allowed_reports
from frappe.cache_manager import (
build_domain_restriced_doctype_cache,
build_domain_restriced_page_cache,
build_table_count_cache,
)
from frappe.core.doctype.custom_role.custom_role import get_custom_allowed_roles

from frappe.desk.desktop import Workspace

@frappe.whitelist()
@frappe.read_only()
def get_desktop_page(page):
“”"Applies permissions, customizations and returns the configruration for a page
on desk.

Args:
        page (json): page data

Returns:
        dict: dictionary of cards, charts and shortcuts to be displayed on website
"""
try:
	#HELKYDS 10-02-2024
	workspace_overrides = frappe.get_hooks("override_module_workspace")
	if workspace_overrides and workspace_overrides.get(loads(page)['parent_page']):
		import_file = workspace_overrides[loads(page)['parent_page']][-1]
		module_path, classname, filetype = import_file.rsplit(".", 2)
		module = frappe.get_module(module_path)
		module_name =  import_file.split(".", 1)[0]
		if not filetype == "json":
			raise ImportError(f"{loads(page)['parent_page']}: {import_file} is not a JSON file")
		if module:
			tmp_mpath, tmp = frappe.modules.get_module_path(module_name).rsplit("/",1)
			mpath = tmp_mpath + import_file.replace(module_name,'').replace('.json','').replace('.','/') + ".json"
			with open(mpath) as ws_file:
				workspace_contents = ws_file.read()
				parsed_json = loads(workspace_contents)
#FIX 13-02-2024; To avoid load on other pages just bcs parent is the same
if parsed_json[‘name’] == loads(page)[‘name’]:
				#Replace page
				page = workspace_contents

			#TODO:Needs to rewrite the content based on the Card break from the JSON
				
			for pp,valor in enumerate(parsed_json['links']):
				existe_workspacelink = False
				for d in frappe.get_all("Workspace Link", fields=["*"], filters={"parent": parsed_json['name']}):
					if d.label == valor['label']:
						existe_workspacelink = True
						break
				if not existe_workspacelink:
					print ('Adds to...... WORKSPACE')
					wwlink = frappe.get_doc('Workspace',parsed_json['name'])
					links = wwlink.get("links")
					if valor['type'] == "Card Break":
						wwlink.append(
							"links",
							{
								"label": valor['label'],
								"type": valor['type'],
								"link_count": valor['link_count'],
								"onboard": valor['onboard'],
								"hidden": valor['hidden'],
								"is_query_report": valor['is_query_report'],
								"idx": 1 if not wwlink.links else wwlink.links[-1].idx + 1,
							},
						)
					else:
						wwlink.append(
							"links",
							{
								"label": valor['label'],
								"type": valor['type'],
								"link_type": valor['link_type'],
								"link_to": valor['link_to'],
								"link_count": valor['link_count'],
								"onboard": valor['onboard'],
								"hidden": valor['hidden'],
								"is_query_report": valor['is_query_report'],
								"idx": wwlink.links[-1].idx + 1,
							},
						)
					wwlink.save(ignore_permissions=True)
					frappe.db.commit()

		workspace = Workspace(loads(page))
		workspace.build_workspace()
		return {
			"charts": workspace.charts,
			"shortcuts": workspace.shortcuts,
			"cards": workspace.cards,
			"onboardings": workspace.onboardings,
			"quick_lists": workspace.quick_lists,
			"number_cards": workspace.number_cards,
			"custom_blocks": workspace.custom_blocks,
		}

	else:
		workspace = Workspace(loads(page))
		workspace.build_workspace()
		return {
			"charts": workspace.charts,
			"shortcuts": workspace.shortcuts,
			"cards": workspace.cards,
			"onboardings": workspace.onboardings,
			"quick_lists": workspace.quick_lists,
			"number_cards": workspace.number_cards,
			"custom_blocks": workspace.custom_blocks,
		}
except DoesNotExistError:
	frappe.log_error("Workspace Missing")
	return {}

@frappe.whitelist()
def get_workspace_sidebar_items():
“”“Get list of sidebar items for desk”“”
has_access = “Workspace Manager” in frappe.get_roles()

# don't get domain restricted pages
blocked_modules = frappe.get_doc("User", frappe.session.user).get_blocked_modules()
blocked_modules.append("Dummy Module")

# adding None to allowed_domains to include pages without domain restriction
allowed_domains = [None] + frappe.get_active_domains()

filters = {
	"restrict_to_domain": ["in", allowed_domains],
	"module": ["not in", blocked_modules],
}

if has_access:
	filters = []

# pages sorted based on sequence id
order_by = "sequence_id asc"
fields = [
	"name",
	"title",
	"for_user",
	"parent_page",
	"content",
	"public",
	"module",
	"icon",
	"is_hidden",
]
all_pages = frappe.get_all(
	"Workspace", fields=fields, filters=filters, order_by=order_by, ignore_permissions=True
)

#HELKYDS 12-02-2024; To load CUSTOM workspace...
for page in all_pages:
	workspace_overrides = frappe.get_hooks("override_module_workspace")
	if workspace_overrides and workspace_overrides.get(page.parent_page):
		import_file = workspace_overrides[page.parent_page][-1]
		module_path, classname, filetype = import_file.rsplit(".", 2)
		module = frappe.get_module(module_path)
		module_name =  import_file.split(".", 1)[0]
		if not filetype == "json":
			raise ImportError(f"{page.parent_page}: {import_file} is not a JSON file")
		if module:
			tmp_mpath, tmp = frappe.modules.get_module_path(module_name).rsplit("/",1)
			mpath = tmp_mpath + import_file.replace(module_name,'').replace('.json','').replace('.','/') + ".json"
			with open(mpath) as ws_file:
				workspace_contents = ws_file.read()
				parsed_json = loads(workspace_contents)
#FIX 13-02-2024; To avoid load on other pages just bcs parent is the same
if parsed_json[‘name’] == page.name:
				print ('UPDATE CONTENT....')
				page.content = parsed_json['content']

pages = []
private_pages = []

# Filter Page based on Permission
for page in all_pages:
	try:
		workspace = Workspace(page, True)
		if has_access or workspace.is_permitted():
			if page.public and (has_access or not page.is_hidden) and page.title != "Welcome Workspace":
				pages.append(page)
			elif page.for_user == frappe.session.user:
				private_pages.append(page)
			page["label"] = _(page.get("name"))
	except frappe.PermissionError:
		pass
if private_pages:
	pages.extend(private_pages)

if len(pages) == 0:
	pages = [frappe.get_doc("Workspace", "Welcome Workspace").as_dict()]
	pages[0]["label"] = _("Welcome Workspace")

return {"pages": pages, "has_access": has_access}

Don’t forget to under hooks add this

override_whitelisted_methods = {
“frappe.desk.desktop.get_desktop_page”: “customapp.overrides.customdesktop.get_desktop_page”,
“frappe.desk.desktop.get_workspace_sidebar_items”: “customapp.overrides.customdesktop.get_workspace_sidebar_items”
}

So basically it will load your Cards …
Few more tweaks might be needed but for my needs now it is ok.

Good luck :slight_smile:

@Helio_Jesus, Not working. Desk page shows blank

Hi,
Sure you copied the text and followed the steps?
Of course you still have to make some changes in your local Hooks.py

I really don’t see point of adding overrides to workspaces. If you don’t like default ones why not just disable them and create new ones? If you just want minor modifications then duplicate standard ones and modify them.

It’s that simple :sweat_smile:

1 Like

@ankush, I like the default workspace. But, if I want to add my custom report in the default workspace from my custom app, what should I do? If I change default workspace and add my custom reports link there, then JSON file changes in ERPNext app.

You can duplicate the existing workspace and modify, and you can disable the default one.