Your Purchasing Process Is Costing You More Than You Think
Most mid-size companies still manage purchasing through a patchwork of email threads, shared spreadsheets, and verbal approvals. An operations manager emails three suppliers for quotes, copies the cheapest price into a PO template, prints it for a signature, and then manually reconciles the invoice against the delivery note two weeks later. Every step is a manual handoff—and every handoff is a place where errors, delays, and unauthorized spending creep in.
The cost is tangible. Maverick spending—purchases made outside the approved workflow—accounts for 20-30% of total procurement spend in organizations without enforced approval chains. Duplicate orders happen when the warehouse and the project manager both submit RFQs for the same part. Late deliveries go unnoticed because nobody tracks vendor lead-time performance systematically.
Odoo 19's Purchase module provides a complete procurement automation pipeline: from vendor onboarding and RFQ generation, through bid comparison and multi-level approval, to three-way matching and blanket order management. This guide walks through every stage of the RFQ-to-PO workflow, the automation rules that eliminate manual steps, and the configuration decisions that determine whether your procurement runs itself or creates more work.
Vendor Management and Evaluation in Odoo 19
Before you can automate purchasing, you need structured vendor data. Odoo's vendor management isn't just a contact list—it's a performance tracking system that feeds directly into RFQ routing, bid comparison, and reorder automation.
Configuring Vendor Pricelists
Every product in Odoo can have multiple vendor pricelists attached. These pricelists define who supplies what, at what price, with what minimum quantity and lead time. The system uses these records to auto-select vendors during RFQ generation:
# Vendor pricelist records live on product.supplierinfo
# Each record links a vendor to a product with pricing tiers
class ProductSupplierinfo(models.Model):
_inherit = "product.supplierinfo"
x_quality_rating = fields.Float(
string="Quality Rating",
digits=(3, 1),
help="Vendor quality score (0-10) based on "
"incoming inspection pass rate.",
)
x_on_time_rate = fields.Float(
string="On-Time Delivery %",
digits=(5, 2),
compute="_compute_on_time_rate",
store=True,
help="Percentage of POs delivered within "
"the promised lead time.",
)Vendor Evaluation Criteria
| Criteria | Source | Weight | Impact |
|---|---|---|---|
| Unit Price | product.supplierinfo.price | 40% | Auto-selects cheapest vendor for reorder rules |
| Lead Time | product.supplierinfo.delay | 25% | Factors into MTO and safety stock calculations |
| Minimum Order Qty | product.supplierinfo.min_qty | 15% | Determines order consolidation opportunities |
| On-Time Delivery | Computed from stock.picking dates | 10% | Flags unreliable vendors before RFQ generation |
| Quality Score | Custom field from QC inspections | 10% | Deprioritizes vendors with high rejection rates |
Set a second-source vendor on every critical product. If your primary vendor's lead time slips or they go out of stock, Odoo's reorder rules can automatically fall back to the next vendor in the pricelist sequence. Single-source dependency is a supply chain risk that Odoo can mitigate—but only if you configure the backup.
RFQ Creation: Manual, Reordering Rules, and Make-to-Order
Odoo 19 supports three distinct paths for creating Requests for Quotation. Each serves a different procurement strategy, and most implementations use all three simultaneously for different product categories.
Path 1: Manual RFQ Creation
The simplest approach—a procurement officer navigates to Purchase > Orders > Requests for Quotation and manually creates an RFQ. This is appropriate for one-off purchases, services, and capital expenditures that don't follow a recurring pattern:
# Create an RFQ via code (useful for integrations)
rfq = self.env['purchase.order'].create({
'partner_id': vendor.id,
'date_order': fields.Datetime.now(),
'order_line': [(0, 0, {
'product_id': product.id,
'product_qty': 100.0,
'price_unit': 0.0, # Vendor fills this in
'date_planned': fields.Datetime.now()
+ timedelta(days=product.seller_ids[0].delay),
})],
})
# Send RFQ to vendor by email
rfq.action_rfq_send()Path 2: Automatic RFQs via Reordering Rules
Reordering rules (also called min/max rules) are the backbone of automated procurement. When a product's on-hand quantity drops below the minimum threshold, Odoo's scheduler automatically generates an RFQ for the quantity needed to reach the maximum level:
<!-- Key fields on stock.warehouse.orderpoint -->
<field name="product_id"/> <!-- Which product -->
<field name="warehouse_id"/> <!-- Which warehouse -->
<field name="product_min_qty"/> <!-- Trigger threshold -->
<field name="product_max_qty"/> <!-- Target quantity -->
<field name="qty_multiple"/> <!-- Round up to multiples -->
<field name="route_id"/> <!-- Buy / Manufacture -->
<!-- The scheduler runs via cron:
Purchase > Configuration > Settings >
"Run Scheduler" or automatic daily cron -->Path 3: Make-to-Order (MTO)
MTO creates procurement on demand. When a sales order confirms a product configured with the MTO route, Odoo immediately generates an RFQ (or manufacturing order) for exactly the quantity sold. No inventory buffer, no reorder rules—procurement is triggered by customer demand:
# Enable MTO on a product
product = self.env['product.template'].browse(product_id)
mto_route = self.env.ref('stock.route_warehouse0_mto')
buy_route = self.env.ref('purchase_stock.route_warehouse0_buy')
# Product needs BOTH routes: MTO triggers the procurement,
# Buy route tells the system to create an RFQ (not an MO)
product.route_ids = [(6, 0, [mto_route.id, buy_route.id])]
# When a SO confirms with this product:
# 1. SO confirmation creates a delivery order (OUT)
# 2. MTO route detects no stock, creates procurement
# 3. Buy route converts procurement into RFQ
# 4. RFQ links back to the SO for traceabilityManual RFQs for one-off purchases (office furniture, consulting services). Reordering rules for stock-driven items with predictable demand (raw materials, packaging). MTO for expensive or custom items where you don't want to hold inventory (custom fabrication, drop-shipped goods). Mixing all three in the same Odoo instance is normal and expected.
Bid Comparison and Vendor Selection in Odoo 19
When you need competitive pricing, Odoo's Purchase Agreements (formerly called Calls for Tenders) let you send the same RFQ to multiple vendors and compare responses side by side. This is critical for high-value purchases where a 2% price difference represents thousands of dollars.
Setting Up a Purchase Agreement
# Enable Purchase Agreements in:
# Purchase > Configuration > Settings > Purchase Agreements
# Create a Call for Tenders
agreement = self.env['purchase.requisition'].create({
'type_id': self.env.ref(
'purchase_requisition.type_multi'
).id, # Multiple RFQs
'vendor_id': False, # No pre-selected vendor
'ordering_date': fields.Date.today(),
'schedule_date': fields.Date.today()
+ timedelta(days=14),
'line_ids': [(0, 0, {
'product_id': product.id,
'product_qty': 500.0,
'product_uom_id': product.uom_id.id,
})],
})
# Generate RFQs for each vendor
for vendor in vendor_list:
agreement.action_create_alternative(vendor)The Comparison Matrix
Once vendors respond with their quotes, the Compare Product Lines button displays a matrix showing unit price, lead time, and total cost per vendor. Odoo highlights the best price automatically, but procurement managers should evaluate the full picture:
| Factor | Vendor A | Vendor B | Vendor C |
|---|---|---|---|
| Unit Price | $12.50 | $11.80 | $12.10 |
| Lead Time | 5 days | 14 days | 7 days |
| MOQ | 100 units | 500 units | 50 units |
| On-Time History | 96% | 82% | 91% |
| Total Cost (500 units) | $6,250 | $5,900 | $6,050 |
In this example, Vendor B has the lowest price but the longest lead time and worst delivery track record. The cheapest bid isn't always the best bid. Odoo lets you select different vendors for different lines within the same purchase agreement—split-awarding based on each vendor's strengths.
Purchase Order Approval Workflows: Multi-Level, Amount-Based, and Role-Based
Confirming an RFQ into a Purchase Order is where spending actually gets committed. Without approval controls, any user with Purchase access can commit the company to six-figure obligations. Odoo 19 provides configurable approval workflows that enforce spending authority.
Enabling PO Approvals
# Purchase > Configuration > Settings
# ✓ Purchase Order Approval
# Set the minimum amount that requires approval
# The approval threshold is company-specific:
# Settings > Purchase Order Approval >
# Minimum Amount: 5,000.00
#
# POs below this amount: auto-confirmed
# POs at or above this amount: status = "To Approve"
# Only users with "Purchase Manager" group can approveMulti-Level Approval with Studio or Custom Module
The built-in approval is single-tier: below the threshold or above it. For enterprises that need multi-level approval (e.g., manager up to $10K, director up to $50K, VP above $50K), you need either Odoo Studio's Approval feature or a custom module:
class PurchaseOrder(models.Model):
_inherit = "purchase.order"
x_approval_level = fields.Selection([
('none', 'No Approval Needed'),
('manager', 'Manager Approval'),
('director', 'Director Approval'),
('vp', 'VP Approval'),
], compute="_compute_approval_level", store=True)
@api.depends('amount_total')
def _compute_approval_level(self):
for order in self:
total = order.amount_total
if total < 5000:
order.x_approval_level = 'none'
elif total < 25000:
order.x_approval_level = 'manager'
elif total < 100000:
order.x_approval_level = 'director'
else:
order.x_approval_level = 'vp'
def button_approve(self, force=False):
for order in self:
if not force and not order._check_approval_authority():
raise UserError(
"This PO requires %s approval. "
"Your role does not have sufficient "
"authority." % order.x_approval_level
)
return super().button_approve(force=force)Every PO approval in Odoo is logged in the chatter with the approver's name, timestamp, and the PO amount at the time of approval. This is your audit trail for compliance. If your industry requires SOX or ISO 9001 procurement controls, enable the "Lock Confirmed Purchase Orders" setting so approved POs cannot be edited without creating an amendment.
Three-Way Matching: PO, Receipt, and Vendor Bill Reconciliation
Three-way matching is the gold standard of procurement controls. Before paying a vendor invoice, you verify that what was ordered (PO) matches what was received (receipt) matches what was billed (vendor bill). Any discrepancy triggers a review. This prevents paying for undelivered goods, unauthorized price increases, and duplicate invoices.
Enabling Three-Way Matching
# Purchase > Configuration > Settings
# ✓ 3-way matching: purchases, receptions, and bills
#
# This adds a "Should Be Paid" status to vendor bills:
#
# - "Yes" = quantities and prices match across
# PO, receipt, and bill
# - "Exception" = mismatch detected, needs review
#
# The matching logic checks:
# 1. Billed qty <= Received qty (can't bill for
# items not yet delivered)
# 2. Billed price <= PO price (within tolerance)
# 3. Bill references a valid PO numberThe Matching Workflow
| Step | Document | Who | System Check |
|---|---|---|---|
| 1. Order | Purchase Order | Procurement | Approved amount and quantities recorded |
| 2. Receive | Receipt (stock.picking) | Warehouse | Received qty compared against PO qty |
| 3. Bill | Vendor Bill (account.move) | Accounts Payable | Billed qty/price checked against receipt and PO |
| 4. Pay | Payment | Finance | Only if "Should Be Paid" = Yes |
class AccountMove(models.Model):
_inherit = "account.move"
def _check_three_way_matching(self):
"""Custom tolerance for price matching.
Allow up to 2% price variance before flagging."""
for bill in self:
for line in bill.invoice_line_ids:
po_line = line.purchase_line_id
if not po_line:
continue
price_variance = abs(
line.price_unit - po_line.price_unit
) / po_line.price_unit
if price_variance > 0.02: # 2% tolerance
line.x_matching_status = 'exception'
line.x_matching_note = (
f"Price variance: "
f"{{price_variance:.1%}} "
f"(PO: {{po_line.price_unit}}, "
f"Bill: {{line.price_unit}})"
)Three-way matching handles partial deliveries gracefully. If you ordered 100 units, received 60, and the vendor bills for 60—that's a match. If they bill for 100 but you only received 60, the bill is flagged as an exception. Always process receipts before entering vendor bills. If the bill arrives first, the matching logic has no receipt to compare against and the bill stays in exception status until the warehouse confirms delivery.
Blanket Orders: Pre-Negotiated Pricing for Recurring Purchases
Blanket orders (also called framework agreements) let you negotiate pricing and terms with a vendor once, then release individual purchase orders against that agreement over time. Instead of negotiating every order, you lock in a price for a defined period or quantity and draw down as needed.
Configuring a Blanket Order
# Blanket orders use purchase.requisition with type "blanket"
blanket = self.env['purchase.requisition'].create({
'type_id': self.env.ref(
'purchase_requisition.type_blanket'
).id,
'vendor_id': vendor.id,
'ordering_date': fields.Date.today(),
'date_end': fields.Date.today()
+ timedelta(days=365), # 1-year agreement
'line_ids': [(0, 0, {
'product_id': product.id,
'product_qty': 10000.0, # Total agreed qty
'price_unit': 11.50, # Negotiated price
'product_uom_id': product.uom_id.id,
})],
})
blanket.action_in_progress() # Confirm the agreement
# Now any PO created for this vendor + product
# automatically uses the blanket order price.
# The system tracks remaining quantity against
# the total agreement.Blanket Order vs. Standard PO
| Feature | Standard PO | Blanket Order |
|---|---|---|
| Price negotiation | Per order | Once, for the agreement period |
| Delivery schedule | Single delivery | Multiple releases over time |
| Quantity commitment | Exact quantity ordered | Total volume over agreement term |
| Best for | One-off or irregular purchases | Recurring raw materials, MRO supplies |
| Vendor leverage | Limited | Volume discount from committed spend |
The real power of blanket orders emerges when you combine them with reordering rules. Set up a blanket order with your preferred vendor at negotiated pricing, then configure reordering rules on the same products. When stock drops below the minimum, the scheduler generates an RFQ that automatically pulls the blanket order price. No manual price lookup, no risk of someone entering the wrong rate. Procurement runs on autopilot at pre-negotiated rates.
3 Purchase Workflow Mistakes That Silently Cost You Money
Reordering Rules Without Accurate Lead Times
You configure reordering rules with a minimum quantity of 50 and a maximum of 200. The system triggers an RFQ when stock hits 50. But the vendor's lead time is 14 days, and your daily consumption is 10 units. By the time the order arrives, you've been out of stock for 9 days. The reorder point was set without accounting for demand during the lead time. The minimum quantity should be daily_demand × lead_time_days + safety_stock, not an arbitrary number.
Use the formula: Min Qty = (Average Daily Demand × Vendor Lead Time) + Safety Stock. In Odoo, set the product's vendor lead time on the product.supplierinfo record and the security lead time for purchase in Inventory Settings. The scheduler factors both into its calculations. Review and adjust quarterly based on actual consumption data from inventory reports.
Skipping Three-Way Matching to "Speed Up" Payments
Finance complains that three-way matching creates bottlenecks. Vendor bills sit in "exception" status because the warehouse hasn't processed the receipt yet. The CFO asks to disable matching "just temporarily" to clear the payment backlog. Six months later, the company discovers it paid $47,000 for goods that were never delivered and $12,000 in duplicate invoices that slipped through because nobody was comparing bills against receipts.
Don't disable matching—fix the bottleneck. The real problem is usually that receipts aren't being processed promptly. Implement barcode scanning for warehouse receipts (Odoo's Barcode app), set up email notifications when bills are waiting for receipts, and create a daily dashboard showing unmatched bills aged by days. A 2% price tolerance (configurable in your matching logic) eliminates false positives from rounding differences.
Not Consolidating Vendor RFQs from Multiple Source Documents
Three different sales orders trigger three separate MTO procurements for the same product from the same vendor. Odoo creates three separate RFQs instead of one combined order. You end up with three shipments, three invoices, and three sets of shipping charges. Worse, you miss the quantity discount threshold because each RFQ is below the vendor's MOQ for the discounted tier.
Enable "Merge RFQs" in Purchase Settings. When the scheduler creates a new RFQ for a vendor that already has a draft RFQ, it adds lines to the existing draft instead of creating a new one. For MTO flows, configure the purchase lead time on the vendor pricelist to create a consolidation window—the scheduler waits for the lead time buffer before sending, allowing multiple demands to batch into one order.
What Purchase Workflow Automation Saves Your Business
Procurement automation isn't about replacing buyers with software. It's about removing the repetitive tasks that prevent your procurement team from doing strategic work—vendor negotiations, spend analysis, and supply chain risk management.
Reordering rules and MTO flows generate RFQs automatically. Your procurement team stops copying product codes, quantities, and vendor details from emails into purchase orders.
Bid comparison surfaces better pricing. Blanket orders lock in volume discounts. RFQ consolidation reduces shipping costs and hits MOQ thresholds for tiered pricing.
Three-way matching catches overbilling, duplicate invoices, and charges for undelivered goods before payment. Every dollar saved goes straight to the bottom line.
For a company processing 200 purchase orders per month with an average PO value of $8,000, a 5% cost reduction through better bid management and vendor consolidation saves $960,000 annually. Factor in the 4 hours per week your AP team recovers from eliminating manual three-way matching, and the business case writes itself.
Optimization Metadata
Complete guide to automating RFQ-to-PO workflows in Odoo 19. Covers vendor management, reordering rules, bid comparison, PO approvals, three-way matching, and blanket orders.
1. "Vendor Management and Evaluation in Odoo 19"
2. "RFQ Creation: Manual, Reordering Rules, and Make-to-Order"
3. "Bid Comparison and Vendor Selection in Odoo 19"
4. "Purchase Order Approval Workflows"
5. "Three-Way Matching: PO, Receipt, and Vendor Bill Reconciliation"
6. "Blanket Orders: Pre-Negotiated Pricing for Recurring Purchases"