The technical counterpart to the CAFM proposal. Seventeen forms, 189 fields and seven Deluge routines — with the reasoning behind each design decision, so the next person to touch this system can tell an intentional constraint from an accident.
This is the build document for the Zoho Creator application described in the CAFM proposal. It is written for whoever builds it, not for whoever buys it — the commercial case, the timeline and the exclusions live in the proposal and are not repeated here.
tpl.html and gen2.py in the handoff bundle), which is the current and
authoritative description of scope. It is not a transcription of the earlier
CAFM_Creator_Build_Spec.md — that file was referenced in the handoff README but was not
included in the bundle and could not be located. Where the two disagree, reconcile them before building,
and treat the proposal as the tie-breaker.Two rules govern everything below.
The proposal is the contract. Every form, field and routine here traces to something the client has been shown. Where the spec adds detail the proposal does not mention, that detail is an implementation choice and is marked as such. Where a design decision was made to protect an assumption in section 09 of the proposal, it says so.
Spare parts live in Creator. Zoho Inventory was deliberately excluded. Parts, stock movements and consumption are all Creator forms, so a technician logs a part on the same screen as the job and there is no sync to reconcile. The cost of that decision — no purchase orders, no VAT-compliant procurement trail — is documented in the proposal exclusions and is not a gap in this build.
| Reads this for | Where to start |
|---|---|
| Building the application | Section 04 form by form, then section 05 in the order given by section 09. |
| Reviewing the design | Sections 02 and 03 for shape, then the design notes under each routine in 05. |
| Testing it | The completion gate in the state machine and the idempotency guard in the scheduler — those are where it breaks. |
| Taking it over later | Section 05. The Deluge layer is where the business rules are; the forms are just shape. |
Seventeen forms in six groups, 189 fields in total. The groups are not folders — they are the order things have to be built in, because each depends on the one above it.
Four levels, each belonging to exactly one level above. Entered once, referenced everywhere.
The lists that carry defaults. Time spent here is time not spent on every asset afterwards.
The spine, plus the append-only history of what changed and who changed it.
Reusable checks and the rules that turn them into work without anyone asking.
What a technician actually opens. One job form, whatever raised it.
Catalogue and ledger. Inside Creator by design — no Inventory, no sync.
Location is entered once, at the room, and inherited upward. An asset points at a room and nothing else — building, floor and zone are derived. This is the single most important structural decision in the build, because everything downstream reports on it.
| Decision | Consequence |
|---|---|
| Asset points at Room only | One lookup to maintain. Moving an asset is a single field change, and its whole path follows. |
| Location Path denormalised | Reports and the technician's search work on one indexed field instead of a four-table join. |
| Codes unique and stable | Room and asset codes end up on physical tags. Renumbering after go-live means re-tagging a building. |
| Level Index on Floors | Floors sort physically — basement, ground, mezzanine, level 3 — rather than alphabetically. |
All seventeen forms, field by field. * marks a required field. Types are Creator field types as written in the form builder. Fields described as written by a routine should be read-only on every layout — if a user can type into them, the ledger and the schedule stop being trustworthy.
| Field | Type | Rule / note |
|---|---|---|
| Building Code* | Single Line | Unique. Feeds the location path and the asset ID prefix. |
| Building Name* | Single Line | Display name used across reports. |
| Site / Campus | Single Line | Groups buildings where a client runs more than one site. |
| Address | Multi Line | Postal address for vendor call-out. |
| City | Single Line | |
| Total Floors | Number | Sense check against the Floors records created. |
| Gross Area (sqm) | Decimal | Used for area-normalised cost reporting in Analytics. |
| Handover Date | Date | Drives warranty and DLP expiry views. |
| Facility Manager | Lookup → Technicians | Escalation point for every asset below this building. |
| Active | Checkbox | Default true. Inactive buildings drop out of all selection lists. |
| Field | Type | Rule / note |
|---|---|---|
| Building* | Lookup → Buildings | Parent. Cascades into every location path below. |
| Floor Code* | Single Line | Unique within the building, not globally. |
| Floor Name* | Single Line | e.g. Ground, Mezzanine, Level 3. |
| Level Index | Number | Sort order. Basements negative, ground zero — so floors list in physical order rather than alphabetically. |
| Floor Area (sqm) | Decimal | |
| Active | Checkbox | Default true. |
| Field | Type | Rule / note |
|---|---|---|
| Floor* | Lookup → Floors | Parent. |
| Zone Code* | Single Line | Unique within the floor. |
| Zone Name* | Single Line | |
| Zone Type | Dropdown | Plant room / Office / Common area / Retail / Car park / External / Riser. Drives permit rules on the work order. |
| Active | Checkbox | Default true. |
| Field | Type | Rule / note |
|---|---|---|
| Zone* | Lookup → Zones | Parent. |
| Room Code* | Single Line | Globally unique. Printed on asset tags. |
| Room Name | Single Line | |
| Room Number | Single Line | The number on the door, which often differs from the code. |
| Location Path | Formula | Building › Floor › Zone › Room, concatenated. Denormalised deliberately so reports and search do not traverse four lookups. |
| Access Notes | Multi Line | Key holder, permit requirement, restricted hours. |
| Active | Checkbox | Default true. |
| Field | Type | Rule / note |
|---|---|---|
| Vendor Code* | Single Line | Unique. |
| Vendor Name* | Single Line | |
| Vendor Type* | Multi Select | OEM / Service contractor / Parts supplier / Consultant. Multi-select because most vendors are more than one. |
| Contact Person | Single Line | |
| Used by the contract-expiry notification. | ||
| Phone | Phone | |
| Contract Reference | Single Line | |
| Contract Start | Date | |
| Contract Expiry | Date | Drives the 60-day expiry warning report. |
| Response SLA (hours) | Number | Contracted response time, reported against actual. |
| Documents Folder | URL | WorkDrive folder holding the signed contract. |
| Active | Checkbox | Default true. |
| Field | Type | Rule / note |
|---|---|---|
| Category Code* | Single Line | Unique. Becomes the asset ID prefix. |
| Category Name* | Single Line | e.g. Chiller, Fire pump, Passenger lift. |
| Discipline* | Dropdown | HVAC / Electrical / Plumbing / Fire & life safety / Vertical transport / Civil / BMS. Drives technician routing. |
| Default Checklist Template | Lookup → Checklist Templates | Copied onto every schedule created for this category. |
| Default PPM Frequency | Dropdown | Pre-fills the schedule so the supervisor confirms rather than composes. |
| Default Criticality | Dropdown | Critical / High / Medium / Low. |
| Track Running Hours | Checkbox | Turns on the predictive path for assets in this category. |
| Default Runtime Threshold | Decimal | Service interval in running hours. |
| Asset ID Prefix* | Single Line | Two-to-four characters. Consumed by the asset ID generator. |
| Active | Checkbox | Default true. |
| Field | Type | Rule / note |
|---|---|---|
| Employee Code* | Single Line | Unique. |
| Full Name* | Single Line | |
| Zoho User* | Users | Binds the record to a Creator login. Required for the technician's own-records-only filter to work. |
| Role* | Dropdown | Administrator / Supervisor / Technician / Storekeeper. Mirrors the permission set. |
| Discipline | Multi Select | Matches the category discipline for assignment suggestions. |
| Phone | Phone | |
| Assignment notifications. | ||
| Shift | Dropdown | Day / Night / Rotating. Reference only — SLA clocks run continuously (see assumptions). |
| Active | Checkbox | Default true. Inactive technicians cannot be assigned new work. |
| Field | Type | Rule / note |
|---|---|---|
| Asset ID* | Single Line | Unique, system-generated on create, read-only thereafter. See the asset ID generator routine. |
| Asset Name* | Single Line | |
| Asset Category* | Lookup → Asset Categories | Pulls criticality, checklist and runtime defaults on selection. |
| Room* | Lookup → Rooms | The single location field. Building, floor and zone are derived, never entered twice. |
| Location Path | Formula | Fetched from the room. Denormalised for search and reporting. |
| Make | Single Line | |
| Model | Single Line | |
| Serial Number | Single Line | |
| Manufacturer | Lookup → Vendors | |
| Service Vendor | Lookup → Vendors | Who is called when this fails, if not in-house. |
| Install Date | Date | |
| Commissioning Date | Date | |
| Warranty Expiry | Date | Flagged on the work order so warranty work is not paid for twice. |
| Criticality* | Dropdown | Critical / High / Medium / Low. Defaults from category, overridable per asset. |
| Status* | Dropdown | In service / Under repair / Standby / Decommissioned. Decommissioned assets pause their schedules. |
| Track Running Hours | Checkbox | Defaults from category. |
| Current Running Hours | Decimal | Entered manually — see the running-hours assumption. |
| Runtime Threshold | Decimal | Hours between services. Compared against hours-since-last-service. |
| Hours At Last Service | Decimal | Written by the state machine on completion of a predictive or preventive job. |
| Documents Folder | URL | WorkDrive folder for this asset's O&M manuals. |
| Photo | Image | |
| Notes | Multi Line |
| Field | Type | Rule / note |
|---|---|---|
| Reference Type* | Dropdown | Asset / Work order / PPM schedule / Stock movement / Parts master. |
| Reference ID* | Single Line | The business key of the record changed, not the Creator row ID. |
| Action* | Dropdown | Created / Updated / Status change / Deleted / Imported. |
| Field Changed | Single Line | |
| Old Value | Multi Line | |
| New Value | Multi Line | |
| Changed By* | Users | Default zoho.loginuser. |
| Changed On* | Date-Time | Default zoho.currenttime. |
| Field | Type | Rule / note |
|---|---|---|
| Template Code* | Single Line | Unique. |
| Template Name* | Single Line | |
| Asset Category* | Lookup → Asset Categories | |
| Job Type* | Dropdown | Preventive / Corrective / Predictive / Any. |
| Frequency | Dropdown | The interval this template is written for — a monthly check list differs from an annual one. |
| Estimated Duration (minutes) | Number | Feeds technician workload reporting. |
| Permit Required | Checkbox | Surfaces a permit warning on every work order using this template. |
| Safety Notes | Multi Line | Isolation, PPE, lock-out requirements. Shown to the technician before the first check. |
| Version | Number | Default 1. Increment rather than edit in place, so completed jobs keep the checks that were actually performed. |
| Active | Checkbox | Default true. |
| Field | Type | Rule / note |
|---|---|---|
| Template* | Lookup → Checklist Templates | Parent. |
| Sequence* | Number | Display order. Numbered in tens so items can be inserted later without renumbering. |
| Check Description* | Multi Line | Phrased as an instruction, not a question. |
| Response Type* | Dropdown | Pass/Fail / Yes-No / Numeric reading / Text / Photo. |
| Unit | Single Line | For numeric readings — bar, °C, A, dB. |
| Min Acceptable | Decimal | |
| Max Acceptable | Decimal | Readings outside min–max flag the check as out of range on the job. |
| Mandatory | Checkbox | Mandatory items block work order completion until answered. |
| Field | Type | Rule / note |
|---|---|---|
| Schedule Code* | Single Line | Unique, system-generated. |
| Asset* | Lookup → Assets | |
| Checklist Template* | Lookup → Checklist Templates | Defaults from the asset's category. |
| Frequency* | Dropdown | Daily / Weekly / Monthly / Quarterly / Half-yearly / Yearly / Custom days. |
| Custom Interval (days) | Number | Required only when frequency is Custom days. |
| Start Date* | Date | First occurrence. |
| Next Due Date* | Date | Maintained by the scheduler. The single field the daily routine queries. |
| Last Completed Date | Date | Written by the state machine on job completion. |
| Lead Time (days) | Number | Default 3. How many days before the due date the work order is raised, so the technician has notice. |
| Assigned Technician | Lookup → Technicians | Default assignee for generated jobs. |
| Estimated Duration (minutes) | Number | Defaults from the template. |
| Status* | Dropdown | Active / Paused / Ended. Only Active schedules are picked up by the daily run. |
| End Date | Date | Optional. Past this date the schedule stops generating. |
| Field | Type | Rule / note |
|---|---|---|
| Work Order Number* | Single Line | Unique, system-generated. WO-YYYYMM-nnnn. |
| Job Type* | Dropdown | Preventive / Corrective / Predictive. Set by whichever routine created the record. |
| Asset* | Lookup → Assets | |
| Location Path | Formula | Fetched from the asset at creation. |
| Source Schedule | Lookup → PPM Schedules | Populated on preventive jobs only. The back-reference the state machine uses to book the next occurrence. |
| Desk Ticket ID | Single Line | Populated on corrective jobs only, written by Zoho Flow. The key that carries status back to the requester. |
| Checklist Template | Lookup → Checklist Templates | Copied into Task Checklist rows at creation. |
| Priority* | Dropdown | Critical / High / Medium / Low. Defaults from asset criticality. |
| Status* | Dropdown | Draft / Open / Assigned / In progress / On hold / Completed / Verified / Cancelled. Transitions are enforced — see the state machine. |
| Assigned Technician | Lookup → Technicians | |
| Raised On* | Date-Time | Default zoho.currenttime. |
| Scheduled Date | Date | The date the work is due, distinct from the date it was raised. |
| Target Completion | Date-Time | Derived from priority. Drives the overdue flag. |
| Started On | Date-Time | Stamped on transition to In progress. |
| Completed On | Date-Time | Stamped on transition to Completed. |
| On Time | Checkbox | Set by the state machine: Completed On ≤ Target Completion. |
| Completion % | Decimal | Rolled up from Task Checklist rows answered. |
| Downtime (hours) | Decimal | Asset out of service. Feeds the downtime dashboard. |
| Failure Cause | Dropdown | Wear / Misuse / Design / Environment / No fault found / Consumable. Corrective and predictive only. |
| Work Performed | Multi Line | |
| Photos | File Upload | Multiple. Before-and-after evidence. |
| Technician Sign-off | Checkbox | The technician's own confirmation. Cannot be set by a supervisor. |
| Verified By | Lookup → Technicians | Supervisor closing the loop. Drives the Verified status. |
| Verified On | Date-Time |
| Field | Type | Rule / note |
|---|---|---|
| Work Order* | Lookup → Work Orders | Parent. |
| Sequence* | Number | Carried over from the template item. |
| Check Description* | Multi Line | Copied, not looked up — the template can change later without rewriting history. |
| Response Type* | Dropdown | Copied from the template item. |
| Result | Dropdown | Pass / Fail / Not applicable. |
| Reading | Decimal | For numeric checks. |
| Unit | Single Line | Copied. |
| Min Acceptable | Decimal | Copied, so the range test survives a template revision. |
| Max Acceptable | Decimal | Copied. |
| Within Range | Checkbox | Set on save when a reading is entered. |
| Mandatory | Checkbox | Copied. Unanswered mandatory rows block completion. |
| Remarks | Multi Line | |
| Photo | Image | |
| Completed On | Date-Time | Stamped when the row is first answered. |
| Field | Type | Rule / note |
|---|---|---|
| Work Order* | Lookup → Work Orders | Parent. |
| Part* | Lookup → Parts Master | |
| Quantity Used* | Decimal | Validated against the current balance before save. |
| Unit | Formula | Fetched from the part. |
| Unit Cost | Decimal | Snapshot of standard cost at the time of issue, so historic job cost does not move when the price does. |
| Line Value | Formula | Quantity × unit cost. |
| Issued On* | Date-Time | Default zoho.currenttime. |
| Stock Movement | Lookup → Stock Movements | Written back by the ledger routine. The audit link between consumption and balance. |
| Field | Type | Rule / note |
|---|---|---|
| Part Code* | Single Line | Unique. |
| Part Name* | Single Line | |
| Description | Multi Line | |
| Unit of Measure* | Dropdown | Each / Litre / Metre / Kilogram / Set / Roll. |
| Store Location | Single Line | Free text. One store only — see the single-store assumption. |
| Bin / Shelf | Single Line | |
| Reorder Level* | Decimal | The threshold the daily low-stock check compares against. |
| Current Balance | Decimal | Read-only on every layout. Maintained solely by the stock ledger routine. |
| Standard Cost | Decimal | Used to value consumption. Not a purchase price — procurement is out of scope. |
| Preferred Vendor | Lookup → Vendors | |
| Compatible Categories | Multi Select → Asset Categories | Filters the part picker on the work order to plausible parts. |
| Active | Checkbox | Default true. |
| Field | Type | Rule / note |
|---|---|---|
| Movement Number* | Single Line | Unique, system-generated. SM-YYYYMM-nnnn. |
| Part* | Lookup → Parts Master | |
| Movement Type* | Dropdown | Receipt / Issue / Return / Adjustment / Transfer. |
| Quantity* | Decimal | Always positive. Direction comes from the movement type, not the sign. |
| Signed Quantity | Formula | Receipt and Return positive, Issue negative, Adjustment as entered. What the balance actually sums. |
| Balance After | Decimal | Written by the ledger routine. Makes the ledger auditable without replaying it. |
| Work Order | Lookup → Work Orders | Set on issues and returns raised from a job. |
| Reference | Single Line | Delivery note or invoice number for receipts. Free text — there is no purchase order in this build. |
| Moved On* | Date-Time | Default zoho.currenttime. |
| Moved By* | Users | Default zoho.loginuser. |
| Remarks | Multi Line | Mandatory on Adjustment, so unexplained corrections cannot be saved. |
Seven routines. Five of them — the scheduler, the next-due helper, the predictive trigger, the state machine and the asset ID generator — are named in the proposal. The stock ledger and the low-stock check are what the Creator-only parts design requires in place of Zoho Inventory.
Issues the permanent, human-readable asset identifier. It runs once and never again: the guard on an existing ID is what stops a re-save from renumbering an asset that is already stencilled on a plate in a plant room.
// Assets > On Add > On Success void CAFM.generateAssetId(int assetRecordId) { rec = Assets[ID == assetRecordId]; if(rec.count() == 0) { return; } // an asset is numbered once, ever if(rec.Asset_ID != null && rec.Asset_ID.trim() != "") { return; } prefix = rec.Asset_Category.Asset_ID_Prefix; if(prefix == null || prefix.trim() == "") { prefix = "AST"; } yr = zoho.currentdate.toString("yyyy"); stem = prefix + "-" + yr + "-"; // highest number already issued under this prefix and year issued = Assets[Asset_ID.startsWith(stem)] sort by Asset_ID desc; seq = 1; if(issued.count() > 0) { tail = issued.Asset_ID.substring(stem.length()); seq = tail.toLong() + 1; } padded = seq.toString(); while(padded.length() < 4) { padded = "0" + padded; } rec.Asset_ID = stem + padded; thisapp.CAFM.writeAudit("Asset",rec.Asset_ID,"Created","","",rec.Asset_Name); }
One place that knows what every frequency word means. Both the scheduler and the state machine call it, so a change to how 'Quarterly' is interpreted lands in one file rather than two.
// Standalone function — no record context date CAFM.nextDueDate(date fromDate, string frequency, int customDays) { nextDate = fromDate; if(frequency == "Daily") { nextDate = fromDate.addDay(1); } else if(frequency == "Weekly") { nextDate = fromDate.addDay(7); } else if(frequency == "Monthly") { nextDate = fromDate.addMonth(1); } else if(frequency == "Quarterly") { nextDate = fromDate.addMonth(3); } else if(frequency == "Half-yearly") { nextDate = fromDate.addMonth(6); } else if(frequency == "Yearly") { nextDate = fromDate.addYear(1); } else if(frequency == "Custom days") { days = if(customDays == null || customDays < 1,30,customDays); nextDate = fromDate.addDay(days); } return nextDate; }
The routine the whole preventive promise rests on. It finds every active schedule whose lead-time window has opened, raises the work order, and copies the checklist onto it.
// Scheduled function — daily 02:00 void CAFM.ppmScheduler() { today = zoho.currentdate; raised = 0; skipped = 0; openSchedules = PPM_Schedules[Status == "Active" && Next_Due_Date != null]; for each sch in openSchedules { // past its end date — retire the schedule and move on if(sch.End_Date != null && sch.Next_Due_Date > sch.End_Date) { sch.Status = "Ended"; continue; } // asset out of service: hold, do not end if(sch.Asset.Status == "Decommissioned") { skipped = skipped + 1; continue; } lead = if(sch.Lead_Time == null,0,sch.Lead_Time); raiseOn = sch.Next_Due_Date.subDay(lead); if(today < raiseOn) { continue; } // IDEMPOTENCY — never raise the same occurrence twice already = Work_Orders[Source_Schedule == sch.ID && Scheduled_Date == sch.Next_Due_Date]; if(already.count() > 0) { continue; } prio = if(sch.Asset.Criticality == null,"Medium",sch.Asset.Criticality); wo = insert into Work_Orders [ Job_Type = "Preventive" Asset = sch.Asset Location_Path = sch.Asset.Location_Path Source_Schedule = sch.ID Checklist_Template = sch.Checklist_Template Priority = prio Status = "Open" Assigned_Technician = sch.Assigned_Technician Raised_On = zoho.currenttime Scheduled_Date = sch.Next_Due_Date Target_Completion = sch.Next_Due_Date.toDateTime() Completion_Percent = 0 ]; thisapp.CAFM.copyChecklist(wo.ID,sch.Checklist_Template); raised = raised + 1; } info "PPM scheduler " + today + " — raised: " + raised + ", held: " + skipped; }
Raises a service job when a machine has run its threshold hours, independently of the calendar. A generator that ran through a bad summer gets serviced early; one that sat idle does not get serviced for nothing.
// Assets > On Edit > On Success, and from the daily sweep void CAFM.predictiveCheck(int assetRecordId) { ast = Assets[ID == assetRecordId]; if(ast.count() == 0 || ast.Track_Running_Hours == false) { return; } if(ast.Status == "Decommissioned" || ast.Runtime_Threshold == null || ast.Runtime_Threshold <= 0) { return; } current = if(ast.Current_Running_Hours == null,0.0,ast.Current_Running_Hours); atLast = if(ast.Hours_At_Last_Service == null,0.0,ast.Hours_At_Last_Service); sinceService = current - atLast; if(sinceService < ast.Runtime_Threshold) { return; } // one open predictive job per asset at a time openJob = Work_Orders[Asset == ast.ID && Job_Type == "Predictive" && Status != "Completed" && Status != "Verified" && Status != "Cancelled"]; if(openJob.count() > 0) { return; } tpl = ast.Asset_Category.Default_Checklist_Template; wo = insert into Work_Orders [ Job_Type = "Predictive" Asset = ast.ID Location_Path = ast.Location_Path Checklist_Template = tpl Priority = if(ast.Criticality == "Critical","Critical","High") Status = "Open" Raised_On = zoho.currenttime Scheduled_Date = zoho.currentdate Target_Completion = zoho.currenttime.addDay(2) Completion_Percent = 0 Work_Performed = "Raised on running hours: " + sinceService + " h since last service (threshold " + ast.Runtime_Threshold + " h)." ]; if(tpl != null) { thisapp.CAFM.copyChecklist(wo.ID,tpl); } thisapp.CAFM.writeAudit("Asset",ast.Asset_ID,"Status change","Running hours",atLast.toString(),current.toString()); }
The gatekeeper. It decides which status transitions are legal, stamps the timestamps nobody should be typing by hand, blocks completion while mandatory checks are unanswered, and — on completion of a preventive job — books the next occurrence.
// Work_Orders > On Edit > On Success void CAFM.workOrderState(int woId, string oldStatus) { wo = Work_Orders[ID == woId]; if(wo.count() == 0 || wo.Status == oldStatus) { return; } allowed = Map(); allowed.put("Draft",{"Open","Cancelled"}); allowed.put("Open",{"Assigned","On hold","Cancelled"}); allowed.put("Assigned",{"In progress","On hold","Open","Cancelled"}); allowed.put("In progress",{"Completed","On hold","Cancelled"}); allowed.put("On hold",{"Assigned","In progress","Cancelled"}); allowed.put("Completed",{"Verified","In progress"}); allowed.put("Verified",{}); allowed.put("Cancelled",{}); legal = ifnull(allowed.get(oldStatus),{}); if(!legal.contains(wo.Status)) { wo.Status = oldStatus; alert "That status change is not allowed from " + oldStatus + "."; return; } // ---- completion gate: every mandatory check must be answered if(wo.Status == "Completed") { unanswered = Task_Checklist[Work_Order == woId && Mandatory == true && Result == null]; if(unanswered.count() > 0) { wo.Status = oldStatus; alert unanswered.count() + " mandatory check(s) are still unanswered."; return; } if(wo.Technician_Sign_off == false) { wo.Status = oldStatus; alert "The assigned technician must sign off before completion."; return; } } // ---- timestamps if(wo.Status == "In progress" && wo.Started_On == null) { wo.Started_On = zoho.currenttime; } if(wo.Status == "Completed") { wo.Completed_On = zoho.currenttime; wo.On_Time = wo.Target_Completion == null || wo.Completed_On <= wo.Target_Completion; total = Task_Checklist[Work_Order == woId].count(); done = Task_Checklist[Work_Order == woId && Result != null].count(); wo.Completion_Percent = if(total == 0,100.0,(done * 100.0) / total); // ---- roll the schedule forward — the ONLY place this happens if(wo.Job_Type == "Preventive" && wo.Source_Schedule != null) { sch = PPM_Schedules[ID == wo.Source_Schedule]; if(sch.count() > 0 && sch.Status == "Active") { custom = if(sch.Custom_Interval == null,0,sch.Custom_Interval); sch.Last_Completed_Date = zoho.currentdate; sch.Next_Due_Date = thisapp.CAFM.nextDueDate(sch.Next_Due_Date,sch.Frequency,custom); if(sch.End_Date != null && sch.Next_Due_Date > sch.End_Date) { sch.Status = "Ended"; } } } // ---- reset the predictive baseline if(wo.Job_Type == "Predictive" || wo.Job_Type == "Preventive") { ast = Assets[ID == wo.Asset]; if(ast.count() > 0 && ast.Track_Running_Hours == true) { ast.Hours_At_Last_Service = ifnull(ast.Current_Running_Hours,0.0); } } } if(wo.Status == "Verified") { wo.Verified_On = zoho.currenttime; } thisapp.CAFM.writeAudit("Work order",wo.Work_Order_Number,"Status change","Status",oldStatus,wo.Status); }
Turns a line on a work order into a movement in the ledger and a new balance on the part. This is the routine that earns the decision to keep spare parts inside Creator instead of Zoho Inventory.
// Parts_Used > On Add > On Success void CAFM.postIssue(int lineId) { line = Parts_Used[ID == lineId]; if(line.count() == 0) { return; } part = Parts_Master[ID == line.Part]; if(part.count() == 0) { return; } // snapshot the cost so historic job value never moves if(line.Unit_Cost == null) { line.Unit_Cost = ifnull(part.Standard_Cost,0.0); } seq = Stock_Movements[ID != 0].count() + 1; ref = "SM-" + zoho.currentdate.toString("yyyyMM") + "-" + leftPad(seq.toString(),4,"0"); mv = insert into Stock_Movements [ Movement_Number = ref Part = part.ID Movement_Type = "Issue" Quantity = line.Quantity_Used Work_Order = line.Work_Order Moved_On = zoho.currenttime Moved_By = zoho.loginuser Remarks = "Consumed on " + line.Work_Order.Work_Order_Number ]; line.Stock_Movement = mv.ID; // ---- recompute from the ledger; never increment in place balance = 0.0; for each m in Stock_Movements[Part == part.ID] { if(m.Movement_Type == "Receipt" || m.Movement_Type == "Return") { balance = balance + m.Quantity; } else if(m.Movement_Type == "Issue") { balance = balance - m.Quantity; } else if(m.Movement_Type == "Adjustment" || m.Movement_Type == "Transfer") { balance = balance + m.Signed_Quantity; } } part.Current_Balance = balance; mv.Balance_After = balance; if(balance < 0) { info "NEGATIVE BALANCE on " + part.Part_Code + " — investigate."; } }
One digest a day to the storekeeper listing every part at or below its reorder level. A daily digest rather than an alert per movement — an alert that fires eleven times before lunch gets filtered into a folder nobody reads.
// Scheduled function — daily 07:00 void CAFM.lowStockCheck() { low = Parts_Master[Active == true && Reorder_Level != null && Current_Balance <= Reorder_Level] sort by Part_Name; if(low.count() == 0) { return; // nothing low — say nothing } rows = ""; for each p in low { short = ifnull(p.Reorder_Level,0.0) - ifnull(p.Current_Balance,0.0); rows = rows + "<tr><td>" + p.Part_Code + "</td><td>" + p.Part_Name + "</td><td align='right'>" + ifnull(p.Current_Balance,0.0) + "</td><td align='right'>" + p.Reorder_Level + "</td><td align='right'>" + short + "</td></tr>"; } body = "<p>" + low.count() + " part(s) at or below reorder level as at " + zoho.currentdate.toString("dd MMM yyyy") + ".</p>"; body = body + "<table border='1' cellpadding='6' cellspacing='0'><tr><th>Code</th><th>Part</th><th>Balance</th><th>Reorder at</th><th>Short by</th></tr>" + rows + "</table>"; storekeepers = Technicians[Role == "Storekeeper" && Active == true]; for each s in storekeepers { sendmail [ from: zoho.adminuserid to: s.Email subject: "Low stock — " + low.count() + " part(s) need reordering" message: body ] } }
Reporting inside Creator is operational — what do I do next, what is late, what is running out. Management reporting is Analytics' job and is configured separately. The split matters: a technician should never need a dashboard to find their work.
| Report | Type | Visible to | What it is for |
|---|---|---|---|
| Asset register | List | All | Grouped by building, filtered by category and criticality. The default landing view. |
| Asset detail | Detail | All | Single asset with its schedules, open jobs, document link and full job history. |
| My work | List | Technician | Filtered to the logged-in technician, status not Completed. The technician's home page. |
| Work board | Kanban | Supervisor, Admin | Columns by status. Drag between columns fires the state machine. |
| Overdue jobs | List | Supervisor, Admin | Target Completion in the past, status not Completed. Red flag view. |
| PPM calendar | Calendar | Supervisor, Admin | Next Due Date across all active schedules, month view. |
| PPM compliance | Summary | Admin | Scheduled versus completed by month, on-time percentage. |
| Checklist results | List | Supervisor, Admin | Failed and out-of-range checks across all jobs — the early warning view. |
| Stock on hand | List | Storekeeper, Admin | Balance against reorder level, low rows highlighted. |
| Stock ledger | List | Storekeeper, Admin | Every movement for a part in date order, with running balance. |
| Parts consumption | Summary | Admin | Quantity and value by part, by asset and by period. |
| Warranty expiry | List | Admin | Assets whose warranty expires within 90 days. |
| Contract expiry | List | Admin | Vendor contracts expiring within 60 days. |
| Audit trail | List | Admin | Read-only, filtered by reference type and date. |
Four roles against five named users. Permissions are built last but designed first, because the technician filter shapes how work orders are assigned.
| Role | Scope | What it means in practice |
|---|---|---|
| Administrator | Everything | Full create, edit and delete on every form; the only role that can delete. Sole access to the audit trail and to masters. |
| Supervisor | Everything except masters and deletion | Creates and assigns work, verifies completed jobs, manages schedules. Cannot edit the asset register structure or delete records. |
| Technician | Own assigned work only | Sees Work Orders where Assigned Technician resolves to their own user. Edits Task Checklist rows and Parts Used on those jobs. Read-only on assets. Cannot reassign, cannot verify, cannot sign off for anyone else. |
| Storekeeper | Parts and stock | Full access to Parts Master and Stock Movements for receipts, returns and adjustments. Read-only on work orders, so consumption can be traced but not altered. |
Creator is the system of record. Everything else either feeds it or reads from it — nothing else writes a work order except Desk, and it does so through one Flow with one key.
| Route | Mechanism | Behaviour |
|---|---|---|
| Zoho Desk → Creator | Zoho Flow | New ticket in a CAFM department creates a corrective Work Order with Desk Ticket ID stamped on it. Asset is matched on the ticket's asset field, falling back to unassigned for the supervisor to resolve. |
| Creator → Zoho Desk | Zoho Flow | Work order reaching Completed or Verified posts a comment and status back to the originating ticket, so the requester is updated without anyone retyping it. |
| Creator → Zoho Analytics | Native connector | Scheduled sync of Assets, Work Orders, Task Checklist, PPM Schedules, Parts Used and Stock Movements. Desk syncs separately; the two are joined inside Analytics. |
| Creator → WorkDrive | URL field | Each asset and vendor carries a folder link. Deliberately a link, not an embedded file store — documents stay where document version control already works. |
| Outlook | Zoho Flow | Email triggers for assignment and escalation notifications. |
| ERP | Not built | Connection points prepared only. Scoped and quoted once the client's ERP APIs are documented — see the proposal exclusions. |
Dependency order, not preference. Each step is testable on its own, and each one is blocked by the step above it. This maps onto weeks 2–3 of the proposal timeline.
One import, at week 5, in this order. The proposal covers a single import of client-supplied spreadsheets; historic cleansing and repeat migrations are excluded.
| Stage | Forms | Notes |
|---|---|---|
| Locations | Buildings, Floors, Zones, Rooms | Four sheets, imported parent-first. Rooms cannot import until Zones exist. Codes must be unique and stable — they end up on asset tags. |
| Masters | Vendors, Asset Categories, Technicians | Categories carry the ID prefix and defaults, so they must land before assets. Getting the prefix right before import avoids renumbering afterwards. |
| Assets | Assets | The big one. Asset ID is left blank on import and generated on insert. Room Code is the join key — a room that does not exist fails the row rather than creating an orphan. |
| Checklists | Checklist Templates, Template Items | Supplied by the client's maintenance team. Sequence numbered in tens. |
| Schedules | PPM Schedules | Start Date and Next Due Date seeded from the client's existing maintenance calendar, so the first daily run does not raise a year of backlog on day one. |
| Opening stock | Parts Master, Stock Movements | Balances enter as Receipt movements dated at go-live, never typed into Current Balance. The ledger has to be the only source of the balance from the first day. |
Carried forward from the handoff, plus what this specification surfaced. Each needs an answer before the phase it affects.
| Item | Detail |
|---|---|
| End client name | The proposal cover reads Prepared for DSquare Global. If this document reaches the Saudi facility management company directly, the cover and this spec's header both change. |
| Data residency | Government exposure may force the Saudi data centre. This is a Zoho account-provisioning decision and cannot be changed after the account is created — it needs answering before week 2. |
| Import templates | The xlsx templates for locations, assets and opening stock are listed above but not yet built. |
| Backlog seeding | Whether historic completed maintenance is loaded at go-live. Currently assumed not — the first PPM compliance report therefore starts from an empty baseline. |
| Sequence atomicity | The asset ID and movement number generators read-then-increment. Safe at five users; if parallel bulk import is ever expected, they need a locked Counters form. |
| Working-hours SLA | SLA clocks currently run continuously. If the client wants shift-based or business-hours-only timers, that is a Desk rebuild, not a Creator change — flagged in the proposal assumptions. |