Skip to content

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

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

Default Ordering

Records are ordered by company:

_order = "company_id"

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"}


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

# Good: Monitor expiration proactively
# Set up background job to check expiring accounts

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.