codymaster 4.6.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/CHANGELOG.md +74 -8
  2. package/README.md +192 -95
  3. package/dist/advisory-handoff.js +89 -0
  4. package/dist/advisory-report.js +105 -0
  5. package/dist/browse-server.js +251 -0
  6. package/dist/cli/command-registry.js +34 -0
  7. package/dist/cli/commands/agent.js +120 -0
  8. package/dist/cli/commands/bench.js +69 -0
  9. package/dist/cli/commands/brain.js +108 -0
  10. package/dist/cli/commands/dashboard.js +93 -0
  11. package/dist/cli/commands/design-studio.js +111 -0
  12. package/dist/cli/commands/distro.js +25 -0
  13. package/dist/cli/commands/engineering.js +596 -0
  14. package/dist/cli/commands/evolve.js +123 -0
  15. package/dist/cli/commands/mcp-serve.js +104 -0
  16. package/dist/cli/commands/project.js +324 -0
  17. package/dist/cli/commands/skill-chain.js +269 -0
  18. package/dist/cli/commands/system.js +89 -0
  19. package/dist/cli/commands/task.js +254 -0
  20. package/dist/cli/update-check.js +83 -0
  21. package/dist/cm-config.js +92 -0
  22. package/dist/cm-suggest.js +77 -0
  23. package/dist/codybench/judges/automated.js +31 -0
  24. package/dist/codybench/runners/claude-code.js +32 -0
  25. package/dist/codybench/suites/memory-retention.js +85 -0
  26. package/dist/codybench/suites/tdd-regression.js +35 -0
  27. package/dist/codybench/suites/token-efficiency.js +55 -0
  28. package/dist/codybench/types.js +2 -0
  29. package/dist/context-db.js +157 -0
  30. package/dist/continuity.js +2 -6
  31. package/dist/distro-validate.js +54 -0
  32. package/dist/execution-analyzer.js +138 -0
  33. package/dist/guardian-core.js +74 -0
  34. package/dist/index.js +36 -2759
  35. package/dist/indexer/skills-lib.js +533 -0
  36. package/dist/indexer/skills-map.js +1374 -0
  37. package/dist/indexer/skills.js +16 -0
  38. package/dist/learning-promoter.js +246 -0
  39. package/dist/mcp-context-server.js +289 -1
  40. package/dist/mcp-skills-tools.js +81 -0
  41. package/dist/retro-summary.js +70 -0
  42. package/dist/second-opinion-providers.js +79 -0
  43. package/dist/skill-chain.js +63 -1
  44. package/dist/skill-evolver.js +456 -0
  45. package/dist/skill-execution-cache.js +254 -0
  46. package/dist/smart-brain-router.js +184 -0
  47. package/dist/sprint-pipeline.js +228 -0
  48. package/dist/storage-backend.js +14 -67
  49. package/dist/token-budget.js +88 -0
  50. package/dist/utils/cli-utils.js +76 -0
  51. package/dist/utils/skill-utils.js +32 -0
  52. package/package.json +17 -7
  53. package/scripts/build-skills.mjs +51 -0
  54. package/scripts/gate-0-repo-hygiene.js +75 -0
  55. package/scripts/postinstall.js +34 -28
  56. package/scripts/security-scan.js +1 -1
  57. package/scripts/validate-skills.mjs +42 -0
  58. package/skills/CLAUDE.md +2 -7
  59. package/skills/_shared/helpers.md +2 -8
  60. package/skills/cm-ads-tracker/SKILL.md +3 -6
  61. package/skills/cm-browse/SKILL.md +34 -0
  62. package/skills/cm-conductor-worktrees/SKILL.md +28 -0
  63. package/skills/cm-content-factory/SKILL.md +1 -1
  64. package/skills/cm-content-factory/landing/docs/content/changelog.md +36 -0
  65. package/skills/cm-content-factory/landing/docs/content/deployment.md +46 -0
  66. package/skills/cm-content-factory/landing/docs/content/execution-flow.md +67 -0
  67. package/skills/cm-content-factory/landing/docs/content/memory-system.md +38 -0
  68. package/skills/cm-content-factory/landing/docs/content/openspace.md +27 -0
  69. package/skills/cm-content-factory/landing/docs/content/use-cases.md +26 -0
  70. package/skills/cm-content-factory/landing/docs/content/v5-intro.md +28 -0
  71. package/skills/cm-content-factory/landing/docs/index.html +240 -0
  72. package/skills/cm-content-factory/landing/index.html +100 -100
  73. package/skills/cm-content-factory/landing/script.js +42 -0
  74. package/skills/cm-content-factory/landing/translations.js +400 -400
  75. package/skills/cm-continuity/SKILL.md +32 -33
  76. package/skills/cm-design-studio/SKILL.md +34 -0
  77. package/skills/cm-ecosystem-roadmap/SKILL.md +15 -0
  78. package/skills/cm-engineering-meta/SKILL.md +73 -0
  79. package/skills/cm-growth-hacking/SKILL.md +1 -12
  80. package/skills/cm-guardian-runtime/SKILL.md +26 -0
  81. package/skills/cm-mcp-engineering/SKILL.md +22 -0
  82. package/skills/cm-notebooklm/SKILL.md +1 -17
  83. package/skills/cm-post-deploy-canary/SKILL.md +22 -0
  84. package/skills/cm-project-bootstrap/SKILL.md +11 -0
  85. package/skills/cm-qa-visual-cli/SKILL.md +22 -0
  86. package/skills/cm-retro-cli/SKILL.md +23 -0
  87. package/skills/cm-second-opinion-cli/SKILL.md +23 -0
  88. package/skills/cm-secret-shield/SKILL.md +2 -2
  89. package/skills/cm-security-gate/SKILL.md +1 -0
  90. package/skills/cm-skill-chain/SKILL.md +25 -4
  91. package/skills/cm-skill-evolution/SKILL.md +83 -0
  92. package/skills/cm-skill-health/SKILL.md +83 -0
  93. package/skills/cm-skill-index/SKILL.md +11 -3
  94. package/skills/cm-skill-search/SKILL.md +49 -0
  95. package/skills/cm-skill-share/SKILL.md +58 -0
  96. package/skills/cm-sprint-bus/SKILL.md +33 -0
  97. package/skills/cm-start/SKILL.md +0 -10
  98. package/skills/cm-tdd/SKILL.md +59 -72
  99. package/skills/profiles/README.md +21 -0
  100. package/skills/profiles/core.txt +23 -0
  101. package/skills/profiles/design.txt +6 -0
  102. package/skills/profiles/full.txt +62 -0
  103. package/skills/profiles/growth.txt +10 -0
  104. package/skills/profiles/knowledge.txt +7 -0
  105. package/install.sh +0 -901
  106. package/scripts/test-gemini.js +0 -13
  107. package/skills/cm-frappe-agent/SKILL.md +0 -134
  108. package/skills/cm-frappe-agent/agents/doctype-architect.md +0 -596
  109. package/skills/cm-frappe-agent/agents/erpnext-customizer.md +0 -643
  110. package/skills/cm-frappe-agent/agents/frappe-backend.md +0 -814
  111. package/skills/cm-frappe-agent/agents/frappe-custom-frontend.md +0 -557
  112. package/skills/cm-frappe-agent/agents/frappe-debugger.md +0 -625
  113. package/skills/cm-frappe-agent/agents/frappe-fixer.md +0 -275
  114. package/skills/cm-frappe-agent/agents/frappe-frontend.md +0 -660
  115. package/skills/cm-frappe-agent/agents/frappe-installer.md +0 -158
  116. package/skills/cm-frappe-agent/agents/frappe-performance.md +0 -307
  117. package/skills/cm-frappe-agent/agents/frappe-planner.md +0 -419
  118. package/skills/cm-frappe-agent/agents/frappe-remote-ops.md +0 -153
  119. package/skills/cm-frappe-agent/agents/github-workflow.md +0 -286
  120. package/skills/cm-frappe-agent/commands/frappe-app.md +0 -351
  121. package/skills/cm-frappe-agent/commands/frappe-backend.md +0 -162
  122. package/skills/cm-frappe-agent/commands/frappe-bench.md +0 -254
  123. package/skills/cm-frappe-agent/commands/frappe-debug.md +0 -263
  124. package/skills/cm-frappe-agent/commands/frappe-doctype-create.md +0 -272
  125. package/skills/cm-frappe-agent/commands/frappe-doctype-field.md +0 -310
  126. package/skills/cm-frappe-agent/commands/frappe-erpnext.md +0 -210
  127. package/skills/cm-frappe-agent/commands/frappe-fix.md +0 -59
  128. package/skills/cm-frappe-agent/commands/frappe-frontend.md +0 -210
  129. package/skills/cm-frappe-agent/commands/frappe-fullstack.md +0 -243
  130. package/skills/cm-frappe-agent/commands/frappe-github.md +0 -57
  131. package/skills/cm-frappe-agent/commands/frappe-install.md +0 -52
  132. package/skills/cm-frappe-agent/commands/frappe-plan.md +0 -442
  133. package/skills/cm-frappe-agent/commands/frappe-remote.md +0 -58
  134. package/skills/cm-frappe-agent/commands/frappe-test.md +0 -356
  135. package/skills/cm-frappe-agent/docs/README.md +0 -51
  136. package/skills/cm-frappe-agent/docs/agents-catalog.md +0 -113
  137. package/skills/cm-frappe-agent/docs/architecture.md +0 -149
  138. package/skills/cm-frappe-agent/docs/commands-catalog.md +0 -82
  139. package/skills/cm-frappe-agent/docs/resources-catalog.md +0 -66
  140. package/skills/cm-frappe-agent/docs/sitemap-urls.txt +0 -52
  141. package/skills/cm-frappe-agent/docs/sitemap.md +0 -81
  142. package/skills/cm-frappe-agent/docs/sop/user-guide.md +0 -178
  143. package/skills/cm-frappe-agent/docs/sop/vibe-coding-guide.md +0 -122
  144. package/skills/cm-frappe-agent/resources/7-layer-architecture.md +0 -985
  145. package/skills/cm-frappe-agent/resources/bench_commands.md +0 -73
  146. package/skills/cm-frappe-agent/resources/code-patterns-guide.md +0 -948
  147. package/skills/cm-frappe-agent/resources/common_pitfalls.md +0 -266
  148. package/skills/cm-frappe-agent/resources/doctype-registry.md +0 -158
  149. package/skills/cm-frappe-agent/resources/installation-guide.md +0 -289
  150. package/skills/cm-frappe-agent/resources/rest-api-patterns.md +0 -182
  151. package/skills/cm-frappe-agent/resources/scaffold_checklist.md +0 -82
  152. package/skills/cm-frappe-agent/resources/upgrade_patterns.md +0 -113
  153. package/skills/cm-frappe-agent/resources/web-form-patterns.md +0 -252
  154. package/skills/cm-frappe-agent/skills/bench-commands/SKILL.md +0 -621
  155. package/skills/cm-frappe-agent/skills/client-scripts/SKILL.md +0 -642
  156. package/skills/cm-frappe-agent/skills/doctype-patterns/SKILL.md +0 -576
  157. package/skills/cm-frappe-agent/skills/frappe-api/SKILL.md +0 -740
  158. package/skills/cm-frappe-agent/skills/remote-operations/SKILL.md +0 -47
  159. package/skills/cm-frappe-agent/skills/server-scripts/SKILL.md +0 -608
  160. package/skills/cm-frappe-agent/skills/web-forms/SKILL.md +0 -46
  161. package/skills/frappe-app-builder.zip +0 -0
@@ -1,985 +0,0 @@
1
- ---
2
- name: frappe-app-builder
3
- description: |
4
- Build, upgrade, and maintain production-ready Frappe custom apps from scratch.
5
- Covers the full lifecycle: project scaffold, DocType design, engine architecture,
6
- API endpoints, scheduler tasks, hooks, workflows, reports, client-side JS,
7
- testing, and deployment. Synthesized from real production experience.
8
-
9
- ALWAYS trigger for: frappe app, frappe doctype, bench, erpnext, frappe hooks,
10
- frappe scheduler, frappe workflow, frappe report, frappe api, frappe controller,
11
- "create frappe app", "write doctype", "frappe custom app", "bench migrate",
12
- "upgrade frappe", "frappe engine", "frappe webhook", "frappe client script",
13
- "frappe server script", "frappe test", "improve frappe", "frappe permission",
14
- "frappe fixture", "frappe workspace", "doc_events"
15
- ---
16
-
17
- # Goal
18
-
19
- Build production-ready Frappe custom apps that are modular, testable, and
20
- maintainable. Every app follows the same battle-tested architecture: separate
21
- business logic engines from Frappe ORM, expose clean APIs, schedule tasks,
22
- test pure-logic without Frappe, and use hooks/fixtures for portability.
23
-
24
- ---
25
-
26
- # Instructions
27
-
28
- ## 🏗️ Architecture — The 7-Layer Frappe App
29
-
30
- Every non-trivial Frappe app should follow this layered architecture:
31
-
32
- ```
33
- my_app/
34
- ├── my_app/
35
- │ └── doctype/ # Layer 1: Data Model (DocType JSON + controllers)
36
- │ ├── my_doctype/
37
- │ │ ├── my_doctype.json
38
- │ │ ├── my_doctype.py # Server controller
39
- │ │ └── my_doctype.js # Client controller
40
- │ └── ...
41
- ├── engines/ # Layer 2: Business Logic (PURE Python, no Frappe in core)
42
- │ ├── scoring_engine.py
43
- │ └── benefit_engine.py
44
- ├── api/ # Layer 3: API Endpoints (@frappe.whitelist)
45
- │ ├── external.py # Webhooks / external system integration
46
- │ └── internal.py # Internal UI-facing APIs
47
- ├── tasks/ # Layer 4: Scheduled Tasks (daily/weekly/monthly/cron)
48
- │ ├── daily.py
49
- │ ├── weekly.py
50
- │ └── monthly.py
51
- ├── setup/ # Layer 5: Install & Migrate hooks
52
- │ └── install.py
53
- ├── tests/ # Layer 6: Tests (standalone, no Frappe needed for pure logic)
54
- │ ├── test_engine.py
55
- │ └── test_benefit.py
56
- ├── public/js/ # Layer 7: Shared Client JS (badges, utilities, list views)
57
- │ └── my_app.js
58
- ├── hooks.py # The nervous system — connects everything
59
- ├── fixtures/ # Exported Roles, Workflows, Custom Fields
60
- └── modules.txt
61
- ```
62
-
63
- ### Why This Architecture?
64
-
65
- | Layer | Why Separate? |
66
- |-------|--------------|
67
- | **DocType** | Frappe manages schema via JSON. Controllers handle validate/submit/cancel |
68
- | **Engines** | Pure logic = testable without Frappe instance. Import in API, tasks, controllers |
69
- | **API** | Clean separation of external webhooks vs internal UI calls |
70
- | **Tasks** | Scheduler events are simple wrappers calling engine functions |
71
- | **Setup** | Idempotent install/migrate = safe `bench migrate` every time |
72
- | **Tests** | Pure-logic tests run with `pytest -v`, no bench needed |
73
- | **Shared JS** | Global utilities, badges, list view settings, reusable across all DocTypes |
74
-
75
- ---
76
-
77
- ## 📐 Layer 1: DocType Design
78
-
79
- ### Controller Pattern (Python)
80
-
81
- ```python
82
- import frappe
83
- from frappe import _
84
- from frappe.model.document import Document
85
- from frappe.utils import now
86
-
87
-
88
- class WarehouseViolation(Document):
89
- def validate(self):
90
- """Auto-fill derived fields BEFORE save."""
91
- self.fetch_employee_details()
92
- self.set_period()
93
- self.fetch_penalty_from_type()
94
-
95
- def fetch_employee_details(self):
96
- """Pull company, branch, name from Employee — avoid manual entry."""
97
- if self.employee:
98
- emp = frappe.db.get_value(
99
- "Employee", self.employee,
100
- ["company", "branch", "employee_name"], as_dict=True,
101
- )
102
- if emp:
103
- self.company = emp.company
104
- self.branch = emp.branch
105
- self.employee_name = emp.employee_name
106
-
107
- def set_period(self):
108
- """Auto-derive period (YYYY-MM) from date field."""
109
- if self.date:
110
- self.period = str(self.date)[:7]
111
-
112
- def fetch_penalty_from_type(self):
113
- """Auto-fill penalty points from master data if not manually set."""
114
- if self.violation_type and not self.penalty_points:
115
- pts = frappe.db.get_value(
116
- "Warehouse Violation Type", self.violation_type, "penalty_points"
117
- )
118
- if pts:
119
- self.penalty_points = pts
120
-
121
- def on_submit(self):
122
- """Record who confirmed and when."""
123
- self.confirmed_by = frappe.session.user
124
- self.confirmed_at = now()
125
- self.db_update()
126
- frappe.msgprint(
127
- _("Confirmed. Penalty: {0} pts for {1}.").format(
128
- self.penalty_points, self.employee_name
129
- ),
130
- indicator="orange", alert=True,
131
- )
132
-
133
- def on_cancel(self):
134
- frappe.msgprint(
135
- _("Cancelled for {0}.").format(self.employee_name), alert=True
136
- )
137
- ```
138
-
139
- **Key patterns:**
140
- - `validate()` = auto-fill derived fields (period from date, employee details from Link)
141
- - `on_submit()` = record audit trail (who, when), show user feedback
142
- - `on_cancel()` = cleanup or notify
143
- - Use `self.db_update()` after modifying fields in `on_submit` (doc already saved)
144
- - Use `frappe.db.get_value()` for single-field lookups (fast, no full doc load)
145
-
146
- ### Controller Pattern (JavaScript)
147
-
148
- ```javascript
149
- frappe.ui.form.on("Warehouse Violation", {
150
- refresh(frm) {
151
- // Color-coded workflow state indicator
152
- if (frm.doc.workflow_state) {
153
- const colors = {
154
- "Pending": "orange", "Confirmed": "green",
155
- "Appealed": "blue", "Waived": "red"
156
- };
157
- frm.page.set_indicator(
158
- frm.doc.workflow_state,
159
- colors[frm.doc.workflow_state] || "gray"
160
- );
161
- }
162
-
163
- // Dashboard indicators for key metrics
164
- if (frm.doc.penalty_points) {
165
- frm.dashboard.add_indicator(
166
- __("Penalty: {0} pts", [frm.doc.penalty_points]),
167
- frm.doc.penalty_points >= 8 ? "red" :
168
- frm.doc.penalty_points >= 5 ? "orange" : "blue"
169
- );
170
- }
171
-
172
- // Custom action buttons (non-submitted docs only)
173
- if (!frm.is_new() && frm.doc.docstatus === 0) {
174
- frm.add_custom_button(__("View Dashboard"), function () {
175
- frappe.set_route("query-report", "Employee Summary", {
176
- period: frm.doc.period
177
- });
178
- }, __("Actions"));
179
- }
180
- },
181
-
182
- date(frm) {
183
- // Auto-fill period from date
184
- boxme.autoSetPeriod(frm, "date", "period");
185
- },
186
-
187
- violation_type(frm) {
188
- // Auto-fill penalty from master data
189
- if (frm.doc.violation_type) {
190
- frappe.db.get_value(
191
- "Warehouse Violation Type",
192
- frm.doc.violation_type,
193
- "penalty_points",
194
- (r) => {
195
- if (r && r.penalty_points) {
196
- frm.set_value("penalty_points", r.penalty_points);
197
- }
198
- }
199
- );
200
- }
201
- }
202
- });
203
- ```
204
-
205
- ### DocType Design Rules
206
-
207
- ```
208
- ✅ DO:
209
- - Use Link fields to reference other DocTypes (Employee, Company, Branch)
210
- - Add "period" field (Data, YYYY-MM) auto-derived from date
211
- - Use Submittable DocTypes for records that need approval workflows
212
- - Set "module" in DocType JSON to your app module name
213
- - Add naming_rule or autoname for meaningful document names
214
-
215
- ❌ DON'T:
216
- - Hardcode company/branch/employee names — always use Link fields
217
- - Put business logic in DocType controllers — use engines/ instead
218
- - Forget to add "module" property — makes fixtures export fail
219
- - Create DocType without think about workflow states first
220
- ```
221
-
222
- ---
223
-
224
- ## ⚙️ Layer 2: Business Logic Engines
225
-
226
- **The most important pattern.** Separate pure logic from Frappe ORM.
227
-
228
- ### Pure-Logic Functions (No Frappe Imports)
229
-
230
- ```python
231
- """
232
- All pure-logic functions have NO frappe DB calls.
233
- Safe to unit-test without a running Frappe instance.
234
- """
235
- PPH_LEVEL_ORDER = ["needs_improvement", "average", "good", "excellent"]
236
-
237
- _DEFAULT_THRESHOLDS = [
238
- {"level": "needs_improvement", "min_pph": 0, "max_pph": 35},
239
- {"level": "average", "min_pph": 35, "max_pph": 55},
240
- {"level": "good", "min_pph": 55, "max_pph": 75},
241
- {"level": "excellent", "min_pph": 75, "max_pph": None},
242
- ]
243
-
244
- def classify_pph_level(pph: float, thresholds: list) -> str:
245
- """Classify using half-open intervals [min, max). Falls back to defaults."""
246
- if not thresholds:
247
- thresholds = _DEFAULT_THRESHOLDS
248
- for t in sorted(thresholds, key=lambda x: x["min_pph"]):
249
- min_v = float(t["min_pph"])
250
- max_v = t.get("max_pph")
251
- if pph >= min_v and (max_v is None or pph < float(max_v)):
252
- return t["level"]
253
- return thresholds[-1]["level"]
254
-
255
- def pph_level_gte(level: str, min_level: str) -> bool:
256
- """True if level >= min_level in ordering."""
257
- return PPH_LEVEL_ORDER.index(level) >= PPH_LEVEL_ORDER.index(min_level)
258
- ```
259
-
260
- ### Config Cascade Pattern (Frappe DB)
261
-
262
- ```python
263
- def get_config_with_cascade(
264
- company: str, branch: str = None, designation: str = None,
265
- as_of_date: str = None,
266
- ) -> list:
267
- """
268
- Fetch config with specificity cascade:
269
- 1. Company + Branch + Designation (most specific)
270
- 2. Company + Branch
271
- 3. Company only
272
- Returns the most specific matching rows.
273
- """
274
- if not as_of_date:
275
- as_of_date = frappe.utils.today()
276
-
277
- date_filter = """
278
- AND effective_from <= %(date)s
279
- AND (effective_to IS NULL OR effective_to = '' OR effective_to >= %(date)s)
280
- AND is_active = 1
281
- """
282
-
283
- for b, d in [(branch, designation), (branch, None), (None, None)]:
284
- conds = "company = %(company)s"
285
- params = {"company": company, "date": as_of_date}
286
-
287
- if b:
288
- conds += " AND branch = %(branch)s"
289
- params["branch"] = b
290
- else:
291
- conds += " AND (branch IS NULL OR branch = '')"
292
-
293
- if d:
294
- conds += " AND designation = %(designation)s"
295
- params["designation"] = d
296
- else:
297
- conds += " AND (designation IS NULL OR designation = '')"
298
-
299
- rows = frappe.db.sql(
300
- f"SELECT * FROM `tabConfig DocType` WHERE {conds} {date_filter}",
301
- params, as_dict=True,
302
- )
303
- if rows:
304
- return rows
305
- return []
306
- ```
307
-
308
- ### Main Pipeline Pattern
309
-
310
- ```python
311
- def calculate_period_score(employee: str, period_type: str, period_value: str) -> dict:
312
- """
313
- Full scoring pipeline for one employee over a period.
314
- Returns a dict matching the target DocType fields.
315
- """
316
- start_date, end_date = get_period_date_range(period_type, period_value)
317
-
318
- emp = frappe.db.get_value("Employee", employee,
319
- ["company", "branch", "designation"], as_dict=True)
320
- if not emp:
321
- frappe.throw(f"Employee {employee!r} not found")
322
-
323
- # Step 1: Aggregate raw data
324
- raw_data = frappe.db.sql("""
325
- SELECT value, hours FROM `tabRaw Data`
326
- WHERE employee = %(emp)s AND date BETWEEN %(s)s AND %(e)s
327
- """, {"emp": employee, "s": start_date, "e": end_date}, as_dict=True)
328
-
329
- # Step 2: Classify using pure-logic functions
330
- avg_value = statistics.mean([r.value for r in raw_data]) if raw_data else 0
331
- level = classify_level(avg_value, get_config(emp.company, emp.branch))
332
-
333
- # Step 3: Aggregate related documents (submitted only = docstatus=1)
334
- penalties = frappe.db.sql("""
335
- SELECT COALESCE(SUM(points), 0) AS total
336
- FROM `tabPenalty` WHERE employee = %(emp)s AND period = %(p)s AND docstatus = 1
337
- """, {"emp": employee, "p": period_value[:7]}, as_dict=True)
338
-
339
- # Step 4: Return structured result matching DocType fields
340
- return {
341
- "employee": employee,
342
- "period_type": period_type,
343
- "period_value": period_value,
344
- "company": emp.company,
345
- "avg_value": avg_value,
346
- "level": level,
347
- "total_penalty": penalties[0].total if penalties else 0,
348
- }
349
- ```
350
-
351
- **Key patterns:**
352
- - Separate pure functions (classify, compare) from DB functions (fetch, aggregate)
353
- - Always filter by `docstatus = 1` for submitted documents
354
- - Use `COALESCE(SUM(...), 0)` to avoid NULL results
355
- - Return dicts that match target DocType field names exactly
356
-
357
- ---
358
-
359
- ## 🌐 Layer 3: API Endpoints
360
-
361
- ### External Webhook Pattern (WMS/ERP Integration)
362
-
363
- ```python
364
- @frappe.whitelist(allow_guest=False)
365
- def receive_data(data):
366
- """
367
- POST /api/method/my_app.api.external.receive_data
368
- Body: {"data": [{...}, {...}]}
369
- Auth: token api_key:api_secret
370
- """
371
- if isinstance(data, str):
372
- data = json.loads(data)
373
-
374
- created = skipped = 0
375
- errors = []
376
-
377
- for rec in data:
378
- emp_code = rec.get("employee_code")
379
- try:
380
- employee = frappe.db.get_value(
381
- "Employee", {"employee_number": emp_code}, "name"
382
- )
383
- if not employee:
384
- errors.append({"code": emp_code, "error": f"Not found"})
385
- skipped += 1
386
- continue
387
-
388
- # Deduplicate: skip if already exists
389
- if frappe.db.exists("My DocType", {"employee": employee, "date": rec["date"]}):
390
- skipped += 1
391
- continue
392
-
393
- doc = frappe.get_doc({
394
- "doctype": "My DocType",
395
- "employee": employee,
396
- "date": rec.get("date"),
397
- "value": rec.get("value", 0),
398
- "source": "external_system",
399
- })
400
- doc.insert(ignore_permissions=True)
401
- created += 1
402
-
403
- except Exception as exc:
404
- errors.append({"code": emp_code, "error": str(exc)})
405
- skipped += 1
406
- frappe.log_error(str(exc), "receive_data")
407
-
408
- frappe.db.commit()
409
- return {"status": "ok" if not errors else "partial",
410
- "created": created, "skipped": skipped, "errors": errors}
411
- ```
412
-
413
- ### Internal API Pattern (UI + Dashboard)
414
-
415
- ```python
416
- @frappe.whitelist()
417
- def calculate_for_employee(employee, period_type, period_value, overwrite=False):
418
- """Manual trigger with idempotent upsert."""
419
- if not frappe.has_permission("My DocType", "write"):
420
- frappe.throw(_("Permission denied"), frappe.PermissionError)
421
-
422
- data = engine.calculate(employee, period_type, period_value)
423
-
424
- existing = frappe.db.get_value("My DocType", {
425
- "employee": employee, "period_type": period_type,
426
- "period_value": period_value,
427
- })
428
-
429
- if existing:
430
- if not overwrite:
431
- return {"status": "exists", "name": existing}
432
- doc = frappe.get_doc("My DocType", existing)
433
- if doc.docstatus == 1:
434
- frappe.throw(_("Cannot overwrite submitted record"))
435
- doc.update(data)
436
- doc.save(ignore_permissions=True)
437
- frappe.db.commit()
438
- return {"status": "updated", "name": doc.name}
439
-
440
- doc = frappe.get_doc({"doctype": "My DocType", **data})
441
- doc.insert(ignore_permissions=True)
442
- frappe.db.commit()
443
- return {"status": "created", "name": doc.name}
444
- ```
445
-
446
- ### Permission Query Pattern
447
-
448
- ```python
449
- def get_permission_query(user: str) -> str:
450
- """Row-level security: user sees only their own records unless manager/admin."""
451
- roles = frappe.get_roles(user)
452
- admin_roles = {"System Manager", "HR Reviewer", "Department Manager"}
453
- if admin_roles & set(roles):
454
- return "" # No filter = see all
455
- emp = frappe.db.get_value("Employee", {"user_id": user}, "name")
456
- if emp:
457
- return f"`tabMy DocType`.employee = '{emp}'"
458
- return "1=0" # See nothing
459
- ```
460
-
461
- **Key patterns:**
462
- - `allow_guest=False` for webhooks (requires API key auth)
463
- - Parse `data` as string or dict (Frappe may pass either)
464
- - Batch processing with created/skipped/errors counters
465
- - Idempotent upsert: check existing → update or insert
466
- - `frappe.db.commit()` after bulk operations
467
- - Permission queries for row-level security
468
-
469
- ---
470
-
471
- ## ⏰ Layer 4: Scheduler Tasks
472
-
473
- ### Pattern: Batch Processing All Employees
474
-
475
- ```python
476
- def run_monthly_calculation(year_month: str = None):
477
- """Monthly task: calculate scores for ALL active employees."""
478
- if not year_month:
479
- year_month = add_months(today(), -1)[:7]
480
-
481
- frappe.logger("my_app").info(f"[Monthly] Starting for {year_month}")
482
- employees = frappe.get_all("Employee", filters={"status": "Active"}, fields=["name"])
483
- ok = err = 0
484
-
485
- for emp in employees:
486
- try:
487
- data = engine.calculate(emp.name, "month", year_month)
488
- existing = frappe.db.get_value("Score", {
489
- "employee": emp.name, "period_type": "month",
490
- "period_value": year_month,
491
- })
492
- if existing:
493
- doc = frappe.get_doc("Score", existing)
494
- if doc.docstatus == 0: # Only update drafts
495
- doc.update(data)
496
- doc.save(ignore_permissions=True)
497
- else:
498
- frappe.get_doc({"doctype": "Score", **data}).insert(ignore_permissions=True)
499
- ok += 1
500
- except Exception as exc:
501
- err += 1
502
- frappe.log_error(str(exc), f"Monthly — {emp.name}")
503
-
504
- frappe.db.commit()
505
- frappe.logger("my_app").info(f"[Monthly] Done: {ok} ok, {err} errors")
506
- ```
507
-
508
- ### Pattern: Reminder Notifications
509
-
510
- ```python
511
- def send_pending_reminders():
512
- """Notify managers about records pending > 3 days."""
513
- cutoff = add_days(today(), -3)
514
- pending = frappe.db.sql("""
515
- SELECT r.name, r.employee, e.employee_name, e.reports_to
516
- FROM `tabMy Record` r
517
- LEFT JOIN `tabEmployee` e ON r.employee = e.name
518
- WHERE r.docstatus = 0 AND r.creation <= %(cutoff)s
519
- """, {"cutoff": cutoff}, as_dict=True)
520
-
521
- by_manager = {}
522
- for row in pending:
523
- if row.reports_to:
524
- by_manager.setdefault(row.reports_to, []).append(row)
525
-
526
- for mgr, records in by_manager.items():
527
- mgr_email = frappe.db.get_value("Employee", mgr, "user_id")
528
- if mgr_email:
529
- frappe.sendmail(
530
- recipients=[mgr_email],
531
- subject=f"[My App] {len(records)} records need review",
532
- message=format_reminder_html(records),
533
- )
534
- ```
535
-
536
- ### hooks.py Scheduler Config
537
-
538
- ```python
539
- scheduler_events = {
540
- "daily": [
541
- "my_app.tasks.daily.run_daily_aggregation",
542
- "my_app.tasks.daily.send_pending_reminders",
543
- ],
544
- "weekly": [
545
- "my_app.tasks.weekly.run_weekly_calculation",
546
- ],
547
- "cron": {
548
- "0 1 1 * *": [ # 1st of month at 01:00
549
- "my_app.tasks.monthly.run_monthly_calculation",
550
- ]
551
- },
552
- }
553
- ```
554
-
555
- ---
556
-
557
- ## 🔗 hooks.py — The Nervous System
558
-
559
- ```python
560
- app_name = "my_app"
561
- app_title = "My App"
562
- app_publisher = "My Company"
563
- app_description = "App description"
564
- app_license = "MIT"
565
-
566
- required_apps = ["frappe/hrms"] # Declare dependencies
567
-
568
- # ── Assets ────────────────────────────────────────────────────────────────
569
- app_include_js = ["/assets/my_app/js/my_app.js"]
570
-
571
- # ── DocType JS overrides (for core DocTypes like Employee) ────────────────
572
- doctype_js = {
573
- "Employee": "public/js/employee.js"
574
- }
575
-
576
- # ── After install / migrate ───────────────────────────────────────────────
577
- after_install = "my_app.setup.install.after_install"
578
- after_migrate = "my_app.setup.install.after_migrate"
579
-
580
- # ── Doc Events (server-side hooks) ────────────────────────────────────────
581
- doc_events = {
582
- "My Submittable Doc": {
583
- "on_submit": "my_app.engines.my_engine.on_doc_submit",
584
- "on_cancel": "my_app.engines.my_engine.on_doc_cancel",
585
- },
586
- "My Auto Doc": {
587
- "after_insert": "my_app.engines.my_engine.on_doc_insert",
588
- },
589
- }
590
-
591
- # ── Fixtures (portable across sites) ─────────────────────────────────────
592
- fixtures = [
593
- {"dt": "Role", "filters": [["name", "in", ["My Role 1", "My Role 2"]]]},
594
- {"dt": "Custom Field", "filters": [["module", "=", "My App"]]},
595
- {"dt": "Workflow", "filters": [["document_type", "in", ["My Doc"]]]},
596
- {"dt": "Workflow State", "filters": [["name", "in", [
597
- "Draft", "Pending", "Approved", "Rejected"
598
- ]]]},
599
- ]
600
-
601
- # ── Permission query conditions ───────────────────────────────────────────
602
- permission_query_conditions = {
603
- "My Score Doc": "my_app.api.internal.get_permission_query",
604
- }
605
- ```
606
-
607
- ---
608
-
609
- ## 🛠️ Layer 5: Setup / Install
610
-
611
- ```python
612
- def after_install():
613
- """Run once after bench install-app. Must be idempotent."""
614
- _create_roles()
615
- _create_custom_fields()
616
- _create_workflow_states()
617
- frappe.db.commit()
618
-
619
- def after_migrate():
620
- """Run after every bench migrate. Must be idempotent."""
621
- _ensure_roles()
622
- _create_workflow_states()
623
- frappe.db.commit()
624
-
625
- def _create_roles():
626
- for role in ["My Role 1", "My Role 2"]:
627
- if not frappe.db.exists("Role", role):
628
- frappe.get_doc({
629
- "doctype": "Role", "role_name": role, "desk_access": 1
630
- }).insert(ignore_permissions=True)
631
-
632
- def _create_custom_fields():
633
- """Add custom fields to core DocTypes (e.g., Employee)."""
634
- fields = [
635
- {
636
- "dt": "Employee",
637
- "fieldname": "my_app_code",
638
- "fieldtype": "Data",
639
- "label": "My App Code",
640
- "insert_after": "employee_number",
641
- "unique": 1, "search_index": 1,
642
- "module": "My App",
643
- },
644
- ]
645
- for cf in fields:
646
- if not frappe.db.exists("Custom Field", {"dt": cf["dt"], "fieldname": cf["fieldname"]}):
647
- frappe.get_doc({"doctype": "Custom Field", **cf}).insert(ignore_permissions=True)
648
-
649
- def _create_workflow_states():
650
- states = [
651
- {"state": "Draft", "icon": "edit", "style": ""},
652
- {"state": "Pending", "icon": "time", "style": "Warning"},
653
- {"state": "Approved", "icon": "ok-sign", "style": "Success"},
654
- {"state": "Rejected", "icon": "remove", "style": "Danger"},
655
- ]
656
- for ws in states:
657
- if not frappe.db.exists("Workflow State", ws["state"]):
658
- frappe.get_doc({
659
- "doctype": "Workflow State",
660
- "workflow_state_name": ws["state"],
661
- "icon": ws["icon"], "style": ws["style"],
662
- }).insert(ignore_permissions=True)
663
- ```
664
-
665
- ---
666
-
667
- ## 📊 Layer 6: Reports
668
-
669
- ### Script Report Pattern
670
-
671
- **Python (employee_scoring_summary.py):**
672
-
673
- ```python
674
- def execute(filters=None):
675
- filters = filters or {}
676
- columns = get_columns()
677
- data = get_data(filters)
678
- chart = get_chart(data)
679
- report_summary = get_report_summary(data)
680
- return columns, data, None, chart, report_summary
681
-
682
- def get_columns():
683
- return [
684
- {"fieldname": "employee", "label": "Employee", "fieldtype": "Link",
685
- "options": "Employee", "width": 120},
686
- {"fieldname": "value", "label": "Value", "fieldtype": "Float",
687
- "precision": 1, "width": 90},
688
- # ... more columns
689
- ]
690
-
691
- def get_data(filters):
692
- conditions = "WHERE d.docstatus != 2"
693
- values = {}
694
- if filters.get("company"):
695
- conditions += " AND d.company = %(company)s"
696
- values["company"] = filters["company"]
697
- # ... more filters
698
- return frappe.db.sql(f"""
699
- SELECT d.employee, d.value, ...
700
- FROM `tabMy Doc` d {conditions}
701
- ORDER BY d.value DESC
702
- """, values, as_dict=True)
703
-
704
- def get_chart(data):
705
- if not data:
706
- return None
707
- top = data[:10]
708
- return {
709
- "data": {
710
- "labels": [r.employee_name for r in top],
711
- "datasets": [
712
- {"name": "Value", "values": [r.value for r in top]},
713
- ],
714
- },
715
- "type": "bar",
716
- "colors": ["#2980b9"],
717
- }
718
-
719
- def get_report_summary(data):
720
- if not data:
721
- return []
722
- total = len(data)
723
- avg = sum(r.value for r in data) / total if total else 0
724
- return [
725
- {"value": total, "label": "Total", "datatype": "Int", "indicator": "Blue"},
726
- {"value": round(avg, 1), "label": "Average", "datatype": "Float",
727
- "indicator": "Green" if avg >= 50 else "Orange"},
728
- ]
729
- ```
730
-
731
- **JavaScript (employee_scoring_summary.js):**
732
-
733
- ```javascript
734
- frappe.query_reports["Employee Scoring Summary"] = {
735
- filters: [
736
- {
737
- fieldname: "company",
738
- label: __("Company"),
739
- fieldtype: "Link",
740
- options: "Company",
741
- default: frappe.defaults.get_user_default("Company"),
742
- },
743
- {
744
- fieldname: "period",
745
- label: __("Period (YYYY-MM)"),
746
- fieldtype: "Data",
747
- default: frappe.datetime.get_today().substring(0, 7),
748
- },
749
- ],
750
- };
751
- ```
752
-
753
- ---
754
-
755
- ## 🎨 Layer 7: Shared Client JS
756
-
757
- ```javascript
758
- /**
759
- * Global client-side utilities namespace.
760
- * Loaded via app_include_js in hooks.py.
761
- */
762
- window.myapp = window.myapp || {};
763
-
764
- // Color-coded badge
765
- myapp.levelBadge = function (level, colorMap) {
766
- const info = colorMap[level] || { color: "#888", label: level || "N/A" };
767
- return `<span style="background:${info.color};color:#fff;padding:2px 8px;
768
- border-radius:4px;font-size:12px;font-weight:600;">${info.label}</span>`;
769
- };
770
-
771
- // Promise-based Frappe method call
772
- myapp.callMethod = function (method, args) {
773
- return new Promise((resolve, reject) => {
774
- frappe.call({
775
- method, args,
776
- callback: (r) => r?.message != null ? resolve(r.message) : reject(r),
777
- error: reject,
778
- });
779
- });
780
- };
781
-
782
- // Auto-set period from date field
783
- myapp.autoSetPeriod = function (frm, dateField, periodField) {
784
- const d = frm.doc[dateField];
785
- if (d && d.length >= 7) frm.set_value(periodField, d.substring(0, 7));
786
- };
787
-
788
- // List View Settings with workflow color indicators
789
- frappe.listview_settings['My Submittable Doc'] = {
790
- get_indicator(doc) {
791
- const colors = {
792
- "Pending": "orange", "Approved": "green",
793
- "Rejected": "red", "Draft": "gray"
794
- };
795
- const c = colors[doc.workflow_state] || "gray";
796
- return [__(doc.workflow_state), c, "workflow_state,=," + doc.workflow_state];
797
- }
798
- };
799
- ```
800
-
801
- ---
802
-
803
- ## 🧪 Layer 6: Testing
804
-
805
- ### Standalone Pure-Logic Tests (No Frappe!)
806
-
807
- ```python
808
- """
809
- Run: python -m pytest my_app/tests/test_engine.py -v
810
- No Frappe instance needed — tests only pure-logic functions.
811
- """
812
- import unittest
813
-
814
- # Inline pure-logic (copy from engine, no frappe imports)
815
- PPH_LEVEL_ORDER = ["needs_improvement", "average", "good", "excellent"]
816
- _DEFAULT_THRESHOLDS = [
817
- {"level": "needs_improvement", "min_pph": 0, "max_pph": 35},
818
- {"level": "average", "min_pph": 35, "max_pph": 55},
819
- ]
820
- # ... paste pure functions here
821
-
822
- class TestClassifyLevel(unittest.TestCase):
823
- def test_boundary(self):
824
- self.assertEqual(classify_level(35, _DEFAULT_THRESHOLDS), "average")
825
-
826
- def test_empty_defaults(self):
827
- self.assertEqual(classify_level(60, []), "good")
828
-
829
- class TestLevelOrdering(unittest.TestCase):
830
- def test_gte(self):
831
- self.assertTrue(level_gte("excellent", "good"))
832
- self.assertFalse(level_gte("average", "good"))
833
-
834
- if __name__ == "__main__":
835
- unittest.main(verbosity=2)
836
- ```
837
-
838
- **Why inline pure functions in test files?**
839
- - Tests run with `pytest -v` — no bench, no MariaDB, no Frappe site
840
- - CI/CD friendly — fast, isolated, reliable
841
- - Keep the test file self-contained
842
-
843
- ---
844
-
845
- ## 🔄 Workflow Design
846
-
847
- ### Common Workflow Patterns
848
-
849
- | DocType Category | States | Submittable? |
850
- |-----------------|--------|--------------|
851
- | **Violation/Penalty** | Pending → Confirmed → Appealed / Waived | Yes |
852
- | **Score/Review** | Draft → Manager Review → HR Approved → Final | Yes |
853
- | **Recovery/Request** | Pending → Approved / Rejected | Yes |
854
- | **Bonus/Reward** | Pending Approval → Approved / Rejected | Yes |
855
- | **Config/Master** | N/A (no workflow, just CRUD) | No |
856
-
857
- ### Workflow State Colors
858
-
859
- ```python
860
- STATE_STYLES = {
861
- "Draft": ("edit", ""), # Gray
862
- "Pending": ("time", "Warning"), # Orange
863
- "Approved": ("ok-sign", "Success"), # Green
864
- "Rejected": ("remove", "Danger"), # Red
865
- "Review": ("eye-open","Warning"), # Orange
866
- "Final": ("star", "Success"), # Green
867
- }
868
- ```
869
-
870
- ---
871
-
872
- # Examples
873
-
874
- ## Example 1: Scaffold a New Frappe Custom App
875
-
876
- **WARNING:** Always use `bench new-app` to create the initial app structure! Do not create it manually, as Frappe Cloud and other tools rely on standard boilerplate files (`setup.py`, `MANIFEST.in`, `patches.txt`, `hooks.py`, etc.) that are generated by `bench`. Failure to do so will result in "Not a valid Frappe App" errors during deployment.
877
-
878
- ```bash
879
- # 1. Create app (this generates setup.py, pyproject.toml, MANIFEST.in, requirements.txt, etc.)
880
- bench new-app my_custom_app
881
-
882
- # 2. Create module structure inside the generated app
883
- mkdir -p apps/my_custom_app/my_custom_app/{engines,api,tasks,setup,tests,scripts,fixtures}
884
- mkdir -p apps/my_custom_app/my_custom_app/public/js
885
- touch apps/my_custom_app/my_custom_app/{engines,api,tasks,setup,tests}/__init__.py
886
-
887
- # 3. Install on site
888
- bench --site mysite.localhost install-app my_custom_app
889
-
890
- # 4. Enable dev mode
891
- bench --site mysite.localhost set-config developer_mode 1
892
- ```
893
-
894
- ## Example 2: Add a New DocType with Full Stack
895
-
896
- 1. Create DocType via Frappe UI or JSON
897
- 2. Write controller (`.py`) with validate/on_submit/on_cancel
898
- 3. Write client script (`.js`) with refresh/field triggers
899
- 4. Register doc_events in `hooks.py`
900
- 5. Add engine function for business logic
901
- 6. Write API endpoint calling engine
902
- 7. Add scheduler task for batch processing
903
- 8. Write tests for pure logic
904
- 9. `bench --site mysite migrate && bench build --app my_custom_app`
905
-
906
- ---
907
-
908
- ## 🌍 Layer 8: Multi-Language (i18n)
909
-
910
- ### Translation Workflow
911
-
912
- Frappe uses bare strings wrapped in translation functions: `_("String")` in Python and `__("String")` in JavaScript. Do not use translation keys; use the English baseline string as the key.
913
-
914
- 1. **Wrap all UI-facing strings:**
915
- - Python: `frappe._("User {0} not found").format(user_id)`
916
- - JS: `__("User {0} not found", [user_id])`
917
- 2. **Export strings to CSV:**
918
- `bench --site <site> get-untranslated <language-code> <path/to/output.csv>`
919
- 3. **Translate and Import:**
920
- Add translations to the CSV, then place the translated translations in `my_app/translations/<lang>.csv`.
921
-
922
- ### Translation Rules
923
- ```
924
- ✅ DO:
925
- - Use English as the default bare string.
926
- - Use `{0}`, `{1}` for interpolation (positional args) to allow word reordering in other languages.
927
- - Run `bench --site <site> migrate` to clear caches and load new translations.
928
-
929
- ❌ DON'T:
930
- - Translate log messages or internal system errors meant for developers.
931
- - Use concatenation (`_("Hello") + " " + user`) — always interpolate (`_("Hello {0}").format(user)`).
932
- ```
933
-
934
- ---
935
-
936
- ## 🚀 Layer 9: CI/CD & GitHub Actions
937
-
938
- When building or fixing CI/CD pipelines (`ci.yml`, `linter.yml`) for Frappe apps, adhere to the following stability rules:
939
-
940
- ### Versions & Environments
941
- - **Python:** Strictly unify the Python version across ALL jobs (e.g., `python-version: '3.12'`). Avoid alpha versions (e.g., 3.14) that break dependencies.
942
- - **Node.js:** Use Node 20+ (e.g., `node-version: 20`). Frappe v15 ecosystem deeply relies on packages like `jsdom@29+` which explicitly drop Node 18 support.
943
- - **GitHub Actions:** Use stable tags (`@v4`, `@v5`). Never guess action versions (`@v6`) as it causes silent failures.
944
-
945
- ### Frappe Installation in CI
946
- - **Skip Assets:** Always use `--skip-assets` with `bench get-app` to prevent memory exhaustion (OOM errors) and esbuild race conditions during concurrent CI setups.
947
- - **Local App Registration:** If you `bench get-app /path/to/local/app --skip-assets`, the app is **NOT** added to `sites/apps.txt` automatically.
948
- - **apps.txt Fix:** Manually append it safely before `bench install-app`:
949
- ```bash
950
- grep -q my_app sites/apps.txt 2>/dev/null || printf '\nmy_app\n' >> sites/apps.txt
951
- ```
952
- *(Always use `printf '\n...'` instead of `echo` to prevent concatenating with the last line if it lacks a trailing newline.)*
953
-
954
- ---
955
-
956
- # Constraints
957
-
958
- ## NEVER
959
- - Push directly to production branch
960
- - Delete or modify DocType JSON files without `bench migrate` after
961
- - Hardcode employee/company references — always use Link fields
962
- - Skip `frappe.db.commit()` after bulk operations
963
- - Use `frappe.db.sql` for INSERT/UPDATE when ORM is available
964
- - Put complex business logic directly in DocType controllers
965
- - Ignore `docstatus` when querying submitted documents
966
-
967
- ## ALWAYS
968
- - Run `bench --site <site> migrate` after changing DocType schemas
969
- - Run `bench build --app <app>` after changing JS/CSS
970
- - Use `@frappe.whitelist()` decorator for API endpoints
971
- - Use `frappe.has_permission()` before operations in APIs
972
- - Separate pure logic into `engines/` for testability
973
- - Use `frappe.logger("app_name")` for structured logging
974
- - Make `after_install` and `after_migrate` idempotent
975
- - Filter by `docstatus = 1` when aggregating submitted records
976
- - Use `COALESCE(SUM(...), 0)` to avoid NULL in SQL aggregations
977
- - Add `frappe.db.commit()` at the end of batch task functions
978
-
979
- ## CONFIRM
980
- - Before running `bench --site <site> reinstall` (destroys data)
981
- - Before bulk data migration scripts
982
- - Before modifying workflow states (affects existing records)
983
- - Before `bench drop-site` or `--force` commands
984
-
985
- <!-- Generated by Skill Creator Ultra v1.0 -->