Skip to content

Payment Model (account.payment)

Overview

The Payment model (account.payment) manages all cash receipts and disbursements in Netforce. It handles both direct payments (one-off transactions) and invoice payments (allocating payments against existing invoices). This model is essential for cash flow management, bank reconciliation, payment tracking, and financial reporting. It integrates closely with invoices, bank accounts, tax calculations, and journal entries.


Model Information

Model Name: account.payment
Display Name: Payment
Key Fields: company_id, number
Source File: netforce_account/models/account_payment.py

Features

  • ✅ Audit logging enabled (_audit_log = True)
  • ✅ Multi-company support (_multi_company = True)
  • ✅ Full-text content search (_content_search = True)
  • ✅ Unique key constraint per company (_key = ["company_id", "number"])
  • ✅ Name field: number (_name_field = "number")

Understanding Key Fields

What are Key Fields?

In Netforce models, Key Fields are a combination of fields that together create a composite unique identifier for a record. Think of them as a business key that ensures data integrity across the system.

For the account.payment model, the key fields are:

_key = ["company_id", "number"]

This means the combination of these fields must be unique: - company_id - The company owning this payment - number - The payment number (auto-generated from sequence)

Why Key Fields Matter

Uniqueness Guarantee - Key fields prevent duplicate records by ensuring unique combinations:

# Examples of valid combinations:
Company A, PAY-2025-001   Valid
Company A, PAY-2025-002   Valid
Company B, PAY-2025-001   Valid (different company)

# This would fail - duplicate key:
Company A, PAY-2025-001   ERROR: Payment number already exists!

Database Implementation

The key fields are enforced at the database level through unique constraints, ensuring payment numbers are unique per company.


Payment Types and Subtypes

The payment model supports multiple types and subtypes for different payment scenarios:

Primary Type (type)

Type Code Description
Paid out Money going out (disbursement) - supplier payments, expenses
Received in Money coming in (receipt) - customer payments, income

Subtype (pay_type)

Subtype Code Description
Direct Payment direct One-off payment not linked to invoices
Invoice Payment invoice Payment allocated against one or more invoices

State Workflow

draft → posted → voided
State Description Allowed Transitions
draft Initial state - editable → posted
posted Posted to journal, affects books → voided, → draft (via to_draft)
voided Cancelled, journal reversed None (final state)

Key Fields Reference

Header Fields

Field Type Required Description
number Char Payment number (auto-generated from sequence)
type Selection Payment direction: in (received) or out (paid)
pay_type Selection Payment subtype: direct or invoice
contact_id Many2One Payer or payee contact
date Date Payment date
account_id Many2One Bank/cash account for payment
currency_id Many2One Transaction currency
currency_rate Decimal(6) Currency conversion rate
tax_type Selection Tax handling: tax_ex, tax_in, or no_tax
state Selection Current workflow state
company_id Many2One Owning company
user_id Many2One Payment owner/creator

Reference Fields

Field Type Description
ref Char External reference (deprecated)
memo Char Memo/notes
tax_no Char Tax invoice number
wht_no Char Withholding tax certificate number
transaction_no Char Online payment transaction ID

Amount Fields (Computed)

Field Type Description
amount_subtotal Decimal Subtotal before tax
amount_tax Decimal Total tax amount (VAT)
amount_total Decimal Total amount including tax
amount_wht Decimal Withholding tax amount
amount_wht_base Decimal Base amount for WHT calculation
amount_payment Decimal Net payment amount (total - WHT)
amount_adjust Decimal Adjustment amount
amount_change Decimal Change amount (cash payments)
amount_total_words Char Amount in words

Payment Line Collections

Field Type Description
lines One2Many All payment lines (account.payment.line)
direct_lines One2Many Direct payment lines (type='direct')
invoice_lines One2Many Invoice payment lines (type='invoice')
prepay_lines One2Many Prepayment lines (type='prepay')
overpay_lines One2Many Overpayment lines (type='overpay')
claim_lines One2Many Expense claim lines (type='claim')
adjust_lines One2Many Adjustment lines (type='adjust')

Configuration Fields

Field Type Description
journal_id Many2One Accounting journal
sequence_id Many2One Number sequence
pay_method_id Many2One Payment method
default_line_desc Boolean Auto-fill line descriptions from memo
track_id Many2One Tracking category (analytics)

Relationship Fields

Field Type Description
move_id Many2One Journal entry
credit_invoices One2Many Created prepay/overpay invoices
related_id Reference Source document (sale, purchase, etc.)
employee_id Many2One Employee (for expense claims)
documents One2Many Attached documents
comments One2Many Comments/messages
cheques One2Many Related cheques
landed_costs One2Many Related landed costs
payment_category_id Many2One Payment category

Cheque Fields (Computed)

Field Type Description
cheque_number Char Cheque number (from related cheque)
cheque_bank Char Cheque bank (from related cheque)
cheque_branch Char Cheque branch (from related cheque)
cheque_date Date Cheque date (from related cheque)

Invoice Integration (Computed)

Field Type Description
invoice_entries One2Many Related invoice journal entries
invoices One2Many Related invoices

Additional Fields

Field Type Description
image File Attached image (receipt, proof)
wht_cert_pages Json WHT certificate data (computed)
date_week Char Week string (computed)
date_month Char Month string (computed)

API Methods

1. Create Payment

Method: create(vals, context)

Creates a new payment record with automatic number generation.

Parameters:

vals = {
    "type": "in",                     # Required: 'in' or 'out'
    "pay_type": "direct",             # Required: 'direct' or 'invoice'
    "contact_id": 123,                # Optional: Payer/payee ID
    "date": "2025-01-15",            # Required: Payment date
    "account_id": 10,                 # Required: Bank/cash account
    "currency_id": 1,                 # Required: Currency ID
    "tax_type": "tax_ex",            # Required: 'tax_ex', 'tax_in', 'no_tax'
    "memo": "Customer payment",       # Optional: Memo
    "lines": [                        # Payment lines
        ("create", {
            "type": "direct",
            "description": "Service fee",
            "amount": 1000.00,
            "account_id": 401,
            "tax_id": 1
        })
    ]
}

context = {
    "type": "in"                      # Payment type for sequence
}

Returns: int - New payment ID

Example:

# Create customer payment receipt
payment_id = get_model("account.payment").create({
    "type": "in",
    "pay_type": "direct",
    "contact_id": customer_id,
    "date": "2025-01-15",
    "account_id": bank_account_id,
    "currency_id": currency_id,
    "tax_type": "tax_ex",
    "memo": "Payment for services",
    "direct_lines": [
        ("create", {
            "description": "Consulting services",
            "amount": 5000.00,
            "account_id": revenue_account_id,
            "tax_id": vat_tax_id
        })
    ]
}, context={"type": "in"})


2. Post Payment

Method: post(ids, context)

Posts the payment to accounting by creating a journal entry. This transitions the payment from draft to posted state.

Parameters: - ids (list): Payment IDs to post

Context Options:

context = {
    "job_id": job_id,                 # For progress tracking in batch jobs
    "overpay_description": "text",    # Description for overpayment invoices
    "no_copy": True                   # Skip copying (for repost)
}

Behavior: - Validates payment account exists - Calculates currency conversion rate - Creates journal entry with: - Bank/cash line (payment amount) - Revenue/expense lines (per payment line) - Tax lines (VAT, WHT) - Invoice reconciliation lines - Adjustment lines - Currency gain/loss lines - Posts journal entry - Reconciles with invoice lines - Creates prepayment/overpayment invoices if needed - Updates state to posted

Returns: None

Example:

# Post payment receipt
payment = get_model("account.payment").browse(payment_id)
payment.post()

# Verify posting
payment = payment.browse()[0]  # Refresh
print(f"State: {payment.state}")  # Should be 'posted'
print(f"Journal Entry: {payment.move_id.number}")

Validation Errors: - "Missing account" - No bank/cash account - "Receipts journal not found" - Missing journal - "Disbursements journal not found" - Missing journal - "Missing payment number" - "Missing currency rate" - "Invalid account currency for this payment"


3. Post with Overpayment Check

Method: post_check_overpay(ids, context)

Posts payment after validating no invoice overpayments.

Parameters: - ids (list): Payment IDs to post

Behavior: - Validates invoice payment amounts don't exceed due amounts - Calls post() method - Raises exception if overpayment detected

Example:

# Post with validation
get_model("account.payment").post_check_overpay([payment_id])

Validation Errors: - "Payment amount is over invoice due amount (X > Y, INV-123)"


4. Void Payment

Method: void(ids, context)

Voids (cancels) a payment by reversing the journal entry.

Parameters: - ids (list): Payment IDs to void

Behavior: - Updates state to voided - Deletes prepayment/overpayment invoices - Voids and deletes journal entry - Updates related invoice states

Returns: None

Example:

get_model("account.payment").void([payment_id])


5. Return to Draft

Method: to_draft(ids, context)

Returns payment to draft state by removing journal entry.

Parameters: - ids (list): Payment IDs to reset

Behavior: - Updates state to draft - Deletes prepayment/overpayment invoices - Voids and deletes journal entry - Updates related invoice states

Returns: None

Example:

get_model("account.payment").to_draft([payment_id])


6. Delete Payment

Method: do_delete(ids, context)

Deletes a payment and redirects to bank transactions.

Parameters: - ids (list): Payment IDs to delete

Behavior: - Deletes journal entry if exists - Deletes payment record - Returns navigation to bank transactions

Returns: dict with next and flash

Example:

result = get_model("account.payment").do_delete([payment_id])
# Redirects to bank_transactions view


7. Copy Payment

Method: copy(ids, context)

Creates a copy of a payment with a new number.

Parameters: - ids (list): Source payment IDs

Behavior: - Copies payment header fields - Copies all payment lines - Generates new payment number - Creates new record in draft state

Returns: dict with next and flash

Example:

result = get_model("account.payment").copy([payment_id])
new_id = result["next"]["active_id"]
print(result["flash"])  # "New payment X copied from Y"


8. Create Prepayment Invoice

Method: create_prepay_invoice(ids, context)

Creates a prepayment or overpayment invoice from a payment.

Parameters: - ids (list): Payment IDs

Behavior: - For prepay payments: Creates prepayment invoice - For invoice payments: Creates prepayment invoice from customer deposit lines - Links invoice to payment - Sets invoice state to waiting_payment

Returns: None (creates invoice internally)

Example:

# Automatically called during post() for prepayments
payment = get_model("account.payment").browse(payment_id)
payment.create_prepay_invoice()


9. Delete Credit Invoices

Method: delete_credit_invoices(ids, context)

Deletes associated prepayment/overpayment invoices.

Parameters: - ids (list): Payment IDs

Behavior: - Finds credit invoices linked to payment - Voids each invoice - Deletes each invoice

Returns: None

Example:

# Automatically called during void() or to_draft()
payment.delete_credit_invoices()


10. Post Asynchronously

Method: post_async(ids, context)

Schedules payment posting as a background task.

Parameters: - ids (list): Payment IDs to post

Behavior: - Creates background task record - Posts payment in separate process - Useful for batch posting

Returns: None

Example:

# Post multiple payments in background
get_model("account.payment").post_async(payment_ids)


UI Events (onchange methods)

onchange_account

Triggered when account is selected on payment line. Updates: - Tax from account default - Recalculates amounts - Sets line description if default_line_desc enabled

Usage:

data = {
    "default_line_desc": True,
    "memo": "Service payment",
    "direct_lines": [{
        "account_id": 401
    }]
}
result = get_model("account.payment").onchange_account(
    context={"data": data, "path": "direct_lines.0"}
)


onchange_invoice

Triggered when invoice is selected on payment line. Updates: - Account from invoice - Amount to invoice due amount (with proper sign) - Recalculates totals

Usage:

data = {
    "type": "in",
    "invoice_lines": [{
        "invoice_id": invoice_id
    }]
}
result = get_model("account.payment").onchange_invoice(
    context={"data": data, "path": "invoice_lines.0"}
)


onchange_amount_invoice

Triggered when invoice currency amount changes. Updates: - Payment currency amount (converts)


onchange_amount_payment

Triggered when payment currency amount changes. Updates: - Invoice currency amount (converts)


onchange_contact

Triggered when contact is selected. Updates: - Journal (pay_in_journal or pay_out_journal) - Currency from contact or default - Currency rate


onchange_type

Triggered when payment type changes. Updates: - Payment number (regenerates) - Journal from contact


onchange_date

Triggered when date changes. Updates: - Payment number (regenerates for new date)


onchange_journal

Triggered when journal changes. Updates: - Sequence from journal - Payment number


onchange_payment_account

Triggered when payment account changes. Updates: - Sequence from account - Payment number


onchange_sequence

Triggered when sequence changes. Updates: - Payment number


onchange_pay_type

Triggered when payment subtype changes. Updates: - For invoice payments: Loads outstanding invoices for contact - Populates invoice_lines with due amounts


onchange_employee

Triggered when employee selected (for expense claims). Updates: - Loads expense claims for employee - Populates claim_lines


onchange_product

Triggered when product selected on payment line. Updates: - Description from product - Quantity to 1 - Unit price (sale/purchase price) - Account (sale/purchase account) - Tax (sale/purchase tax) - Recalculates amounts


Search Functions

Search by Date Range

# Find payments in date range
condition = [
    ["date", ">=", "2025-01-01"],
    ["date", "<=", "2025-01-31"]
]

Search by Type

# Find customer receipts
condition = [["type", "=", "in"]]

# Find supplier payments
condition = [["type", "=", "out"]]

Search by State

# Find posted payments
condition = [["state", "=", "posted"]]

Search by Contact

# Find payments for specific contact
condition = [["contact_id", "=", contact_id]]

Search by Amount

# Find large payments
condition = [["amount_total", ">=", 10000]]

Computed Fields Functions

get_amount(ids, context)

Calculates all monetary amounts: - amount_subtotal: Base before tax - amount_tax: Total VAT - amount_total: Total including tax - amount_wht: Withholding tax - amount_wht_base: WHT base amount - amount_payment: Net payment (total - WHT) - amount_adjust: Adjustment total

Handles different line types (direct, invoice, prepay, overpay, claim, adjust) with appropriate tax calculations.

get_amount_total_words(ids, context)

Converts total amount to words for checks and receipts.

get_cheque(ids, context)

Retrieves cheque details from related cheque records.

get_wht_cert_pages(ids, context)

Generates withholding tax certificate data with: - Company details - Supplier details - Tax amounts and percentages - Certificate dates

get_date_agg(ids, context)

Calculates date aggregations (week, month) for reporting.

get_invoice_entries(ids, context)

Returns related invoice journal entry lines through reconciliation.

get_invoices(ids, context)

Returns related invoice IDs through reconciliation.


Best Practices

1. Always Set Context When Creating

# Bad: No context
payment_id = get_model("account.payment").create({
    "type": "in",
    "pay_type": "direct",
    ...
})

# Good: Provide context for proper sequence
payment_id = get_model("account.payment").create({
    "type": "in",
    "pay_type": "direct",
    ...
}, context={"type": "in"})

2. Use Correct Payment Type

# Bad: Wrong combination
vals = {
    "type": "in",           # Received
    "account_id": expense_account  # Expense account (wrong!)
}

# Good: Matching types
vals = {
    "type": "in",           # Received
    "account_id": bank_account,
    "direct_lines": [{
        "account_id": revenue_account  # Revenue account
    }]
}

3. Handle Currency Conversion

# Good: Set currency rate for foreign currency
payment_vals = {
    "type": "in",
    "currency_id": foreign_currency_id,
    "currency_rate": 1.35,  # Explicit rate
    ...
}

4. Validate Before Posting

# Good: Check required fields
payment = get_model("account.payment").browse(payment_id)

if not payment.account_id:
    raise Exception("Payment account required")

if not payment.lines:
    raise Exception("Payment lines required")

for line in payment.lines:
    if line.type == "direct" and not line.account_id:
        raise Exception("Account required on payment line")

payment.post()

5. Use Proper Line Types

# Good: Correct line type usage
vals = {
    "type": "in",
    "pay_type": "direct",
    "direct_lines": [        # Use direct_lines for direct payment
        ("create", {
            "type": "direct",  # Set line type
            "description": "Service fee",
            "amount": 1000.00,
            "account_id": revenue_account_id
        })
    ]
}

# For invoice payments
vals = {
    "type": "in",
    "pay_type": "invoice",
    "invoice_lines": [        # Use invoice_lines for invoice payment
        ("create", {
            "type": "invoice",  # Set line type
            "invoice_id": invoice_id,
            "amount": 5000.00
        })
    ]
}

Database Constraints

Unique Key Constraint

_key = ["company_id", "number"]

Ensures payment numbers are unique within each company.


Model Relationship Description
contact Many2One Payer or payee
account.payment.line One2Many Payment line items
account.move Many2One Journal entry
account.move.line Many2Many Journal entry lines (via move_id)
account.invoice Many2Many Related invoices (via payment lines)
account.account Many2One Bank/cash account
account.journal Many2One Accounting journal
currency Many2One Transaction currency
account.cheque One2Many Related cheques
expense.claim Many2Many Related expense claims (via payment lines)
document One2Many Attachments
message One2Many Comments
company Many2One Owning company
base.user Many2One Owner
payment.method Many2One Payment method
payment.category Many2One Payment category
hr.employee Many2One Employee (for claims)
landed.cost One2Many Landed costs

Common Use Cases

Use Case 1: Receive Customer Payment (Invoice Payment)

# 1. Create payment receipt for invoice
payment_id = get_model("account.payment").create({
    "type": "in",
    "pay_type": "invoice",
    "contact_id": customer_id,
    "date": "2025-01-15",
    "account_id": bank_account_id,
    "currency_id": currency_id,
    "tax_type": "tax_ex",
    "memo": "Payment received for INV-2025-001",
    "invoice_lines": [
        ("create", {
            "invoice_id": invoice_id,
            "amount": 5000.00  # Full invoice amount
        })
    ]
}, context={"type": "in"})

# 2. Post payment
payment = get_model("account.payment").browse(payment_id)
payment.post()

# 3. Verify invoice is paid
invoice = get_model("account.invoice").browse(invoice_id)
print(f"Invoice state: {invoice.state}")  # Should be 'paid'
print(f"Amount due: {invoice.amount_due}")  # Should be 0

Use Case 2: Make Supplier Payment (Invoice Payment)

# 1. Create supplier payment
payment_id = get_model("account.payment").create({
    "type": "out",
    "pay_type": "invoice",
    "contact_id": supplier_id,
    "date": "2025-01-15",
    "account_id": bank_account_id,
    "currency_id": currency_id,
    "tax_type": "tax_ex",
    "memo": "Payment for supplier invoices",
    "invoice_lines": [
        ("create", {
            "invoice_id": invoice1_id,
            "amount": 3000.00
        }),
        ("create", {
            "invoice_id": invoice2_id,
            "amount": 2000.00
        })
    ]
}, context={"type": "out"})

# 2. Post payment
get_model("account.payment").post([payment_id])

# 3. Check payment details
payment = get_model("account.payment").browse(payment_id)
print(f"Total paid: {payment.amount_payment}")
print(f"Journal entry: {payment.move_id.number}")

Use Case 3: Direct Payment (No Invoice)

# 1. Create direct expense payment
payment_id = get_model("account.payment").create({
    "type": "out",
    "pay_type": "direct",
    "contact_id": vendor_id,
    "date": "2025-01-15",
    "account_id": bank_account_id,
    "currency_id": currency_id,
    "tax_type": "tax_ex",
    "memo": "Office supplies purchase",
    "direct_lines": [
        ("create", {
            "description": "Office supplies",
            "amount": 500.00,
            "account_id": office_expense_account_id,
            "tax_id": vat_tax_id
        }),
        ("create", {
            "description": "Delivery fee",
            "amount": 50.00,
            "account_id": delivery_expense_account_id,
            "tax_id": None
        })
    ]
}, context={"type": "out"})

# 2. Post payment
get_model("account.payment").post([payment_id])

Use Case 4: Customer Payment with Withholding Tax

# 1. Create payment with WHT
payment_id = get_model("account.payment").create({
    "type": "in",
    "pay_type": "invoice",
    "contact_id": customer_id,
    "date": "2025-01-15",
    "account_id": bank_account_id,
    "currency_id": currency_id,
    "tax_type": "tax_ex",
    "memo": "Payment with 3% WHT",
    "invoice_lines": [
        ("create", {
            "invoice_id": invoice_id,
            "amount": 10000.00  # Invoice total
        })
    ]
}, context={"type": "in"})

# 2. Post payment (WHT calculated automatically)
payment = get_model("account.payment").browse(payment_id)
payment.post()

# 3. Check WHT calculation
print(f"Invoice amount: {payment.amount_total}")
print(f"WHT amount: {payment.amount_wht}")
print(f"Net payment: {payment.amount_payment}")

Use Case 5: Prepayment from Customer

# 1. Create prepayment
payment_id = get_model("account.payment").create({
    "type": "in",
    "pay_type": "prepay",
    "contact_id": customer_id,
    "date": "2025-01-15",
    "account_id": bank_account_id,
    "currency_id": currency_id,
    "tax_type": "tax_ex",
    "memo": "Prepayment for future order",
    "prepay_lines": [
        ("create", {
            "description": "Prepayment",
            "amount": 5000.00,
            "account_id": customer_deposit_account_id
        })
    ]
}, context={"type": "in"})

# 2. Post payment (creates prepayment invoice automatically)
payment = get_model("account.payment").browse(payment_id)
payment.post()

# 3. Check created prepayment invoice
prepay_inv = payment.credit_invoices[0] if payment.credit_invoices else None
if prepay_inv:
    print(f"Prepayment invoice: {prepay_inv.number}")
    print(f"Available credit: {prepay_inv.amount_credit_remain}")

Use Case 6: Partial Invoice Payment with Currency Conversion

# 1. Create partial payment in foreign currency
payment_id = get_model("account.payment").create({
    "type": "out",
    "pay_type": "invoice",
    "contact_id": supplier_id,
    "date": "2025-01-15",
    "account_id": bank_account_id,
    "currency_id": usd_currency_id,
    "currency_rate": 1.35,  # USD to base currency
    "tax_type": "tax_ex",
    "memo": "Partial payment in USD",
    "invoice_lines": [
        ("create", {
            "invoice_id": invoice_id,  # Invoice in supplier currency
            "amount_invoice": 3000.00,  # Amount in invoice currency
            "amount": 2222.22           # Amount in payment currency (USD)
        })
    ]
}, context={"type": "out"})

# 2. Post payment
get_model("account.payment").post([payment_id])

# 3. Check remaining balance
invoice = get_model("account.invoice").browse(invoice_id)
print(f"Original amount: {invoice.amount_total}")
print(f"Amount paid: {invoice.amount_paid}")
print(f"Amount due: {invoice.amount_due}")

Performance Tips

1. Batch Process Payments

# Good: Use async posting for batches
get_model("account.payment").post_async(payment_ids)

2. Use Stored Computed Fields

Amount fields are stored, so queries are fast:

# Good: Query stored fields
payments = get_model("account.payment").search_browse([
    ["amount_payment", ">", 1000],
    ["date", ">=", "2025-01-01"]
])

# Avoid loading all payment lines if not needed
payment = get_model("account.payment").browse(payment_id)
total = payment.amount_payment  # Just get computed field
# Don't iterate lines if not necessary

Troubleshooting

"Missing account"

Cause: Payment account_id not set
Solution: Ensure bank/cash account is specified before posting

"Receipts journal not found" / "Disbursements journal not found"

Cause: Missing journals in settings
Solution: Configure journals: Settings → Accounting → Journals

"Missing currency rate for [currency]"

Cause: No exchange rate for payment date
Solution: Add currency rate: Settings → Currencies → [Currency] → Add Rate

"Invalid account currency for this payment"

Cause: Payment account currency doesn't match payment currency
Solution: Use bank account with correct currency or configure multi-currency account

"Can't delete posted payments"

Cause: Attempting to delete posted payment
Solution: Use void() or to_draft() first, then delete

"Payment amount is over invoice due amount"

Cause: Payment exceeds invoice balance (when using post_check_overpay)
Solution: Adjust payment amount or use regular post() to allow overpayment

"Missing currency gain account" / "Missing currency loss account"

Cause: Rounding differences in foreign currency
Solution: Configure accounts: Settings → Accounting → Currency Gain/Loss Accounts

"Missing journal entry for invoice"

Cause: Trying to pay unposted invoice
Solution: Post invoice before creating payment


Testing Examples

Unit Test: Create and Post Direct Payment

def test_create_and_post_payment():
    # Setup
    bank_account_id = 101  # Bank account
    revenue_account_id = 401  # Revenue account

    # Create payment
    payment_id = get_model("account.payment").create({
        "type": "in",
        "pay_type": "direct",
        "contact_id": customer_id,
        "date": "2025-01-15",
        "account_id": bank_account_id,
        "currency_id": 1,
        "tax_type": "no_tax",
        "memo": "Test payment",
        "direct_lines": [
            ("create", {
                "description": "Service fee",
                "amount": 1000.00,
                "account_id": revenue_account_id
            })
        ]
    }, context={"type": "in"})

    # Verify creation
    payment = get_model("account.payment").browse(payment_id)
    assert payment.state == "draft"
    assert payment.amount_total == 1000.00

    # Post payment
    payment.post()
    payment = payment.browse()[0]  # Refresh

    # Verify posting
    assert payment.state == "posted"
    assert payment.move_id is not None
    assert len(payment.move_id.lines) == 2  # Bank + Revenue

    # Cleanup
    payment.void()
    payment.delete()

Unit Test: Invoice Payment Reconciliation

def test_invoice_payment_reconciliation():
    # Create and post invoice
    invoice_id = get_model("account.invoice").create({
        "type": "out",
        "inv_type": "invoice",
        "contact_id": customer_id,
        "date": "2025-01-10",
        "currency_id": 1,
        "tax_type": "no_tax",
        "lines": [("create", {
            "description": "Service",
            "qty": 1,
            "unit_price": 1000.00,
            "account_id": 401
        })]
    })
    get_model("account.invoice").post([invoice_id])

    # Create payment
    payment_id = get_model("account.payment").create({
        "type": "in",
        "pay_type": "invoice",
        "contact_id": customer_id,
        "date": "2025-01-15",
        "account_id": 101,
        "currency_id": 1,
        "tax_type": "no_tax",
        "invoice_lines": [("create", {
            "invoice_id": invoice_id,
            "amount": 1000.00
        })]
    })

    # Post payment
    get_model("account.payment").post([payment_id])

    # Verify invoice is paid
    invoice = get_model("account.invoice").browse(invoice_id)
    assert invoice.state == "paid"
    assert invoice.amount_due == 0

    # Verify reconciliation
    payment = get_model("account.payment").browse(payment_id)
    assert payment.move_id is not None
    # Check reconciliation exists
    reconciled = False
    for line in payment.move_id.lines:
        if line.reconcile_id:
            reconciled = True
            break
    assert reconciled

    # Cleanup
    payment.void()
    invoice.void()

Security Considerations

Permission Model

  • payment_view: View payments
  • payment_create: Create payments
  • payment_edit: Edit draft payments
  • payment_delete: Delete draft payments
  • payment_post: Post payments
  • payment_void: Void payments

Data Access

  • Multi-company: Users only see payments for their company
  • Audit log: All changes tracked
  • Field-level security: Sensitive fields restricted
  • State-based permissions: Actions restricted by state

Best Practices

  • Validate user permissions before operations
  • Use company context for multi-company
  • Log financial operations
  • Validate amounts server-side
  • Prevent SQL injection via ORM

Configuration Settings

Required Settings

Setting Location Description
currency_id Settings Base currency
pay_in_journal_id Settings Receipts journal
pay_out_journal_id Settings Disbursements journal

Optional Settings

Setting Default Description
currency_gain_id None Currency gain account
currency_loss_id None Currency loss account

Sequence Settings

Configure sequences for payment numbering: - Receipts (in): pay_in - Disbursements (out): pay_out - WHT Certificate: wht_no


Integration Points

Internal Modules

  • Invoices: Allocates payments against invoices
  • Bank Reconciliation: Links to bank statement lines
  • Expenses: Pays expense claims
  • Cheques: Manages cheque payments
  • Tax: Handles VAT and WHT
  • Reporting: Source for cash flow reports

Version History

Last Updated: November 7, 2025
Model File: netforce_account/models/account_payment.py
Framework: Netforce
Document Version: 1.0.0
Complexity: ⭐⭐⭐⭐ (High - Core cash management)


Additional Resources


Support & Feedback

For issues or questions about this module: 1. Check related model documentation (payment.line, invoice, move) 2. Review system logs for detailed error messages 3. Verify accounts, journals, and currencies configured 4. Test in development environment first 5. Validate amounts and currency rates


This documentation is generated for developer onboarding and reference purposes.