1. Tính năng chính
2. Cấu trúc module
2.1. Flow
2.2. Endpoint API
3. PAYMENT TRANSACTION MODEL
3.1. Define model
Model payment.online.transaction là model chính của module, quản lý toàn bộ thông tin giao dịch thanh toán.
class PaymentTransaction(models.Model):
_name = "payment.online.transaction"
_description = "Online Payment Transaction"
_inherit = ["mail.thread", "mail.activity.mixin"]
_order = "create_date desc"
name = fields.Char(string="Mã giao dịch", required=True, readonly=True, default=lambda self: _("New"), copy=False)
partner_id = fields.Many2one("res.partner", string="Khách hàng", required=True, tracking=True)
amount = fields.Monetary(string="Số tiền", required=True, tracking=True)
currency_id = fields.Many2one("res.currency", string="Tiền tệ", default=lambda self: self.env.company.currency_id)
provider = fields.Selection([("stripe", "Stripe"), ("vnpay", "VNPAY")], string="Nhà cung cấp", required=True, default="stripe", tracking=True)
state = fields.Selection([("draft", "Nháp"), ("pending", "Chờ thanh toán"), ("done", "Thành công"), ("failed", "Thất bại"), ("cancelled", "Đã hủy")], string="Trạng thái", default="draft", tracking=True)
transaction_id = fields.Char("Mã GD nhà cung cấp", readonly=True)
request_id = fields.Char("Request ID", readonly=True)
payment_url = fields.Char("Link thanh toán", readonly=True)
response_code = fields.Char("Mã phản hồi", readonly=True)
response_message = fields.Text("Thông báo phản hồi", readonly=True)
payment_date = fields.Datetime("Thời gian thanh toán", readonly=True)
note = fields.Text("Ghi chú")
description = fields.Char("Mô tả thanh toán")
3.2. State machine
3.3. Action methods
4. Tích hợp stripe
4.1. Giới thiệu
Stripe Checkout là giải pháp thanh toán hosted của Stripe, cung cấp giao diện thanh toán hoàn chỉnh, hỗ
trợ nhiều phương thức thanh toán và đa tiền tệ.
Ưu điểm:
Giao diện thanh toán đẹp, responsive
Hỗ trợ nhiều phương thức: Card, Apple Pay, Google Pay
PCI Compliance - không cần lưu thông tin thẻ
Hỗ trợ đa tiền tệ (135+ currencies)
Webhook đáng tin cậy
4.2 Cấu hình Stripe
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
stripe_publishable_key = fields.Char(string="Stripe Publishable Key", config_parameter="online_paying.stripe_publishable_key")
stripe_secret_key = fields.Char(string="Stripe Secret Key", config_parameter="online_paying.stripe_secret_key")
stripe_webhook_secret = fields.Char(string="Stripe Webhook Secret", config_parameter="online_paying.stripe_webhook_secret")
stripe_success_url = fields.Char(string="Stripe Success URL", config_parameter="online_paying.stripe_success_url")
stripe_cancel_url = fields.Char(string="Stripe Cancel URL", config_parameter="online_paying.stripe_cancel_url")
4.3 Method: action_create_stripe_payment()
def action_create_stripe_payment(self):
"""Tạo yêu cầu thanh toán Stripe Checkout Session"""
self.ensure_one()
if self.provider != "stripe":
raise UserError(_("Giao dịch này không phải thanh toán Stripe"))
config = self._get_stripe_config()
if not config["secret_key"]:
raise UserError(
_("Chưa cấu hình đầy đủ thông tin Stripe.\n"
"Vào SetYings → Online Payment để cấu hình.")
)
# Stripe uses smallest currency unit
currency_code = self.currency_id.name.lower() if self.currency_id else "usd"
# Stripe requires amount in smallest currency unit
if currency_code in ["vnd", "jpy", "krw"]:
amount = int(self.amount)
else:
amount = int(self.amount * 100)
order_info = self.description or f"Thanh toán đơn hàng {self.name}"
payload = {
"payment_method_types[]": "card",
"line_items[0][price_data][currency]": currency_code,
"line_items[0][price_data][product_data][name]": order_info,
"line_items[0][price_data][unit_amount]": str(amount),
"line_items[0][quantity]": "1",
"mode": "payment",
"success_url": f"{config['success_url']}?session_id={{CHECKOUT_SESSION_ID}}&transaction_id={self.id}",
"cancel_url": f"{config['cancel_url']}?transaction_id={self.id}",
"client_reference_id": str(self.id),
"metadata[odoo_transaction_id]": str(self.id),
"metadata[odoo_transaction_name]": self.name,
}
if self.partner_id.email:
payload["customer_email"] = self.partner_id.email
try:
response = requests.post(
"https://api.stripe.com/v1/checkout/sessions",
data=payload,
auth=(config["secret_key"], ""),
timeout=30
)
result = response.json()
if response.status_code == 200 and result.get("url"):
payment_url = result.get("url")
session_id = result.get("id")
self.write({
"state": "pending",
"request_id": session_id,
"transaction_id": session_id,
"payment_url": payment_url,
})
return {
"type": "ir.actions.act_url",
"url": payment_url,
"target": "new",
}
else:
error_msg = result.get("error", {}).get("message", "Unknown error")
raise UserError(_("Stripe Error: %s") % error_msg)
except requests.exceptions.RequestException as e:
raise UserError(_("Lỗi kết nối Stripe: %s") % str(e))
4.4 Xử lý Currency
Stripe yêu cầu số tiền phải truyền ở đơn vị nhỏ nhất của tiền tệ.
| Tiền tệ | Xử lý | Ví dụ |
|---|---|---|
| USD, EUR, GBP | ×100 (cents) | $10.50 → 1050 |
| VND, JPY, KRW | Giữ nguyên | 150,000đ → 150000 |
Ví dụ xử lý:
if currency_code in ["vnd", "jpy", "krw"]:
amount = int(self.amount)
else:
amount = int(self.amount * 100)
Ví dụ:
-
10.50 USD→1050 -
150000 VND→150000 -
2000 JPY→2000
Dưới đây là format Dev.to sạch, chỉ giữ phần có giá trị, code được format lại:
5: Tích hợp VNPAY
5.1 Tổng quan VNPAY
VNPAY là cổng thanh toán phổ biến tại Việt Nam, hỗ trợ:
- 🏦 Internet Banking (40+ ngân hàng)
- 💳 Thẻ nội địa (Napas)
- 💳 Thẻ quốc tế (Visa, MasterCard, JCB, Amex)
- 📱 Ví điện tử VNPAY
5.2 Cấu hình VNPAY
Thêm cấu hình vào res.config.settings
# VNPAY CONFIG
vnpay_tmn_code = fields.Char(
string="VNPAY Terminal ID (vnp_TmnCode)",
config_parameter="online_paying.vnpay_tmn_code",
help="Mã website tại VNPAY"
)
vnpay_hash_secret = fields.Char(
string="VNPAY Hash Secret (vnp_HashSecret)",
config_parameter="online_paying.vnpay_hash_secret",
help="Chuỗi bí mật tạo checksum"
)
vnpay_url = fields.Char(
string="VNPAY Payment URL",
config_parameter="online_paying.vnpay_url",
default="https://sandbox.vnpayment.vn/paymentv2/vpcpay.html"
)
vnpay_return_url = fields.Char(
string="VNPAY Return URL",
config_parameter="online_paying.vnpay_return_url"
)
5.3 Method: action_create_vnpay_payment()
def action_create_vnpay_payment(self):
"""Tạo yêu cầu thanh toán VNPAY"""
self.ensure_one()
import hashlib
import hmac
import urllib.parse
from datetime import datetime, timedelta, timezone
if self.provider != "vnpay":
raise UserError(_("Giao dịch này không phải thanh toán VNPAY"))
config = self._get_vnpay_config()
if not config["tmn_code"] or not config["hash_secret"]:
raise UserError(
_("Chưa cấu hình đầy đủ thông tin VNPAY")
)
# VNPAY chỉ hỗ trợ VND
if self.currency_id.name != "VND":
raise UserError(_("VNPAY chỉ hỗ trợ thanh toán bằng VND"))
vnp_txn_ref = f"{self.id}-{datetime.now().strftime('%Y%m%d%H%M%S')}"
# VNPAY yêu cầu số tiền x100
vnp_amount = int(self.amount * 100)
order_info = self.description or f"Thanh toan don hang {self.name}"
order_info = self._remove_diacritics(order_info)
vn_tz = timezone(timedelta(hours=7))
vn_now = datetime.now(vn_tz)
vnp_create_date = vn_now.strftime("%Y%m%d%H%M%S")
vnp_expire_date = (
vn_now + timedelta(minutes=60)
).strftime("%Y%m%d%H%M%S")
vnp_params = {
"vnp_Version":"2.1.0",
"vnp_Command":"pay",
"vnp_TmnCode":config["tmn_code"],
"vnp_Amount":str(vnp_amount),
"vnp_CurrCode":"VND",
"vnp_TxnRef":vnp_txn_ref,
"vnp_OrderInfo":order_info,
"vnp_OrderType":"other",
"vnp_Locale":"vn",
"vnp_ReturnUrl":config["return_url"],
"vnp_IpAddr":"127.0.0.1",
"vnp_CreateDate":vnp_create_date,
"vnp_ExpireDate":vnp_expire_date
}
Tiếp theo tạo chữ ký:
sorted_params = sorted(vnp_params.items())
hash_data = "&".join([
f"{urllib.parse.quote_plus(str(k))}={urllib.parse.quote_plus(str(v))}"
for k, v in sorted_params
])
secure_hash = hmac.new(
config["hash_secret"].encode("utf-8"),
hash_data.encode("utf-8"),
hashlib.sha512
).hexdigest()
Build payment URL:
query_string = urllib.parse.urlencode(sorted_params)
payment_url = (
f"{config['vnpay_url']}?"
f"{query_string}"
f"&vnp_SecureHash={secure_hash}"
)
self.write({
"state":"pending",
"request_id":vnp_txn_ref,
"payment_url":payment_url
})
return {
"type":"ir.actions.act_url",
"url":payment_url,
"target":"new"
}
5.4 Thuật toán tạo chữ ký VNPAY
INPUT:
- vnp_params
- hash_secret
STEP 1:
Sort parameters alphabetically
STEP 2:
Build hash string
key1=value1&key2=value2...
(URL encode key + value)
STEP 3:
Generate SHA512 HMAC
secure_hash = HMAC_SHA512(
hash_secret,
hash_data
)
STEP 4:
Return secure_hash.hexdigest()
5.5 Xử lý dấu tiếng Việt
VNPAY không hỗ trợ Unicode trong vnp_OrderInfo.
Ví dụ:
"Thanh toán đơn hàng"
↓
"Thanh toan don hang"
Implementation:
def _remove_diacritics(self, text):
"""Remove Vietnamese diacritics"""
import unicodedata
import re
text = unicodedata.normalize("NFD", text)
text = re.sub(
r'[\u0300-\u036f]',
'',
text
)
text = (
text.replace('đ','d')
.replace('Đ','D')
)
return text
5.6 Mã lỗi VNPAY
| Code | Ý nghĩa |
|---|---|
| 00 | Giao dịch thành công |
| 07 | Giao dịch nghi ngờ |
| 09 | Chưa đăng ký Internet Banking |
| 10 | Xác thực sai quá 3 lần |
| 11 | Hết hạn thanh toán |
| 12 | Tài khoản bị khóa |
| 13 | Sai OTP |
| 24 | Khách hàng hủy giao dịch |
| 51 | Không đủ số dư |
| 65 | Vượt hạn mức giao dịch |
| 75 | Ngân hàng bảo trì |
| 79 | Sai mật khẩu thanh toán |
| 99 | Lỗi khác |
6.Controllers & Webhooks
6.1 Tổng quan
| Controller | File | Chức năng |
|---|---|---|
| OnlinePaymentController | main.py | Trang trạng thái |
| StripeWebhookController | stripe.py | Xử lý webhook Stripe |
| VNPayController | vnpay.py | Callback VNPAY |
6.2 Main Controller
class OnlinePaymentController(http.Controller):
@http.route(
"/payment/status",
type="http",
auth="public",
methods=["GET"],
csrf=False,
website=True
)
def payment_status(self, **kwargs):
transaction_id = kwargs.get(
"transaction_id"
)
status = kwargs.get(
"status",
"unknown"
)
values = {
"transaction_id":transaction_id,
"status":status,
"message":self._get_status_message(status)
}
return request.render(
"online_paying.payment_status_page",
values
)
6.3 Stripe Webhook Controller
class StripeWebhookController(http.Controller):
@http.route(
"/payment/stripe/webhook",
type="json",
auth="public",
methods=["POST"],
csrf=False
)
def stripe_webhook(self, **kwargs):
payload = request.get_json_data()
event_type = payload.get("type","")
if event_type=="checkout.session.completed":
return self._handle_checkout_completed(payload)
elif event_type=="checkout.session.expired":
return self._handle_checkout_expired(payload)
return {"status":"ignored"}
6.4 VNPAY Controller
class VNPayController(http.Controller):
@http.route(
"/payment/vnpay/return",
type="http",
auth="public",
methods=["GET"],
csrf=False,
website=True
)
def vnpay_return(self, **kwargs):
vnp_response_code=kwargs.get(
"vnp_ResponseCode"
)
vnp_txn_ref=kwargs.get(
"vnp_TxnRef"
)
if not self._verify_vnpay_signature(kwargs):
return request.redirect(
"/payment/status?status=failed"
)
transaction_id=vnp_txn_ref.split("-")[0]
transaction=request.env[
"payment.online.transaction"
].sudo().browse(
int(transaction_id)
)
if vnp_response_code=="00":
transaction.write({
"state":"done"
})
return request.redirect(
f"/payment/status?transaction_id={transaction.id}&status=success"
)
else:
error_message = self._get_vnpay_error_message(
vnp_response_code
)
transaction.write({
"state":"failed",
"response_code":vnp_response_code,
"response_message":error_message
})
return request.redirect(
f"/payment/status?transaction_id={transaction_id}&status=failed"
)
except Exception:
return request.redirect(
"/payment/status?status=failed"
)
6.5 Xác thực chữ ký VNPAY
def _verify_vnpay_signature(self, params):
"""Xác thực chữ ký VNPAY"""
try:
import hashlib
import hmac
import urllib.parse
ir_config = request.env[
"ir.config_parameter"
].sudo()
hash_secret = ir_config.get_param(
"online_paying.vnpay_hash_secret",
""
)
if not hash_secret:
return False
# Lấy hash từ VNPAY response
vnp_secure_hash = params.get(
"vnp_SecureHash",
""
)
# Loại bỏ các field không dùng để hash
input_data = {
k:v for k,v in params.items()
if k not in [
"vnp_SecureHash",
"vnp_SecureHashType"
] and v
}
# Sort theo alphabet
sorted_data = sorted(
input_data.items()
)
# Build hash string
hash_data = "&".join([
f"{urllib.parse.quote_plus(str(k))}={urllib.parse.quote_plus(str(v))}"
for k,v in sorted_data
])
# Tính lại checksum
calculated_hash = hmac.new(
hash_secret.encode("utf-8"),
hash_data.encode("utf-8"),
hashlib.sha512
).hexdigest().upper()
return (
vnp_secure_hash.upper()
== calculated_hash
)
except Exception:
return False







Top comments (0)