Case study · Odoo addon
Odoo POS × NetSuite
Every POS order, on NetSuite's books.
I designed and built an Odoo add-on that integrates Odoo Point of Sale with NetSuite. Each POS order is converted into the correct NetSuite documents (invoices, credit memos, customer payments, refunds and gift certificates) and posted in the sequence NetSuite requires, with no duplicates. NetSuite remains the system of record, supplying the configuration and product catalogue to Odoo.
NetSuite POS Integration
Point of Sale · Odoo 18
Invoices, returns, payments and gift cards, posted to NetSuite.
depends: point_of_sale · account · loyalty · queue_job
- Role
- Freelancer · end-to-end ownership
- Client
- InnovateNex
- Timeline
- Jul – Sep 2026 · delivered
- Built on
- Odoo 18 · NetSuite
NetSuite transaction types
Sync states, each with a rule
Automated tests
QA cases signed off
Add-on›Overview
Connecting Odoo POS to NetSuite (GCC Region)
NetSuite is one of the most widely used cloud ERPs, but its own point of sale is only offered in the US, Canada, Australia, England and New Zealand. It isn't available in the GCC. Retailers in the region who run their business on NetSuite have two options: build a POS themselves, or pair NetSuite with a third-party one. Odoo POS is a popular choice because it's fast to set up and easy for cashiers to learn.
That leaves two systems that don't talk to each other. Sales are rung up in Odoo, while the books, stock and reporting live in NetSuite. Someone has to move every sale, return and payment across by hand, and reconcile the gaps when they don't match.
My job was to connect them. I built an Odoo add-on that sends every POS transaction to NetSuite automatically and correctly. It runs inside Odoo and talks to NetSuite directly, so there's no middleware platform to license or maintain.

Each product on the order becomes a line on a NetSuite document, matched by its NetSuite item ID. Products are created in NetSuite and pushed into Odoo, so every item already has one.
- Line → NetSuite item + tax code
- Payment
Add-on›Architecture
Add-on architecture
Odoo 18
Point of Sale
- pos.order
- pos.payment · tenders
- account.move · invoices, credit notes
- loyalty.history · gift cards
The add-on
netsuite_pos_integration
- /api/netsuite/* controllers
- invoice · credit note · payment sync
- queue_job · root.netsuite
- mappings · sync log
NetSuite
RESTlets & SuiteQL
- invoice · credit memo · payment RESTlets
- payment method list
- SuiteQL · subsidiaries, locations
- configuration record · products
The add-on runs inside Odoo and communicates with NetSuite directly, with no middleware in between. NetSuite owns the configuration and product catalogue and sends both to Odoo; Odoo owns the POS transactions and sends those to NetSuite. Outgoing requests are signed with OAuth 1.0a; incoming requests are authenticated with an Odoo API key.
Add-on›Configuration
Managed via NetSuite
Finance, management and operations teams work in NetSuite daily, so I designed the integration to be configured there. NetSuite supplies the credentials, sync mode, merge rules, retry policy and concurrency limit, which Odoo stores as read-only settings. A single source of configuration ensures the two systems cannot drift apart.
POST /api/netsuite/config/update
{ "configuration": {"integration_mode": "realtime""consolidate_invoices": false"consolidate_payments": false"max_concurrent_jobs": 3"log_retention_days": 90} }A single, protected configuration. Saving the configuration in NetSuite sends it to Odoo. Odoo accepts only one configuration record; an attempt to delete it is blocked and logged.
- Each invoice and payment is queued the moment it's created and sent individually.
- Real-time can't be combined with merged invoices; the add-on rejects that setting when it arrives.
queue_job.channels = root.netsuite:3
Within NetSuite's limits. NetSuite limits how many requests an integration can make at once. The add-on reads that limit (1–4) from the configuration and sizes its job queue to match.
Add-on›Master Data Mappings
Mapping warehouses, payment methods and taxes
Most POS customers are anonymous, so the customer record cannot identify which branch a sale belongs to; the point of sale can. I implemented a mapping that resolves each order through its shop's warehouse to a NetSuite subsidiary, department and location. Payment methods and tax codes are mapped using the same approach.
POS counter
Counter 1
Operation type
Shop A: PoS Orders
Warehouse
Shop A
Warehouse
→ Subsidiary · Department · Location
POS payment method · Cash
→ NetSuite payment method (fetched live)
Sale tax · VAT 5%
→ NetSuite tax code
Turn a mapping off to see what happens.
{
"shop": "Shop A",
"subsidiary_id": "3",
"department_id": "12",
"location_id": "14",
"lines": [{ "item_id": "456", "tax_code_id": "-8" }]
}Subsidiaries, departments and locations are loaded live from NetSuite, so nobody copies IDs by hand. Each warehouse can be mapped only once.
Add-on›Invoices & Payments
Invoice and payment sync
I implemented the synchronisation of every POS invoice, and every payment recorded against it, to NetSuite. It supports two modes: real-time, where each document is sent as it is created, and scheduled, where transactions are consolidated per shop at the end of the day. Each payment is applied to its invoice, net of any change returned to the customer.
NetSuite invoice
Consolidated 6 invoices (Shop A)
NetSuite invoice
Consolidated 4 invoices (Shop B)
Daily merge per shop
In scheduled mode, each shop's invoices for the day are consolidated into a single NetSuite invoice, with lines merged by item and tax. Payments are consolidated into one customer payment per payment method, settling all of that day's invoices. Shops and days are never combined.
Change handled correctly
Odoo records change returned to the customer as a separate negative line. I designed the add-on to net these per order and payment method, so NetSuite receives the amount the customer actually paid.
Payments wait for their invoice
Where an invoice has not yet reached NetSuite, its payment is re-checked every 30 seconds for up to 30 minutes, without consuming its retry allowance.
Add-on›Returns & Exchanges
Handling returns and exchanges
A return is posted as a NetSuite credit memo. An exchange is more complex: a single order that both returns one item and sells another. The walkthrough below follows a real exchange from testing and shows how the add-on separates it into three linked NetSuite documents.
Shop A/0003 REFUND
Tea (Small)
1.00 × 6.00
Coffee (Small)
-1.00 × 10.00
01 / 06 · The order
A customer returns a Coffee (−10.00) and takes a Tea (+6.00) in one order, and receives 4.00 back on their card.
Add-on›Gift Cards
Gift card and eWallet support
I extended the integration to cover gift cards and eWallets, which customers can purchase or top up at the counter and later redeem as payment. Each purchase and top-up is recorded in NetSuite as a gift certificate, including sender and recipient details, and each redemption is drawn from the correct certificate when its invoice is posted. eWallets follow the same process, as NetSuite has no native wallet record.
Two NetSuite constraints shaped the design. Odoo gift card codes are 14 characters, whereas NetSuite accepts 9, and a NetSuite certificate cannot be topped up. I therefore issued a separate certificate for each purchase or top-up, with a code derived from the card number.
Enter any card code. It runs the add-on's own algorithm in your browser. The same card and top-up always give the same code, so no lookup table is needed.
NetSuite gift certificate
Purchase 200
Oldest balance first
When a card is used, the add-on replays its history and spends the oldest certificate first, telling NetSuite exactly how much comes from each.
Top-ups before spending
When a day is merged, gift card sales go first on their own invoice, so a certificate exists before anything in the same batch uses it.
Add-on›Sync Order
Sync order, retries and duplicate protection
NetSuite rejects a payment whose invoice doesn't exist yet, and a refund whose credit memo is missing. It also won't stop the same document from being sent twice. I built the sync so that ordering and duplicates are handled by the add-on, not left to timing.
step 1
Invoices
step 2
Payments
step 3
Credit notes
step 4
Refunds
A fixed sync order
Documents are processed in a fixed sequence: invoices, payments, credit notes, then refunds. Credit notes follow payments so that a credit cannot absorb a balance a payment was intended to settle, and a failed invoice step halts everything after it.
A: SELECT … FOR UPDATE NOWAIT
No duplicate processing
Each record is locked while it is being sent. A concurrent job attempting the same record waits, re-checks, and finds it already processed.
retry 0/3 · backoff 2^n min
Failures are never lost
When a sync fails, the job's changes are rolled back while the failure status and its reason are retained, giving support a clear record of what went wrong.
Add-on›Sync Logs
Monitoring, sync status and audit logs
Replacing manual reconciliation depends on trust in what the integration has done. I therefore gave every record a sync status governed by a clear rule, and added a view to each order listing every document it created in NetSuite. Hover over a status to see when it applies, then review the screens the client's team uses daily.
synced · Accepted by NetSuite, with its IDs saved back on the record.

A standard sale. The invoice and its cash payment are listed with their NetSuite document numbers and IDs.
What I owned
My role: end-to-end ownership
InnovateNex brought me in as a freelancer to connect Odoo POS to NetSuite for one of their clients. I owned the whole of it: the architecture, the NetSuite integration, the business rules, the back-office experience, and delivery.
Designed how every POS transaction reaches NetSuite.
- Routed documents by warehouse rather than customer, since POS customers are usually anonymous.
- Used individual POS payments as the source for NetSuite payments, instead of end-of-session totals.
- Built one ordered sync that stops as soon as a step fails.
Log notes
Key decisions, limitations and lessons
Mustafa Patharia · Design decision
Branch comes from the warehouse.
POS customers are usually anonymous, so subsidiary, department and location come from the counter's warehouse.
Mustafa Patharia · Design decision
Real-time means one record at a time.
Merging needs a full day of sales, so it only happens in the scheduled run.
Mustafa Patharia · Design decision
Every NetSuite ID is saved immediately.
A retry continues where it stopped instead of creating duplicates.
Mustafa Patharia · Design decision
Products are pushed from NetSuite.
NetSuite maintains the catalogue and sends each change, so the add-on never polls for updates.
Mustafa Patharia · Known limitation
One Odoo card, several NetSuite certificates.
NetSuite can't top up a certificate, so each load gets its own. The full Odoo card code stays on the invoice line for reconciliation.
Mustafa Patharia · Known limitation
Refunded gift card balances aren't sent back.
Refunding a gift card doesn't change certificates already in NetSuite. If that balance is later spent, the sync stops with a clear error instead of posting a wrong amount.
Mustafa Patharia · Known limitation
At most four jobs at once.
NetSuite limits concurrent requests, so the queue never runs more than four jobs in parallel.
Mustafa Patharia · Lesson learned
My first Odoo add-on.
This was the first add-on I built for Odoo, and it required a working understanding of how Odoo operates internally: its module structure, the POS data model, and its background job processing. That understanding now allows me to design and deliver future Odoo add-ons considerably more efficiently.
Stack