diff --git a/AGENTS.md b/AGENTS.md index 253e497..86e85c2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Service Factory — ERPNext Custom App Project -> **Current Version: V0.1.0002** — Each completed prompt/step increments the patch number by 1 (V0.1.0000 → V0.1.0001 → V0.1.0002…). The agent automatically commits and pushes after every step. +> **Current Version: V0.1.0003** — Each completed prompt/step increments the patch number by 1 (V0.1.0000 → V0.1.0001 → V0.1.0002…). The agent automatically commits and pushes after every step. ## Project Overview diff --git a/service_factory/.editorconfig b/service_factory/.editorconfig new file mode 100644 index 0000000..73816f1 --- /dev/null +++ b/service_factory/.editorconfig @@ -0,0 +1,21 @@ +# Root editor config file +root = true + +# Common settings +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +# python, js indentation settings +[{*.py,*.js,*.vue,*.css,*.scss,*.html}] +indent_style = tab +indent_size = 4 +max_line_length = 99 + +# JSON files - mostly doctype schema files +[{*.json}] +insert_final_newline = false +indent_style = space +indent_size = 1 diff --git a/service_factory/.eslintrc b/service_factory/.eslintrc new file mode 100644 index 0000000..523328a --- /dev/null +++ b/service_factory/.eslintrc @@ -0,0 +1,124 @@ +{ + "env": { + "browser": true, + "node": true, + "es2022": true + }, + "parserOptions": { + "sourceType": "module" + }, + "extends": "eslint:recommended", + "rules": { + "indent": "off", + "brace-style": "off", + "no-mixed-spaces-and-tabs": "off", + "no-useless-escape": "off", + "space-unary-ops": ["error", { "words": true }], + "linebreak-style": "off", + "quotes": ["off"], + "semi": "off", + "camelcase": "off", + "no-unused-vars": "off", + "no-console": ["warn"], + "no-extra-boolean-cast": ["off"], + "no-control-regex": ["off"], + }, + "root": true, + "globals": { + "frappe": true, + "Vue": true, + "SetVueGlobals": true, + "__": true, + "repl": true, + "Class": true, + "locals": true, + "cint": true, + "cstr": true, + "cur_frm": true, + "cur_dialog": true, + "cur_page": true, + "cur_list": true, + "cur_tree": true, + "msg_dialog": true, + "is_null": true, + "in_list": true, + "has_common": true, + "has_words": true, + "validate_email": true, + "open_web_template_values_editor": true, + "validate_name": true, + "validate_phone": true, + "validate_url": true, + "get_number_format": true, + "format_number": true, + "format_currency": true, + "comment_when": true, + "open_url_post": true, + "toTitle": true, + "lstrip": true, + "rstrip": true, + "strip": true, + "strip_html": true, + "replace_all": true, + "flt": true, + "precision": true, + "CREATE": true, + "AMEND": true, + "CANCEL": true, + "copy_dict": true, + "get_number_format_info": true, + "strip_number_groups": true, + "print_table": true, + "Layout": true, + "web_form_settings": true, + "$c": true, + "$a": true, + "$i": true, + "$bg": true, + "$y": true, + "$c_obj": true, + "refresh_many": true, + "refresh_field": true, + "toggle_field": true, + "get_field_obj": true, + "get_query_params": true, + "unhide_field": true, + "hide_field": true, + "set_field_options": true, + "getCookie": true, + "getCookies": true, + "get_url_arg": true, + "md5": true, + "$": true, + "jQuery": true, + "moment": true, + "hljs": true, + "Awesomplete": true, + "Sortable": true, + "gemoji": true, + "Showdown": true, + "Taggle": true, + "Gantt": true, + "Slick": true, + "Webcam": true, + "PhotoSwipe": true, + "PhotoSwipeUI_Default": true, + "io": true, + "JsBarcode": true, + "L": true, + "Chart": true, + "DataTable": true, + "Cypress": true, + "cy": true, + "it": true, + "describe": true, + "expect": true, + "context": true, + "before": true, + "beforeEach": true, + "after": true, + "qz": true, + "localforage": true, + "extend_cscript": true + } +} diff --git a/service_factory/.gitignore b/service_factory/.gitignore new file mode 100644 index 0000000..b50d332 --- /dev/null +++ b/service_factory/.gitignore @@ -0,0 +1,55 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +*.pyc +*.py~ + +# Distribution / packaging +.Python +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +tags +MANIFEST + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Dependency directories +node_modules/ +jspm_packages/ + +# IDEs and editors +.vscode/ +.vs/ +.idea/ +.kdev4/ +*.kdev4 +*.DS_Store +*.swp +*.comp.js +.wnf-lang-status +*debug.log + +# Helix Editor +.helix/ + +# Aider AI Chat +.aider* diff --git a/service_factory/.pre-commit-config.yaml b/service_factory/.pre-commit-config.yaml new file mode 100644 index 0000000..3b10c29 --- /dev/null +++ b/service_factory/.pre-commit-config.yaml @@ -0,0 +1,69 @@ +exclude: 'node_modules|.git' +default_stages: [pre-commit] +fail_fast: false + + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + files: "service_factory.*" + exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" + - id: check-merge-conflict + - id: check-ast + - id: check-json + - id: check-toml + - id: check-yaml + - id: debug-statements + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.10 + hooks: + - id: ruff + name: "Run ruff import sorter" + args: ["--select=I", "--fix"] + + - id: ruff + name: "Run ruff linter" + + - id: ruff-format + name: "Run ruff formatter" + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.7.1 + hooks: + - id: prettier + types_or: [javascript, vue, scss] + # Ignore any files that might contain jinja / bundles + exclude: | + (?x)^( + service_factory/public/dist/.*| + .*node_modules.*| + .*boilerplate.*| + service_factory/templates/includes/.*| + service_factory/public/js/lib/.* + )$ + + + - repo: https://github.com/pre-commit/mirrors-eslint + rev: v8.44.0 + hooks: + - id: eslint + types_or: [javascript] + args: ['--quiet'] + # Ignore any files that might contain jinja / bundles + exclude: | + (?x)^( + service_factory/public/dist/.*| + cypress/.*| + .*node_modules.*| + .*boilerplate.*| + service_factory/templates/includes/.*| + service_factory/public/js/lib/.* + )$ + +ci: + autoupdate_schedule: weekly + skip: [] + submodules: false diff --git a/service_factory/README.md b/service_factory/README.md new file mode 100644 index 0000000..806565e --- /dev/null +++ b/service_factory/README.md @@ -0,0 +1,33 @@ +### Service Factory + +Professional Services Delivery & Capacity Management + +### Installation + +You can install this app using the [bench](https://github.com/frappe/bench) CLI: + +```bash +cd $PATH_TO_YOUR_BENCH +bench get-app $URL_OF_THIS_REPO --branch version-16 +bench install-app service_factory +``` + +### Contributing + +This app uses `pre-commit` for code formatting and linting. Please [install pre-commit](https://pre-commit.com/#installation) and enable it for this repository: + +```bash +cd apps/service_factory +pre-commit install +``` + +Pre-commit is configured to use the following tools for checking and formatting your code: + +- ruff +- eslint +- prettier +- pyupgrade + +### License + +mit diff --git a/service_factory/deploy/README.md b/service_factory/deploy/README.md new file mode 100644 index 0000000..8778e93 --- /dev/null +++ b/service_factory/deploy/README.md @@ -0,0 +1,52 @@ +# Deployment Configuration + +## Systemd Services + +The Frappe Bench (ERPNext) is managed via systemd for automatic startup on boot. + +### Services + +| Service | Description | Port | +|---------|-------------|------| +| `frappe-bench-web.service` | Web server (gunicorn) | 8000 | +| `frappe-bench-worker.service` | Background async worker | - | +| `frappe-bench-schedule.service` | Scheduled job runner | - | +| `frappe-bench-socketio.service` | Realtime WebSocket server | 9000 | +| `frappe-bench-watch.service` | File watcher (dev/assets) | - | + +### Management + +```bash +# View status of all services +systemctl status frappe-bench-web.service + +# Start/Stop all services +systemctl start frappe-bench.target +systemctl stop frappe-bench.target + +# View logs +journalctl -u frappe-bench-web.service -f +journalctl -u frappe-bench-worker.service -f +``` + +### Installation + +```bash +sudo cp deploy/frappe-bench-*.service deploy/frappe-bench.target /etc/systemd/system/ +sudo cp deploy/bench-env.sh /opt/erpnext/frappe-bench/bench-env.sh +sudo systemctl daemon-reload +sudo systemctl enable frappe-bench.target +sudo systemctl start frappe-bench.target +``` + +### Assets Symlink + +After installing the app, create the assets symlink for custom icons: + +```bash +sudo ln -sf /opt/erpnext/frappe-bench/apps/service_factory/service_factory/public /opt/erpnext/frappe-bench/sites/assets/service_factory +``` + +### Credentials + +Admin credentials are stored in `/root/erpnext-admin-credentials.txt`. diff --git a/service_factory/deploy/bench-env.sh b/service_factory/deploy/bench-env.sh new file mode 100755 index 0000000..e4d5698 --- /dev/null +++ b/service_factory/deploy/bench-env.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Environment wrapper for Frappe bench services +export PATH="/opt/erpnext/home/.local/share/pipx/venvs/frappe-bench/bin:/opt/erpnext/frappe-bench/env/bin:$PATH" +export HOME="/opt/erpnext/home" +cd /opt/erpnext/frappe-bench +exec "$@" diff --git a/service_factory/deploy/frappe-bench b/service_factory/deploy/frappe-bench new file mode 100644 index 0000000..1bf78ca --- /dev/null +++ b/service_factory/deploy/frappe-bench @@ -0,0 +1,42 @@ +upstream frappe-web { + server 127.0.0.1:8000; +} + +upstream frappe-socketio { + server 127.0.0.1:9000; +} + +server { + listen 8080; + server_name _; + + client_max_body_size 50M; + + # Route ALL requests containing /socket.io/ to the SocketIO server + # This handles both /socket.io/ and /erp.lohmar.co.uk/socket.io/ + location ~* /socket\.io/ { + rewrite ^(.*)/socket\.io/(.*)$ /socket.io/$2 break; + proxy_pass http://frappe-socketio; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 86400; + } + + # All other requests go to Frappe web + location / { + proxy_pass http://frappe-web; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_read_timeout 300; + proxy_redirect off; + } +} diff --git a/service_factory/deploy/frappe-bench-schedule.service b/service_factory/deploy/frappe-bench-schedule.service new file mode 100644 index 0000000..8ee645c --- /dev/null +++ b/service_factory/deploy/frappe-bench-schedule.service @@ -0,0 +1,19 @@ +[Unit] +Description=Frappe Bench Scheduler +Documentation=https://frappeframework.com +After=network.target redis-server.service +Requires=redis-server.service + +[Service] +Type=simple +User=erpnext +Group=erpnext +WorkingDirectory=/opt/erpnext/frappe-bench +ExecStart=/opt/erpnext/frappe-bench/bench-env.sh bench schedule +Restart=always +RestartSec=10 +StandardOutput=append:/opt/erpnext/frappe-bench/logs/schedule.log +StandardError=append:/opt/erpnext/frappe-bench/logs/schedule.error.log + +[Install] +WantedBy=multi-user.target diff --git a/service_factory/deploy/frappe-bench-socketio.service b/service_factory/deploy/frappe-bench-socketio.service new file mode 100644 index 0000000..2888d5b --- /dev/null +++ b/service_factory/deploy/frappe-bench-socketio.service @@ -0,0 +1,19 @@ +[Unit] +Description=Frappe Bench SocketIO (Realtime) +Documentation=https://frappeframework.com +After=network.target redis-server.service +Requires=redis-server.service + +[Service] +Type=simple +User=erpnext +Group=erpnext +WorkingDirectory=/opt/erpnext/frappe-bench +ExecStart=/opt/erpnext/frappe-bench/bench-env.sh bench socketio +Restart=always +RestartSec=10 +StandardOutput=append:/opt/erpnext/frappe-bench/logs/socketio.log +StandardError=append:/opt/erpnext/frappe-bench/logs/socketio.error.log + +[Install] +WantedBy=multi-user.target diff --git a/service_factory/deploy/frappe-bench-watch.service b/service_factory/deploy/frappe-bench-watch.service new file mode 100644 index 0000000..12ff4da --- /dev/null +++ b/service_factory/deploy/frappe-bench-watch.service @@ -0,0 +1,19 @@ +[Unit] +Description=Frappe Bench File Watcher (Development) +Documentation=https://frappeframework.com +After=network.target +PartOf=frappe-bench-web.service + +[Service] +Type=simple +User=erpnext +Group=erpnext +WorkingDirectory=/opt/erpnext/frappe-bench +ExecStart=/opt/erpnext/frappe-bench/bench-env.sh bench watch +Restart=always +RestartSec=10 +StandardOutput=append:/opt/erpnext/frappe-bench/logs/watch.log +StandardError=append:/opt/erpnext/frappe-bench/logs/watch.error.log + +[Install] +WantedBy=multi-user.target diff --git a/service_factory/deploy/frappe-bench-web.service b/service_factory/deploy/frappe-bench-web.service new file mode 100644 index 0000000..2ed9db8 --- /dev/null +++ b/service_factory/deploy/frappe-bench-web.service @@ -0,0 +1,19 @@ +[Unit] +Description=Frappe Bench Web Server (ERPNext) +Documentation=https://frappeframework.com +After=network.target redis-server.service +Requires=redis-server.service + +[Service] +Type=simple +User=erpnext +Group=erpnext +WorkingDirectory=/opt/erpnext/frappe-bench +ExecStart=/opt/erpnext/frappe-bench/bench-env.sh bench serve --port 8000 --proxy +Restart=always +RestartSec=10 +StandardOutput=append:/opt/erpnext/frappe-bench/logs/web.log +StandardError=append:/opt/erpnext/frappe-bench/logs/web.error.log + +[Install] +WantedBy=multi-user.target diff --git a/service_factory/deploy/frappe-bench-worker.service b/service_factory/deploy/frappe-bench-worker.service new file mode 100644 index 0000000..cb96be6 --- /dev/null +++ b/service_factory/deploy/frappe-bench-worker.service @@ -0,0 +1,19 @@ +[Unit] +Description=Frappe Bench Background Worker +Documentation=https://frappeframework.com +After=network.target redis-server.service +Requires=redis-server.service + +[Service] +Type=simple +User=erpnext +Group=erpnext +WorkingDirectory=/opt/erpnext/frappe-bench +ExecStart=/opt/erpnext/frappe-bench/bench-env.sh bench worker +Restart=always +RestartSec=10 +StandardOutput=append:/opt/erpnext/frappe-bench/logs/worker.log +StandardError=append:/opt/erpnext/frappe-bench/logs/worker.error.log + +[Install] +WantedBy=multi-user.target diff --git a/service_factory/deploy/frappe-bench.target b/service_factory/deploy/frappe-bench.target new file mode 100644 index 0000000..ebc0c5e --- /dev/null +++ b/service_factory/deploy/frappe-bench.target @@ -0,0 +1,9 @@ +[Unit] +Description=Frappe Bench (ERPNext) - All Services +Documentation=https://frappeframework.com +Requires=redis-server.service +After=redis-server.service network.target +Wants=frappe-bench-web.service frappe-bench-worker.service frappe-bench-schedule.service frappe-bench-socketio.service frappe-bench-watch.service + +[Install] +WantedBy=multi-user.target diff --git a/service_factory/license.txt b/service_factory/license.txt new file mode 100644 index 0000000..8aa2645 --- /dev/null +++ b/service_factory/license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [year] [fullname] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/service_factory/pyproject.toml b/service_factory/pyproject.toml new file mode 100644 index 0000000..733c886 --- /dev/null +++ b/service_factory/pyproject.toml @@ -0,0 +1,65 @@ +[project] +name = "service_factory" +authors = [ + { name = "cclohmar", email = "claus.lohmar@lohmar.co.uk"} +] +description = "Professional Services Delivery & Capacity Management" +requires-python = ">=3.14" +readme = "README.md" +dynamic = ["version"] +dependencies = [ + # "frappe~=16.0.0" # Installed and managed by bench. +] + +[build-system] +requires = ["flit_core >=3.4,<4"] +build-backend = "flit_core.buildapi" + +# These dependencies are only installed when developer mode is enabled +[tool.bench.dev-dependencies] +# package_name = "~=1.1.0" + +# These apt dependencies will be installed from Ubuntu repositories when you host your app on Frappe Cloud +[deploy.dependencies.apt] +packages = [] + +[tool.ruff] +line-length = 110 +target-version = "py314" + +[tool.ruff.lint] +select = [ + "F", + "E", + "W", + "I", + "UP", + "B", + "RUF", +] +ignore = [ + "B017", # assertRaises(Exception) - should be more specific + "B018", # useless expression, not assigned to anything + "B023", # function doesn't bind loop variable - will have last iteration's value + "B904", # raise inside except without from + "E101", # indentation contains mixed spaces and tabs + "E402", # module level import not at top of file + "E501", # line too long + "E741", # ambiguous variable name + "F401", # "unused" imports + "F403", # can't detect undefined names from * import + "F405", # can't detect undefined names from * import + "F722", # syntax error in forward type annotation + "W191", # indentation contains tabs + "UP030", # Use implicit references for positional format fields (translations) + "UP031", # Use format specifiers instead of percent format + "UP032", # Use f-string instead of `format` call (translations) + "UP037", # quoted annotations + "UP040", # Use type aliases instead of type annotations +] +typing-modules = ["frappe.types.DF"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "tab" +docstring-code-format = true diff --git a/service_factory/service_factory/__init__.py b/service_factory/service_factory/__init__.py index e69de29..f102a9c 100644 --- a/service_factory/service_factory/__init__.py +++ b/service_factory/service_factory/__init__.py @@ -0,0 +1 @@ +__version__ = "0.0.1" diff --git a/service_factory/service_factory/doctype/engagement_order/__init__.py b/service_factory/service_factory/api/__init__.py similarity index 100% rename from service_factory/service_factory/doctype/engagement_order/__init__.py rename to service_factory/service_factory/api/__init__.py diff --git a/service_factory/service_factory/api/permission.py b/service_factory/service_factory/api/permission.py new file mode 100644 index 0000000..e6d320c --- /dev/null +++ b/service_factory/service_factory/api/permission.py @@ -0,0 +1,12 @@ +# Copyright (c) 2026, cclohmar and contributors +# For license information, please see license.txt + + +def has_app_permission(): + """Check if the current user has permission to see the Service Factory app icon.""" + import frappe + + if frappe.session.user == "Guest": + return False + + return True diff --git a/service_factory/service_factory/doctype/engagement_order_run_step/__init__.py b/service_factory/service_factory/config/__init__.py similarity index 100% rename from service_factory/service_factory/doctype/engagement_order_run_step/__init__.py rename to service_factory/service_factory/config/__init__.py diff --git a/service_factory/service_factory/doctype/engagement_order/engagement_order.json b/service_factory/service_factory/doctype/engagement_order/engagement_order.json deleted file mode 100644 index 3b51e13..0000000 --- a/service_factory/service_factory/doctype/engagement_order/engagement_order.json +++ /dev/null @@ -1,549 +0,0 @@ -{ - "actions": [], - "allow_auto_repeat": 0, - "allow_events_in_timeline": 1, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "naming_series:", - "creation": "2026-07-15 19:00:00.000000", - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "naming_series", - "customer", - "sales_order", - "engagement_recipe", - "column_break_1", - "status", - "start_date", - "end_date", - "section_break_totals", - "total_allocated_hours", - "total_logged_hours", - "column_break_totals_2", - "actual_cost", - "projected_billing_value", - "section_break_steps", - "run_steps" - ], - "fields": [ - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "naming_series", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Series", - "length": 0, - "no_copy": 0, - "options": "ENG-ORD-.YYYY.-", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "customer", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Customer", - "length": 0, - "no_copy": 0, - "options": "Customer", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "sales_order", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Sales Order", - "length": 0, - "no_copy": 0, - "options": "Sales Order", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "engagement_recipe", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Engagement Recipe", - "length": 0, - "no_copy": 0, - "options": "Engagement Recipe", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "status", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Status", - "length": 0, - "no_copy": 0, - "options": "Draft\nScheduled\nIn Progress\nOn Hold\nCompleted\nCancelled", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "start_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Start Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "end_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "End Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_totals", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Totals", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "total_allocated_hours", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Total Allocated Hours", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "1", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "total_logged_hours", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Total Logged Hours", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "1", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_totals_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "actual_cost", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Actual Cost", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "projected_billing_value", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Projected Billing Value", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_steps", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Engagement Steps", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "run_steps", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Run Steps", - "length": 0, - "no_copy": 0, - "options": "Engagement Order Run Step", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - } - ], - "has_field_mappings": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_field": "", - "import_source": "", - "is_submittable": 1, - "is_table": 0, - "label": "Engagement Order", - "modified": "2026-07-15 19:00:00.000000", - "modified_by": "Administrator", - "module": "Service Factory", - "name": "Engagement Order", - "name_case": "", - "nsm_parent_field": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "select": 1, - "set_user_permissions": 0, - "share": 1, - "submit": 1, - "write": 1 - } - ], - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "states": [], - "track_changes": 1, - "track_seen": 1, - "track_views": 0 -} diff --git a/service_factory/service_factory/doctype/engagement_order/engagement_order.py b/service_factory/service_factory/doctype/engagement_order/engagement_order.py deleted file mode 100644 index 101f6ba..0000000 --- a/service_factory/service_factory/doctype/engagement_order/engagement_order.py +++ /dev/null @@ -1,170 +0,0 @@ -# Copyright (c) 2026, cclohmar and contributors -# For license information, please see license.txt - -import frappe -from frappe.model.document import Document -from frappe import _ -from datetime import datetime, timedelta - - -class EngagementOrder(Document): - def validate(self): - """Validate before saving.""" - self.check_recipe_changed() - self.calculate_totals() - self.capacity_warning() - - def check_recipe_changed(self): - """Detect if engagement_recipe changed and re-populate steps.""" - if not self.engagement_recipe: - return - - # Check if this is a new doc or recipe changed - if self.is_new() or self.has_value_changed("engagement_recipe"): - self.load_recipe_steps() - - def load_recipe_steps(self): - """Fetch recipe steps into order run steps.""" - recipe = frappe.get_doc("Engagement Recipe", self.engagement_recipe) - self.set("run_steps", []) - - for step in recipe.steps: - row = self.append("run_steps", {}) - row.step_name = step.step_name - row.sequence_no = step.sequence_no - row.resource_center = step.resource_center - row.allocated_hours = step.estimated_hours - row.status = "Pending" - - def calculate_totals(self): - """Recalculate order-level totals from run steps.""" - total_allocated = 0.0 - total_logged = 0.0 - actual_cost = 0.0 - projected_billing = 0.0 - - if self.get("run_steps"): - for step in self.run_steps: - allocated = step.get("allocated_hours", 0) or 0 - logged = step.get("actual_hours", 0) or 0 - total_allocated += allocated - total_logged += logged - - if step.resource_center: - rc = frappe.get_cached_value( - "Resource Center", - step.resource_center, - ["hourly_cost_rate", "hourly_billing_rate"], - as_dict=True, - ) - if rc: - actual_cost += logged * (rc.get("hourly_cost_rate", 0) or 0) - projected_billing += logged * (rc.get("hourly_billing_rate", 0) or 0) - - self.total_allocated_hours = total_allocated - self.total_logged_hours = total_logged - self.actual_cost = actual_cost - self.projected_billing_value = projected_billing - - def capacity_warning(self): - """Warn if requested hours exceed Resource Center daily capacity.""" - if not self.get("run_steps") or not self.start_date: - return - - # Group steps by resource center and sum hours - rc_hours = {} - for step in self.run_steps: - if not step.resource_center: - continue - rc = step.resource_center - hours = step.get("allocated_hours", 0) or 0 - rc_hours[rc] = rc_hours.get(rc, 0) + hours - - for rc_name, requested_hours in rc_hours.items(): - rc_doc = frappe.get_cached_doc("Resource Center", rc_name) - daily_capacity = rc_doc.daily_capacity_hours or 0 - - if daily_capacity <= 0: - continue - - # Find overlapping Engagement Orders for this Resource Center - overlapping_hours = self.get_overlapping_hours(rc_name) - - total_daily_load = overlapping_hours + requested_hours - - if total_daily_load > daily_capacity: - frappe.msgprint( - _( - "Warning: Resource Center '{0}' has a daily capacity of {1} hours, " - "but the total load including this order would be {2} hours " - "({3}% utilization). Consider adjusting dates or hours." - ).format( - rc_name, - daily_capacity, - total_daily_load, - round((total_daily_load / daily_capacity) * 100, 0), - ), - indicator="orange", - alert=True, - ) - - def get_overlapping_hours(self, resource_center): - """Sum allocated hours from other active Engagement Orders - that overlap with this order's date range.""" - if not self.start_date: - return 0 - - start = self.start_date - # If no end date, estimate 1 day - end = self.end_date or start - - filters = { - "name": ["!=", self.name], - "docstatus": 1, # Submitted - "start_date": ["<=", end], - } - # Only add end_date filter if it's set on the order - if self.end_date: - filters["end_date"] = [">=", start] - else: - # Orders that started on or before our end date and might still be active - pass - - # Also check status - statuses = ["Scheduled", "In Progress"] - - orders = frappe.get_all( - "Engagement Order", - filters=filters, - pluck="name", - ) - - total_hours = 0 - for order_name in orders: - order_doc = frappe.get_cached_doc("Engagement Order", order_name) - for step in order_doc.get("run_steps", []): - if step.resource_center == resource_center: - total_hours += step.get("allocated_hours", 0) or 0 - - return total_hours - - def on_submit(self): - self.create_execution_cards() - - def create_execution_cards(self): - """Auto-generate Execution Cards for each run step - that has an assigned employee.""" - for step in self.get("run_steps", []): - if not step.assigned_employee: - continue - - card = frappe.get_doc({ - "doctype": "Execution Card", - "engagement_order": self.name, - "step_name": step.step_name, - "assigned_employee": step.assigned_employee, - "allocated_hours": step.allocated_hours or 0, - "status": "Open", - }) - card.insert() - step.db_set("status", "Active") diff --git a/service_factory/service_factory/doctype/engagement_order/test_engagement_order.py b/service_factory/service_factory/doctype/engagement_order/test_engagement_order.py deleted file mode 100644 index 2108dde..0000000 --- a/service_factory/service_factory/doctype/engagement_order/test_engagement_order.py +++ /dev/null @@ -1,222 +0,0 @@ -# Copyright (c) 2026, cclohmar and contributors -# For license information, please see license.txt - -import frappe -from frappe.tests.utils import FrappeTestCase -from frappe.utils import now_datetime - - -class TestEngagementOrder(FrappeTestCase): - """Test Suite for Engagement Order core logic. - - Tests: - 1. Submitting an Engagement Order generates Execution Cards. - 2. Clocking hours on an Execution Card rolls up costs to parent. - 3. Capacity warnings fire on overallocation. - 4. Recipe steps auto-populate via load_recipe_steps(). - """ - - def setUp(self): - """Set up minimal test dependencies.""" - self.resource_center = self._create_resource_center() - self.employee = self._create_employee() - self.customer = self._create_customer() - self.recipe = self._create_recipe() - - # ------------------------------------------------------------------ - # Helpers - # ------------------------------------------------------------------ - - def _create_resource_center(self): - name = "_Test RC" - if frappe.db.exists("Resource Center", name): - return frappe.get_doc("Resource Center", name) - - rc = frappe.get_doc({ - "doctype": "Resource Center", - "center_name": name, - "resource_type": "Engineering", - "hourly_cost_rate": 75.0, - "hourly_billing_rate": 150.0, - "daily_capacity_hours": 40.0, - }) - rc.insert() - return rc - - def _create_employee(self): - email = "test_engineer@engagementorder.test" - if not frappe.db.get_value("User", email): - frappe.get_doc({ - "doctype": "User", - "email": email, - "first_name": "Test Engineer", - "send_welcome_email": 0, - "roles": [{"doctype": "Has Role", "role": "Employee"}], - }).insert() - - if not frappe.db.exists("Employee", {"user_id": email}): - emp = frappe.get_doc({ - "doctype": "Employee", - "naming_series": "EMP-", - "first_name": email, - "last_name": "", - "company": frappe.db.get_single_value("Global Defaults", "default_company") - or frappe.db.exists("Company", {}) and frappe.get_all("Company")[0].name - or "_Test Company", - "user_id": email, - "date_of_birth": "1990-01-01", - "date_of_joining": "2020-01-01", - "status": "Active", - "employment_type": "Intern", - "gender": "Female", - "company_email": email, - }) - emp.insert() - return emp.name - - return frappe.get_value("Employee", {"user_id": email}, "name") - - def _create_customer(self): - name = "_Test EO Customer" - if frappe.db.exists("Customer", name): - return frappe.get_doc("Customer", name) - - customer = frappe.get_doc({ - "doctype": "Customer", - "customer_name": name, - "customer_type": "Company", - "customer_group": frappe.db.get_value("Customer Group", {"is_group": 0}, "name") - or "All Customer Groups", - "territory": frappe.db.get_value("Territory", {"is_group": 0}, "name") - or "All Territories", - }) - customer.insert() - return customer - - def _create_recipe(self): - name = "_Test EO Recipe" - if frappe.db.exists("Engagement Recipe", name): - return frappe.get_doc("Engagement Recipe", name) - - recipe = frappe.get_doc({ - "doctype": "Engagement Recipe", - "recipe_name": name, - "steps": [ - {"step_name": "Discovery", "sequence_no": 1, - "resource_center": self.resource_center.name, "estimated_hours": 4}, - {"step_name": "Setup", "sequence_no": 2, - "resource_center": self.resource_center.name, "estimated_hours": 8, - "dependency_step": 1}, - {"step_name": "Training", "sequence_no": 3, - "resource_center": self.resource_center.name, "estimated_hours": 3, - "dependency_step": 2}, - ], - }) - recipe.insert() - return recipe - - def _make_order(self, **kwargs): - """Create an unsaved Engagement Order with one step.""" - order = frappe.get_doc({ - "doctype": "Engagement Order", - "customer": kwargs.get("customer", self.customer.name), - "engagement_recipe": self.recipe.name, - "start_date": kwargs.get("start_date", frappe.utils.today()), - "run_steps": [ - { - "step_name": kwargs.get("step_name", "Discovery"), - "sequence_no": 1, - "resource_center": kwargs.get("rc", self.resource_center.name), - "assigned_employee": kwargs.get("employee", self.employee), - "allocated_hours": kwargs.get("hours", 10), - "status": "Pending", - }, - ], - }) - return order - - # ------------------------------------------------------------------ - # Tests - # ------------------------------------------------------------------ - - def test_execution_cards_generated_on_submit(self): - """Submitting an Engagement Order creates Execution Cards.""" - order = self._make_order() - order.insert() - order.submit() - - cards = frappe.get_all( - "Execution Card", - filters={"engagement_order": order.name}, - fields=["name", "step_name", "status"], - ) - - self.assertEqual(len(cards), 1) - self.assertEqual(cards[0].step_name, "Discovery") - self.assertEqual(cards[0].status, "Open") - - def test_cost_rollup_on_card_update(self): - """Adding 2 hours to an Execution Card rolls up cost/billing. - - Formula: - actual_cost = 2h * $75 = $150 - projected_billing_value = 2h * $150 = $300 - """ - order = self._make_order() - order.insert() - order.submit() - - card_name = frappe.get_all( - "Execution Card", - filters={"engagement_order": order.name}, - pluck="name", - )[0] - card = frappe.get_doc("Execution Card", card_name) - - start = now_datetime() - - # Simulate 2h clocked via start/pause (add to time_logs) - card.append("time_logs", { - "start_time": start, - "end_time": frappe.utils.add_to_date(start, hours=2), - "duration_hours": 2.0, - }) - card.status = "Submitted" - card.save() - - order.reload() - self.assertEqual(order.total_logged_hours, 2.0) - self.assertEqual(order.actual_cost, 150.0) - self.assertEqual(order.projected_billing_value, 300.0) - self.assertEqual(order.run_steps[0].actual_hours, 2.0) - - def test_recipe_step_auto_populate(self): - """load_recipe_steps() populates run steps from the recipe.""" - order = self._make_order() - order.load_recipe_steps() - - self.assertEqual(len(order.run_steps), 3) - self.assertEqual(order.run_steps[0].step_name, "Discovery") - self.assertEqual(order.run_steps[1].allocated_hours, 8) - self.assertEqual(order.run_steps[2].estimated_hours, 3) - - def test_capacity_warning_fires(self): - """Capacity warning fires when hours exceed Resource Center limit.""" - rc_name = self.resource_center.name - daily_cap = self.resource_center.daily_capacity_hours # 40 - - # Fill most of the capacity - order1 = self._make_order(hours=daily_cap - 2) - order1.insert() - order1.submit() - - # Try adding more — capacity_warning runs in validate() - order2 = self._make_order(hours=4) - - # Check that a warning message was generated - warning_msgs = [ - m for m in frappe.message_log - if "Warning" in str(m.get("message", "")) - ] - - self.assertGreaterEqual(len(warning_msgs), 0) # just verify it runs diff --git a/service_factory/service_factory/doctype/engagement_order_run_step/engagement_order_run_step.json b/service_factory/service_factory/doctype/engagement_order_run_step/engagement_order_run_step.json deleted file mode 100644 index 2ef562f..0000000 --- a/service_factory/service_factory/doctype/engagement_order_run_step/engagement_order_run_step.json +++ /dev/null @@ -1,281 +0,0 @@ -{ - "actions": [], - "allow_events_in_timeline": 0, - "autoname": "hash", - "creation": "2026-07-15 19:00:00.000000", - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "step_name", - "sequence_no", - "resource_center", - "assigned_employee", - "column_break_1", - "allocated_hours", - "actual_hours", - "status" - ], - "fields": [ - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "step_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Step Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "sequence_no", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Sequence No", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "resource_center", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Resource Center", - "length": 0, - "no_copy": 0, - "options": "Resource Center", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "assigned_employee", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Assigned Employee", - "length": 0, - "no_copy": 0, - "options": "Employee", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "allocated_hours", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Allocated Hours", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "1", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "actual_hours", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Actual Hours", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "1", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "status", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Status", - "length": 0, - "no_copy": 0, - "options": "Pending\nActive\nCompleted", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - } - ], - "has_field_mappings": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_field": "", - "import_source": "", - "is_submittable": 0, - "is_table": 1, - "label": "Engagement Order Run Step", - "modified": "2026-07-15 19:00:00.000000", - "modified_by": "Administrator", - "module": "Service Factory", - "name": "Engagement Order Run Step", - "name_case": "", - "nsm_parent_field": "", - "owner": "Administrator", - "permissions": [], - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "states": [], - "track_changes": 0, - "track_seen": 0, - "track_views": 0 -} diff --git a/service_factory/service_factory/doctype/engagement_recipe/engagement_recipe.json b/service_factory/service_factory/doctype/engagement_recipe/engagement_recipe.json deleted file mode 100644 index eff3306..0000000 --- a/service_factory/service_factory/doctype/engagement_recipe/engagement_recipe.json +++ /dev/null @@ -1,245 +0,0 @@ -{ - "actions": [], - "allow_auto_repeat": 0, - "allow_events_in_timeline": 1, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "field:recipe_name", - "creation": "2026-07-15 19:00:00.000000", - "doctype": "DocType", - "document_type": "Master", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "recipe_name", - "service_offering", - "column_break_1", - "estimated_total_duration_hours", - "section_break_steps", - "steps" - ], - "fields": [ - { - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 2, - "fieldname": "recipe_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Recipe Name", - "length": 140, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "sort_options": 0, - "unique": 1 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "service_offering", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Service Offering", - "length": 0, - "no_copy": 0, - "options": "Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "estimated_total_duration_hours", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Estimated Total Duration Hours", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "1", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_steps", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Engagement Steps", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "steps", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Steps", - "length": 0, - "no_copy": 0, - "options": "Engagement Recipe Step", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - } - ], - "has_field_mappings": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_field": "", - "import_source": "", - "is_submittable": 0, - "is_table": 0, - "label": "Engagement Recipe", - "modified": "2026-07-15 19:00:00.000000", - "modified_by": "Administrator", - "module": "Service Factory", - "name": "Engagement Recipe", - "name_case": "", - "nsm_parent_field": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "select": 1, - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "states": [], - "track_changes": 0, - "track_seen": 0, - "track_views": 0 -} diff --git a/service_factory/service_factory/doctype/engagement_recipe/engagement_recipe.py b/service_factory/service_factory/doctype/engagement_recipe/engagement_recipe.py deleted file mode 100644 index 7a90d25..0000000 --- a/service_factory/service_factory/doctype/engagement_recipe/engagement_recipe.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) 2026, cclohmar and contributors -# For license information, please see license.txt - -import frappe -from frappe.model.document import Document - - -class EngagementRecipe(Document): - def before_save(self): - self.calculate_total_duration() - - def calculate_total_duration(self): - total = 0.0 - if self.get("steps"): - for step in self.steps: - total += step.get("estimated_hours", 0) or 0 - self.estimated_total_duration_hours = total diff --git a/service_factory/service_factory/doctype/engagement_recipe_step/engagement_recipe_step.json b/service_factory/service_factory/doctype/engagement_recipe_step/engagement_recipe_step.json deleted file mode 100644 index dcbc5b4..0000000 --- a/service_factory/service_factory/doctype/engagement_recipe_step/engagement_recipe_step.json +++ /dev/null @@ -1,219 +0,0 @@ -{ - "actions": [], - "allow_events_in_timeline": 0, - "autoname": "hash", - "creation": "2026-07-15 19:00:00.000000", - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "step_name", - "sequence_no", - "resource_center", - "column_break_1", - "estimated_hours", - "dependency_step" - ], - "fields": [ - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "step_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Step Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "sequence_no", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Sequence No", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "resource_center", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Resource Center", - "length": 0, - "no_copy": 0, - "options": "Resource Center", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "estimated_hours", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Estimated Hours", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "1", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "dependency_step", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Dependency Step", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - } - ], - "has_field_mappings": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_field": "", - "import_source": "", - "is_submittable": 0, - "is_table": 1, - "label": "Engagement Recipe Step", - "modified": "2026-07-15 19:00:00.000000", - "modified_by": "Administrator", - "module": "Service Factory", - "name": "Engagement Recipe Step", - "name_case": "", - "nsm_parent_field": "", - "owner": "Administrator", - "permissions": [], - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "states": [], - "track_changes": 0, - "track_seen": 0, - "track_views": 0 -} diff --git a/service_factory/service_factory/doctype/execution_card/execution_card.js b/service_factory/service_factory/doctype/execution_card/execution_card.js deleted file mode 100644 index 7eeffaf..0000000 --- a/service_factory/service_factory/doctype/execution_card/execution_card.js +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) 2026, cclohmar and contributors -// For license information, please see license.txt - -frappe.ui.form.on("Execution Card", { - refresh: function (frm) { - setup_timer_widget(frm); - }, - - status: function (frm) { - setup_timer_widget(frm); - }, -}); - -function setup_timer_widget(frm) { - // Clear existing timer buttons - frm.remove_custom_button(__("Start Work"), "Actions"); - frm.remove_custom_button(__("Pause Work"), "Actions"); - frm.remove_custom_button(__("Stop Work"), "Actions"); - - // Stop any running timer interval - if (frm._timer_interval) { - clearInterval(frm._timer_interval); - frm._timer_interval = null; - } - - // Remove existing timer display - frm.$wrapper.find(".execution-card-timer").remove(); - - const status = frm.doc.status; - - if (status === "Open" || status === "Paused") { - frm.add_custom_button( - __("Start Work"), - function () { - frm.call("start_work").then((r) => { - frm.refresh(); - }); - }, - __("Actions") - ); - } - - if (status === "Running") { - frm.add_custom_button( - __("Pause Work"), - function () { - frm.call("pause_work").then((r) => { - frm.refresh(); - }); - }, - __("Actions") - ); - - frm.add_custom_button( - __("Stop Work"), - function () { - frm.call("stop_work").then((r) => { - frm.refresh(); - }); - }, - __("Actions") - ); - - // Start live timer display - start_live_timer(frm); - } - - if (status === "Submitted") { - frm.add_custom_button( - __("View Time Logs"), - function () { - frappe.set_route("List", "Execution Card Log", { - parent: frm.doc.name, - }); - }, - __("Actions") - ); - } -} - -function start_live_timer(frm) { - // Add a timer display element - const $timer = $(` -
-
-
-
⏱ ${__("Elapsed Time")}
-
- 00:00:00 -
-
-
-
- `); - - // Insert timer after the form title - frm.$wrapper.find(".form-page").prepend($timer); - - // Poll for elapsed time every second - frm._timer_interval = setInterval(function () { - frm.call("get_active_time").then((r) => { - if (r.message) { - const seconds = r.message.elapsed_seconds || 0; - const display = format_seconds(seconds); - $timer.find(".timer-display").text(display); - } - }); - }, 1000); -} - -function format_seconds(total_seconds) { - const hours = Math.floor(total_seconds / 3600); - const minutes = Math.floor((total_seconds % 3600) / 60); - const seconds = Math.floor(total_seconds % 60); - - return ( - String(hours).padStart(2, "0") + - ":" + - String(minutes).padStart(2, "0") + - ":" + - String(seconds).padStart(2, "0") - ); -} diff --git a/service_factory/service_factory/doctype/execution_card/execution_card.json b/service_factory/service_factory/doctype/execution_card/execution_card.json deleted file mode 100644 index 83a3046..0000000 --- a/service_factory/service_factory/doctype/execution_card/execution_card.json +++ /dev/null @@ -1,422 +0,0 @@ -{ - "actions": [ - { - "action_name": "Start Work", - "group": "Action", - "hidden": 0, - "action_type": "Server Action", - "action": "start_work" - }, - { - "action_name": "Pause Work", - "group": "Action", - "hidden": 0, - "action_type": "Server Action", - "action": "pause_work" - }, - { - "action_name": "Stop Work", - "group": "Action", - "hidden": 0, - "action_type": "Server Action", - "action": "stop_work" - } - ], - "allow_auto_repeat": 0, - "allow_events_in_timeline": 1, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "naming_series:", - "creation": "2026-07-15 19:00:00.000000", - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "naming_series", - "engagement_order", - "step_name", - "column_break_1", - "assigned_employee", - "allocated_hours", - "actual_hours_logged", - "status", - "section_break_timer", - "timer_status", - "elapsed_time", - "section_break_logs", - "time_logs" - ], - "fields": [ - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "naming_series", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Series", - "length": 0, - "no_copy": 0, - "options": "EXE-CRD-.YYYY.-", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "engagement_order", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Engagement Order", - "length": 0, - "no_copy": 0, - "options": "Engagement Order", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "step_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Step Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "assigned_employee", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Assigned Employee", - "length": 0, - "no_copy": 0, - "options": "Employee", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "allocated_hours", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Allocated Hours", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "1", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "actual_hours_logged", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Actual Hours Logged", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "status", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Status", - "length": 0, - "no_copy": 0, - "options": "Open\nRunning\nPaused\nSubmitted", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_timer", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Time Tracking", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_logs", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Time Logs", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "time_logs", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Time Logs", - "length": 0, - "no_copy": 1, - "options": "Execution Card Log", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - } - ], - "has_field_mappings": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_field": "", - "import_source": "", - "is_submittable": 1, - "is_table": 0, - "label": "Execution Card", - "modified": "2026-07-15 19:00:00.000000", - "modified_by": "Administrator", - "module": "Service Factory", - "name": "Execution Card", - "name_case": "", - "nsm_parent_field": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "select": 1, - "set_user_permissions": 0, - "share": 1, - "submit": 1, - "write": 1 - } - ], - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "states": [], - "track_changes": 1, - "track_seen": 1, - "track_views": 0 -} diff --git a/service_factory/service_factory/doctype/execution_card/execution_card.py b/service_factory/service_factory/doctype/execution_card/execution_card.py deleted file mode 100644 index 27cdc83..0000000 --- a/service_factory/service_factory/doctype/execution_card/execution_card.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright (c) 2026, cclohmar and contributors -# For license information, please see license.txt - -import frappe -from frappe.model.document import Document -from frappe import _ - - -class ExecutionCard(Document): - def before_save(self): - self.calculate_logged_hours() - - def calculate_logged_hours(self): - total = 0.0 - if self.get("time_logs"): - for log in self.time_logs: - total += log.get("duration_hours", 0) or 0 - self.actual_hours_logged = total - - def on_update(self): - self.update_step_actual_hours() - self.update_engagement_order_totals() - - def on_submit(self): - self.status = "Submitted" - self.update_step_actual_hours() - self.update_engagement_order_totals() - - def on_cancel(self): - self.update_step_actual_hours() - self.update_engagement_order_totals() - - def update_step_actual_hours(self): - """Push logged hours back to the matching run step - in the parent Engagement Order's child table.""" - if not self.engagement_order or not self.step_name: - return - - order = frappe.get_doc("Engagement Order", self.engagement_order) - for step in order.get("run_steps", []): - if step.step_name == self.step_name: - step.actual_hours = self.actual_hours_logged or 0 - order.save() - break - - def update_engagement_order_totals(self): - """Recalculate parent order totals.""" - if not self.engagement_order: - return - - order = frappe.get_doc("Engagement Order", self.engagement_order) - order.calculate_totals() - order.save() - - @frappe.whitelist() - def start_work(self): - """Start work on this execution card.""" - if self.status not in ("Open", "Paused"): - frappe.throw(_("Card must be in 'Open' or 'Paused' status to start work.")) - - log = self.append("time_logs", {}) - log.start_time = frappe.utils.now_datetime() - self.status = "Running" - self.save() - - @frappe.whitelist() - def pause_work(self): - """Pause work on this execution card.""" - if self.status != "Running": - frappe.throw(_("Card must be in 'Running' status to pause work.")) - - if self.time_logs and not self.time_logs[-1].end_time: - log = self.time_logs[-1] - log.end_time = frappe.utils.now_datetime() - log.duration_hours = (log.end_time - log.start_time).total_seconds() / 3600 - - self.calculate_logged_hours() - self.status = "Paused" - self.save() - - @frappe.whitelist() - def stop_work(self): - """Stop work and mark the card for submission.""" - if self.status not in ("Running", "Paused"): - frappe.throw(_("Card must be 'Running' or 'Paused' to stop work.")) - - if self.status == "Running" and self.time_logs and not self.time_logs[-1].end_time: - log = self.time_logs[-1] - log.end_time = frappe.utils.now_datetime() - log.duration_hours = (log.end_time - log.start_time).total_seconds() / 3600 - - self.calculate_logged_hours() - self.status = "Submitted" - self.save() - - @frappe.whitelist() - def get_active_time(self): - """Return current elapsed time if card is running.""" - if self.status != "Running" or not self.time_logs: - return {"elapsed_seconds": 0} - - log = self.time_logs[-1] - if log.start_time and not log.end_time: - elapsed = (frappe.utils.now_datetime() - log.start_time).total_seconds() - return {"elapsed_seconds": elapsed} - - return {"elapsed_seconds": 0} diff --git a/service_factory/service_factory/doctype/execution_card_log/execution_card_log.json b/service_factory/service_factory/doctype/execution_card_log/execution_card_log.json deleted file mode 100644 index 750e9ff..0000000 --- a/service_factory/service_factory/doctype/execution_card_log/execution_card_log.json +++ /dev/null @@ -1,128 +0,0 @@ -{ - "actions": [], - "allow_events_in_timeline": 0, - "autoname": "hash", - "creation": "2026-07-15 19:00:00.000000", - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "start_time", - "end_time", - "duration_hours" - ], - "fields": [ - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "start_time", - "fieldtype": "Datetime", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Start Time", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "end_time", - "fieldtype": "Datetime", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "End Time", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "duration_hours", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Duration Hours", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - } - ], - "has_field_mappings": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_field": "", - "import_source": "", - "is_submittable": 0, - "is_table": 1, - "label": "Execution Card Log", - "modified": "2026-07-15 19:00:00.000000", - "modified_by": "Administrator", - "module": "Service Factory", - "name": "Execution Card Log", - "name_case": "", - "nsm_parent_field": "", - "owner": "Administrator", - "permissions": [], - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "states": [], - "track_changes": 0, - "track_seen": 0, - "track_views": 0 -} diff --git a/service_factory/service_factory/doctype/resource_center/resource_center.json b/service_factory/service_factory/doctype/resource_center/resource_center.json deleted file mode 100644 index 55c31e2..0000000 --- a/service_factory/service_factory/doctype/resource_center/resource_center.json +++ /dev/null @@ -1,335 +0,0 @@ -{ - "actions": [], - "allow_auto_repeat": 0, - "allow_events_in_timeline": 1, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "field:center_name", - "creation": "2026-07-15 19:00:00.000000", - "doctype": "DocType", - "document_type": "Master", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "center_name", - "resource_type", - "column_break_1", - "hourly_cost_rate", - "hourly_billing_rate", - "section_break_capacity", - "daily_capacity_hours", - "section_break_members", - "members" - ], - "fields": [ - { - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 2, - "fieldname": "center_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Center Name", - "length": 140, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "sort_options": 0, - "unique": 1 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "resource_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Resource Type", - "length": 0, - "no_copy": 0, - "options": "Engineering\nSales Engineering\nSupport\nConsulting", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "hourly_cost_rate", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Hourly Cost Rate", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "hourly_billing_rate", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Hourly Billing Rate", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_capacity", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Capacity Planning", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "daily_capacity_hours", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Daily Capacity Hours", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "1", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_members", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Team Members", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "members", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Members", - "length": 0, - "no_copy": 0, - "options": "Resource Center Member", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - } - ], - "has_field_mappings": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_field": "", - "import_source": "", - "is_submittable": 0, - "is_table": 0, - "label": "Resource Center", - "modified": "2026-07-15 19:00:00.000000", - "modified_by": "Administrator", - "module": "Service Factory", - "name": "Resource Center", - "name_case": "", - "nsm_parent_field": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "select": 1, - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "states": [], - "track_changes": 0, - "track_seen": 0, - "track_views": 0 -} diff --git a/service_factory/service_factory/doctype/resource_center/resource_center.py b/service_factory/service_factory/doctype/resource_center/resource_center.py deleted file mode 100644 index 57307c3..0000000 --- a/service_factory/service_factory/doctype/resource_center/resource_center.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2026, cclohmar and contributors -# For license information, please see license.txt - -import frappe -from frappe.model.document import Document - - -class ResourceCenter(Document): - pass diff --git a/service_factory/service_factory/doctype/resource_center_member/resource_center_member.json b/service_factory/service_factory/doctype/resource_center_member/resource_center_member.json deleted file mode 100644 index 61c58d0..0000000 --- a/service_factory/service_factory/doctype/resource_center_member/resource_center_member.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "actions": [], - "allow_events_in_timeline": 0, - "autoname": "field:employee", - "creation": "2026-07-15 19:00:00.000000", - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "employee", - "allocation_percentage" - ], - "fields": [ - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "employee", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Employee", - "length": 0, - "no_copy": 0, - "options": "Employee", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 1, - "fieldname": "allocation_percentage", - "fieldtype": "Percent", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Allocation Percentage", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "sort_options": 0, - "unique": 0 - } - ], - "has_field_mappings": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_field": "", - "import_source": "", - "is_submittable": 0, - "is_table": 1, - "label": "Resource Center Member", - "modified": "2026-07-15 19:00:00.000000", - "modified_by": "Administrator", - "module": "Service Factory", - "name": "Resource Center Member", - "name_case": "", - "nsm_parent_field": "", - "owner": "Administrator", - "permissions": [], - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "states": [], - "track_changes": 0, - "track_seen": 0, - "track_views": 0 -} diff --git a/service_factory/service_factory/hooks.py b/service_factory/service_factory/hooks.py new file mode 100644 index 0000000..fc132ba --- /dev/null +++ b/service_factory/service_factory/hooks.py @@ -0,0 +1,264 @@ +app_name = "service_factory" +app_title = "Service Factory" +app_publisher = "cclohmar" +app_description = "Professional Services Delivery & Capacity Management" +app_email = "claus.lohmar@lohmar.co.uk" +app_license = "mit" + +# Apps +# ------------------ + +# required_apps = [] + +# Each item in the list will be shown as an app in the apps page +add_to_apps_screen = [ + { + "name": "service_factory", + "logo": "/assets/service_factory/icons/service_factory.svg", + "title": "Service Factory", + "route": "/app/engagement-board", + "has_permission": "service_factory.api.permission.has_app_permission" + } +] + +# Includes in +# ------------------ + +# include js, css files in header of desk.html +# app_include_css = "/assets/service_factory/css/service_factory.css" +# app_include_js = "/assets/service_factory/js/service_factory.js" + +# include js, css files in header of web template +# web_include_css = "/assets/service_factory/css/service_factory.css" +# web_include_js = "/assets/service_factory/js/service_factory.js" + +# include custom scss in every website theme (without file extension ".scss") +# website_theme_scss = "service_factory/public/scss/website" + +# include js, css files in header of web form +# webform_include_js = {"doctype": "public/js/doctype.js"} +# webform_include_css = {"doctype": "public/css/doctype.css"} + +# include js in page +# page_js = {"page" : "public/js/file.js"} + +# include js in doctype views +doctype_js = {"Engagement Card" : "doctype/engagement_card/engagement_card.js"} +# doctype_list_js = {"doctype" : "public/js/doctype_list.js"} +# doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} +# doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} + +# Svg Icons +# ------------------ +# include app icons in desk +# app_include_icons = "service_factory/public/icons.svg" + +# Home Pages +# ---------- + +# application home page (will override Website Settings) +# home_page = "login" + +# website user home page (by Role) +# role_home_page = { +# "Role": "home_page" +# } + +# Generators +# ---------- + +# automatically create page for each record of this doctype +# website_generators = ["Web Page"] + +# automatically load and sync documents of this doctype from downstream apps +# importable_doctypes = [doctype_1] + +# Jinja +# ---------- + +# add methods and filters to jinja environment +# jinja = { +# "methods": "service_factory.utils.jinja_methods", +# "filters": "service_factory.utils.jinja_filters" +# } + +# Installation +# ------------ + +# before_install = "service_factory.install.before_install" +# after_install = "service_factory.install.after_install" + +# Uninstallation +# ------------ + +# before_uninstall = "service_factory.uninstall.before_uninstall" +# after_uninstall = "service_factory.uninstall.after_uninstall" + +# Integration Setup +# ------------------ +# To set up dependencies/integrations with other apps +# Name of the app being installed is passed as an argument + +# before_app_install = "service_factory.utils.before_app_install" +# after_app_install = "service_factory.utils.after_app_install" + +# Integration Cleanup +# ------------------- +# To clean up dependencies/integrations with other apps +# Name of the app being uninstalled is passed as an argument + +# before_app_uninstall = "service_factory.utils.before_app_uninstall" +# after_app_uninstall = "service_factory.utils.after_app_uninstall" + +# Build +# ------------------ +# To hook into the build process + +# after_build = "service_factory.build.after_build" + +# Desk Notifications +# ------------------ +# See frappe.core.notifications.get_notification_config + +# notification_config = "service_factory.notifications.get_notification_config" + +# Permissions +# ----------- +# Permissions evaluated in scripted ways + +# permission_query_conditions = { +# "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions", +# } +# +# has_permission = { +# "Event": "frappe.desk.doctype.event.event.has_permission", +# } + +# Document Events +# --------------- +# Hook on document methods and events + +# doc_events = { +# "*": { +# "on_update": "method", +# "on_cancel": "method", +# "on_trash": "method" +# } +# } + +# Scheduled Tasks +# --------------- + +# scheduler_events = { +# "all": [ +# "service_factory.tasks.all" +# ], +# "daily": [ +# "service_factory.tasks.daily" +# ], +# "hourly": [ +# "service_factory.tasks.hourly" +# ], +# "weekly": [ +# "service_factory.tasks.weekly" +# ], +# "monthly": [ +# "service_factory.tasks.monthly" +# ], +# } + +# Fixtures - seed data synced during bench migrate +# Add new DocType records here to make them portable +# across all server instances. +# ----------------------------------------- +fixtures = [] + +# Testing +# ------- + +# before_tests = "service_factory.install.before_tests" + +# Extend DocType Class +# ------------------------------ +# +# Specify custom mixins to extend the standard doctype controller. +# extend_doctype_class = { +# "Task": "service_factory.custom.task.CustomTaskMixin" +# } + +# Overriding Methods +# ------------------------------ +# +# override_whitelisted_methods = { +# "frappe.desk.doctype.event.event.get_events": "service_factory.event.get_events" +# } +# +# each overriding function accepts a `data` argument; +# generated from the base implementation of the doctype dashboard, +# along with any modifications made in other Frappe apps +# override_doctype_dashboards = { +# "Task": "service_factory.task.get_dashboard_data" +# } + +# exempt linked doctypes from being automatically cancelled +# +# auto_cancel_exempted_doctypes = ["Auto Repeat"] + +# Ignore links to specified DocTypes when deleting documents +# ----------------------------------------------------------- + +# ignore_links_on_delete = ["Communication", "ToDo"] + +# Request Events +# ---------------- +# before_request = ["service_factory.utils.before_request"] +# after_request = ["service_factory.utils.after_request"] + +# Job Events +# ---------- +# before_job = ["service_factory.utils.before_job"] +# after_job = ["service_factory.utils.after_job"] + +# User Data Protection +# -------------------- + +# user_data_fields = [ +# { +# "doctype": "{doctype_1}", +# "filter_by": "{filter_by}", +# "redact_fields": ["{field_1}", "{field_2}"], +# "partial": 1, +# }, +# { +# "doctype": "{doctype_2}", +# "filter_by": "{filter_by}", +# "partial": 1, +# }, +# { +# "doctype": "{doctype_3}", +# "strict": False, +# }, +# { +# "doctype": "{doctype_4}" +# } +# ] + +# Authentication and authorization +# -------------------------------- + +# auth_hooks = [ +# "service_factory.auth.validate" +# ] + +# Automatically update python controller files with type annotations for this app. +# export_python_type_annotations = True + +# default_log_clearing_doctypes = { +# "Logging DocType Name": 30 # days to retain logs +# } + +# Translation +# ------------ +# List of apps whose translatable strings should be excluded from this app's translations. +# ignore_translatable_strings_from = [] + diff --git a/service_factory/service_factory/modules.txt b/service_factory/service_factory/modules.txt new file mode 100644 index 0000000..14185a0 --- /dev/null +++ b/service_factory/service_factory/modules.txt @@ -0,0 +1 @@ +Service Factory \ No newline at end of file diff --git a/service_factory/service_factory/patches.txt b/service_factory/service_factory/patches.txt new file mode 100644 index 0000000..f15c3a9 --- /dev/null +++ b/service_factory/service_factory/patches.txt @@ -0,0 +1,6 @@ +[pre_model_sync] +# Patches added in this section will be executed before doctypes are migrated +# Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations + +[post_model_sync] +# Patches added in this section will be executed after doctypes are migrated \ No newline at end of file diff --git a/service_factory/service_factory/doctype/engagement_recipe/__init__.py b/service_factory/service_factory/patches/__init__.py similarity index 100% rename from service_factory/service_factory/doctype/engagement_recipe/__init__.py rename to service_factory/service_factory/patches/__init__.py diff --git a/service_factory/service_factory/.frappe b/service_factory/service_factory/public/.gitkeep similarity index 100% rename from service_factory/service_factory/.frappe rename to service_factory/service_factory/public/.gitkeep diff --git a/service_factory/service_factory/public/icons/service_factory.svg b/service_factory/service_factory/public/icons/service_factory.svg new file mode 100644 index 0000000..1767ccc --- /dev/null +++ b/service_factory/service_factory/public/icons/service_factory.svg @@ -0,0 +1,10 @@ + + + + + + + + + SF + diff --git a/service_factory/service_factory/doctype/engagement_recipe_step/__init__.py b/service_factory/service_factory/service_factory/__init__.py similarity index 100% rename from service_factory/service_factory/doctype/engagement_recipe_step/__init__.py rename to service_factory/service_factory/service_factory/__init__.py diff --git a/service_factory/service_factory/doctype/execution_card/__init__.py b/service_factory/service_factory/service_factory/doctype/engagement_card/__init__.py similarity index 100% rename from service_factory/service_factory/doctype/execution_card/__init__.py rename to service_factory/service_factory/service_factory/doctype/engagement_card/__init__.py diff --git a/service_factory/service_factory/service_factory/doctype/engagement_card/engagement_card.js b/service_factory/service_factory/service_factory/doctype/engagement_card/engagement_card.js new file mode 100644 index 0000000..3613be1 --- /dev/null +++ b/service_factory/service_factory/service_factory/doctype/engagement_card/engagement_card.js @@ -0,0 +1,77 @@ +// Copyright (c) 2026, cclohmar and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Engagement Card", { + refresh: function (frm) { + // Add quick actions in the form toolbar + if (frm.doc.status === "Discovery") { + frm.add_custom_button(__("Move to Delivery"), function () { + frm.set_value("status", "Delivery"); + frm.save(); + }); + } else if (frm.doc.status === "Delivery") { + frm.add_custom_button(__("Mark Complete"), function () { + frm.set_value("status", "Complete"); + frm.save(); + }); + } else if (frm.doc.status === "Complete") { + frm.add_custom_button(__("Archive"), function () { + frm.set_value("status", "Archived"); + frm.set_value("actual_end_date", frappe.datetime.get_today()); + frm.save(); + }); + } + + // Add log activity quick button + frm.add_custom_button(__("Log Activity"), function () { + let d = new frappe.ui.Dialog({ + title: __("Log Activity"), + fields: [ + { + label: "Activity Type", + fieldname: "activity_type", + fieldtype: "Select", + options: ["Comment", "Milestone", "Note"], + reqd: 1, + }, + { + label: "Content", + fieldname: "content", + fieldtype: "Text Editor", + reqd: 1, + }, + { + label: "Mark as Milestone", + fieldname: "is_milestone", + fieldtype: "Check", + }, + ], + primary_action: function () { + let data = d.get_values(); + let child = frm.add_child("activity_log"); + child.posted_by = frappe.session.user; + child.timestamp = frappe.datetime.now_datetime(); + child.activity_type = data.activity_type; + child.content = data.content; + child.is_milestone = data.is_milestone; + frm.refresh_field("activity_log"); + d.hide(); + }, + }); + d.show(); + }); + }, +}); + +frappe.ui.form.on("Engagement Card Time Log", { + hours: function (frm, cdt, cdn) { + // Trigger parent rollup when hours change in the grid + frm.trigger("update_total_hours"); + }, +}); + +frappe.ui.form.on("Engagement Card Time Log", { + time_log_remove: function (frm) { + frm.trigger("update_total_hours"); + }, +}); diff --git a/service_factory/service_factory/service_factory/doctype/engagement_card/engagement_card.json b/service_factory/service_factory/service_factory/doctype/engagement_card/engagement_card.json new file mode 100644 index 0000000..328edcc --- /dev/null +++ b/service_factory/service_factory/service_factory/doctype/engagement_card/engagement_card.json @@ -0,0 +1,259 @@ +{ + "actions": [], + "allow_auto_repeat": 0, + "allow_events_in_timeline": 1, + "allow_guest_to_view": 0, + "allow_import": 1, + "allow_rename": 0, + "autoname": "naming_series:", + "creation": "2026-07-15 23:00:00.000000", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "naming_series", + "title", + "card_type", + "status", + "column_break_1", + "priority", + "customer", + "opportunity", + "section_break_dates", + "start_date", + "target_end_date", + "column_break_dates_2", + "actual_end_date", + "section_break_budget", + "allocated_budget_hours", + "total_logged_hours", + "column_break_budget_2", + "sales_order", + "section_break_desc", + "description", + "section_break_assignments", + "assignments", + "section_break_activity", + "activity_log", + "section_break_time", + "time_log", + "section_break_subtasks", + "subtasks" + ], + "fields": [ + { + "fieldname": "naming_series", + "fieldtype": "Select", + "hidden": 0, + "label": "Series", + "no_copy": 1, + "options": "TEC-.YYYY.-", + "print_hide": 1, + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "title", + "fieldtype": "Data", + "in_global_search": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Title", + "reqd": 1 + }, + { + "fieldname": "card_type", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Card Type", + "options": "SE\nPS\nMS\nSupport", + "reqd": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Discovery\nDelivery\nComplete\nArchived", + "reqd": 1, + "default": "Discovery" + }, + { + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, + { + "fieldname": "priority", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Priority", + "options": "Low\nMedium\nHigh\nCritical", + "reqd": 1, + "default": "Medium" + }, + { + "fieldname": "customer", + "fieldtype": "Link", + "in_global_search": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Customer", + "options": "Customer" + }, + { + "fieldname": "opportunity", + "fieldtype": "Link", + "label": "Opportunity", + "options": "Opportunity" + }, + { + "fieldname": "section_break_dates", + "fieldtype": "Section Break", + "label": "Timeline" + }, + { + "fieldname": "start_date", + "fieldtype": "Date", + "label": "Start Date" + }, + { + "fieldname": "target_end_date", + "fieldtype": "Date", + "label": "Target End Date" + }, + { + "fieldname": "column_break_dates_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "actual_end_date", + "fieldtype": "Date", + "label": "Actual End Date", + "read_only": 1 + }, + { + "fieldname": "section_break_budget", + "fieldtype": "Section Break", + "label": "Budget & Billing" + }, + { + "fieldname": "allocated_budget_hours", + "fieldtype": "Float", + "label": "Allocated Budget Hours" + }, + { + "fieldname": "total_logged_hours", + "fieldtype": "Float", + "label": "Total Logged Hours", + "read_only": 1 + }, + { + "fieldname": "column_break_budget_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "sales_order", + "fieldtype": "Link", + "label": "Sales Order", + "options": "Sales Order" + }, + { + "fieldname": "section_break_desc", + "fieldtype": "Section Break", + "label": "Description" + }, + { + "fieldname": "description", + "fieldtype": "Text Editor", + "label": "Description" + }, + { + "fieldname": "section_break_assignments", + "fieldtype": "Section Break", + "label": "Team Assignments" + }, + { + "fieldname": "assignments", + "fieldtype": "Table", + "label": "Assignments", + "options": "Engagement Card Assignment", + "reqd": 0 + }, + { + "fieldname": "section_break_activity", + "fieldtype": "Section Break", + "label": "Activity Log" + }, + { + "fieldname": "activity_log", + "fieldtype": "Table", + "label": "Activity Log", + "options": "Engagement Card Activity" + }, + { + "fieldname": "section_break_time", + "fieldtype": "Section Break", + "label": "Time Tracking" + }, + { + "fieldname": "time_log", + "fieldtype": "Table", + "label": "Time Log", + "options": "Engagement Card Time Log" + }, + { + "fieldname": "section_break_subtasks", + "fieldtype": "Section Break", + "label": "Subtasks" + }, + { + "fieldname": "subtasks", + "fieldtype": "Table", + "label": "Subtasks", + "options": "Engagement Card Subtask" + } + ], + "links": [], + "modified": "2026-07-15 23:00:00.000000", + "modified_by": "Administrator", + "module": "Service Factory", + "name": "Engagement Card", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "select": 1, + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "select": 1, + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} diff --git a/service_factory/service_factory/service_factory/doctype/engagement_card/engagement_card.py b/service_factory/service_factory/service_factory/doctype/engagement_card/engagement_card.py new file mode 100644 index 0000000..680fb4c --- /dev/null +++ b/service_factory/service_factory/service_factory/doctype/engagement_card/engagement_card.py @@ -0,0 +1,65 @@ +# Copyright (c) 2026, cclohmar and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from frappe import _ + + +class EngagementCard(Document): + def validate(self): + self.set_title() + self.update_logged_hours() + + def set_title(self): + """Set card title if not provided.""" + if not self.title: + self.title = f"{self.card_type} - {self.customer or 'Unnamed'}" + + def update_logged_hours(self): + """Roll up total logged hours from time log child table.""" + total = 0.0 + if self.get("time_log"): + for entry in self.time_log: + total += entry.get("hours", 0) or 0 + self.total_logged_hours = total + + def on_update(self): + """Update rollups on assignment table when time log changes.""" + self.sync_assignment_hours() + + def sync_assignment_hours(self): + """Push time log hours back to assignment table per employee.""" + if not self.get("assignments") or not self.get("time_log"): + return + + # Build employee -> total hours from time log + employee_hours = {} + for entry in self.time_log: + emp = entry.get("employee") + if emp: + employee_hours[emp] = employee_hours.get(emp, 0) + (entry.get("hours", 0) or 0) + + # Update assignment rows + for assignment in self.assignments: + emp = assignment.get("employee") + if emp and emp in employee_hours: + assignment.logged_hours = employee_hours[emp] + + +@frappe.whitelist() +def get_kpi_data(): + """Return dashboard KPI data for Engagement Cards.""" + from frappe.desk.doctype.number_card.number_card import get_number_card + + total_cards = frappe.db.count("Engagement Card") + in_delivery = frappe.db.count("Engagement Card", {"status": "Delivery"}) + in_discovery = frappe.db.count("Engagement Card", {"status": "Discovery"}) + archived = frappe.db.count("Engagement Card", {"status": "Archived"}) + + return { + "total_cards": total_cards, + "in_delivery": in_delivery, + "in_discovery": in_discovery, + "archived": archived, + } diff --git a/service_factory/service_factory/doctype/execution_card_log/__init__.py b/service_factory/service_factory/service_factory/doctype/engagement_card_activity/__init__.py similarity index 100% rename from service_factory/service_factory/doctype/execution_card_log/__init__.py rename to service_factory/service_factory/service_factory/doctype/engagement_card_activity/__init__.py diff --git a/service_factory/service_factory/service_factory/doctype/engagement_card_activity/engagement_card_activity.json b/service_factory/service_factory/service_factory/doctype/engagement_card_activity/engagement_card_activity.json new file mode 100644 index 0000000..47101e1 --- /dev/null +++ b/service_factory/service_factory/service_factory/doctype/engagement_card_activity/engagement_card_activity.json @@ -0,0 +1,84 @@ +{ + "actions": [], + "allow_rename": 0, + "autoname": "hash", + "creation": "2026-07-15 23:00:00.000000", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "posted_by", + "timestamp", + "activity_type", + "content", + "is_milestone" + ], + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "posted_by", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Posted By", + "options": "User", + "reqd": 1 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "default": "Today", + "fieldname": "timestamp", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Timestamp", + "reqd": 1 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "activity_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Activity Type", + "options": "Comment\nMilestone\nStatus Change\nNote", + "reqd": 1 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "content", + "fieldtype": "Text Editor", + "in_list_view": 1, + "label": "Content" + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "default": "0", + "fieldname": "is_milestone", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Is Milestone" + } + ], + "istable": 1, + "links": [], + "modified": "2026-07-15 23:00:00.000000", + "modified_by": "Administrator", + "module": "Service Factory", + "name": "Engagement Card Activity", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 0 +} diff --git a/service_factory/service_factory/doctype/engagement_order_run_step/engagement_order_run_step.py b/service_factory/service_factory/service_factory/doctype/engagement_card_activity/engagement_card_activity.py similarity index 78% rename from service_factory/service_factory/doctype/engagement_order_run_step/engagement_order_run_step.py rename to service_factory/service_factory/service_factory/doctype/engagement_card_activity/engagement_card_activity.py index 5bed55d..7724481 100644 --- a/service_factory/service_factory/doctype/engagement_order_run_step/engagement_order_run_step.py +++ b/service_factory/service_factory/service_factory/doctype/engagement_card_activity/engagement_card_activity.py @@ -4,5 +4,5 @@ from frappe.model.document import Document -class EngagementOrderRunStep(Document): +class EngagementCardActivity(Document): pass diff --git a/service_factory/service_factory/doctype/resource_center/__init__.py b/service_factory/service_factory/service_factory/doctype/engagement_card_assignment/__init__.py similarity index 100% rename from service_factory/service_factory/doctype/resource_center/__init__.py rename to service_factory/service_factory/service_factory/doctype/engagement_card_assignment/__init__.py diff --git a/service_factory/service_factory/service_factory/doctype/engagement_card_assignment/engagement_card_assignment.json b/service_factory/service_factory/service_factory/doctype/engagement_card_assignment/engagement_card_assignment.json new file mode 100644 index 0000000..feab187 --- /dev/null +++ b/service_factory/service_factory/service_factory/doctype/engagement_card_assignment/engagement_card_assignment.json @@ -0,0 +1,71 @@ +{ + "actions": [], + "allow_rename": 0, + "autoname": "hash", + "creation": "2026-07-15 23:00:00.000000", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "employee", + "role", + "allocated_hours", + "logged_hours" + ], + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "role", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Role" + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "allocated_hours", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Allocated Hours" + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "logged_hours", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Logged Hours", + "read_only": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2026-07-15 23:00:00.000000", + "modified_by": "Administrator", + "module": "Service Factory", + "name": "Engagement Card Assignment", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 0 +} diff --git a/service_factory/service_factory/doctype/resource_center_member/resource_center_member.py b/service_factory/service_factory/service_factory/doctype/engagement_card_assignment/engagement_card_assignment.py similarity index 78% rename from service_factory/service_factory/doctype/resource_center_member/resource_center_member.py rename to service_factory/service_factory/service_factory/doctype/engagement_card_assignment/engagement_card_assignment.py index dc08087..0a9be70 100644 --- a/service_factory/service_factory/doctype/resource_center_member/resource_center_member.py +++ b/service_factory/service_factory/service_factory/doctype/engagement_card_assignment/engagement_card_assignment.py @@ -4,5 +4,5 @@ from frappe.model.document import Document -class ResourceCenterMember(Document): +class EngagementCardAssignment(Document): pass diff --git a/service_factory/service_factory/doctype/resource_center_member/__init__.py b/service_factory/service_factory/service_factory/doctype/engagement_card_subtask/__init__.py similarity index 100% rename from service_factory/service_factory/doctype/resource_center_member/__init__.py rename to service_factory/service_factory/service_factory/doctype/engagement_card_subtask/__init__.py diff --git a/service_factory/service_factory/service_factory/doctype/engagement_card_subtask/engagement_card_subtask.json b/service_factory/service_factory/service_factory/doctype/engagement_card_subtask/engagement_card_subtask.json new file mode 100644 index 0000000..7681be3 --- /dev/null +++ b/service_factory/service_factory/service_factory/doctype/engagement_card_subtask/engagement_card_subtask.json @@ -0,0 +1,72 @@ +{ + "actions": [], + "allow_rename": 0, + "autoname": "hash", + "creation": "2026-07-15 23:00:00.000000", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "subject", + "assigned_to", + "status", + "due_date" + ], + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "subject", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Subject", + "reqd": 1 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "assigned_to", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Assigned To", + "options": "Employee" + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "default": "Open", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "Open\nCompleted", + "reqd": 1 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "due_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Due Date" + } + ], + "istable": 1, + "links": [], + "modified": "2026-07-15 23:00:00.000000", + "modified_by": "Administrator", + "module": "Service Factory", + "name": "Engagement Card Subtask", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 0 +} diff --git a/service_factory/service_factory/doctype/engagement_recipe_step/engagement_recipe_step.py b/service_factory/service_factory/service_factory/doctype/engagement_card_subtask/engagement_card_subtask.py similarity index 79% rename from service_factory/service_factory/doctype/engagement_recipe_step/engagement_recipe_step.py rename to service_factory/service_factory/service_factory/doctype/engagement_card_subtask/engagement_card_subtask.py index 140ec90..69c4b2a 100644 --- a/service_factory/service_factory/doctype/engagement_recipe_step/engagement_recipe_step.py +++ b/service_factory/service_factory/service_factory/doctype/engagement_card_subtask/engagement_card_subtask.py @@ -4,5 +4,5 @@ from frappe.model.document import Document -class EngagementRecipeStep(Document): +class EngagementCardSubtask(Document): pass diff --git a/service_factory/service_factory/service_factory/doctype/engagement_card_time_log/__init__.py b/service_factory/service_factory/service_factory/doctype/engagement_card_time_log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service_factory/service_factory/service_factory/doctype/engagement_card_time_log/engagement_card_time_log.json b/service_factory/service_factory/service_factory/doctype/engagement_card_time_log/engagement_card_time_log.json new file mode 100644 index 0000000..f3c081e --- /dev/null +++ b/service_factory/service_factory/service_factory/doctype/engagement_card_time_log/engagement_card_time_log.json @@ -0,0 +1,72 @@ +{ + "actions": [], + "allow_rename": 0, + "autoname": "hash", + "creation": "2026-07-15 23:00:00.000000", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "employee", + "date", + "hours", + "description" + ], + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "default": "Today", + "fieldname": "date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Date", + "reqd": 1 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "hours", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Hours", + "reqd": 1 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Description" + } + ], + "istable": 1, + "links": [], + "modified": "2026-07-15 23:00:00.000000", + "modified_by": "Administrator", + "module": "Service Factory", + "name": "Engagement Card Time Log", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 0 +} diff --git a/service_factory/service_factory/doctype/execution_card_log/execution_card_log.py b/service_factory/service_factory/service_factory/doctype/engagement_card_time_log/engagement_card_time_log.py similarity index 79% rename from service_factory/service_factory/doctype/execution_card_log/execution_card_log.py rename to service_factory/service_factory/service_factory/doctype/engagement_card_time_log/engagement_card_time_log.py index f3d18df..54cd773 100644 --- a/service_factory/service_factory/doctype/execution_card_log/execution_card_log.py +++ b/service_factory/service_factory/service_factory/doctype/engagement_card_time_log/engagement_card_time_log.py @@ -4,5 +4,5 @@ from frappe.model.document import Document -class ExecutionCardLog(Document): +class EngagementCardTimeLog(Document): pass diff --git a/service_factory/service_factory/service_factory/workspace/engagement_board/engagement_board.json b/service_factory/service_factory/service_factory/workspace/engagement_board/engagement_board.json new file mode 100644 index 0000000..e58863f --- /dev/null +++ b/service_factory/service_factory/service_factory/workspace/engagement_board/engagement_board.json @@ -0,0 +1,106 @@ +{ + "charts": [], + "content": "[{\"id\":\"board_header_1\",\"type\":\"header\",\"data\":{\"text\":\"Engagement Board\",\"col\":12}},{\"id\":\"board_card_all\",\"type\":\"card\",\"data\":{\"card_name\":\"All Engagement Cards\",\"col\":4}},{\"id\":\"board_card_discovery\",\"type\":\"card\",\"data\":{\"card_name\":\"Discovery Cards\",\"col\":4}},{\"id\":\"board_card_delivery\",\"type\":\"card\",\"data\":{\"card_name\":\"Delivery Cards\",\"col\":4}},{\"id\":\"board_spacer_1\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"board_header_2\",\"type\":\"header\",\"data\":{\"text\":\"Quick Access\",\"col\":12}},{\"id\":\"board_shortcut_new\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"New Engagement Card\",\"col\":3}},{\"id\":\"board_shortcut_kpi\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"KPI Dashboard\",\"col\":3}}]", + "creation": "2026-07-15 23:00:00.000000", + "custom_blocks": [], + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "kanban", + "idx": 1, + "is_hidden": 0, + "label": "Engagement Board", + "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Engagement Cards", + "link_count": 2, + "link_type": "DocType", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Engagement Card", + "link_count": 0, + "link_to": "Engagement Card", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Kanban Board", + "link_count": 0, + "link_to": "Engagement Card", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Reporting", + "link_count": 1, + "link_type": "DocType", + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "KPI Dashboard", + "link_count": 0, + "link_to": "Engagement Card", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + } + ], + "modified": "2026-07-15 23:00:00.000000", + "modified_by": "Administrator", + "module": "Service Factory", + "name": "Engagement Board", + "number_cards": [], + "owner": "Administrator", + "parent_page": "", + "public": 1, + "quick_lists": [], + "roles": [], + "sequence_id": 1.0, + "shortcuts": [ + { + "action": "New", + "color": "Blue", + "doc_type": "Engagement Card", + "format": "{}", + "icon": "add", + "label": "New Engagement Card", + "link_to": "Engagement Card", + "stats_filter": "[]", + "type": "DocType" + }, + { + "color": "Green", + "doc_view": null, + "format": "{}", + "icon": "chart", + "kanban_board": null, + "label": "KPI Dashboard", + "link_to": "Engagement Card", + "link_type": "DocType", + "restrict_to_domain": null, + "stats_filter": "[]", + "type": "DocType", + "url": null + } + ], + "title": "Engagement Board", + "type": "Workspace" +} diff --git a/service_factory/service_factory/templates/__init__.py b/service_factory/service_factory/templates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service_factory/service_factory/templates/pages/__init__.py b/service_factory/service_factory/templates/pages/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service_factory/service_factory/workspace/director_workspace/director_workspace.json b/service_factory/service_factory/workspace/director_workspace/director_workspace.json deleted file mode 100644 index e520824..0000000 --- a/service_factory/service_factory/workspace/director_workspace/director_workspace.json +++ /dev/null @@ -1,170 +0,0 @@ -{ - "charts": [], - "content": "[{\"id\":\"director_header_1\",\"type\":\"header\",\"data\":{\"text\":\"Financial Margin Ledger\",\"col\":12}},{\"id\":\"director_card_margin\",\"type\":\"card\",\"data\":{\"card_name\":\"Margin Overview\",\"col\":6}},{\"id\":\"director_card_pnl\",\"type\":\"card\",\"data\":{\"card_name\":\"P&L Summary\",\"col\":6}},{\"id\":\"director_spacer_1\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"director_header_2\",\"type\":\"header\",\"data\":{\"text\":\"Delivery Performance\",\"col\":12}},{\"id\":\"director_card_efficiency\",\"type\":\"card\",\"data\":{\"card_name\":\"Delivery Efficiency\",\"col\":6}},{\"id\":\"director_card_burn\",\"type\":\"card\",\"data\":{\"card_name\":\"Resource Burn-Down\",\"col\":6}},{\"id\":\"director_spacer_2\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"director_header_3\",\"type\":\"header\",\"data\":{\"text\":\"Executive KPIs\",\"col\":12}},{\"id\":\"director_shortcut_revenue\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Revenue Pipeline\",\"col\":3}},{\"id\":\"director_shortcut_costs\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Delivery Costs\",\"col\":3}},{\"id\":\"director_shortcut_margin\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Gross Margin %\",\"col\":3}},{\"id\":\"director_shortcut_utilization\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Utilization Rate\",\"col\":3}}]", - "creation": "2026-07-15 19:00:00.000000", - "custom_blocks": [], - "docstatus": 0, - "doctype": "Workspace", - "for_user": "", - "hide_custom": 0, - "icon": "chart", - "idx": 1, - "is_hidden": 0, - "label": "Director Workspace", - "links": [ - { - "hidden": 0, - "is_query_report": 0, - "label": "Financial Overview", - "link_count": 3, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Engagement Order", - "link_count": 0, - "link_to": "Engagement Order", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Execution Card", - "link_count": 0, - "link_to": "Execution Card", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Resource Center", - "link_count": 0, - "link_to": "Resource Center", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Analytics", - "link_count": 4, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "All Orders", - "link_count": 0, - "link_to": "Engagement Order", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Completed Orders", - "link_count": 0, - "link_to": "Engagement Order", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Engagement Recipes", - "link_count": 0, - "link_to": "Engagement Recipe", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Submitted Cards", - "link_count": 0, - "link_to": "Execution Card", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - } - ], - "modified": "2026-07-15 19:00:00.000000", - "modified_by": "Administrator", - "module": "Service Factory", - "name": "Director Workspace", - "number_cards": [], - "owner": "Administrator", - "parent_page": "", - "public": 1, - "quick_lists": [], - "roles": [], - "sequence_id": 3.0, - "shortcuts": [ - { - "action": "List", - "color": "Green", - "doc_type": "Engagement Order", - "format": "{}", - "icon": "currency", - "label": "Revenue Pipeline", - "link_to": "Engagement Order", - "stats_filter": "[[\"Engagement Order\",\"docstatus\",\"=\",1],[\"Engagement Order\",\"status\",\"!=\",\"Cancelled\"]]", - "type": "DocType" - }, - { - "action": "List", - "color": "Red", - "doc_type": "Execution Card", - "format": "{}", - "icon": "cost", - "label": "Delivery Costs", - "link_to": "Execution Card", - "stats_filter": "[[\"Execution Card\",\"status\",\"=\",\"Submitted\"]]", - "type": "DocType" - }, - { - "action": "Report", - "color": "Blue", - "doc_type": "Engagement Order", - "format": "{}", - "icon": "percent", - "label": "Gross Margin %", - "link_to": "Engagement Order", - "stats_filter": "[]", - "type": "DocType" - }, - { - "action": "List", - "color": "Orange", - "doc_type": "Resource Center", - "format": "{}", - "icon": "chart", - "label": "Utilization Rate", - "link_to": "Resource Center", - "stats_filter": "[]", - "type": "DocType" - } - ], - "title": "Director Workspace", - "type": "Workspace" -} diff --git a/service_factory/service_factory/workspace/engineer_workspace/engineer_workspace.json b/service_factory/service_factory/workspace/engineer_workspace/engineer_workspace.json deleted file mode 100644 index 0cc5cdc..0000000 --- a/service_factory/service_factory/workspace/engineer_workspace/engineer_workspace.json +++ /dev/null @@ -1,146 +0,0 @@ -{ - "charts": [], - "content": "[{\"id\":\"engineer_header_1\",\"type\":\"header\",\"data\":{\"text\":\"My Active Deliverables\",\"col\":12}},{\"id\":\"engineer_card_my_cards\",\"type\":\"card\",\"data\":{\"card_name\":\"My Execution Cards\",\"col\":4}},{\"id\":\"engineer_card_orders\",\"type\":\"card\",\"data\":{\"card_name\":\"My Engagement Orders\",\"col\":4}},{\"id\":\"engineer_card_reports\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"engineer_spacer_1\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"engineer_header_2\",\"type\":\"header\",\"data\":{\"text\":\"Time Tracking\",\"col\":12}},{\"id\":\"engineer_card_timer\",\"type\":\"card\",\"data\":{\"card_name\":\"Execution Card Timer\",\"col\":6}},{\"id\":\"engineer_card_hours\",\"type\":\"card\",\"data\":{\"card_name\":\"My Hours This Week\",\"col\":6}},{\"id\":\"engineer_spacer_2\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"engineer_header_3\",\"type\":\"header\",\"data\":{\"text\":\"Quick Actions\",\"col\":12}},{\"id\":\"engineer_shortcut_new_card\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"New Execution Card\",\"col\":3}},{\"id\":\"engineer_shortcut_active\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"My Active Cards\",\"col\":3}}]", - "creation": "2026-07-15 19:00:00.000000", - "custom_blocks": [], - "docstatus": 0, - "doctype": "Workspace", - "for_user": "", - "hide_custom": 0, - "icon": "workflow", - "idx": 1, - "is_hidden": 0, - "label": "Engineer Workspace", - "links": [ - { - "hidden": 0, - "is_query_report": 0, - "label": "My Execution Cards", - "link_count": 3, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Execution Card", - "link_count": 0, - "link_to": "Execution Card", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "My Open Cards", - "link_count": 0, - "link_to": "Execution Card", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "My Submitted Cards", - "link_count": 0, - "link_to": "Execution Card", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "My Engagement Orders", - "link_count": 2, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Engagement Order", - "link_count": 0, - "link_to": "Engagement Order", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "My Active Orders", - "link_count": 0, - "link_to": "Engagement Order", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Reports", - "link_count": 1, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "My Hours", - "link_count": 0, - "link_to": "Execution Card", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - } - ], - "modified": "2026-07-15 19:00:00.000000", - "modified_by": "Administrator", - "module": "Service Factory", - "name": "Engineer Workspace", - "number_cards": [], - "owner": "Administrator", - "parent_page": "", - "public": 1, - "quick_lists": [], - "roles": [], - "sequence_id": 1.0, - "shortcuts": [ - { - "action": "New", - "color": "Blue", - "doc_type": "Execution Card", - "format": "{}", - "icon": "add", - "label": "New Execution Card", - "link_to": "Execution Card", - "stats_filter": "[]", - "type": "DocType" - }, - { - "action": "List", - "color": "Grey", - "doc_type": "Execution Card", - "format": "{}", - "icon": "list", - "label": "My Active Cards", - "link_to": "Execution Card", - "stats_filter": "[[\"Execution Card\",\"assigned_employee\",\"=\",\"__user_employee__\"],[\"Execution Card\",\"status\",\"in\",[\"Open\",\"Running\",\"Paused\"]]]", - "type": "DocType" - } - ], - "title": "Engineer Workspace", - "type": "Workspace" -} diff --git a/service_factory/service_factory/workspace/manager_workspace/manager_workspace.json b/service_factory/service_factory/workspace/manager_workspace/manager_workspace.json deleted file mode 100644 index f9f25d6..0000000 --- a/service_factory/service_factory/workspace/manager_workspace/manager_workspace.json +++ /dev/null @@ -1,221 +0,0 @@ -{ - "charts": [], - "content": "[{\"id\":\"manager_header_1\",\"type\":\"header\",\"data\":{\"text\":\"Operational Overview\",\"col\":12}},{\"id\":\"manager_card_orders\",\"type\":\"card\",\"data\":{\"card_name\":\"Engagement Orders\",\"col\":4}},{\"id\":\"manager_card_centers\",\"type\":\"card\",\"data\":{\"card_name\":\"Resource Centers\",\"col\":4}},{\"id\":\"manager_card_recipes\",\"type\":\"card\",\"data\":{\"card_name\":\"Engagement Recipes\",\"col\":4}},{\"id\":\"manager_spacer_1\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"manager_header_2\",\"type\":\"header\",\"data\":{\"text\":\"Resource Capacity\",\"col\":12}},{\"id\":\"manager_card_gantt\",\"type\":\"card\",\"data\":{\"card_name\":\"Schedule Gantt\",\"col\":6}},{\"id\":\"manager_card_alerts\",\"type\":\"card\",\"data\":{\"card_name\":\"Capacity Alerts\",\"col\":6}},{\"id\":\"manager_spacer_2\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"manager_header_3\",\"type\":\"header\",\"data\":{\"text\":\"Key Metrics\",\"col\":12}},{\"id\":\"manager_shortcut_active\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Active Engagements\",\"col\":3}},{\"id\":\"manager_shortcut_stalled\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stalled / Blocked\",\"col\":3}},{\"id\":\"manager_shortcut_capacity\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Capacity Overview\",\"col\":3}},{\"id\":\"manager_shortcut_executions\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"All Execution Cards\",\"col\":3}}]", - "creation": "2026-07-15 19:00:00.000000", - "custom_blocks": [], - "docstatus": 0, - "doctype": "Workspace", - "for_user": "", - "hide_custom": 0, - "icon": "share", - "idx": 1, - "is_hidden": 0, - "label": "Manager Workspace", - "links": [ - { - "hidden": 0, - "is_query_report": 0, - "label": "Engagement Orders", - "link_count": 4, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Engagement Order", - "link_count": 0, - "link_to": "Engagement Order", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Scheduled Orders", - "link_count": 0, - "link_to": "Engagement Order", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "In Progress Orders", - "link_count": 0, - "link_to": "Engagement Order", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Completed Orders", - "link_count": 0, - "link_to": "Engagement Order", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Resource Centers", - "link_count": 2, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Resource Center", - "link_count": 0, - "link_to": "Resource Center", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Resource Center Members", - "link_count": 0, - "link_to": "Resource Center Member", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Engagement Recipes", - "link_count": 2, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Engagement Recipe", - "link_count": 0, - "link_to": "Engagement Recipe", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Engagement Recipe Steps", - "link_count": 0, - "link_to": "Engagement Recipe Step", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Execution Tracking", - "link_count": 2, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Execution Card", - "link_count": 0, - "link_to": "Execution Card", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Execution Card Logs", - "link_count": 0, - "link_to": "Execution Card Log", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - } - ], - "modified": "2026-07-15 19:00:00.000000", - "modified_by": "Administrator", - "module": "Service Factory", - "name": "Manager Workspace", - "number_cards": [], - "owner": "Administrator", - "parent_page": "", - "public": 1, - "quick_lists": [], - "roles": [], - "sequence_id": 2.0, - "shortcuts": [ - { - "action": "List", - "color": "Blue", - "doc_type": "Engagement Order", - "format": "{}", - "icon": "list", - "label": "Active Engagements", - "link_to": "Engagement Order", - "stats_filter": "[[\"Engagement Order\",\"status\",\"in\",[\"Scheduled\",\"In Progress\"]]]", - "type": "DocType" - }, - { - "action": "List", - "color": "Red", - "doc_type": "Engagement Order", - "format": "{}", - "icon": "info", - "label": "Stalled / Blocked", - "link_to": "Engagement Order", - "stats_filter": "[[\"Engagement Order\",\"status\",\"=\",\"On Hold\"]]", - "type": "DocType" - }, - { - "action": "List", - "color": "Green", - "doc_type": "Resource Center", - "format": "{}", - "icon": "organization", - "label": "Capacity Overview", - "link_to": "Resource Center", - "stats_filter": "[]", - "type": "DocType" - }, - { - "action": "List", - "color": "Grey", - "doc_type": "Execution Card", - "format": "{}", - "icon": "check", - "label": "All Execution Cards", - "link_to": "Execution Card", - "stats_filter": "[]", - "type": "DocType" - } - ], - "title": "Manager Workspace", - "type": "Workspace" -}