Odoo Mobile WMS & Barcode Scanner (Paperless Warehouse) documentation
A guide to Odoo Mobile WMS & Barcode Scanner: installing the module and the app, setting up Zebra, Honeywell and Datalogic scanners, bins and check digits, GS1 and barcodes, receipts, deliveries, moves, waves, lots and expiry, cycle counts, offline sync, labels, reports, the API, limits and troubleshooting.
Documents version 1.0.0Updated Oct 29, 2026
Start here
taqnix_wms_barcode) with the barcode engine, the operations, the waves, the cycle counts and the audit log, and the Flutter source code of a rugged-scanner app for Zebra, Honeywell and Datalogic devices, with the camera of a normal phone as a fallback (346 Dart tests and 53 Kotlin tests, run end to end on the iOS simulator against this module; not run on a real rugged scanner, see chapter 31). Operators receive, pick, move and count with the trigger of the scanner. Nobody types in a text field.stock, barcodes and the GS1 nomenclature that Odoo Community already ships.What is in the package
| Item | What it is |
|---|---|
taqnix_wms_barcode.zip | The Odoo 19 module: the barcode resolver (GS1, EAN, UPC, packaging, weight and price barcodes), the scan engine for receipts, deliveries and internal moves, waves along an optimized path, cycle counts, bin check digits and pick sequence, labels, the scan log, the API of the app, Arabic translation, automated tests. |
flutter-app-source.zip | The Flutter project of the scanner app for Android (hardware scanners and camera) and iOS (camera), with its tests, README, changelog and docs/ARCHITECTURE.md. See chapters 6 to 8 and 26. |
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, updates included). |
preview-images/ | Screenshots of the Odoo screens and of the app. |
changelog.txt | History of the versions. |
How it works
| Moment | Who | What happens on the scanner | In Odoo |
|---|---|---|---|
| Receiving | Operator | Opens the receipt, scans the staging bin, scans the products (each scan adds one, a case barcode adds the whole case). Lots, serials and expiry dates are asked only when the product needs them. | The scans are kept on the lines of the transfer. Nothing moves yet. |
| Receiving | Operator | Presses Validate. If something is missing the scanner asks: backorder or not. | The receipt is validated, lots are created, a backorder is created if asked. |
| Put-away | Operator | Opens the storage transfer, scans the pallet (one scan moves the whole package) and the destination bin, types the two check digits printed on the bin label. | An internal transfer is validated. The quants of the pallet move exactly. |
| Picking | Operator | Opens a delivery or a wave, follows the path bin by bin (the next bin is shown in large amber letters), scans the bin, the product and the lot. | Move lines get the scanned quantity; a wrong item, an expired lot or too much is refused with a buzzer. |
| Counting | Operator | Scans the shelf, scans what is on it. A blind count never shows the expected quantity. | Counts keep the expected quantity frozen at the start, the live stock and the drift. |
| Approval | Manager | Reviews the differences in Odoo and approves. | One inventory adjustment per line, once. |
What you need
| Item | Details |
|---|---|
| Odoo | 19.0 Community or Enterprise. The module depends on Inventory (stock), product, barcodes, the GS1 nomenclature (barcodes_gs1_nomenclature) and mail. Purchase and Sales are not required; the receipts of a purchase order are used when Purchase is installed. |
| Locations | Bins as stock locations. Multi-step locations, several warehouses, lots and serial numbers, packages and units of measure with barcodes all work; switch on the Inventory settings you use (the module activates the internal transfer operation type). |
| Address | A public https address for your Odoo (or your VPN) that the scanners can reach. A self-signed certificate does not work in the app. |
| Scanners | Android 7.0 or newer. Rugged: Zebra (DataWedge), Honeywell (Data Intent), Datalogic (DecodeWedge), or any scanner that sends an intent or keys. Any Android phone or iPhone (iOS 15.5 or newer) works with the camera. To build the app: Flutter 3.41. |
| PDF engine | wkhtmltopdf, as for any Odoo, to print the bin and package labels. |
Install the Odoo module
- Unzip the package. Inside is
taqnix_wms_barcode.zip; unzip it to get the foldertaqnix_wms_barcode. - 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 Mobile WMS and press Install.
# or from the command line ./odoo-bin -c odoo.conf -d YOUR_DATABASE -i taqnix_wms_barcode --stop-after-init
Install the app on the scanners
taqnix_wms_scanner, application id and bundle id com.taqnix.wmsscanner, display name Taqnix WMS Scanner. It uses no Firebase and no push service: the only thing to configure is the address of your Odoo.- Unzip
flutter-app-source.zipand open a terminal in the folder. - Fetch the packages:
flutter pub get. The database layer (Drift) is already generated and included; rundart run build_runner build --delete-conflicting-outputsonly if you change the tables. After editing the texts of the app,python3 tool/l10n_src.py && flutter gen-l10nregenerates the translations. - Check the project:
flutter analyzeandflutter test(the suite is described in chapter 26). - Run it on a connected Android device or an emulator:
flutter run --dart-define=SITE_URL=https://odoo.example.com.SITE_URLis optional: without it the sign-in screen shows a Server address field. - Build the Android app for the rugged devices:
flutter build apk --release --dart-define=SITE_URL=https://odoo.example.com. Install the APK by USB, by your MDM (Zebra StageNow, Honeywell Enterprise Provisioner, SOTI...) or from a download link. The release APK is signed with the debug key until you createandroid/key.propertiesas in the standard Flutter signing guide: do that before you distribute it. - Build the iPhone app on a Mac with Xcode:
flutter build ios --release --dart-define=SITE_URL=https://odoo.example.com(iPhones use the camera, see chapter 8). - Open the app. Sign in with the login and password of a user of the group Mobile WMS / Operator. The app downloads the master data (products, packaging barcodes, bins, pallets, lots) and is ready to scan, even without network from then on.
- In Settings > Scanner check the scanner profile (chapters 7 and 8).
| Permission | Why |
|---|---|
INTERNET, ACCESS_NETWORK_STATE | Talk to Odoo and know when the network is back. |
CAMERA (optional) | Camera scanning when there is no hardware scanner. |
VIBRATE | The vibration of the scan feedback. |
WAKE_LOCK | The screen stays on while the app is open. |
<queries> | Declared for the DataWedge, Honeywell and Datalogic packages, so that Android 11 or newer lets the app see them. |
python3 tool/fake_server.py --port 8765 (Python standard library only, 60 demo products, receipts, a delivery, a wave and two counts, logins demo / demo and manager / manager), then flutter run --dart-define=SITE_URL=http://127.0.0.1:8765. Debug builds can also show a thin Type a barcode to inject bar above the scan area (it is visible in the screenshots of this guide); release builds never show it. Settings > Scanner test always has one.Scanner setup: Zebra DataWedge
TaqnixWMS at start-up through the DataWedge API (CREATE_PROFILE then one SET_CONFIG); the button Create DataWedge profile does the same on demand. The profile has barcode input on, intent output on (action com.taqnix.wms.SCAN, category android.intent.category.DEFAULT, delivery broadcast), keystroke output off, and is associated with com.taqnix.wmsscanner for all its activities. The scanner plugin is switched off when the app goes to the background and on again when it comes back, so the trigger does not scan for another app. The action can be changed in DataWedge output action.- Open the DataWedge app and tap New profile; call it
TaqnixWMS. - Under Associated apps add
com.taqnix.wmsscannerwith activity*. - Under Barcode input keep Enabled and tick the symbologies you use: EAN-13, EAN-8, UPC-A, UPC-E, Code 128, GS1-128, DataMatrix and QR if you label with them.
- Under Keystroke output switch Enabled off: the scan must not be typed anywhere.
- Under Intent output switch Enabled on. Intent action
com.taqnix.wms.SCAN(or the one you typed in the app), categoryandroid.intent.category.DEFAULT, Intent delivery: Broadcast intent. - Test: open Settings > Scanner test in the app and pull the trigger. The code, the symbology and the raw intent appear on the screen.
com.symbol.datawedge.data_string and the label type, with the older com.motorolasolutions.emdk.datawedge names too). With multi-barcode scanning on, every code of the same read arrives as a scan of its own.SET_CONFIG bundle follow the Zebra documentation and are unit tested on the parsing side. They have not been run on a real Zebra device by the author (chapter 31). Check the first trigger pull on your own device, and if DataWedge refuses the automatic profile (DataWedge 6.x and 8.x differ), use the manual steps.Scanner setup: Honeywell, Datalogic and others
| Device | Where | Settings |
|---|---|---|
| Honeywell (EDA, CT, CK, Dolphin) | Settings app > Honeywell Settings > Scanning > Internal scanner > Default profile > Data processing settings | Switch Data Intent on (the Intent API; older images call it Data Collection Service). The app listens to com.honeywell.decode.intent.action.EDIT_DATA and com.honeywell.sample.action.BARCODE_DATA and reads the extras data (or dataBytes), aimId, codeId and charset. Switch Keyboard output off, or leave the wedge on: the key-burst assembler below also works. |
| Datalogic (Memor, Skorpio) | Datalogic scanner settings > DecodeWedge / Intent Wedge | Switch Intent output on, delivery broadcast, action com.datalogic.decodewedge.decode_action, category com.datalogic.decodewedge.decode_category. The app reads the string extra com.datalogic.decode.intentwedge.barcode_string (the type is in ...barcode_type). |
| Any other Android scanner | The vendor tool, then Settings > Scanner | Choose Custom intent: type the Custom intent action and the Custom intent extra name that carries the barcode. Any broadcast with a string extra (or a byte array) works. |
| Keyboard wedge scanners | Settings > Scanner > Keyboard wedge scanners | The scanner types the code and presses Enter. The app collects keys that arrive less than 50 ms apart and end with Enter as one scan, and ignores such bursts inside a text field. It needs no other setting, but it is slower than an intent. |
| Phones and iPhones | Camera | The camera scanner (1D and 2D codes, continuous mode, torch, a scan window that reads only inside the frame). It is the only scanner on iOS. |
293, 294; some Zebra devices use 103 and 104, some Datalogic 291 and 292) and choose what the button does: nothing (the hardware does the scan), open the camera, or start a soft scan through DataWedge. The codes depend on the model: check them on your device with Settings > Scanner test, which logs the trigger key events.codeId letters are mapped only for a few common symbologies (the AIM id is the reliable source). See chapter 31.First setup in Odoo
- Open Inventory > Configuration > Operation Types, open Receipts, Delivery Orders and Internal Transfers and tick Mobile WMS on the new tab of the same name (the options are in the table below).
- Give the bins their barcode, aisle, bay, level and check digits (chapter 10) and print the bin labels (chapter 24).
- Create the users of the warehouse team and give them the group Mobile WMS / Operator; the supervisors get Mobile WMS / Manager (chapter 22).
- Open Settings > Mobile WMS and look at the tolerance, the expiry rules, the count thresholds and the barcode prefixes (chapter 23).
- For products with a lot or a serial number, make sure Tracking is set; tick Ask for the expiry date when receiving on products with a shelf life.
- Print the barcodes of the products and sign in with the app.
| Option of the operation type | What it does |
|---|---|
| Mobile WMS | The operation type appears in the app and the scan engine works on its transfers. |
| Tile in the app, order | Which tile of the menu shows these transfers (Receipts, Deliveries, Internal moves, Pick, Pack) and in which order. |
| Scan the source bin / destination bin | Whether the app asks for the bin before the product. |
| Confirm the bin with its check digits | The operator types the digits printed on the bin label (when the bin has some). |
| Lot / serial prompt | Ask at the first scan: the scanner asks for the lot as soon as the product is scanned. Ask when validating: the quantity is accepted and the lot is required when the transfer is validated. |
| Extra tolerance (%) | Quantity above the demand accepted for this type; the larger of this value and the company tolerance applies. |
| Jump to the next line | The app jumps to the next line when a line is complete. |
| Backorder policy in the app | Ask, Create a backorder or No backorder, cancel the rest. |
Bins, check digits and the pick path
| Field | Use |
|---|---|
| Barcode | What the label shows (Code 128). Unique per company. Scanned to confirm the bin. |
| Check digits | 2 to 4 digits printed in a box on the label. The operator types them to prove that he or she is standing at the right bin. Generate them with the bulk action Generate check digits or the Generate link on the form (random, never repeated inside the aisle). |
| Zone | A picking zone such as Cold, Fast movers, Mezzanine. Waves and counts can be limited to a zone. |
| Aisle, bay, level | The address of the bin. Aisles and bays sort naturally (2 before 10). |
| Pick sequence | The position of the bin on the path. It is generated from aisle, bay and level in snake order: the first aisle goes from bay 01 up, the next aisle comes back from the last bay, and so on, so the picker never walks back empty. Tick Keep my pick sequence to protect a number you typed. |
Barcodes: what the scanner understands
| Barcode | What the engine does |
|---|---|
| EAN-13, EAN-8, UPC-A, UPC-E, GTIN-14 | Matches the product barcode with the leading zeros of the same GTIN (a UPC-A scanned as EAN-13 is the same product), checks the check digit and expands UPC-E. A wrong check digit is reported as such instead of an unknown product. |
| Packaging (case, pack) | The barcode of a unit of measure of the product (Odoo 19 packaging) adds the quantity of the pack. A GTIN-14 case that is not registered falls back to the unit product with a warning. |
| GS1-128 (AI 01, 02, 10, 21, 13, 15, 17, 30, 37, 310x, 320x, 00, 91) | Reads the GTIN, the lot (10) or the serial (21), the expiry (17, day 00 means the end of the month), the best-before date (15), the count (30, 37), the net weight (310x kg, 320x lb converted to the unit of the product) and the SSCC of a pallet (00). Identifiers not in the nomenclature (11, 20, 240, 400, 420, 92 to 99...) are skipped. |
| Separators | The FNC1 character, #, Alt029, the ]C1 prefix of the scanner, the human readable form (01)...(10)... and spaces between the elements are all accepted. A variable-length lot followed by another element needs a separator, as the GS1 standard requires. |
| Weight and price barcodes | EAN-13 made of a two digit prefix (21 for weight, 23 for price by default), a 5 digit item code, 5 digits of value and the check digit. The product is found by its barcode with the value digits at zero; the quantity is the weight in the unit of the product, or the price divided by the sales price. A weight (GS1 310x, 320x or a weight barcode) sets a quantity only for a product counted in a weight unit (kg, g, lb...); for a product in units it stays information. |
| Bins, packages, lots, documents | Bin barcode, package name (a pallet SSCC), lot name, the name or the source document of a transfer (a purchase order number opens its receipt), the name of a wave or of a count. |
Receipts
- Open the Receipts tile and choose the transfer (or scan the purchase order number: it opens its receipt).
- Scan the staging bin if your operation type asks for it. For a 3-step reception this is the input location.
- Scan the products. Each scan adds one unit; the quantity keypad of the app changes the amount; a case barcode adds its pack quantity.
- For a lot product the scanner asks for the lot (or reads it from the GS1 barcode). If the product needs an expiry date the scanner asks for it unless AI 17 gave it.
- Press Validate. A transfer that is not complete asks whether to create a backorder.
| Situation | Answer of the scanner |
|---|---|
| Product not on the receipt | Error wrong_product, buzzer; nothing is changed. |
| More than ordered | Refused with over_pick unless the tolerance allows it; within the tolerance it is accepted and flagged. |
| Serial already received or in stock | serial_duplicate. |
| Expired lot | lot_expired when Block expired lots is on. |
| Lot or expiry missing | A prompt (needs_lot, needs_serial, needs_expiry): the scanner opens the entry sheet. |
Deliveries and picking
- Open the delivery. It is claimed by you: nobody else can scan on it (a manager can).
- Scan the bin. If the bin has check digits the app asks for them. Scanning another bin than the suggested one is accepted and flagged
wrong_location. - Scan the product (and the lot). The reserved lot is suggested; another lot is accepted and flagged
substitute_lot. - When all lines are done the app shows the completion chime. Press Validate.
Internal moves and pallets
- Open an internal transfer, or use Free move in the app to move stock without an order.
- Scan the source bin, then the product (or the pallet), then the destination bin and its check digits.
- Scanning a pallet (a package) moves its whole content in one scan: every product, lot and quantity of the package. A pallet with a product that is not on the transfer is refused as a whole.
- Validate. The quants move exactly and the pallet keeps its identity in the destination bin.
Wave picking along the path
- Open Mobile WMS > Transfers, select the ready transfers of one operation type and choose Action > Create a wave. Or create a wave and press Add transfers: the wizard proposes the ready transfers of the same type that are not in another wave.
- Press Start. The picker (or you, if you start it) claims the wave and all its transfers.
- The picker opens the wave in the app. The stops are the bins sorted by pick sequence, each with the lines of all the orders; the lines of the same product in the same bin are merged for display.
- A scan of a unit goes to the first order, by priority, deadline and date, that still needs the product; a larger quantity is spread over the orders in the same order.
- Press Validate: the wizard asks what to do with unfinished transfers (backorder or cancel the rest). Transfers without any scan leave the wave and stay open.
Lots, serial numbers and expiry
| Rule | Detail |
|---|---|
| Lots | A lot is created when the receipt is validated (name from the scan, expiry from the scan). An existing lot is reused. A lot of a delivery must exist; an unknown lot is refused (unknown_lot). |
| Serial numbers | One unit per serial. The same serial twice on a transfer, or a serial that is already in stock when it is received, is refused (serial_duplicate). |
| Expiry date | Stored on the lot. When the Odoo Expiration Dates feature is installed its date is used and kept in step; otherwise the module keeps the date in the field Expiry date (WMS) of the lot. |
| Ask for the expiry on receipt | Tick it on the product (lot tracked products). The scanner then asks for a date for every new lot unless the GS1 barcode has it. |
| Expired | With Block expired lots a delivery or a receipt of an expired lot is refused (lot_expired); an internal move of an expired lot is accepted and flagged expired_lot, so that expired goods can go to quarantine. |
| Expiring soon | Lots that expire within the number of days of the settings are accepted with the flag expiring_soon. |
| Suggested lots | The app shows up to five lots in the bin, the earliest expiry first. |
Packages and pallets
stock.package); its name is what the label shows. A pallet label with a valid SSCC can be scanned as 00 + 18 digits or as the plain name.| Where | What a package scan does |
|---|---|
| Receipt, package unknown | The package is created with the SSCC as its name and becomes the destination package of the products scanned next. |
| Receipt, package already in stock | Refused (package_in_stock): the same pallet cannot arrive twice. |
| Delivery or internal move | All the content of the package is scanned at once; the package leaves with its goods (deliveries) or moves to the destination bin keeping its name (internal moves). |
Backorders and tolerance
| Policy | What happens to what was not picked |
|---|---|
| Create a backorder | A new transfer for the remaining quantity, with its reservation. The scanned quantities of the first transfer are validated. |
| No backorder | The remaining quantity is cancelled and its reservation released. |
| Ask | The server answers needs_backorder_decision with the remaining quantities and changes nothing (the scans are kept). The operator chooses and validates again. |
over_pick; above that, over_pick is an error.Cycle counts
- Create a count (Mobile WMS > Cycle counts) with its scope and press Start counting, or let the operator create one in the app. At the start the expected quantity of every product, lot and package in the scope is frozen.
- The operator scans a shelf, then the items, and types or scans the quantity. An item that was not expected is added with the quantity the books had at that moment.
- Submit for review. The items that the books expected in a bin that was counted, but that nobody scanned, are counted as zero (missing). Bins that were never touched are not counted at all.
- A manager reviews the differences. The Drift column shows what entered or left the bin since the start, so a receipt that arrived during the count is not mistaken for a loss. The manager can correct a quantity or ask for a recount.
- Approve and book. One inventory adjustment per line, of the difference found by the counter (counted minus expected), so the movements made in between are kept. The count becomes Done and cannot be applied twice.
Offline behaviour and synchronization
| Rule | Detail |
|---|---|
| Operations queue | Every change (scan, line update, validation, free move, wave scan, count) is stored with a unique uuid and a local_seq that never goes back, and sent with POST /sync in batches of up to 100. |
| Idempotent | The server stores each applied operation. Sending it again answers duplicate and changes nothing, so a lost answer never double-counts. |
| Rejected operations | A refused operation is answered rejected with the code. If the data is fixed and the operation is sent again, it is run again. |
| Late operations | An operation older than 15 minutes when it arrives is applied (it happened) and flagged late_sync with a to-do for the manager. |
| Several devices | An operation id that belongs to another user is refused. |
| Changes | The app asks GET /changes for the transfers, waves and counts that changed since its last visit. |
| The outbox of the app | Detail |
|---|---|
| Order | Operations are sent in the order of an unchangeable local_seq (it never goes back, a deleted row never gives its number again). An operation that waits for its retry time blocks the later ones. |
| Sending | A debounce of 2 seconds after a scan, batches of at most 100 operations through POST /sync. After a refusal of the batch (HTTP 400 or 413) the app narrows the batch down to find the operation that is refused. |
| Answers | applied and duplicate remove the operation; rejected parks it as an issue with its code; a network error retries with a back-off and jitter. An operation that was in flight when the app crashed goes back to pending. |
| Restart | The queue survives the restart of the app and of the phone. |
| Documents | While operations are unsynced, a refresh from the server never overwrites the counts of the operator. A transfer you validated disappears from the open lists at once and comes back if the server rejects the validation. |
| Counts | Count quantities are absolute, so a resend can never count twice. |
Sounds and vibration
| Verdict | Sound | Vibration |
|---|---|---|
Good scan (ok) | High chime | One short pulse |
Error (error): wrong product or bin, expired lot, over-pick | Deep buzzer | Two long pulses |
Operation complete (complete: line, stop or document done) | Ascending double tone | Short, short, long |
Information (info) | Soft tick | None |
python3 tool/make_sounds.py regenerates them), preloaded at start and played in low-latency mode (Android SoundPool, iOS AVAudioPlayer). In Settings each operator can switch the sound, the vibration, and a loud mode (full volume and longer vibration for noisy docks). The company settings Scan sounds on the devices and Vibration on the devices are only the defaults that the server sends.Roles and access
| Group | Can do |
|---|---|
| Mobile WMS / Operator | Signs in to the app, receives, picks, moves and counts on the operation types with Mobile WMS enabled, works on the transfers it claimed, sees the free waves and counts and its own. Has no access to the Odoo stock screens through this group. |
| Mobile WMS / Manager | Everything of the operator, plus: create and start waves and counts, approve counts, take over a transfer, read the scan log, the devices and the synchronized operations, and change the bins and the operation types. Includes the Inventory administrator rights, multi-location, lots and packages. |
| Rule | Detail |
|---|---|
| Claim | The first operator who scans a transfer claims it. Others (not managers) get claimed. A manager who helps does not take the transfer over. Release it from the app or set another operator on the form. |
| Companies | Every model has a company rule; the app works in the company of the user. |
| Field protection | The expected quantity of a count is readable by managers only; the state of waves and counts, and who approved, cannot be written through RPC: only the buttons change them. |
Settings
| Setting | Meaning |
|---|---|
| Read GS1 barcodes, GS1 nomenclature | Switch GS1 on or off; choose another GS1 nomenclature. |
| Weight and price barcode prefix | Two digits each, or empty to switch off. |
| Over-pick / over-receipt tolerance (%) | Quantity above the demand that is accepted and flagged. 0 means none. |
| Block expired lots, warn when a lot expires within (days) | Expiry rules of chapter 16. |
| Snake pick path | Alternate the direction in every aisle. |
| Count difference flagged above (% and units) | A difference is red when it exceeds one of them. |
| Operators must count blind | On by default: an operator cannot create a guided count, and receives no expected quantity or difference. |
| A manager may approve the count he or she made | Off by default. |
| Scan sounds, vibration | Defaults for the devices. |
| App refresh interval, operations per sync call | How often the app asks for changes (10 s to one day) and the size of a batch (1 to 100). |
| Keep the scan log for (days) | Old rows are deleted every night; 0 keeps everything. |
Labels
Reports and analysis
The mobile app
fluentui_system_icons).| Screen | What it shows |
|---|---|
| Operations menu | Tiles for Receipts, Deliveries, Internal transfers, Bin-to-bin move, Batch picking and Inventory count, each with the number of transfers that are ready and badges for those that are mine or late. A bar at the top opens a document by scanning its barcode (a transfer, a wave, a count, a purchase order). Settings, the sync status, and a Scanner test row complete the menu. |
| List of transfers | Mine first, then by priority and deadline; chips All, Mine and Urgent; search by name, origin or customer, or scan a document; a progress bar per transfer. |
| Scan terminal | Receipts, deliveries, pick and pack. A giant target at the top (the bin to go to, Bin confirmed, Put away to), the card of the product with its picture, Picked / Target and a progress bar, the allocation per order for waves, the next line, a sheet for lot, serial and expiry, a keypad to type a quantity, and the buttons Undo, Lines (or Packing) and Validate. |
| Bin-to-bin move | A free move in three steps shown as a stepper: Source, Items, Destination. Pallets move with one scan. Buttons Undo, Reset and Confirm move. |
| Batch picking | The wave along its pick path: Stop 1 of 11 with the aisle and the bin in large letters, the product card with the quantity allocated to each order, packing. |
| Inventory count | A draft count with a Start counting button; the shelves as chips; the counted items per shelf; Undo, Next shelf and Review; the count review for supervisors with the discrepancies and Apply inventory. |
| Sync status | Online or offline, the last sync, Sync now, the queue of operations with retry and discard. |
| Settings, Scanner test | Scanner profile, DataWedge, custom intent, trigger keys, camera options, sound, vibration, loud mode, theme, language, master data, sign-out. |
flutter analyze is clean. The app was built as a debug APK for Android, built for the iOS simulator, and run end to end on the iOS simulator against a real Odoo with this module: receipts with lot, serial and expiry from GS1 barcodes, deliveries with a partial pick and a backorder, bin moves, a wave, a blind count by an operator approved by a manager, error paths, and an offline run (server stopped, scans queued, outbox synced after the restart). What was not verified is listed in chapter 31.docs/ARCHITECTURE.md (layers, the scan path, the rules, the outbox, the native layer, the not-verified list).For developers: the API and the resolver
documentation/API.md.# is the server reachable? (no sign-in)
curl https://odoo.example.com/api/wms/v1/ping
# {"ok": true, "api_version": 1, "server_time": "2026-10-28T08:00:00Z"}
# sign in as a warehouse operator (returns a token, the configuration and the GS1 rules)
curl -X POST https://odoo.example.com/api/wms/v1/auth/login -H 'Content-Type: application/json' \
-d '{"login": "omar", "password": "...", "device": {"id": "TC52-0147", "name": "TC52 dock 1", "platform": "android", "scanner": "zebra"}}'
# what is this barcode? (GS1-128 with GTIN, lot and expiry; the answer says which product, which lot and which date)
curl -X POST https://odoo.example.com/api/wms/v1/resolve -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"barcode": "010629105000040710L2610A\u001d17261118", "context": {"picking_id": 12}}'
# push the queued operations: every operation has a uuid and a local_seq, a repeat changes nothing
curl -X POST https://odoo.example.com/api/wms/v1/sync -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"device_id": "TC52-0147", "ops": [{"uuid": "9d1c...", "type": "scan", "local_seq": 41, "device_time": "2026-10-28T08:01:00Z",
"payload": {"picking_id": 12, "barcode": "6291050000407", "qty": 6, "location_id": 31}}]}'| Area | Endpoints under /api/wms/v1 |
|---|---|
| Session | GET /ping, /config; POST /auth/login, /auth/logout |
| Resolver | POST /resolve (barcode and context); GET /locations/<barcode>/check?digits=NN |
| Lists | GET /menu, /pickings, /pickings/<id>, /batches, /batches/<id>, /counts, /counts/<id>, /changes; POST /pickings/<id>/claim, /release, /batches/<id>/start, /claim, /counts/<id>/start |
| Operations | POST /sync (scan, line_update, validate, move, batch_pick_line, batch_validate, count_line, count_start, count_submit, count_create) or POST /ops/<type> with the same payload |
| Offline cache | POST /master (cursor, scope: products, locations, packages, lots), paged |
Authorization: Bearer <token> and optionally Accept-Language. Errors are {"error": {"code", "message"}} with the HTTP status 400, 401, 403, 404, 409 or 422. Sign-in is limited to 8 wrong attempts per 5 minutes for the same address and login.# the engine of the module, in a shell or in your own code
env["wms.barcode.resolver"].resolve("010629105000040710L2610A\x1d17261118", {"picking_id": 12})
# {"hits": [{"type": "product", "id": 9209, "lot_name": "L2610A", "expiration_date": "2026-11-18", ...}, {"type": "lot", "exists": False, ...}],
# "primary": {...}, "gs1": {"parsed": [{"ai": "01", "value": "..."}, ...], "raw": "..."}, "warnings": [], "ms": 2.7}ms field of the answer); the network and the HTTP stack add their own time. Larger databases were not measured. Each kind of record is looked up with one indexed query.stock.picking: process_barcode_scan(barcode, location_id, package_id, ...) and action_mobile_validate(picking_id, lines_data, backorder_policy); on wms.batch.pick: wms_scan, wms_validate; on wms.count.session: wms_count_line, wms_submit, action_apply. They check the group of the user themselves and do their work with elevated rights after the checks.Security
| Topic | What the module does |
|---|---|
| Tokens | A device signs in with login and password and receives a random token; only its SHA-256 hash is stored. The token ends when the user is archived, loses the group or is signed out; tokens not used for 180 days are deleted. |
| Authorization | Every public method checks the group, the company, the operation type and the claim. Nothing is decided from a context key or from a field that a client can write. |
| Workflow fields | State, approved by, applied and the scanned quantities of a count can only be changed by the buttons or the API, never directly. |
| Input | Quantities must be finite numbers in range, dates are validated, barcodes are limited to 300 characters, payloads to 100 operations per call; NaN and infinity are rejected. |
| Count integrity | Expected quantities are frozen and hidden from operators; approval is serialized with a row lock, so a count is never applied twice; whoever counted, submitted or corrected a count cannot approve it, and the counter of a started count cannot be changed. |
| Companies | A scan answer only shows data of the companies of the signed-in user: asking for another company in the request context is refused. |
| Audit | The scan log and the synchronized operations keep who, when, which device and what the answer was. |
Update
Replace the folder with the new version, restart Odoo and run -u taqnix_wms_barcode. The bins, the sequences, the settings, the waves, the counts and the scan log are kept. Update the app on the devices with the new build; the app and the module talk API version 1.
Troubleshooting
| Problem | What to do |
|---|---|
| The app cannot connect | Open https://your-odoo/api/wms/v1/ping on the device: it must answer {"ok": true}. Check https, the certificate, the address without a trailing slash and the database (if you have several, use the dbfilter setting or send the header X-Odoo-Database). |
| Sign-in says the account is not an operator | Give the user the group Mobile WMS / Operator (or Manager). |
| The transfer is not in the list | Its operation type must have Mobile WMS ticked, its state must be Ready (or Waiting), and it must belong to the company of the user. |
| The scanner beeps but nothing happens in the app | The intent action of the scanner profile and the one in the app settings must be identical, delivery must be Broadcast, and keystroke output must be off. |
| The scan types into a field | Keystroke output (DataWedge) or keyboard wedge is still on. |
| Unknown barcode | Check the barcode of the product (Inventory > Products), the packaging barcode, and whether the code is GS1 (the engine tells bad check digit when the number is wrong). |
| Check digit refused | The digits are on the label and on the bin form; a bin without digits accepts anything. |
| Serial or lot refused | Read the code in the answer: serial_duplicate, unknown_lot, lot_expired, tracking_required. |
| Over-pick | Raise the tolerance in the settings or in the operation type, or validate with a backorder. |
| The wave cannot be validated | The message names the transfer that fails; fix it (lot, destination) and validate again: nothing was written. |
| A count cannot be approved | The count must be in Review; the approver must not be the counter (settings); someone else may already have approved it. |
| Labels print without the barcode | Check that wkhtmltopdf works and that the bin has a barcode. |
| Operations stay in the queue | Open Synchronized operations to see the code and message of the rejected ones. |
| Scanner problem | What to do |
|---|---|
| Nothing happens when I pull the trigger | Open Settings > Scanner test and pull it. If Last raw intent stays empty, the intent never arrived: compare the action in the scanner profile with the app, check that delivery is Broadcast, and that the profile is associated with com.taqnix.wmsscanner. |
| The trigger button does not scan | The key codes of the button depend on the model: read them in Scanner test and type them in Scan trigger key codes. |
| The scan is counted twice | Repeats inside 400 ms are dropped. If your scanner sends the same read with two different profiles (for example intent and keyboard wedge at the same time), switch one off. |
| The camera does not open | Allow the camera permission to the app in the Android or iOS settings. |
Limits and what has not been verified
| Topic | Detail |
|---|---|
| Real scanners | No Zebra, Honeywell or Datalogic device was available to the author. The DataWedge profile (SET_CONFIG), the intents and extras of the three vendors, the soft scan trigger and the default trigger key codes follow the vendors' documentation and are unit tested on the parsing side only. The delivery of a real broadcast, the behaviour of the receiver on a given Android image, and the differences between DataWedge 6.x and 8.x must be confirmed on your hardware. Test one device before you buy fifty. |
| Honeywell symbologies | The Honeywell codeId letters are mapped for a few common symbologies only; the AIM id is the reliable source. |
| Camera, audio, battery | Camera scanning (torch, scan window, continuous mode) was not run on a device: the iOS simulator has no camera. The latency of the sounds, the strength of the vibration, the battery drain with the screen kept on and the behaviour of the receiver when the screen is off or in doze were not measured. |
| Android emulator | The debug APK builds, but the app was not run on an Android emulator or phone by the author. It was run on the iOS simulator against the real module. |
| iOS | iOS has no hardware scanner plugin: iPhones scan with the camera. |
| Offline weight barcodes | A weight-embedded EAN-13 resolves on the device only when the product record carries its code (the plu field) or its internal reference contains it; otherwise the server resolves it, so it needs the network. |
| Serial numbers | One scan is one unit. A lot change in the middle of a line replaces the lot of that line on the device; the server splits the line when it receives it. |
| Units of measure | A unit of measure that is not the product's own is shown, but quantities are not converted on the device. |
| Pack barcode in a count | Scanning the barcode of a pack of a product during a count creates a separate count line instead of landing on the line of the package that holds it. The review then shows the package line as missing (negative) and the separate line as found (positive): read the two together and correct them before you apply. |
| Manufacturing | Manufacturing orders (picking components, producing) are not handled. |
| Wave planning | Waves are made by hand or from a selection. There is no automatic wave planning by weight, volume, carrier cut-off or zone. |
| Put-away | The destination bin of a line is the one that Odoo's putaway rules give; the module does not add its own put-away strategy. |
| GS1 without separators | A GS1 string with a variable-length element followed by another element and no separator is ambiguous, as in the GS1 standard; the scanner must send FNC1. |
| Expiry | Without Odoo's Expiration Dates feature the expiry is kept in a field of this module. The behaviour with that feature installed is covered by the same code path but was not tested in a database that has it. |
| Offline | The app works on what it cached; creating a lot unknown to the server is possible on receipts only. Two devices working on the same transfer are prevented by the claim, not merged. |
| Scale | The resolver was measured on a database with about 2,200 products and 2,200 bins (chapter 27). Larger databases were not measured. |
| Odoo version | Built for and tested on Odoo 19.0. |
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.