Building Quote to Order to Invoice Detection in SuiteCRM
A SuiteCRM case study about turning a quote into an order, turning an order into an invoice, and detecting what changed after billing so the CRM can create the right follow-up document.
A CRM workflow is easy to describe and harder to make reliable: quote, order, invoice. The real work is not only moving data from one module to another. The system also has to remember what was billed, detect later changes, and decide whether the next document should be a supplemental invoice, a credit note, or both.
I built this flow for a SuiteCRM installation that used the standard AOS quote and invoice modules plus a custom A3000 Orders module. The business needed accepted quotes to become service orders automatically, orders to invoice according to customer billing rules, and line item changes after invoicing to be visible without users manually comparing records.
That last part became the most important design decision. Instead of treating an invoice as a one-time copy of the order, the system stores a billing snapshot on each order line item. Later, when the order changes, the CRM compares the current line item against the invoiced snapshot and raises the correct action flag.
Closed Accepted
quote trigger
The quote after_save hook only runs when the stage changes into the accepted state.
1 join table
duplicate guard
A quote is converted once by checking the Quote to Order relationship before creating anything.
4 snapshot fields
invoice memory
Each order line stores invoiced quantity, price, product ID, and name after billing.
2 flags
next action
Orders expose invoice update and credit note flags so users know what to do next.
Quote to order starts with a narrow trigger
The first automation runs from an after_save hook on AOS_Quotes. It only fires when the quote stage changes to Closed Accepted. If the quote was already accepted before the save, the hook exits. That keeps ordinary quote edits from creating duplicate orders.
There is a second guard before any order is created. The hook checks the Quote to Order join table to see whether an order is already linked to the quote. If a relationship already exists, conversion stops. This matters because CRM users often save the same record many times after a deal is accepted.
Once the guards pass, the hook creates an A3000 Orders record with practical defaults: procurement status, not billed billing status, per-request service frequency, one-time job type, copied service dates, copied description, and the same assigned user.
The line items need special handling. In SuiteCRM's quote save process, the parent record can be saved before grouped line items have finished writing to the database. The conversion logic explicitly saves the quote's line item groups first, then clones the groups and line items onto the order with new IDs and a remapped group structure.
The order also inherits the important business relationships: account, products, vendors derived from product-vendor relationships, and site location. That makes the order operationally useful instead of being only a copied quote header.
The invoice service bills deltas, not guesses
The order-to-invoice service creates AOS_Invoices records from A3000 Orders. It supports normal invoices, credit notes, and a combined path for mixed changes. The combined path is important for product swaps because the system may need to invoice a new product and credit the old product from the same order change.
The first invoice includes all unbilled order lines. Later invoices only include the difference. If a line item quantity increases, the service invoices the extra quantity. If the price increases with the same quantity, it creates a price adjustment line. If a new item is added after the first invoice, only that new item is invoiced.
After invoice creation, the service links the invoice back to the order and updates the line item snapshot. The snapshot stores what the invoice just covered: quantity, unit price, product ID, and product name. From that point forward, the order has memory.
Detection is a comparison against invoice memory
The detection hook runs on AOS_Products_Quotes line items, but only when the parent type is A3000_Orders. That scope is deliberate. The same line item module is used by quotes and invoices, so the hook must avoid firing on unrelated records.
When an order line changes, the hook asks whether the order has ever had an invoice linked. If no invoice exists yet, snapshot fields are intentionally empty and should not be treated as a change. The hook clears both action flags and simply recalculates order totals.
Once invoices exist, the hook compares current line item values to the snapshot. An increase means the customer has not been billed enough, so the order gets an invoice update flag. A decrease means the customer has been billed too much, so the order gets a credit note flag. A product swap means both can be true at once.
| Change | Detection signal | CRM action |
|---|---|---|
| New line item after invoice | No invoiced quantity or price exists | Create supplemental invoice |
| Quantity or price increase | Current total is greater than snapshot total | Create supplemental invoice for the delta |
| Quantity or price reduction | Current total is lower than snapshot total | Create credit note |
| Product swap | Current product ID differs from invoiced product ID | Create both invoice and credit note |
| Deleted invoiced item | Deleted row still has invoiced snapshot values | Create credit note |
| Free item quantity change | Totals remain zero, so quantity is compared directly | Invoice or credit based on quantity direction |
The edge cases shaped the final design
Deletions needed their own hook because mark_deleted does not behave like an ordinary save. A removed line item can still carry invoiced snapshot values, and those values are exactly what the credit note calculation needs. The after_delete path reads the deleted row directly from the database and decides whether the removal created an over-billing situation.
Service lines needed another adjustment. Some service-style rows use product ID 0 and may not store quantity or unit price in the same way as product rows. The invoice service normalizes those rows by treating empty service quantity as one and using total price as the effective unit price when needed.
Free items also needed explicit handling. If both the old and new prices are zero, comparing totals cannot detect a quantity change. The hook compares quantity directly in that case so the CRM can still document added or removed free work.
SuiteCRM customization needs to survive repair and rebuild
The implementation uses SuiteCRM's extension framework for hook registration and vardef additions instead of editing compiled extension files directly. That is an important distinction. Compiled files under Ext output paths can be regenerated during repair and rebuild. The source customizations belong in the Extension framework files that SuiteCRM compiles from.
The action flags are stored on the main A3000 Orders table and are visible in Studio. The snapshot fields live on the line item table because detection has to happen at line-item granularity, not only at the order header. The result is a CRM that can answer a practical question quickly: what needs to happen next on this order?
The detail view then turns those flags into user-facing banners. New or increased charges show a Create Invoice action. Reductions show a Create Credit Note action. Mixed changes show Process All Changes so both documents are created before the snapshot is updated.
What this workflow taught me
- Lifecycle automation is not enough. Moving data from quote to order to invoice only solves the happy path.
- Invoices need memory. Without a per-line snapshot, every later comparison becomes unreliable or manual.
- Flags are better than silent behavior. Users should see whether the next step is an invoice, credit note, or combined process.
- Mixed changes must be processed atomically. Product swaps can need both documents before the snapshot changes.
- SuiteCRM extension discipline matters. Repair and rebuild should preserve the customization, not erase it.
Quick answers
Why not just regenerate the invoice from the order?
Because previous invoices may already exist, and replacing them can create accounting problems. The safer pattern is to leave existing invoices alone and create a supplemental invoice or credit note for the difference.
Why store snapshot fields on line items?
Order totals alone cannot explain what changed. A line-level snapshot can detect added items, removed items, quantity changes, price changes, free item changes, and product swaps.
Work with me
Need CRM automation that matches how your business bills?
I build SuiteCRM and PHP workflow systems that connect real business operations: quotes, orders, invoices, payments, dashboards, permissions, and automation that survives production use.