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 |
Related Models¶
| 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.