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¶
Default Ordering¶
Records ordered by UUID descending:
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:
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}}
Related Models¶
| 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¶
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.