A maintenance ticket takes stock off the rentable inventory while it is being worked on. That is the whole point of it: a camera in the workshop must not be bookable, and the moment the ticket is closed it must be bookable again. These operations let you list, open, change, move on and delete tickets, add costs and photos, run service schedules and checklists, charge for damage and read the reports. They apply the same stock effects the admin screens do.
They come from the Maintenance extension (SalesIgniter_Maintenance 2.0.0 or later, which needs the Rental extension 1.2.216 or later), not the base rental extension. Every operation checks a permission on the token’s admin user; the permission is named in each section. The ticket operations need SalesIgniter_Maintenance::tickets to read and SalesIgniter_Maintenance::tickets_manage to write. A user with only SalesIgniter_Maintenance::tickets_own may work on tickets assigned to them. The admin screens are described in Tickets and the board.
Version 1’s operations still work. Their old field names (summary, start_date, end_date, maintainer_id, cost, comments, date_added, added_from) are kept and marked deprecated; the 2.0 names are in the tables below.
What a ticket actually holds #
Worth understanding before you write one, because a ticket holds stock in two different ways:
- Quantity. An open ticket places a reservation for its
quantity, so that many units come off the calendar. This is what holds stock for a product that is not serial-tracked. - Named serials. If you list
serials, those specific units are held by the ticket and stop being rentable individually. Each unit is one line of the ticket (lines).
“Open” means the ticket’s status takes units out of stock: blocks_stock is a flag on each status (rentalMaintenanceStatuses), not a hard-coded set of names. The built-in statuses are Reported (does not block), New, In progress, Waiting for parts and At vendor (block), and Ready to rent, Written off and Cancelled (closed, never block). Moving a ticket to a status that does not block releases the units. A ticket can also be opened with hold_mode: none (“Don’t block”): it is tracked, and the units stay rentable. Day and night products are held for whole days; timed products to the minute.
A ticket never cancels a booking. If the hold leaves future bookings short, the answer carries a conflict sentence naming them.
Opening a ticket #
createRentalMaintenanceTicket needs SalesIgniter_Maintenance::tickets_manage.
mutation SendToWorkshop {
createRentalMaintenanceTicket(input: {
sku: "CANON-R6"
serials: ["CR6-0011"]
title: "Sensor cleaning + shutter count check"
problem: "Not working"
description: "Customer reported dust spots on returned images."
kind: "repair"
priority: "high"
assignee_id: 3
hold_start: "2026-10-06 09:00:00"
expected_back: "2026-10-09"
}) {
conflict
ticket {
id
number
status_label
hold { mode start expected_back blocks_stock overdue }
assignee { id name }
serials
}
}
}
statusdefaults to New,hold_modeto your “New tickets block stock” setting,hold_startto now andsourcetoapi, so you can tell API-created tickets from ones raised in the admin or by a service schedule.expected_backleft out takes the “Usual repair time” setting;nullmeans open-ended. A bare date means the end of that day.quantityis for a product without serial numbers; withserialsit must match their count.- Every serial is validated. It must belong to that product, must not already be on another open ticket (the error names it), and must not be retired.
costadds an “other” cost line andcommentsadds a comment, as in version 1.
To send units from a booking the way the Return screen’s Needs service does, use sendRentalUnitToMaintenance. A serial that is already on an open ticket gets the problem added to that ticket (existing: true) instead of a second ticket.
Finding tickets #
rentalMaintenanceTickets and rentalMaintenanceTicket need SalesIgniter_Maintenance::tickets.
query WhatIsInTheWorkshop {
rentalMaintenanceTickets(
filter: { category: { eq: "open" }, priority: { in: ["high", "urgent"] } }
sort: { expected_back: ASC }
pageSize: 50
) {
total_count
items {
number
title
sku
product_name
serials
status_label
assignee { name }
hold { expected_back overdue }
}
page_info { current_page total_pages }
}
}
Filter on id, number, product_id, sku, status_id, status, category (open, waiting or closed), kind, priority, source, location, order_id and assignee_id (each {eq} or {in}), on hold_start, expected_back, created_at and updated_at as {from, to} ranges, and on serial as a {match} substring, which is how you answer “has this unit been in before?”. Sort on id, created_at, updated_at, hold_start, expected_back, priority or status_id; newest first by default. Fetch one with rentalMaintenanceTicket(id: 91) or rentalMaintenanceTicket(number: "MT-91").
rentalMaintenanceStatuses lists the statuses with their category and blocks_stock. rentalMaintenanceTechnicians lists the people tickets can be assigned to.
A ticket also carries its costs, vendor, charge, events (the timeline, oldest first) and attachments (photo details). Fields such as product_name and assignee come back null rather than erroring if the underlying product or technician has since been deleted, so old tickets stay readable.
Updating, moving on and closing #
updateRentalMaintenanceTicket needs tickets_manage. Only the fields you send are written.
mutation ChangeTheDate {
updateRentalMaintenanceTicket(id: 91, input: {
expected_back: "2026-10-12"
priority: "urgent"
add_serials: ["CR6-0012"]
}) {
ticket { number hold { expected_back } serials }
}
}
serialsreplaces the whole list, and the stock is reconciled against the change: units you dropped go back in, units you added come out. Sending a partial list by mistake therefore releases the ones you left out. Useadd_serialsandremove_serialsto change one unit at a time.- The product a ticket is against cannot be changed, because the serials belong to a product. Delete the ticket and open another.
Moving a ticket on goes through the same workflow as the admin buttons:
mutation Finish {
changeRentalMaintenanceTicketStatus(id: 91, action: "ready", note: "Sensor cleaned, shutter at 48k.") {
ticket { number status_label outcome closed_at serials }
}
}
Pass either a status (a code such as in_progress, waiting_parts, done, cancelled, or an id) or an action: start, ready, resume, wait_parts, send_vendor, back_from_vendor, confirm (a customer report), reopen or cancel. ready reaches Ready to rent: the units go straight back to Available, a running labour timer stops, and a service ticket records its service. A technician with only tickets_own may do this on a ticket assigned to them.
Other ticket operations:
| Operation | What it does |
|---|---|
addRentalMaintenanceTicketComment | Adds a comment to the timeline |
assignRentalMaintenanceTicket | Assigns a technician (technician_id), an admin user (admin_user_id), or yourself (me: true); no one unassigns |
addRentalMaintenanceTicketAttachment | Adds a photo (JPEG, PNG, WebP or HEIC as base64) |
retireRentalSerialNumber | Writes a unit off: retired, its ticket closed as written off, and by default the product’s stock reduced by one |
Deleting #
mutation Remove {
deleteRentalMaintenanceTicket(id: 91) { success id outcome }
}
Deleting gives the units back. A ticket with history (comments, status changes, costs, photos, an order, or one that is closed) is cancelled instead, with its history kept, unless you pass force: true; the answer’s outcome says deleted, cancelled or kept. Prefer closing a ticket by status to deleting it: a closed ticket keeps the maintenance history and cost against that unit, which is what makes serial filtering useful later. Delete is for tickets raised in error.
Costs, parts, vendors and damage charges #
| Operation | What it does | Permission |
|---|---|---|
addRentalMaintenanceTicketCost | Adds a cost line (kind: part, labour, vendor or other; a part_id from the parts list, minutes and rate, or an amount) | costs, or tickets_manage / tickets_own |
removeRentalMaintenanceTicketCost, setRentalMaintenanceCostBillable | Removes a line, or ticks “Bill to customer” | costs |
startRentalMaintenanceLabourTimer, stopRentalMaintenanceLabourTimer | The ticket’s labour timer | costs, tickets_manage or tickets_own |
saveRentalMaintenanceTicketVendor | The vendor card: vendor, reference, sent, due back, quote | costs |
rentalMaintenanceParts, rentalMaintenanceVendors | Read the parts and vendors lists | tickets |
saveRentalMaintenancePart, removeRentalMaintenancePart, saveRentalMaintenanceVendor, removeRentalMaintenanceVendor | Change the lists | costs |
chargeRentalMaintenanceDamage, actOnRentalMaintenanceDamageCharge | Charge the customer for damage, then collect, record, waive or resend | charge |
See Repair costs, parts and vendors and Damage charges for what these do.
Service schedules, checklists and inspections #
| Operation | What it does |
|---|---|
rentalMaintenanceSchedules, rentalMaintenanceDue | Service schedules, and each unit’s state per schedule (ok, due_soon, due, overdue, snoozed) |
snoozeRentalMaintenanceService, completeRentalMaintenanceService | Snooze a unit’s service, or record a service as done without a ticket |
rentalMaintenanceChecklists, saveRentalMaintenanceChecklist | Which serial custom field sets are checklists, and what fails |
rentalMaintenanceInspections, recordRentalMaintenanceInspection | Inspections recorded at check-out and check-in; fill a checklist for a unit outside a rental |
See Service schedules and the check-out guard and Checklists and photos.
Reports #
rentalMaintenanceReport(type: ..., filter: ...) returns one of the five reports as columns and rows, the same as its Export CSV, and rentalMaintenanceDashboard returns the downtime and cost figures of the month. See Maintenance reports and the dashboard.
Check-in and customer reports #
- The Rental extension’s return mutations take a
maintenanceinput for Needs service, and answer with the tickets opened, the failed inspections and the check-out guard warnings (maintenance_warnings).uploadRentalMaintenancePhotouploads a damage photo first; pass its id inmaintenance.photo_idswithin a day. - A customer’s token can call
reportRentalProblemfor a rental on one of their own orders (when “Customers can report a problem” is on). It opens a ticket in status Reported, which holds no stock until staff confirm it.CustomerOrder.rental_problem_reportslists the units a customer can report on, and their earlier reports. See Customer problem reports.
Tying it back to the stock #
A unit held by a ticket reports status: MAINTENANCE and a maintenance_ticket_id in rentalSerialNumbers, so you can go in either direction: from a serial to the work being done on it, or from a ticket to the units it holds.
query WhyIsThisUnitUnavailable {
rentalSerialNumbers(filter: { serial_number: { match: "CR6-0011" } }) {
items { serial_number status maintenance_ticket_id reservation_id }
}
}
