taxes workspace: add Taxes Az subsystem via after_migrate setup

Creates a custom /desk subsystem "Taxes Az" listing user-facing doctypes
from the taxes_az module in three cards (Declarations & Returns, Setup &
Reference, Period Closing & Tools). The doctype list is a STATIC snapshot
baked into taxes_workspace.py — adding a new DocType to taxes_az will not
automatically put it into the workspace; edit the tuples and re-run setup.

- Uses "Taxes Az" name (not "Taxes") to avoid collision with erpnext's
  built-in Workspace Sidebar "Taxes", which otherwise hijacks routing to
  /app/sales-taxes-and-charges-template.
- Creates a matching Workspace Sidebar whose first Link is link_type=
  "Workspace" so Frappe's desktop.js get_route() generates a clean
  /app/taxes-az route and the click stays in-tab (standard behaviour).
- Desktop Icon with idx=999 pins it to the end of the /desk grid.
- Cleanup of the obsolete "Taxes" doc we created earlier is idempotent
  and scoped to jey_theme-owned records (never touches erpnext's).

Registered via after_migrate hook so every `bench migrate` re-asserts
the configuration. setup() is idempotent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-04-14 13:10:04 +00:00
parent 6c59231f43
commit 084c92466a
3 changed files with 284 additions and 0 deletions

View File

@ -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"
# }

View File

View File

@ -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": "<span class=\"h4\"><b>Taxes (Azerbaijan)</b></span>", "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)