Sales Order Documentation¶
Overview¶
The Sales Order module (sale.order) is the core model for managing the entire sales order lifecycle in Netforce. It handles customer orders from initial draft creation through reservation, confirmation, delivery, and completion. This model serves as the central hub for sales operations, integrating with invoicing, inventory management, purchasing, production, and shipping. It supports multi-company operations, tracks costs and profit margins, manages promotions and vouchers, and provides comprehensive order tracking capabilities.
Model Information¶
Model Name: sale.order
Display Name: Sales Order
Key Fields: company_id, number
Features¶
- ✅ Audit logging enabled (
_audit_log = True) - ✅ Multi-company support (
_multi_company = True) - ✅ Full-text content search (
_content_search = True) - ✅ Unique key constraint per company (composite key:
company_id+number)
Understanding Key Fields¶
What are Key Fields?¶
In Netforce models, Key Fields are a combination of fields that together create a composite unique identifier for a record. Think of them as a business key that ensures data integrity across the system.
For the sale.order model, the key fields are:
This means the combination of these fields must be unique:
- company_id - The company that owns this sales order (Many2One to "company")
- number - The sales order number (automatically generated from sequence)
Why Key Fields Matter¶
Uniqueness Guarantee - Key fields prevent duplicate records by ensuring unique combinations:
# Examples of valid combinations:
Company A, SO-001 ✅ Valid
Company A, SO-002 ✅ Valid
Company B, SO-001 ✅ Valid (different company, same number is OK)
# This would fail - duplicate key:
Company A, SO-001 ❌ ERROR: Sales order number already exists for this company!
Database Implementation¶
The key fields are enforced at the database level using a unique constraint:
# The framework automatically creates a unique constraint for _key fields
# Equivalent to:
CREATE UNIQUE INDEX sale_order_unique_key
ON sale_order (company_id, number);
This ensures that each sales order number is unique within a company, allowing different companies to use the same numbering sequence without conflicts in multi-company environments.
State Workflow¶
| State | Description |
|---|---|
draft |
Initial state when order is created. Order can be edited freely and deleted. |
reserved |
Stock is reserved for this order. Reservation picking may be created based on settings. |
confirmed |
Order is approved and confirmed. Triggers creation of pickings, invoices, purchase orders, or production orders based on settings. Cannot be deleted. |
done |
Order is completed. All deliveries and invoices are finalized. |
voided |
Order is cancelled. Associated pickings may be voided. Cannot be deleted. |
Key Fields Reference¶
Header Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
number |
Char | ✅ | Sales order number (auto-generated from sequence) |
ref |
Char | ❌ | External reference or customer PO number |
memo |
Char | ❌ | Internal memo or notes |
date |
Date | ✅ | Order date (defaults to current date) |
state |
Selection | ✅ | Order status (draft/reserved/confirmed/done/voided) |
contact_id |
Many2One | ✅ | Customer contact (must have customer=True) |
contact_person_id |
Many2One | ❌ | Contact person at customer location |
user_id |
Many2One | ❌ | Sales order owner/responsible user |
company_id |
Many2One | ❌ | Company (defaults to active company) |
currency_id |
Many2One | ✅ | Currency for order amounts |
tax_type |
Selection | ✅ | Tax calculation method (tax_ex/tax_in/no_tax) |
Customer Information Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
customer_name |
Char | ✅ | Customer name |
customer_phone |
Char | ✅ | Customer phone number |
customer_dob |
Date | ❌ | Customer date of birth |
customer_address1 |
Char | ✅ | Customer address line 1 |
customer_address2 |
Char | ✅ | Customer address line 2 |
customer_postal_code |
Char | ✅ | Customer postal code |
customer_city |
Char | ✅ | Customer city |
customer_state |
Char | ❌ | Customer state/province |
customer_country |
Selection | ✅ | Customer country (MY/SG/TH/ID/VN/PH/MM) |
customer_fb_account |
Char | ❌ | Customer Facebook account |
customer_discount_amount |
Decimal | ❌ | Customer-specific discount amount |
Amount Fields¶
| Field | Type | Description |
|---|---|---|
amount_subtotal |
Decimal | Subtotal excluding tax (computed, stored) |
amount_tax |
Decimal | Total tax amount (computed, stored) |
amount_total |
Decimal | Total amount including tax and freight (computed, stored) |
amount_total_cur |
Decimal | Total converted to base currency (computed, stored) |
amount_total_discount |
Decimal | Total discount amount (computed, stored) |
amount_subtotal_before_discount |
Decimal | Subtotal before applying discounts (computed, stored) |
amount_total_words |
Char | Amount in words for printing |
qty_total |
Decimal | Total quantity of all line items (computed) |
freight_charges |
Decimal | Additional freight/shipping charges |
Line Items¶
| Field | Type | Description |
|---|---|---|
lines |
One2Many | Order line items (sale.order.line) |
Address Fields¶
| Field | Type | Description |
|---|---|---|
bill_address_id |
Many2One | Billing address (linked to address model) |
ship_address_id |
Many2One | Shipping/delivery address |
addresses |
One2Many | All related addresses |
Shipping & Delivery Fields¶
| Field | Type | Description |
|---|---|---|
ship_method_id |
Many2One | Shipping method |
ship_tracking |
Char | Tracking numbers (computed from pickings) |
ship_term_id |
Many2One | Shipping terms (FOB, CIF, etc.) |
ship_port_id |
Many2One | Shipping port |
ship_time |
Char | Shipping time |
due_date |
Date | Shipping date (ETD - Estimated Time of Departure) |
delivery_date |
Date | Delivery date (ETA - Estimated Time of Arrival) |
delivery_slot_id |
Many2One | Delivery time slot |
ninjavan_tracking_number |
Char | NinjaVan specific tracking number |
ninjavan_account |
Many2One | NinjaVan courier account |
dhl_account |
Many2One | DHL courier account |
courier_remarks |
Text | Remarks for courier service |
Integration Fields¶
| Field | Type | Description |
|---|---|---|
invoices |
Many2Many | Related invoices (computed) |
invoices_direct |
One2Many | Direct invoice relationship |
invoice_lines |
One2Many | Invoice lines linked to this order |
pickings |
Many2Many | Related stock pickings (computed) |
stock_moves |
One2Many | Stock movements for this order |
purchase_lines |
One2Many | Purchase order lines created from this order |
purchase_orders |
One2Many | Purchase orders generated |
production_orders |
One2Many | Production orders for manufacturing |
job_orders |
One2Many | Job/service orders |
jobs |
One2Many | Service orders |
transforms |
One2Many | Stock transformations |
Cost & Profit Fields¶
| Field | Type | Description |
|---|---|---|
costs |
One2Many | Manual cost entries (sale.cost) |
est_costs |
One2Many | Estimated costs |
cost_amount |
Decimal | Total cost amount (computed) |
profit_amount |
Decimal | Total profit amount (computed) |
margin_percent |
Decimal | Profit margin percentage (computed) |
est_cost_amount |
Float | Estimated cost amount (computed) |
est_profit_amount |
Float | Estimated profit amount (computed) |
est_margin_percent |
Float | Estimated margin percentage (computed) |
act_cost_amount |
Float | Actual cost amount (computed) |
act_profit_amount |
Float | Actual profit amount (computed) |
act_margin_percent |
Float | Actual margin percentage (computed) |
act_mfg_cost |
Decimal | Actual manufacturing cost (computed) |
est_cost_total |
Decimal | Legacy estimated cost total (computed, stored) |
est_profit |
Decimal | Legacy estimated profit (computed, stored) |
est_profit_percent |
Decimal | Legacy estimated profit % (computed, stored) |
act_cost_total |
Decimal | Legacy actual cost total (computed, stored) |
act_profit |
Decimal | Legacy actual profit (computed, stored) |
act_profit_percent |
Decimal | Legacy actual profit % (computed, stored) |
Promotion & Discount Fields¶
| Field | Type | Description |
|---|---|---|
coupon_id |
Many2One | Applied coupon |
used_promotions |
One2Many | Promotions applied to this order |
voucher_id |
Many2One | Applied discount voucher |
is_voucher_applied |
Boolean | Whether voucher has been applied (computed) |
Payment Fields¶
| Field | Type | Description |
|---|---|---|
pay_method_id |
Many2One | Payment method |
pay_term_id |
Many2One | Payment terms |
transaction_no |
Char | Payment transaction number |
is_paid |
Boolean | Whether order is fully paid (computed) |
Price & Tax Fields¶
| Field | Type | Description |
|---|---|---|
price_list_id |
Many2One | Price list for calculating product prices |
tax_type |
Selection | Tax calculation type (tax_ex/tax_in/no_tax) |
Tracking & Organization Fields¶
| Field | Type | Description |
|---|---|---|
track_id |
Many2One | Accounting tracking category |
track_entries |
One2Many | Tracking entries |
track_balance |
Decimal | Tracking balance (computed) |
sale_channel_id |
Many2One | Sales channel (online, retail, wholesale, etc.) |
sale_categ_id |
Many2One | Sales category |
seller_id |
Many2One | Seller entity |
seller_contact_id |
Many2One | Seller contact person |
project_id |
Many2One | Related project |
Status & Workflow Fields¶
| Field | Type | Description |
|---|---|---|
is_delivered |
Boolean | Whether order is fully delivered (computed) |
is_invoiced |
Boolean | Whether order is fully invoiced (computed) |
is_paid |
Boolean | Whether order is fully paid (computed) |
production_status |
Json | Production order status summary (computed) |
overdue |
Boolean | Whether order is overdue (computed) |
state_label |
Char | Human-readable state label (computed) |
approved_by_id |
Many2One | User who approved the order |
Related Records Fields¶
| Field | Type | Description |
|---|---|---|
related_id |
Reference | Source document (quotation, cart, PO, etc.) |
quot_id |
Many2One | Related quotation (deprecated) |
quote_id |
Many2One | Related quotation |
orig_sale_id |
Many2One | Original sales order (for returns/copies) |
sale_orders |
One2Many | Child/related sales orders |
Communication & Documents¶
| Field | Type | Description |
|---|---|---|
comments |
One2Many | Comments/messages on this order |
emails |
One2Many | Email messages |
documents |
One2Many | Attached documents |
activities |
One2Many | Sales activities |
other_info |
Text | Additional remarks/information |
Reporting & Analytics Fields¶
| Field | Type | Description |
|---|---|---|
year |
Char | Year from order date (SQL function) |
quarter |
Char | Quarter from order date (SQL function) |
month |
Char | Month from order date (SQL function) |
week |
Char | Week from order date (SQL function) |
date_week |
Char | Week grouping (computed) |
date_month |
Char | Month grouping (computed) |
due_date_weekday |
Char | Shipping date weekday (computed) |
report_no |
Char | Report number for daily reporting |
receipt_printed |
Boolean | Whether receipt has been printed |
incl_report |
Boolean | Include in report (search function) |
Legacy/Specialized Fields¶
| Field | Type | Description |
|---|---|---|
location_id |
Many2One | Default warehouse location (deprecated) |
payment_terms |
Text | Payment terms text (deprecated) |
team_id |
Many2One | Production team |
sequence_id |
Many2One | Number sequence used |
job_template_id |
Many2One | Service order template |
currency_rates |
One2Many | Custom currency rates |
agent_id |
Many2One | Notify-to agent/forwarder |
agent_person_id |
Many2One | Notify-to contact person |
agent_address_id |
Many2One | Notify-to address |
pic_id |
Many2One | Person in charge (employee) |
gross_weight |
Decimal | Gross weight for shipping |
net_weight |
Decimal | Net weight for shipping |
customer_date |
Date | Customer-specified date |
raw_mats |
Json | Raw materials summary (computed) |
so_company_id |
Many2One | Sales order company override |
customer_postal_code_backup |
Char | Postal code backup |
customer_postal_code2 |
Integer | Postal code alternative format |
Aggregate Fields (for reporting)¶
| Field | Type | Description |
|---|---|---|
agg_amount_total |
Decimal | Sum of amount_total |
agg_amount_total_cur |
Decimal | Sum of amount_total_cur |
agg_amount_subtotal |
Decimal | Sum of amount_subtotal |
agg_est_profit |
Decimal | Sum of est_profit |
agg_act_profit |
Decimal | Sum of act_profit |
Search-Only Fields¶
| Field | Type | Description |
|---|---|---|
product_id |
Many2One | Search by product in order lines |
product_categ_id |
Many2One | Search by product category |
API Methods¶
1. Create Sales Order¶
Method: create(vals, context)
Creates a new sales order record and triggers function store calculations.
Parameters:
vals = {
"contact_id": contact_id, # Required: Customer
"date": "2024-01-15", # Required: Order date
"currency_id": currency_id, # Required: Currency
"tax_type": "tax_ex", # Required: Tax type
"customer_name": "John Doe", # Required: Customer name
"customer_phone": "+1234567890", # Required: Phone
"customer_address1": "123 Main St", # Required: Address
"customer_address2": "Suite 100", # Required: Address 2
"customer_postal_code": "12345", # Required: Postal code
"customer_city": "New York", # Required: City
"customer_country": "MY", # Required: Country
"lines": [ # Order line items
("create", {
"product_id": product_id,
"qty": 10,
"unit_price": 100,
"uom_id": uom_id
})
]
}
context = {
"sequence_id": seq_id, # Optional: Use specific sequence
}
Returns: int - New record ID
Example:
# Create a basic sales order
order_id = get_model("sale.order").create({
"contact_id": 123,
"date": "2024-01-15",
"currency_id": 1,
"tax_type": "tax_ex",
"customer_name": "ACME Corp",
"customer_phone": "+60123456789",
"customer_address1": "123 Business Park",
"customer_address2": "Building A",
"customer_postal_code": "50000",
"customer_city": "Kuala Lumpur",
"customer_country": "MY",
"lines": [
("create", {
"product_id": 456,
"description": "Widget A",
"qty": 5,
"unit_price": 150.00,
"uom_id": 1,
"tax_id": 2
})
]
})
2. Reserve Sales Order¶
Method: reserve(ids, context)
Reserves stock for the sales order and optionally creates reservation picking.
Parameters:
- ids (list): Sales order IDs to reserve
Behavior: - Validates user has "reserve_sale" permission if required - Changes state from "draft" to "reserved" - Creates reservation picking if setting enabled - Triggers "reserve" workflow event
Returns: dict - Navigation info and flash message
Example:
result = get_model("sale.order").reserve([order_id])
# Returns: {
# "next": {"name": "sale", "mode": "form", "active_id": order_id},
# "flash": "Sales order SO-001 reserved"
# }
Permission Requirements:
- reserve_sale: Required if settings.reserve_sale_reserve is enabled
3. Confirm Sales Order¶
Method: confirm(ids, context)
Confirms the sales order and triggers downstream processes (invoicing, picking, production, etc.).
Parameters:
- ids (list): Sales order IDs to confirm
Behavior: - Validates user has "approve_sale" permission if required - Validates order is in "draft" or "reserved" state - Checks minimum price constraints on all line items - Changes state to "confirmed" - Creates picking if settings.sale_copy_picking is enabled - Creates invoice if settings.sale_copy_invoice is enabled - Creates production orders if settings.sale_copy_production is enabled - Triggers "confirm" workflow event
Returns: dict - Navigation info and flash message
Example:
result = get_model("sale.order").confirm([order_id])
# Returns: {
# "next": {"name": "sale", "mode": "form", "active_id": order_id},
# "flash": "Sales order SO-001 confirmed"
# }
Exceptions: - "User does not have permission to approve sales orders" - Missing approve_sale permission - "Invalid state" - Order not in draft or reserved state - "Product X is below the minimum sales price" - Price validation failed
Permission Requirements:
- approve_sale: Required if settings.approve_sale is enabled
4. Mark as Done¶
Method: done(ids, context)
Marks confirmed sales orders as completed.
Parameters:
- ids (list): Sales order IDs to mark as done
Behavior: - Validates order is in "confirmed" state - Changes state to "done"
Example:
Exceptions: - "Invalid state" - Order must be in confirmed state
5. Void Sales Order¶
Method: void(ids, context)
Cancels/voids the sales order and handles associated documents.
Parameters:
- ids (list): Sales order IDs to void
Behavior: - Validates no invoices are in "waiting_payment" status - If central courier integration enabled: - Cancels NinjaVan orders - Cancels DHL orders - Voids associated pickings - Otherwise validates no pending pickings exist - Changes state to "voided"
Example:
Exceptions: - "There are still invoices waiting payment for this sales order" - "There are still pending goods issues for this sales order"
6. Return to Draft¶
Method: to_draft(ids, context)
Returns sales order to draft state for editing.
Parameters:
- ids (list): Sales order IDs to return to draft
Behavior: - Changes state to "draft" - No validations (use with caution)
Example:
7. Create Delivery Picking¶
Method: copy_to_picking(ids, context)
Generates stock picking (delivery order) from sales order lines.
Parameters:
- ids (list): Sales order ID (only first ID is processed)
Behavior: - Creates customer delivery picking (type="out") - Groups lines by shipping address, method, and due date - Only includes stock/consumable/bundle products with qty > 0 - Calculates remaining quantity to deliver (qty - qty_delivered) - Sets location_from based on line reservation or default warehouse - Sets location_to to customer location - Links picking to sales order via related_id
Returns: dict - Navigation info, flash message, and picking IDs
Example:
result = get_model("sale.order").copy_to_picking([order_id])
# Returns: {
# "next": {"name": "pick_out", "mode": "form", "active_id": picking_id},
# "flash": "Picking created from sales order SO-001",
# "picking_ids": [picking_id],
# "picking_id": picking_id
# }
Exceptions: - "Customer location not found" - No customer location configured - "Warehouse not found" - No internal location configured - "Missing shipping date for sales order X" - No due_date set - "Nothing left to deliver" - All items already delivered
8. Create Invoice¶
Method: copy_to_invoice(ids, context)
Generates customer invoice from one or more sales orders.
Parameters:
- ids (list): Sales order IDs to invoice (can process multiple)
Behavior: - Validates all orders are in "confirmed" state - Can combine multiple orders if same customer, currency, tax type - Creates invoice lines from order lines - Includes promotion discounts - Sets payment terms and due date - Links invoice lines to sales order
Returns: Invoice creation result
Example:
# Invoice single order
get_model("sale.order").copy_to_invoice([order_id])
# Invoice multiple orders together
get_model("sale.order").copy_to_invoice([order_id1, order_id2])
Exceptions: - "Sales order must be confirmed first" - "Sales orders are for different customers" - "Sales orders have different billing address" - "Sales orders have different currencies" - "Sales orders have different tax types" - "Sales orders have different payment methods" - "Sales orders are in different companies"
9. Create Purchase Orders¶
Method: copy_to_purchase(ids, context)
Generates purchase requisitions for products with supply_method="purchase".
Parameters:
- ids (list): Sales order ID (only first ID is processed)
Behavior: - Groups order lines by supplier and due date - Only processes products with supply_method="purchase" - Creates purchase orders linked to sales order - Calculates purchase quantity based on UOM conversion - Sets supplier from line or product default - Calculates purchase date based on purchase lead time
Returns: dict - Flash message and order IDs
Example:
result = get_model("sale.order").copy_to_purchase([order_id])
# Returns: {
# "flash": "Purchase orders created successfully",
# "order_ids": [po_id1, po_id2]
# }
Exceptions: - "Missing supplier for product X" - "Missing shipping date in sales order"
10. Create Production Orders¶
Method: copy_to_production(ids, context)
Generates production/manufacturing orders for products with supply_method="production".
Parameters:
- ids (list): Sales order IDs to process
Context Options:
Behavior: - Groups order lines by product and due date - Only processes products with supply_method="production" - Validates BoM (Bill of Materials) exists - Creates production order with components and operations - Links to sales order lines - Calculates production date based on mfg_lead_time
Returns: dict - Flash message and order IDs
Example:
result = get_model("sale.order").copy_to_production([order_id])
# Returns: {
# "flash": "Production orders created successfully",
# "order_ids": [prod_id1, prod_id2]
# }
Exceptions: - "BoM not found for product X" - "Missing FG location in BoM X" - "Missing production location in BoM X" - "Missing shipping date in sales order X" - "Production order already created for sales order X, product Y"
11. Calculate Amounts¶
Method: get_amount(ids, context)
Computes all amount fields for the sales order.
Parameters:
- ids (list): Sales order IDs to calculate
Behavior: - Calculates subtotal from line amounts - Computes tax based on tax_type (tax_ex/tax_in/no_tax) - Applies promotion discounts from used_promotions - Adds freight charges to total - Converts total to base currency - Stores all computed values
Returns: dict - Dictionary of computed values per order ID
Example:
amounts = get_model("sale.order").get_amount([order_id])
# Returns: {
# order_id: {
# "amount_subtotal": 1000.00,
# "amount_tax": 100.00,
# "amount_total": 1100.00,
# "amount_total_cur": 1100.00,
# "amount_total_discount": 50.00,
# "amount_subtotal_before_discount": 1050.00
# }
# }
12. Apply Voucher¶
Method: apply_voucher(ids, context)
Applies a discount voucher to the sales order.
Parameters:
- ids (list): Sales order ID (only first ID is processed)
Behavior: - Validates voucher not already applied - Validates voucher is selected - Calls voucher's apply_voucher method with order context - Creates negative line item for discount amount - Links voucher product to line
Example:
# First set voucher on order
get_model("sale.order").write([order_id], {"voucher_id": voucher_id})
# Then apply it
get_model("sale.order").apply_voucher([order_id])
Exceptions: - "Voucher is already applied" - "No voucher selected" - Error messages from voucher validation (minimum amount, product restrictions, etc.)
13. Check Minimum Prices¶
Method: check_min_prices(ids, context)
Validates all order lines meet minimum price requirements.
Parameters:
- ids (list): Sales order IDs to validate
Behavior: - Checks each line's unit_price against product.sale_min_price - Raises exception if any line is below minimum
Example:
Exceptions: - "Product X is below the minimum sales price"
UI Events (onchange methods)¶
onchange_contact¶
Triggered when customer contact is selected. Updates order with customer defaults.
Updates:
- contact_person_id - Default contact person
- pay_term_id - Customer payment terms
- price_list_id - Customer price list
- bill_address_id - Default billing address
- ship_address_id - Default shipping address
- seller_contact_id - Assigned seller
- currency_id - Customer currency or system default
Usage:
data = {
"contact_id": 123,
}
result = get_model("sale.order").onchange_contact(
context={"data": data}
)
# Returns updated data with populated fields
onchange_product¶
Triggered when product is selected in order line. Populates line item details.
Updates:
- description - Product description or name
- qty - Default quantity (1)
- uom_id - Sale UOM or base UOM
- discount - Customer default discount
- unit_price - From price list or product sale_price
- tax_id - Product tax or customer tax
- location_id - Default location from product
- reserve_location_id - Reservation location
- cost_price - Product cost price
- qty2 / uom2_id - Secondary UOM if auto-convert enabled
Usage:
data = {
"contact_id": 123,
"currency_id": 1,
"price_list_id": 5,
"lines": [
{
"product_id": 456
}
]
}
result = get_model("sale.order").onchange_product(
context={
"data": data,
"path": "lines.0" # Path to the line being edited
}
)
# Returns data with populated line fields and updated amounts
onchange_qty¶
Triggered when quantity changes in order line. Recalculates price based on quantity tiers.
Updates:
- unit_price - Updated from price list based on quantity
- qty2 - Secondary quantity if auto-convert enabled
- Recalculates all amounts
Usage:
data = {
"contact_id": 123,
"currency_id": 1,
"price_list_id": 5,
"lines": [
{
"product_id": 456,
"qty": 100 # New quantity
}
]
}
result = get_model("sale.order").onchange_qty(
context={
"data": data,
"path": "lines.0"
}
)
# Returns data with updated unit price and amounts
Search Functions¶
Search by Product¶
Searches sales orders containing a specific product (including variants and components):
# Search for orders with product ID 123
condition = [["product_id", "=", 123]]
order_ids = get_model("sale.order").search(condition)
Search by Product Category¶
Searches sales orders containing products in a specific category (including child categories):
# Search for orders with products in category 456
condition = [["product_categ_id", "child_of", 456]]
order_ids = get_model("sale.order").search(condition)
Search by Date Range¶
Search by State¶
# All confirmed orders
condition = [["state", "=", "confirmed"]]
# All active orders (not voided)
condition = [["state", "!=", "voided"]]
Search by Customer¶
Search Overdue Orders¶
Computed Fields Functions¶
get_amount(ids, context)¶
Calculates amount_subtotal, amount_tax, amount_total, amount_total_cur, amount_total_discount, and amount_subtotal_before_discount based on line items, tax type, promotions, and freight charges.
get_invoices(ids, context)¶
Returns list of related invoices by searching invoice lines linked to this sales order.
get_pickings(ids, context)¶
Returns list of related stock pickings by searching pickings with related_id="sale.order,{id}".
get_delivered(ids, context)¶
Returns True if all order lines are fully delivered (qty_delivered >= qty for all lines).
get_paid(ids, context)¶
Returns True if all related invoices are paid (state="paid").
get_invoiced(ids, context)¶
Returns True if all order lines are fully invoiced.
get_production_status(ids, context)¶
Returns JSON with production order status: {"num_done": X, "num_total": Y}.
get_overdue(ids, context)¶
Returns True if due_date is in the past and order is not done/voided.
get_ship_tracking(ids, context)¶
Compiles tracking numbers from all related pickings.
get_profit(ids, context)¶
Calculates cost_amount, profit_amount, and margin_percent based on line costs and actual costs.
get_est_profit(ids, context)¶
Calculates estimated cost, profit, and margin from sale.cost entries.
get_act_profit(ids, context)¶
Calculates actual cost, profit, and margin from completed deliveries and production orders.
get_act_mfg_cost(ids, context)¶
Calculates actual manufacturing cost from completed production orders.
Workflow Integration¶
Trigger Events¶
The sales order module fires workflow triggers:
self.trigger(ids, "reserve") # When order is reserved
self.trigger(ids, "confirm") # When order is confirmed
These can be configured in workflow automation to: - Send email notifications - Create tasks or activities - Update external systems - Trigger custom business logic
Best Practices¶
1. Use Reserve for Stock Allocation¶
# Bad: Confirming without checking stock availability
get_model("sale.order").confirm([order_id])
# Good: Reserve first to allocate stock, then confirm
get_model("sale.order").reserve([order_id])
# Check stock availability
get_model("sale.order").confirm([order_id])
Reserve state allows you to allocate stock without committing to delivery, useful for managing inventory in high-demand scenarios.
2. Handle Order Modifications Properly¶
# Bad: Modifying confirmed orders directly
obj.write({"lines": [...]}) # Can cause inconsistencies
# Good: Return to draft, modify, then re-confirm
get_model("sale.order").to_draft([order_id])
obj.write({"lines": [
("create", {...}),
("write", [line_id, {...}]),
("delete", [old_line_id])
]})
get_model("sale.order").confirm([order_id])
Note: Use to_draft with caution - ensure no invoices or deliveries are pending.
3. Integration with Inventory and Invoicing¶
# Good: Let settings control automatic document creation
# Configure in Settings > Sales:
# - sale_copy_picking: Auto-create delivery on confirm
# - sale_copy_invoice: Auto-create invoice on confirm
# - sale_copy_production: Auto-create production orders
# Then just confirm:
get_model("sale.order").confirm([order_id])
# Manual approach when needed:
get_model("sale.order").confirm([order_id])
# Then manually trigger specific documents:
get_model("sale.order").copy_to_picking([order_id])
get_model("sale.order").copy_to_invoice([order_id])
4. Multi-Company Considerations¶
# Ensure company context is set when creating orders
set_active_company(company_id)
order_id = get_model("sale.order").create({
"company_id": company_id, # Explicitly set company
"number": "SO-001", # Unique per company
...
})
# Remember: number uniqueness is per company
# Company A can have SO-001
# Company B can also have SO-001
5. Price List and Currency Handling¶
# Good: Set currency and price list before adding lines
order_vals = {
"contact_id": contact_id,
"currency_id": currency_id,
"price_list_id": price_list_id,
"lines": [...] # Prices will be calculated correctly
}
# Bad: Adding lines before setting currency/price list
# Can cause incorrect pricing
6. Voucher Application Timing¶
# Good: Apply voucher after all lines are added
order_id = get_model("sale.order").create({...})
# Add all lines first
get_model("sale.order").write([order_id], {
"lines": [("create", {...}), ...]
})
# Then apply voucher
get_model("sale.order").write([order_id], {"voucher_id": voucher_id})
get_model("sale.order").apply_voucher([order_id])
# Bad: Applying voucher before finalizing lines
# Discount may be calculated incorrectly
Database Constraints¶
Unique Key Constraint¶
-- Automatically enforced by _key = ["company_id", "number"]
CREATE UNIQUE INDEX sale_order_unique_key
ON sale_order (company_id, number);
This ensures each sales order number is unique within a company, preventing duplicates while allowing multi-company use of same numbering sequences.
Related Models¶
| Model | Relationship | Description |
|---|---|---|
sale.order.line |
One2Many (lines) | Order line items with products, quantities, prices |
contact |
Many2One (contact_id) | Customer contact information |
account.invoice |
Many2Many (invoices) | Customer invoices generated from order |
account.invoice.line |
One2Many (invoice_lines) | Invoice lines linked to order |
stock.picking |
Many2Many (pickings) | Delivery orders for shipment |
stock.move |
One2Many (stock_moves) | Stock movements for deliveries |
purchase.order |
One2Many (purchase_orders) | Purchase requisitions for procurement |
purchase.order.line |
One2Many (purchase_lines) | Purchase order lines |
production.order |
One2Many (production_orders) | Manufacturing orders |
job.order |
One2Many (job_orders) | Service/job orders |
job |
One2Many (jobs) | Service orders |
sale.cost |
One2Many (costs) | Manual cost entries |
sale.promotion |
One2Many (used_promotions) | Applied promotions |
sale.coupon |
Many2One (coupon_id) | Discount coupon |
sale.voucher |
Many2One (voucher_id) | Discount voucher |
sale.quot |
Many2One (quot_id, quote_id) | Source quotation |
price.list |
Many2One (price_list_id) | Pricing rules |
payment.method |
Many2One (pay_method_id) | Payment method |
payment.term |
Many2One (pay_term_id) | Payment terms |
ship.method |
Many2One (ship_method_id) | Shipping method |
ship.term |
Many2One (ship_term_id) | Shipping terms (Incoterms) |
ship.port |
Many2One (ship_port_id) | Shipping port |
address |
Many2One (bill_address_id, ship_address_id) | Billing and shipping addresses |
currency |
Many2One (currency_id) | Transaction currency |
company |
Many2One (company_id) | Owning company |
sale.channel |
Many2One (sale_channel_id) | Sales channel |
sale.categ |
Many2One (sale_categ_id) | Sales category |
seller |
Many2One (seller_id) | Seller entity |
project |
Many2One (project_id) | Related project |
account.track.categ |
Many2One (track_id) | Tracking category |
message |
One2Many (comments) | Comments and messages |
email.message |
One2Many (emails) | Email communications |
document |
One2Many (documents) | Attached documents |
sale.activ |
One2Many (activities) | Sales activities |
Common Use Cases¶
Use Case 1: Creating and Confirming a Basic Sales Order¶
# 1. Create sales order with lines
order_id = get_model("sale.order").create({
"contact_id": 123,
"date": "2024-01-15",
"currency_id": 1,
"tax_type": "tax_ex",
"customer_name": "ABC Corporation",
"customer_phone": "+60123456789",
"customer_address1": "123 Business Street",
"customer_address2": "Suite 200",
"customer_postal_code": "50000",
"customer_city": "Kuala Lumpur",
"customer_country": "MY",
"bill_address_id": 456,
"ship_address_id": 789,
"lines": [
("create", {
"product_id": 101,
"description": "Widget Premium",
"qty": 10,
"uom_id": 1,
"unit_price": 150.00,
"tax_id": 2,
"location_id": 5
}),
("create", {
"product_id": 102,
"description": "Widget Standard",
"qty": 20,
"uom_id": 1,
"unit_price": 100.00,
"tax_id": 2,
"location_id": 5
})
]
})
# 2. Review order totals
obj = get_model("sale.order").browse(order_id)
print(f"Subtotal: {obj.amount_subtotal}")
print(f"Tax: {obj.amount_tax}")
print(f"Total: {obj.amount_total}")
# 3. Confirm order (auto-creates picking and invoice if configured)
result = get_model("sale.order").confirm([order_id])
print(result["flash"]) # "Sales order SO-001 confirmed"
Use Case 2: Creating Order with Delivery and Invoice¶
# 1. Create order
order_id = get_model("sale.order").create({
"contact_id": 123,
"date": "2024-01-15",
"due_date": "2024-01-20", # Shipping date
"currency_id": 1,
"tax_type": "tax_ex",
"ship_method_id": 3,
"customer_name": "XYZ Ltd",
"customer_phone": "+60123456789",
"customer_address1": "456 Industrial Road",
"customer_address2": "Building B",
"customer_postal_code": "51000",
"customer_city": "Petaling Jaya",
"customer_country": "MY",
"lines": [
("create", {
"product_id": 201,
"qty": 50,
"unit_price": 25.00,
"uom_id": 1,
"location_id": 5
})
]
})
# 2. Confirm order
get_model("sale.order").confirm([order_id])
# 3. Create delivery picking manually (if not auto-created)
picking_result = get_model("sale.order").copy_to_picking([order_id])
picking_id = picking_result["picking_id"]
print(f"Created picking: {picking_id}")
# 4. Process delivery
get_model("stock.picking").pending([picking_id])
get_model("stock.picking").set_done([picking_id])
# 5. Create invoice manually (if not auto-created)
get_model("sale.order").copy_to_invoice([order_id])
# 6. Mark order as complete
get_model("sale.order").done([order_id])
Use Case 3: Handling Order with Production¶
# 1. Create sales order for manufactured product
order_id = get_model("sale.order").create({
"contact_id": 123,
"date": "2024-01-15",
"due_date": "2024-02-15", # Allow time for production
"currency_id": 1,
"tax_type": "tax_ex",
"customer_name": "Manufacturing Customer",
"customer_phone": "+60123456789",
"customer_address1": "789 Factory Lane",
"customer_address2": "Unit C",
"customer_postal_code": "52000",
"customer_city": "Subang Jaya",
"customer_country": "MY",
"lines": [
("create", {
"product_id": 301, # Product with supply_method="production"
"qty": 100,
"unit_price": 500.00,
"uom_id": 1,
"location_id": 5
})
]
})
# 2. Confirm order
get_model("sale.order").confirm([order_id])
# 3. Create production order (if not auto-created)
prod_result = get_model("sale.order").copy_to_production([order_id])
prod_order_ids = prod_result["order_ids"]
print(f"Created production orders: {prod_order_ids}")
# 4. Process production orders
for prod_id in prod_order_ids:
# Confirm production
get_model("production.order").confirm([prod_id])
# Issue raw materials
get_model("production.order").create_components_picking([prod_id])
# Complete production
get_model("production.order").done([prod_id])
# 5. Check production status
obj = get_model("sale.order").browse(order_id)
print(f"Production status: {obj.production_status}")
# 6. Create delivery after production complete
get_model("sale.order").copy_to_picking([order_id])
# 7. Create invoice
get_model("sale.order").copy_to_invoice([order_id])
# 8. Complete order
get_model("sale.order").done([order_id])
Use Case 4: Applying Promotions and Vouchers¶
# 1. Create order with regular lines
order_id = get_model("sale.order").create({
"contact_id": 123,
"date": "2024-01-15",
"currency_id": 1,
"tax_type": "tax_ex",
"customer_name": "Discount Customer",
"customer_phone": "+60123456789",
"customer_address1": "100 Shopping Mall",
"customer_address2": "Level 2",
"customer_postal_code": "53000",
"customer_city": "Kuala Lumpur",
"customer_country": "MY",
"lines": [
("create", {
"product_id": 401,
"qty": 5,
"unit_price": 200.00,
"uom_id": 1
})
]
})
# 2. Apply voucher code
voucher = get_model("sale.voucher").search_browse([["code", "=", "SAVE20"]])
if voucher:
get_model("sale.order").write([order_id], {
"voucher_id": voucher[0].id
})
get_model("sale.order").apply_voucher([order_id])
# 3. Check updated totals
obj = get_model("sale.order").browse(order_id)
print(f"Total before discount: {obj.amount_subtotal_before_discount}")
print(f"Discount: {obj.amount_total_discount}")
print(f"Total after discount: {obj.amount_total}")
# 4. Confirm and process
get_model("sale.order").confirm([order_id])
Use Case 5: Multi-Order Invoice Consolidation¶
# Create multiple orders for same customer
order_ids = []
for i in range(3):
order_id = get_model("sale.order").create({
"contact_id": 123, # Same customer
"date": "2024-01-15",
"currency_id": 1, # Same currency
"tax_type": "tax_ex", # Same tax type
"pay_method_id": 5, # Same payment method
"customer_name": "Bulk Customer",
"customer_phone": "+60123456789",
"customer_address1": "200 Wholesale Center",
"customer_address2": "Warehouse 1",
"customer_postal_code": "54000",
"customer_city": "Kuala Lumpur",
"customer_country": "MY",
"bill_address_id": 456, # Same billing address
"lines": [
("create", {
"product_id": 500 + i,
"qty": 10,
"unit_price": 100.00,
"uom_id": 1
})
]
})
order_ids.append(order_id)
# Confirm all orders
for order_id in order_ids:
get_model("sale.order").confirm([order_id])
# Create single consolidated invoice
get_model("sale.order").copy_to_invoice(order_ids)
Performance Tips¶
1. Use Batch Operations¶
- When processing multiple orders, use batch operations instead of loops where possible
- The framework handles function_store calculations efficiently for batches
2. Optimize Line Item Creation¶
# Bad: Creating lines one by one in separate writes
order_id = get_model("sale.order").create({...})
for product in products:
get_model("sale.order.line").create({
"order_id": order_id,
"product_id": product["id"],
...
})
# Good: Create all lines in single operation
order_id = get_model("sale.order").create({
...,
"lines": [
("create", {
"product_id": product["id"],
...
})
for product in products
]
})
3. Use Stored Computed Fields¶
The amount fields are stored (store=True), so they don't need recalculation on every read. However, they update automatically when lines change.
4. Search Optimization¶
# Bad: Loading all fields when you only need a few
orders = get_model("sale.order").search_browse([...])
for order in orders:
print(order.number) # Loads all fields
# Good: Use search with specific fields (when supported)
order_ids = get_model("sale.order").search([...])
# Process IDs directly or load specific objects as needed
Troubleshooting¶
"Can not delete sales order in this status"¶
Cause: Attempting to delete an order in "confirmed" or "done" state
Solution:
- Only draft and reserved orders can be deleted
- For confirmed/done orders, use void instead: get_model("sale.order").void([order_id])
- If you must remove it, first return to draft (with caution): get_model("sale.order").to_draft([order_id]) then delete
"Sales order number already exists for this company"¶
Cause: Duplicate number within the same company (violates unique key constraint)
Solution:
- Number is auto-generated from sequence - this usually indicates sequence issues
- Check sequence configuration: get_model("sequence").find_sequence(type="sale_order")
- Verify sequence is incrementing properly
- If manually setting number, ensure it's unique within the company
"Product X is below the minimum sales price"¶
Cause: Line item unit_price is less than product.sale_min_price
Solution:
- Update unit price: get_model("sale.order.line").write([line_id], {"unit_price": min_price})
- Or update product minimum: get_model("product").write([product_id], {"sale_min_price": new_min})
- Or remove minimum price constraint if not needed
"Missing location for product X"¶
Cause: Stock product line missing location_id
Solution:
- Set location on line: get_model("sale.order.line").write([line_id], {"location_id": location_id})
- Or configure default location on product: get_model("product").write([product_id], {"locations": [("create", {"location_id": loc_id})]})
"Nothing left to deliver"¶
Cause: Attempting to create picking when all lines are already delivered (qty_delivered >= qty)
Solution:
- Check delivery status: obj.is_delivered
- Review line quantities: Check each line's qty vs qty_delivered
- If delivered, no picking needed - proceed to invoice or mark done
"There are still pending goods issues for this sales order"¶
Cause: Attempting to void order with pending (not yet completed) pickings
Solution:
- Complete pickings first: get_model("stock.picking").set_done([picking_id])
- Or void pickings first: get_model("stock.picking").void([picking_id])
- Then void order: get_model("sale.order").void([order_id])
"There are still invoices waiting payment for this sales order"¶
Cause: Attempting to void order with unpaid invoices
Solution:
- Pay invoices first: Process payment through account.invoice
- Or void invoices: get_model("account.invoice").void([invoice_id])
- Then void order: get_model("sale.order").void([order_id])
"User does not have permission to approve sales orders (approve_sale)"¶
Cause: User lacks approve_sale permission and settings.approve_sale is enabled Solution: - Grant permission to user/role in access control - Or disable approval requirement in Settings > Sales if not needed - Or have authorized user confirm the order
"Missing shipping date for sales order X"¶
Cause: Creating picking, purchase, or production without due_date set
Solution:
- Set due_date before confirming: get_model("sale.order").write([order_id], {"due_date": "2024-02-15"})
- The due_date field represents shipping/delivery date (ETD)
"BoM not found for product X"¶
Cause: Creating production order for product without Bill of Materials Solution: - Create BoM for product: Use production.bom model - Or change product supply_method to "purchase" or "stock" - Or exclude product from production order creation
Security Considerations¶
Permission Model¶
approve_sale- Required to confirm sales orders (if enabled in settings)reserve_sale- Required to reserve sales orders (if enabled in settings)- Standard CRUD permissions apply (create, read, write, delete)
Data Access¶
- Multi-company support ensures users only see orders for their active company (unless cross-company permissions granted)
- Audit log tracks all changes to sales orders (created by, modified by, timestamps)
- Deleted orders are logged in audit trail
Best Practices¶
- Restrict approve_sale permission to authorized sales managers
- Use workflow triggers for approval processes requiring multiple sign-offs
- Review audit logs regularly for compliance
- Implement role-based access control for sensitive fields (cost, profit)
Configuration Settings¶
Required Settings¶
| Setting | Location | Description |
|---|---|---|
| Number Sequence | Settings > Sequences | Define sale_order sequence for auto-numbering |
| Default Currency | Settings > Company | Base currency for conversions |
| Customer Location | Stock > Locations | Customer location (type="customer") for deliveries |
| Warehouse Location | Stock > Locations | Internal warehouse location for stock |
Optional Settings¶
| Setting | Default | Description |
|---|---|---|
approve_sale |
False | Require approve_sale permission to confirm orders |
reserve_sale_reserve |
False | Require reserve_sale permission to reserve orders |
sale_copy_picking |
False | Auto-create picking when confirming order |
sale_copy_invoice |
False | Auto-create invoice when confirming order |
sale_copy_production |
False | Auto-create production orders when confirming |
sale_copy_reserve_picking |
False | Auto-create reservation picking when reserving |
sale_check_delivered_qty |
None | Maximum over-delivery percentage allowed |
sale_void_gi_central_courier |
False | Auto-void courier orders when voiding sale |
Integration Points¶
External Systems¶
- NinjaVan API: Shipping label generation and tracking (via ninjavan_account)
- DHL API: Shipping services integration (via dhl_account)
- Central Courier: Unified courier management integration
- E-commerce Platforms: Can link to ecom.cart, shopee.order via related_id
Internal Modules¶
- Account Module: Invoice generation, payment tracking, financial reporting
- Stock Module: Inventory management, pickings, stock moves, locations
- Purchase Module: Automated purchase requisitions for procurement
- Production Module: Manufacturing order generation, BoM management
- Job Module: Service order creation and tracking
- CRM Module: Contact management, activities, opportunities
- Project Module: Project-based sales and cost tracking
- Promotion Module: Discount rules, coupons, vouchers
Version History¶
Last Updated: 2024-01-05 Model Version: sale_order.py Framework: Netforce
Additional Resources¶
- sale.order.line Documentation: Order line items model
- account.invoice Documentation: Customer invoicing
- stock.picking Documentation: Delivery and warehouse operations
- production.order Documentation: Manufacturing operations
- sale.promotion Documentation: Promotion and discount management
- contact Documentation: Customer and supplier management
Support & Feedback¶
For issues or questions about this module:
1. Check related model documentation for integrated functionality
2. Review system logs for detailed error messages (/var/log/netforce.log)
3. Verify permissions and settings configuration in Settings > Sales
4. Test in development environment first before production changes
5. Review audit logs for historical changes and troubleshooting
This documentation is generated for developer onboarding and reference purposes.