Skip to content

LHDN Invoice Documentation

Overview

The LHDN Invoice model (account.invoice.lhdn) stores and manages e-invoices synchronized with Malaysia's LHDN (Lembaga Hasil Dalam Negeri) MyInvois system. It tracks invoice status, stores document details, and handles document cancellation and rejection workflows.


Model Information

Model Name: account.invoice.lhdn Display Name: LHDN Invoice Name Field: uuid Key Fields: ["uuid"]

Features

  • ❌ Audit logging enabled (_audit_log)
  • ❌ Multi-company support (via lhdn_account_id)
  • ❌ Full-text content search (_content_search)
  • ✅ UUID-based unique identification
  • ✅ XML and JSON document parsing
  • ✅ Status synchronization with account.invoice
  • ✅ Cancel/Reject workflow with authorization

Key Fields Reference

Identification Fields

Field Type Description
uuid Char LHDN document UUID (unique)
submission_id Char Submission UID
long_id Char Long ID for validation URL
internal_id Char Internal document ID
system_invoice_id Many2One Linked system invoice

Status Fields

Field Type Description
status Char LHDN status (Valid, Invalid, Cancelled, etc.)
state Selection Sync state (lhdn_synced/lhdn_unsynced)
document_status_reason Char Status change reason
date_time_validated Char Validation timestamp
cancel_date_time Char Cancellation timestamp
reject_request_date_time Char Rejection timestamp

Party Fields

Field Type Description
issuer_tin Char Issuer Tax ID
issuer_name Char Issuer name
receiver_id Char Receiver ID
receiver_name Char Receiver name
accounting_supplier_party_* Various Supplier details
accounting_customer_party_* Various Customer details

Amount Fields

Field Type Description
total_excluding_tax Char Subtotal
total_discount Char Total discounts
total_net_amount Char Net amount
total_payable_amount Char Payable amount
tax_total_tax_amount Char Tax amount

Document Fields

Field Type Description
document Text Raw document (XML/JSON)
invoice_type_code Char Document type code
invoice_type Selection Invoice type (client/supplier)
invoice_subtype Selection Subtype (AR/AP)

Computed Fields

Field Type Description
can_cancel Boolean Whether user can cancel
can_reject Boolean Whether user can reject
validation_url Char Public validation URL

Default Values

_defaults = {
    "state": "lhdn_unsynced",
}

Default Ordering

Records ordered by UUID descending:

_order = "uuid desc"

State Options

Value Label Description
lhdn_synced Synced Document synced with LHDN
lhdn_unsynced Unsynced Pending sync

Invoice Type Options

Value Label
client_invoice Client Invoice
supplier_invoice Supplier Invoice

API Methods

1. Fetch All Documents

Method: fetch_all(ids=None, context={})

Fetches all documents from LHDN for all active accounts.

Behavior: 1. Iterates through all LHDN accounts 2. Authenticates with each account 3. Searches for documents since last sync 4. Creates/updates LHDN invoice records 5. Updates last_sync_time

Example:

result = get_model("account.invoice.lhdn").fetch_all()
# Returns: {"flash": "LHDN documents fetched successfully"}


2. Get Document from LHDN

Method: get_document_from_lhdn(ids, context={})

Fetches and updates a single document from LHDN.

Example:

get_model("account.invoice.lhdn").get_document_from_lhdn([lhdn_invoice_id])


3. Cancel LHDN Document

Method: cancel_lhdn_document(ids, context={})

Initiates cancellation of a document (issuer only).

Validation: - Document must have status "Valid" - User must be issuer (TIN match) - Within 72-hour window

Raises: - Exception if not issuer - Exception if document > 3 days old - Exception if already cancelled

Example:

result = get_model("account.invoice.lhdn").cancel_lhdn_document([lhdn_invoice_id])
# Returns: {"flash": "The document has been requested for cancellation."}


4. Reject LHDN Document

Method: reject_lhdn_document(ids, context={})

Initiates rejection of a document (receiver only).

Validation: - Document must have status "Valid" - User must be receiver (TIN match) - Within 72-hour window

Raises: - Exception if not receiver - Exception if document > 3 days old - Exception if already rejected

Example:

result = get_model("account.invoice.lhdn").reject_lhdn_document([lhdn_invoice_id])
# Returns: {"flash": "The document has been requested for rejection."}


5. Get Can Cancel/Reject

Method: get_can_cancel_reject(ids, context={})

Computes whether current user can cancel or reject.

Logic: - can_cancel: True if company TIN matches issuer TIN - can_reject: True if company TIN matches customer TIN

Example:

perms = get_model("account.invoice.lhdn").get_can_cancel_reject([lhdn_invoice_id])
# Returns: {id: {"can_cancel": True, "can_reject": False}}


Model Relationship Description
account.lhdn Many2One LHDN account
account.invoice Many2One System invoice
account.invoice.lhdn.line One2Many Invoice lines

Common Use Cases

Use Case 1: Sync All Documents

# Fetch all documents from LHDN
result = get_model("account.invoice.lhdn").fetch_all()
print(result["flash"])

Use Case 2: Check Document Status

# Find documents by status
valid_docs = get_model("account.invoice.lhdn").search_browse([
    ["status", "=", "Valid"]
])

print(f"Valid documents: {len(valid_docs)}")
for doc in valid_docs:
    print(f"  {doc.uuid}: {doc.issuer_name} -> {doc.receiver_name}")

Use Case 3: Cancel Invoice

# Cancel an invoice (must be issuer)
lhdn_invoice = get_model("account.invoice.lhdn").browse([lhdn_id])[0]

if lhdn_invoice.can_cancel:
    result = get_model("account.invoice.lhdn").cancel_lhdn_document([lhdn_id])
    print(result["flash"])
else:
    print("Cannot cancel - not the issuer")

Use Case 4: Find Linked System Invoice

# Find LHDN invoice by system invoice number
invoice_number = "INV-2024-001"
lhdn_docs = get_model("account.invoice.lhdn").search_browse([
    ["internal_id", "=", invoice_number]
])

if lhdn_docs:
    doc = lhdn_docs[0]
    print(f"LHDN UUID: {doc.uuid}")
    print(f"Status: {doc.status}")
    print(f"Validation URL: {doc.validation_url}")

Use Case 5: Check 72-Hour Window

from datetime import datetime, timedelta
from pytz import timezone

MALAYSIA_TZ = timezone('Asia/Kuala_Lumpur')
now = datetime.now(MALAYSIA_TZ)
three_days_ago = (now - timedelta(days=3)).strftime("%Y-%m-%d %H:%M:%S")

# Find documents within cancellation window
valid_recent = get_model("account.invoice.lhdn").search_browse([
    ["status", "=", "Valid"],
    ["date_time_validated", ">=", three_days_ago]
])

print(f"Documents that can still be cancelled/rejected: {len(valid_recent)}")

Document Flow

1. Invoice Submitted to LHDN
2. Document Created in LHDN Portal
3. fetch_all() or manual sync
4. LHDN Invoice record created
   ├─ Valid: Can be cancelled/rejected within 72 hours
   │   ├─ Issuer can Cancel
   │   └─ Receiver can Reject
   └─ Invalid: Needs correction
5. Status Synced to System Invoice

Best Practices

1. Regular Sync

# Good: Set up scheduled job for regular syncing
# Run fetch_all() periodically (e.g., every hour)

2. Check Permissions Before Actions

# Good: Check can_cancel/can_reject before UI buttons
doc = get_model("account.invoice.lhdn").browse([doc_id])[0]
if doc.can_cancel:
    # Show cancel button
if doc.can_reject:
    # Show reject button

3. Handle 72-Hour Window

# Good: Warn users about approaching deadline
# Documents can only be cancelled/rejected within 72 hours

Troubleshooting

"Cannot cancel - not the issuer"

Cause: Company TIN doesn't match supplier TIN Solution: Only issuer can cancel; receivers must reject

"Cannot reject - not the receiver"

Cause: Company TIN doesn't match customer TIN Solution: Only receiver can reject; issuers must cancel

"Document is 3 or more days old"

Cause: 72-hour window has passed Solution: Issue credit/debit note instead

"Document not found after fetch"

Cause: Document may be from different LHDN account Solution: Ensure correct LHDN account is active


Testing Examples

Unit Test: Can Cancel/Reject Logic

def test_can_cancel_reject():
    # Create LHDN invoice record
    lhdn_id = get_model("account.invoice.lhdn").create({
        "uuid": "test-uuid",
        "lhdn_account_id": account_id,
        "status": "Valid",
        "accounting_supplier_party_tin": company_tin,  # Our company
        "accounting_customer_party_tin": other_tin,
    })

    # Verify we can cancel (we're issuer)
    doc = get_model("account.invoice.lhdn").browse([lhdn_id])[0]
    assert doc.can_cancel == True
    assert doc.can_reject == False

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

Security Considerations

Authorization

  • Cancel restricted to issuer
  • Reject restricted to receiver
  • TIN matching enforced

Data Access

  • Links to system invoices for audit trail
  • Status changes logged

Version History

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


Additional Resources

  • LHDN Account Documentation: account.lhdn
  • LHDN Invoice Line Documentation: account.invoice.lhdn.line
  • System Invoice Documentation: account.invoice

This documentation is generated for developer onboarding and reference purposes.