Skip to content
TAQNIX
Odoo ModulesWordPress PluginsWordPress ThemesShopify ThemesApp TemplatesScripts & UtilitiesBlogSell on TAQNIX

Odoo ePOD & Last-Mile Dispatch documentation

A guide to Odoo ePOD & Last-Mile Dispatch: installing the module and building the driver app, runs, the optimizer, proof of delivery, customer codes, failed deliveries, backorders, cash on delivery and settlement, reports, roles, settings and the API.

Documents version 1.0.0Updated Oct 29, 2026

Start here

In plain words: This package gives you a complete Electronic Proof of Delivery (ePOD) and Last-Mile Dispatch system for Odoo Community: an Odoo module (taqnix_epod_core) for the dispatcher and the office, and the Flutter source code of an offline-first driver app for Android and iPhone. The module plans the daily runs, optimizes the stop order, receives the proof of every drop (GPS and time, photos, signature, customer code), validates the delivery order, creates the backorder for goods that were not delivered, and settles the cash that the driver collected.
Plan, deliver, prove and settle: runs, proof of delivery and the printable ePOD.
Plan, deliver, prove and settle: runs, proof of delivery and the printable ePOD.
Tip: No Odoo Enterprise app is needed. The module uses Inventory (stock), Fleet, Invoicing and the geolocation of contacts, and links to Google Maps or OpenStreetMap with plain links (no map key to buy).

What is in the package

ItemWhat it is
taqnix_epod_core.zipThe Odoo 19 module: runs (route manifests) and stops, the route optimizer, geofenced arrival, proof of delivery, customer codes, exception reasons, cash on delivery, driver settlement with automatic payments, reports, the REST API of the app, Arabic translation and 217 automated tests.
flutter-app-source.zipThe Flutter project of the driver app (Android and iOS): offline database and outbox, background sync, the stop stepper, camera with watermark, signature pad, deep links to the navigation apps.
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.txtHistory of the versions.

How a day works

MomentWhoWhat happensIn Odoo
MorningDispatcherCreates the run: driver, vehicle, date, and adds the ready delivery orders. Presses Optimize sequence, then Confirm.A run in Confirmed with ordered stops. Customers whose order is worth more than the limit receive their delivery code by e-mail.
On the roadDriverOpens the app, sees the stops in order, taps Start navigation (Google Maps, Waze or Apple Maps) and comes back to the app.The stop is En route; the run is In progress.
At the doorDriverThe app detects the arrival inside the geofence. The driver scans the parcels, takes photos, collects the signature or the customer code, takes the cash and finishes the stop.The stop is Delivered with the proof; the delivery order is validated; a short quantity makes a backorder.
No one homeDriverChooses a reason (customer not home, access restricted, refused...), takes the required photo.The stop is Failed; the dispatcher gets a to-do to reschedule.
Back at the hubDriver, cashierThe driver declares the cash by method; the cashier counts it.A settlement sheet in Submitted.
EveningManagerApproves the settlement.One customer payment per collection, posted and reconciled with the open invoices.
In plain words: Offline first. Everything the driver does is saved on the phone first and sent to Odoo when there is a network. Every call has a unique ID, so a lost answer or a retry never validates a delivery twice (chapter 29). A delivery that really happened is never refused by the server: problems become flags for the dispatcher (chapter 20).

What you need

ItemDetails
Odoo19.0 Community or Enterprise. The apps Inventory, Fleet, Invoicing, Contacts geolocation and Discuss are installed automatically with the module. Sales is optional: when it is installed the cash to collect is read from the sales order.
Chart of accountsA chart of accounts and, for the settlement, one journal per payment method (cash, cheque, card, transfer); they can be the same journal.
Outgoing e-mailAn outgoing mail server for the delivery codes and the proof e-mails (Settings, Technical, Outgoing Mail Servers).
PDF enginewkhtmltopdf as for any Odoo (the ePOD, run sheet and settlement reports).
AddressA public https address for your Odoo: the phones must reach it.
PhonesAndroid or iPhone with GPS and a camera. To build the iPhone app you need a Mac with Xcode.

Install the Odoo module

  1. Unzip the package. Inside is taqnix_epod_core.zip; unzip it to get the folder taqnix_epod_core.
  2. Copy the folder into one of the addons_path folders of your Odoo server and restart Odoo.
  3. Log in as an administrator, activate Developer mode, open Apps and press Update Apps List.
  4. Remove the Apps filter, search ePOD and press Install.
  5. Open Settings, Users and give your people the roles ePOD Dispatch / Driver, Dispatcher or Manager (chapter 22).
# or from the command line
./odoo-bin -c odoo.conf -d YOUR_DATABASE -i taqnix_epod_core --stop-after-init
Tip: Test it first on a copy of your database. The module adds fields to delivery orders and contacts (all prefixed epod_) and does not change any existing data.

Build and install the driver app

In plain words: The app is a Flutter project (Taqnix ePOD Driver, Flutter 3.41 with Dart 3.11, Android 7.0 or newer (API 24), Xcode 16 with CocoaPods for the iPhone). It is delivered as source code so that it carries your name, icon and colour. No Firebase file and no Google services file are needed.
  1. Unzip flutter-app-source.zip and open a terminal in the folder.
  2. Fetch the packages: flutter pub get. The database code (Drift) and the translation files are already generated and included; run dart run build_runner build only after you change the tables and flutter gen-l10n only after you change the texts.
  3. Check the project: flutter analyze (no issues) and flutter test (356 tests).
  4. Run it on a phone or a simulator: flutter run --dart-define=SITE_URL=https://odoo.example.com. SITE_URL is optional: without it the sign-in screen shows a Server address field and remembers what you type.
  5. Build the Android file: flutter build apk --release --dart-define=SITE_URL=https://odoo.example.com, or flutter build appbundle --release ... for the Play Store.
  6. Build the iPhone app on a Mac: flutter build ios --release --dart-define=SITE_URL=https://odoo.example.com, then archive and upload with Xcode.
  7. Open the app and sign in with the login and password of a user who has the role ePOD Dispatch / Driver (chapter 22). The app downloads the configuration and today's run.
SettingWhere
Application id / bundle id com.taqnix.epoddriver, display name "Taqnix ePOD Driver"android/app/build.gradle.kts, android/app/src/main/res/values/strings.xml, ios/Runner/Info.plist and the Xcode project. Keep the iOS background task identifier (<bundle id>.sync) in step with lib/core/services/background_sync.dart.
Brand colourAppConfig.seedColorValue in lib/core/constants/app_config.dart
Icon and splashReplace assets/branding/*.png, then dart run flutter_launcher_icons and dart run flutter_native_splash:create
Release signing (Android)Create android/key.properties (storeFile, storePassword, keyAlias, keyPassword). Without it the release build is signed with the debug key.
PermissionUsed for
Location (when in use, always)Distance to the stops, geofence arrival, the coordinates stamped on photos, the GPS trail. "Always" lets arrivals be detected with the screen off.
CameraProof photos and the barcode scanner.
NotificationsThe "arrived at..." notification and the Android foreground-service notice.
NetworkTalking to Odoo.
Tip: Without an Odoo server you can try the app against the small fake server of the source: python3 tool/fake_server.py --port 8197 (Python standard library only, a driver and six Dubai stops).
Important: Use an https address: phones refuse plain http on recent systems. Google Play asks for a declaration for background location (the app uses it only while the driver has open stops: arrival detection) and Apple asks you to explain it in the review notes. The app has no ads, no analytics and no SDK that sends data anywhere except to your Odoo.

First setup in ten minutes

  1. Open ePOD Dispatch, Configuration, Settings. Check the geofence radius (default 100 m) and the proof rules (chapter 23).
  2. In the same screen choose the journals for cash, cheque, card and transfer: the settlement approval needs them.
  3. Create your drivers as internal users with the role ePOD Dispatch / Driver and a password. Attach each driver to a vehicle in Fleet (field Driver).
  4. Check that your customers have a position: open the contact, tab Delivery (ePOD), press Geolocate (chapter 8).
  5. Create the first run (chapter 9), confirm it and sign in on a phone.
Geofence and GPS settings.
Geofence and GPS settings.

Customers: position, gate code, window

In plain words: The arrival check and the navigation links need the latitude and longitude of the customer. The module reuses the fields of Odoo's contact geolocation and adds what the driver needs at the door.
The tab Delivery (ePOD) of a contact.
The tab Delivery (ePOD) of a contact.
FieldUse
Gate / door codeShown to the driver on the stop (gate, intercom, building access).
Delivery windowFor example 09:00-12:00. Shown to the driver; it is information, not a rule.
Delivery instructionsWhere to park, which entrance, whom to call.
Geofence radiusOverrides the company radius for this address (0 = company setting).
GeolocateLooks the address up with the provider of Odoo's geolocation and fills latitude and longitude. If nothing is found you get a message and can type the coordinates.
Tip: A stop whose customer has no position still works: the arrival cannot be checked by the server (the app's own check is used) and the stop goes to the end when you optimize.

Runs: create and fill

In plain words: A run (route manifest) is one driver, one vehicle, one day and an ordered list of stops; each stop is one delivery order. Runs are numbered RUN/2026/0001.
The runs by status.
The runs by status.
  1. Open Operations, Runs and press New. Choose the date, the driver and the vehicle (choosing a vehicle fills its driver). Leave the depot empty to start from the company address.
  2. Press Add deliveries: the wizard lists the ready outgoing delivery orders of the company (all stock reserved) that are not on an open stop. Select them and press Add to the run. Running it again never duplicates a stop.
  3. You can also select delivery orders in Inventory, Delivery orders and use the action Add to an ePOD run, or the button Add to a run on the delivery order.
  4. Drag the stops by the handle on the left to change the order, or use Optimize sequence (chapter 10).
  5. Press Confirm.
Adding delivery orders to a run.
Adding delivery orders to a run.
A run in progress: stops, status, cash to collect and flags.
A run in progress: stops, status, cash to collect and flags.

Optimize the stop order

In plain words: Optimize sequence computes a short route: it starts at the depot, always goes to the nearest customer next (nearest neighbour) and then improves the result with a 2-opt pass. It uses the straight-line distance between coordinates (the haversine formula); it is deterministic, so the same stops always give the same order.
The preview shows the distance now, the optimized distance and the saving.
The preview shows the distance now, the optimized distance and the saving.
RuleDetail
Stops without a positionGo last, in their current order.
Delivered and failed stopsKeep their place at the top; only open stops are re-ordered.
No depot positionThe first located stop stays first.
ApplyPress Apply this order; the sequence numbers are renumbered 10, 20, 30... You can still drag stops afterwards.
Important: Straight-line distance is not road distance. It is a good first order, not a traffic-aware route: the dispatcher knows the city better.

Confirm, start, close, reassign, reschedule

ActionWhat it does
ConfirmNeeds a driver with the Driver role and at least one pending stop. Refreshes the cash to collect from the invoices, creates the customer codes and e-mails them.
Start the runOptional: the run also starts by itself when the driver taps the first stop.
Close the runFinishes the run; stops that are still open are cancelled and the driver is told. A run also closes by itself when its last stop is closed.
ReopenA finished run (less than a week old) goes back to In progress to take a late stop.
Reassign driverGives the open stops to another driver (and vehicle). The old phone loses them, the new one gets them; cash already collected stays with the driver who took it.
CancelCancels the run when nothing was delivered yet.
RescheduleOn a failed stop: creates a new stop on another run (or a new run). The failed stop stays failed with its proof.
In plain words: A stop can be pending, en route, arrived, delivered, partially delivered, failed or cancelled. Only the driver app and the buttons above change the status; nobody can type it into a form.

Geofenced arrival

In plain words: The app watches the position of the driver. When the phone is inside the geofence of the stop (100 m by default) the stop moves from En route to Arrived, and the arrival time and the distance are saved. The server recomputes the distance from the position of the customer; the distance reported by the phone is kept next to it.
Field on the stopMeaning
Arrived atFirst arrival stamp (a second arrival ping never changes it).
Distance at arrivalMeters between the phone and the customer, computed by the server.
Arrived inside the geofenceTrue when that distance is within the radius.
Arrival detected byGeofence (automatic) or Driver tap.
Tip: A driver can also tap Arrived by hand. If it is far from the address the stop gets the flag Outside geofence for the dispatcher.

The proof of delivery

In plain words: Every completed stop stores a multi-point proof. Open it from Operations, Stops, from the run, or from the Proof of delivery button on the delivery order.
A delivered stop: receiver, signature, arrival and completion with time, distance and GPS.
A delivered stop: receiver, signature, arrival and completion with time, distance and GPS.
The photos with the watermark band (position, time and delivery number).
The photos with the watermark band (position, time and delivery number).
PointWhat is stored
GPS and timePosition, accuracy, time of completion, distance from the address and whether it was inside the geofence.
PhotosDoorstep drop, open box / serial, damaged parcel, place of delivery. Stored as attachments with their time, position, size and SHA-256 fingerprint; the content is checked (WebP, JPEG or PNG only).
SignatureA PNG of the signature with the printed name of the receiver and the relationship (recipient, neighbor, receptionist, concierge, other) and the time.
Customer codeWhether the code was checked on the phone or by the server (chapter 14).
GoodsOrdered and delivered quantity of every line, lot and serial numbers, the reason of every short line.
CashEvery payment taken at the door (chapter 17).
In plain words: The proof cannot be edited. Not even a manager can change a photo, a signature or a quantity from the screens or over RPC: they are written only by the app workflow. If something is wrong, add a note in the chatter.

Customer code (one-time password)

In plain words: For valuable deliveries the customer receives a 4 to 6 digit code by e-mail and gives it to the driver at the door. The code is created by the server when you confirm the run.
DetailHow it works
WhenA stop needs a code when the order is worth at least the setting One-time password from this order value (0 = never), or when you tick Always ask for the customer code on the stop.
What is storedOnly hashes: a server-side hash (HMAC-SHA256 keyed with a secret that never leaves the server) that the server uses for every check, and a separate salted hash for the phone. The code itself is never stored; it is only in the e-mail, which is sent by the system, is not linked to the stop, and cannot be read by drivers or in the chatter.
Assurance levelsChecked by the server (best): the digits were verified online through the server, or the setting Offline code check on the phone is OFF and the digits typed at the door matched when the stop was completed. Checked on the phone: the phone had a hash of the code, so the proof is recorded as On the phone (offline) with the flag Code checked on the phone only.
Offline code check settingON (default): the app receives the salt and hash and can check the code without a network. OFF: the app receives no hash at all; it checks online when it can, or sends the digits when it completes the stop and the server verifies them.
Wrong tries5 wrong codes lock the stop for 10 minutes; the counter is never reset by a resend, and after 15 wrong codes over the life of the stop only a dispatcher can unlock it (button Unlock the code). A locked stop is never recorded as verified: the flag Code locked is raised.
ResendThe driver or the dispatcher can send a new code (old code invalid) up to 3 times per stop, only while the run is confirmed or in progress and the code is not locked.
ValiditySetting validity (minutes), default 24 hours. An hourly job wipes expired hashes; ask for a new code if that happens (flag Code expired if it was too late).
Other channelsThe method _epod_send_otp of the stop is a hook: another module can send an SMS or WhatsApp message instead of the e-mail.
Important: Residual risk. A 4 to 6 digit code has at most a million values. If the phone holds a salted hash of it (setting ON), someone who holds the phone can find the code in seconds, and the server cannot tell that from a customer who really gave it. That is why such a proof is labelled checked on the phone and flagged. For stronger proof switch the offline check OFF (the driver needs a network or the server check at completion) or verify online first. Whatever the setting, a code protects against honest mistakes and casual fraud, not against a determined attacker.

Failed deliveries and exception reasons

In plain words: When the driver cannot deliver he chooses an exception reason. A failed stop does not validate the delivery order, which stays ready for the next attempt.
A failed stop: the reason, the driver's note and the flag <em>To reschedule</em>.
A failed stop: the reason, the driver's note and the flag To reschedule.
The exception reasons (Configuration, Exception reasons).
The exception reasons (Configuration, Exception reasons).
KindMeaningExamples
Failed stopNothing was delivered.Customer not home, Access restricted, Wrong address, Unsafe to deliver
LineGoods rejected or damaged on a stop that was delivered.Damaged in transit, Wrong item delivered, Missing item
BothCan be used for either.Refused by customer, Customer did not order it
In plain words: Every reason says whether a photo and a note are required and whether the dispatcher should reschedule. Managers can add or change reasons; the four reasons of the specification (not_home, access, damaged, refused) are installed with their translations.

Partial deliveries and backorders

In plain words: When the customer takes less than the ordered quantity, the driver enters the delivered quantity of the line and a reason. The server validates the delivery order with the quantities that were really delivered, and Odoo creates a backorder automatically for the rest.
A partial delivery: 9 of 10 delivered, the reason, and the backorder created.
A partial delivery: 9 of 10 delivered, the reason, and the backorder created.
CaseResult
All lines completeStop Delivered, delivery order Done.
A line is shortStop Partially delivered (flag Partial delivery), delivery order Done, a backorder with the remaining quantity, and a to-do to put it on a new run.
Quantity above the order, negative, not a numberRefused by the server (malformed).
Serial and lot numbersThe app shows the lots reserved on the line and defaults to them; the driver can name other lots (matched ignoring case and blanks). If no lot is named, the reserved lots are delivered. A name that does not exist is not refused: the delivery is recorded and the stop shows the valid lots to choose from.
Odoo refuses to validateFor example an unknown serial number. The proof and the delivery stay recorded, the stop shows the problem and the flag Validation failed, and the button Validate the delivery order tries again after you fix it.

Cash on delivery

In plain words: For each stop the module knows the amount to collect: the open amount of the posted customer invoices of the delivery (found through the sales order, or through the invoice origin), or the order total when nothing is invoiced yet (needs Sales). Credit notes and paid invoices are ignored.
The Cash on delivery tab: amount expected, collected, and every payment.
The Cash on delivery tab: amount expected, collected, and every payment.
FieldMeaning
Amount to collectFrom the open invoices / order (automatic), Amount set by the dispatcher or No cash on delivery.
CollectionsEvery payment the driver took: cash, cheque, card / POS or bank transfer / wallet, with a reference for the last three.
DifferenceCollected minus expected; a difference raises the flag Cash difference but never refuses the delivery.
All collections with their method, reference and settlement.
All collections with their method, reference and settlement.

Driver settlement

In plain words: At the end of the shift the driver declares the cash he hands over, per method. The cashier counts it at the hub and a manager approves. The module compares everything with what the app recorded.
A settlement: expected against declared and counted per method, the difference and the driver's signature.
A settlement: expected against declared and counted per method, the difference and the driver's signature.
  1. The driver sends the declaration from the app (or the dispatcher creates the sheet under Cash, Driver settlements and presses Submit). The sheet shows the expected amount per method, taken from the collections of that driver and day. The declaration is then locked.
  2. The cashier counts the money, corrects the Counted column if it differs from the declaration, and presses Cash counted. The declared and counted amounts are written in the chatter.
  3. If the difference between counted and expected is larger than the tolerance of the settings, a note is mandatory.
  4. A manager (not the driver himself) presses Approve and register payments. It is refused until the cash is counted and while the driver still has open stops on that day.
On approvalDetail
PaymentsOne posted customer payment per collection, at the amount the customer paid, in the journal of its method, dated on the run day, with the delivery and cheque numbers in the memo.
ReconciliationEach payment is reconciled with the open invoices of the stop, oldest first. Invoices in another currency are left open and noted in the chatter; a payment without an open invoice stays as customer credit.
Shortage or surplusThe difference between the cash counted and the cash collected is booked in one journal entry: a shortage debits the Driver shortage account (partner: the driver) and credits the liquidity account of the method's journal; a surplus does the opposite. Choose the journal and the account in the settings. If they are not configured, a difference beyond the tolerance can only be approved when a manager writes a note and ticks Accept the difference without booking it; that choice is logged.
SafetyApproving twice creates nothing; you cannot approve your own settlement; the approved sheet is locked.
RejectSends the sheet back: the driver declares again.
Late collectionsA collection that reaches the server after the sheet of the day was submitted or approved (a phone that was offline, a reopened run) goes to a follow-up sheet of the same day (sheet 2, 3...) and the dispatcher gets a to-do. The driver declares it from the app like the first one.
An approved settlement with its payments.
An approved settlement with its payments.

Printable reports

ReportWhereContent
Proof of delivery (ePOD)Print on a stop, on a delivery order, or e-mailed to the customerCustomer, run and driver, arrival and completion with time and place, customer code result, goods, receiver, signature, photos, flags.
Run sheetPrint on a runThe stops in order with address, instructions, code required and cash to collect, and signature lines.
Driver settlementPrint on a settlementExpected, declared and difference, the collections and signature lines.
The ePOD of a delivered stop.
The ePOD of a delivered stop.
The ePOD of a partial delivery with the reason and the damage photo.
The ePOD of a partial delivery with the reason and the damage photo.
The run sheet.
The run sheet.
In plain words: With E-mail the proof of delivery to the customer switched on (Settings), the customer receives the ePOD as a PDF when the stop is completed, in the language of the customer.

Stops to review (flags)

In plain words: The server never refuses a delivery that happened. When something does not look right it records the stop anyway and raises flags, plus a to-do for the dispatcher of the run.
Operations, Stops to review.
Operations, Stops to review.
FlagRaised when
Outside geofenceThe distance at arrival or completion is larger than the radius.
Low GPS accuracy / GPS missingThe accuracy is worse than the setting, or no position came with the stop.
Code missing / mismatch / expired / lockedThe stop needs a customer code and none, a wrong one, or an expired one was entered, or the stop was locked after too many wrong tries.
Code checked on the phone onlyThe code matched but the phone had a hash of it: weaker proof (chapter 14).
Signature / Photo missingThe stop needs them and they were not sent.
Cash differenceCollected is not what was expected.
Late syncThe proof arrived more than 6 hours after the delivery (the phone was offline).
Partial deliveryA line was short; a backorder exists.
Reference missingA cheque, card or transfer payment has no reference.
Validation failedOdoo refused to validate the delivery order.
To rescheduleA failed stop whose reason asks for a new attempt.
In plain words: Press Mark flags as reviewed on the stop when you have dealt with it; the stop leaves the review list.

Dashboard and tracking

Stops by status and day.
Stops by status and day.
In plain words: Reporting, Dashboard shows stops by status and day as a graph and as a pivot (by driver, with the on-time percentage and the cash collected). On-time delivery is the share of delivered stops completed on or before the day of the run.
The pivot by driver and status.
The pivot by driver and status.
The GPS trail (breadcrumbs sent by the app while a run is open).
The GPS trail (breadcrumbs sent by the app while a run is open).
Devices: the phones that are signed in (Configuration, Devices). Switch one off to sign it out.
Devices: the phones that are signed in (Configuration, Devices). Switch one off to sign it out.
The sync log lists every operation received from the phones, applied or rejected, with the reason.
The sync log lists every operation received from the phones, applied or rejected, with the reason.

Roles and access rights

RoleCan do
DriverSigns in to the app. In Odoo he sees only his own runs, stops, collections and settlements, read only.
DispatcherSees everything of the company: creates and confirms runs, adds deliveries, optimizes, reassigns, reschedules, reviews flags, counts the cash. Cannot change the proof or approve settlements. Includes Inventory user and Fleet user rights.
ManagerEverything of the dispatcher plus: approve or reject settlements, the settings, exception reasons, flags, devices and the sync log.
In plain words: Rules: every record belongs to a company and users only see their companies (multi-company safe); a driver's rights are limited to his own run by record rules, also on photos, collections and lines; workflow buttons check the role on the server; status and proof fields cannot be written from the screens or over RPC.

Settings

Proof of delivery settings.
Proof of delivery settings.
SettingDefaultMeaning
Geofence radius100 mArrival and completion are checked against this distance (10 to 5000 m). A contact can override it.
Worst GPS accuracy60 mA worse position raises the low accuracy flag.
Photo required / minimum / maximumyes / 1 / 12Proof rules sent to the app; missing photos only raise flags.
Maximum photo size3 MBLarger photos are refused (413).
Photo long side / quality1600 px / 70Sent to the app for WebP compression.
Signature required from value0 (always)Signature required on stops worth at least this.
One-time password from value / digits / validity500 / 6 / 1440 minSee chapter 14.
Offline code check on the phoneonOFF = the phone never receives a hash of the code (chapter 14).
E-mail the proofoffSend the ePOD PDF to the customer.
Journals by method-Used by the settlement approval.
Settlement tolerance1.00A difference up to this per method needs no note.
Shortage journal and account-Where the difference between counted and collected cash is booked (chapter 18).
GPS trail retention30 daysOlder breadcrumbs are deleted by a daily job.
Forget devices after180 daysPhones not seen for this long must sign in again.
Cash on delivery and settlement settings.
Cash on delivery and settlement settings.

The driver app: sign in, the run and navigation

In plain words: Taqnix ePOD Driver is made for the cab and the doorstep: big touch targets, one-handed use, high contrast, status colours (pending amber, en route orange, arrived blue, delivered green, failed red) and icons from Fluent UI System Icons. Nothing is hard-coded: the radius, the proof rules, the reasons and the payment methods come from your Odoo settings.
ScreenWhat the driver does
Sign inServer address (when the build has no fixed address), e-mail login and password, Remember me on this phone. A language switch is on the screen.
Today's runThe run of the day: number, vehicle and plate, a progress bar, the counts (stops, delivered, failed, remaining) and the cash collected against the cash expected. Below, the To do list in order with the next stop highlighted, distance markers in meters or kilometers, the delivery window and the amount to collect.
My orderA drag handle lets the driver re-order the stops for himself. This is local only: the API has no re-order call, nothing is sent to Odoo, and a re-order by the dispatcher arrives by itself.
Stop detailsThe customer, address, delivery window, a Call button, the gate and access instructions from the contact (with a copy button), the items with their quantities, and the proofs this stop needs (photo, signature, one-time code, scan).
Sign in: the server address, the login and the password.
Sign in: the server address, the login and the password.
Today's run: progress, counts, the cash collected against the cash expected, and the stops in order.
Today's run: progress, counts, the cash collected against the cash expected, and the stops in order.
A stop: address, delivery window, call button, gate instructions, items and the proof it needs.
A stop: address, delivery window, call button, gate instructions, items and the proof it needs.
In plain words: Navigation. The arrow on a stop opens the navigation app of the driver with the coordinates of the customer: Google Maps, Waze, Apple Maps or any app that handles a geo: link. The first time the driver chooses (and can tick Always use this app); in Settings he can change it, and the default can come from Odoo. There is no turn-by-turn guidance inside the app. When the driver comes back from the navigation app, a sheet asks Welcome back: have you arrived? with Yes, I have arrived, Open the stop and Not yet.
Navigate with: Google Maps, Waze or Apple Maps, and the option to always use one.
Navigate with: Google Maps, Waze or Apple Maps, and the option to always use one.
In plain words: Geofenced arrival. While the driver has open stops the app watches the position (an Android foreground service with a visible notice, or iOS background location). When the phone enters the radius of a stop the stop becomes Arrived by itself, with the time, the distance, the accuracy and the trigger GPS, and a banner offers to open the stop. A manual Arrived button is always there. The arrival is stamped on the phone first, so it works without a network.
The arrival banner: Arrived at the customer, 15 m from the address, detected by GPS.
The arrival banner: Arrived at the customer, 15 m from the address, detected by GPS.

The driver app: the six-step proof of delivery

In plain words: Opening a stop and pressing Complete delivery starts a stepper of six steps: Arrived, Scan, Photos, Sign / OTP, Cash, Finish. Every step shows Required or Done; the driver can move back and forth. The server never refuses a delivery that happened: a missing proof only raises a flag for the dispatcher, and the last step lists what the dispatcher will be told.
StepWhat it does
1 ArrivedConfirms the arrival (automatic by the geofence, or by hand) and records the time and the distance.
2 ScanScan a package with the camera or type the code, or Mark all as delivered. Every line shows the ordered quantity and a stepper for the delivered quantity: a lower quantity asks for a reason and makes a partial delivery.
3 PhotosThe camera shows the watermark live (latitude, longitude, accuracy, time and the delivery number) and burns it into the picture. Photo types: doorstep drop, open box / serial, damaged parcel, place of delivery. Without a live camera the system camera or the gallery can be used. Photos are encoded to WebP with flutter_image_compress; when a device cannot, the photo is kept as JPEG and the screen says so (the server accepts WebP, JPEG and PNG).
4 Sign / OTPThe receiver signs on the canvas (smoothed strokes, Clear), his printed name and his relationship (recipient, neighbor, receptionist, concierge, other). When the stop needs the customer's code the driver types it and presses Check code; Resend code asks Odoo for a new one.
5 CashThe amount expected, the total received and the payment methods (cash, cheque, card / POS, bank transfer / wallet) with the amount and the reference where the method needs one. A short collection is shown in red.
6 FinishThe summary (lines checked, photos, signature, receiver, cash) and the warnings, then Complete delivery.
Step 2: check the packages, with the delivered quantity of every line.
Step 2: check the packages, with the delivered quantity of every line.
The photo with the live watermark: position, accuracy, time and delivery number.
The photo with the live watermark: position, accuracy, time and delivery number.
Step 4: the signature, the printed name and the relationship.
Step 4: the signature, the printed name and the relationship.
The customer's one-time code, typed and checked.
The customer's one-time code, typed and checked.
In plain words: The customer's code. When your setting Offline code check on the phone is on, the phone receives a salted hash and checks the digits without a network; the server verifies them again and flags a mismatch. When it is off, the phone checks online when it can, or just sends the digits for the server to verify. After five wrong digits the stop is locked for ten minutes (chapter 14); the screen says so. The code itself never reaches the phone.
In plain words: Lots and serial numbers. For a tracked line the app shows Lots / serial numbers with the lots reserved in Odoo and their quantity, and the driver picks what he really delivers. With no pick, the reserved lots are delivered.
A serial-tracked line: the reserved serial number is offered, delivered 1 of 1.
A serial-tracked line: the reserved serial number is offered, delivered 1 of 1.
Step 5: cash on delivery, the payment method, the amount and the shortfall.
Step 5: cash on delivery, the payment method, the amount and the shortfall.

The driver app: failed stops, outbox and cash settlement

In plain words: Cannot deliver. From the stop the driver opens Cannot deliver: he chooses one of the reasons configured in Odoo (customer not home, access restricted, wrong address, refused...), takes the photo when the reason requires one, writes the note when it requires one, and presses Finish as failed. The delivery order stays ready in Odoo and the dispatcher gets a to-do to reschedule.
Cannot deliver: the reason, the photo (camera, gallery or sample) and Finish as failed.
Cannot deliver: the reason, the photo (camera, gallery or sample) and Finish as failed.
In plain words: Outbox. Every action is written in a local database first and then sent in order with its unique ID. The Outbox tab shows what has not reached the server yet (stop proofs, GPS points, settlement), a sync button, the Synced or Offline badge and a Sync log. A background task also tries to send the queue every 15 minutes or so (the system decides). Signing out warns when operations are still waiting, and removes the local data.
The Outbox tab: what is waiting, and the sync log.
The Outbox tab: what is waiting, and the sync log.
In plain words: Cash settlement. The Cash tab lists what the driver collected today per method, warns when stops are still open, and lets him type what he hands in per method (the button Use expected fills it). Submitting sends the declaration; the hub cashier counts and a manager approves in Odoo (chapter 18). The screen shows the status of the sheet, and a follow-up sheet when late collections need one.
Cash settlement: collected today per method, and what the driver hands in.
Cash settlement: collected today per method, and what the driver hands in.

The driver app: settings, languages and themes

In plain words: Settings shows the driver, vehicle, plate and company, and lets him choose the language (English or Arabic, with full right-to-left layout), the theme (system, light or dark), high contrast (black and white with thick borders for sunlight), the navigation app, a photo preset (smaller or larger pictures) and the location permission card that explains why "Allow all the time" is needed. On a tablet the layout switches to a navigation rail.
Settings: driver, language, theme, high contrast and the navigation app.
Settings: driver, language, theme, high contrast and the navigation app.
The same run in dark, high contrast and Arabic (right to left).
The same run in dark, high contrast and Arabic (right to left).

Offline behaviour

SituationWhat happens
No network at the doorThe status changes, photos, signature, code, cash and the outcome are saved in a local database queue on the phone.
Network is backThe queue is sent in order; photos are compressed to WebP and streamed; every call carries a unique ID.
The answer is lostThe app repeats the call; the server answers with the stored result, nothing is done twice.
The dispatcher edits the run meanwhileRe-ordering, reassignment and cancellation reach the phone with the next refresh (every 2 minutes by default).
A stop was already closedA second completion of a closed stop is refused (409).
One day at a timeThe phone caches the run of today. Yesterday's operations that were not sent are still sent. A stop that the dispatcher removes after the driver worked on it stays on the phone, marked, so queued work is not lost.
In plain words: Field reality wins. Only malformed data, unknown or foreign records and closed stops are rejected. Everything else is accepted and flagged.

The API (for developers)

In plain words: The app talks to /api/v1/epod: ping, login, logout, config, manifest, status, location, otp/verify, otp/resend, complete, settlement and changes. Authentication is a bearer token (stored hashed); a login replaces the token of the same device. API.md in the package is the complete contract.
# is the server reachable?
curl https://odoo.example.com/api/v1/epod/ping
# {"ok": true, "api_version": 1, "server_time": "2026-10-28T06:00:00Z"}

# sign in (returns a token, the vehicle and the configuration)
curl -X POST https://odoo.example.com/api/v1/epod/login -H 'Content-Type: application/json' \
  -d '{"login": "omar@atlas-trading.example", "password": "...", "device": {"id": "abc", "name": "Pixel 7", "platform": "android", "app_version": "1.0.0"}}'

# today's run of the driver
curl https://odoo.example.com/api/v1/epod/manifest -H "Authorization: Bearer $TOKEN"

# complete a stop (multipart: the JSON payload and the photo files; every call has an op_uuid, repeating it is safe)
curl -X POST https://odoo.example.com/api/v1/epod/complete -H "Authorization: Bearer $TOKEN" \
  -F 'payload={"op_uuid": "5f0e2c1a-8d7b-4c10-9a55-0d6a1b2c3d4e", "local_seq": 12, "stop_id": 101, "outcome": "delivered", "completed_at": "2026-10-28T09:30:00Z",
        "location": {"lat": 25.2, "lng": 55.27, "accuracy": 7.5}, "receiver": {"name": "Sara Ali", "relationship": "self"},
        "media": [{"field": "photo_0", "kind": "doorstep"}], "signature": {"field": "signature", "strokes": 41}}' \
  -F 'photo_0=@door.webp' -F 'signature=@signature.png'
RuleDetail
IdempotencyEvery state-changing call has op_uuid and local_seq. A repeat answers 200 with duplicate: true and the stored result. A rejected call can be sent again with the same ID.
ErrorsHTTP status plus {error: {code, message}}: 400 invalid_payload, 401 unauthorized, 403 forbidden / not_driver / otp_locked, 404 not_found, 409 stop_closed / conflict / settlement_closed, 413 too_large.
LanguageAccept-Language selects the language of the server texts.
OwnershipA driver can only touch his own stops; another driver's stop or operation ID is refused with 403.

Security notes

TopicWhat the module does
TokensStored as SHA-256 hashes; revoked on logout; devices are listed and a manager can only switch them off (the token and the user cannot be rewritten).
Customer codesServer-side keyed hash plus a separate salted hash for the phone; the code is never stored and the e-mail with it cannot be read by drivers; 5 tries per window, 15 in total, resends limited to 3.
UploadsPhotos and signatures are checked by their content (WebP, JPEG, PNG), their size in bytes, their size in pixels (at most 12000 px a side and 40 megapixels) and their number per stop.
WorkflowState and proof fields are written only by the workflow with the right role; checks use the user and groups, never a value sent by the client.
EvidencePhotos, signatures, lines and collections cannot be edited or deleted through the screens or RPC.
PrivacyOnly the driver of the run and the dispatchers see a stop; the GPS trail is deleted after the retention period.

Limits and what is not verified

In plain words: Please read this before you promise anything to your customers.
TopicLimit
SMSNo SMS or WhatsApp channel is built in: the code goes by e-mail. The hook _epod_send_otp lets a developer add another channel.
Offline code checkThe hash check on the phone can be brute-forced by someone who holds the phone. Such proof is recorded as checked on the phone and flagged; the server cannot make it stronger. Switch the setting OFF for server-side proof (chapter 14).
NavigationNo turn-by-turn guidance inside the app: it opens Google Maps, Waze or Apple Maps through links.
Route optimizationStraight-line distance, nearest neighbour and 2-opt; no traffic, no time windows, no vehicle capacity.
Cash to collectRead from posted invoices and, with Sales, from the order. An order that was prepaid online without an invoice would be shown as cash to collect: set No cash on delivery on the stop.
SettlementPayments are created in the company currency; invoices in another currency are not reconciled automatically. A shortage is booked against the liquidity account of the journal of the method, which is a simplification if your payment methods use outstanding accounts.
Ready stockOnly delivery orders that are ready with all stock reserved can go on a run. If the reservation disappears before the door, the delivery is recorded but Odoo is not asked to validate it (no negative stock): the stop shows the problem.
Tested hereThe module has 217 automated tests on a fresh database (the reports were rendered as HTML in them; e-mail was tested up to the outgoing queue). The app has 356 Dart tests, flutter analyze is clean, and it was run on the iOS simulator against this module with a scripted GPS.
No physical deviceThe app was not run on a physical phone and not on an Android emulator: only the Android debug build was verified; all runtime checks were done on the iOS simulator. The screenshots of this guide come from the simulator.
Real camera and scannerSimulators have no camera: the live preview, the torch, the camera switch, the system camera and the barcode scanner with a real camera were not tried (photos came from a debug sample image and the gallery path; manual code entry and the matching logic are tested).
Background geofenceThe geofence with the screen off on a real device (Android foreground service, iOS "Always" permission) was not verified, nor the notification in the background nor the Android "all the time" permission flow. The logic is unit-tested and the foreground flow ran with a scripted GPS. In basements or garages the app says the position is not available and the dispatcher gets GPS missing.
Navigation appsApple Maps (deep link, return, Welcome back) was verified on the simulator; Google Maps, Waze and the Android geo: intents were not opened on a device.
Release builds and tabletsRelease builds (signing, shrinking, App Store archive) were not made, and the tablet layout is covered by a widget test only.
Only unit-testedThe follow-up settlement sheet in the app, the 413 answer for an oversized photo, otp_expired and the dispatcher's unlock were covered by unit and widget tests, not run against the server from the app.
Long offline periodsHundreds of photos in the outbox were not tried; the queue is limited only by the phone's storage.
Real mail and PDFA real mail server and the PDF engine of a production Odoo were not part of the tests of the module.

Troubleshooting

ProblemWhat to check
The driver cannot sign inHe needs an internal user with the role Driver (error not_driver otherwise) and the right password; accounts with two-factor authentication cannot use the app.
No stops on the phoneThe run must be Confirmed or In progress and its date must be today in the time zone of the driver. Check the user's time zone.
The arrival never triggersThe customer needs a position; check the radius; check that the phone allows location all the time.
The delivery order was not validatedOpen the stop: the red message says why (for example an unknown serial number). Fix it and press Validate the delivery order.
The customer got no codeThe contact needs an e-mail address and Odoo needs an outgoing mail server. The stop shows where the code was sent.
Approval says to choose a journalSettings, Cash on delivery and settlement, choose the journals.
Photos are refusedLarger than the limit, not WebP/JPEG/PNG, or more than the maximum per stop.
See what the phone sentConfiguration, Sync log shows every call with its answer.

Update to a new version

Back up your database, replace the module folder with the new version, restart Odoo and run -u taqnix_epod_core. Runs, proofs, settlements and settings are kept. Rebuild the app from the new source when the changelog says the API changed.

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.