diff --git a/jey_theme/hooks.py b/jey_theme/hooks.py index c364057..cca47ec 100644 --- a/jey_theme/hooks.py +++ b/jey_theme/hooks.py @@ -28,6 +28,9 @@ has_permission = { "*": "jey_theme.access_control.has_permission", } +# Idempotent setup of the custom Taxes workspace (see setup/taxes_workspace.py). +after_migrate = ["jey_theme.setup.taxes_workspace.setup"] + # override_doctype_class = { # "Report": "jey_theme.custom_report.CustomReport" # } diff --git a/jey_theme/setup/__init__.py b/jey_theme/setup/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/jey_theme/setup/taxes_workspace.py b/jey_theme/setup/taxes_workspace.py new file mode 100644 index 0000000..c6fc195 --- /dev/null +++ b/jey_theme/setup/taxes_workspace.py @@ -0,0 +1,281 @@ +"""Idempotent setup of a 'Taxes Az' workspace and /desk icon. + +The doctype list is a STATIC snapshot baked into this file. Adding a new +DocType to the taxes_az app will NOT automatically put it into the workspace +— to include one, append it to the appropriate tuple below and re-run +`bench migrate` (or `bench execute jey_theme.setup.taxes_workspace.setup`). +""" + +import json + +import frappe + +WORKSPACE_NAME = "Taxes Az" +WORKSPACE_MODULE = "Taxes Az" +WORKSPACE_ICON = "landmark" +DESKTOP_ICON_LABEL = "Taxes Az" +# High idx pushes the icon to the very end of the /desk grid. +DESKTOP_ICON_IDX = 999 + +CARD_DECLARATIONS = "Declarations & Returns" +CARD_SETUP = "Setup & Reference" +CARD_TOOLS = "Period Closing & Tools" + +# Obsolete docs we created under the old "Taxes" name; cleaned up on setup +# (only if they're owned by jey_theme — we never touch erpnext's own docs). +OBSOLETE_NAMES = ("Taxes",) + +# --- Static doctype snapshot (taxes_az as of 2026-04-14) ----------------- +# To add/remove items, edit these tuples by hand. + +DECLARATIONS_DOCTYPES: tuple[str, ...] = ( + "Building construction and simplified tax declaration", + "Certificate on non-emergence of tax liability", + "Declaration of mining tax", + "Declaration of simplified tax", + "Declaration of simplified tax on cash withdrawals", + "Declaration of value added tax", + "Declaration of withholding tax on winnings", + "Excise declaration", + "Export declaration", + "Housing construction simplified tax declaration", + "Income tax return", + "Income tax return of a private notary", + "Land tax declaration", + "Property tax return", + "Quarterly report on unemployment insurance", + "Real estate simplified tax declaration", + "REFERENCE on controlled transactions", + "Report on collection of state duty", + "Road tax 2", + "Single declaration related to salaried and non-salaried work", + "Tax return withheld at source of payment", + "VAT allocation", + "VAT calculated on the amount paid to a non-resident", + "Withholding tax return of foreign subcontractors", +) + +SETUP_DOCTYPES: tuple[str, ...] = ( + "Agricultural Purpose Type", + "Business Classification", + "Classification code", + "EQM Codes", + "Main type of activity", + "Profit tax with special tax regime", + "Tax Article", + "Tax Article Items Report", +) + +TOOLS_DOCTYPES: tuple[str, ...] = ( + "Tax Period Closing Settings", + "Tax Period Closing Wizard", + "Tax Year Closing Wizard", +) + + +def _all_doctypes() -> list[str]: + return list(DECLARATIONS_DOCTYPES) + list(SETUP_DOCTYPES) + list(TOOLS_DOCTYPES) + + +def _link_row(label: str) -> dict: + return { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": label, + "link_count": 0, + "link_to": label, + "link_type": "DocType", + "onboard": 0, + "type": "Link", + } + + +def _card_break(label: str, count: int) -> dict: + return { + "hidden": 0, + "is_query_report": 0, + "label": label, + "link_count": count, + "onboard": 0, + "type": "Card Break", + } + + +def _build_links() -> list[dict]: + links: list[dict] = [] + for card_label, items in ( + (CARD_DECLARATIONS, DECLARATIONS_DOCTYPES), + (CARD_SETUP, SETUP_DOCTYPES), + (CARD_TOOLS, TOOLS_DOCTYPES), + ): + links.append(_card_break(card_label, len(items))) + for dt in items: + links.append(_link_row(dt)) + return links + + +def _build_content() -> str: + return json.dumps([ + {"id": "jeyTxH1", "type": "header", + "data": {"text": "Taxes (Azerbaijan)", "col": 12}}, + {"id": "jeyTxC1", "type": "card", + "data": {"card_name": CARD_DECLARATIONS, "col": 12}}, + {"id": "jeyTxC2", "type": "card", + "data": {"card_name": CARD_SETUP, "col": 6}}, + {"id": "jeyTxC3", "type": "card", + "data": {"card_name": CARD_TOOLS, "col": 6}}, + ]) + + +def _cleanup_obsolete() -> list[str]: + msgs = [] + for old in OBSOLETE_NAMES: + if frappe.db.exists("Workspace", old): + if frappe.db.get_value("Workspace", old, "app") == "jey_theme": + frappe.delete_doc("Workspace", old, ignore_permissions=True, force=1) + msgs.append(f"deleted obsolete Workspace {old!r}") + di_name = frappe.db.get_value("Desktop Icon", {"label": old, "app": "jey_theme"}) + if di_name: + frappe.delete_doc("Desktop Icon", di_name, ignore_permissions=True, force=1) + msgs.append(f"deleted obsolete Desktop Icon {old!r}") + return msgs + + +def ensure_workspace() -> str: + links = _build_links() + content = _build_content() + + if frappe.db.exists("Workspace", WORKSPACE_NAME): + ws = frappe.get_doc("Workspace", WORKSPACE_NAME) + ws.module = WORKSPACE_MODULE + ws.icon = WORKSPACE_ICON + ws.public = 1 + ws.type = "Workspace" + ws.content = content + ws.links = [] + for link in links: + ws.append("links", link) + ws.save(ignore_permissions=True) + return (f"updated {WORKSPACE_NAME} " + f"(decl={len(DECLARATIONS_DOCTYPES)} " + f"setup={len(SETUP_DOCTYPES)} " + f"tools={len(TOOLS_DOCTYPES)})") + + ws = frappe.get_doc({ + "doctype": "Workspace", + "name": WORKSPACE_NAME, + "label": WORKSPACE_NAME, + "title": WORKSPACE_NAME, + "module": WORKSPACE_MODULE, + "app": "jey_theme", + "type": "Workspace", + "icon": WORKSPACE_ICON, + "public": 1, + "is_hidden": 0, + "content": content, + "links": links, + }) + ws.insert(ignore_permissions=True) + return (f"created {WORKSPACE_NAME} " + f"(decl={len(DECLARATIONS_DOCTYPES)} " + f"setup={len(SETUP_DOCTYPES)} " + f"tools={len(TOOLS_DOCTYPES)})") + + +def _build_sidebar_items() -> list[dict]: + """First item is a Workspace link — get_route() in desktop.js picks the + first Link and, because its link_type is 'Workspace', routes to the + workspace page. The rest are the doctype links so the left sidebar in + /app/taxes-az shows every Taxes Az doctype.""" + items: list[dict] = [{ + "type": "Link", + "label": WORKSPACE_NAME, + "link_type": "Workspace", + "link_to": WORKSPACE_NAME, + "icon": WORKSPACE_ICON, + }] + for dt in _all_doctypes(): + items.append({ + "type": "Link", + "label": dt, + "link_type": "DocType", + "link_to": dt, + }) + return items + + +def ensure_workspace_sidebar() -> str: + """Manual Workspace Sidebar override so Desktop Icon's link_type + 'Workspace Sidebar' validates and get_route() routes to our workspace.""" + item_rows = _build_sidebar_items() + + if frappe.db.exists("Workspace Sidebar", WORKSPACE_NAME): + sb = frappe.get_doc("Workspace Sidebar", WORKSPACE_NAME) + sb.title = WORKSPACE_NAME + sb.module = WORKSPACE_MODULE + sb.header_icon = WORKSPACE_ICON + sb.app = "jey_theme" + sb.standard = 1 + sb.items = [] + for row in item_rows: + sb.append("items", row) + sb.save(ignore_permissions=True) + return f"updated Workspace Sidebar {WORKSPACE_NAME!r}" + + sb = frappe.get_doc({ + "doctype": "Workspace Sidebar", + "name": WORKSPACE_NAME, + "title": WORKSPACE_NAME, + "module": WORKSPACE_MODULE, + "header_icon": WORKSPACE_ICON, + "app": "jey_theme", + "standard": 1, + "items": item_rows, + }) + sb.insert(ignore_permissions=True) + return f"created Workspace Sidebar {WORKSPACE_NAME!r}" + + +def ensure_desktop_icon() -> str: + existing = frappe.db.exists("Desktop Icon", {"label": DESKTOP_ICON_LABEL}) + if existing: + icon = frappe.get_doc("Desktop Icon", existing) + icon.icon = WORKSPACE_ICON + icon.icon_type = "Link" + icon.link_type = "Workspace Sidebar" + icon.link_to = WORKSPACE_NAME + icon.link = None + icon.app = "jey_theme" + icon.hidden = 0 + icon.idx = DESKTOP_ICON_IDX + icon.save(ignore_permissions=True) + return f"updated Desktop Icon {DESKTOP_ICON_LABEL!r}" + + icon = frappe.get_doc({ + "doctype": "Desktop Icon", + "label": DESKTOP_ICON_LABEL, + "icon": WORKSPACE_ICON, + "icon_type": "Link", + "link_type": "Workspace Sidebar", + "link_to": WORKSPACE_NAME, + "standard": 1, + "app": "jey_theme", + "hidden": 0, + "idx": DESKTOP_ICON_IDX, + }) + icon.insert(ignore_permissions=True) + return f"created Desktop Icon {DESKTOP_ICON_LABEL!r}" + + +def setup(): + """Entry point — safe to call repeatedly (e.g. from after_migrate).""" + results: list[str] = [] + results.extend(_cleanup_obsolete()) + results.append(ensure_workspace()) + results.append(ensure_workspace_sidebar()) + results.append(ensure_desktop_icon()) + frappe.db.commit() + for r in results: + if r: + print(r)