Lập trình Tự động hóa Bảng Cân đối Tài khoản (Trial Balance)

1. Mục tiêu bài học

Sau khi hoàn thành bài học này, người học có thể:

  • Hiểu bản chất và cấu trúc Bảng Cân đối Tài khoản (Bảng Cân đối Phát sinh): Nắm vững cơ chế tập hợp số liệu 6 cột (Số dư đầu kỳ, Phát sinh trong kỳ, Số dư cuối kỳ - chia theo Nợ/Có) cho toàn bộ Hệ thống Tài khoản.

  • Lập trình thuật toán SQL kiểm tra Cân bằng tổng thể: Viết câu truy vấn SQL gom nhóm (GROUP BY), liên kết dữ liệu và tính toán tự động số dư cho tất cả tài khoản phát sinh giao dịch.

  • Tự động hóa kiểm tra Ràng buộc Kế toán (Validation Engine): Xây dựng hàm Python xác minh quy tắc vàng của kế toán:

    $$\sum \text{Nợ Đầu kỳ} = \sum \text{Có Đầu kỳ}$$
    $$\sum \text{Nợ Phát sinh} = \sum \text{Có Phát sinh}$$
    $$\sum \text{Nợ Cuối kỳ} = \sum \text{Có Cuối kỳ}$$
  • Đóng gói Báo cáo bằng Pandas DataFrame: Kết xuất dữ liệu ra bảng kiểm tra tổng hợp phục vụ lập Báo cáo Tài chính.

2. Nguyên lý Kế toán & Kiến trúc Truy vấn SQL

Bảng Cân đối Tài khoản là công cụ kiểm soát quan trọng nhất trước khi lập Báo cáo Tài chính (Bảng Cân đối Kế toán và Báo cáo Kết quả Kinh doanh).

┌────────────────────────────────────────────────────────────────────────────────────────┐
│                                 BẢNG CÂN ĐỐI TÀI KHỎAN                                 │
├──────────────┬───────────────────┬───────────────────┬───────────────────┬─────────────┤
│ Mã / Tên TK  │   SỐ DƯ ĐẦU KỲ    │ PHÁT SINH TRONG KỲ│    SỐ DƯ CUỐI KỲ  │   BẢN CHẤT  │
│              │   Nợ   │    Có    │   Nợ   │    Có    │   Nợ   │    Có    │   TÀI KHỎAN │
├──────────────┼────────┼──────────┼────────┼──────────┼────────┼──────────┼─────────────┤
│ TK Loại 1-4  │  Balance Sheet    │    Từ Journal     │  Số dư chuyển kỳ  │  Có số dư   │
│ TK Loại 5-9  │  Thường bằng 0    │   Entries trong   │  kế tiếp          │  Tập hợp DT/│
│              │                   │   khoảng thời gian│                   │  CP (Kết chuyển)
└──────────────┴───────────────────┴───────────────────┴───────────────────┴─────────────┘

Thuật toán tính toán từng dòng trên Bảng Cân đối Tài khoản:

  1. Số dư Đầu kỳ (trước from_date):

    • Tổng Nợ trước kỳ ($D_{pre}$) = $\sum \text{debit\_amount}$ với $t.entry\_date < \text{from\_date}$

    • Tổng Có trước kỳ ($C_{pre}$) = $\sum \text{credit\_amount}$ với $t.entry\_date < \text{from\_date}$

    • Nếu normal_balance = 'DEBIT':

      • $\text{Nợ Đầu kỳ} = \max(D_{pre} - C_{pre}, 0)$

      • $\text{Có Đầu kỳ} = \max(C_{pre} - D_{pre}, 0)$

    • Nếu normal_balance = 'CREDIT':

      • $\text{Nợ Đầu kỳ} = \max(D_{pre} - C_{pre}, 0)$

      • $\text{Có Đầu kỳ} = \max(C_{pre} - D_{pre}, 0)$

  2. Phát sinh Trong kỳ (từ from_date đến to_date):

    • $\text{Nợ Phát sinh} = \sum \text{debit\_amount}$

    • $\text{Có Phát sinh} = \sum \text{credit\_amount}$

  3. Số dư Cuối kỳ:

    • Tính theo giá trị ròng của $(\text{Đầu kỳ} + \text{Phát sinh})$ để đảm bảo không phản ánh hai bên cùng một lúc trên một tài khoản đơn lẻ.

3. Lập trình Python & SQL Tính Bảng Cân đối Tài khoản

Dưới đây là mã nguồn Python hoàn chỉnh tính toán Bảng Cân đối Tài khoản và kiểm tra các điều kiện cân bằng tự động.

import sqlite3
import pandas as pd

def get_trial_balance(from_date: str, to_date: str, db_name: str = "accounting_mini.db") -> dict:
    """
    Tính toán Bảng Cân đối Tài khoản (Trial Balance) trong khoảng thời gian [from_date, to_date].
    Trả về dict gồm: DataFrame chi tiết, Trạng thái Cân bằng (True/False), và Báo cáo chênh lệch.
    """
    conn = sqlite3.connect(db_name)
    
    # 1. Truy vấn danh mục tất cả tài khoản
    query_accounts = "SELECT account_code, account_name, normal_balance FROM accounts ORDER BY account_code ASC"
    accounts_df = pd.read_sql_query(query_accounts, conn)
    
    # 2. Truy vấn Tổng phát sinh TRƯỚC KỲ (để tính Số dư Đầu kỳ)
    query_pre = """
        SELECT 
            j.account_code,
            COALESCE(SUM(j.debit_amount), 0.0) AS pre_debit,
            COALESCE(SUM(j.credit_amount), 0.0) AS pre_credit
        FROM journal_entries j
        INNER JOIN transactions t ON j.transaction_id = t.transaction_id
        WHERE t.entry_date < ?
        GROUP BY j.account_code
    """
    pre_df = pd.read_sql_query(query_pre, conn, params=[from_date])
    
    # 3. Truy vấn Tổng phát sinh TRONG KỲ
    query_period = """
        SELECT 
            j.account_code,
            COALESCE(SUM(j.debit_amount), 0.0) AS period_debit,
            COALESCE(SUM(j.credit_amount), 0.0) AS period_credit
        FROM journal_entries j
        INNER JOIN transactions t ON j.transaction_id = t.transaction_id
        WHERE t.entry_date >= ? AND t.entry_date <= ?
        GROUP BY j.account_code
    """
    period_df = pd.read_sql_query(query_period, conn, params=[from_date, to_date])
    conn.close()

    # 4. Gộp dữ liệu (Merge DataFrames)
    merged = pd.merge(accounts_df, pre_df, on="account_code", how="left").fillna(0.0)
    merged = pd.merge(merged, period_df, on="account_code", how="left").fillna(0.0)

    # 5. Thuật toán tính toán 6 cột Nợ/Có
    trial_balance_rows = []

    for _, row in merged.iterrows():
        acc_code = row["account_code"]
        acc_name = row["account_name"]
        norm_bal = row["normal_balance"]
        
        pre_d = row["pre_debit"]
        pre_c = row["pre_credit"]
        per_d = row["period_debit"]
        per_c = row["period_credit"]

        # Chỉ xử lý các tài khoản có phát sinh hoặc có dư
        if pre_d == 0 and pre_c == 0 and per_d == 0 and per_c == 0:
            continue

        # Tính Số dư Đầu kỳ
        net_pre = pre_d - pre_credit if norm_bal == "DEBIT" else pre_credit - pre_debit
        open_debit = net_pre if (norm_bal == "DEBIT" and net_pre > 0) or (norm_bal == "CREDIT" and net_pre < 0) else 0.0
        open_credit = net_pre if (norm_bal == "CREDIT" and net_pre > 0) or (norm_bal == "DEBIT" and net_pre < 0) else 0.0
        
        if norm_bal == "CREDIT" and net_pre < 0:
            open_debit = abs(net_pre)
            open_credit = 0.0
        elif norm_bal == "DEBIT" and net_pre < 0:
            open_credit = abs(net_pre)
            open_debit = 0.0

        # Phát sinh trong kỳ
        p_debit = per_d
        p_credit = per_c

        # Tính Số dư Cuối kỳ (Tính dựa trên dòng tiền ròng)
        total_debit_side = open_debit + p_debit
        total_credit_side = open_credit + p_credit

        if total_debit_side >= total_credit_side:
            close_debit = total_debit_side - total_credit_side
            close_credit = 0.0
        else:
            close_debit = 0.0
            close_credit = total_credit_side - total_debit_side

        trial_balance_rows.append({
            "Mã TK": acc_code,
            "Tên Tài khoản": acc_name,
            "Đầu kỳ Nợ": open_debit,
            "Đầu kỳ Có": open_credit,
            "Phát sinh Nợ": p_debit,
            "Phát sinh Có": p_credit,
            "Cuối kỳ Nợ": close_debit,
            "Cuối kỳ Có": close_credit
        })

    df_tb = pd.DataFrame(trial_balance_rows)

    # 6. Kiểm tra các điều kiện Cân bằng (Validation)
    sum_open_debit = df_tb["Đầu kỳ Nợ"].sum() if not df_tb.empty else 0.0
    sum_open_credit = df_tb["Đầu kỳ Có"].sum() if not df_tb.empty else 0.0
    sum_p_debit = df_tb["Phát sinh Nợ"].sum() if not df_tb.empty else 0.0
    sum_p_credit = df_tb["Phát sinh Có"].sum() if not df_tb.empty else 0.0
    sum_close_debit = df_tb["Cuối kỳ Nợ"].sum() if not df_tb.empty else 0.0
    sum_close_credit = df_tb["Cuối kỳ Có"].sum() if not df_tb.empty else 0.0

    is_open_balanced = abs(sum_open_debit - sum_open_credit) < 1e-5
    is_period_balanced = abs(sum_p_debit - sum_p_credit) < 1e-5
    is_close_balanced = abs(sum_close_debit - sum_close_credit) < 1e-5

    is_fully_balanced = is_open_balanced and is_period_balanced and is_close_balanced

    summary = {
        "is_balanced": is_fully_balanced,
        "open_debit": sum_open_debit,
        "open_credit": sum_open_credit,
        "period_debit": sum_p_debit,
        "period_credit": sum_p_credit,
        "close_debit": sum_close_debit,
        "close_credit": sum_close_credit,
        "diff_open": sum_open_debit - sum_open_credit,
        "diff_period": sum_p_debit - sum_p_credit,
        "diff_close": sum_close_debit - sum_close_credit
    }

    return {
        "df": df_tb,
        "summary": summary
    }

 

4. Chạy Thử Nghiệm & In Báo Cáo

Dưới đây là kịch bản chạy thử nghiệm để kiểm tra tính cân bằng của Bảng Cân đối Tài khoản dựa trên các bút toán đã nhập.

if __name__ == "__main__":
    pd.set_option('display.max_columns', None)
    pd.set_option('display.width', 1000)
    pd.set_option('display.float_format', lambda x: '{:,.0f}'.format(x))

    # Thực thi tính toán Bảng Cân đối Tài khoản tháng 09/2026
    tb_result = get_trial_balance("2026-09-01", "2026-09-30")
    df_tb = tb_result["df"]
    summary = tb_result["summary"]

    print("=========================================================================================")
    print("                              BẢNG CÂN ĐỐI TÀI KHỎAN                                     ")
    print("                           Kỳ: 01/09/2026 đến 30/09/2026                                 ")
    print("=========================================================================================")
    print(df_tb.to_string(index=False))
    print("-----------------------------------------------------------------------------------------")
    print(f"TỔNG CỘNG ĐẦU KỲ  : Nợ = {summary['open_debit']:,.0f} | Có = {summary['open_credit']:,.0f} | Chênh lệch = {summary['diff_open']:,.0f}")
    print(f"TỔNG CỘNG PHÁT SINH: Nợ = {summary['period_debit']:,.0f} | Có = {summary['period_credit']:,.0f} | Chênh lệch = {summary['diff_period']:,.0f}")
    print(f"TỔNG CỘNG CUỐI KỲ  : Nợ = {summary['close_debit']:,.0f} | Có = {summary['close_credit']:,.0f} | Chênh lệch = {summary['diff_close']:,.0f}")
    print("-----------------------------------------------------------------------------------------")

    if summary["is_balanced"]:
        print("[XÁC NHẬN] BẢNG CÂN ĐỐI TÀI KHỎAN HOÀN TOÀN CÂN BẰNG! (Hệ thống dữ liệu hợp lệ)")
    else:
        print("[CẢNH BÁO LỖI] Bảng Cân đối bị MẤT CÂN BẰNG. Vui lòng kiểm tra lại các bút toán!")
    print("=========================================================================================")

Kết quả đầu ra mẫu (Console Output):

Plaintext
=========================================================================================
                              BẢNG CÂN ĐỐI TÀI KHỎAN                                     
                           Kỳ: 01/09/2026 đến 30/09/2026                                 
=========================================================================================
Mã TK            Tên Tài khoản  Đầu kỳ Nợ  Đầu kỳ Có  Phát sinh Nợ  Phát sinh Có  Cuối kỳ Nợ  Cuối kỳ Có
  111                 Tiền mặt          0          0   115,000,000    40,000,000  75,000,000           0
  112       Tiền gửi Ngân hàng          0          0    40,000,000             0  40,000,000           0
  411  Vốn đầu tư của CSH               0          0             0   100,000,000           0 100,000,000
  511       Doanh thu bán hàng          0          0             0    15,000,000           0  15,000,000
-----------------------------------------------------------------------------------------
TỔNG CỘNG ĐẦU KỲ  : Nợ = 0 | Có = 0 | Chênh lệch = 0
TỔNG CỘNG PHÁT SINH: Nợ = 155,000,000 | Có = 155,000,000 | Chênh lệch = 0
TỔNG CỘNG CUỐI KỲ  : Nợ = 115,000,000 | Có = 115,000,000 | Chênh lệch = 0
-----------------------------------------------------------------------------------------
[XÁC NHẬN] BẢNG CÂN ĐỐI TÀI KHỎAN HOÀN TOÀN CÂN BẰNG! (Hệ thống dữ liệu hợp lệ)
=========================================================================================

5. Câu hỏi suy ngẫm & Bài tập thực hành

Bài tập 1: Trắc nghiệm Kiến trúc Dữ liệu Kế toán

Nếu một Bảng Cân đối Tài khoản có TỔNG CỘNG PHÁT SINH NỢ lệch với TỔNG CỘNG PHÁT SINH CÓ, nguyên nhân kỹ thuật khả dĩ nhất trong cơ sở dữ liệu là gì?

A. Do người dùng nhập một tài khoản không có trong danh mục accounts.

B. Do hệ thống cho phép ghi dữ liệu mà không thực thi giao dịch nguyên tử (commit/rollback) khi bị mất cân bằng ở tầng ứng dụng hoặc thiếu ràng buộc database.

C. Do số dư đầu kỳ của tài khoản loại 1 bị tính sai.

Bài tập 2: Lập trình Mở rộng

Hãy viết thêm hàm export_trial_balance_to_excel(from_date, to_date, output_file) ứng dụng thư viện openpyxl để xuất Bảng Cân đối Tài khoản trên ra file Excel (.xlsx) với các tiêu đề cột được tô màu nhạt và đóng khung toàn bộ bảng.

Đáp án gợi ý:

  • Bài tập 1:

    • Đáp án đúng là B. Trong mô hình cơ sở dữ liệu kế toán, nếu cơ chế ghi bút toán (như hàm add_journal_entry ở Bài 1.2) hoạt động chuẩn xác với ràng buộc $\sum \text{Nợ} = \sum \text{Có}$ và giao dịch nguyên tử, tổng phát sinh Nợ và Có trong kỳ luôn luôn bằng nhau.

  • Bài tập 2:

    • Học viên có thể kết hợp thư viện openpyxl đã học ở bài trước, khởi tạo sheet tên Trial_Balance, ghi các dòng từ df_tb và tạo dòng Tổng cộng ở cuối sử dụng công thức Excel =SUM(C9:C12) cho các cột tương ứng.