LHDN Account Documentation¶
Overview¶
The LHDN Account model (account.lhdn) manages the configuration and credentials for connecting to Malaysia's LHDN (Lembaga Hasil Dalam Negeri) MyInvois e-invoice system. It stores API credentials, tracks client secret expiration, and manages account activation state.
Model Information¶
Model Name: account.lhdn
Display Name: LHDN Account
Name Field: name
Key Fields: None (company_id key commented out)
Features¶
- ❌ Audit logging enabled (
_audit_log) - ❌ Multi-company support (linked via company_id)
- ❌ Full-text content search (
_content_search) - ✅ Single active account enforcement
- ✅ Client secret expiration tracking
- ✅ Last sync time tracking
Key Fields Reference¶
Core Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
name |
Char | ❌ | Account name (searchable) |
company_id |
Many2One | ✅ | Associated company |
client_id |
Char | ✅ | LHDN API Client ID |
client_secret_1 |
Char | ✅ | Primary client secret |
client_secret_2 |
Char | ✅ | Secondary client secret |
Expiration Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
client_secret_expiration |
Selection | ✅ | Expiration period (1-3 years) |
client_secret_expiry |
Date | ✅ | Actual expiration date |
Status Fields¶
| Field | Type | Description |
|---|---|---|
state |
Selection | Account state (active/inactive/expired) |
last_sync_time |
DateTime | Last successful sync with LHDN |
Default Values¶
Default Ordering¶
Records are ordered by company:
State Options¶
| Value | Label | Description |
|---|---|---|
active |
Active | Currently active account |
inactive |
Inactive | Not active (default) |
expired |
Expired | Client secret has expired |
Expiration Options¶
| Value | Label | Days |
|---|---|---|
1 |
1 Year | 365 |
2 |
2 Years | 730 |
3 |
3 Years | 1095 |
API Methods¶
1. Calculate Expiry¶
Method: calculate_expiry(context)
Calculates client secret expiration date based on selected period.
Context Data:
- client_secret_expiration: Required period selection
Behavior:
1. Gets current date
2. Adds days based on expiration period
3. Sets client_secret_expiry date
Example:
context = {"data": {"client_secret_expiration": "2"}}
result = get_model("account.lhdn").calculate_expiry(context)
# Sets expiry date to 2 years from now
2. Set as Active¶
Method: set_as_active(ids, context={})
Activates the account and deactivates all other accounts.
Behavior: 1. Finds all currently active accounts 2. Sets them to inactive 3. Sets selected account to active
Returns: Flash message confirmation
Example:
result = get_model("account.lhdn").set_as_active([account_id])
# Returns: {"flash": "LHDN Account set as active"}
3. Set as Inactive¶
Method: set_as_inactive(ids, context={})
Deactivates the account.
Example:
result = get_model("account.lhdn").set_as_inactive([account_id])
# Returns: {"flash": "LHDN Account set as inactive"}
Related Models¶
| Model | Relationship | Description |
|---|---|---|
company |
Many2One (company_id) | Associated company |
account.invoice.lhdn |
via lhdn_account_id | LHDN invoices |
Common Use Cases¶
Use Case 1: Create LHDN Account¶
account_id = get_model("account.lhdn").create({
"name": "Main LHDN Account",
"company_id": company_id,
"client_id": "your-client-id",
"client_secret_1": "primary-secret",
"client_secret_2": "secondary-secret",
"client_secret_expiration": "2",
"client_secret_expiry": "2026-12-15",
})
# Activate the account
get_model("account.lhdn").set_as_active([account_id])
Use Case 2: Check Active Account¶
# Get active account
active_accounts = get_model("account.lhdn").search_browse([
["state", "=", "active"]
])
if active_accounts:
account = active_accounts[0]
print(f"Active account: {account.name}")
print(f"Company: {account.company_id.name}")
print(f"Secret expires: {account.client_secret_expiry}")
else:
print("No active LHDN account configured")
Use Case 3: Check Expiring Accounts¶
import time
from datetime import datetime, timedelta
# Find accounts expiring within 30 days
warning_date = (datetime.now() + timedelta(days=30)).strftime("%Y-%m-%d")
expiring = get_model("account.lhdn").search_browse([
["client_secret_expiry", "<=", warning_date],
["state", "!=", "expired"],
])
for account in expiring:
print(f"Warning: {account.name} expires on {account.client_secret_expiry}")
Use Case 4: Rotate Client Secret¶
# Update client secret before expiration
account = get_model("account.lhdn").browse([account_id])[0]
# Get new secret from LHDN portal and update
get_model("account.lhdn").write([account_id], {
"client_secret_1": "new-primary-secret",
"client_secret_2": "new-secondary-secret",
"client_secret_expiration": "2",
})
# Calculate new expiry date
context = {
"data": {
"client_secret_expiration": "2",
"client_secret_expiry": None
}
}
result = get_model("account.lhdn").calculate_expiry(context)
Account Flow¶
1. Create LHDN Account
│
├─ Enter API credentials from LHDN portal
├─ Select secret expiration period
└─ Calculate expiry date
│
2. Activate Account
│
└─ Only ONE account can be active at a time
│
3. Use for E-Invoice Operations
│
├─ Submit invoices to LHDN
├─ Fetch documents from LHDN
└─ Cancel/Reject documents
│
4. Monitor Expiration
│
└─ Renew secret before expiry
Best Practices¶
1. Single Active Account¶
# Good: Only activate one account
# System enforces this automatically
get_model("account.lhdn").set_as_active([new_account_id])
# Previous active account is automatically deactivated
2. Track Expiration¶
3. Secure Credentials¶
# Good: Never log or expose credentials
# client_secret fields should be protected
# Bad: Don't print secrets in logs
# print(f"Secret: {account.client_secret_1}") # Never do this
Troubleshooting¶
"No active LHDN account found"¶
Cause: No account has state="active" Solution: Use set_as_active() to activate an account
"Authentication failed"¶
Cause: Invalid or expired credentials Solution: Verify client_id and client_secret, check expiry date
"Multiple active accounts"¶
Cause: Database inconsistency Solution: set_as_active() will fix this automatically
Testing Examples¶
Unit Test: LHDN Account Activation¶
def test_lhdn_account_activation():
# Create two accounts
account1_id = get_model("account.lhdn").create({
"name": "Account 1",
"company_id": company_id,
"client_id": "client-1",
"client_secret_1": "secret-1",
"client_secret_2": "secret-2",
"client_secret_expiration": "1",
"client_secret_expiry": "2025-12-31",
})
account2_id = get_model("account.lhdn").create({
"name": "Account 2",
"company_id": company_id,
"client_id": "client-2",
"client_secret_1": "secret-3",
"client_secret_2": "secret-4",
"client_secret_expiration": "1",
"client_secret_expiry": "2025-12-31",
})
# Activate first account
get_model("account.lhdn").set_as_active([account1_id])
account1 = get_model("account.lhdn").browse([account1_id])[0]
assert account1.state == "active"
# Activate second account
get_model("account.lhdn").set_as_active([account2_id])
# Verify first is now inactive
account1 = get_model("account.lhdn").browse([account1_id])[0]
account2 = get_model("account.lhdn").browse([account2_id])[0]
assert account1.state == "inactive"
assert account2.state == "active"
# Cleanup
get_model("account.lhdn").delete([account1_id, account2_id])
Security Considerations¶
Permission Model¶
- Restricted to accounting administrators
- Credentials should be highly protected
Data Protection¶
- Client secrets are sensitive API credentials
- Never expose in logs or API responses
- Consider encryption at rest
Configuration Requirements¶
| Requirement | Description |
|---|---|
| LHDN Portal Access | Register at MyInvois portal |
| Valid TIN | Company must have valid Tax ID |
| Digital Certificate | P12/PFX certificate for signing |
Version History¶
Last Updated: December 2024 Model Version: lhdn_account.py Framework: Netforce
Additional Resources¶
- LHDN Invoice Documentation:
account.invoice.lhdn - E-Invoice Module:
einvoice.py - MyInvois Portal: https://myinvois.hasil.gov.my
This documentation is generated for developer onboarding and reference purposes.