Back

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 module icon

NetSuite POS Integration

Point of Sale · Odoo 18

Invoices, returns, payments and gift cards, posted to NetSuite.

Module Info

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.

point_of_sale / cart
Odoo POS cart with one Coffee line and a total of 10.00 AED

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
posted / paid → queued

The add-on

netsuite_pos_integration

  • /api/netsuite/* controllers
  • invoice · credit note · payment sync
  • queue_job · root.netsuite
  • mappings · sync log
OAuth 1.0a · JSON
config · products · X-API-Key

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} }
Receiving from NetSuite…
Integration modeReal-time
Consolidate invoicesNo
Consolidate paymentsNo
Max concurrent jobs3
Log retention90 days

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.
closed

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.

Sells at Shop A

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.

invoice payload · headerqueued
{
  "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.

Shop A

NetSuite invoice

Consolidated 6 invoices (Shop A)

Shop B

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.

pos.payment · Cash+100.00
pos.payment · Cash (change)−5.00

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.

invoicequeued
paymentwait 1/60 · re-queue in 30s

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

NewPaidPostedInvoiced

Tea (Small)

1.00 × 6.00

6.00

Coffee (Small)

-1.00 × 10.00

-10.00
Total-4.00 AED
tender · Card · −4.00
Sync typeAmountStatus
Invoice6.00
Credit Memo-10.00
Refund (Card)-4.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

line: 0447-0562-429d_1

Purchase 200

cert 1 · _1200.00
cert 2 · _2not issued

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.

Gift card invoice · order Aposted first
Consolidated invoice · A + Bthen everything else

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

sync_pos_netsuite() · 0/4 steps done

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.

job A
account_move · 70
job B

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.

job transactionrunning…
separate cursorstatus = failed · error kept

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.

odoo / netsuite / sale
POS order NYC Shop/0003 with its NetSuite Overview tab: invoice and cash payment, both synced

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.
Technical Design DocumentData model

Log notes

Key decisions, limitations and lessons

MP

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.

MP

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.

MP

Mustafa Patharia · Design decision

Every NetSuite ID is saved immediately.

A retry continues where it stopped instead of creating duplicates.

MP

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.

MP

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

Odoo 18PythonPostgreSQLOCA queue_jobOAuth 1.0aSuiteQLNetSuite RESTletsDocker

Have something worth building?

Tell me what you're building and where it's stuck. Every message reaches me directly, and I reply personally.

Available for freelance© 2026 Mustafa Patharia. All rights reserved.