Skip to content

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?

  1. Modularity: Sale module functionality stays in sale module
  2. Maintainability: Base account module remains unchanged
  3. Upgradability: Base model can be updated independently
  4. 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:

  1. Invoice Payment Detection
  2. Only processes payments where pay_type == "invoice"
  3. Examines invoice_lines to find related invoices

  4. Sale Order Linkage Discovery

  5. Checks invoice's related_id field for sale.order references
  6. Checks individual invoice line related_id fields
  7. Uses polymorphic field pattern: model_name,record_id

  8. Payment Status Tracking

  9. Records sale orders that are unpaid BEFORE posting
  10. Checks same orders AFTER posting
  11. Only triggers event for orders that transitioned to paid status

  12. Event Triggering

  13. Fires "paid" workflow event on sale orders
  14. Can trigger downstream automations
  15. 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

The extension discovers sale order relationships through multiple paths:

Path 1: Invoice Level Linkage

invoice.related_id = "sale.order,123"

When an invoice is directly related to a sale order:

Payment → Invoice → related_id → Sale Order

Path 2: Invoice Line Level Linkage

invoice_line.related_id = "sale.order,123"

When invoice lines reference sale order (common for partial invoices):

Payment → Invoice → Invoice Lines → related_id → Sale Order

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

get_model("sale.order").trigger(paid_sale_ids, "paid")

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

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:

  1. Email Notifications
  2. Send payment confirmation to customer
  3. Notify sales rep of payment received
  4. Alert accounting team

  5. Status Updates

  6. Change order state to "ready_to_ship"
  7. Update payment status fields
  8. Set flags for fulfillment

  9. Process Automation

  10. Trigger shipping preparation
  11. Generate packing lists
  12. Update inventory reservations

  13. External Integrations

  14. Sync payment status to external systems
  15. Update CRM records
  16. Trigger third-party webhooks

Troubleshooting

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.payment model
  • 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.