Skip to content

Account Budget Line Documentation

Overview

The Account Budget Line model (account.budget.line) represents individual line items within a budget. Each line specifies a budgeted amount for a specific account, with computed fields that automatically calculate actual amounts and variance from the general ledger.


Model Information

Model Name: account.budget.line Display Name: Budget Line Key Fields: None (no unique constraint defined)

Features

  • ❌ Audit logging enabled (_audit_log)
  • ❌ Multi-company support (company_id)
  • ❌ Full-text content search (_content_search)
  • ✅ Automatic actual amount calculation from GL
  • ✅ Variance calculation and percentage
  • ✅ Optional tracking and contact filtering
  • ✅ Cascade delete with parent budget

Key Fields Reference

Required Fields

Field Type Required Description
budget_id Many2One Parent budget record
account_id Many2One GL account to budget
budget_amount Decimal Budgeted amount

Optional Fields

Field Type Description
sequence Integer Display order sequence
track_id Many2One Filter actuals by tracking category
contact_id Many2One Filter actuals by supplier/contact

Computed Fields

Field Type Description
actual_amount Decimal Actual GL balance for the account
variance Decimal Difference: actual - budget
variance_percent Decimal Variance as percentage of budget
paid_amount Decimal Paid portion (placeholder)
unpaid_amount Decimal Unpaid portion (placeholder)

Default Ordering

Records are ordered by sequence:

_order = "sequence"

Computed Fields Functions

get_actual(ids, context)

Calculates actual amounts from the general ledger based on the parent budget's date range.

Logic: 1. Gets date range from parent budget (date_from, date_to) 2. Applies optional contact or tracking filter 3. Retrieves account balance for the period 4. Calculates variance and variance percentage

Formula:

actual_amount = account.balance  # Within date range
variance = actual_amount - budget_amount
variance_percent = (variance * 100) / budget_amount  # if budget_amount > 0

Example Result:

{
    "actual_amount": 45000.00,   # From GL
    "variance": -5000.00,        # Under budget by 5000
    "variance_percent": -10.00,  # 10% under budget
}


get_paid(ids, context)

Placeholder function for paid/unpaid tracking.

Current Implementation:

{
    "paid_amount": 0,
    "unpaid_amount": actual_amount,  # FIXME: Not fully implemented
}


API Methods

1. Create Budget Line

Method: create(vals, context)

Creates a new budget line. Usually created via parent budget.

Parameters:

vals = {
    "budget_id": budget_id,           # Required: Parent budget
    "account_id": account_id,         # Required: GL account
    "budget_amount": 50000.00,        # Required: Budget amount
    "sequence": 10,                   # Optional: Display order
    "track_id": track_id,             # Optional: Tracking filter
    "contact_id": supplier_id,        # Optional: Contact filter
}

Returns: int - New record ID

Example (Standalone):

line_id = get_model("account.budget.line").create({
    "budget_id": budget_id,
    "account_id": marketing_expense_id,
    "budget_amount": 25000.00,
    "sequence": 1,
})

Example (Via Parent):

budget_id = get_model("account.budget").create({
    "name": "2024 Budget",
    "date_from": "2024-01-01",
    "date_to": "2024-12-31",
    "lines": [
        ("create", {
            "account_id": salary_account_id,
            "budget_amount": 500000.00,
            "sequence": 1,
        }),
        ("create", {
            "account_id": rent_account_id,
            "budget_amount": 120000.00,
            "sequence": 2,
        }),
    ]
})


2. Read Budget Lines

Method: browse(ids, context)

Retrieve budget line records.

Example:

lines = get_model("account.budget.line").browse(line_ids)
for line in lines:
    print(f"Account: {line.account_id.name}")
    print(f"  Budget: {line.budget_amount:,.2f}")
    print(f"  Actual: {line.actual_amount:,.2f}")
    print(f"  Variance: {line.variance:,.2f}")
    if line.variance_percent:
        print(f"  Variance %: {line.variance_percent:.1f}%")


3. Update Budget Line

Method: write(ids, vals, context)

Update existing budget line records.

Example:

# Increase budget amount
get_model("account.budget.line").write([line_id], {
    "budget_amount": 60000.00
})

# Add tracking filter
get_model("account.budget.line").write([line_id], {
    "track_id": department_track_id
})


4. Delete Budget Line

Method: delete(ids, context)

Delete budget line records. Lines are also automatically deleted when parent budget is deleted (cascade).

Example:

get_model("account.budget.line").delete([line_id])


Model Relationship Description
account.budget Many2One (budget_id) Parent budget record
account.account Many2One (account_id) GL account being budgeted
account.track.categ Many2One (track_id) Tracking category filter
contact Many2One (contact_id) Supplier/contact filter

Common Use Cases

Use Case 1: Budget vs Actual Analysis

# Get budget with actual comparison
budget = get_model("account.budget").browse([budget_id])[0]

print(f"Budget: {budget.name}")
print(f"Period: {budget.date_from} to {budget.date_to}")
print("-" * 70)
print(f"{'Account':<30} {'Budget':>12} {'Actual':>12} {'Variance':>12}")
print("-" * 70)

for line in budget.lines:
    var_indicator = "!" if (line.variance or 0) > 0 else " "
    print(f"{line.account_id.name:<30} "
          f"{line.budget_amount:>12,.2f} "
          f"{(line.actual_amount or 0):>12,.2f} "
          f"{(line.variance or 0):>12,.2f}{var_indicator}")

Use Case 2: Budget by Tracking Category

# Create budget with tracking filters (e.g., by department)
budget_id = get_model("account.budget").create({
    "name": "2024 Departmental Expenses",
    "date_from": "2024-01-01",
    "date_to": "2024-12-31",
    "lines": [
        # Sales department
        ("create", {
            "account_id": expense_account_id,
            "budget_amount": 50000.00,
            "track_id": sales_dept_id,
            "sequence": 1,
        }),
        # Marketing department
        ("create", {
            "account_id": expense_account_id,
            "budget_amount": 75000.00,
            "track_id": marketing_dept_id,
            "sequence": 2,
        }),
        # Operations department
        ("create", {
            "account_id": expense_account_id,
            "budget_amount": 100000.00,
            "track_id": operations_dept_id,
            "sequence": 3,
        }),
    ]
})

Use Case 3: Supplier-Specific Budget

# Budget spending with specific suppliers
budget_id = get_model("account.budget").create({
    "name": "2024 Supplier Contracts",
    "date_from": "2024-01-01",
    "date_to": "2024-12-31",
    "lines": [
        ("create", {
            "account_id": it_services_account_id,
            "budget_amount": 200000.00,
            "contact_id": cloud_provider_id,  # AWS/Azure contract
            "sequence": 1,
        }),
        ("create", {
            "account_id": office_supplies_id,
            "budget_amount": 15000.00,
            "contact_id": office_depot_id,
            "sequence": 2,
        }),
    ]
})

Use Case 4: Budget Utilization Report

# Generate utilization report
budget = get_model("account.budget").browse([budget_id])[0]

print("Budget Utilization Report")
print("=" * 50)

over_budget = []
under_utilized = []

for line in budget.lines:
    utilization = ((line.actual_amount or 0) / line.budget_amount * 100) if line.budget_amount else 0

    if utilization > 100:
        over_budget.append({
            "account": line.account_id.name,
            "budget": line.budget_amount,
            "actual": line.actual_amount,
            "over_by": line.variance,
            "percent": utilization,
        })
    elif utilization < 50:
        under_utilized.append({
            "account": line.account_id.name,
            "budget": line.budget_amount,
            "actual": line.actual_amount,
            "percent": utilization,
        })

print("\nOver Budget Items:")
for item in over_budget:
    print(f"  {item['account']}: {item['percent']:.1f}% utilized (over by {item['over_by']:,.2f})")

print("\nUnder-Utilized Items (< 50%):")
for item in under_utilized:
    print(f"  {item['account']}: {item['percent']:.1f}% utilized")

Use Case 5: Adjust Budget Mid-Year

# Find line and adjust budget
budget = get_model("account.budget").browse([budget_id])[0]

for line in budget.lines:
    # If actual is 80% of budget with 6 months remaining, increase budget
    if line.budget_amount and line.actual_amount:
        utilization = line.actual_amount / line.budget_amount
        if utilization > 0.8:  # More than 80% used
            new_budget = line.budget_amount * 1.5  # Increase by 50%
            line.write({"budget_amount": new_budget})
            print(f"Increased {line.account_id.name} budget to {new_budget:,.2f}")

Best Practices

1. Use Sequence for Logical Ordering

# Good: Ordered by importance/category
lines = [
    ("create", {"account_id": revenue_id, "budget_amount": 1000000, "sequence": 1}),
    ("create", {"account_id": cogs_id, "budget_amount": 600000, "sequence": 2}),
    ("create", {"account_id": opex_id, "budget_amount": 200000, "sequence": 3}),
]

# Bad: Random order
lines = [
    ("create", {"account_id": opex_id, "budget_amount": 200000}),
    ("create", {"account_id": revenue_id, "budget_amount": 1000000}),
    ("create", {"account_id": cogs_id, "budget_amount": 600000}),
]

2. Use Tracking for Granular Analysis

# Good: Track by department for detailed analysis
("create", {
    "account_id": expense_id,
    "budget_amount": 50000,
    "track_id": sales_department_id,  # Filter actuals to this department
})

# Limited: No tracking (all expenses combined)
("create", {
    "account_id": expense_id,
    "budget_amount": 50000,
})

3. Review Variance Regularly

# Check for significant variances (> 10%)
budget = get_model("account.budget").browse([budget_id])[0]
alerts = []

for line in budget.lines:
    if line.variance_percent and abs(line.variance_percent) > 10:
        alerts.append({
            "account": line.account_id.name,
            "variance_pct": line.variance_percent,
            "variance_amt": line.variance,
        })

if alerts:
    print("Budget Variance Alerts:")
    for alert in alerts:
        status = "OVER" if alert["variance_pct"] > 0 else "UNDER"
        print(f"  {alert['account']}: {abs(alert['variance_pct']):.1f}% {status} budget")

Troubleshooting

"Actual amount is None or 0"

Cause: Parent budget has no date range, or no transactions in period Solution: Ensure budget has date_from and date_to set

"Variance percentage is None"

Cause: Budget amount is 0 or None Solution: Set a valid budget_amount > 0

"Wrong actual amount"

Cause: Account balance includes transactions outside budget period Solution: Verify budget date range matches expected period

"Tracking filter not working"

Cause: Transactions not tagged with tracking category Solution: Ensure journal entries have proper track_id values


Testing Examples

Unit Test: Budget Line Calculations

def test_budget_line_variance():
    # Create budget with line
    budget_id = get_model("account.budget").create({
        "name": "Test Budget",
        "date_from": "2024-01-01",
        "date_to": "2024-12-31",
        "lines": [
            ("create", {
                "account_id": test_account_id,
                "budget_amount": 10000.00,
            })
        ]
    })

    budget = get_model("account.budget").browse([budget_id])[0]
    line = budget.lines[0]

    # Verify budget amount
    assert line.budget_amount == 10000.00

    # Verify computed fields exist
    assert hasattr(line, 'actual_amount')
    assert hasattr(line, 'variance')
    assert hasattr(line, 'variance_percent')

    # Cleanup
    get_model("account.budget").delete([budget_id])

Security Considerations

Permission Model

  • Typically restricted to finance/management
  • May grant read access to department managers for their budgets

Data Access

  • Budget data is sensitive financial information
  • Actuals are pulled from GL with proper filtering

Database Behavior

Cascade Delete

When a parent budget is deleted, all its lines are automatically deleted:

on_delete="cascade"

Performance Notes

The get_actual function queries the account balance for each line, which involves: - Reading parent budget dates - Querying account model with date context - Filtering by contact or tracking if specified

For budgets with many lines, this may impact performance. Consider caching or batch processing for reports.


Version History

Last Updated: December 2024 Model Version: account_budget_line.py Framework: Netforce


Additional Resources

  • Account Budget Documentation: account.budget
  • Account Documentation: account.account
  • Tracking Category Documentation: account.track.categ
  • Contact Documentation: contact

This documentation is generated for developer onboarding and reference purposes.