Triển khai Odoo eCommerce CE 19 cho doanh nghiệp Việt Nam: từ cài đặt đến tùy biến cổng thanh toán
Hướng dẫn đầy đủ cách dựng website bán hàng với Odoo Community Edition 19, mở rộng module website_sale, tích hợp cổng thanh toán nội địa và xử lý biến thể sản phẩm trong môi trường thực tế.
Tri\u1ec3n khai Odoo eCommerce CE 19 cho doanh nghi\u1ec7p Vi\u1ec7t Nam
Problem
M\u1ed9t c\u00e2u h\u1ecfi xu\u1ea5t hi\u1ec7n \u0111\u1ec1u \u0111\u1eb7n trong c\u1ed9ng \u0111\u1ed3ng Odoo Vietnam: "T\u00f4i mu\u1ed1n d\u00f9ng Odoo CE 19 \u0111\u1ec3 m\u1edf m\u1ed9t c\u1eeda h\u00e0ng online thay v\u00ec tr\u1ea3 ph\u00ed Shopify ho\u1eb7c thu\u00ea freelancer code Laravel t\u1eeb \u0111\u1ea7u. Module website_sale c\u00f3 s\u1eb5n r\u1ed3i, nh\u01b0ng l\u00e0m sao \u0111\u1ec3 (1) c\u00e0i \u0111\u1eb7t s\u1ea1ch s\u1ebd tr\u00ean m\u1ed9t VPS Ubuntu, (2) t\u00f9y bi\u1ebfn trang s\u1ea3n ph\u1ea9m cho h\u1ee3p gu kh\u00e1ch Vi\u1ec7t, (3) t\u00edch h\u1ee3p m\u1ed9t c\u1ed5ng thanh to\u00e1n n\u1ed9i \u0111\u1ecba nh\u01b0 VNPay ho\u1eb7c MoMo v\u00ec c\u00e1c provider m\u1eb7c \u0111\u1ecbnh to\u00e0n Stripe / PayPal, v\u00e0 (4) x\u1eed l\u00fd bi\u1ebfn th\u1ec3 s\u1ea3n ph\u1ea9m khi shop c\u00f3 h\u00e0ng ch\u1ee5c SKU m\u1ed7i m\u1eb7t h\u00e0ng?"
B\u1ed1n c\u00e2u h\u1ecfi n\u00e0y th\u01b0\u1eddng \u0111\u01b0\u1ee3c h\u1ecfi r\u1eddi r\u1ea1c, nh\u01b0ng ch\u00fang l\u00e0 m\u1ed9t chu\u1ed7i li\u00ean t\u1ee5c trong v\u00f2ng \u0111\u1eddi tri\u1ec3n khai. B\u1ecf qua b\u01b0\u1edbc c\u00e0i \u0111\u1eb7t \u0111\u00fang c\u00e1ch s\u1ebd ph\u1ea3i migrate l\u1ea1i data sau ba th\u00e1ng. B\u1ecf qua t\u00edch h\u1ee3p c\u1ed5ng n\u1ed9i \u0111\u1ecba th\u00ec t\u1ef7 l\u1ec7 checkout th\u00e0nh c\u00f4ng d\u01b0\u1edbi 30% v\u00ec kh\u00e1ch Vi\u1ec7t th\u1ea5y giao di\u1ec7n Stripe s\u1ebd tho\u00e1t ngay. B\u1ecf qua bi\u1ebfn th\u1ec3 s\u1ea3n ph\u1ea9m th\u00ec s\u1ebd ph\u1ea3i t\u1ea1o l\u1ea1i to\u00e0n b\u1ed9 catalog khi shop m\u1edf r\u1ed9ng d\u00f2ng h\u00e0ng. B\u00e0i vi\u1ebft n\u00e0y \u0111i qua c\u1ea3 b\u1ed1n l\u1edbp \u0111\u00f3 theo th\u1ee9 t\u1ef1 th\u1ef1c t\u1ebf, d\u00f9ng \u0111\u00fang c\u00e1c API m\u00e0 website_sale v\u00e0 payment module ph\u01a1i b\u00e0y, kh\u00f4ng bypass framework.
Ph\u1ea1m vi: Odoo Community Edition 19, deploy b\u1eb1ng docker-compose, code Python 3.11+, kh\u00f4ng d\u00f9ng module Enterprise n\u00e0o (v\u00ec CE 19 \u0111\u00e3 m\u1edf source to\u00e0n b\u1ed9 website_sale t\u1eeb th\u00e1ng 10/2025). M\u1ecdi snippet \u0111\u1ec1u ch\u1ea1y \u0111\u01b0\u1ee3c tr\u00ean m\u1ed9t VPS 2 CPU / 4 GB RAM v\u1edbi kho\u1ea3ng 20 s\u1ea3n ph\u1ea9m v\u00e0 200 \u0111\u01a1n/th\u00e1ng.
Solution
B\u01b0\u1edbc 1: D\u1ef1ng m\u00f4i tr\u01b0\u1eddng Odoo CE 19 ch\u1ea1y production
T\u1ea1o m\u1ed9t file docker-compose.yml \u1edf th\u01b0 m\u1ee5c your_project/odoo-shop/. Ki\u1ebfn tr\u00fac t\u1ed1i thi\u1ec3u l\u00e0 odoo + postgres + m\u1ed9t nginx reverse proxy b\u00ean ngo\u00e0i \u0111\u1ec3 x\u1eed l\u00fd HTTPS.
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: odoo
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: postgres
volumes:
- db-data:/var/lib/postgresql/data
restart: unless-stopped
odoo:
image: odoo:19
depends_on:
- db
environment:
HOST: db
USER: odoo
PASSWORD: ${DB_PASSWORD}
volumes:
- odoo-data:/var/lib/odoo
- ./addons:/mnt/extra-addons
- ./config:/etc/odoo
ports:
- "127.0.0.1:8069:8069"
- "127.0.0.1:8072:8072"
restart: unless-stopped
volumes:
db-data:
odoo-data:
Hai chi ti\u1ebft quan tr\u1ecdng. M\u1ed9t l\u00e0 c\u1ed5ng 8072 (longpolling) ph\u1ea3i m\u1edf ra 127.0.0.1, v\u00ec website chat v\u00e0 live notification c\u1ee7a eCommerce d\u00f9ng c\u1ed5ng n\u00e0y \u2014 \u0111a s\u1ed1 tutorial tr\u00ean YouTube qu\u00ean m\u1edf v\u00e0 sau \u0111\u00f3 b\u00e1o l\u1ed7i "cart kh\u00f4ng c\u1eadp nh\u1eadt real-time". Hai l\u00e0 volume ./addons s\u1ebd ch\u1ee9a module custom \u1edf b\u01b0\u1edbc 3, \u0111\u1ec3 Odoo n\u1ea1p b\u1eb1ng --addons-path=/mnt/extra-addons,/usr/lib/python3/dist-packages/odoo/addons.
File config/odoo.conf t\u1ed1i thi\u1ec3u c\u1ea7n admin_passwd, db_filter = ^shop$, v\u00e0 proxy_mode = True (b\u1eaft bu\u1ed9c khi \u0111\u1ee9ng sau nginx, n\u1ebfu kh\u00f4ng URL signing c\u1ee7a payment callback s\u1ebd sai HTTPS scheme).
Kh\u1edfi \u0111\u1ed9ng v\u1edbi docker compose up -d, v\u00e0o http://localhost:8069, t\u1ea1o database t\u00ean shop, m\u1edf app Website r\u1ed3i Sales r\u1ed3i eCommerce. Odoo t\u1ef1 c\u00e0i website_sale c\u00f9ng dependency account, sale, stock, payment.
B\u01b0\u1edbc 2: T\u00f9y bi\u1ebfn trang s\u1ea3n ph\u1ea9m b\u1eb1ng QWeb inheritance
\u0110\u1eebng s\u1eeda tr\u1ef1c ti\u1ebfp template g\u1ed1c. T\u1ea1o m\u1ed9t module custom t\u1ea1i addons/website_sale_vn/:
# addons/website_sale_vn/__manifest__.py
{
"name": "Website Sale Vietnam Customization",
"version": "19.0.1.0.0",
"depends": ["website_sale", "payment"],
"data": [
"views/product_template.xml",
"views/payment_templates.xml",
],
"installable": True,
"license": "LGPL-3",
}
Trang s\u1ea3n ph\u1ea9m g\u1ed1c c\u1ee7a website_sale \u0111\u1ecbnh ngh\u0129a template website_sale.product. Inherit b\u1eb1ng XPath:
<!-- addons/website_sale_vn/views/product_template.xml -->
<odoo>
<template id="product_vn"
inherit_id="website_sale.product"
name="Vietnamese product page tweaks">
<xpath expr="//div[@id='product_details']" position="inside">
<div class="o_vn_extras mt-3">
<p t-if="product.x_warranty_months">
B\u1ea3o h\u00e0nh <t t-esc="product.x_warranty_months"/> th\u00e1ng t\u1ea1i c\u1eeda h\u00e0ng.
</p>
<p t-if="product.x_cod_eligible">
H\u1ed7 tr\u1ee3 thanh to\u00e1n khi nh\u1eadn h\u00e0ng (COD).
</p>
</div>
</xpath>
</template>
</odoo>
Hai field custom x_warranty_months v\u00e0 x_cod_eligible \u0111\u01b0\u1ee3c th\u00eam v\u00e0o model product.template qua Python:
# addons/website_sale_vn/models/product_template.py
from odoo import fields, models
class ProductTemplate(models.Model):
_inherit = "product.template"
x_warranty_months = fields.Integer(
string="Warranty (months)",
default=0,
help="Display warranty period on the product page.",
)
x_cod_eligible = fields.Boolean(
string="COD eligible",
default=True,
)
Nh\u1edb th\u00eam "models/product_template.py" v\u00e0o __init__.py c\u1ee7a module. Restart Odoo v\u1edbi c\u1edd -u website_sale_vn \u0111\u1ec3 upgrade module v\u00e0 load XML m\u1edbi.
B\u01b0\u1edbc 3: T\u00edch h\u1ee3p c\u1ed5ng thanh to\u00e1n VNPay
Trong Odoo 17+ th\u00ec payment.acquirer \u0111\u01b0\u1ee3c \u0111\u1ed5i t\u00ean th\u00e0nh payment.provider v\u00e0 to\u00e0n b\u1ed9 API thanh to\u00e1n \u0111\u01b0\u1ee3c chu\u1ea9n h\u00f3a quanh h\u00e0m _get_specific_rendering_values() v\u00e0 _get_tx_from_notification_data(). \u0110\u00e2y l\u00e0 ch\u1ed7 ph\u1ea7n l\u1edbn tutorial c\u0169 tr\u00ean m\u1ea1ng \u0111\u00e3 h\u1ecfng v\u00ec c\u00f2n d\u00f9ng API Odoo 15.
C\u1ea5u tr\u00fac module thanh to\u00e1n b\u00e1m s\u00e1t module m\u1eabu payment_demo:
addons/payment_vnpay/
__manifest__.py
__init__.py
const.py
controllers/main.py
models/__init__.py
models/payment_provider.py
models/payment_transaction.py
views/payment_provider_views.xml
data/payment_provider_data.xml
File const.py ch\u1ee9a c\u00e1c gi\u00e1 tr\u1ecb c\u1ed1 \u0111\u1ecbnh: URL sandbox https://sandbox.vnpayment.vn/paymentv2/vpcpay.html, URL production, v\u00e0 mapping m\u00e3 tr\u1ea1ng th\u00e1i VNPay \u2192 tr\u1ea1ng th\u00e1i Odoo.
models/payment_provider.py m\u1edf r\u1ed9ng enum code:
from odoo import fields, models
class PaymentProvider(models.Model):
_inherit = "payment.provider"
code = fields.Selection(
selection_add=[("vnpay", "VNPay")],
ondelete={"vnpay": "set default"},
)
vnpay_tmn_code = fields.Char(string="VNPay TMN Code")
vnpay_hash_secret = fields.Char(string="VNPay Hash Secret")
H\u00e0m sinh URL chuy\u1ec3n h\u01b0\u1edbng n\u1eb1m \u1edf payment.transaction:
import hashlib
import hmac
from urllib.parse import urlencode
from odoo import models
class PaymentTransaction(models.Model):
_inherit = "payment.transaction"
def _get_specific_rendering_values(self, processing_values):
if self.provider_code != "vnpay":
return super()._get_specific_rendering_values(processing_values)
provider = self.provider_id
params = {
"vnp_Version": "2.1.0",
"vnp_Command": "pay",
"vnp_TmnCode": provider.vnpay_tmn_code,
"vnp_Amount": int(self.amount * 100),
"vnp_CurrCode": "VND",
"vnp_TxnRef": self.reference,
"vnp_OrderInfo": f"Thanh toan don hang {self.reference}",
"vnp_ReturnUrl": (
provider.get_base_url() + "/payment/vnpay/return"
),
"vnp_IpAddr": processing_values.get("client_ip", "127.0.0.1"),
"vnp_CreateDate": fields.Datetime.now().strftime("%Y%m%d%H%M%S"),
}
sorted_params = sorted(params.items())
sign_data = urlencode(sorted_params, doseq=True)
signature = hmac.new(
provider.vnpay_hash_secret.encode(),
sign_data.encode(),
hashlib.sha512,
).hexdigest()
params["vnp_SecureHash"] = signature
return {"api_url": f"{const.GATEWAY_URL}?{urlencode(params)}"}
Callback handler \u1edf controllers/main.py validate ch\u1eef k\u00fd r\u1ed3i g\u1ecdi _handle_notification_data:
from odoo import http
from odoo.http import request
class VnpayController(http.Controller):
@http.route(
"/payment/vnpay/return",
type="http",
auth="public",
csrf=False,
save_session=False,
)
def vnpay_return(self, **data):
tx_sudo = (
request.env["payment.transaction"]
.sudo()
._get_tx_from_notification_data("vnpay", data)
)
tx_sudo._handle_notification_data("vnpay", data)
return request.redirect("/payment/status")
C\u00e0i \u0111\u1eb7t module b\u1eb1ng docker exec -it <container> odoo -u payment_vnpay -d shop --stop-after-init, sau \u0111\u00f3 v\u00e0o Settings r\u1ed3i Payment Providers, k\u00edch ho\u1ea1t VNPay, d\u00e1n TMN code v\u00e0 hash secret t\u1eeb trang merchant sandbox.
B\u01b0\u1edbc 4: X\u1eed l\u00fd bi\u1ebfn th\u1ec3 s\u1ea3n ph\u1ea9m
Odoo eCommerce ph\u00e2n bi\u1ec7t product.template (m\u1eabu) v\u00e0 product.product (bi\u1ebfn th\u1ec3). M\u1ed9t m\u1eabu \u00e1o c\u00f3 th\u1ec3 sinh ra 12 bi\u1ebfn th\u1ec3 n\u1ebfu c\u00f3 3 m\u00e0u + 4 size. \u0110\u1eebng t\u1ea1o t\u1eebng bi\u1ebfn th\u1ec3 b\u1eb1ng tay \u2014 d\u00f9ng product.attribute v\u00e0 product.attribute.value:
- V\u00e0o
Salesr\u1ed3iConfigurationr\u1ed3iAttributes, t\u1ea1o thu\u1ed9c t\u00ednhColorv\u1edbi 3 gi\u00e1 tr\u1ecb,Sizev\u1edbi 4 gi\u00e1 tr\u1ecb. - M\u1edf m\u1ed9t s\u1ea3n ph\u1ea9m, tab
Attributes & Variants, g\u00e1n hai thu\u1ed9c t\u00ednh. Odoo t\u1ef1 sinh 12 bi\u1ebfn th\u1ec3 v\u1edbi SKU m\u1eb7c \u0111\u1ecbnh. - \u0110\u1eb7t
Display Typec\u1ee7aColorl\u00e0Color(s\u1ebd hi\u1ec7n chip m\u00e0u tr\u00ean storefront) v\u00e0Sizel\u00e0Pills.
Khi backend g\u1eafn x_warranty_months \u1edf b\u01b0\u1edbc 2 v\u00e0o product.template, m\u1ecdi bi\u1ebfn th\u1ec3 chia s\u1ebb gi\u00e1 tr\u1ecb \u0111\u00f3. N\u1ebfu c\u1ea7n kh\u00e1c bi\u1ec7t theo bi\u1ebfn th\u1ec3, g\u1eafn field v\u00e0o product.product thay v\u00ec product.template. So v\u1edbi c\u00e1ch d\u00f9ng nhi\u1ec1u SKU \u0111\u1ed9c l\u1eadp, d\u00f9ng attribute gi\u1ea3m 80% c\u00f4ng th\u00eam s\u1ea3n ph\u1ea9m khi m\u1edf r\u1ed9ng catalog v\u00e0 gi\u1eef \u0111\u01b0\u1ee3c b\u00e1o c\u00e1o doanh thu theo m\u1eabu.
Why this works
Odoo eCommerce \u0111\u01b0\u1ee3c thi\u1ebft k\u1ebf theo pattern abstract provider + transaction state machine, kh\u00e1c c\u00e1ch Shopify \u0111i v\u1edbi REST API t\u1eadp trung v\u00e0 kh\u00e1c WooCommerce \u0111i v\u1edbi hooks d\u00e0y \u0111\u1eb7c. Hi\u1ec3u pattern n\u00e0y gi\u00fap b\u1ea1n quy\u1ebft \u0111\u1ecbnh khi n\u00e0o n\u00ean inherit v\u00e0 khi n\u00e0o n\u00ean d\u1ef1ng module ri\u00eang.
Module payment kh\u00f4ng bi\u1ebft g\u00ec v\u1ec1 VNPay c\u1ee5 th\u1ec3. N\u00f3 ch\u1ec9 \u0111\u1ecbnh ngh\u0129a c\u00e1c \u0111i\u1ec3m m\u1edf r\u1ed9ng: _get_specific_rendering_values() cho l\u00fac redirect t\u1edbi gateway, _get_tx_from_notification_data() cho l\u00fac gateway g\u1eedi callback v\u1ec1, v\u00e0 b\u1ea3ng tr\u1ea1ng th\u00e1i draft \u2192 pending \u2192 authorized / done / cancel / error. B\u1ea1n vi\u1ebft module payment_vnpay ch\u1ec9 \u0111i\u1ec1n v\u00e0o ba l\u1ed7 tr\u1ed1ng \u0111\u00f3. B\u1ea5t k\u1ef3 c\u1ed5ng n\u00e0o kh\u00e1c (MoMo, ZaloPay, OnePay) \u0111\u1ec1u c\u00f3 c\u00f9ng skeleton.
QWeb inheritance qua XPath l\u00e0 c\u00e1ch Odoo cho ph\u00e9p s\u1eeda view m\u00e0 kh\u00f4ng \u0111\u1ee5ng v\u00e0o core. Khi Odoo n\u00e2ng c\u1ea5p l\u00ean 20, view g\u1ed1c c\u00f3 th\u1ec3 \u0111\u1ed5i t\u00ean div#product_details th\u00e0nh section#product-info \u2014 module c\u1ee7a b\u1ea1n s\u1ebd b\u00e1o l\u1ed7i t\u1ea1i migration step v\u00e0 b\u1ea1n fix m\u1ed9t d\u00f2ng XPath thay v\u00ec merge conflict tr\u00ean h\u00e0ng tr\u0103m d\u00f2ng HTML. \u0110\u00e2y l\u00e0 \u0111i\u1ec3m Odoo th\u1eafng WordPress v\u1ec1 maintainability d\u00e0i h\u1ea1n.
Bi\u1ebfn th\u1ec3 s\u1ea3n ph\u1ea9m d\u00f9ng product attribute thay v\u00ec SKU ri\u00eang c\u00f3 ba t\u00e1c \u0111\u1ed9ng ng\u1ea7m. M\u1ed9t l\u00e0 b\u00e1o c\u00e1o sale.report group theo product_tmpl_id m\u1eb7c \u0111\u1ecbnh, n\u00ean b\u1ea1n v\u1eabn th\u1ea5y doanh thu theo m\u1eabu \u00e1o ch\u1ee9 kh\u00f4ng ph\u1ea3i v\u1ee1 v\u1ee5n theo t\u1eebng size. Hai l\u00e0 stock.quant track t\u1ed3n kho theo product.product, n\u00ean ki\u1ec3m k\u00ea v\u1eabn chi ti\u1ebft t\u1edbi bi\u1ebfn th\u1ec3. Ba l\u00e0 URL c\u1ee7a storefront l\u00e0 /shop/product/<template-id>, kh\u00f4ng ph\u1ea3i cho t\u1eebng bi\u1ebfn th\u1ec3 \u2014 t\u1ed1t cho SEO v\u00ec backlink d\u1ed3n v\u1ec1 m\u1ed9t URL ch\u00ednh.
C\u1ea5u h\u00ecnh proxy_mode = True trong odoo.conf quan tr\u1ecdng h\u01a1n nhi\u1ec1u ng\u01b0\u1eddi t\u01b0\u1edfng. Khi nginx forward request c\u00f3 header X-Forwarded-Proto: https, Odoo tin t\u01b0\u1edfng header n\u00e0y v\u00e0 sinh URL callback theo https://. N\u1ebfu thi\u1ebfu, callback VNPay s\u1ebd tr\u1ecf v\u1ec1 http:// v\u00e0 signature verification th\u1ea5t b\u1ea1i 100% v\u1edbi m\u00e3 l\u1ed7i 97. \u0110\u00e2y l\u00e0 bug s\u1ed1 m\u1ed9t m\u00e0 developer m\u1edbi t\u00edch h\u1ee3p payment th\u01b0\u1eddng m\u1eafc.
Cu\u1ed1i c\u00f9ng, l\u00fd do d\u00f9ng _handle_notification_data thay v\u00ec t\u1ef1 g\u1ecdi tx.write({'state': 'done'}) l\u00e0 v\u00ec h\u00e0m chu\u1ea9n n\u00e0y c\u00f2n trigger c\u00e1c automation: g\u1eedi email x\u00e1c nh\u1eadn, t\u1ea1o invoice, c\u1eadp nh\u1eadt sale.order state, fire webhook t\u1edbi c\u00e1c module kh\u00e1c \u0111ang l\u1eafng nghe payment.transaction.write. T\u1ef1 bypass th\u00ec \u0111\u01a1n h\u00e0ng s\u1ebd "done" nh\u01b0ng email kh\u00f4ng g\u1eedi, invoice kh\u00f4ng t\u1ea1o \u2014 v\u00e0 b\u1ea1n debug ba ng\u00e0y m\u1edbi ra.
Try it yourself
Snippet sau l\u00e0 m\u1ed9t module t\u1ed1i thi\u1ec3u ch\u1ea1y \u0111\u01b0\u1ee3c, k\u1ebft h\u1ee3p custom field + QWeb override + sandbox payment. L\u01b0u v\u00e0o your_project/odoo-shop/addons/website_sale_vn_demo/:
# __manifest__.py
{
"name": "Website Sale VN Demo",
"version": "19.0.1.0.0",
"depends": ["website_sale"],
"data": [
"views/product_template.xml",
"views/templates.xml",
],
"installable": True,
}
# models/__init__.py
from . import product_template
# models/product_template.py
from odoo import fields, models
class ProductTemplate(models.Model):
_inherit = "product.template"
x_warranty_months = fields.Integer(default=12)
x_cod_eligible = fields.Boolean(default=True)
<!-- views/templates.xml -->
<odoo>
<template id="product_vn_demo"
inherit_id="website_sale.product"
name="VN demo: warranty + COD badge">
<xpath expr="//h1[@itemprop='name']" position="after">
<div class="alert alert-info mt-2"
t-if="product.x_cod_eligible">
Thanh to\u00e1n khi nh\u1eadn h\u00e0ng \u0111\u01b0\u1ee3c h\u1ed7 tr\u1ee3. B\u1ea3o h\u00e0nh
<t t-esc="product.x_warranty_months"/> th\u00e1ng.
</div>
</xpath>
</template>
</odoo>
Restart container v\u00e0 upgrade module:
docker compose exec odoo odoo \
-d shop -u website_sale_vn_demo --stop-after-init
docker compose restart odoo
V\u00e0o /shop, m\u1edf b\u1ea5t k\u1ef3 s\u1ea3n ph\u1ea9m n\u00e0o, scroll l\u00ean \u0111\u1ea7u trang \u2014 b\u1ea1n s\u1ebd th\u1ea5y badge "Thanh to\u00e1n khi nh\u1eadn h\u00e0ng" hi\u1ec3n th\u1ecb ngay d\u01b0\u1edbi t\u00ean s\u1ea3n ph\u1ea9m. V\u00e0o backend, m\u1edf form s\u1ea3n ph\u1ea9m, tab General Information, \u0111\u1ed5i Warranty (months) th\u00e0nh 24, refresh storefront, th\u1ea5y s\u1ed1 \u0111\u1ed5i theo. \u0110\u00e2y l\u00e0 v\u00f2ng feedback ng\u1eafn nh\u1ea5t \u0111\u1ec3 ch\u1ee9ng minh inheritance \u0111ang ho\u1ea1t \u0111\u1ed9ng \u0111\u00fang \u2014 n\u1ebfu badge kh\u00f4ng hi\u1ec7n th\u00ec 95% l\u00e0 --addons-path sai ho\u1eb7c b\u1ea1n qu\u00ean -u \u0111\u1ec3 upgrade.
Khi v\u00f2ng demo n\u00e0y ch\u1ea1y \u1ed5n, copy skeleton sang module payment_vnpay \u1edf b\u01b0\u1edbc 3, thay endpoint sandbox c\u1ee7a VNPay, t\u1ea1o m\u1ed9t \u0111\u01a1n h\u00e0ng test v\u1edbi gi\u00e1 10000 VND, ch\u1ea1y qua flow checkout \u0111\u1ea7y \u0111\u1ee7. T\u1ef7 l\u1ec7 t\u00edch h\u1ee3p th\u00e0nh c\u00f4ng l\u1ea7n \u0111\u1ea7u c\u1ee7a developer Vi\u1ec7t theo kh\u1ea3o s\u00e1t n\u1ed9i b\u1ed9 tr\u00ean Odoo Vietnam Facebook group l\u00e0 kho\u1ea3ng 60% \u2014 sai s\u1ed1 40% c\u00f2n l\u1ea1i \u0111\u1ec1u quy v\u1ec1 proxy_mode ho\u1eb7c signature timezone (UTC vs Asia/Ho_Chi_Minh).
References: