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:
parent
6c59231f43
commit
084c92466a
|
|
@ -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"
|
||||
# }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
Loading…
Reference in New Issue