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:
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:
- payment_received() - Handles successful payment confirmations
- payment_pending() - Handles pending payment states
- 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:
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:
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:
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:
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:
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
Related Models¶
| 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¶
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 methodspayment_method_write- Configure payment methodspayment_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:
accountmodule - 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.