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:
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¶
| 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:
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:
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:
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:
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:
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:
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¶
Search by Contact¶
Search by Amount¶
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¶
Ensures payment numbers are unique within each company.
Related Models¶
| 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¶
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"]
])
3. Limit Related Field Loading¶
# 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 paymentspayment_create: Create paymentspayment_edit: Edit draft paymentspayment_delete: Delete draft paymentspayment_post: Post paymentspayment_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¶
- Payment Line Documentation:
account.payment.line - Invoice Documentation:
account.invoice - Journal Entry Documentation:
account.move - Bank Reconciliation Documentation:
account.statement
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.