- APPS
- GHN Express Shipping 18.0
| Lines of Code | 754 |
| Technical name | viin_delivery_ghn |
| License | OPL-1 |
| Website | https://viindoo.com/apps/modules/18.0/viin_delivery_ghn |
| Read description for | |
| Required Apps | Invoicing (account) Discuss (mail) Inventory (stock) |
| Included Dependencies | API Request Logger Vietnam Delivery Connectors |
What it does
Connects Odoo to Giao Hàng Nhanh over its public REST API, so a transfer becomes a GHN parcel without anyone retyping an address into the carrier's own portal.
Key Features
- Rates and delivery estimates before you commit: GHN prices the exact route, weight and dimensions of the transfer, and reports the day it commits to delivering, both shown on the sales order.
- Booking that cannot double-charge your customer: the transfer reference is sent as GHN's own client order code, so a retried booking returns the parcel that already exists instead of creating a second one.
- Cash on delivery, with GHN's ceiling checked before the request leaves Odoo rather than after the carrier rejects it.
- Waybills in A5, 80x80 and 52x70, fetched and attached to the transfer.
- Cancellation while GHN still allows it, refused clearly once it does not.
- Realtime status over webhooks, with the full GHN status vocabulary mapped onto Odoo's, and every event kept as a timeline entry on the transfer.
- COD remittance reconciliation: GHN reports each transfer of collected cash on its webhook, and those reports are matched back to the deliveries they paid for, so short payments are visible.
- Both address maps: the two-level provinces and wards Vietnam introduced on 01/07/2025 for booking, and GHN's legacy three-level identifiers for its rate and lead-time endpoints, which still require them. Wards are fetched as they are needed rather than all at once.
- Staging and production as a switch on the delivery method, so an integration is proven before a real parcel moves.
Editions Supported
- Community Edition
- Enterprise Edition
GHN Express Shipping
This guide connects Odoo to Giao Hàng Nhanh so a delivery order becomes a GHN parcel: priced, booked, labelled and tracked without anyone opening GHN's own portal.
It assumes Vietnam Delivery Connectors is installed, which it is automatically with this app. That app's guide covers the parts common to every carrier - addressing, rate comparison, COD reconciliation - and this one covers what is specific to GHN.
Installation
- Go to Apps, remove the default Apps filter, search for GHN Express Shipping and click Install.
Getting your credentials
- Create an account at khachhang.ghn.vn for production, or at 5sao.ghn.dev for the staging environment. They are separate accounts: a staging token does not work in production.
- Open your account settings and copy the API token and the Shop ID.
- Give the shop a pickup address, if the sign-up flow did not ask for one.
That last step is not optional and is the most common thing to go wrong. A shop registered without an address is a perfectly valid account: the token works, the shop exists, and the catalogue downloads. But GHN refuses to price or book anything collected from it, with a bare HTTP 400 naming the shop. Test the connection checks for this and says so in one sentence, so run it before anything else.
Configuration
- Go to Inventory > Configuration > Delivery Methods and create a method with Provider set to GHN Express.
- On the Vietnam Shipping tab, fill in GHN Token and GHN Shop ID.
- Leave Test Environment on. Odoo then talks to dev-online-gateway.ghn.vn and no real parcel is created.
- Click Test the connection.
- Click Synchronise provinces and wards.
About that last step: Vietnam removed the district level on 01/07/2025, and GHN answers on both the new two-level map and the old three-level one. Neither covers the whole country on its own - measured against GHN's sandbox, Ha Noi and Ho Chi Minh City are refused on the new map and accepted on the old, while Hai Phong and Da Nang are the other way round. So both catalogues are imported, quotes and bookings are offered the map this delivery method is set to and fall back to the other, and an address is only reported as unroutable when GHN has refused both. The old catalogue's wards are fetched one district at a time, the first time something needs them, so the first quote into a city is slower than the ones after it.
Options worth setting
- GHN Service - leave on Choose by weight and the right service is picked from the parcel: GHN's light service under 20 kg, its heavy service at or above, and the heavy service whenever a transfer is packed into more than one box, which is what GHN requires. Forcing one of them is rarely what you want: measured on a real account, a 1 kg parcel from Hanoi to Ho Chi Minh City costs 34,000 ₫ on the light service and 210,000 ₫ on the heavy one, while a 25 kg parcel is cheaper on the heavy service than on the light one.
- Shipping Fee Paid By, Recipient May Inspect, Handover - the defaults for every parcel, changed per delivery in its Shipping block. GHN takes the fee payer as payment_type_id and the inspection rule as required_note; it always collects, so the handover is not sent.
- Declare Parcel Value - declares the goods value so a lost parcel is compensated. GHN charges a percentage of the declared value for this, capped at 5,000,000 ₫.
- COD Ceiling - GHN itself refuses more than 50,000,000 ₫ per parcel. Setting a lower ceiling catches an over-large collection while the user is still on the transfer.
Webhooks
Without this, a parcel's status only moves when Odoo asks - on the hourly poll. With it, the timeline updates as the courier moves, and the COD reconciliation becomes possible at all.
Before you start, check that your Odoo answers on a public HTTPS address. GHN calls you, from its own servers, so an Odoo reachable only on localhost or inside your office network will never receive a callback, however correctly it is registered.
- On the delivery method, click Generate a new secret, then copy the Webhook URL. It looks like https://your-odoo.example.com/viin_delivery/webhook/ghn/12.
- Log in to developer.ghn.vn, open the menu under your name at the top right and choose Webhook settings - or go straight to /account/webhook.
- On the Order tab, paste the URL into Endpoint URL.
- Under Custom headers, add X-Webhook-Secret with the secret you generated. This is the only thing that distinguishes a real GHN callback from anyone else who guesses the URL, because GHN does not sign its callbacks.
- Under Permissions, tick the events you want. See the table below for which ones matter.
- Leave Timeout and Retry count at their defaults unless you have a reason. Ten seconds and three retries suit an Odoo that is not under load.
- Press Create webhook. GHN caches the configuration, so allow about fifteen minutes before the first callback arrives.
What the connector does not do
GHN publishes no endpoint for asking a courier to try again after a failed delivery - its order APIs are create, update, cancel and return. Deliver Again therefore says so rather than pretending; arrange the next attempt with GHN directly, or accept the return once they give up.
Cancelling and returning are answered by GHN per parcel, inside an HTTP 200. A parcel it refuses - one already too far along to cancel, or not yet at a stage it will return - is reported with GHN's own reason, and the transfer is left alone. Nothing here reads a 200 as "done".
Which events to enable
| Event | What it does for you |
|---|---|
| switch_status | Moves the parcel through the timeline. Enable it. |
| cod | Fires when GHN transfers collected cash. Required for the COD reconciliation - see below. |
| create | Confirms GHN accepted the order. Odoo already knows, since it made the call; harmless to enable. |
| update_weight | GHN re-weighed the parcel. Worth having: it usually means the fee changed too. |
| update_cod, | The amount, fee or fee payer changed at GHN's end. Enable |
| update_fee, | them if anyone edits orders in GHN's portal as well as in |
| update_payment_type | Odoo. |
| update_partial_return | The recipient took part of the parcel and returned the rest. |
The four Extra data switches add fields to the payload rather than firing anything:
- pod - the proof-of-delivery link, sent once, on the way into delivered. Odoo stores it on the timeline entry as Proof of Delivery, which is what you show a customer who says a parcel never arrived. Worth enabling on its own.
- warehouse - the current warehouse name, shown as the event location.
- shipper - the courier's name and phone.
- fee - the fee breakdown.
What GHN expects back
Odoo answers 200 to everything it can read, including callbacks it decides not to act on, which is what you want: GHN treats anything else as a failure and tries again on a widening curve - 30 seconds, 2 minutes, 5 minutes, and on out to 12 hours - up to the retry count you set. A 4xx other than 408 or 429 is treated as permanent and the callback is dropped for good.
One thing to know if a callback ever does fail: GHN delivers in order per parcel, so a callback that keeps failing holds up the ones behind it for that same parcel until it succeeds or is given up. Other parcels are unaffected. When your server comes back, the backlog for that parcel is delivered in one pass, in the right order.
Repeats are normal. GHN can send the same event twice, and fires several callbacks for one change. Odoo drops the duplicates on the parcel, event type and time, so this costs you nothing.
Staging and production are separate accounts, and so are their webhooks: a configuration made on the staging portal does not carry over to production. When you go live, set it up again there.
Reconciling COD
GHN publishes no settlement report. What it does publish is a cod callback, fired when it transfers collected cash to you, and that is what the statement is built from. So the cod event has to be enabled on the webhook; without it, Fetch from Carrier says so rather than returning an empty statement.
Everything after that is the same as any other carrier - see the Vietnam Delivery Connectors guide.
Daily use
Click Send to Carrier on the delivery and the parcel is booked. The transfer name goes to GHN as its client_order_code, which GHN de-duplicates on: booking the same transfer twice returns the parcel that already exists rather than creating a second one, so a retry after a timeout is safe.
Print Waybill fetches the waybill in the size set on the delivery method - A5, 80×80 or 52×70 mm - and attaches it to the transfer.
Cancel works while GHN still allows it. Once the parcel is delivered, returned or already cancelled, Odoo refuses and tells you to raise a return with GHN instead.
Troubleshooting
"GHN does not recognise ... in its legacy address catalogue"
GHN refused this address on both maps. The commonest case is the island special zones created in 2025 - Bach Long Vi, Hoang Sa, Truong Sa, Con Dao: they exist only on the two-level map, and GHN's pricing engine runs on the old catalogue, which has no entry for them. Set the ward explicitly on the contact, or price the method with Odoo's own rules.
"Authorization header is required"
The token is empty or wrong for the environment. A staging token against production fails this way.
The fee comes back as zero
Check the parcel has a weight. GHN rejects a parcel weighing nothing; the Fallback Weight on the delivery method covers products with no weight set.
This software and associated files (the "Software") may only be used (executed, modified, executed after modifications) if you have purchased a valid license from the authors, typically via Odoo Apps, or if you have received a written agreement from the authors of the Software (see the COPYRIGHT file).
You may develop Odoo modules that use the Software as a library (typically by depending on it, importing it and using its resources), but without copying any source code or material from the Software. You may distribute those modules under the license of your choice, provided that this license is compatible with the terms of the Odoo Proprietary License (For example: LGPL, MIT, or proprietary licenses similar to this one).
It is forbidden to publish, distribute, sublicense, or sell copies of the Software or modified copies of the Software.
The above copyright notice and this permission notice must be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.