Quotation Lifecycle
A quotation is a priced proposal you send a customer for approval. The lifecycle has two important properties: statuses are configurable, and once submitted, a quotation is locked. Both exist for the same reason — protecting the integrity of the document the customer agreed to.
The default statuses
Out of the box, TuffOps ships with these quotation statuses:
| Status | What it means | Editable? |
|---|---|---|
pending | Draft state. You're still pricing it. | Yes |
ready_to_review | Internal handoff — costed, waiting for office sign-off. | Yes |
submitted | Sent to the customer for approval. | Locked |
accepted | Customer agreed. Ready to convert to work orders. | Locked |
rejected | Customer declined. Closed out. | Locked |
fulfilled | All work orders spawned from this quotation are done. | Locked |
cancelled | Withdrawn before acceptance. Closed out. | Locked |
The list lives in Settings -> Quotation Statuses — you can add new ones (like pending_approval or revised) or rename the defaults. The names you see in the UI come from this list.
The locking rule
The status determines whether the quotation can still be edited. Five statuses lock the document: submitted, accepted, rejected, fulfilled, cancelled. The rest (pending, ready_to_review, plus any custom status you add) leave it editable.
When a quotation is locked:
- Items, prices, and quantities can't be changed.
- The customer, address, and lead source can't be changed.
- You can still add notes and photos (those are operational, not contractual).
The reason is simple: once the customer has agreed to a price, you don't want a stray edit to silently change what was agreed. If you need to change a locked quotation, you create a new one. The audit history keeps the old one intact for the record.
Why the rule isn't just "no edits after acceptance"
A quotation gets locked at submission, not at acceptance. That's deliberate. The version the customer sees is the version they're being asked to agree to — if you keep editing after sending, the customer's copy and yours drift, and arguments follow. Locking at submit removes the drift.
If you spot a mistake after submitting and the customer hasn't yet replied, the right move is:
- Cancel the current quotation (status ->
cancelled). - Create a new one with the corrections.
- Send the new one and explain the swap.
The history preserves both, so it's clear what changed and why.
The public link
Every quotation has a guid (a UUID) and a public URL of the form /q/<guid>. When you send a quotation, you're sending the customer this URL. The URL is opaque, non-guessable, and shows the quotation in a customer-friendly view (no internal notes, no costs — just prices, line items, and an Accept/Reject action).
The link works without authentication — the GUID is the secret. Don't put quotation GUIDs in public-facing pages or URLs that get indexed.
From quotation to work orders
Once a quotation is accepted, you convert it into work orders. The conversion is one-to-many — a single quotation typically produces several work orders, one per discrete job (one per unit, one per crew, one per visit, depending on how the work is structured).
Each spawned work order:
- Inherits the customer and address from the quotation.
- Carries a back-reference to the quotation (
work_orders.quotation_id), so the source is always visible. - Starts in the
unassignedorpendingstatus (see Work Order Lifecycle).
When all work orders born from a quotation reach completed (or billed), the quotation can move to fulfilled to close the loop.
Lead tracking
Every quotation captures a lead_type and lead_source. The default lead types are call, reference, website, and repair. The lead source is free-text — Yelp ad, referral from XYZ Property Management, walked in. Together they let you measure where work is coming from.