Skip to content

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:

_order = "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:

get_model("payment.plan").delete([plan_id])


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,
})

# 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.