Invoice Model (account.invoice)¶
Overview¶
The Invoice model (account.invoice) is the central document for managing both customer (accounts receivable) and supplier (accounts payable) invoices in Netforce. It handles invoice creation, validation, approval workflows, payment tracking, tax calculations, and financial posting to journal entries. This model is a core component of the accounting system and integrates deeply with contacts, products, payments, taxes, stock movements, and sales/purchase orders.
Model Information¶
Model Name: account.invoice
Display Name: Invoice
Key Fields: company_id, number
Source File: netforce_account/models/account_invoice.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.invoice model, the key fields are:
This means the combination of these fields must be unique:
- company_id - The company owning this invoice
- number - The invoice 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, INV-2025-001 ✅ Valid
Company A, INV-2025-002 ✅ Valid
Company B, INV-2025-001 ✅ Valid (different company)
# This would fail - duplicate key:
Company A, INV-2025-001 ❌ ERROR: Invoice number already exists!
Database Implementation¶
The key fields are enforced at the database level using a unique constraint:
This ensures invoice numbers are unique per company across the entire database.
Invoice Types and Subtypes¶
The invoice model supports multiple types and subtypes to handle different business scenarios:
Primary Type (type)¶
| Type | Code | Description |
|---|---|---|
| Receivable | out |
Customer invoice (sales) - increases accounts receivable |
| Payable | in |
Supplier invoice (purchases) - increases accounts payable |
Subtype (inv_type)¶
| Subtype | Code | Description |
|---|---|---|
| Invoice | invoice |
Standard invoice for goods/services |
| Credit Note | credit |
Reduces amounts owed (returns, adjustments) |
| Debit Note | debit |
Increases amounts owed (additional charges) |
State Workflow¶
| State | Description | Allowed Transitions |
|---|---|---|
draft |
Initial state - editable | → waiting_approval, → voided |
waiting_approval |
Submitted for approval | → approved, → voided, → draft |
approved |
Approved but not yet posted | → waiting_payment |
waiting_payment |
Posted to journal, awaiting payment | → paid, → voided |
paid |
Fully paid/credited | None (final state) |
voided |
Cancelled | → draft (with restrictions) |
repeat |
Template for recurring invoices | None |
Key Fields Reference¶
Header Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
number |
Char | ✅ | Invoice number (auto-generated from sequence) |
type |
Selection | ✅ | Invoice type: out (receivable) or in (payable) |
inv_type |
Selection | ✅ | Invoice subtype: invoice, credit, or debit |
contact_id |
Many2One | ✅ | Customer or supplier contact |
date |
Date | ✅ | Invoice date |
due_date |
Date | ❌ | Payment due date |
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 (computed) |
company_id |
Many2One | ❌ | Owning company |
user_id |
Many2One | ❌ | Invoice owner/creator |
Reference Fields¶
| Field | Type | Description |
|---|---|---|
ref |
Char | External reference number |
memo |
Char | Internal memo/notes |
sup_inv_number |
Char | Supplier's invoice number (for payables) |
tax_no |
Char | Tax invoice number |
tax_date |
Date | Tax invoice date |
tax_branch_no |
Char | Tax branch number |
transaction_no |
Char | Online payment transaction ID |
print_form_no |
Char | Printed invoice form number |
Amount Fields (Computed)¶
| Field | Type | Description |
|---|---|---|
amount_subtotal |
Decimal | Subtotal before tax |
amount_tax |
Decimal | Total tax amount |
amount_total |
Decimal | Total amount including tax |
amount_paid |
Decimal | Amount already paid |
amount_due |
Decimal | Amount still due |
amount_rounding |
Decimal | Rounding adjustment |
amount_wht |
Decimal | Withholding tax amount |
amount_total_net |
Decimal | Net total after WHT |
amount_due_net |
Decimal | Net due after WHT |
amount_discount |
Decimal | Total discount amount |
amount_subtotal_no_discount |
Decimal | Subtotal before discount |
Currency Converted Amounts¶
| Field | Type | Description |
|---|---|---|
amount_total_cur |
Decimal | Total in base currency |
amount_due_cur |
Decimal | Due amount in base currency |
amount_paid_cur |
Decimal | Paid amount in base currency |
amount_credit_remain_cur |
Decimal | Remaining credit in base currency |
Configuration Fields¶
| Field | Type | Description |
|---|---|---|
account_id |
Many2One | Receivable/payable account |
journal_id |
Many2One | Accounting journal |
sequence_id |
Many2One | Number sequence |
pay_method_id |
Many2One | Payment method |
pay_term_id |
Many2One | Payment terms |
bill_address_id |
Many2One | Billing address |
Relationship Fields¶
| Field | Type | Description |
|---|---|---|
lines |
One2Many | Invoice lines (account.invoice.line) |
taxes |
One2Many | Tax breakdown (account.invoice.tax) |
payments |
One2Many | Payment entries (deprecated) |
payment_entries |
One2Many | Payment move lines (computed) |
move_id |
Many2One | Journal entry |
tax_date_move_id |
Many2One | Tax date adjustment entry |
payment_id |
Many2One | Related payment |
related_id |
Reference | Source document (sale, purchase, etc.) |
orig_invoice_id |
Many2One | Original invoice (for credit/debit notes) |
template_id |
Many2One | Template for recurring invoices |
Integration Fields¶
| Field | Type | Description |
|---|---|---|
fixed_assets |
One2Many | Created fixed assets |
stock_moves |
One2Many | Related stock movements |
pickings |
Many2Many | Related stock pickings (computed) |
time_entries |
One2Many | Billable time entries |
documents |
One2Many | Attached documents |
comments |
One2Many | Comments/messages |
Shipping & Delivery¶
| Field | Type | Description |
|---|---|---|
ship_term_id |
Many2One | Shipping terms |
ship_port_id |
Many2One | Shipping port |
ship_track_no |
Char | Shipping tracking number |
total_net_weight |
Decimal | Total net weight (computed) |
total_gross_weight |
Decimal | Total gross weight (computed) |
delivery_term_id |
Many2One | Delivery terms |
packaging_id |
Many2One | Packaging type |
Recurring Invoice Fields¶
| Field | Type | Description |
|---|---|---|
interval_num |
Integer | Interval number for recurring |
interval_unit |
Selection | Interval unit: day, month, year |
next_date |
Date | Next invoice date |
next_due_date |
Date | Next due date |
invoices |
One2Many | Generated invoices from template |
Additional Fields¶
| Field | Type | Description |
|---|---|---|
seller_id |
Many2One | Salesperson |
sale_categ_id |
Many2One | Sales category |
brand_id |
Many2One | Product brand |
procurement_employee_id |
Many2One | Procurement person |
approve_user_id |
Many2One | User who approved |
bill_note_id |
Many2One | Billing note |
freight_charges |
Decimal | Freight/shipping charges |
amount_change |
Decimal | Change amount (cash payments) |
remarks |
Text | Additional remarks |
instructions |
Text | Payment/tax instructions (computed) |
Aggregate/SQL Fields¶
| Field | Type | Description |
|---|---|---|
agg_amount_total |
Decimal | Aggregated total (for reporting) |
agg_amount_subtotal |
Decimal | Aggregated subtotal (for reporting) |
year |
Char | Year from date (SQL function) |
quarter |
Char | Quarter from date (SQL function) |
month |
Char | Month from date (SQL function) |
week |
Char | Week from date (SQL function) |
date_week |
Char | Week string (computed) |
date_month |
Char | Month string (computed) |
API Methods¶
1. Create Invoice¶
Method: create(vals, context)
Creates a new invoice record with automatic number generation and line items.
Parameters:
vals = {
"type": "out", # Required: 'out' or 'in'
"inv_type": "invoice", # Required: 'invoice', 'credit', 'debit'
"contact_id": 123, # Required: Customer/supplier ID
"date": "2025-01-15", # Required: Invoice date
"due_date": "2025-02-15", # Optional: Payment due date
"currency_id": 1, # Required: Currency ID
"tax_type": "tax_ex", # Required: 'tax_ex', 'tax_in', 'no_tax'
"ref": "PO-12345", # Optional: Reference number
"memo": "Q1 Services", # Optional: Memo
"lines": [ # Invoice lines
("create", {
"product_id": 456,
"description": "Consulting",
"qty": 10,
"unit_price": 150.00,
"tax_id": 1,
"account_id": 401
})
]
}
context = {
"type": "out", # Invoice type
"inv_type": "invoice", # Invoice subtype
"date": "2025-01-15" # Date for sequence
}
Returns: int - New invoice ID
Example:
# Create customer invoice
invoice_id = get_model("account.invoice").create({
"type": "out",
"inv_type": "invoice",
"contact_id": customer_id,
"date": "2025-01-15",
"due_date": "2025-02-15",
"currency_id": currency_id,
"tax_type": "tax_ex",
"lines": [
("create", {
"product_id": product_id,
"description": "Web Development Services",
"qty": 40,
"unit_price": 125.00,
"tax_id": vat_tax_id,
"account_id": revenue_account_id
})
]
}, context={"type": "out", "inv_type": "invoice"})
2. Post Invoice¶
Method: post(ids, context)
Posts the invoice to accounting by creating a journal entry. This transitions the invoice from draft/approved to waiting_payment state.
Parameters:
- ids (list): Invoice IDs to post
Behavior:
- Validates invoice data (amounts, lines, etc.)
- Sets payment terms if configured
- Calculates and validates tax amounts
- Determines receivable/payable account from contact or settings
- Creates journal entry with:
- Receivable/payable line (total amount)
- Revenue/expense lines (per invoice line)
- Tax lines (per tax component)
- Freight charges (if applicable)
- Groups and consolidates journal lines
- Handles currency conversion
- Updates state to waiting_payment
- Triggers workflow events
Returns: None
Example:
# Post customer invoice
invoice = get_model("account.invoice").browse(invoice_id)
invoice.post()
# Verify posting
invoice = invoice.browse()[0] # Refresh
print(f"State: {invoice.state}") # Should be 'waiting_payment'
print(f"Journal Entry: {invoice.move_id.number}")
Validation Errors: - "Invoice total is negative" - Amount < 0 - "Account receivable not found" - Missing AR account - "Account payable not found" - Missing AP account - "Missing currency rate" - No exchange rate - "Missing account for invoice line" - Line without account
3. Approve Invoice¶
Method: approve(ids, context)
Approves the invoice and posts it to accounting. Transitions from draft/waiting_approval to waiting_payment.
Parameters:
- ids (list): Invoice IDs to approve
Behavior:
- Validates state (must be draft or waiting_approval)
- Sets active company context
- Calls post() method
- Records approving user
- Creates fixed assets for supplier invoices (if applicable)
- Returns flash message
Returns: dict with flash message
Example:
# Approve invoice
result = get_model("account.invoice").approve([invoice_id])
print(result["flash"]) # "Invoice approved."
# Check approval
invoice = get_model("account.invoice").browse(invoice_id)
print(f"Approved by: {invoice.approve_user_id.name}")
Permission Requirements: - User must have invoice approval permission - Invoice must be in valid state
4. Submit for Approval¶
Method: submit_for_approval(ids, context)
Submits draft invoice for approval. Transitions state to waiting_approval.
Parameters:
- ids (list): Invoice IDs to submit
Behavior: - Validates state (must be draft) - Updates state to waiting_approval - Triggers workflow event
Returns: dict with flash message
Example:
result = get_model("account.invoice").submit_for_approval([invoice_id])
print(result["flash"]) # "Invoice submitted for approval."
5. Void Invoice¶
Method: void(ids, context)
Voids (cancels) an invoice by reversing the journal entry.
Parameters:
- ids (list): Invoice IDs to void
Behavior: - Validates state (draft, waiting_payment, or paid) - Checks for fixed assets (cannot void if FA created) - Checks for payment entries (cannot void if payments exist) - Voids and deletes journal entry - Updates state to voided
Returns: None
Example:
Validation Errors: - "Invalid invoice state" - Cannot void from current state - "Can't void invoice because there are still related payment entries" - "This invoice involves Fixed Assets" - Cannot void
6. Return to Draft¶
Method: to_draft(ids, context)
Returns invoice to draft state by removing journal entry.
Parameters:
- ids (list): Invoice IDs to reset
Behavior: - Validates no payment entries exist - Checks for fixed assets - Voids and deletes journal entry - Removes reconciliation - Deletes tax calculations - Updates state to draft - Clears account_id
Returns: None
Example:
7. Calculate Taxes¶
Method: calc_taxes(ids, context)
Calculates and creates tax breakdown records for the invoice.
Parameters:
- ids (list): Invoice IDs to calculate taxes for
Behavior:
- Deletes existing tax records
- Determines currency rate
- Loops through invoice lines
- Calculates base amounts and tax components
- Groups taxes by component
- Creates account.invoice.tax records
- Generates tax invoice numbers (for output invoices)
Returns: None
Example:
invoice = get_model("account.invoice").browse(invoice_id)
invoice.calc_taxes()
# View tax breakdown
for tax in invoice.taxes:
print(f"{tax.tax_comp_id.name}: {tax.tax_amount}")
8. Copy Invoice¶
Method: copy(ids, vals, context)
Creates a copy of an invoice with optional value overrides.
Parameters:
- ids (list): Source invoice IDs
- vals (dict): Optional fields to override
- context (dict): Context parameters
Returns: dict with next, flash, and new_id
Example:
# Copy invoice with new date
result = get_model("account.invoice").copy([invoice_id], {
"date": "2025-02-01",
"number": None # Will generate new number
})
new_id = result["new_id"]
print(result["flash"]) # "Invoice INV-001 copied to INV-002"
9. Create Credit Note from Invoice¶
Method: copy_to_credit_note(ids, context)
Creates a credit note based on an existing invoice.
Parameters:
- ids (list): Source invoice IDs
Behavior: - Copies invoice details - Sets inv_type to "credit" - Sets reference to original invoice number - Links to original invoice - Copies all lines with same amounts - Creates new record
Returns: dict with next, flash, and invoice_id
Example:
result = get_model("account.invoice").copy_to_credit_note([invoice_id])
credit_note_id = result["invoice_id"]
print(result["flash"]) # "Credit note CN-001 created from invoice INV-001"
10. Create Debit Note from Invoice¶
Method: copy_to_debit_note(ids, context)
Creates a debit note based on an existing invoice (with zero amounts).
Parameters:
- ids (list): Source invoice IDs
Behavior: - Similar to credit note creation - Sets inv_type to "debit" - Sets line unit_price and amount to 0 - User must fill in amounts manually
Returns: dict with next, flash, and invoice_id
Example:
result = get_model("account.invoice").copy_to_debit_note([invoice_id])
debit_note_id = result["invoice_id"]
11. Create Goods Issue (Delivery)¶
Method: copy_to_pick_out(ids, context)
Creates a stock picking (goods issue) from a customer invoice.
Parameters:
- ids (list): Invoice IDs
Behavior: - Creates picking with type "out" - Copies product lines (stock/consumable/bundle only) - Sets locations (warehouse → customer) - Links to invoice - Skips non-stock products and zero quantities
Returns: dict with next and flash
Example:
result = get_model("account.invoice").copy_to_pick_out([invoice_id])
# Redirects to goods issue form
Validation Errors: - "Nothing left to deliver" - No stock items - "Customer location not found" - "Warehouse not found"
12. Create Goods Receipt¶
Method: copy_to_pick_in(ids, context)
Creates a stock picking (goods receipt) from a supplier invoice.
Parameters:
- ids (list): Invoice IDs
Behavior: - Creates picking with type "in" - Copies product lines with cost prices - Calculates currency-converted costs - Sets locations (supplier → warehouse) - Links to invoice
Returns: dict with next and flash
Example:
result = get_model("account.invoice").copy_to_pick_in([invoice_id])
# Redirects to goods receipt form
13. Create Sales Order from Invoice¶
Method: copy_to_sale(ids, context)
Creates a sales order from an invoice.
Parameters:
- ids (list): Invoice IDs
Returns: dict with alert and next
Example:
14. Post Tax Date Adjustment¶
Method: post_tax_date(ids, context)
Creates a journal entry to adjust tax date for already-posted invoices.
Parameters:
- ids (list): Invoice IDs
Behavior: - Validates tax_date field exists - Creates adjustment journal entry - Moves tax from pending to final accounts - Updates tax records with new date
Returns: None
Example:
invoice = get_model("account.invoice").browse(invoice_id)
invoice.write({"tax_date": "2025-02-01"})
invoice.post_tax_date()
15. Create Recurring Invoices¶
Method: create_next_invoice(ids, context)
Generates the next invoice from a recurring template.
Parameters:
- ids (list): Template invoice IDs (state='repeat')
Behavior: - Validates state is "repeat" - Checks if next_date has arrived - Copies template to new invoice - Updates next_date based on interval - Updates next_due_date if set
Returns: dict with alert and invoice_id
Example:
# Set up recurring invoice
template_id = get_model("account.invoice").create({
"type": "out",
"inv_type": "invoice",
"contact_id": customer_id,
"date": "2025-01-01",
"state": "repeat",
"interval_num": 1,
"interval_unit": "month",
"next_date": "2025-02-01",
"lines": [...]
})
# Generate next invoice
result = get_model("account.invoice").create_next_invoice([template_id])
16. Create Recurring Invoices (Batch)¶
Method: create_repeating_all(email_template, context)
Generates all due recurring invoices and optionally emails them.
Parameters:
- email_template (str): Optional email template name
Behavior: - Searches for templates with next_date <= today - Generates invoices - Optionally creates and sends emails
Example:
# Generate and email all due recurring invoices
get_model("account.invoice").create_repeating_all(
email_template="monthly_invoice"
)
17. Add Missing Accounts¶
Method: add_missing_accounts(ids, context)
Automatically fills in missing accounts and taxes on invoice lines based on products.
Parameters:
- ids (list): Invoice IDs
Behavior: - Loops through lines - Skips if account already set - Gets account from product (sale/purchase) - Gets tax from product - Updates line
Example:
18. Create Fixed Assets¶
Method: create_fixed_assets(ids, context)
Creates fixed asset records from supplier invoice lines.
Parameters:
- ids (list): Invoice IDs
Behavior: - Checks if assets already created - Loops through invoice lines - Creates asset if account type is "fixed_asset" - Sets purchase date, price, depreciation settings
Example:
19. Sync to Xero¶
Method: sync_to_xero(ids, context)
Synchronizes invoices to Xero accounting system.
Parameters:
- ids (list): Invoice IDs
Behavior: - Validates Xero configuration - Validates contact has Xero ID - Converts invoice to Xero format - Sends via API - Returns success/error messages
Returns: dict with alert
Example:
UI Events (onchange methods)¶
onchange_product¶
Triggered when a product is selected on an invoice line. Updates: - Description from product - Quantity to 1 - UOM from product - Unit price (sale_price or purchase_price) - Account (sale_account or purchase_account) - Tax (sale_tax or purchase_tax) - Recalculates amounts
Usage:
data = {
"type": "out",
"lines": [{
"product_id": 123
}]
}
result = get_model("account.invoice").onchange_product(
context={"data": data, "path": "lines.0"}
)
onchange_contact¶
Triggered when contact is selected. Updates: - Billing address - Journal (sale/purchase) - Currency from contact or default - Payment terms - Due date (if payment terms set)
Usage:
data = {
"type": "out",
"contact_id": customer_id
}
result = get_model("account.invoice").onchange_contact(
context={"data": data}
)
onchange_account¶
Triggered when account is selected on invoice line. Updates: - Tax from account default - Recalculates amounts
onchange_amount¶
Triggered when amount is manually changed on invoice line. Updates: - Unit price (amount / qty) - Recalculates totals
onchange_date¶
Triggered when invoice date changes. Updates: - Invoice number (regenerates) - Due date (if payment terms set)
onchange_journal¶
Triggered when journal is selected. Updates: - Sequence from journal - Regenerates invoice number
onchange_sequence¶
Triggered when sequence is selected. Updates: - Invoice number
onchange_pay_term¶
Triggered when payment terms selected. Updates: - Due date based on terms
Search Functions¶
Search by Product¶
Search by Contact Category¶
Search by Date Range¶
# Find invoices in date range
condition = [
["date", ">=", "2025-01-01"],
["date", "<=", "2025-01-31"]
]
Search by State¶
Search by Type¶
# Find customer invoices
condition = [["type", "=", "out"]]
# Find supplier credit notes
condition = [
["type", "=", "in"],
["inv_type", "=", "credit"]
]
Computed Fields Functions¶
get_amount(ids, context)¶
Calculates all monetary amounts: - amount_subtotal: Base amount before tax - amount_tax: Total tax - amount_total: Total including tax - amount_paid: Amount received/paid - amount_due: Remaining balance - amount_wht: Withholding tax - Currency-converted amounts
get_state(ids, context)¶
Determines current workflow state based on payment status: - Updates waiting_payment → paid when fully paid - Updates paid → waiting_payment when partially refunded - Handles credit note allocation
get_qty_total(ids, context)¶
Calculates total quantity across all lines
get_discount(ids, context)¶
Calculates total discount amounts and subtotal before discount
get_payment_entries(ids, context)¶
Returns list of payment move line IDs related to this invoice
get_contact_credit(ids, context)¶
Retrieves available credit for contact
get_pickings(ids, context)¶
Returns related stock picking IDs
get_sale(ids, context)¶
Returns related sales order ID
get_picking(ids, context)¶
Returns primary related stock picking ID
get_time_total(ids, context)¶
Calculates total billable hours and amounts from time entries
Workflow Integration¶
Trigger Events¶
The invoice model fires workflow triggers:
self.trigger(ids, "create") # When invoice created
self.trigger(ids, "submit_for_approval") # When submitted
These can be configured in workflow automation to: - Send email notifications - Create tasks - Update related records - Trigger external systems
Best Practices¶
1. Always Set Context When Creating¶
# Bad: No context
invoice_id = get_model("account.invoice").create({
"type": "out",
"inv_type": "invoice",
...
})
# Good: Provide context for proper sequence
invoice_id = get_model("account.invoice").create({
"type": "out",
"inv_type": "invoice",
...
}, context={
"type": "out",
"inv_type": "invoice",
"date": "2025-01-15"
})
2. Use Transactions for Multi-Step Operations¶
# Good: Wrap in transaction
from netforce import database
db = database.get_connection()
try:
# Create invoice
invoice_id = get_model("account.invoice").create({...})
# Post invoice
get_model("account.invoice").post([invoice_id])
# Create payment
payment_id = get_model("account.payment").create({...})
db.commit()
except Exception as e:
db.rollback()
raise
3. Validate Before Posting¶
# Good: Check required fields
invoice = get_model("account.invoice").browse(invoice_id)
if not invoice.due_date:
raise Exception("Due date required before posting")
if not invoice.lines:
raise Exception("Cannot post invoice without lines")
for line in invoice.lines:
if not line.account_id:
raise Exception(f"Missing account on line: {line.description}")
invoice.post()
4. Handle Currency Conversion¶
# Good: Check currency and rate
invoice = get_model("account.invoice").browse(invoice_id)
settings = get_model("settings").browse(1)
if invoice.currency_id.id != settings.currency_id.id:
if not invoice.currency_rate:
raise Exception("Currency rate required for foreign currency")
5. Use Related Documents¶
# Good: Link to source document
vals = {
"type": "out",
"inv_type": "invoice",
"related_id": f"sale.order,{sale_order_id}",
...
}
# Lines also linked
lines = [
("create", {
"product_id": product_id,
"related_id": f"sale.order,{sale_order_id}",
...
})
]
Database Constraints¶
Unique Key Constraint¶
Ensures invoice numbers are unique within each company.
Check Constraints¶
Custom validation method that checks: - Due date is present and after invoice date - Lines exist (for most invoice types)
Related Models¶
| Model | Relationship | Description |
|---|---|---|
contact |
Many2One | Customer or supplier |
account.invoice.line |
One2Many | Invoice line items |
account.invoice.tax |
One2Many | Tax breakdown |
account.move |
Many2One | Journal entry |
account.move.line |
One2Many | Journal entry lines (payments) |
account.payment |
Many2One | Direct payment |
account.payment.line |
One2Many | Payment allocations (deprecated) |
currency |
Many2One | Transaction currency |
account.account |
Many2One | Receivable/payable account |
account.journal |
Many2One | Accounting journal |
payment.method |
Many2One | Payment method |
payment.term |
Many2One | Payment terms |
sale.order |
Reference | Source sales order |
purchase.order |
Reference | Source purchase order |
stock.picking |
Many2Many/Reference | Related deliveries/receipts |
stock.move |
One2Many | Stock movements |
account.fixed.asset |
One2Many | Created fixed assets |
time.entry |
One2Many | Billable time entries |
document |
One2Many | Attachments |
message |
One2Many | Comments |
company |
Many2One | Owning company |
base.user |
Many2One | Owner/approver |
Common Use Cases¶
Use Case 1: Create and Post Customer Invoice¶
# 1. Create invoice with lines
invoice_id = get_model("account.invoice").create({
"type": "out",
"inv_type": "invoice",
"contact_id": customer_id,
"date": "2025-01-15",
"due_date": "2025-02-15",
"currency_id": currency_id,
"tax_type": "tax_ex",
"ref": "SO-12345",
"memo": "Web development project",
"lines": [
("create", {
"product_id": service_product_id,
"description": "Web Development - 40 hours",
"qty": 40,
"unit_price": 125.00,
"tax_id": vat_tax_id,
"account_id": revenue_account_id
}),
("create", {
"product_id": hosting_product_id,
"description": "Web Hosting - Annual",
"qty": 1,
"unit_price": 500.00,
"tax_id": vat_tax_id,
"account_id": revenue_account_id
})
]
}, context={"type": "out", "inv_type": "invoice", "date": "2025-01-15"})
# 2. Review and approve
invoice = get_model("account.invoice").browse(invoice_id)
print(f"Invoice: {invoice.number}")
print(f"Subtotal: {invoice.amount_subtotal}")
print(f"Tax: {invoice.amount_tax}")
print(f"Total: {invoice.amount_total}")
# 3. Post to accounting
invoice.approve()
# 4. Verify journal entry
print(f"Journal Entry: {invoice.move_id.number}")
print(f"State: {invoice.state}") # Should be 'waiting_payment'
Use Case 2: Create Supplier Invoice with Currency Conversion¶
# 1. Create supplier invoice in foreign currency
invoice_id = get_model("account.invoice").create({
"type": "in",
"inv_type": "invoice",
"contact_id": supplier_id,
"sup_inv_number": "SUPP-2025-001",
"date": "2025-01-10",
"due_date": "2025-02-10",
"currency_id": usd_currency_id, # Foreign currency
"currency_rate": 1.35, # Exchange rate
"tax_type": "tax_ex",
"lines": [
("create", {
"product_id": material_product_id,
"description": "Raw Materials",
"qty": 1000,
"unit_price": 2.50,
"tax_id": purchase_tax_id,
"account_id": cogs_account_id
})
]
}, context={"type": "in", "inv_type": "invoice"})
# 2. Post invoice
invoice = get_model("account.invoice").browse(invoice_id)
invoice.post()
# 3. Check converted amounts
print(f"Amount in USD: {invoice.amount_total}")
print(f"Amount in Base Currency: {invoice.amount_total_cur}")
Use Case 3: Create Credit Note for Return¶
# 1. Find original invoice
original_invoice_id = 123
# 2. Create credit note
result = get_model("account.invoice").copy_to_credit_note(
[original_invoice_id]
)
credit_note_id = result["invoice_id"]
# 3. Modify credit note for partial return
credit_note = get_model("account.invoice").browse(credit_note_id)
for line in credit_note.lines:
line.write({"qty": line.qty / 2}) # Half quantity returned
# 4. Recalculate and post
credit_note.write({"state": "draft"})
credit_note.approve()
print(f"Credit Note: {credit_note.number}")
print(f"Original Invoice: {credit_note.orig_invoice_id.number}")
Use Case 4: Create Invoice from Sales Order¶
# From sale order context, invoice is usually created automatically
# But manual creation:
sale_order = get_model("sale.order").browse(sale_order_id)
invoice_id = get_model("account.invoice").create({
"type": "out",
"inv_type": "invoice",
"contact_id": sale_order.contact_id.id,
"date": time.strftime("%Y-%m-%d"),
"currency_id": sale_order.currency_id.id,
"tax_type": sale_order.tax_type,
"related_id": f"sale.order,{sale_order_id}",
"lines": [
("create", {
"product_id": line.product_id.id,
"description": line.description,
"qty": line.qty,
"unit_price": line.unit_price,
"tax_id": line.tax_id.id,
"account_id": line.product_id.sale_account_id.id,
"related_id": f"sale.order,{sale_order_id}"
})
for line in sale_order.lines
]
}, context={"type": "out", "inv_type": "invoice"})
Use Case 5: Set Up Recurring Monthly Invoice¶
# 1. Create template invoice
template_id = get_model("account.invoice").create({
"type": "out",
"inv_type": "invoice",
"contact_id": customer_id,
"date": "2025-01-01",
"currency_id": currency_id,
"tax_type": "tax_ex",
"state": "repeat",
"interval_num": 1,
"interval_unit": "month",
"next_date": "2025-02-01",
"next_due_date": "2025-02-15",
"lines": [
("create", {
"product_id": subscription_product_id,
"description": "Monthly Subscription",
"qty": 1,
"unit_price": 99.00,
"tax_id": vat_tax_id,
"account_id": revenue_account_id
})
]
}, context={"type": "out", "inv_type": "invoice"})
# 2. Generate next invoice manually
result = get_model("account.invoice").create_next_invoice([template_id])
new_invoice_id = result["invoice_id"]
# 3. Or set up automated generation (via cron job)
# This would call: get_model("account.invoice").create_repeating_all()
Use Case 6: Track Invoice Payments¶
# 1. Get invoice
invoice = get_model("account.invoice").browse(invoice_id)
# 2. Check payment status
print(f"Total: {invoice.amount_total}")
print(f"Paid: {invoice.amount_paid}")
print(f"Due: {invoice.amount_due}")
print(f"State: {invoice.state}")
# 3. View payment details
for payment_line in invoice.payment_entries:
payment = payment_line.payment_id if hasattr(payment_line, 'payment_id') else None
print(f"Payment Date: {payment_line.move_id.date}")
print(f"Amount: {payment_line.amount_cur or payment_line.credit}")
if payment:
print(f"Payment Ref: {payment.number}")
Performance Tips¶
1. Batch Process Invoices¶
# Bad: Process one at a time
for invoice_id in invoice_ids:
get_model("account.invoice").post([invoice_id])
# Good: Process in batches
batch_size = 50
for i in range(0, len(invoice_ids), batch_size):
batch = invoice_ids[i:i+batch_size]
for invoice_id in batch:
try:
get_model("account.invoice").post([invoice_id])
except Exception as e:
print(f"Error posting invoice {invoice_id}: {e}")
2. Use Stored Computed Fields¶
Most amount fields are stored, so queries are fast:
# Good: Use stored fields for search
invoices = get_model("account.invoice").search_browse([
["amount_due", ">", 0],
["due_date", "<", "2025-01-01"]
])
3. Limit Field Loading¶
# Bad: Load all fields
invoices = get_model("account.invoice").search_browse([...])
# Good: Specify needed fields (if supported)
# Note: Netforce loads all fields by default, but avoid
# unnecessary operations on large result sets
4. Use Database Queries for Reporting¶
# For large-scale reporting, use direct SQL
from netforce import database
db = database.get_connection()
res = db.query("""
SELECT
contact_id,
SUM(amount_total) as total_sales
FROM account_invoice
WHERE type = 'out'
AND state IN ('waiting_payment', 'paid')
AND date >= '2025-01-01'
GROUP BY contact_id
ORDER BY total_sales DESC
LIMIT 10
""")
Troubleshooting¶
"Unique constraint violation on key"¶
Cause: Duplicate invoice number within the same company
Solution: Check for existing invoice with same number. Delete draft duplicates or use different number sequence.
"Account receivable not found" / "Account payable not found"¶
Cause: Missing accounts in contact, contact category, currency, or settings
Solution: Configure accounts in order: Settings → Currency → Contact Category → Contact
"Missing currency rate for [currency]"¶
Cause: No exchange rate defined for transaction date
Solution: Create currency rate record: Settings → Currencies → [Currency] → Add Rate
"Missing account for invoice line"¶
Cause: Invoice line without account_id
Solution: Ensure products have sale/purchase accounts, or set manually. Use add_missing_accounts() method.
"Due date is before invoice date"¶
Cause: Invalid date configuration
Solution: Check payment terms. Due date must be >= invoice date.
"Can't delete invoice with this status"¶
Cause: Invoice is posted or has payments
Solution: Use to_draft() to unpost, then delete. Or use void() to cancel.
"Can't void invoice because there are still related payment entries"¶
Cause: Payments allocated to invoice
Solution: Unreconcile payments first, then void invoice.
"This invoice involves Fixed Assets"¶
Cause: Fixed assets created from invoice
Solution: Delete or deactivate fixed assets first.
"Invoice total is negative"¶
Cause: Line items sum to negative amount
Solution: Check line quantities and unit prices. Use credit note for negative amounts.
"Missing journal entry for invoice"¶
Cause: Invoice not properly posted
Solution: Ensure post() or approve() was successful. Check move_id field.
"Currency of accounts differs from invoice currency"¶
Cause: Account currency mismatch
Solution: Use accounts with correct currency, or configure multi-currency accounts.
Testing Examples¶
Unit Test: Create and Post Invoice¶
def test_create_and_post_invoice():
# Create customer
customer_id = get_model("contact").create({
"name": "Test Customer",
"type": "customer"
})
# Create product
product_id = get_model("product").create({
"name": "Test Product",
"type": "service",
"sale_price": 100.00
})
# Create invoice
invoice_id = get_model("account.invoice").create({
"type": "out",
"inv_type": "invoice",
"contact_id": customer_id,
"date": "2025-01-15",
"currency_id": 1,
"tax_type": "no_tax",
"lines": [
("create", {
"product_id": product_id,
"description": "Test Service",
"qty": 10,
"unit_price": 100.00,
"account_id": 401
})
]
}, context={"type": "out", "inv_type": "invoice"})
# Verify creation
invoice = get_model("account.invoice").browse(invoice_id)
assert invoice.state == "draft"
assert invoice.amount_total == 1000.00
# Post invoice
invoice.post()
invoice = invoice.browse()[0] # Refresh
# Verify posting
assert invoice.state == "waiting_payment"
assert invoice.move_id is not None
assert invoice.amount_due == 1000.00
# Cleanup
invoice.void()
invoice.delete()
get_model("product").delete([product_id])
get_model("contact").delete([customer_id])
Unit Test: Currency Conversion¶
def test_currency_conversion():
# Setup
settings = get_model("settings").browse(1)
base_currency_id = settings.currency_id.id
# Create foreign currency
foreign_currency_id = get_model("currency").create({
"code": "USD",
"name": "US Dollar"
})
# Create exchange rate
get_model("currency.rate").create({
"currency_id": foreign_currency_id,
"date": "2025-01-15",
"rate": 1.35
})
# Create invoice in foreign currency
invoice_id = get_model("account.invoice").create({
"type": "out",
"inv_type": "invoice",
"contact_id": customer_id,
"date": "2025-01-15",
"currency_id": foreign_currency_id,
"currency_rate": 1.35,
"tax_type": "no_tax",
"lines": [("create", {
"description": "Test",
"qty": 1,
"unit_price": 100.00,
"account_id": 401
})]
})
invoice = get_model("account.invoice").browse(invoice_id)
# Verify conversion
assert invoice.amount_total == 100.00 # Foreign currency
assert invoice.amount_total_cur == 135.00 # Base currency (100 * 1.35)
# Cleanup
invoice.delete()
get_model("currency").delete([foreign_currency_id])
Security Considerations¶
Permission Model¶
invoice_view: View invoicesinvoice_create: Create invoicesinvoice_edit: Edit draft invoicesinvoice_delete: Delete draft invoicesinvoice_approve: Approve and post invoicesinvoice_payment: Process payments
Data Access¶
- Multi-company: Users only see invoices for their company
- Audit log: All changes tracked with user and timestamp
- Field-level security: Certain fields restricted by role
- State-based permissions: Actions restricted by invoice state
Best Practices¶
- Always validate user permissions before operations
- Use company context for multi-company installations
- Log sensitive operations (void, delete)
- Validate amounts and calculations server-side
- Prevent SQL injection by using ORM methods
Configuration Settings¶
Required Settings¶
| Setting | Location | Description |
|---|---|---|
currency_id |
Settings | Base currency |
account_receivable_id |
Settings | Default AR account |
account_payable_id |
Settings | Default AP account |
sale_journal_id |
Settings | Sales journal |
purchase_journal_id |
Settings | Purchase journal |
Optional Settings¶
| Setting | Default | Description |
|---|---|---|
rounding_account_id |
None | Account for rounding differences |
freight_charge_cust_id |
None | Freight charges account (customer) |
freight_charge_supp_id |
None | Freight charges account (supplier) |
Sequence Settings¶
Configure sequences for invoice numbering:
- Customer Invoice: cust_invoice
- Customer Credit Note: cust_credit
- Customer Debit Note: cust_debit
- Supplier Invoice: supp_invoice
- Supplier Credit Note: supp_credit
- Supplier Debit Note: supp_debit
- Tax Invoice No: tax_no
Integration Points¶
External Systems¶
- Xero: Sync invoices to Xero accounting via
sync_to_xero()method - Payment Gateways: Online payment via
pay_online()method - Email: Automated invoice delivery via email templates
Internal Modules¶
- Sales: Creates invoices from sales orders
- Purchases: Creates invoices from purchase orders
- Stock: Links to pickings, creates stock movements
- Projects: Invoices project time entries
- Fixed Assets: Creates assets from supplier invoices
- Payments: Links to payment allocation
- Reports: Source for financial reports
Version History¶
Last Updated: November 7, 2025
Model File: netforce_account/models/account_invoice.py
Framework: Netforce
Document Version: 1.0.0
Complexity: ⭐⭐⭐⭐⭐ (Very High - Core accounting model)
Additional Resources¶
- Invoice Line Documentation:
account.invoice.line - Invoice Tax Documentation:
account.invoice.tax - Payment Documentation:
account.payment - Journal Entry Documentation:
account.move - Contact Documentation:
contact - Product Documentation:
product
Support & Feedback¶
For issues or questions about this module: 1. Check related model documentation (invoice.line, payment, move) 2. Review system logs for detailed error messages 3. Verify accounts, taxes, and currencies are configured 4. Test in development environment first 5. Check workflow state before operations
This documentation is generated for developer onboarding and reference purposes.