Odoo GDT e-VAT setup: full configuration walkthrough
End-to-end Odoo configuration for Cambodia GDT e-VAT compliance: chart of accounts, tax codes, customer master, the GDT submission adapter, and the production cutover.
- No version of Odoo ships native GDT support. Every KH deployment needs a custom adapter module and KH localization configuration.
- Basic setup (chart of accounts, tax codes, customer master) takes 8–16 hours. Production GDT API integration takes 4–6 weeks.
- The most common pitfall is KHR rounding at the line level. GDT expects rounding at the invoice total, not per-line.
What does this guide cover?
This is an end-to-end configuration walkthrough for Odoo 18 and 19 (Community and Enterprise editions) targeting full Cambodia GDT e-VAT compliance. Odoo 18 is the first release to carry an official Cambodia accounting localization, which makes it the practical floor for a new KH deployment; the steps still apply to Odoo 17, but on 17 the Cambodian chart of accounts has to be built by hand. It covers the core accounting configuration (chart of accounts, tax codes, journals, multi-currency setup), the customer and vendor master configuration required by GDT, and the custom adapter module you will need to build or commission for GDT API submission. It also covers CamInvoice readiness at the adapter layer, since the two integration paths share infrastructure. Audience: Odoo administrators, finance managers setting up a new Odoo instance, and integration engineers scoping a GDT adapter build. Time investment: 8–16 hours for the accounting configuration steps covered in this guide; 4–6 weeks for production GDT API integration including sandbox certification. Before starting, read the foundational GDT e-VAT overview. This guide assumes you understand what GDT e-VAT requires legally. It covers only the Odoo-specific implementation. If Odoo is not yet a settled decision, our ERP in Cambodia guide covers the platform choice and our Odoo vs SAP Business One vs Acumatica comparison puts the three main options side by side.
Pre-flight: what you need before you start
Collect these before touching Odoo configuration. Missing any of them mid-setup creates friction that is hard to reverse cleanly.
- GDT-issued TIN for each legal entity you are configuring. The TIN must be active and match the business name exactly as registered, not a trading name.
- e-Tax portal credentials (username and password). Test them before starting; expired credentials block the sandbox access step.
- GDT sandbox access, arranged with GDT directly rather than self-served from a developer portal. Budget 1–2 weeks, and start the conversation early. Without sandbox access you cannot test the submission adapter before going to production.
- Cambodian chart of accounts seed file. On Odoo 18 and 19 the native l10n_kh localization provides the seed chart; on Odoo 17 or earlier you supply your own. Do not start with Odoo's default generic chart of accounts and patch it later; the rework cost is high.
- Odoo instance with admin role and server access (for Community) or Odoo.sh project access (for Enterprise hosted). You need the ability to install third-party modules; pure SaaS Odoo.com plans restrict this.
- NBC official exchange rate table for the last 12 months. You will need this to configure the multi-currency rate source and to backfill historical transactions in the correct KHR amounts.
Step 1: install the KH localization module
Odoo's generic accounting module carries no Cambodian chart of accounts, tax codes, or GDT field mappings. Those come from the localization, and Odoo ships one natively: l10n_kh ('Cambodia - Accounting'), from Odoo S.A., licensed LGPL-3, present in the Community edition from Odoo 18 onwards. It provides the Cambodian chart of accounts, the taxes, withholding-tax support, and the T7001 and WT003 tax form templates. It provides nothing that talks to GDT: the filing adapter is still yours to build. Odoo 17 and earlier have no official Cambodia localization at all, so on those versions the chart of accounts is a manual build.
Install path: Settings → Apps → search 'Cambodia' or 'KH localization' → install the accounting localization module. After installation, navigate to Accounting → Configuration → Chart of Accounts to verify the KH account structure has loaded. You should see the Cambodian account structure rather than Odoo's generic US/international chart. One thing to be clear about: Cambodia mandates reporting standards, not account codes. Companies report under CIFRS or CIFRS for SMEs, and there is no officially mandated chart of accounts or numbering scheme for ordinary businesses (banks are the exception, with a chart prescribed by the NBC). So agree the numbering with your auditor rather than hunting for a legal one. If the chart is empty or shows the generic chart, the localization did not apply correctly; reinstall before proceeding.
Common pitfall: installing the localization module on an instance that already has transactions posted will not retroactively remap those transactions to the KH chart. The localization must be installed on a clean instance before any invoices or journal entries are posted. If you are migrating from an existing Odoo instance with posted transactions, plan a chart-of-accounts migration project separately; it is not covered by the module install.
Step 2: configure the chart of accounts
Even with the KH localization installed, you need to verify and extend the chart for your specific business. GDT-critical accounts that must exist as distinct ledger lines:
- Output VAT liability account: the 10% VAT collected on sales. Maps to GDT VAT 01 output VAT line.
- Input VAT credit account: VAT paid on purchases eligible for credit. Maps to GDT VAT 01 input VAT credit line.
- Withholding tax payable accounts: one account per WHT rate you actually incur, typically 15% (resident services, royalties, interest), 10% (rental), and 14% (non-resident). Separate accounts, not one combined WHT payable, so each reconciles to its own line on the monthly return.
- Salary tax payable account: Tax on Salary withheld from employee payroll and due to GDT. Keep it distinct from any Tax on Income (the annual business income tax) provision account.
- KHR cash and bank accounts: separate from USD cash and bank. Each currency gets its own account in Odoo's chart. Do not use a single multi-currency account if you need to reconcile KHR and USD separately for GDT reporting.
Set up two journal types for each active currency: KHR bank journal and USD bank journal, each linked to the correct bank account. Odoo's multi-currency handling then converts between them using the configured rate. The KHR/USD journals are critical for multi-currency reconciliation; transactions posted to the wrong journal produce currency-gain/loss entries that inflate or deflate the GDT-reported VAT base.
Step 3: configure tax codes
Navigate to Accounting → Configuration → Taxes. You need the following tax codes as a minimum for GDT compliance. Each must be configured with the correct account mapping so that posting an invoice automatically populates the correct VAT ledger account.
- VAT 10% (standard rate): applies to most taxable goods and services sold in Cambodia. Tax group: output VAT. Maps to: output VAT liability account.
- VAT 0% (export rate): applies to direct exports. Tax group: output VAT. Maps to: output VAT liability account at zero. Required as a distinct tax code so that export invoices appear correctly in the VAT 01 return.
- VAT exempt: for non-taxable supplies. The list runs to unprocessed agricultural products (processed food is standard-rated), hospital, medical and dental services, education, public postal services, state-owned public passenger transport, insurance, primary financial services, and waste removal, among others. Tax group: exempt. Must appear on the invoice as a tax line with zero amount so the return correctly identifies the exempt portion of turnover.
- WHT 15% (resident services, royalties, interest): applied on the purchase side when paying a resident supplier for services, royalties, or interest. Services billed by a registered taxpayer against a valid VAT invoice are excluded. Tax group: withholding. Maps to: WHT payable (resident 15%) account.
- WHT 10% (rental): for rent paid on movable or immovable property. Maps to: WHT payable (rental) account.
- WHT 14% (non-resident): for any Cambodian-source payment to a non-resident counterparty, including cross-border services, royalties, interest, and management fees. A double-tax agreement may reduce this, so check the treaty before applying the default. Maps to: WHT payable (non-resident) account.
- Tax on Salary: configured in the Payroll module (if using Odoo Payroll) rather than in Accounting taxes, but the journal entry it produces must post to the salary tax payable account in the chart of accounts for reconciliation to the monthly return.
WHT taxes in Odoo are typically configured as purchase taxes on vendor bills, not as sales taxes. When you post a vendor bill and apply the WHT tax, Odoo credits the WHT payable account and reduces the cash payment due to the supplier. The WHT payable account is then remitted to GDT as part of the monthly WHT return. Verify this journal behaviour in a test transaction before processing real vendor invoices.
Step 4: configure customer and vendor master
Every customer and vendor record needs a TIN field populated for GDT compliance. In Odoo, TIN is stored in the VAT field on the partner record (Contacts → partner → Accounting tab → Tax ID / VAT). This field must be populated for:
- All VAT-registered customer accounts: the TIN appears on issued invoices and must match GDT's registry for that buyer.
- All supplier accounts where you will apply WHT: the rate (15% resident services, 10% rental, 14% non-resident) depends on the counterparty's residence and registration status, which ties to their TIN.
- Government counterparties: for CamInvoice B2G submissions, the buyer TIN must match the ministry or SOE's TIN in GDDE's national registry. Collect and verify these TINs before go-live, not at submission time.
Set default tax codes per partner where possible: for customers who always receive standard-rated invoices, set the default sales tax to VAT 10%. For resident service suppliers who always attract WHT at 15%, set the default purchase tax accordingly. This reduces manual tax selection errors at invoice entry time and makes the WHT classification auditable: the default is on the partner record, not in someone's head.
Multi-currency customer ledgers: if a customer invoices in both USD and KHR (common in Cambodia), the partner record's default currency should reflect the primary invoicing currency. Use Odoo's price list feature to manage dual-currency pricing per customer segment. Ensure that KHR invoices use the KHR pricelist and USD invoices use the USD pricelist. Mixing currencies within a single invoice in Odoo requires careful review of the KHR conversion logic before it reaches the GDT adapter.
Step 5: build the GDT submission adapter
This is the step that no Odoo module handles for you out of the box. The GDT submission adapter is a custom Odoo module (or external microservice) that reads posted invoice and journal data from Odoo and submits it to the GDT e-VAT API. The architecture we use: a scheduled cron job that runs nightly (or on-demand before filing deadline), reads posted invoice lines for the period, maps them to the GDT API field schema, authenticates using GDT API credentials, and submits the monthly return payload. For CamInvoice, the pattern is different: a per-invoice trigger on invoice confirmation rather than a batch cron job. Build both as separate modules in the same integration layer.
Authentication uses credentials tied to the entity's TIN, issued through the GDT portal [confirm the exact mechanism with GDT when you arrange access; it is not published anywhere you can look it up]. The adapter must handle credential rotation, token refresh, and expiry gracefully. A lapsed token at filing time is the most common cause of last-minute manual filing fallback.
Illustrative Python snippet for reading posted invoice lines from Odoo and preparing the GDT payload:
- // Illustrative, not production code
- // Read posted invoices for the period from Odoo ORM
- invoices = env['account.move'].search([('state','=','posted'), ('move_type','=','out_invoice'), ('invoice_date','>=', period_start), ('invoice_date','<=', period_end)])
- // Map each invoice to the GDT field schema
- for inv in invoices:
- payload = build_gdt_line(inv.partner_id.vat, inv.name, inv.amount_tax_khr, inv.amount_total_khr)
- // Submit batch to the GDT filing endpoint [endpoint confirmed with GDT when you arrange access; not published]
- response = gdt_client.post('/evat/monthly-return', payload=batch)
Error handling is not optional. The adapter must log every GDT response (acceptance, rejection, and partial acceptance) back to the Odoo journal entry as a chatter message or a custom field. Rejection codes from GDT must be mapped to human-readable explanations so the finance team can resolve them without decoding API responses manually. Build a reconciliation step that verifies the filed amounts match the Odoo trial balance for the period before submission; catching discrepancies at reconciliation time is cheaper than an amended return.
Step 6: end-to-end test in sandbox
Do not skip sandbox testing. The GDT sandbox environment [access arranged with GDT directly; there is no self-service developer portal] accepts the same calls as production but does not affect live filings. Test cycle: raise a test invoice in Odoo for a test customer with a dummy TIN; post it; trigger the GDT adapter; verify the submission reaches the sandbox; check the GDT sandbox response for the filing reference number; reconcile the reference number back to the Odoo journal entry. Then test the failure cases: submit an invoice with a mismatched TIN and verify the rejection code is logged correctly in Odoo. Submit with a KHR rounding error and verify the adapter catches it before submission rather than receiving a GDT rejection.
Common sandbox test failures and their causes:
- TIN format rejection: GDT TINs must be formatted exactly as issued (typically numeric, fixed length [confirm the exact format with GDT; it is not published]). Leading zeros dropped by Python int casting are a common source of format failures.
- KHR rounding rejection: GDT validates that KHR amounts on the filing match the sum of line-level amounts. If your USD-to-KHR conversion rounds at the line level and then rounds again at the total, you get a 1-riel discrepancy. Convert at line level, sum unrounded, then round the total once.
- WHT classification mismatch: sandbox GDT may reject WHT filings where the counterparty TIN type (company vs individual vs non-resident) does not match the WHT rate applied. Test all three WHT rates against the correct counterparty types.
- Credential expiry: sandbox credentials expire independently of production credentials. If your sandbox tests start failing after a few weeks with an authentication error, refresh the sandbox credentials rather than assuming an adapter bug.
Step 7: production cutover
Production cutover should happen at a period boundary (the beginning of a new month) so that the adapter handles a clean period with no overlap from manual filings. The cutover checklist: obtain production GDT API credentials from the GDT portal (separate from sandbox credentials); update the adapter configuration to point to the production endpoint [confirmed with GDT alongside your production credentials]; verify production credentials authenticate correctly in a dry-run mode before the filing deadline; notify the finance team that the first production filing will be adapter-driven and confirm the review process for that filing; set up monitoring alerts on the cron job and on GDT API response codes so that a failed submission is caught within hours, not days.
For the first 30 days post-go-live, run the adapter output alongside a manual cross-check of the filed amounts against the Odoo trial balance. The parallel check catches any field mapping issues that did not surface in sandbox. After a clean first filing, the manual cross-check can be reduced to a spot-check every quarter. Keep the on-call contact for the integration team reachable during the first filing deadline: 5pm on the 25th of the first month after go-live is not the time to discover a production issue with no one available.
Common pitfalls
- KHR rounding inside line items: this is the most frequent GDT rejection cause in Odoo integrations. Odoo's multi-currency module rounds each invoice line to the currency's decimal precision (2 for USD, 0 for KHR). If your adapter converts and rounds at the line level, the sum of rounded lines does not equal the correctly-rounded total. Always sum exact amounts first, then round the total.
- Multi-warehouse stock-split journals: businesses with multiple Odoo warehouses generate stock-move journal entries that can split across different posting dates. If an invoice references stock moves from two dates and those dates span a period boundary, the VAT period assignment can be wrong. Audit multi-warehouse invoice journals before go-live to verify they post in the correct period.
- Batch posting timing: if the Odoo cron job that posts draft invoices to 'confirmed' status runs at the same time as the GDT adapter cron job, the adapter may read a partially-posted batch. Sequence the cron jobs: post-to-confirmed first, GDT adapter second, with a buffer gap.
- Python character encoding for Khmer product descriptions: Odoo stores product names in UTF-8 in the database, but if any historical data was imported from Excel or a legacy system using Windows-1252 encoding, Khmer characters may be stored incorrectly. GDT and CamInvoice both validate UTF-8 NFC encoding; garbled Khmer in a product description field is a submission failure that is slow to diagnose. Run a pre-migration encoding audit before go-live.
- Odoo Community vs Enterprise reporting gap: Community ships the 'Invoicing' app, not the full Accounting app, and the dynamic financial reports live in Enterprise. If you are on Community and need to reconcile WHT payable accounts against GDT-filed WHT amounts across multiple periods, you will be writing custom reports; the built-in views do not surface WHT payable ageing cleanly. Price that into the scoping phase rather than discovering it at month-end close.
- Credential rotation not automated: GDT credentials have an expiry [confirm the expiry period with GDT when you arrange access; it is not documented publicly]. If credential rotation is manual and the responsible person is on leave when credentials expire, the next filing deadline will fail. Build automated credential expiry alerts into the adapter and document the rotation procedure in the operations runbook.
- Upgrade path across major versions: the l10n_kh localization only exists from Odoo 18, and your GDT adapter is a custom module that must be ported to each new major release. Before committing to an upgrade, confirm both are ready for the target version. A localization or adapter gap during an upgrade means posting in a non-compliant chart of accounts, or missing a filing deadline, for that period.
Frequently asked questions
- Which Odoo version should we use for a new KH GDT deployment?
- Odoo 18 is the first release to include the official Cambodia accounting localization (l10n_kh), so it is the practical floor for a new Cambodian deployment. Odoo 19 is the current stable major release, and starting there buys the longest runway before you are forced into a version upgrade with a custom adapter to port. Either works; on Odoo 17 or earlier you inherit the same GDT adapter work plus a hand-built chart of accounts, which is why we would not start a new KH project there.
- Community or Enterprise for GDT compliance?
- Both can achieve GDT compliance, but the split matters more than it first looks. The Cambodia localization itself is LGPL-3 and ships in Community, so the chart of accounts and tax codes are not Enterprise-gated. The reporting layer around them is: in Community the accounting app is 'Invoicing' only, while the full Accounting app, the dynamic financial reports, bank synchronisation, and customer follow-ups are Enterprise. If your finance team needs Odoo to produce the period reports you reconcile against the GDT return, budget for Enterprise or for custom reports. The GDT adapter is custom either way; that cost does not change.
- Can hosted Odoo (Odoo.sh or Odoo Online) do this?
- Odoo.sh allows custom module installation, so the adapter can run there. Odoo Online (cloud SaaS) restricts custom module installation; you cannot install third-party or custom modules without upgrading to an Odoo.sh or on-premise plan. If you are on Odoo Online, a GDT API adapter is not possible in the standard plan.
- Should we use a third-party connector or build custom?
- Third-party Odoo-to-GDT connectors exist but have variable quality and limited support for Cambodia-specific requirements (WHT multi-rate, multi-currency KHR rounding, Khmer encoding). A custom adapter built to your specific chart of accounts, entity structure, and business rules is more reliable for production use. The cost difference over a 3-year horizon typically favours custom. For ERP implementation scope, see our system solutions practice. For the API integration layer, see our API integration practice.
- What about Odoo Cloud? Can it connect to GDT?
- Odoo Cloud is Odoo Online under a different name; the same module installation restrictions apply. You need Odoo.sh or on-premise for custom module support.
- How do we handle the Odoo upgrade path for the GDT adapter?
- The adapter is a custom module; it must be ported to each new Odoo major version. Build the adapter with minimal Odoo ORM dependencies to reduce the porting cost. Document the GDT field mappings and business logic separately from the Odoo-specific code so that the logic can be re-implemented cleanly during an upgrade rather than patched incrementally.
- What is the ongoing maintenance cost after go-live?
- Expect 4–8 hours per month of adapter maintenance in steady state: primarily monitoring, credential rotation, and absorbing changes on GDT's side. GDT does change how filing works from time to time, and there is no published changelog or developer mailing list to subscribe to, so the practical mitigation is an adapter that alerts on unexpected rejection codes plus someone who maintains the relationship with GDT. Budget for that rather than expecting advance notice.
- Can the same adapter cover both GDT monthly returns and CamInvoice real-time submission?
- Yes. The same integration layer can house both modules. They share upstream data extraction from Odoo, currency conversion logic, and audit trail storage. The submission flows are different (batch vs real-time), but the core infrastructure is shared. See the CamInvoice integration guide and our API integration practice for the full integration scope.