Guide 08 · Developer docs

Commerce

A Core dna store is a set of records and an API that moves a purchase from cart to order: products described in every language and priced in every currency the store sells in, a cart, a checkout that becomes an order, invoices, payments, tax and shipping. The site builds the storefront on top: the cart and checkout pages, their scripts, and the customer emails.

L2–L3
Unit 07 · Selling to businesses

Quotes and the member quote screens

A quote holds a member's cart items at prices your staff set, until an expiry date. How a member requests one, how staff price it, how the member orders it, and the two member screens a site supplies to list and show quotes.

Who this is for. Developers adding quotes to a store that sells to businesses. After this page you can put a "Request a quote" button on the cart, explain to the client how staff price a quote, and build the member's quote screens with an "Order at this price" button.

What a quote is

A quote is a copy of a member's cart items, held at prices your staff set, with an optional expiry date. It is stored beside checkouts and orders and belongs to the member who requested it. It holds the items with their names, quantities and prices, a comments field for staff, and the expiry date. Its prices are the ones the member orders at, which makes the quote the place to agree a price with a business customer.

The life of a quote

A quote passes through four steps. Who moves it on, and how:

StepWhoHow
RequestedThe member, signed inPosts the cart's item ids to POST /quotes/quote. The items are copied onto the quote at their cart price before discounts, and the member lands on the quote's page.
PricedStaff, in the adminSet each item's unit price (the line total is worked out again from price and quantity), the expiry date and the comments. This is where a negotiated price goes. The items, the currency and the member stay as requested.
OrderedThe member, until the expiry datePosts to POST /quotes/quote/<uuid>/restore. That creates a new checkout from the quote, at the quote's prices, with its items and metadata field values, and the member carries on through the normal checkout. The quote stays as it is while the checkout runs, and each request creates a new checkout.
RemovedThe platformWhen a checkout made from the quote is paid in full and becomes an order, the quote is removed.

Three messages come back from these requests. A signed-out visitor who asks for a quote is sent back to the cart with "You must be logged in to create a quote", and a request with no items with "No items provided". Ordering a quote after its expiry date is refused with "Quote has expired": staff can set a later date, or the member requests a new quote.

Keep the link between a quote and its order

A quote is removed once its order is placed. If the member or the sales team need to see which quote an order came from, store the quote's id in an order metadata field on the checkout before payment (Donations, metadata and gift options).

Telling people about a quote

A quote fires the record events a job can start on (Job event reference). A job on its Create record event can email the sales team that a quote was requested, and one on its Update record event can tell the member that their quote is priced. A reminder before a quote expires is a scheduled job that finds the quotes whose expiry date is near (Web events and schedules).

A "Request a quote" button on the cart

Put the form on the cart page, which receives cart, and show it to signed-in members. Each item[] is the id of a cart item to quote; this form sends them all.

Template code
<form method="post" action="/quotes/quote">
	<input type="hidden" name="_csrf" value="{$_csrfToken|default:''}">
	{foreach $cart->getItems() as $item}
		<input type="hidden" name="item[]" value="{$item->getId()}">
	{/foreach}
	<button>Request a quote</button>
</form>
<form method="post" action="/quotes/quote">
	<input type="hidden" name="_csrf" value="{{ _csrfToken ?? '' }}">
	{% for item in cart.getItems() %}
		<input type="hidden" name="item[]" value="{{ item.getId() }}">
	{% endfor %}
	<button>Request a quote</button>
</form>

Its currency, and the language of its lines

A quote is in one currency: the currency of the items copied from the cart, which holds one currency at a time (Prices and currencies). Staff enter its prices in that currency, and the checkout made from it carries on in it.

A line's name is copied from the cart line, which stores the product's name in the client's default language; the quote carries no language of its own. On a multilingual site, print each line in the shopper's language from its product, as on the checkout: take the line's product (getProduct()) and read its description in the visitor's language, with the default as the fallback (Reading the right description). Names and prices are copied when the quote is created, so a later change to a product's descriptions or prices leaves the quote as it was.

The member's quote screens

Two screens in the member area list a member's quotes and show one. Both require a signed-in member and show only that member's own quotes. The routes and records are in place, and the site supplies the two templates:

AddressTemplateReceives
/my-account/quotes, /my-account/quotes/<page>quotes/quotes/index.htmlquotes, a page of the member's quotes
/my-account/quote/<id>quotes/quotes/show.htmlquote

The list loops over quotes and links each one to /my-account/quote/<id>, with the same getters as the detail page. The detail page prints the lines, the total and the expiry date, and offers the "Order at this price" button, which posts to the restore route with the quote's uuid:

Template code quotes/quotes/show.html
<h1>Quote {$quote->getId()}</h1>
{if $quote->getExpiryDate()}<p>Valid until {$quote->getExpiryDate()|format:'date'}</p>{/if}
{foreach $quote->getItems() as $item}
	<p>{$item->getName()|escape} &times; {$item->getQuantity()} {$item->getTotalAsMoney()}</p>
{/foreach}
<p>Total {$quote->getTotalAsMoney()}</p>
<form method="post" action="/quotes/quote/{$quote->getUuid()}/restore">
	<input type="hidden" name="_csrf" value="{$_csrfToken}">
	<button>Order at this price</button>
</form>
<h1>Quote {{ quote.getId() }}</h1>
{% if quote.getExpiryDate() %}<p>Valid until {{ quote.getExpiryDate()|date('j F Y') }}</p>{% endif %}
{% for item in quote.getItems() %}
	<p>{{ item.getName()|e }} &times; {{ item.getQuantity() }} {{ item.getTotalAsMoney() }}</p>
{% endfor %}
<p>Total {{ quote.getTotalAsMoney() }}</p>
<form method="post" action="/quotes/quote/{{ quote.getUuid() }}/restore">
	<input type="hidden" name="_csrf" value="{{ _csrfToken }}">
	<button>Order at this price</button>
</form>

A quote and each of its lines print their totals with getTotalAsMoney(). Smarty's format modifier with 'date' prints the expiry date in the site's date format; Twig takes an explicit format. Both screens sit inside the member area's layout and menu, described on The built-in member screens.

Go deeper

Was this page helpful?

Your answer goes to the docs team.