chore: rebuild Service Factory with Engagement Card DocType and Kanban workspace
This commit is contained in:
parent
746fbf8d1c
commit
5102e6d4bb
64 changed files with 1697 additions and 3468 deletions
|
|
@ -1,6 +1,6 @@
|
|||
# Service Factory — ERPNext Custom App Project
|
||||
|
||||
> **Current Version: V0.1.0002** — Each completed prompt/step increments the patch number by 1 (V0.1.0000 → V0.1.0001 → V0.1.0002…). The agent automatically commits and pushes after every step.
|
||||
> **Current Version: V0.1.0003** — Each completed prompt/step increments the patch number by 1 (V0.1.0000 → V0.1.0001 → V0.1.0002…). The agent automatically commits and pushes after every step.
|
||||
|
||||
## Project Overview
|
||||
|
||||
|
|
|
|||
21
service_factory/.editorconfig
Normal file
21
service_factory/.editorconfig
Normal 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
124
service_factory/.eslintrc
Normal 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
55
service_factory/.gitignore
vendored
Normal 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*
|
||||
69
service_factory/.pre-commit-config.yaml
Normal file
69
service_factory/.pre-commit-config.yaml
Normal 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
33
service_factory/README.md
Normal 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
|
||||
52
service_factory/deploy/README.md
Normal file
52
service_factory/deploy/README.md
Normal 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`.
|
||||
6
service_factory/deploy/bench-env.sh
Executable file
6
service_factory/deploy/bench-env.sh
Executable 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 "$@"
|
||||
42
service_factory/deploy/frappe-bench
Normal file
42
service_factory/deploy/frappe-bench
Normal 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;
|
||||
}
|
||||
}
|
||||
19
service_factory/deploy/frappe-bench-schedule.service
Normal file
19
service_factory/deploy/frappe-bench-schedule.service
Normal 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
|
||||
19
service_factory/deploy/frappe-bench-socketio.service
Normal file
19
service_factory/deploy/frappe-bench-socketio.service
Normal 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
|
||||
19
service_factory/deploy/frappe-bench-watch.service
Normal file
19
service_factory/deploy/frappe-bench-watch.service
Normal 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
|
||||
19
service_factory/deploy/frappe-bench-web.service
Normal file
19
service_factory/deploy/frappe-bench-web.service
Normal 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
|
||||
19
service_factory/deploy/frappe-bench-worker.service
Normal file
19
service_factory/deploy/frappe-bench-worker.service
Normal 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
|
||||
9
service_factory/deploy/frappe-bench.target
Normal file
9
service_factory/deploy/frappe-bench.target
Normal 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
|
||||
21
service_factory/license.txt
Normal file
21
service_factory/license.txt
Normal 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.
|
||||
65
service_factory/pyproject.toml
Normal file
65
service_factory/pyproject.toml
Normal 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
|
||||
|
|
@ -0,0 +1 @@
|
|||
__version__ = "0.0.1"
|
||||
12
service_factory/service_factory/api/permission.py
Normal file
12
service_factory/service_factory/api/permission.py
Normal 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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
264
service_factory/service_factory/hooks.py
Normal file
264
service_factory/service_factory/hooks.py
Normal 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 = []
|
||||
|
||||
1
service_factory/service_factory/modules.txt
Normal file
1
service_factory/service_factory/modules.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
Service Factory
|
||||
6
service_factory/service_factory/patches.txt
Normal file
6
service_factory/service_factory/patches.txt
Normal 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
|
||||
|
|
@ -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 |
|
|
@ -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");
|
||||
},
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -4,5 +4,5 @@
|
|||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class EngagementOrderRunStep(Document):
|
||||
class EngagementCardActivity(Document):
|
||||
pass
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -4,5 +4,5 @@
|
|||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ResourceCenterMember(Document):
|
||||
class EngagementCardAssignment(Document):
|
||||
pass
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -4,5 +4,5 @@
|
|||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class EngagementRecipeStep(Document):
|
||||
class EngagementCardSubtask(Document):
|
||||
pass
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -4,5 +4,5 @@
|
|||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ExecutionCardLog(Document):
|
||||
class EngagementCardTimeLog(Document):
|
||||
pass
|
||||
|
|
@ -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"
|
||||
}
|
||||
0
service_factory/service_factory/templates/__init__.py
Normal file
0
service_factory/service_factory/templates/__init__.py
Normal 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&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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
Loading…
Reference in a new issue