Payment Plan Documentation¶
Overview¶
The Payment Plan model (payment.plan) manages scheduled payment installments for recurring billing scenarios. It tracks payment schedules with support for one-time, monthly, or yearly periodicity, and automatically monitors the invoice state for each installment.
Model Information¶
Model Name: payment.plan
Display Name: Payment Plan
Name Field: description
Key Fields: None (no unique constraint defined)
Features¶
- ❌ Audit logging enabled (
_audit_log) - ❌ Multi-company support (
company_id) - ❌ Full-text content search (
_content_search) - ✅ Flexible periodicity (one-time, monthly, yearly)
- ✅ Automatic state tracking based on invoice status
- ✅ Sequence ordering for installments
- ✅ Reference linking to related records
Key Fields Reference¶
All Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
related_id |
Reference | ❌ | Related record (polymorphic reference) |
sequence |
Integer | ❌ | Display order for installments |
description |
Char | ❌ | Payment description (name field) |
amount |
Decimal | ✅ | Payment amount |
invoice_id |
Many2One | ❌ | Generated invoice for this installment |
period |
Selection | ❌ | Payment periodicity |
state |
Selection | ❌ | Payment status (computed) |
Periodicity Options¶
| Period | Code | Description |
|---|---|---|
| One Time | one |
Single payment |
| Monthly | month |
Recurring monthly |
| Yearly | year |
Recurring yearly |
State Values (Computed)¶
| State | Code | Description |
|---|---|---|
| Waiting | waiting |
No invoice created yet |
| Draft Invoice | draft_invoice |
Invoice created in draft |
| Invoice Sent | invoice_sent |
Invoice sent, awaiting payment |
| Invoice Paid | invoice_paid |
Invoice has been paid |
Default Ordering¶
Records are ordered by sequence:
Computed Fields¶
get_state(ids, context)¶
Calculates the payment plan state based on linked invoice status.
Logic:
if no invoice:
state = "waiting"
elif invoice.state == "draft":
state = "draft_invoice"
elif invoice.state == "waiting_payment":
state = "invoice_sent"
elif invoice.state == "paid":
state = "invoice_paid"
Example:
states = get_model("payment.plan").get_state([plan_id])
print(f"Payment plan state: {states[plan_id]}")
API Methods¶
1. Create Payment Plan¶
Method: create(vals, context)
Creates a new payment plan entry.
Parameters:
vals = {
"related_id": "sale.order,123", # Optional: Related record
"sequence": 1, # Optional: Order
"description": "First installment", # Optional: Description
"amount": 5000.00, # Required: Amount
"period": "one", # Optional: Periodicity
}
Returns: int - New record ID
Example:
plan_id = get_model("payment.plan").create({
"related_id": f"sale.order,{order_id}",
"sequence": 1,
"description": "Deposit Payment (30%)",
"amount": 3000.00,
"period": "one",
})
2. Search Payment Plans¶
Method: search(condition, context)
Search for payment plan records.
Example:
# Find all plans for a specific record
plans = get_model("payment.plan").search([
["related_id", "=", f"sale.order,{order_id}"]
])
# Find all waiting payments
waiting = get_model("payment.plan").search([
["state", "=", "waiting"]
])
3. Browse Payment Plans¶
Method: browse(ids, context)
Retrieve payment plan records.
Example:
plans = get_model("payment.plan").browse(plan_ids)
for plan in plans:
print(f"{plan.sequence}. {plan.description}: {plan.amount}")
print(f" Status: {plan.state}")
if plan.invoice_id:
print(f" Invoice: {plan.invoice_id.number}")
4. Update Payment Plan¶
Method: write(ids, vals, context)
Update payment plan records.
Example:
# Link invoice to payment plan
get_model("payment.plan").write([plan_id], {
"invoice_id": invoice_id
})
# Update amount
get_model("payment.plan").write([plan_id], {
"amount": 5500.00
})
5. Delete Payment Plan¶
Method: delete(ids, context)
Delete payment plan records.
Example:
Related Models¶
| Model | Relationship | Description |
|---|---|---|
account.invoice |
Many2One (invoice_id) | Invoice generated for this installment |
| Various | Reference (related_id) | Polymorphic reference to parent record |
Common Use Cases¶
Use Case 1: Create Installment Payment Plan¶
# Create payment plan for a large order with 3 installments
order_id = 123
total_amount = 10000.00
installments = [
{"desc": "Deposit (30%)", "pct": 0.30, "seq": 1},
{"desc": "Progress Payment (50%)", "pct": 0.50, "seq": 2},
{"desc": "Final Payment (20%)", "pct": 0.20, "seq": 3},
]
for inst in installments:
get_model("payment.plan").create({
"related_id": f"sale.order,{order_id}",
"sequence": inst["seq"],
"description": inst["desc"],
"amount": total_amount * inst["pct"],
"period": "one",
})
Use Case 2: Create Monthly Subscription Plan¶
# Create monthly payment plan for subscription
subscription_id = 456
monthly_fee = 99.00
for month in range(1, 13): # 12 months
get_model("payment.plan").create({
"related_id": f"subscription,{subscription_id}",
"sequence": month,
"description": f"Month {month} Subscription",
"amount": monthly_fee,
"period": "month",
})
Use Case 3: Generate Invoice for Payment Plan¶
# Generate invoice for next pending payment
plan = get_model("payment.plan").browse([plan_id])[0]
if plan.state == "waiting":
# Create invoice
invoice_id = get_model("account.invoice").create({
"type": "out",
"contact_id": customer_id,
"lines": [
("create", {
"description": plan.description,
"qty": 1,
"unit_price": plan.amount,
})
]
})
# Link invoice to payment plan
plan.write({"invoice_id": invoice_id})
print(f"Created invoice for: {plan.description}")
Use Case 4: Track Payment Progress¶
# Get payment progress for an order
order_id = 123
plans = get_model("payment.plan").search_browse([
["related_id", "=", f"sale.order,{order_id}"]
], order="sequence")
total = sum(p.amount for p in plans)
paid = sum(p.amount for p in plans if p.state == "invoice_paid")
pending = total - paid
print(f"Payment Progress for Order {order_id}:")
print(f" Total: {total:,.2f}")
print(f" Paid: {paid:,.2f}")
print(f" Pending: {pending:,.2f}")
print(f" Progress: {(paid/total)*100:.1f}%")
print()
for plan in plans:
status_icon = {
"waiting": "⏳",
"draft_invoice": "📝",
"invoice_sent": "📤",
"invoice_paid": "✅",
}.get(plan.state, "❓")
print(f" {status_icon} {plan.sequence}. {plan.description}: {plan.amount:,.2f}")
Use Case 5: Send Reminder for Pending Invoices¶
# Find payment plans with sent but unpaid invoices
pending_plans = get_model("payment.plan").search_browse([
["state", "=", "invoice_sent"]
])
for plan in pending_plans:
invoice = plan.invoice_id
if invoice:
# Check if invoice is overdue
from datetime import date
if invoice.due_date and invoice.due_date < date.today().isoformat():
print(f"OVERDUE: {plan.description} - Invoice {invoice.number}")
# Send reminder email
# get_model("account.invoice").send_reminder([invoice.id])
Best Practices¶
1. Use Sequence for Ordering¶
# Good: Clear sequence for installment order
plans = [
{"sequence": 1, "description": "Deposit", "amount": 1000},
{"sequence": 2, "description": "Progress 1", "amount": 2000},
{"sequence": 3, "description": "Progress 2", "amount": 2000},
{"sequence": 4, "description": "Final", "amount": 1000},
]
# Bad: No sequence (order unpredictable)
plans = [
{"description": "Deposit", "amount": 1000},
{"description": "Final", "amount": 1000},
]
2. Use Descriptive Names¶
# Good: Clear descriptions
get_model("payment.plan").create({
"description": "Q1 2024 License Fee",
"amount": 2500.00,
"period": "year",
})
# Bad: Vague description
get_model("payment.plan").create({
"description": "Payment",
"amount": 2500.00,
})
3. Link to Parent Record¶
# Good: Always link to parent for context
get_model("payment.plan").create({
"related_id": f"project,{project_id}", # Clear context
"description": "Milestone 1 Payment",
"amount": 5000.00,
})
# Less ideal: Orphan payment plan
get_model("payment.plan").create({
"description": "Some payment",
"amount": 5000.00,
})
4. Match Period to Business Logic¶
# Subscription: Monthly
get_model("payment.plan").create({
"description": "Monthly Subscription",
"amount": 99.00,
"period": "month",
})
# Annual maintenance: Yearly
get_model("payment.plan").create({
"description": "Annual Maintenance",
"amount": 1200.00,
"period": "year",
})
# Project milestone: One-time
get_model("payment.plan").create({
"description": "Project Completion",
"amount": 10000.00,
"period": "one",
})
Troubleshooting¶
"State always shows 'waiting'"¶
Cause: No invoice linked to payment plan
Solution: Create invoice and link via invoice_id field
"State doesn't update"¶
Cause: State is computed - check invoice state Solution: Verify invoice state is updating correctly
"Payment plan not linked to order"¶
Cause: related_id not set correctly
Solution: Use format "model.name,id" (e.g., "sale.order,123")
"Sequence not respected in display"¶
Cause: Query doesn't specify order
Solution: Add order="sequence" to search/search_browse
Testing Examples¶
Unit Test: Payment Plan State Computation¶
def test_payment_plan_state():
# Create payment plan
plan_id = get_model("payment.plan").create({
"description": "Test Payment",
"amount": 1000.00,
"period": "one",
})
# Check initial state
plan = get_model("payment.plan").browse([plan_id])[0]
assert plan.state == "waiting"
# Create draft invoice
invoice_id = get_model("account.invoice").create({
"type": "out",
"contact_id": test_customer_id,
"lines": [("create", {
"description": "Test",
"qty": 1,
"unit_price": 1000.00,
})]
})
# Link invoice
plan.write({"invoice_id": invoice_id})
plan = get_model("payment.plan").browse([plan_id])[0]
assert plan.state == "draft_invoice"
# Cleanup
get_model("account.invoice").delete([invoice_id])
get_model("payment.plan").delete([plan_id])
Security Considerations¶
Permission Model¶
- Typically linked to sales/project permissions
- Users can view payment plans for their records
- Finance can view all payment plans
Data Access¶
- Payment plan access often controlled by parent record access
- Invoice access controls payment visibility
Version History¶
Last Updated: December 2024 Model Version: payment_plan.py Framework: Netforce
Additional Resources¶
- Invoice Documentation:
account.invoice - Payment Documentation:
account.payment - Sales Order Documentation:
sale.order
This documentation is generated for developer onboarding and reference purposes.