Sales Order Process & Viber Send
The Blu Coffee sales pipeline runs on Odoo's standard sale.order model, extended with the operational guardrails the team needs day-to-day:
- A required Mode of Payment so every quote already carries the receivable account it'll post against.
- Validator-only Confirm — the green button is replaced with a custom action that gates discount-bearing quotes behind a special-pricing approver group.
- SLA tracking — region-aware target days and a live On Time / At Risk / Delayed badge on every confirmed order.
- Send to Viber — fire the quote as a Viber chat message with a "View Quotation" button, in addition to email.
- Force Close — when a sale won't be invoiced, an authorized user closes the SO with a recorded reason instead of cancelling.
- Order tracking on the lists — every order shows how much was delivered, how much was collected, and its invoice, BIR SI, BIR CR and trip ticket numbers, all searchable from the search box.
- Pick-only addresses — the Customer, Invoice Address and Delivery Address fields only pick existing records, and the two address fields only offer the customer's own addresses.
For the whole journey from client profile to collection, see Order-to-Cash.
The plumbing lives in two places:
| Module | Provides |
|---|---|
bc17/blu_crm_sales/ | Mode of Payment, custom Confirm, Force Close wizard, Delivery / Collection Status, Invoice / SI / CR No. columns, list-view tweaks, custom TikTok print, sale-order-line On-Hand + Total Weight columns |
common/sla_sales_tracking/ | SLA fields, working-day math, region-based target lookup |
common/blu_8x8/ (blu_viber_sale_order.py) | Send to Viber button on the SO form |
Lifecycle
Cancel is non-destructive (no inventory or accounting impact) and only available before invoicing. Force Close is the safety valve for confirmed orders that have already moved inventory but won't be invoiced — see Force Close below.
Quotation list
Sales → Orders → Quotations opens the quotation list, grouped by Branch (the order's warehouse) and then Salesperson. Remove the Branch > Salesperson facet in the search box to see a flat list. The SLA columns come from sla_sales_tracking; the delivery, collection and document-number columns are described in Tracking an order from the list:

Once confirmed, Sales → Orders → Orders shows the live SO list, grouped the same way. The SLA Status column is shown by default; Expected Delivery and Days Remaining are optional columns you can toggle on:

Tracking an order from the list
You can follow an order from the list without opening it. These columns are on the Quotations, Orders and SQ & SO Dates List screens. They are all shown by default, and you can hide any of them with the column toggle (⇌) at the right end of the header row.
| Column | What it tells you |
|---|---|
| Invoice Status | Standard Odoo: To Invoice, Fully Invoiced, Upselling, Nothing to Invoice |
| Delivery Status | How much of the order has gone out: Not Delivered (red), Partially Delivered (orange), Fully Delivered (green). Nothing to Deliver (grey) means a confirmed order with nothing to ship, such as a service-only order. Quotations leave it blank. |
| Collection Status | From the order's posted invoices: No Invoice, Not Collected, In Payment (a payment is recorded but not yet matched to the invoice), Partially Collected, Fully Collected, Reversed |
| Invoice No. | Odoo invoice number(s), e.g. INV-2026-09-1241. Drafts and cancelled invoices are left out. |
| SI No. | The BIR Sales Invoice number Accounting keys on the invoice (Official Invoice #). Blank until it is encoded. |
| CR No. | The BIR Collection Receipt number keyed on the payment (Customer Receipt) of every payment applied to the order's invoices. Blank until such a payment is matched to the invoice. |
| Trip Ticket | The trip(s) that carried the order's deliveries. See Trip Ticket. |
| Actually Delivered | Hidden by default. The date the client received the goods, from the post-validated trip ticket. |
When an order was invoiced, collected or delivered in more than one go, the number columns list every number, separated by commas. One CR can show on several orders when one receipt paid several invoices.
An order can be Fully Collected while still Not Delivered. That means the client has paid and the goods have not gone out. Use the Collected but Undelivered filter to list these orders.
Credit notes and bank-statement matches carry no CR number. If an order is Fully Collected and the CR No. is blank, the payment was usually posted without its Customer Receipt number. Ask Accounting to add it on the payment, and the order picks it up.
Filters and grouping
The Filters menu has a block for collection (Not Collected, In Payment (unreconciled), Partially Collected, Fully Collected, No Invoice) and one for delivery (Not Delivered, Partially Delivered, Fully Delivered, Collected but Undelivered). Group By adds Branch, Delivery Status and Collection Status.
Searching by invoice, SI or CR number
Type or paste an order number, an INV- number, an SI number or a CR number in the search box and press Enter. The default search, labelled Order / Invoice / SI / CR, checks all of them, plus the customer reference and the customer. To search one kind of number only, pick its line from the dropdown:

The form at a glance
Open any draft and you'll see the customised header:

| Element | Where it comes from | Notes |
|---|---|---|
| Send to Viber button | common/blu_8x8 | Posts the quote as a Viber buttoned message to partner_id.phone. See Send to Viber below. |
| Send by Email button | Standard Odoo | Exposed regardless of permission group (recent change) — recipient is partner_id.email. |
| Confirm button | bc17/blu_crm_sales (rts_action_confirm) | Replaces Odoo's default. Visible only to validator + ecomm groups. If any line has discount > 0, an extra special-pricing group check fires. |
| Cancel button | Standard, gated with a confirmation dialog ("Click OK button to confirm Cancel action") | |
| Mode of Payment field | bc17/blu_crm_sales (account_id) | Required. Domain limited to receivable / cash accounts flagged sales_mop_account=True. |
| Payment Terms field | Standard, but marked required="1" by bc17 | |
| Customer field | bc17/blu_base + bc17/blu_crm_sales | Pick only. You cannot create or open a contact from here, and branch addresses and contact persons are not offered. See Customers & Addresses. |
| Invoice Address / Delivery Address | bc17/blu_crm_sales | Pick only. They offer only the selected customer and its own addresses. Empty until a customer is chosen. |
| Warehouse (Other Info tab) | bc17/blu_crm_sales | Filled in from your default warehouse. The order's delivery (DR) is taken from this warehouse's stock when the order is confirmed. |
Order line On-Hand column | bc17/blu_crm_sales (qty_available) | Live stock display for the line's product, related to product.qty_available. |
Order line Total Weight column | bc17/blu_crm_sales (total_weight) | Computed = product_uom_qty × product.weight. |
Only the Customer's own addresses can be picked as Invoice or Delivery Address. If the branch you are delivering to is not in the list, add it to the customer first, in Contacts → the customer → Contacts & Addresses (how):

Since 10/08/2026, confirming an order takes its delivery (DR) from the stock of the order's own warehouse. Before that, the DR was moved to the confirming user's default warehouse, so a Mandaluyong user confirming a Pampanga order made the DR deduct Mandaluyong stock. Check the Warehouse on the order before confirming. The only exception is e-Comm: an e-Comm user still ships from their e-Comm stock.
Mode of Payment
Each quote must declare where the receivable will post. The picker is restricted to accounts tagged sales_mop_account=True so reps can't accidentally select an arbitrary GL account:
112101 ACCOUNTS RECEIVABLE— default cash receivable112105 ACCOUNTS RECEIVABLE - TIKTOK112106 ACCOUNTS RECEIVABLE - GCASH112108 ACCOUNTS RECEIVABLE - PAYPAL112109 ACCOUNTS RECEIVABLE - XENDIT112110 ACCOUNTS RECEIVABLE - SHOPEE- …and the other channel receivables (PayMaya, Lazada, Web, and the PNB / BDO / Global Payments credit-card accounts)
The field is tracking=True so changes are logged in chatter.
SLA tracking
Every confirmed SO gets a Service Level Agreement group inside the form, comparing the order date against the customer's region delivery target:

Fields
| Field | Computed from |
|---|---|
| Region | partner_id.region_id.region_name (related, stored) |
| Target Days | partner_id.region_id.delivery_time (working days), with fallbacks: NCR (region code 130000000) → 4 days; everywhere else → 12 days |
| Expected Delivery | date_order + target_days, skipping weekends and entries in the sla.holiday model |
| Actual Delivery | Date the last outgoing picking went to state done |
| SLA Status | One of: On Time, At Risk, Delayed, Delivered On Time, Delivered Late, N/A |
| Days Remaining | Working days from today to Expected Delivery (negative = overdue) |
| Variance (Days) | Working days between Actual and Expected (positive = early, negative = late) |
Status logic
Search filters
The Quotations / Orders search bar gets five state filters and two group-bys courtesy of sla_sales_tracking:
| Filter | What it shows |
|---|---|
| On Time | Confirmed SOs still within their SLA window with >2 days to spare |
| At Risk | Confirmed SOs with ≤2 working days remaining |
| Delayed | Confirmed SOs past their Expected Delivery with no completed picking |
| Delivered On Time | Closed orders that hit the window |
| Delivered Late | Closed orders that missed it |
Plus group-bys for SLA Status and Region — useful for weekly sales-ops reviews.
The numbers come from res.region.delivery_time. To change the target for a whole region, update it there — every existing SO for partners in that region recomputes on its next save. There's no need to touch SOs one-by-one.
Current settings: NCR / Region III / CAR / Region VII = 3 working days, all other regions = 5 working days.
For the full configuration walkthrough — region SLA editing, holiday management, customer-region prerequisites, and the hardcoded fallbacks — see Inventory → Delivery Orders SLA → Configuration.
…and on every Delivery Order
The same SLA badge now appears on outgoing pickings in Inventory → Operations → Transfers, so warehouse and dispatch staff can spot at-risk deliveries without opening the underlying SO. The picking-side fields are stored related copies of sale_id.sla_*, so there's only one source of truth.

See Inventory → Delivery Orders SLA for the column toggles, filters, and dispatcher workflow.
Send to Viber
The Send to Viber button lets a salesperson push the quote straight to the customer's Viber number. It reuses the 8x8 messaging sub-account already configured for marketing campaigns — there's no separate setup.
What it does
- Reads
partner_id.phone, validates and sanitises to E.164 (+639xxxxxxxxx). - Generates the standard Odoo customer-portal preview URL for the quote (
action_preview_sale_order()['url']). - POSTs a Viber
buttonspayload tohttps://chatapps.8x8.com/api/v1/subaccounts/<messaging_sub_account>/messages:- Greeting personalised with the customer's name.
- Salesperson's signature with their own phone number.
- View Quotation#:
<SO name>button → portal preview URL.
- On 200 OK, logs the response UMID as a
blu.viber.campaign.partnersrow (so delivery webhooks update its state alongside marketing-campaign messages) and posts a chatter line.
What the customer receives
The customer's journey is two taps — Viber message arrives on their phone, they tap the button, and they're on Odoo's standard public portal where they can review, accept, sign, or reject:


The portal page is fully self-serve — the customer can:
- Accept & Sign electronically (top-left on mobile, bottom-centre on desktop) to confirm the quotation. The SO state flips to Sale Order on Odoo's side immediately.
- View Details to drill into product lines or download the PDF.
- Feedback to add a comment that lands in the SO's chatter.
- Reject to decline (the SO state moves to Cancelled).
They don't need an Odoo login — the portal URL carries the access token from the chat-button payload.
What the SO records
Right after the click, the SO's chatter logs the send so the sales manager can see at a glance that the quote went out:

The chatter entry includes the Viber Message ID (UMID) — same identifier the marketing-campaign delivery webhook uses. Once 8x8 fires the corresponding outbound_message_status_changed event, the matching blu.viber.campaign.partners row's state updates to queued → sent → delivered → read.
If partner_id.phone is missing or unparseable, the action raises <Customer> has no valid Viber/Phone Number and the SO state is unchanged. No partial send happens — clean up the contact first, then retry. Tip: Viber numbers go in the Phone field, not Mobile.
Confirm — and the special-pricing gate
The standard green Confirm button is replaced with rts_action_confirm(). The custom action does three things:
- Special-pricing check — if any line has
discount > 0, the user must be inbc17.blu_sales_order_special_pricing_validators_group, otherwise it raises "You are not allowed to confirm quotations with special pricing." - Confirm visibility — the button itself is only rendered for users in
blu_sales_order_validators_group, the special-pricing validators, or the ecomm admin/user groups. Rank-and-file salespeople can build quotes but not confirm them. - Warehouse routing — after
action_confirm(), every open delivery (draft / waiting / ready) is sourced from the stock location of the order's warehouse. If the confirming user's default warehouse is an e-Comm warehouse, the e-Comm stock is used instead. If the location changes, any stock already reserved is released and reserved again from the new location.
A pre-confirm dialog ("Click OK button to confirm your action") fires on every press to prevent fat-finger commits.
Sales orders cannot be deleted by anyone except Blu Coffee Administrators, so the order numbering has no gaps. Cancel a quotation instead of deleting it.
Force Close — when a sale won't be invoiced
Sometimes inventory has shipped and a final invoice still won't happen — a swap, a goodwill write-off, a discontinued customer. Cancelling isn't right (the moves happened), so the team uses Force Close:

The Close this Order button (with the ⚠️ icon) is only visible when invoice_status == 'to invoice' — i.e. there are lines still expecting an invoice. Clicking it opens a wizard that requires a reason before proceeding.
When confirmed, the action:
- Sets
invoice_status = 'no'on the SO and on every line whose status was'to invoice'. - Stores the reason on the SO (
force_close_so_reason). - Stamps
force_close = True, the closer's user_id (force_close_uid), and the timestamp (force_close_date). - Posts a chatter line: "Forced closing of Sales Order:
<reason>".
After that, the SO disappears from invoicing queues. The reason and closer are stored in tracking fields, so an auditor can find them on any closed order.
SQ & SO Dates List
bc17/blu_crm_sales adds a sales reporting list at Sales → Reporting → SQ & SO Dates List that returns a read-only, tightly-columned view of every confirmed sale (state = sale). It shows confirmed orders only and cannot be used to create, edit or delete — it is designed for reviewing the dates pipeline and for exporting to Excel.

It surfaces:
- Create date, order date, commitment date, expected date
- Order number, customer, salesperson, team
- Invoice status with colour badges (Fully Invoiced / To Invoice / Upselling)
- Delivery Status, Collection Status, Invoice No., SI No., CR No. and Trip Ticket (see Tracking an order from the list)
- Tags, untaxed / tax / total amounts (hidden by default)
Branch on the Sales Report
Blu Reports → Sales → Sales Report (Vatable) lists invoice lines grouped by Branch, the warehouse of the sales order behind the line.

Two fixes in 10/2026 stopped lines landing in a blank (false) Branch group:
- Lines added by hand on the invoice (10/02/2026). A line typed onto the invoice, rather than brought over from the order, has no link to an order line. It now takes the branch of the order behind the rest of the invoice, or of the order named in the invoice's Source Document.
- The first line of a new invoice (10/09/2026). The first line, and the only line of a one-line invoice, used to come out blank until someone refreshed it.
Lines posted before the fix can still be in the false group. Select them in the report and run Action → Refresh Branches.
Reference — code & data map
| Concern | File |
|---|---|
sale.order extension (account_id, force_close, rts_action_confirm, default warehouse, collection / delivery status, invoice / SI / CR numbers, TikTok print) | bc17/blu_crm_sales/models/sale_order.py |
| Top-level-only Customer picker | bc17/blu_base/models/partner_id_domain.py |
| Delete block (Blu Coffee Administrators only) | bc17/blu_base/models/blu_no_delete.py |
| Branch on Sales Report lines | bc17/blu_reports/models/account_move_line.py |
sale.order.line extension (qty_available, total_weight) | bc17/blu_crm_sales/models/sale_order_line.py |
| Force close wizard | bc17/blu_crm_sales/wizard/force_close_so_wizard.py |
| SO form patches (Mode of Payment, custom Confirm, Close this Order button, address pickers), list columns, filters, search | bc17/blu_crm_sales/views/sale_order_view.xml |
| SQ & SO Dates List + custom list view | bc17/blu_crm_sales/views/quotation_order_view.xml |
| SLA fields + working-day calculations | common/sla_sales_tracking/models/sale_order.py |
| SLA list/form/search view patches | common/sla_sales_tracking/views/sale_order_views.xml |
SLA on stock.picking (related fields) | common/sla_sales_tracking/models/stock_picking.py |
| Delivery-Orders tree + search inherit | common/sla_sales_tracking/views/stock_picking_views.xml |
| Holiday lookup for working-day math | common/sla_sales_tracking/models/sla_holiday.py |
| Send to Viber action | common/blu_8x8/models/blu_viber_sale_order.py |
| Send to Viber button injection | common/blu_8x8/views/blu_viber_sale_order_views.xml |
Groups used
| Group | Effect |
|---|---|
bc17.blu_sales_order_validators_group | Sees Confirm button, Close this Order button. |
bc17.blu_sales_order_special_pricing_validators_group | Same as above + can confirm quotes with discounted lines. |
bc17.blu_sales_invoicing_group | Sees Create Invoice + Create Invoice Percentage buttons. |
bc17.blu_ecomm_admin_group / bc17.blu_ecomm_user_group | Can confirm + invoice ecomm orders. |
bc17.blu_admin_group (Blu Coffee Administrators) | The only users who can delete a sales order. |
Custom sale.order fields
| Field | Type | Notes |
|---|---|---|
account_id | Many2one (account.account) | Mode of Payment, required, tracked |
force_close | Boolean | True after Force Close wizard fires |
force_close_so_reason | Text | Reason captured in wizard |
force_close_date | Datetime | When it was closed |
force_close_uid | Many2one (res.users) | Who closed it |
is_ecomm_sale | Boolean | Set by ecomm flow on create |
collection_status | Selection (stored) | no_invoice/not_paid/in_payment/partial/paid/reversed, from posted invoices |
delivery_status | Selection (stored) | sale_stock's pending/started/partial/full, plus nothing for confirmed orders with nothing to ship |
invoice_no | Char (stored) | Odoo invoice numbers, comma-separated |
si_no | Char (stored) | BIR SI numbers (account.move.si_number), comma-separated |
cr_no | Char (stored) | BIR CR numbers (account.payment.cr_number) of reconciled payments, comma-separated |
trip_ticket_no / actual_delivery_date | Char / Date (stored) | From the deliveries' trip tickets |
sla_target_days | Integer | Region-based |
sla_expected_delivery_date | Date | Computed |
sla_actual_delivery_date | Date | From done outgoing pickings |
sla_status | Selection | on_time/at_risk/delayed/delivered_on_time/delivered_late/not_applicable |
sla_days_remaining | Integer | Working days |
sla_days_variance | Integer | Working days; positive = early |
sla_region_name | Char (related) | From partner's region |