Account Payment Extension Documentation (Sale Module)¶
Overview¶
The Account Payment Extension (account.payment via _inherit) is a MODEL EXTENSION that adds sales-specific functionality to the core account.payment model from the account module. This extension enables automatic detection and triggering of sale order payment events, ensuring that sale orders are properly notified when they become fully paid through invoice payment processing.
This is NOT a standalone model - it extends the existing account.payment model using the _inherit mechanism.
Model Information¶
Model Name: account.payment (EXTENSION)
Base Model: account.payment (from account module)
Extension Module: netforce_sale
Extension Mechanism: _inherit = "account.payment"
Purpose: Trigger sale order "paid" events when invoices linked to sale orders are paid
Extension Features¶
- Monitors payment posting for sale-order-related invoices
- Automatically detects sale orders associated with paid invoices
- Triggers "paid" workflow event on sale orders when payment is complete
- Zero new fields added (behavior extension only)
- Transparent integration with base payment model
What This Extension Adds¶
This extension does NOT add any new fields to the payment model. Instead, it:
- Overrides the post() method to add sale order payment tracking
- Monitors invoice payments that relate to sale orders
- Triggers workflow events on sale orders when payment status changes
Understanding Model Extensions¶
What is _inherit?¶
The _inherit mechanism allows one module to extend models defined in other modules without modifying the original code:
class Payment(Model):
_inherit = "account.payment"
# This class extends the existing account.payment model
# All fields and methods from the base model are available
# You can add new fields or override existing methods
Extension Architecture¶
┌─────────────────────────────────────┐
│ account.payment (Base Model) │
│ From: account module │
│ - Fields: date, amount, type, etc.│
│ - Methods: create, post, etc. │
└─────────────────────────────────────┘
↓
[_inherit]
↓
┌─────────────────────────────────────┐
│ Payment Extension (Sale Module) │
│ Adds: Sale order event triggering │
│ Overrides: post() method │
└─────────────────────────────────────┘
Why Extend Instead of Modify?¶
- Modularity: Sale module functionality stays in sale module
- Maintainability: Base account module remains unchanged
- Upgradability: Base model can be updated independently
- Separation of Concerns: Payment logic vs. sales integration logic
Extension Implementation¶
Base Model Location¶
- Module: account
- Model: account.payment
- File:
/netforce_account/netforce_account/models/account_payment.py
Extension Location¶
- Module: netforce_sale
- Model: account.payment (extension)
- File:
/netforce_sale/netforce_sale/models/account_payment.py
Extended Methods¶
The sale module extends only ONE method from the base payment model:
| Method | Extension Type | Purpose |
|---|---|---|
post() |
Override with super() call | Detect sale order payments and trigger "paid" events |
Method Override Details¶
1. post() - Enhanced Payment Posting¶
Method: post(ids, context)
The extension overrides the post() method to add sale order payment event triggering while preserving all original payment posting functionality.
Extension Behavior:
The method follows this workflow:
1. Collect Related Sale Orders
↓
2. Check Which Orders Are Currently Unpaid
↓
3. Call Original post() Method (super())
↓
4. Check Which Orders Became Paid
↓
5. Trigger "paid" Event for Newly Paid Orders
Detailed Algorithm:
def post(self, ids, context={}):
# Step 1: Collect all sale orders related to these payments
sale_ids = []
for obj in self.browse(ids):
if obj.pay_type != "invoice":
continue # Only process invoice payments
# Check payment invoice lines
for line in obj.invoice_lines:
inv = line.invoice_id
# Check if invoice is related to a sale order
rel = inv.related_id
if rel and rel._model == "sale.order":
sale_ids.append(rel.id)
# Check if invoice lines are related to sale orders
for inv_line in inv.lines:
rel = inv_line.related_id
if rel and rel._model == "sale.order":
sale_ids.append(rel.id)
# Step 2: Get unique sale order IDs
sale_ids = list(set(sale_ids))
# Step 3: Remember which orders are currently unpaid
unpaid_sale_ids = []
for sale in get_model("sale.order").browse(sale_ids):
if not sale.is_paid:
unpaid_sale_ids.append(sale.id)
# Step 4: Execute original payment posting
res = super().post(ids, context=context)
# Step 5: Check which orders became paid
paid_sale_ids = []
for sale in get_model("sale.order").browse(unpaid_sale_ids):
if sale.is_paid:
paid_sale_ids.append(sale.id)
# Step 6: Trigger "paid" event for newly paid orders
if paid_sale_ids:
get_model("sale.order").trigger(paid_sale_ids, "paid")
return res
Key Extension Points:
- Invoice Payment Detection
- Only processes payments where
pay_type == "invoice" -
Examines
invoice_linesto find related invoices -
Sale Order Linkage Discovery
- Checks invoice's
related_idfield for sale.order references - Checks individual invoice line
related_idfields -
Uses polymorphic field pattern:
model_name,record_id -
Payment Status Tracking
- Records sale orders that are unpaid BEFORE posting
- Checks same orders AFTER posting
-
Only triggers event for orders that transitioned to paid status
-
Event Triggering
- Fires
"paid"workflow event on sale orders - Can trigger downstream automations
- Enables notification workflows, status updates, etc.
Parameters:
- ids (list): Payment record IDs to post
- context (dict): Optional context information
Returns: Result from base post() method (typically None or status)
Example Usage:
# Post a payment (works exactly like base model)
payment_id = get_model("account.payment").create({
"date": "2025-01-05",
"pay_type": "invoice",
"amount": 1000.00,
"account_id": bank_account_id,
"invoice_lines": [
("create", {
"invoice_id": invoice_id,
"amount": 1000.00
})
]
})
# When posted, the extension automatically:
# 1. Finds sale orders linked to the invoice
# 2. Checks payment status before and after
# 3. Triggers "paid" event if order becomes fully paid
get_model("account.payment").post([payment_id])
Integration with Sale Orders¶
How Payments Link to Sale Orders¶
The extension discovers sale order relationships through multiple paths:
Path 1: Invoice Level Linkage¶
When an invoice is directly related to a sale order:
Path 2: Invoice Line Level Linkage¶
When invoice lines reference sale order (common for partial invoices):
Sale Order Payment Detection¶
Sale orders track payment status through the is_paid computed field:
# Before payment posting
sale.is_paid = False
# Post payment
payment.post([payment_id])
# After payment posting (if fully paid)
sale.is_paid = True # Extension detects this change
The extension only triggers the "paid" event for orders that: 1. Were unpaid before payment posting 2. Became paid after payment posting 3. Are linked to the processed invoices
Workflow Event: "paid"¶
Event Trigger Behavior¶
This event can be configured in workflow automation to: - Send payment confirmation emails - Update order status - Trigger fulfillment processes - Notify sales team - Update customer records - Generate reports
Event Configuration Example¶
In workflow settings, you might configure:
# Workflow: Sale Order Payment Received
# Trigger: sale.order.paid
# Actions:
# 1. Send email to customer: "Payment Received"
# 2. Update order state to "ready_to_ship"
# 3. Notify warehouse team
# 4. Log payment completion in order notes
Related Models¶
| Model | Relationship | Description |
|---|---|---|
account.payment |
Base Model (Extended) | Core payment model from account module |
account.invoice |
Referenced | Invoices being paid (linked via payment lines) |
account.invoice.line |
Referenced | Individual invoice line items |
sale.order |
Event Target | Sale orders that receive "paid" event triggers |
Common Use Cases¶
Use Case 1: Customer Pays Invoice for Sale Order¶
# Customer places an order
order_id = get_model("sale.order").create({
"contact_id": customer_id,
"lines": [
("create", {"product_id": product_id, "qty": 10, "unit_price": 100})
]
})
# Order is confirmed and invoice is created
get_model("sale.order").confirm([order_id])
invoice_id = get_model("sale.order").browse(order_id).invoice_ids[0].id
# Invoice related_id now points to sale order
invoice = get_model("account.invoice").browse(invoice_id)
# invoice.related_id = "sale.order,{order_id}"
# Customer makes payment
payment_id = get_model("account.payment").create({
"date": "2025-01-05",
"pay_type": "invoice",
"amount": 1000.00,
"invoice_lines": [
("create", {
"invoice_id": invoice_id,
"amount": 1000.00
})
]
})
# Post the payment
# Extension automatically:
# 1. Finds order_id through invoice.related_id
# 2. Checks order is currently unpaid
# 3. Posts payment (updates invoice paid status)
# 4. Checks if order is now paid
# 5. Triggers "paid" event on the order
get_model("account.payment").post([payment_id])
# Result: Order's "paid" workflow event is triggered
# This can automatically:
# - Send confirmation email to customer
# - Update order status
# - Notify warehouse for shipment
Use Case 2: Partial Payments Completing Sale Order¶
# Order total: $1000
order_id = 123
# Customer makes first partial payment: $600
payment1_id = get_model("account.payment").create({
"pay_type": "invoice",
"amount": 600.00,
"invoice_lines": [("create", {"invoice_id": invoice_id, "amount": 600})]
})
get_model("account.payment").post([payment1_id])
# Order still unpaid (is_paid = False)
# No "paid" event triggered
# Customer makes second payment: $400
payment2_id = get_model("account.payment").create({
"pay_type": "invoice",
"amount": 400.00,
"invoice_lines": [("create", {"invoice_id": invoice_id, "amount": 400})]
})
get_model("account.payment").post([payment2_id])
# Order now fully paid (is_paid = True)
# Extension detects transition from unpaid → paid
# "paid" event is triggered!
Use Case 3: Multiple Invoices for One Sale Order¶
# Large order split into multiple invoices
order_id = 456
# Invoice 1: $5000 (Invoice related to order)
# Invoice 2: $3000 (Invoice related to order)
# Payment received for first invoice
payment1_id = get_model("account.payment").create({
"pay_type": "invoice",
"invoice_lines": [("create", {"invoice_id": invoice1_id, "amount": 5000})]
})
get_model("account.payment").post([payment1_id])
# Order partially paid, no event triggered
# Payment received for second invoice
payment2_id = get_model("account.payment").create({
"pay_type": "invoice",
"invoice_lines": [("create", {"invoice_id": invoice2_id, "amount": 3000})]
})
get_model("account.payment").post([payment2_id])
# Order now fully paid
# Extension discovers order through invoice2.related_id
# Triggers "paid" event
Use Case 4: Direct Payment (Non-Invoice)¶
# Customer makes direct payment (not linked to invoice)
payment_id = get_model("account.payment").create({
"date": "2025-01-05",
"pay_type": "direct", # NOT "invoice"
"amount": 500.00
})
# Post the payment
get_model("account.payment").post([payment_id])
# Extension behavior:
# - Checks pay_type, sees it's "direct" not "invoice"
# - Skips sale order detection (no invoice to check)
# - No "paid" event triggered
# - Base payment posting proceeds normally
Use Case 5: Batch Payment Posting¶
# Post multiple payments at once
payment_ids = [101, 102, 103, 104, 105]
# Extension processes all payments in batch:
# 1. Collects ALL related sale orders from ALL payments
# 2. Records which orders are currently unpaid
# 3. Posts ALL payments via super()
# 4. Checks which orders became paid
# 5. Triggers "paid" event for all newly-paid orders
get_model("account.payment").post(payment_ids)
# If payments 101, 102, 103 complete order A
# And payments 104, 105 complete order B
# Result: Both order A and order B get "paid" event triggered
Best Practices¶
1. Rely on Automatic Event Triggering¶
# Good: Let extension handle event triggering automatically
payment_id = get_model("account.payment").create({...})
get_model("account.payment").post([payment_id])
# "paid" event triggered automatically when appropriate
# Bad: Don't manually trigger "paid" events
get_model("account.payment").post([payment_id])
get_model("sale.order").trigger([order_id], "paid") # Unnecessary! Already done by extension
Why? The extension already handles event triggering intelligently, checking payment status before and after. Manual triggering could fire events prematurely or redundantly.
2. Ensure Proper Invoice-to-Order Linking¶
# Good: Link invoice to sale order
invoice_id = get_model("account.invoice").create({
"related_id": "sale.order,%d" % order_id,
"lines": [...]
})
# Good: Link invoice lines to sale order
invoice_id = get_model("account.invoice").create({
"lines": [
("create", {
"related_id": "sale.order,%d" % order_id,
"product_id": product_id,
"qty": 10
})
]
})
# Bad: Invoice not linked to order
invoice_id = get_model("account.invoice").create({
"related_id": None, # No link!
"lines": [...]
})
# Result: Payment won't trigger "paid" event on order
Why? The extension discovers sale orders through related_id fields. Without proper linking, payments won't trigger order events.
3. Configure Workflow Actions for "paid" Event¶
# Set up workflow automation to handle "paid" events
# Example workflow configuration:
# Trigger: sale.order.paid
# Conditions: state == "confirmed"
# Actions:
# 1. Send email template "payment_received" to order.contact_id
# 2. Update state to "ready_to_ship"
# 3. Create activity: "Prepare shipment"
# 4. Notify user: order.user_id with message "Order {number} paid"
Why? The extension provides the event trigger, but you need workflow configuration to define what happens when orders are paid.
4. Handle Partial vs. Full Payments¶
# The extension automatically handles partial payments correctly:
# First partial payment
get_model("account.payment").post([payment1_id])
# is_paid = False, no event
# Second partial payment completing order
get_model("account.payment").post([payment2_id])
# is_paid transitions from False → True
# "paid" event triggered!
# Don't try to manually track partial payments for event triggering
# The extension's before/after comparison handles this automatically
5. Batch Processing Optimization¶
# Good: Batch post multiple payments together
payment_ids = [101, 102, 103, 104, 105]
get_model("account.payment").post(payment_ids)
# Extension efficiently:
# - Collects all related orders once
# - Posts all payments together
# - Triggers events for all affected orders
# Less Efficient: Post payments individually
for payment_id in payment_ids:
get_model("account.payment").post([payment_id])
# Extension runs detection logic for each payment separately
Why? Batch processing is more efficient when posting multiple payments that might affect the same orders.
Extension Impact Analysis¶
What Changes When This Extension is Active?¶
| Aspect | Without Extension | With Extension |
|---|---|---|
| Payment Posting | Works normally | Works normally + sale order event triggering |
| Invoice Payments | Pays invoices | Pays invoices + detects related sale orders |
| Sale Order Status | Manual tracking | Automatic "paid" event when fully paid |
| Workflow Triggers | None | "paid" event enables automations |
| Performance | Baseline | Minimal overhead (only for invoice payments) |
Performance Considerations¶
The extension adds minimal overhead:
- Only processes payments where pay_type == "invoice"
- Direct payments bypass sale order detection entirely
- Uses efficient set operations for unique order collection
- Single batch trigger call for all affected orders
Workflow Integration¶
Trigger Events¶
The extension fires workflow triggers:
get_model("sale.order").trigger(paid_sale_ids, "paid")
# Fired when: Sale orders transition from unpaid to paid status
Configurable Workflow Actions¶
You can configure workflows to respond to the "paid" event:
- Email Notifications
- Send payment confirmation to customer
- Notify sales rep of payment received
-
Alert accounting team
-
Status Updates
- Change order state to "ready_to_ship"
- Update payment status fields
-
Set flags for fulfillment
-
Process Automation
- Trigger shipping preparation
- Generate packing lists
-
Update inventory reservations
-
External Integrations
- Sync payment status to external systems
- Update CRM records
- Trigger third-party webhooks
Troubleshooting¶
"paid" Event Not Triggering¶
Cause 1: Invoice not linked to sale order
Solution: Ensure invoice has related_id = "sale.order,{order_id}" or invoice lines have this linkage
Cause 2: Payment type is not "invoice"
Solution: Extension only processes pay_type == "invoice". Direct payments don't trigger sale events.
Cause 3: Order was already paid Solution: Extension only triggers event for orders that BECOME paid. If order was already paid before this payment, no event is triggered.
Cause 4: Partial payment doesn't complete order Solution: This is expected behavior. Event only triggers when order becomes fully paid.
Multiple "paid" Events for Same Order¶
Cause: Multiple payments posted simultaneously that all complete the order
Solution: The extension uses list(set(sale_ids)) to deduplicate, so this shouldn't happen. If it does, check workflow configuration for multiple rules responding to "paid" event.
Payment Posting Fails¶
Cause: Error in base payment posting, not extension
Solution: Extension calls super().post() which can fail for various account-module reasons. Check account module configuration, account balances, journal setup, etc.
Testing Examples¶
Unit Test: Payment Triggers Sale Order Paid Event¶
def test_payment_triggers_sale_order_paid_event():
# Create a sale order
order_id = get_model("sale.order").create({
"contact_id": customer_id,
"lines": [
("create", {
"product_id": product_id,
"qty": 5,
"unit_price": 200.00
})
]
})
# Confirm order to generate invoice
get_model("sale.order").confirm([order_id])
# Get the invoice
order = get_model("sale.order").browse(order_id)
invoice_id = order.invoice_ids[0].id
# Verify order is not yet paid
assert not order.is_paid
# Create payment for the invoice
payment_id = get_model("account.payment").create({
"date": "2025-01-05",
"pay_type": "invoice",
"amount": 1000.00,
"invoice_lines": [
("create", {
"invoice_id": invoice_id,
"amount": 1000.00
})
]
})
# Set up event listener to verify trigger fires
events_fired = []
def event_listener(ids, event):
events_fired.append((ids, event))
# Mock the trigger method to capture events
original_trigger = get_model("sale.order").trigger
get_model("sale.order").trigger = event_listener
# Post the payment
get_model("account.payment").post([payment_id])
# Restore original trigger
get_model("sale.order").trigger = original_trigger
# Verify "paid" event was triggered for our order
assert ([order_id], "paid") in events_fired
# Verify order is now paid
order = get_model("sale.order").browse(order_id)
assert order.is_paid
Unit Test: Partial Payment Doesn't Trigger Event¶
def test_partial_payment_no_event():
# Create order with $1000 total
order_id = get_model("sale.order").create({...})
get_model("sale.order").confirm([order_id])
order = get_model("sale.order").browse(order_id)
invoice_id = order.invoice_ids[0].id
# Make partial payment of $600
payment_id = get_model("account.payment").create({
"pay_type": "invoice",
"amount": 600.00,
"invoice_lines": [
("create", {"invoice_id": invoice_id, "amount": 600})
]
})
# Capture events
events_fired = []
original_trigger = get_model("sale.order").trigger
get_model("sale.order").trigger = lambda ids, event: events_fired.append((ids, event))
# Post partial payment
get_model("account.payment").post([payment_id])
# Restore trigger
get_model("sale.order").trigger = original_trigger
# Verify NO "paid" event was triggered
assert ([order_id], "paid") not in events_fired
# Verify order is still unpaid
order = get_model("sale.order").browse(order_id)
assert not order.is_paid
Security Considerations¶
Permission Model¶
- Extension uses same permissions as base
account.paymentmodel - Posting payments requires account module permissions
- Triggering sale order events doesn't require separate permissions
Data Access¶
- Extension only accesses invoices already linked to the payment
- No additional data access beyond what payment posting already requires
- Event triggering operates on orders already related to the payment's invoices
Version History¶
Last Updated: 2025-01-05 Model Version: account_payment.py (Extension) Framework: Netforce Base Model: account.payment from account module Extension Module: netforce_sale
Additional Resources¶
- Base Model Documentation:
account.payment - Sale Order Documentation:
sale.order - Invoice Documentation:
account.invoice - Workflow Configuration Guide
Support & Feedback¶
For issues or questions about this extension:
1. Verify invoice is properly linked to sale order via related_id
2. Check payment pay_type is "invoice" not "direct"
3. Confirm order payment status before and after payment posting
4. Review workflow configurations for "paid" event handlers
5. Test in development environment with logging enabled
This documentation covers the sale module's extension of the account.payment model for automatic sale order payment event triggering.