Skip to content

LHDN Invoice Line Documentation

Overview

The LHDN Invoice Line model (account.invoice.lhdn.line) stores individual line items from e-invoices synchronized with Malaysia's LHDN MyInvois system. Each line represents a product or service with its quantity, pricing, and tax information.


Model Information

Model Name: account.invoice.lhdn.line Display Name: LHDN Invoice Line Name Field: uuid Key Fields: None (no unique constraint defined)

Features

  • ✅ Audit logging enabled (_audit_log)
  • ❌ Multi-company support (company_id)
  • ❌ Full-text content search (_content_search)
  • ✅ Cascade delete with parent invoice

Key Fields Reference

All Fields

Field Type Required Description
lhdn_invoice_id Many2One Parent LHDN invoice
product_id Char Product ID/Line ID
description Text Item description
qty Char Quantity
uom_id Char Unit of measure code
unit_price Char Unit price
discount_percent Char Discount percentage
discount_amount Char Discount amount
fee_charge_percent Char Fee/charge percentage
fee_charge_amount Char Fee/charge amount
product_classification_codes_id Char LHDN classification code
amount Char Line amount
amount_tax Char Tax amount

Model Relationship Description
account.invoice.lhdn Many2One (lhdn_invoice_id) Parent invoice

Common Use Cases

Use Case 1: View Invoice Lines

# Get lines for an LHDN invoice
lhdn_invoice = get_model("account.invoice.lhdn").browse([lhdn_id])[0]

print(f"Invoice Lines for {lhdn_invoice.uuid}:")
for line in lhdn_invoice.invoice_line_ids:
    print(f"  {line.product_id}: {line.description}")
    print(f"    Qty: {line.qty} {line.uom_id}")
    print(f"    Price: {line.unit_price}")
    print(f"    Amount: {line.amount}")
    print(f"    Tax: {line.amount_tax}")

Use Case 2: Calculate Line Totals

# Sum up line amounts
lines = get_model("account.invoice.lhdn.line").search_browse([
    ["lhdn_invoice_id", "=", lhdn_id]
])

total_amount = 0
total_tax = 0
for line in lines:
    total_amount += float(line.amount or 0)
    total_tax += float(line.amount_tax or 0)

print(f"Total amount: {total_amount}")
print(f"Total tax: {total_tax}")
print(f"Grand total: {total_amount + total_tax}")

Use Case 3: Find Lines by Classification

# Find lines with specific classification code
lines = get_model("account.invoice.lhdn.line").search_browse([
    ["product_classification_codes_id", "=", "01111"]
])

for line in lines:
    invoice = line.lhdn_invoice_id
    print(f"Invoice {invoice.uuid}: {line.description} - {line.amount}")

Use Case 4: Check Discounts

# Find lines with discounts
lines = get_model("account.invoice.lhdn.line").search_browse([
    ["discount_amount", "!=", "0"],
    ["discount_amount", "!=", ""],
])

for line in lines:
    print(f"{line.description}")
    print(f"  Discount: {line.discount_percent}% = {line.discount_amount}")

Data Structure

Line data comes from LHDN in two formats:

XML Format (Valid Documents)

<cac:InvoiceLine>
    <cbc:ID>1</cbc:ID>
    <cbc:InvoicedQuantity unitCode="C62">1.00</cbc:InvoicedQuantity>
    <cbc:LineExtensionAmount currencyID="MYR">100.00</cbc:LineExtensionAmount>
    <cac:TaxTotal>
        <cbc:TaxAmount currencyID="MYR">8.00</cbc:TaxAmount>
    </cac:TaxTotal>
    <cac:Item>
        <cbc:Description>Product Name</cbc:Description>
        <cac:CommodityClassification>
            <cbc:ItemClassificationCode listID="CLASS">01111</cbc:ItemClassificationCode>
        </cac:CommodityClassification>
    </cac:Item>
    <cac:Price>
        <cbc:PriceAmount currencyID="MYR">100.00</cbc:PriceAmount>
    </cac:Price>
</cac:InvoiceLine>

JSON Format (Invalid Documents)

{
    "InvoiceLine": [{
        "ID": [{"_": "1"}],
        "InvoicedQuantity": [{"_": "1.00", "unitCode": "C62"}],
        "Item": [{"Description": [{"_": "Product Name"}]}],
        "Price": [{"PriceAmount": [{"_": "100.00"}]}]
    }]
}

Best Practices

1. Use Parent Relationship

# Good: Access lines through parent invoice
invoice = get_model("account.invoice.lhdn").browse([lhdn_id])[0]
for line in invoice.invoice_line_ids:
    # Process line

2. Handle Missing Data

# Good: Check for None/empty values
amount = float(line.amount or 0)
tax = float(line.amount_tax or 0)

# Fields are stored as Char, convert as needed

3. Preserve Original Data

# Good: Don't modify synced data
# Lines are read-only copies from LHDN
# Modifications won't sync back

Troubleshooting

"Lines not showing"

Cause: Lines not parsed from document Solution: Re-sync document using get_document_from_lhdn()

"Amount is empty"

Cause: Different XML/JSON structure Solution: Check document format and parsing

"Classification code missing"

Cause: Not all lines have classification Solution: Field may be optional in some document types


Testing Examples

Unit Test: Line Creation

def test_lhdn_invoice_line():
    # Create parent invoice
    lhdn_id = get_model("account.invoice.lhdn").create({
        "uuid": "test-uuid",
    })

    # Create line
    line_id = get_model("account.invoice.lhdn.line").create({
        "lhdn_invoice_id": lhdn_id,
        "product_id": "1",
        "description": "Test Product",
        "qty": "1.00",
        "uom_id": "C62",
        "unit_price": "100.00",
        "amount": "100.00",
        "amount_tax": "8.00",
    })

    # Verify
    line = get_model("account.invoice.lhdn.line").browse([line_id])[0]
    assert line.description == "Test Product"
    assert line.amount == "100.00"

    # Cleanup (cascade delete)
    get_model("account.invoice.lhdn").delete([lhdn_id])

Security Considerations

Permission Model

  • Read access for invoice viewing
  • Lines are auto-populated from LHDN

Data Integrity

  • Lines reflect LHDN document exactly
  • No manual modification expected

Version History

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


Additional Resources

  • LHDN Invoice Documentation: account.invoice.lhdn
  • LHDN Account Documentation: account.lhdn

This documentation is generated for developer onboarding and reference purposes.