Sau khi hoàn thành bài học này, người học có thể:
Thành thạo logic kiểm tra giao dịch: Lập trình thuật toán tự động xác định tính hợp lệ của một bút toán (Check quy tắc "Tổng Nợ = Tổng Có" và sự tồn tại của Mã Tài khoản).
Quản trị Giao dịch DB (Transaction Management): Hiểu và ứng dụng commit() và rollback() trong SQLite để đảm bảo tính toàn vẹn dữ liệu (ACID). Nếu bút toán lỗi, toàn bộ dữ liệu liên quan sẽ tự động hủy bỏ.
Đóng gói Python API: Xây dựng hàm Python hoàn chỉnh add_journal_entry() tiếp nhận dữ liệu bút toán phức tạp và lưu trữ an toàn vào cơ sở dữ liệu.
Một phần mềm kế toán chuẩn mực tuyệt đối không cho phép lưu các bút toán "mất cân bằng" vào cơ sở dữ liệu.
[ DỮ LIỆU BÚT TOÁN ĐẦU VÀO ]
│
▼
┌──────────────────────────────────┐
│ 1. Kiểm tra Danh mục Tài khoản │
│ (Mã TK có tồn tại không?) │
└─────────────────┬────────────────┘
│
▼
┌──────────────────────────────────┐
│ 2. Kiểm tra Cân bằng Nợ / Có │
│ (Tổng Nợ == Tổng Có ?) │
└─────────────────┬────────────────┘
│
┌────────────┴────────────┐
│ │
[ ĐÚNG ] [ SAI ]
│ │
▼ ▼
┌────────────────────────┐ ┌────────────────────────┐
│ Ghi vào DB: │ │ Thực hiện ROLLBACK │
│ • Insert Transactions │ │ Hủy bỏ toàn bộ thao tác│
│ • Insert Entries │ │ Báo lỗi người dùng │
│ • COMMIT │ └────────────────────────┘
└────────────────────────┘
Ràng buộc Cân bằng Nợ/Có:
Ràng buộc Tính hợp lệ của Tài khoản: Tất cả account_code xuất hiện trong bút toán phải tồn tại sẵn trong bảng accounts.
Ràng buộc Giao dịch Nguyên tử (Atomic Transaction): Cả 2 thao tác (Thêm vào transactions và Thêm danh sách vào journal_entries) phải cùng thành công. Nếu có lỗi ở bất kỳ dòng chi tiết nào, hệ thống phải rollback() lại trạng thái ban đầu.
Dưới đây là mã nguồn Python chi tiết xây dựng hàm ghi bút toán kế toán an toàn, kèm theo cơ chế xử lý ngoại lệ (Exception Handling).
import sqlite3
from typing import List, Dict
def add_journal_entry(
entry_date: str,
ref_no: str,
description: str,
entries: List[Dict[str, float]],
db_name: str = "accounting_mini.db"
) -> bool:
"""
Hàm thêm một bút toán kế toán hoàn chỉnh vào Database.
Structure of `entries`:
[
{"account_code": "111", "debit": 1000000, "credit": 0},
{"account_code": "511", "debit": 0, "credit": 1000000}
]
"""
# 1. KIỂM TRA ĐẦU VÀO: Tính tổng Nợ và tổng Có
total_debit = sum(item.get("debit", 0.0) for item in entries)
total_credit = sum(item.get("credit", 0.0) for item in entries)
# Ràng buộc 1: Tổng Nợ phải bằng Tổng Có (cho phép sai số làm tròn cực nhỏ 1e-6)
if abs(total_debit - total_credit) > 1e-6:
print(f"[LỖI BÚT TOÁN] Mất cân bằng Nợ/Có! Tổng Nợ ({total_debit:,.0f}) != Tổng Có ({total_credit:,.0f})")
return False
# Ràng buộc 2: Bút toán không được có giá trị bằng 0
if total_debit <= 0:
print("[LỖI BÚT TOÁN] Giá trị giao dịch phải lớn hơn 0!")
return False
conn = sqlite3.connect(db_name)
cursor = conn.cursor()
cursor.execute("PRAGMA foreign_keys = ON;")
try:
# Bắt đầu giao dịch DB
conn.execute("BEGIN TRANSACTION;")
# Ràng buộc 3: Kiểm tra các Mã Tài khoản có tồn tại trong hệ thống
account_codes = [item["account_code"] for item in entries]
placeholders = ",".join("?" for _ in account_codes)
cursor.execute(f"SELECT account_code FROM accounts WHERE account_code IN ({placeholders})", account_codes)
existing_accounts = {row[0] for row in cursor.fetchall()}
for acc in account_codes:
if acc not in existing_accounts:
raise ValueError(f"Mã tài khoản '{acc}' không tồn tại trong Bảng Hệ thống Tài khoản!")
# 2. THÊM VÀO BẢNG TRANSACTIONS (Bút toán tổng thể)
cursor.execute("""
INSERT INTO transactions (entry_date, ref_no, description)
VALUES (?, ?, ?);
""", (entry_date, ref_no, description))
# Lấy transaction_id vừa tự động tạo
transaction_id = cursor.lastrowid
# 3. THÊM DANH SÁCH CHI TIẾT VÀO BẢNG JOURNAL_ENTRIES
journal_rows = [
(
transaction_id,
item["account_code"],
item.get("debit", 0.0),
item.get("credit", 0.0)
)
for item in entries
]
cursor.executemany("""
INSERT INTO journal_entries (transaction_id, account_code, debit_amount, credit_amount)
VALUES (?, ?, ?, ?);
""", journal_rows)
# Xác nhận lưu thay đổi vào DB nếu mọi thứ hợp lệ
conn.commit()
print(f"[THÀNH CÔNG] Đã ghi nhận Bút toán #{transaction_id} ({ref_no}) - Tổng tiền: {total_debit:,.0f} VNĐ")
return True
except Exception as e:
# Nếu gặp bất kỳ lỗi nào, hủy bỏ toàn bộ thao tác đã ghi dở dang
conn.rollback()
print(f"[GIAO DỊCH THẤT BẠI] Đã khôi phục trạng thái ban đầu. Chi tiết lỗi: {e}")
return False
finally:
conn.close()
Dưới đây là mã kịch bản để test thử cả 3 trường hợp: Ghi thành công, Ghi thất bại do mất cân bằng Nợ/Có, và Ghi thất bại do sai mã tài khoản.
if __name__ == "__main__":
# Tình huống 1: Bút toán chuẩn - Bán hàng thu tiền mặt 5.000.000 VNĐ
print("--- KỊCH BẢN 1: Bút toán Hợp lệ ---")
valid_entry = [
{"account_code": "111", "debit": 5000000, "credit": 0},
{"account_code": "511", "debit": 0, "credit": 5000000}
]
add_journal_entry("2026-09-01", "PT001", "Thu tiền bán hàng trực tiếp", valid_entry)
# Tình huống 2: Bút toán Lỗi Mất Cân Bằng (Nợ 10tr - Có 8tr)
print("\n--- KỊCH BẢN 2: Bút toán Mất cân bằng Nợ/Có ---")
unbalanced_entry = [
{"account_code": "112", "debit": 10000000, "credit": 0},
{"account_code": "511", "debit": 0, "credit": 8000000}
]
add_journal_entry("2026-09-02", "UNC001", "Rút tiền gửi thu doanh thu bị sai", unbalanced_entry)
# Tình huống 3: Bút toán Lỗi Sai Mã Tài khoản (Mã TK 999 không tồn tại)
print("\n--- KỊCH BẢN 3: Mã Tài khoản không tồn tại ---")
invalid_account_entry = [
{"account_code": "111", "debit": 2000000, "credit": 0},
{"account_code": "999", "debit": 0, "credit": 2000000}
]
add_journal_entry("2026-09-03", "PC001", "Chi tiền mặt cho tài khoản lạ", invalid_account_entry)
--- KỊCH BẢN 1: Bút toán Hợp lệ ---
[THÀNH CÔNG] Đã ghi nhận Bút toán #1 (PT001) - Tổng tiền: 5,000,000 VNĐ
--- KỊCH BẢN 2: Bút toán Mất cân bằng Nợ/Có ---
[LỖI BÚT TOÁN] Mất cân bằng Nợ/Có! Tổng Nợ (10,000,000) != Tổng Có (8,000,000)
--- KỊCH BẢN 3: Mã Tài khoản không tồn tại ---
[GIAO DỊCH THẤT BẠI] Đã khôi phục trạng thái ban đầu. Chi tiết lỗi: Mã tài khoản '999' không tồn tại trong Bảng Hệ thống Tài khoản!
Tại sao chúng ta phải gọi lệnh conn.rollback() bên trong khối except khi xảy ra lỗi?
A. Để xóa hoàn toàn cơ sở dữ liệu SQLite và tạo lại file mới.
B. Để đảm bảo tính chất Nguyên tử (Atomicity) - nếu không thêm được chi tiết dòng Nợ/Có thì thông tin chứng từ ở bảng transactions cũng sẽ bị hủy, tránh tạo ra dữ liệu rác/mồ côi.
C. Để ngắt kết nối với SQLite ngay lập tức.
Hãy dùng hàm add_journal_entry() để ghi nhận nghiệp vụ phức tạp sau:
Ngày 05/09/2026: Mua máy vi tính phục vụ quản lý trị giá 20.000.000 VNĐ (chưa thuế), thuế GTGT 10% 2.000.000 VNĐ. Thanh toán bằng Tiền mặt 10.000.000 VNĐ, số tiền còn lại 12.000.000 VNĐ nợ người bán.
(Gợi ý: Bút toán gồm 2 dòng Nợ [TK 211: 20tr, TK 133: 2tr] và 2 dòng Có [TK 111: 10tr, TK 331: 12tr]).
Bài tập 1:
Đáp án đúng là B. rollback() khôi phục trạng thái database về trước khi giao dịch bắt đầu, ngăn chặn việc dòng dữ liệu nửa vời bị lưu vào hệ thống.
Bài tập 2:
complex_entry = [
{"account_code": "211", "debit": 20000000, "credit": 0},
{"account_code": "133", "debit": 2000000, "credit": 0},
{"account_code": "111", "debit": 0, "credit": 10000000},
{"account_code": "331", "debit": 0, "credit": 12000000}
]
add_journal_entry(
entry_date="2026-09-05",
ref_no="PC002",
description="Mua máy vi tính quản lý, trả một phần tiền mặt",
entries=complex_entry
)