Reference build

Ledger: the quotation that is a web page, and asks the ERP before it shows a price

The revision cycle is the cost centre in B2B quoting. A customer who wants to see the number without one option sends an email and waits a day. This is that day, removed.

A hairline quotation document with toggled line items.

The quotation, running

A fabrication quotation with four optional lines and six quantities that move within agreed bands. Toggle something. Watch the status in the header — the page marks its previous prices stale, waits for simulated repricing, and only then displays the updated amounts.

A reference build with an invented customer. This standalone demo includes a mock pricing service in the browser, with volume discounts, an illustrative tax calculation and a short delay. It makes no ERP requests and creates no orders. In production, Odoo owns the pricing rules and returns the amounts the page displays.

What a PDF actually costs

The standard B2B quotation is a PDF with a "recommended options" section. What happens next is so routine that nobody counts it:

  1. The customer wants to see the number without the roller shutters.
  2. They send an email, or they annotate the PDF, or they telephone.
  3. Someone reopens the quotation, edits it, regenerates it, re-sends it.
  4. A day passes. Sometimes three.
  5. Two revisions later nobody is certain which version is current, and both sides are working from different attachments.

Every step there is a person doing arithmetic a computer already knows how to do. And the delay is not neutral: a quotation loses momentum with every day it sits, and the competitor who answered in an hour is now the one being compared against.

The interesting number is not the salesperson's time, though that is real. It is that the customer's question goes unanswered while they are still interested in the answer.

The rule: Odoo owns the price

This is the entire architecture, and it is the thing that separates a useful tool from a liability.

If the page can compute a price the ERP would not honour, you have built a liability.

It is tempting to copy the pricing rules into the browser. Changes feel instant and there is no network call to wait for. That convenience comes with a second set of rules to maintain.

Pricing in a real business is not qty × unit. It is customer-specific pricelists, volume breaks, contract rates, currency, tax treatment per line, a discount matrix, and whatever one salesperson agreed in 2023 that is now a permanent term. Copy those rules into JavaScript and you have created a second pricing engine that is correct on the day it ships and wrong the first time somebody edits a pricelist — silently, and in the customer's favour or yours, both of which are bad.

So: the page owns the configuration. The ERP owns the price. On every change the page marks itself stale, asks, and waits.

You can see the interaction in the demo. Take the cladding quantity to 180 m² or more and the unit price drops with a small marker beside it. The mock service applies that volume break after a delay. Until it finishes, the previous amounts remain marked stale and acceptance is unavailable.

The distinction matters when you inspect the source. This demo calculates unit prices, line amounts and illustrative tax locally so it can run on a static page. In production, Odoo must return the unit prices, line amounts, tax and final total for the priced revision. Multiplying a displayed unit price by a quantity is not enough to reproduce an invoice: precision, discounts, tax treatment and rounding can change the result.

The round trip gives the customer an ERP-priced quotation to review. The commercial guarantee depends on what happens next: the server validates the revision being accepted and records its agreed configuration and amounts together.

Bands, not free text

Quantities move between a minimum and a maximum agreed when the quotation is built. The customer can take four to twelve bays, not any number they like.

Those bands keep each quantity within an agreed range. They do not establish that every combination of lines is deliverable. Where one option depends on another, the configuration needs that relationship too. In production, the server must enforce the bands, locked lines and dependencies when repricing and again when accepting; the browser controls alone cannot validate an order.

The same logic applies to the locked lines. Base scope is shown, priced and not removable, because a structural frame is not optional and offering a checkbox next to it would imply otherwise.

The general principle, which we apply in configurators too: make invalid combinations unrepresentable rather than merely disabled. It is cheaper to constrain the data than to explain the constraint in a tooltip.

The by-product: intent data nobody had

Every toggle is a signal you have never previously been able to collect.

Today a salesperson knows what was quoted and what was ordered. They do not know that this customer switched the solar-ready reinforcement on, looked at it, and switched it off again — twice — before accepting. That is a price objection on a specific line, observed rather than guessed at.

Across a quarter this becomes genuinely strategic: which options get considered and rejected, which get accepted without hesitation, which quantities get pushed to the top of their band. That is product and pricing information, and it currently evaporates into an email thread.

One caution, and it belongs in the same breath: tell the customer the page is instrumented. A quotation that silently reports the buyer's hesitation to the seller is the kind of thing that reads badly when discovered. Where those events identify a buyer, the PDPA obligations apply. Put a brief disclosure beside the quotation and link it to a privacy notice covering what is collected, why, who receives it and the customer's rights. The disclosure introduces that notice; it does not replace it or the other obligations involved in handling personal data. The Commissioner's notice guidance sets out the requirements.

Accepting, and what has to be frozen

Acceptance is where a nice interface becomes a commercial instrument, and it has to be exact.

  • Every change increments a revision. The customer accepts rev 7, not "the quotation". Both parties can name the same thing afterwards.
  • Only a priced revision can be accepted. The demo blocks acceptance while repricing is pending. In production, the server must also check that the submitted revision is still valid and matches the configuration and amounts the customer reviewed. If it has changed, the customer reviews and accepts the updated quotation.
  • Acceptance freezes it. The server validates and records the configuration, unit prices, line amounts, tax, total, timestamp and revision in the same transaction that confirms the order. That accepted record remains unchanged if a pricelist changes an hour later.
  • It writes a real order. Not a notification email for someone to re-key — the confirmed sales order in the ERP, from the same data the customer was looking at. The whole point is that nobody re-types the specification.
  • Expiry is enforced, not printed. "Valid 30 days" on a PDF is decoration. Here the link stops accepting after it lapses, and says so.

Building it on Odoo

On an existing Odoo instance this is a focused piece of work, because most of it already exists.

  • Odoo already has the quotation, the pricelist and the portal. The sales order and its lines are the model; the customer portal is the delivery route. You are adding an interaction layer, not a system.
  • Optional lines need a flag and a band. Two fields on the order line, plus the rule that only a salesperson can set them.
  • Repricing uses Odoo's own pricing and tax calculations. The controller validates the configuration, applies it to the quotation and returns Odoo's prices, line amounts, tax and total with the priced revision. No pricing logic is written by us, which is exactly the point.
  • The interface is roughly the demo above. Under nine kilobytes, no framework, and it works on the phone the buyer is actually holding.

The parts that take the time are never the interface — they are deciding which lines are genuinely optional, agreeing the bands, and the conversation with whoever currently approves a discount.

Of everything on this site, this is the piece with the most direct return and the one clients ask for least, because nobody thinks of a quotation as software. It is the most-read document your company produces. Let your customers revise the quotation while they're still interested.

Questions

It is the useful half of one, without the platform. Full configure-price-quote suites are substantial products with a rules engine, an approval matrix and a licence to match. Most Malaysian SMEs quoting configurable work need something much narrower: the customer can see the price of the version they actually want, and accepting it writes a real order. That is weeks, not a programme.

In production, the page displays the prices, tax and totals returned by Odoo. Acceptance stays unavailable while repricing is pending. When the customer accepts, the server checks the quotation's expiry, configuration and priced revision before confirming the order. If that revision is no longer valid, the customer must review the updated quotation and accept again. The server records the agreed amounts with the accepted revision.

No, and requiring one is the most common way this fails. The customer opens a signed link with an expiry. A login is a wall between your customer and their own quotation, and it will be the reason they go back to asking by email.

They get one, generated from the same quotation at the same revision. Plenty of buyers need a document for their own approval process, and the answer is never to fight that. The web page is where the decision happens; the PDF is the artefact that goes in a folder.

Have a version of this problem?

A technical review with the engineer who would do the work. No pitch deck, no discovery invoice.