chore: rebuild Service Factory with Engagement Card DocType and Kanban workspace

This commit is contained in:
root 2026-07-15 23:22:32 +00:00
parent 746fbf8d1c
commit 5102e6d4bb
64 changed files with 1697 additions and 3468 deletions

View file

@ -1,6 +1,6 @@
# Service Factory — ERPNext Custom App Project # 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 ## Project Overview

View file

@ -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

124
service_factory/.eslintrc Normal file
View file

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

55
service_factory/.gitignore vendored Normal file
View file

@ -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*

View file

@ -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

33
service_factory/README.md Normal file
View file

@ -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

View file

@ -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`.

View file

@ -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 "$@"

View file

@ -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;
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -0,0 +1 @@
__version__ = "0.0.1"

View file

@ -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

View file

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

View file

@ -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")

View file

@ -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

View file

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

View file

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

View file

@ -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

View file

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

View file

@ -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 = $(`
<div class="execution-card-timer">
<div class="row">
<div class="col-md-12 text-center" style="padding: 15px 0;">
<div style="font-size: 14px; color: #666;"> ${__("Elapsed Time")}</div>
<div class="timer-display" style="font-size: 48px; font-weight: bold; font-family: monospace; color: #2490ef;">
00:00:00
</div>
</div>
</div>
</div>
`);
// 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")
);
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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

View file

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

View file

@ -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 <head>
# ------------------
# 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 = []

View file

@ -0,0 +1 @@
Service Factory

View file

@ -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

View file

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
<rect width="48" height="48" rx="8" fill="#2490ef"/>
<g fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round">
<path d="M10 16h28l-4 8H14l-4-8z"/>
<path d="M14 24h20l-3 6H17l-3-6z"/>
<path d="M18 30h12l-2 4H20l-2-4z"/>
<circle cx="24" cy="12" r="2" fill="#fff" stroke="none"/>
</g>
<text x="24" y="42" text-anchor="middle" fill="#fff" font-size="6" font-family="sans-serif">SF</text>
</svg>

After

Width:  |  Height:  |  Size: 511 B

View file

@ -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");
},
});

View file

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

View file

@ -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,
}

View file

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

View file

@ -4,5 +4,5 @@
from frappe.model.document import Document from frappe.model.document import Document
class EngagementOrderRunStep(Document): class EngagementCardActivity(Document):
pass pass

View file

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

View file

@ -4,5 +4,5 @@
from frappe.model.document import Document from frappe.model.document import Document
class ResourceCenterMember(Document): class EngagementCardAssignment(Document):
pass pass

View file

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

View file

@ -4,5 +4,5 @@
from frappe.model.document import Document from frappe.model.document import Document
class EngagementRecipeStep(Document): class EngagementCardSubtask(Document):
pass pass

View file

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

View file

@ -4,5 +4,5 @@
from frappe.model.document import Document from frappe.model.document import Document
class ExecutionCardLog(Document): class EngagementCardTimeLog(Document):
pass pass

View file

@ -0,0 +1,106 @@
{
"charts": [],
"content": "[{\"id\":\"board_header_1\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Engagement Board</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Quick Access</b></span>\",\"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"
}

View file

@ -1,170 +0,0 @@
{
"charts": [],
"content": "[{\"id\":\"director_header_1\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Financial Margin Ledger</b></span>\",\"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&amp;L Summary\",\"col\":6}},{\"id\":\"director_spacer_1\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"director_header_2\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Delivery Performance</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Executive KPIs</b></span>\",\"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"
}

View file

@ -1,146 +0,0 @@
{
"charts": [],
"content": "[{\"id\":\"engineer_header_1\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>My Active Deliverables</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Time Tracking</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Quick Actions</b></span>\",\"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"
}

View file

@ -1,221 +0,0 @@
{
"charts": [],
"content": "[{\"id\":\"manager_header_1\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Operational Overview</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Resource Capacity</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Key Metrics</b></span>\",\"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"
}