Skip to content

Sales Order Modification Documentation

Overview

The Sales Order Modification module (sale.modif) provides a structured way to modify confirmed sales orders through a wizard interface. This transient model handles common modification scenarios such as adding products, removing products, changing quantities, and updating order header information. It manages the complexity of updating related documents (pickings, invoices) and maintains data integrity across the entire sales process.


Model Information

Model Name: sale.modif Display Name: Sale Modif Model Type: Transient (not stored permanently)

Features

  • ❌ Audit logging (transient model)
  • ❌ Multi-company support (inherits from parent order)
  • ❌ Full-text content search
  • ❌ Unique key constraint
  • ✅ Transient model for temporary wizard data

Understanding Transient Models

Transient models (_transient = True) are temporary database records used for: - Wizard interfaces - User input collection - Complex operations requiring validation - Multi-step processes

Key Characteristics: - Records are automatically deleted after a period - Not meant for long-term data storage - Perfect for modification wizards - Simplified validation and processing


Modification Types

Type Code Description
Add Product add_prod Add a new product line to the order
Remove Product del_prod Remove all lines for a specific product
Change Quantity change_qty Modify quantity for a product
Change Header change_order Update order header information (dates, terms, etc.)

Key Fields Reference

Core Fields

Field Type Required Description
order_id Many2One Sales order being modified (on_delete="cascade")
type Selection Type of modification (add_prod/del_prod/change_qty/change_order)
contact_id Many2One Customer (from order_id.contact_id, function field)

Product Modification Fields

Field Type Description
product_id Many2One Product to add, remove, or change
qty Decimal New quantity (for add_prod or change_qty)
unit_price Decimal Unit price (for add_prod)
location_id Many2One Stock location (for add_prod)

Header Modification Fields

Field Type Description
due_date Date Shipping date (ETD)
delivery_date Date Delivery date (ETA)
ship_term_id Many2One Shipping terms
ship_port_id Many2One Shipping port
Field Type Description
update_related Selection How to handle related documents: blank (don't modify) / recreate (delete & recreate) / update (update without deleting)

API Methods

1. Apply Modification

Method: apply_modif(ids, context)

Applies the modification to the sales order.

Parameters: - ids (list): Modification wizard IDs

Behavior by Type:

Add Product (add_prod): - Creates new sale.order.line with specified product - Sets product, quantity, price, location - Updates order totals automatically

Remove Product (del_prod): - Deletes all lines for specified product - Removes corresponding picking lines (pending/approved only) - Handles invoice lines: - If invoice paid: Creates credit note - If invoice waiting payment: Reverts to draft, modifies, reposts - If invoice draft: Deletes lines directly

Change Quantity (change_qty): - Updates quantity on matching product lines - Recalculates order totals

Change Header (change_order): - Updates due_date, delivery_date, ship_term_id, ship_port_id - Only updates fields that are provided

Related Document Updates: - update_related="": No changes to pickings/invoices - update_related="recreate": Deletes related docs and recreates via confirm() - update_related="update": Updates existing docs without deletion

Returns: dict - Navigation to modified sales order

Example:

# Add a product to existing order
modif_id = get_model("sale.modif").create({
    "order_id": 123,
    "type": "add_prod",
    "product_id": 789,
    "qty": 5,
    "unit_price": 100.00,
    "location_id": 10
})

result = get_model("sale.modif").apply_modif([modif_id])
# Returns: {
#     "flash": "Sales order SO-2025-0001 modified successfully",
#     "next": {"name": "sale", "mode": "form", "active_id": 123}
# }


Helper Methods

_get_contact(context)

Default function to populate contact_id from the related order.

Used in: Defaults dictionary

Behavior: - Retrieves order_id from context defaults - Returns contact_id from that order


update_related(ids, context)

Placeholder method for updating related documents.

Current Status: Not implemented (pass statement)

Intended Use: Custom logic for updating pickings/invoices without recreation


Common Use Cases

Use Case 1: Add Product to Confirmed Order

# Scenario: Customer calls to add items to existing order

# Step 1: Create modification wizard
modif_id = get_model("sale.modif").create({
    "order_id": 123,
    "type": "add_prod",
    "product_id": 789,
    "qty": 10,
    "unit_price": 50.00,
    "location_id": 5
})

# Step 2: Apply modification
result = get_model("sale.modif").apply_modif([modif_id])

# Step 3: Optionally recreate related documents
modif_id2 = get_model("sale.modif").create({
    "order_id": 123,
    "type": "add_prod",  # Any type works for update_related
    "update_related": "recreate"
})
result = get_model("sale.modif").apply_modif([modif_id2])

Use Case 2: Remove Product from Order

# Scenario: Product discontinued, need to remove from pending orders

# Create modification to remove product
modif_id = get_model("sale.modif").create({
    "order_id": 123,
    "type": "del_prod",
    "product_id": 789,
    "update_related": "recreate"  # Recreate pickings/invoices
})

# Apply the modification
result = get_model("sale.modif").apply_modif([modif_id])

Use Case 3: Change Product Quantity

# Scenario: Customer wants to increase order quantity

# Modify quantity
modif_id = get_model("sale.modif").create({
    "order_id": 123,
    "type": "change_qty",
    "product_id": 789,
    "qty": 50  # New quantity
})

result = get_model("sale.modif").apply_modif([modif_id])

Use Case 4: Update Shipping Information

# Scenario: Shipping terms changed

# Update header information
modif_id = get_model("sale.modif").create({
    "order_id": 123,
    "type": "change_order",
    "due_date": "2025-02-15",
    "delivery_date": "2025-02-20",
    "ship_term_id": 5,
    "ship_port_id": 10
})

result = get_model("sale.modif").apply_modif([modif_id])

Use Case 5: Complex Modification with Document Recreation

# Scenario: Major order changes requiring full document refresh

# Step 1: Add new product
modif_id = get_model("sale.modif").create({
    "order_id": 123,
    "type": "add_prod",
    "product_id": 999,
    "qty": 20,
    "unit_price": 75.00,
    "location_id": 5
})
get_model("sale.modif").apply_modif([modif_id])

# Step 2: Change quantity of existing product
modif_id = get_model("sale.modif").create({
    "order_id": 123,
    "type": "change_qty",
    "product_id": 789,
    "qty": 30
})
get_model("sale.modif").apply_modif([modif_id])

# Step 3: Recreate all related documents
modif_id = get_model("sale.modif").create({
    "order_id": 123,
    "type": "change_order",  # Type doesn't matter here
    "update_related": "recreate"
})
result = get_model("sale.modif").apply_modif([modif_id])

Best Practices

1. Always Specify Modification Type

# Bad: No type specified
{
    "order_id": 123,
    "product_id": 789
}

# Good: Clear modification intent
{
    "order_id": 123,
    "type": "add_prod",
    "product_id": 789,
    "qty": 5,
    "unit_price": 100.00,
    "location_id": 10
}

# Option 1: Manual control (default)
modif_vals = {
    "order_id": 123,
    "type": "add_prod",
    "product_id": 789,
    "qty": 5,
    "unit_price": 100.00
    # No update_related - manual handling required
}

# Option 2: Safe recreation
modif_vals = {
    "order_id": 123,
    "type": "del_prod",
    "product_id": 789,
    "update_related": "recreate"  # Safest for major changes
}

# Option 3: In-place update (use with caution)
modif_vals = {
    "order_id": 123,
    "type": "change_qty",
    "product_id": 789,
    "qty": 10,
    "update_related": "update"  # Update existing docs
}

3. Validate Order State Before Modification

# Check order state first
order = get_model("sale.order").browse([order_id])[0]

if order.state not in ("confirmed", "done"):
    raise Exception("Order must be confirmed to modify")

if order.state == "done" and order.invoices:
    # Check if invoices are paid
    for inv in order.invoices:
        if inv.state == "paid":
            print("Warning: Order has paid invoices")

4. Use Separate Modifications for Different Changes

# Bad: Trying to do too much in one modification
{
    "type": "add_prod",  # Can't add and change at same time
    "qty": 10  # Conflicting fields
}

# Good: Separate modifications
# First, add product
modif1_id = get_model("sale.modif").create({
    "order_id": 123,
    "type": "add_prod",
    "product_id": 789,
    "qty": 5,
    "unit_price": 100.00
})
get_model("sale.modif").apply_modif([modif1_id])

# Then, change quantity of another product
modif2_id = get_model("sale.modif").create({
    "order_id": 123,
    "type": "change_qty",
    "product_id": 456,
    "qty": 20
})
get_model("sale.modif").apply_modif([modif2_id])

Transaction Flow

Add Product Flow

1. Create modification wizard (type="add_prod")
2. Apply modification
   ├─> Create new sale.order.line
   ├─> Set product, qty, price, location, tax
   └─> Call order.function_store() to update totals
3. Optionally update related documents
   ├─> If update_related="recreate":
   │   ├─> Call order.delete_related()
   │   ├─> Call order.to_draft()
   │   └─> Call order.confirm()
   └─> If update_related="update":
       └─> Call modif.update_related() (not implemented)
4. Return to updated order form

Remove Product Flow

1. Create modification wizard (type="del_prod")
2. Apply modification
   ├─> Delete matching sale.order.lines
   ├─> For each picking (pending/approved):
   │   └─> Delete matching stock.moves
   ├─> For each invoice:
   │   ├─> If state="paid":
   │   │   ├─> Create credit note
   │   │   └─> Post credit note
   │   ├─> If state="waiting_payment":
   │   │   ├─> Revert to draft
   │   │   ├─> Delete invoice lines
   │   │   └─> Repost invoice
   │   └─> If state="draft":
   │       └─> Delete invoice lines
   └─> Call order.function_store()
3. Return confirmation

Change Quantity Flow

1. Create modification wizard (type="change_qty")
2. Apply modification
   ├─> Find all lines matching product
   ├─> Update qty for each line
   └─> Call order.function_store()
3. Return confirmation

Change Header Flow

1. Create modification wizard (type="change_order")
2. Apply modification
   ├─> Update due_date (if provided)
   ├─> Update delivery_date (if provided)
   ├─> Update ship_term_id (if provided)
   └─> Update ship_port_id (if provided)
3. Return confirmation

Model Relationship Description
sale.order Many2One Sales order being modified
sale.order.line Indirect Order lines created/modified/deleted
product Many2One Product for modifications
contact Function Field Customer information
stock.location Many2One Location for added products
stock.picking Indirect Pickings affected by modifications
stock.move Indirect Stock moves deleted/modified
account.invoice Indirect Invoices affected by modifications
account.invoice.line Indirect Invoice lines deleted/modified
ship.term Many2One Shipping terms
ship.port Many2One Shipping port

Troubleshooting

"Missing product"

Cause: type="del_prod" or "change_qty" without product_id Solution: Always specify product_id for product-level modifications

"Missing qty"

Cause: type="change_qty" without qty field Solution: Provide the new quantity value

"Invalid modification type"

Cause: Type not in allowed values Solution: Use one of: add_prod, del_prod, change_qty, change_order

Invoice Handling Issues

Cause: Trying to modify order with complex invoice states Solution: Check invoice states before modification; system handles paid invoices with credit notes

Picking Not Updated

Cause: Picking already in shipped/done state Solution: Only pending/approved pickings are modified; completed pickings are skipped


Performance Tips

1. Batch Modifications

# If making multiple changes, batch them together
modifications = [
    {"type": "add_prod", "product_id": 789, "qty": 5, "unit_price": 100},
    {"type": "change_qty", "product_id": 456, "qty": 20},
    {"type": "del_prod", "product_id": 123}
]

for mod_vals in modifications:
    mod_vals["order_id"] = order_id
    modif_id = get_model("sale.modif").create(mod_vals)
    get_model("sale.modif").apply_modif([modif_id])

# Then recreate documents once
modif_id = get_model("sale.modif").create({
    "order_id": order_id,
    "type": "change_order",
    "update_related": "recreate"
})
get_model("sale.modif").apply_modif([modif_id])

2. Avoid Unnecessary Recreation

# Bad: Recreating after every small change
modif_vals = {
    "update_related": "recreate"  # Every time!
}

# Good: Recreate only when done with all changes
# Make all modifications first, then recreate once at the end

Security Considerations

Data Access

  • Modifications inherit permissions from sale.order
  • Transient records auto-cleanup prevents data buildup
  • Cascade delete ensures cleanup when order deleted

Validation

  • Order ID required (enforced)
  • Modification type required (enforced)
  • Product validation for product-specific operations
  • Quantity validation for change_qty operations

Configuration Settings

Required Settings

Setting Location Description
Products Product Master Products must exist for add_prod
Locations Stock Locations for stock products
Tax Rates Account Tax configuration for new lines

Integration Points

Sales

  • sale.order: Main document being modified
  • sale.order.line: Lines created/updated/deleted
  • product: Product information for additions

Stock

  • stock.picking: Pickings updated when products removed
  • stock.move: Moves deleted for removed products
  • stock.location: Locations for new product lines

Accounting

  • account.invoice: Invoices modified or credit notes created
  • account.invoice.line: Lines deleted or credited

Version History

Last Updated: 2025-01-05 Model Version: sale_modif.py (197 lines) Framework: Netforce


Additional Resources

  • Sales Order Documentation: sale.order
  • Sales Order Line Documentation: sale.order.line
  • Stock Picking Documentation: stock.picking
  • Account Invoice Documentation: account.invoice

Support & Feedback

For issues or questions about this module: 1. Check sales order state before modifying 2. Review invoice and picking states 3. Test modifications in development first 4. Use recreate option for major changes 5. Validate product and location availability


This documentation is generated for developer onboarding and reference purposes.