Skip to content

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:

_key = ["company_id", "number"]

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:

_constraints = ["check_fields"]

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

draft → waiting_approval → approved → waiting_payment → paid
                                          voided

                                         repeat (recurring invoices)
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:

get_model("account.invoice").void([invoice_id])

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:

get_model("account.invoice").to_draft([invoice_id])


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:

result = get_model("account.invoice").copy_to_sale([invoice_id])


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:

get_model("account.invoice").add_missing_accounts([invoice_id])


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:

# Automatically called during approval for supplier invoices
invoice.create_fixed_assets()


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:

result = get_model("account.invoice").sync_to_xero([invoice_id])


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

# Find invoices containing a specific product
condition = [["product_id", "=", product_id]]

Search by Contact Category

# Find invoices by customer category
condition = [["contact_categ_id", "=", category_id]]

Search by Date Range

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

Search by State

# Find unpaid invoices
condition = [["state", "=", "waiting_payment"]]

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")

# 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

_key = ["company_id", "number"]

Ensures invoice numbers are unique within each company.

Check Constraints

_constraints = ["check_fields"]

Custom validation method that checks: - Due date is present and after invoice date - Lines exist (for most invoice types)


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.

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 invoices
  • invoice_create: Create invoices
  • invoice_edit: Edit draft invoices
  • invoice_delete: Delete draft invoices
  • invoice_approve: Approve and post invoices
  • invoice_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


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.