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 |
Related Document Handling¶
| 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
}
2. Handle Related Documents Carefully¶
# 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
Related Models¶
| 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.