Odoo Van Sales & DSD documentation
A guide to Odoo Van Sales & DSD: installing the module, vans, routes, prices, credit, loading, selling, returns, collections, printing, settlement, and building and publishing the salesman app.
Documents version 1.0.0Updated Oct 29, 2026
Start here
In plain words: This package gives you a complete Van Sales and Direct Store Delivery (DSD) system for Odoo Community: an Odoo module (
taqnix_van_sales) for the depot and the office, and the Flutter source code of the iPhone and Android app your salesmen use in the street, with or without network coverage. The supervisor loads the van from the warehouse; the salesman signs the loading, follows the route of the day, sells from the van with live stock, prints the receipt on a Bluetooth thermal printer, collects cash, digital payments and post-dated checks, takes returns, and at the end of the day counts the stock and hands over the cash; the supervisor approves. Every document is a normal Odoo document: sale order, delivery, invoice, payment, credit note.Tip: No Odoo Enterprise app is needed: the module uses only Sales, Inventory and Invoicing, and OpenStreetMap for the maps (no paid map key).
What is in the package
| Item | What it is |
|---|---|
taqnix_van_sales.zip | The Odoo 19 module: vans, routes, visits with geofence, ex-van sales, returns, collections and post-dated checks, credit control, loading, settlement, receipts, supervisor map, the API of the app, Arabic translation, automated tests. |
flutter-app-source.zip | The Flutter project of the salesman app (Android and iOS): offline database and outbox, sync engine, route and visits, basket with barcode scanner and units, pricing and taxes, credit checks, payments, returns, settlement, ESC/POS Bluetooth printing (58 mm and 80 mm), GPS tracking. With a setup script, its own README and tests. |
documentation/ | This guide in English (index.html) and Arabic (index-ar.html), with PDFs, and API.md for developers. |
licensing/ | The licence (OPL-1: one purchase = one company and one app brand, updates included). |
preview-images/ | Screenshots of the app and of the Odoo screens. |
changelog.txt | History of the versions. |
How a day works
| Moment | Who | What happens | In Odoo |
|---|---|---|---|
| Morning | Supervisor | Loads the van from the warehouse (a normal internal transfer, with a wizard that fills the stock levels). | A validated transfer to the location of the van. |
| Morning | Salesman | Opens the app, reviews the loading line by line, notes differences and signs. | The load becomes Accepted or Disputed. |
| Route | Salesman | Follows the plan of the day. A visit can only start near the customer. | A visit with GPS, distance and time. |
| At the customer | Salesman | Scans or picks products in units or cases, gets the right price and tax, takes cash, a digital payment, a post-dated check or leaves it on credit, prints the receipt. | Sale order, delivery from the van, posted invoice, payments. |
| At the customer | Salesman | Takes back damaged, expired or surplus goods and collects old invoices. | Credit notes, stock back to the van or to quarantine, payments on the oldest invoices. |
| Evening | Salesman | Counts the stock, counts the cash, signs. | A settlement waiting for approval. |
| Evening | Supervisor | Compares, approves: stock is adjusted, unsold goods can go back to the warehouse, the cash handover is booked. | Inventory adjustment, transfer, accounting entry. |
In plain words: Offline first. Everything the salesman does is saved on the phone first with an unchangeable sequence number and sent to Odoo when there is a network. Each operation has a unique ID, so a lost answer or a retry never invoices a customer twice (chapter 20).
What you need
| Item | Details |
|---|---|
| Odoo | 19.0 Community or Enterprise. The apps Sales, Inventory, Invoicing and Contacts geolocation are installed automatically with the module; the pricelists feature is switched on. |
| Chart of accounts | A chart of accounts with at least one cash and one bank journal (any localization). Taxes of the kind percentage are supported. |
| Python libraries | PyJWT and requests (push notifications). pip install PyJWT requests if your Odoo does not have them. |
| PDF engine | wkhtmltopdf as for any Odoo (invoice and receipt PDF, settlement report). |
| Address | A public https address for your Odoo: the phones must reach it. A self-signed certificate does not work on phones. |
| Phones | Android 8 or newer, or iPhone with iOS 15 or newer, GPS and a camera. Salesmen should use a phone with an up-to-date battery. |
| Printers | Any portable ESC/POS thermal printer of 58 mm (2 inch) or 80 mm (3 inch) with Bluetooth. Android works with the common Bluetooth Classic models; iPhone only works with printers that offer Bluetooth Low Energy (chapter 14). |
| A computer to build the app | A Mac for iPhone and Android (Windows or Linux can build Android only), about 30 GB free, Flutter 3.41 or newer, Android Studio and Xcode. |
| Store accounts | Google Play Console and Apple Developer Program, or internal distribution to your own staff. |
| Optional | A free Firebase project for push notifications; an outgoing mail server in Odoo to email invoices. |
Install the Odoo module
- Unzip the package. Inside is
taqnix_van_sales.zip; unzip it to get the foldertaqnix_van_sales. - Copy the folder into one of the
addons_pathfolders of your Odoo server and restart Odoo. - Log in as an administrator, activate Developer mode, open Apps and press Update Apps List.
- Remove the “Apps” filter, search Van Sales and press Install.
# or from the command line ./odoo-bin -c odoo.conf -d YOUR_DATABASE -i taqnix_van_sales --stop-after-init
In plain words: After the installation a new app Van Sales appears in the main menu. The installation also creates the parent stock location Vans and a Return quarantine location, the payment modes Cash, Digital / QR, Post-dated check and On credit (with your first cash and bank journals), return reasons, no-sale reasons and the settlement and return sequences. You can change all of them.
Important: Take a backup before installing on a production database, as for any module.
First setup in 15 minutes
- Open Van Sales → Configuration → Settings and walk through the sections (chapter 10). The defaults fit most distributors.
- Check the payment modes (Configuration → Payment modes): which journal receives digital payments and post-dated checks.
- In Settings → Settlement choose the cashier journal and the cash difference account.
- Create the vans and their salesmen (chapter 7).
- Prepare the products (units, cases, barcodes, taxes), the pricelists and the customers with their GPS position (chapters 8 and 9).
- Create the routes (chapter 9) and load the first van (chapter 11).
- Build the app (chapter 21), sign in as the salesman and sell something.
Tip: Customers need their GPS position for the geofence: open the customer, press the geolocation button of the address, or import latitude and longitude. A customer without a position can be visited from anywhere.
Vans, salesmen, supervisors
In plain words: There are three groups in Settings → Users → Van Sales:
| Group | Can | Uses |
|---|---|---|
| Salesman | Sign in to the mobile app of the van that is assigned to them. In Odoo they see only the documents of their own van. | The phone |
| Supervisor | Loads vans, plans routes, follows the map, reviews flagged sales, approves settlements, handles checks. Sees all vans. | Odoo in the browser |
| Manager | Everything of the supervisor plus the settings, payment modes, reasons and devices. | Odoo in the browser |
In plain words: Open Van Sales → Planning → Vans and press New: the name, a short receipt prefix (for example
V01: it starts every receipt number printed by this van), the salesman (an Odoo user; the group Salesman is added automatically), the supervisor who receives the to-dos, the warehouse, and whether the van can visit customers outside the plan or create new customers. The module creates the stock location of the van under Vans; press Create cash journal for the cash the salesman collects.Tip: Sign out all devices on the van revokes every phone at once. One salesman drives one van; to change the salesman, change the field on the van.
Products, units, prices and taxes
In plain words: The app sells the products of Odoo that are sellable, not lot or serial tracked and have Sold from vans ticked (a box on the product form, on by default). Everything below is standard Odoo.
| What | Where in Odoo | In the app |
|---|---|---|
| Units and cases | Product → Sales tab → Packagings (a case is a unit of measure of 6, 12, 24... base units, add it to the product). | The salesman switches the line between Units and Case; stock moves, prices and receipts follow. |
| Barcodes | Product barcode, and a barcode for each packaging. | Scanning a unit barcode adds a unit, scanning a case barcode adds a case. |
| Pricelists | Sales → Products → Pricelists. Rules per product, category or all products: fixed price, discount, formula; minimum quantities. | The price of the customer's pricelist, with quantity breaks, even offline. |
| Taxes | Product → Customer taxes (percentage taxes, included or excluded in the price) and fiscal positions. | Tax per line, per customer fiscal position (for example exempt customers). |
| Stock | Inventory of the van location. | Live van stock: a sale can never exceed it. |
In plain words: How prices reach the phone. Odoo computes the price of every product for every pricelist at the quantities where the rules change (the tiers) and sends them with the data; the phone multiplies the tier price of one base unit by the unit factor of the line. Rules that only apply to specific dates or to groups of customers are respected at the time of the sync. If the price of the receipt differs from the pricelist by more than the tolerance, the sale is flagged for the supervisor (chapter 20).
Customers, credit and routes
In plain words: A customer of a van is any customer on one of its routes (and, when unplanned visits are allowed, every customer with the customer rank). The tab Van sales of the customer form holds what is specific to the street:
| Field | Meaning |
|---|---|
| Credit control | As in the settings, none, soft (the app warns, the sale is flagged for the credit controller) or hard (the app refuses credit sales over the limit; cash sales are still possible). |
| Credit limit | Maximum receivable balance of the customer (post-dated checks that are not cleared yet are still inside it, because their invoices stay open). 0 = the default limit of the settings. |
| Blocked for van sales | The salesman cannot sell to this customer from the app (with a reason shown on the phone). |
| Visit radius | A distance in meters that replaces the default of the settings for this customer (a large shop, a mall...). |
| Routes | The routes where the customer appears, and their days. |
In plain words: Routes (the permanent journey plan). Van Sales → Planning → Routes: a route belongs to a van and lists customers in visiting order with the weekdays (Monday to Sunday), every week, odd weeks or even weeks and an optional start and end date. The phone receives the plan of the next days (7 by default) and marks customers as done when a visit exists.
Tip: New customers created in the app wait for your approval (button Approve this customer) and can only buy for cash until then.
The settings, section by section
In plain words: Van Sales → Configuration → Settings holds every rule of the company. Each block says what it does and when to change it.
| Setting | What it does |
|---|---|
| Unplanned visits, new customers, orders for later | Which extra actions a salesman may take. Unplanned and new customers can also be switched off per van. |
| Days of the plan | How many days of the plan the phone receives (default 7). |
| Geofence | A visit can only start within the allowed distance (default 100 m) and with a GPS accuracy better than the limit (default 50 m). With override the salesman can start anyway by writing a reason; the visit is flagged and the supervisor gets a to-do. |
| Setting | What it does |
|---|---|
| Salesmen can change prices | Off by default: the price is the one of the pricelist. |
| Maximum discount | A discount up to this percentage can be given on a line. |
| Price tolerance | A sale whose unit price differs from the pricelist by more than this percentage is flagged price_mismatch. |
| Setting | What it does |
|---|---|
| Credit sales allowed | If off, every sale must be paid on the spot. |
| Default credit control and limit | Used by customers that do not have their own. |
| Overdue days | Refuse credit when an invoice is overdue by more than this (0 = do not check). |
| Credit controller | Receives the to-do when a sale exceeds the limit. |
| Setting | What it does |
|---|---|
| Parent of the van locations / quarantine location | Where vans live and where damaged and expired returns go. |
| Signature on loading / on every sale | Ask the salesman to sign the loading, the customer to sign each sale. |
| Returns | Allow returns; require a photo for damaged goods. |
| Setting | What it does |
|---|---|
| Count required, tolerances | The salesman must count the stock; cash and stock differences above the tolerance are highlighted. |
| Stock differences | Adjust the van stock to the count when the settlement is approved, or only record the difference. |
| Cashier journal and difference account | The cash handover is booked from the van's cash journal to the cashier journal; shortages and surpluses go to the difference account. |
| Empty van | Send the unsold stock back to the warehouse on approval. |
| Setting | What it does |
|---|---|
| Paper width | 58 mm (2 inch) or 80 mm (3 inch); the salesman can also choose on the phone. |
| Print mode | Image: the receipt is drawn as a picture (any language including Arabic, your logo, the QR code). Text: ESC/POS text, the fastest, Latin only. |
| Header, footer, tax label | Lines printed on every receipt; the tax ID of the company comes from the company record, its label (TRN, VAT no...) from here. |
| Show tax column, balance, QR code | What the receipt shows. The QR code opens the public verification page of the invoice. |
| Setting | What it does |
|---|---|
| Tracking | Off, between working hours, or always while the app is open. Intervals while moving and still, minimum distance and battery limit make it battery friendly. Points are kept for 30 days by default. |
| Privacy text | Shown in the app before the location permission is asked. |
| Setting | What it does |
|---|---|
| Push notifications | Van loaded, plan changed, settlement decided. The Firebase project and the service account are entered here (chapter 21). |
| Sync interval | How often the app syncs in the background (default 120 seconds). |
| Business | Suggested settings |
|---|---|
| Beverage and water distributor | Cases and units, quantity-break pricelists, hard credit limits for small shops, digital payments, quarantine for broken bottles, empty van every evening off. |
| Snacks, FMCG and confectionery | Many lines per visit: barcode scanning, discount limit 5 %, soft credit, price tolerance 1 %, receipts in image mode. |
| Dairy, bakery and fresh food | Expiry returns (reason Expired goes to quarantine), empty van every evening on, settlement with stock count required, stock tolerance 0 %. |
| Ice cream and frozen | Empty van on, strict stock count, cash and digital only (credit sales off), 80 mm receipts. |
| Pharmacy and healthcare wholesale | Signature on every sale, hard credit control, post-dated checks, no unplanned visits, no price editing. |
| Cash vans (no credit) | Credit sales allowed off: every receipt is paid in full; the settlement is only about cash and stock. |
Loading the van and the morning signature
In plain words: Van Sales → Depot → Van loadings lists the transfers into the vans and their state: Waiting for the salesman, Accepted, Disputed. To load a van press Load the van on the van (or in the list): choose the warehouse location, add products and quantities, or press Fill up to the stock levels to add what is missing to reach the levels you defined on the van (Stock levels tab: the quantity to load up to and an alert level).
- Check the quantities. Load creates and validates the internal transfer (untick Validate the transfer at once to let the storekeeper do it).
- The salesman gets a push notification, opens Load in the app, checks every line (product, unit, quantity), changes the received quantity where it differs, writes a note and signs.
- If everything matches the load is Accepted; with differences it is Disputed and the supervisor gets a to-do. The difference lines are on the Van loading tab of the transfer.
A day in the app
In plain words: The app is built for a salesman who is standing in a shop with one hand free. Big buttons, one flow, and no waiting for the network.
| Step | What the salesman does | Rules |
|---|---|---|
| Route | Opens a customer from the list or the map. | The balance and overdue amount of the customer are shown before the visit. |
| Start the visit | Presses Start visit at the shop. | Allowed only inside the radius with a good GPS; otherwise the screen shows the distance and, if the company allows it, an override with a reason path. |
| Sell | Adds products by search, category, barcode scan or the customer's favourites; changes quantity, unit (case or unit), discount within the limit. | A line can never exceed the live van stock. Prices and taxes come from the pricelist and fiscal position of the customer. |
| Pay | Splits the total across cash, digital (reference), post-dated check (number, bank, date, photo) and credit. | Credit is blocked for hard-limit customers over the limit or with overdue invoices, with a clear message. |
| The receipt prints on the Bluetooth printer; a copy can be shared as PDF or image. | Receipt number = van prefix + local sequence number. | |
| Collect | Takes payment of old invoices from the same screen. | Oldest invoices first, or the invoices the salesman chooses. |
| Return | Takes back goods with a reason and, if needed, a photo and an expiry date. | Resellable goods go back into the van stock; damaged and expired goods do not. |
| End the visit | Chooses the outcome: sale, no sale (with a reason), shop closed, skipped. | Visits without a sale are recorded with their reason. |
Selling: products, barcode, units and payment
In plain words: Barcode scanning uses the camera. Scan a unit barcode to add one unit, scan the barcode of a case to add one case; scanning the same code again adds one more. The scanner closes by itself when you are done and the basket keeps the lines.
| Rule | Detail |
|---|---|
| Stock | The quantity of a product in the basket cannot exceed what is in the van now (server stock minus the sales not yet synced). The sale is applied to the device stock immediately. |
| Units | A line has a unit; changing it re-prices the line (tier price of one base unit × the factor of the unit). Stock moves are always computed in the unit of the line. |
| Prices and discounts | The price comes from the pricelist of the customer; a discount is limited by the settings; the price can only be edited if the company allows it. |
| Taxes | Percentage taxes, included or excluded, mapped by the fiscal position of the customer; rounded per line. The tax IDs used are sent to Odoo so the invoice shows exactly what the receipt shows. |
| Orders for later | If allowed, Book an order confirms an order without taking stock from the van and without an invoice; the warehouse delivers it. |
Receipts and Bluetooth printing
In plain words: The app prints on ESC/POS thermal printers of 58 mm (2 inch, 384 dots) and 80 mm (3 inch, 576 dots). The ESC/POS commands are generated by the app itself.
| Block | Content |
|---|---|
| Header | Logo, distributor name, address, phone, tax ID (TRN) and the header lines of the settings. |
| Document | Receipt number, invoice number (on a reprint after the sync), date and time, salesman. |
| Customer | Name, code, address, tax ID. |
| Lines | Product, quantity and unit, unit price, tax, line total. |
| Totals | Untaxed, tax, total. |
| Payment | Each payment line (cash, digital with reference, check with number and date, credit), paid, outstanding balance of the customer. |
| Verification | A QR code. After the sale is synced it opens the public verification page of the invoice (genuine document, number, date, total, masked customer name); before the sync it carries the receipt number, the tax ID, the total and the time. |
| Footer | The footer lines and the number of copies. |
- Switch the printer on and pair it in the phone's Bluetooth settings if your printer needs it (Android). In the app open Settings → Printer.
- Press Search, choose your printer, choose the paper width (58 or 80 mm) and press Test print.
- The app remembers the printer and reconnects by itself. After a sale the receipt prints automatically; Receipts in the menu lets you reprint or share any receipt.
| Mode | When to use it |
|---|---|
| Image (raster) | Default. Prints Arabic, other languages, your logo and the QR code exactly as the preview. A little slower (a few seconds for a long receipt). |
| Text | Fastest and uses the built-in font of the printer: English and numbers only. |
Important: iPhone and Bluetooth printers. iOS only talks to Bluetooth Low Energy devices, and most cheap portable printers are Bluetooth Classic (SPP), which iOS cannot use. On iPhone choose a printer that supports BLE, or print to a PDF/AirPrint. On Android the common portable printers work. The app does not ship drivers: a printer that understands ESC/POS and the raster command works; try a test print before buying a fleet.
Returns, damages and expiry
In plain words: In the app press Return on a customer, add products (from an invoice of the customer or free), choose a reason for each line and press Save. The reason decides where the goods go:
| Reason type | Stock | Accounting |
|---|---|---|
| Resellable (wrong item, overstock) | Back into the stock of the van. | A credit note for the customer. |
| Damaged | To the quarantine location; a photo can be required. | A credit note. |
| Expired (with the expiry date) | To the quarantine location. | A credit note. |
In plain words: The credit note is posted and matched with the open invoice of the return when the invoice is given, otherwise it stays as credit on the customer account. Cash refund also pays the customer from the cash journal of the van, which lowers the cash to hand over. The supervisor sees the goods in quarantine in Operations → Returns and presses Scrap damaged and expired goods when they are destroyed.
Collections and post-dated checks
In plain words: Collect on a customer lists the open invoices (oldest first). The salesman enters the amount and the payment mode; the app allocates it to the oldest invoices (or to the invoices he ticks), and any remainder stays as credit on the customer account.
| Mode | What is recorded in Odoo |
|---|---|
| Cash | A customer payment on the cash journal of the van, reconciled with the invoices. |
| Digital / QR | A customer payment on the journal of the mode (bank), with the reference the customer gave. |
| Post-dated check | Not a payment yet: a record in Operations → Post-dated checks with the number, the bank, the check date and the photo. The invoice stays open and counts in the balance of the customer. When the bank pays, press Cleared by the bank and Odoo registers the payment on the invoices; Bounced warns the supervisor. |
| On credit | The invoice stays open and the amount counts on the customer balance. |
End of day: count, cash and approval
In plain words: At the end of the route the salesman opens End of day. The flow has four steps and works offline:
- Summary of the day: sales, returns, cash, digital and checks collected, credit given, visits.
- Stock count: every product that is in the van or was sold today; type or scan the counted quantity. The app shows the difference in units and in value.
- Cash handover: the expected cash (cash collected minus cash refunds, since the last settlement), the declared amount and an optional count by banknote; the difference is shown.
- Sign and submit. After submitting, the day is closed on the phone until the supervisor decides.
In plain words: The supervisor opens Depot → Settlements. Differences above the tolerance are highlighted. Approve does, in one step: the stock of the van is adjusted to the count (if the setting says so), the unsold stock goes back to the warehouse (if the setting says so), the day's payments are marked as settled and the cash handover is booked (debit cashier journal, credit van cash journal; a shortage or surplus goes to the difference account). Reject asks the salesman to count again.
Credit control explained
| Situation | On the phone | In Odoo |
|---|---|---|
| Cash sale paid in full | Always allowed (unless the customer is blocked). | No credit check. |
| Credit sale, customer without a limit or mode none | Allowed. | Nothing. |
| Credit sale, soft, over the limit | A warning; the salesman confirms. | The sale is flagged credit_exceeded; the credit controller gets a to-do. |
| Credit sale, hard, over the limit or with overdue invoices | Credit is refused; a cash sale is still possible. | Not applicable: nothing can reach Odoo. |
| Post-dated checks | Their invoices stay open and count in the balance. | Counted in the balance until the bank clears the check; shown separately as pending checks. |
| Blocked customer | Sales are refused with the reason. | If a sale arrives anyway (old data), it is accepted and flagged blocked_customer. |
In plain words: Why the phone decides. Hard limits can only work in the shop if the phone knows the balance: the balance of the last sync plus the sales and payments not yet synced is used, so two salesmen selling to the same shop between syncs can exceed the limit slightly; Odoo flags it afterwards. A sale that already happened is never rejected by the server for credit, price, stock or geofence reasons: it is recorded and flagged for the supervisor.
The supervisor map, visits and reports
In plain words: Operations → Van map shows the vans (with their salesman, battery, visits done against planned and the sales of the day) and today's customers as numbered pins colored by status: to visit, visiting, sale, no sale, shop closed, skipped. Select a van to list its stops and draw today's route from the recorded positions.
| Where | What you see |
|---|---|
| Operations → Visits | Every visit with the distance from the customer, the time on site, whether it was planned and whether it was inside the radius. Filters: today, with a sale, without a sale, outside the radius, not on the plan. |
| Reporting → Visit analysis | Visits by van and outcome: the strike rate (visits with a sale against visits) and the compliance to the plan. |
| Operations → Sales | Van sales and booked orders; the filter To review shows the flagged ones; On credit the open ones. |
| Reporting → Sales analysis | Sales by van, day, product and customer (pivot and graph). |
| Reporting → Location history | The recorded GPS points (time, accuracy, speed, battery, network). |
Offline, sync and what the flags mean
In plain words: The outbox. Every action is written to the database of the phone together with an operation: a unique ID (UUID), a type, the time of the phone and an immutable sequence number (it only goes up, stays after a restart and continues from the highest number Odoo has seen if the app is reinstalled). The sync sends the operations in order (100 per call), Odoo applies each one once and answers applied, duplicate (already seen: the stored result is returned), rejected (with a code) or conflict; then the phone downloads what changed. If the answer is lost, the retry is harmless.
| Flag in Odoo | Meaning | What to do |
|---|---|---|
| price_mismatch | The unit price differs from the pricelist by more than the tolerance. | Check the sale; fix the pricelist or coach the salesman. |
| discount_exceeded | A discount above the maximum of the settings. | Check the sale. |
| tax_mismatch | The phone used other taxes than Odoo; the invoice uses the taxes of Odoo. | Update the app data (sync) and check the fiscal position of the customer. |
| total_mismatch | The invoice total differs from the total the phone printed. | Compare the taxes and rounding settings. |
| credit_exceeded | The customer is over the credit limit or has overdue invoices. | The credit controller decides; the invoice is already posted. |
| outside_geofence | The visit started outside the radius (override reason on the visit). | Review the reason; fix the GPS position of the customer if it is wrong. |
| negative_stock | The sale took more than the van had (stock went negative). | A loading was not recorded or the count is wrong; the settlement will show it. |
| late_sync | The operation reached Odoo more than 12 hours after it happened. | Information only. |
| blocked_customer | A sale to a blocked customer. | Check why. |
| return_exceeds_invoice, return_price_mismatch, invoice_mismatch, return_without_invoice | A return larger or dearer than the invoice it refers to, an invoice of another customer, or a return without an invoice. | Check the return before paying a refund; a cash refund without an invoice is flagged. |
Tip: Rejected operations never create half a document: if an operation fails, nothing of it remains in Odoo, and the phone keeps it with the reason until you fix the data and retry, or discard it on purpose.
Build and publish the app
In plain words: Skip this chapter if you only want to test:
flutter run works on a simulator or a connected phone.- Install Flutter (3.41 or newer), Android Studio, and on a Mac Xcode with CocoaPods; run
flutter doctorand fix what it reports. - Unzip
flutter-app-source.zipin a folder without spaces. - Run the setup script with your Odoo address and your own package name:
dart run tools/setup.dart --url https://odoo.example.com --package com.yourcompany.vansales. Add--name "Your Company Vans",--color "#166534"and--icon your-icon-1024.pngto set the app name, the brand color and the icon (the script is safe to run again). - Run
flutter pub get, thenflutter runto try it, signing in with a salesman of your Odoo.
cd flutter-app dart run tools/setup.dart --url https://odoo.example.com --package com.yourcompany.vansales flutter pub get flutter run --dart-define=SITE_URL=https://odoo.example.com
| Target | Command | Then |
|---|---|---|
| Android (Google Play) | flutter build appbundle --release | Upload the .aab in the Play Console. Create the signing key once and keep it safe. |
| Android (internal, no store) | flutter build apk --release | Send the .apk to your salesmen (allow installation from this source). |
| iPhone (App Store, TestFlight) | flutter build ipa --release | Upload with Xcode or Transporter; create the App ID with Push Notifications and Background Modes (location updates). |
Important: Both stores ask to justify the background location: use the sentence The app records the position of the salesman during working hours to prove customer visits and plan routes, and publish a privacy policy. On Android 12 and newer the app asks for the Bluetooth permission to find the printer; on iPhone it asks for Bluetooth access. The app shows its own explanation before each permission.
In plain words: Push notifications (Firebase): create a Firebase project, add an Android and an iOS app with your package names, upload your APNs key for iOS in Project settings → Cloud Messaging, enter the project ID, the API key and the app IDs in Odoo (Settings → Notifications and sync) and paste a service account JSON. The app reads the identifiers from your server when it signs in, so the source code contains no key.
Security and privacy
| Topic | How it is handled |
|---|---|
| Access | The app signs in with the Odoo login; only users of the group Salesman with an assigned van. A salesman sees only the customers of the routes of the van (and all customers if unplanned visits are allowed). In Odoo a salesman sees only the documents of the own van and cannot change anything. |
| Tokens | A random token per device, only its hash is stored in Odoo, devices unused for 180 days are forgotten. Devices lists them; Sign out all devices on the van revokes them. Odoo slows down repeated wrong passwords. Accounts with two-factor authentication cannot sign in to the app. |
| Operations | Every operation is checked on the server: the customer, products, units, taxes, payment modes and documents must exist in the company and belong to the van of the user; numbers must be finite and in range; images are limited to 6 MB. |
| Idempotency | A document is created once per unique ID even if the phone retries; a settlement of a day is created once. |
| Verification page | The QR code of a receipt opens /vs/verify/<token>: it shows only the company, the invoice number, the date, the total and the masked customer name, and not found otherwise. The token is random and long. |
| Location | Collected only according to the setting, with a privacy text and the system permission; points older than the retention (30 days by default) are deleted every day. Customers never see the position of the salesman. |
| Secrets | The Firebase service account is readable only by Odoo administrators. The app contains only public identifiers. |
Tip: In some countries employee location tracking needs a written notice or consent: use the privacy text of the settings and your own policy.
Good to know
| Limit | Detail |
|---|---|
| Promotions and loyalty | Not included: the app sells at pricelist prices with a limited manual discount. Free goods and bundle offers are not computed. |
| Lot and serial numbers | Products with lot or serial tracking are not offered in the app. |
| Price rules | Prices follow the pricelist at the quantity breaks it defines. Very complex formulas (based on another pricelist, rounding rules by customer group) are taken from what Odoo computes for the product; check a sample. |
| Taxes | Percentage taxes only (included or excluded). Other tax types (fixed amounts, groups, taxes computed by Python) are not supported on the phone. |
| Several currencies | The app sells in the currency of the company. |
| Credit limits on two phones | The limit is checked on the phone with the data it has; Odoo flags the excess afterwards. |
| iPhone and printers | Only Bluetooth Low Energy printers (chapter 14). |
| Background location | Android and iOS may stop apps to save the battery. Ask salesmen to allow location always and disable battery optimisation for the app; points wait on the phone until the network is back. |
| Odoo version | Built and tested on Odoo 19.0. |
For developers
In plain words: Skip this chapter if you only use the product. The complete contract of the app API is in
documentation/API.md.# is the server reachable?
curl https://odoo.example.com/api/vs/v1/ping
# {"ok": true, "api_version": 1, "server_time": "2026-10-26T08:00:00Z"}
# sign in (returns a token, the van and the configuration)
curl -X POST https://odoo.example.com/api/vs/v1/auth/login -H 'Content-Type: application/json' \
-d '{"login": "ahmed@example.com", "password": "...", "device": {"id": "abc", "platform": "android"}}'
# push the queued operations and pull the changes (every operation has a uuid and an immutable local_seq)
curl -X POST https://odoo.example.com/api/vs/v1/sync -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"cursor": null, "include_master": true, "ops": [{"uuid": "9d1c...", "type": "visit_start", "local_seq": 41, "device_time": "2026-10-26T08:01:00Z",
"payload": {"visit_uuid": "5f0e...", "partner_id": 7, "planned": true, "lat": 25.2, "lng": 55.27, "accuracy": 8}}]}'| Area | Endpoints under /api/vs/v1 |
|---|---|
| Session | GET /ping, /config, /auth/me; POST /auth/login, /auth/logout, /auth/push-token |
| Sync | POST /sync (operations: visit_start, visit_end, van_sale, order_booking, return, payment, load_accept, settlement_submit, customer_create) |
| Tracking | POST /tracking/batch (also /api/vs/tracking/batch) |
| Data | GET /partners/<id>/credit, /products/<id>/image, /company/logo |
| Documents | GET /orders/<id>/receipt, /orders/<id>/invoice.pdf, /orders/<id>/receipt.pdf; public: GET /vs/verify/<token> |
In plain words: Calls send
Authorization: Bearer <token> and optionally Accept-Language. Every operation of /sync has a unique uuid and an immutable local_seq and is answered applied, duplicate, rejected or conflict. The module has 110+ automated tests (--test-tags /taqnix_van_sales) and the app has unit tests (flutter test).Troubleshooting
| Problem | What to do |
|---|---|
| The app cannot connect | Open https://your-odoo/api/vs/v1/ping on the phone: it must answer {"ok": true}. Check https, the certificate, the address without a trailing slash and that the module is installed on that database (use the dbfilter setting if you have several). |
| Sign-in says the account is not a salesman, or has no van | Give the user the group Van Sales: Salesman and assign the user to a van. |
| The route is empty | The van needs a route with customers on today's weekday; check odd/even weeks and the date range of the lines. |
| A visit cannot start | The customer needs a GPS position; the phone needs a GPS fix better than the accuracy limit and to be inside the radius. Move closer, or let the salesman use the override with a reason, or raise the radius for this customer. |
| A product is missing | It must be sellable, not tracked by lot or serial, have Sold from vans ticked, belong to the company, and the phone must have synced after the change. |
| The price is not the one of the pricelist | Check the pricelist of the customer (a customer without a pricelist uses the default of the van) and that the pricelist feature is on. The phone refreshes pricelists at every sync. |
| Stock is short in the van | The sale is limited to the van stock. Load the van, or accept the load if it is still waiting for the signature. |
| The printer is not found | Switch it on, keep it near, allow the Bluetooth permission, and on iPhone use a Bluetooth Low Energy printer. Try Share as PDF meanwhile. |
| The receipt prints garbage or Arabic is wrong | Use Image mode; in Text mode the printer's own font is used. |
| The settlement says the day is closed | A settlement of that day was already submitted. Ask the supervisor to reject it if the salesman must count again. |
| Sale in the sync issues | Open the entry for the reason (unknown product, unit not valid, period closed...). Fix the data in Odoo and press Retry. |
| No push notifications | Check the Firebase identifiers and the service account in the settings, the APNs key for iPhone, and that the user allowed notifications. |
Support
Installation help and bug fixes are included for 6 months, with lifetime updates. Contact us through the marketplace page where you bought the module or write to support@taqnix.com with your Odoo version and a screenshot.