Skip to content

Payment Method (Sale Module Extension) Documentation

Overview

The Payment Method Extension module extends the base payment.method model from the account module with sale-specific functionality. This is NOT a standalone model but an EXTENSION that adds sales order payment processing capabilities, e-commerce integration, and sales-specific payment callbacks to the existing payment.method infrastructure.

IMPORTANT: This module uses _inherit to extend the existing payment.method model rather than creating a new one. All functionality described here is ADDED to the base payment method model when the sale module is installed.


Model Information

Model Name: payment.method (INHERITED/EXTENDED) Display Name: Payment Method Extension Type: Model Inheritance (_inherit) Base Module: account module

Features Added by Sale Module

  • ✅ Sales order payment processing
  • ✅ E-commerce payment gateway integration
  • ✅ Payment callback handling (received, pending, error)
  • ✅ Transaction number tracking for online payments
  • ✅ Automated invoice payment creation

Extension Architecture

Understanding Model Inheritance

The sale module uses Netforce's inheritance mechanism to add functionality:

class PaymentMethod(Model):
    _inherit = "payment.method"  # Extends existing model

This means: - The base payment.method model is defined in the account module - The sale module ADDS new methods to handle sales-specific payments - All base fields and methods remain available - New methods can override base methods using super()

Methods Added by Sale Module

The sale module extension adds three critical payment callback methods:

  1. payment_received() - Handles successful payment confirmations
  2. payment_pending() - Handles pending payment states
  3. payment_error() - Handles payment failures

Base Payment Method Fields

The base payment.method model (from account module) provides:

Field Type Description
name Char Payment method name (Cash, Credit Card, Bank Transfer, etc.)
type Selection Payment type code used by payment gateways
account_id Many2One Account for recording payments
require_report Boolean Whether receipt/report printing is required

Note: Refer to account module documentation for complete base field reference.


Sale Module Extensions

Field Usage in Sale Context

When used with sales orders, these additional fields are referenced:

Field Model Usage in Sale
pay_method_id sale.order Links order to selected payment method
transaction_no Context Unique transaction identifier from payment gateway
is_paid sale.order Payment status flag

API Methods (Sale Module Extensions)

1. payment_received

Method: payment_received(context)

Handles successful payment notifications from payment gateways for sales orders. This is the callback method triggered when a customer completes payment.

Parameters:

context = {
    "transaction_no": str,        # Required: Unique transaction identifier
    "type": str,                  # Payment type from gateway
    "pay_method_id": int          # Payment method ID
}

Behavior: 1. Calls base implementation via super() first 2. Looks up sales order by transaction_no 3. Verifies order payment status (skip if already paid) 4. Validates payment method type matches order 5. Creates payment record for the order's invoice 6. Returns redirect URL for success page

Returns:

{
    "next_url": str  # URL to redirect customer after payment
}

Example:

# Called by payment gateway webhook/callback
result = get_model("payment.method").payment_received(context={
    "transaction_no": "TXN-2026-001234",
    "type": "credit_card",
    "pay_method_id": 5
})

# Returns:
# {
#     "next_url": "/ui#name=sale&mode=form&active_id=123"
# }

Process Flow:

Payment Gateway → payment_received() → Find Sale Order
                                      ↓
                              Verify Not Paid
                                      ↓
                              Validate Method
                                      ↓
                              Create Payment
                                      ↓
                              Return Success URL

Audit Logging:

audit_log("Payment received: transaction_no=%s" % transaction_no)
audit_log("Creating payment for sales order %s: transaction_no=%s" % (sale.number, transaction_no))


2. payment_pending

Method: payment_pending(context)

Handles pending payment notifications when payment is initiated but not yet confirmed (e.g., bank transfer pending, payment gateway processing).

Parameters:

context = {
    "transaction_no": str  # Required: Transaction identifier
}

Behavior: 1. Calls base implementation via super() 2. Looks up sales order by transaction_no 3. Returns redirect URL to sales order form 4. Does NOT create payment record (payment not confirmed)

Returns:

{
    "next_url": str  # URL to redirect customer
}

Example:

# Payment gateway indicates processing
result = get_model("payment.method").payment_pending(context={
    "transaction_no": "TXN-2026-001234"
})

# Customer sees "Payment pending" message at redirect URL

Use Cases: - Bank transfer initiated but not cleared - Payment gateway processing delay - Multi-step payment approval process - Waiting for fraud verification


3. payment_error

Method: payment_error(context)

Handles payment failure notifications when customer payment is declined or encounters errors.

Parameters:

context = {
    "transaction_no": str  # Required: Transaction identifier
}

Behavior: 1. Calls base implementation via super() 2. Looks up sales order by transaction_no 3. Returns redirect URL to sales order (no payment created) 4. Order remains unpaid for retry

Returns:

{
    "next_url": str  # URL to redirect customer
}

Example:

# Payment declined by bank
result = get_model("payment.method").payment_error(context={
    "transaction_no": "TXN-2026-001234"
})

# Customer redirected to retry payment or choose different method

Common Causes: - Insufficient funds - Credit card declined - Gateway timeout - Invalid payment details - Fraud detection triggered


Integration with Sale Order

Sale Order Payment Flow

1. Customer places order → sale.order created
2. Order confirmed → Invoice generated
3. Customer initiates payment → transaction_no assigned
4. Payment gateway processes → Callback to payment_received()
5. Payment verified → Payment record created
6. Invoice marked paid → Order completed

Transaction Number Assignment

The transaction_no field is critical for linking gateway payments to orders:

# In sale.order.pay_online() method
transaction_no = str(uuid.uuid4())  # Generate unique ID
obj.write({"transaction_no": transaction_no})

# Send to payment gateway
gateway.process_payment(
    amount=obj.amount_total,
    transaction_no=transaction_no,
    callback_url="/payment/callback"
)

Payment Method Selection

Sales orders can specify payment method in two ways:

1. Pre-selected on Order:

sale_id = get_model("sale.order").create({
    "contact_id": customer_id,
    "lines": [...],
    "pay_method_id": credit_card_method_id  # Set during order creation
})

2. Selected at Checkout:

# Customer chooses at payment time
result = get_model("sale.order").pay_online(
    [sale_id],
    context={"pay_method_id": paypal_method_id}
)


E-commerce Integration

E-commerce Settings

The payment callbacks reference ecom2.settings for return URLs:

settings = get_model("ecom2.settings").browse(1)
if settings.ecom_return_url:
    url = settings.ecom_return_url + str(sale_id)
else:
    url = "/ui#name=sale&mode=form&active_id=%d" % sale_id

Configuration: - ecom_return_url: Base URL for e-commerce success page - If not set, defaults to internal UI form view


Model Relationship Description
sale.order Many2One (reverse) Sales orders using this payment method
account.invoice Via sale.order Invoices paid through this method
account.payment Created by Payments generated from sales
ecom2.settings Configuration E-commerce return URL settings
account.account Many2One (base) Accounting account for payments

Common Use Cases

Use Case 1: Process Online Credit Card Payment

# Customer completes payment on gateway site
# Gateway calls webhook with payment confirmation

# Webhook handler receives notification
webhook_data = {
    "transaction_id": "TXN-2026-001234",
    "status": "completed",
    "payment_type": "credit_card"
}

# Find payment method
credit_card_method = get_model("payment.method").search([
    ["type", "=", "credit_card"]
])[0]

# Process payment
result = get_model("payment.method").payment_received(context={
    "transaction_no": webhook_data["transaction_id"],
    "type": webhook_data["payment_type"],
    "pay_method_id": credit_card_method
})

# Redirect customer to success page
redirect_url = result["next_url"]
print(f"Payment successful, redirect to: {redirect_url}")

Use Case 2: Handle PayPal Payment Pending

# PayPal sends pending notification (e-check processing)

# Find PayPal method
paypal_method = get_model("payment.method").search([
    ["name", "=", "PayPal"]
])[0]

# Handle pending status
result = get_model("payment.method").payment_pending(context={
    "transaction_no": "TXN-2026-001235"
})

# Show pending message to customer
print("Payment is being processed, please check back later")
print(f"View order at: {result['next_url']}")

Use Case 3: Handle Failed Payment with Retry

# Payment gateway reports card declined

# Process error
result = get_model("payment.method").payment_error(context={
    "transaction_no": "TXN-2026-001236"
})

# Get the sales order for retry
transaction_no = "TXN-2026-001236"
sale_ids = get_model("sale.order").search([
    ["transaction_no", "=", transaction_no]
])

if sale_ids:
    sale = get_model("sale.order").browse(sale_ids[0])

    # Check if already paid (shouldn't be, but verify)
    if not sale.is_paid:
        # Allow customer to try different payment method
        print(f"Payment failed for order {sale.number}")
        print("Please try a different payment method")
        print(f"Retry at: {result['next_url']}")

Use Case 4: Configure Payment Method for E-commerce

# Setup Stripe payment method for online sales

# 1. Create payment method (in account module)
stripe_account_id = get_model("account.account").search([
    ["code", "=", "1020"]  # Stripe clearing account
])[0]

stripe_method_id = get_model("payment.method").create({
    "name": "Stripe Credit Card",
    "type": "stripe",
    "account_id": stripe_account_id,
    "require_report": False  # No receipt needed, online confirmation
})

# 2. Configure e-commerce settings
settings = get_model("ecom2.settings").browse(1)
settings.write({
    "ecom_return_url": "https://mystore.com/order-success/"
})

# 3. Use in sales order
sale_id = get_model("sale.order").create({
    "contact_id": customer_id,
    "pay_method_id": stripe_method_id,
    "lines": [
        ("create", {
            "product_id": product_id,
            "qty": 1,
            "unit_price": 99.99
        })
    ]
})

# 4. Process payment when customer pays
result = get_model("sale.order").pay_online([sale_id], context={
    "pay_method_id": stripe_method_id
})

# Customer redirected to: https://mystore.com/order-success/123

Use Case 5: Validate Payment Method Before Processing

# Verify payment method configuration before accepting payments

def validate_payment_method(method_id):
    method = get_model("payment.method").browse(method_id)

    # Check required fields
    if not method.account_id:
        raise Exception(f"Payment method {method.name} has no account configured")

    if not method.type:
        raise Exception(f"Payment method {method.name} has no type set")

    # Verify account is active
    if not method.account_id.active:
        raise Exception(f"Payment account for {method.name} is not active")

    return True

# Use before creating sales order
payment_method_id = 5
if validate_payment_method(payment_method_id):
    sale_id = get_model("sale.order").create({
        "contact_id": customer_id,
        "pay_method_id": payment_method_id,
        "lines": [...]
    })

Best Practices

1. Always Validate Payment Method Type

# Bad - Assuming method type matches:
result = get_model("payment.method").payment_received(context={
    "transaction_no": "TXN123",
    "type": "paypal",
    "pay_method_id": method_id
})

# Good - Verify method type matches:
sale = get_model("sale.order").browse(sale_id)
method = sale.pay_method_id

if method.type != payment_type:
    raise Exception(
        f"Payment type mismatch: gateway={payment_type}, order={method.type}"
    )

Why this matters: - Prevents recording wrong payment type - Ensures accounting accuracy - Detects payment gateway configuration errors - Maintains audit trail integrity


2. Check Payment Status Before Processing

# Bad - Processing without checking:
result = get_model("payment.method").payment_received(context={...})

# Good - Verify not already paid:
sale = get_model("sale.order").search([
    ["transaction_no", "=", transaction_no]
])[0]
sale_obj = get_model("sale.order").browse(sale)

if sale_obj.is_paid:
    print(f"Order {sale_obj.number} already paid, skipping")
    return

# Proceed with payment processing
result = get_model("payment.method").payment_received(context={...})

Benefits: - Prevents duplicate payment records - Avoids accounting errors - Handles gateway retry scenarios - Maintains payment idempotency


3. Use Audit Logging for Payment Tracking

The extension already includes audit logging - ensure it's enabled:

from netforce.logger import audit_log

# Payment received callback already logs:
audit_log("Payment received: transaction_no=%s" % transaction_no)
audit_log("Creating payment for sales order %s: transaction_no=%s" % (sale.number, transaction_no))

Review audit logs for: - Payment processing timeline - Failed payment attempts - Duplicate transaction detection - Gateway callback patterns


4. Configure E-commerce Return URLs Properly

# Bad - Hardcoding URLs in code:
return {
    "next_url": "https://mystore.com/success"  # Wrong - not order-specific
}

# Good - Using configured settings:
settings = get_model("ecom2.settings").browse(1)
if settings.ecom_return_url:
    url = settings.ecom_return_url + str(sale_id)  # Order-specific
else:
    url = "/ui#name=sale&mode=form&active_id=%d" % sale_id  # Fallback

return {"next_url": url}

Benefits: - Environment-specific URLs (dev/staging/production) - Centralized configuration - Easy to update without code changes - Proper fallback handling


5. Handle Payment Callback Failures Gracefully

# Robust payment callback handler

def handle_payment_callback(transaction_no, status, payment_type):
    try:
        # Find sales order
        sale_ids = get_model("sale.order").search([
            ["transaction_no", "=", transaction_no]
        ])

        if not sale_ids:
            audit_log(f"ERROR: No order found for transaction {transaction_no}")
            return {"error": "Order not found"}

        # Find payment method
        method_ids = get_model("payment.method").search([
            ["type", "=", payment_type]
        ])

        if not method_ids:
            audit_log(f"ERROR: No payment method for type {payment_type}")
            return {"error": "Invalid payment method"}

        # Process based on status
        if status == "completed":
            result = get_model("payment.method").payment_received(context={
                "transaction_no": transaction_no,
                "type": payment_type,
                "pay_method_id": method_ids[0]
            })
        elif status == "pending":
            result = get_model("payment.method").payment_pending(context={
                "transaction_no": transaction_no
            })
        else:
            result = get_model("payment.method").payment_error(context={
                "transaction_no": transaction_no
            })

        return result

    except Exception as e:
        audit_log(f"ERROR in payment callback: {str(e)}")
        return {"error": str(e)}

Integration with Payment Gateways

Webhook Configuration

Payment gateways need to call your callback URLs:

Stripe Example:

Success URL: https://yoursite.com/payment/success
Cancel URL: https://yoursite.com/payment/cancel
Webhook URL: https://yoursite.com/payment/webhook

PayPal Example:

Return URL: https://yoursite.com/payment/return
Cancel URL: https://yoursite.com/payment/cancel
IPN URL: https://yoursite.com/payment/ipn

Callback Handler Implementation

# Webhook endpoint receiving payment notifications

def payment_webhook_handler(request):
    # Parse gateway notification
    transaction_no = request.POST.get("transaction_id")
    status = request.POST.get("status")
    payment_type = request.POST.get("type")

    # Verify webhook signature (gateway-specific)
    if not verify_signature(request):
        return {"error": "Invalid signature"}

    # Process payment
    if status == "completed":
        result = get_model("payment.method").payment_received(context={
            "transaction_no": transaction_no,
            "type": payment_type
        })
    elif status == "pending":
        result = get_model("payment.method").payment_pending(context={
            "transaction_no": transaction_no
        })
    else:
        result = get_model("payment.method").payment_error(context={
            "transaction_no": transaction_no
        })

    return result

Performance Tips

1. Index Transaction Numbers

CREATE INDEX sale_order_transaction_no_idx
    ON sale_order (transaction_no);

Fast lookup of orders by transaction number improves callback response time.


2. Cache Payment Method Lookups

# Bad: Search every time
for callback in callbacks:
    method = get_model("payment.method").search([["type", "=", callback.type]])[0]

# Good: Cache method IDs by type
payment_methods_cache = {}
for method in get_model("payment.method").search([]):
    m = get_model("payment.method").browse(method)
    payment_methods_cache[m.type] = m.id

# Use cached values
method_id = payment_methods_cache.get(payment_type)

3. Batch Process Pending Payments

# Process multiple pending payments efficiently

pending_transactions = [...]  # List from gateway

# Batch search for all orders
transaction_nos = [t["transaction_no"] for t in pending_transactions]
sale_ids = get_model("sale.order").search([
    ["transaction_no", "in", transaction_nos],
    ["is_paid", "=", False]
])

# Process in batch
for sale_id in sale_ids:
    sale = get_model("sale.order").browse(sale_id)
    # Process payment for this order

Troubleshooting

"Missing sales order payment method" Error

Cause: Sale order created without pay_method_id Solution: Ensure payment method is set before processing payment

sale = get_model("sale.order").browse(sale_id)
if not sale.pay_method_id:
    # Set default payment method
    default_method = get_model("payment.method").search([
        ["name", "=", "Credit Card"]
    ])[0]
    sale.write({"pay_method_id": default_method})

"Missing account for payment method" Error

Cause: Payment method not properly configured with accounting account Solution: Configure account on payment method

method = get_model("payment.method").browse(method_id)
if not method.account_id:
    # Assign appropriate account
    account_id = get_model("account.account").search([
        ["code", "=", "1020"]  # Bank/clearing account
    ])[0]
    method.write({"account_id": account_id})

"Received sales order payment with wrong method" Error

Cause: Payment type from gateway doesn't match order's payment method Solution: Verify payment method configuration and gateway setup

# Check what the order expects
sale = get_model("sale.order").browse(sale_id)
print(f"Order expects: {sale.pay_method_id.type}")

# Check what gateway sent
print(f"Gateway sent: {payment_type}")

# They must match - fix gateway configuration or update order

Sales Order Not Found for Transaction

Cause: Transaction number mismatch or order not created properly Solution: Verify transaction number assignment

# Check if order has transaction number
sale = get_model("sale.order").browse(sale_id)
print(f"Order transaction_no: {sale.transaction_no}")

# Verify it matches gateway callback
print(f"Gateway sent: {callback_transaction_no}")

# Update if needed
if not sale.transaction_no:
    sale.write({"transaction_no": callback_transaction_no})

Security Considerations

Permission Model

  • Inherits base payment.method permissions
  • payment_method_read - View payment methods
  • payment_method_write - Configure payment methods
  • payment_received - Process payment callbacks (system/gateway only)

Data Access

  • Payment callbacks should be authenticated from gateway IP addresses only
  • Transaction numbers should be UUID or cryptographically secure
  • Payment method types should validate against allowed values
  • Webhook signatures must be verified before processing

Webhook Security

# Verify webhook authenticity
def verify_webhook_signature(request, secret_key):
    signature = request.headers.get("X-Gateway-Signature")
    payload = request.body

    expected = hmac.new(
        secret_key.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()

    return signature == expected

# Use in webhook handler
if not verify_webhook_signature(request, GATEWAY_SECRET):
    raise Exception("Invalid webhook signature")

Testing Examples

Unit Test: Payment Received Callback

def test_payment_received_callback():
    # Create test sale order
    sale_id = get_model("sale.order").create({
        "contact_id": test_customer_id,
        "pay_method_id": test_payment_method_id,
        "transaction_no": "TEST-TXN-001",
        "lines": [
            ("create", {
                "product_id": test_product_id,
                "qty": 1,
                "unit_price": 100.00
            })
        ]
    })

    # Confirm order to create invoice
    get_model("sale.order").do_confirm([sale_id])

    # Simulate payment callback
    result = get_model("payment.method").payment_received(context={
        "transaction_no": "TEST-TXN-001",
        "type": "credit_card",
        "pay_method_id": test_payment_method_id
    })

    # Verify payment created
    sale = get_model("sale.order").browse(sale_id)
    assert sale.is_paid == True

    # Verify redirect URL returned
    assert "next_url" in result

    # Cleanup
    get_model("sale.order").delete([sale_id])

Version History

Last Updated: 2026-01-05 Model Version: payment_method.py (sale module extension) Framework: Netforce Base Model: payment.method (account module)


Additional Resources

  • Base Payment Method Documentation: account module
  • Sale Order Documentation: sale.order
  • E-commerce Settings: ecom2.settings
  • Account Payment Documentation: account.payment
  • Payment Gateway Integration Guide: External payment providers

Support & Feedback

For issues or questions about this module: 1. Check base payment.method documentation in account module 2. Verify e-commerce settings configuration 3. Review payment gateway webhook logs 4. Test payment callbacks in development environment 5. Enable audit logging to track payment processing 6. Verify accounting account configuration on payment methods


This documentation is generated for developer onboarding and reference purposes.