Skip to content

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:

_key = ["company_id", "number"]

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

draft → reserved → confirmed → done
        voided (can be triggered from any state)
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
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:

get_model("sale.order").done([order_id])

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:

get_model("sale.order").void([order_id])

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:

get_model("sale.order").to_draft([order_id])


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:

context = {
    "due_date": "2024-02-15"    # Optional: Only create for specific due date
}

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:

get_model("sale.order").check_min_prices([order_id])

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

# Orders in January 2024
condition = [
    ["date", ">=", "2024-01-01"],
    ["date", "<=", "2024-01-31"]
]

Search by State

# All confirmed orders
condition = [["state", "=", "confirmed"]]

# All active orders (not voided)
condition = [["state", "!=", "voided"]]

Search by Customer

# All orders for customer 123
condition = [["contact_id", "=", 123]]

Search Overdue Orders

# Orders past their shipping date
condition = [["overdue", "=", True]]

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.


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.