Lập trình Trích xuất Bảng Cân đối Kế toán (Balance Sheet)

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ể:

  • Nắm vững bản chất Bảng Cân đối Kế toán (Balance Sheet): Hiểu rõ phương trình kế toán cơ bản ($\text{Tài sản} = \text{Nợ phải trả} + \text{Vốn chủ sở hữu}$) và cách phản ánh trạng thái tài chính tại một thời điểm cụ thể (as_of_date).

  • Hiểu cơ chế tài khoản lưỡng tính và số dư ròng: Xử lý chính xác các tài khoản có thể có số dư hai bên (như TK 131 - Phải thu khách hàng, TK 331 - Phải trả người bán) để phân loại đúng vào Tài sản hoặc Nợ phải trả.

  • Xây dựng thuật toán SQL trích xuất Số dư Cuối kỳ dồn tích: Viết các truy vấn tính toán số dư lũy kế từ ngày đầu khởi tạo hệ thống đến thời điểm lập báo cáo.

  • Đóng gói Python API Báo cáo Cân đối Kế toán: Lập trình hàm Python tự động kiểm tra cân đối tổng thể ($\text{Tổng Tài sản} - \text{Tổng Nguồn vốn} = 0$) và xuất kết quả ra Pandas DataFrame.

2. Nguyên lý Kế toán: Cấu trúc Bảng Cân đối Kế toán

Bảng Cân đối Kế toán phản ánh toàn bộ Tài sản và Nguồn vốn của doanh nghiệp tại thời điểm cuối ngày của ngày báo cáo (as_of_date).

┌─────────────────────────────────────────────────────────────────────────┐
│                       TỔNG TÀI SẢN (ASSETS)                             │
│  Tài sản Ngắn hạn (Loại 1) + Tài sản Dài hạn (Loại 2)                  │
├─────────────────────────────────────────────────────────────────────────┤
│                                  ||                                     │
│                            BẰNG (EQUAL TO)                              │
│                                  ||                                     │
├─────────────────────────────────────────────────────────────────────────┤
│                      TỔNG NGUỒN VỐN (EQUITY & LIABILITIES)              │
│  Nợ phải trả (Loại 3) + Vốn chủ sở hữu (Loại 4)                        │
└─────────────────────────────────────────────────────────────────────────┘

Quy tắc Phân loại & Tính toán Số dư:

  1. Số dư Dồn tích (Cumulative Balance):

    Khác với Báo cáo P&L chỉ lấy trong khoảng thời gian, Bảng Cân đối Kế toán lấy tổng lũy kế toàn bộ bút toán từ trước tới thời điểm as_of_date:

    $$\text{Số dư ròng} = \sum \text{debit\_amount} - \sum \text{credit\_amount} \quad (\text{với } \text{entry\_date} \le \text{as\_of\_date})$$
  2. Xử lý Bản chất Tài khoản:

    • Tài khoản Tài sản (Loại 1, 2): Số dư chuẩn là Nợ ($\text{Debit} > \text{Credit}$).

      • Ngoại lệ (Tài khoản điều chỉnh giảm): TK 214 (Hao mòn tài sản cố định), TK 229 (Dự phòng tổn thất tài sản) có bản chất số dư Có, ghi giá trị ÂM bên cột Tài sản.

    • Tài khoản Nguồn vốn (Loại 3, 4): Số dư chuẩn là Có ($\text{Credit} > \text{Debit}$).

    • Tài khoản Lưỡng tính (TK 131, 331):

      • TK 131 dư Nợ $\rightarrow$ Phải thu khách hàng (Tài sản).

      • TK 131 dư Có $\rightarrow$ Người mua trả tiền trước (Nợ phải trả).

      • TK 331 dư Có $\rightarrow$ Phải trả người bán (Nợ phải trả).

      • TK 331 dư Nợ $\rightarrow$ Trả trước cho người bán (Tài sản).

3. Lập trình Python & SQL Trích xuất Bảng Cân đối Kế toán

Dưới đây là mã nguồn Python chiết xuất dữ liệu số dư lũy kế từ SQL và lập Bảng Cân đối Kế toán.

import sqlite3
import pandas as pd

def get_balance_sheet(as_of_date: str, db_name: str = "accounting_mini.db") -> dict:
    """
    Trích xuất Bảng Cân đối Kế toán tại thời điểm cuối ngày as_of_date.
    """
    conn = sqlite3.connect(db_name)
    
    # 1. Truy vấn tổng lũy kế Nợ/Có đến thời điểm as_of_date cho các TK Bảng Cân đối (Loại 1, 2, 3, 4)
    query = """
        SELECT 
            a.account_code,
            a.account_name,
            a.normal_balance,
            COALESCE(SUM(j.debit_amount), 0.0) AS cum_debit,
            COALESCE(SUM(j.credit_amount), 0.0) AS cum_credit
        FROM accounts a
        LEFT JOIN journal_entries j ON a.account_code = j.account_code
        LEFT JOIN transactions t ON j.transaction_id = t.transaction_id AND t.entry_date <= ?
        WHERE a.account_code LIKE '1%' OR a.account_code LIKE '2%' 
           OR a.account_code LIKE '3%' OR a.account_code LIKE '4%'
        GROUP BY a.account_code, a.account_name, a.normal_balance
        ORDER BY a.account_code ASC
    """
    
    df_raw = pd.read_sql_query(query, conn, params=[as_of_date])
    conn.close()

    # Dictionary lưu trữ số dư ròng theo tài khoản
    # Quy ước: Dương = Nợ, Âm = Có
    balances = {}
    for _, row in df_raw.iterrows():
        code = str(row["account_code"])
        net = row["cum_debit"] - row["cum_credit"]
        balances[code] = net

    # Hàm lấy số dư tài khoản (trả về 0 nếu chưa có)
    def b(code: str) -> float:
        return balances.get(code, 0.0)

    # -------------------------------------------------------------------------
    # 2. TÍNH TOÁN CÁC CHỈ TIÊU TÀI SẢN (ASSETS)
    # -------------------------------------------------------------------------
    # A. TÀI SẢN NGẮN HẠN
    cash_and_equiv = b("111") + b("112")                            # Tiền và tương đương tiền
    
    # Xử lý TK 131 Lưỡng tính: Chỉ lấy phần dư Nợ vào Tài sản
    receivables_131 = b("131") if b("131") > 0 else 0.0
    prepaid_331 = abs(b("331")) if b("331") < 0 else 0.0           # Trả trước cho người bán (nếu dư Nợ)
    trade_receivables = receivables_131 + prepaid_331 + b("138")
    
    inventory = b("152") + b("155") + b("156")                      # Hàng tồn kho
    other_short_assets = b("141") + b("133")                       # Tài sản ngắn hạn khác
    
    total_short_term_assets = cash_and_equiv + trade_receivables + inventory + other_short_assets

    # B. TÀI SẢN DÀI HẠN
    fixed_assets_cost = b("211")                                    # Nguyên giá TSCĐ
    accumulated_depreciation = b("214")                             # Giá trị hao mòn lũy kế (thường mang dấu âm/Có)
    fixed_assets_net = fixed_assets_cost + accumulated_depreciation # Giá trị còn lại
    
    total_long_term_assets = fixed_assets_net + b("241") + b("228")
    
    # TỔNG TÀI SẢN
    total_assets = total_short_term_assets + total_long_term_assets

    # -------------------------------------------------------------------------
    # 3. TÍNH TOÁN CÁC CHỈ TIÊU NGUỒN VỐN (EQUITY & LIABILITIES)
    # -------------------------------------------------------------------------
    # A. NỢ PHẢI TRẢ
    payables_331 = abs(b("331")) if b("331") > 0 else 0.0          # Phải trả người bán (dư Có)
    advance_131 = abs(b("131")) if b("131") < 0 else 0.0           # Người mua trả tiền trước (dư Có)
    trade_payables = payables_331 + advance_131 + abs(b("334")) + abs(b("338"))
    
    tax_payables = abs(b("3331")) + abs(b("3334"))                 # Thuế và các khoản phải nộp nhà nước
    short_term_loans = abs(b("341"))                               # Vay ngắn hạn
    
    total_liabilities = trade_payables + tax_payables + short_term_loans

    # B. VỐN CHỦ SỞ HỮU
    owner_capital = abs(b("411"))                                  # Vốn góp của chủ sở hữu
    retained_earnings = abs(b("4211")) + abs(b("4212"))            # Lợi nhuận sau thuế chưa phân phối
    if b("4212") < 0: # Trường hợp lỗ
        retained_earnings = abs(b("4211")) - abs(b("4212"))

    total_equity = owner_capital + retained_earnings

    # TỔNG NGUỒN VỐN
    total_resources = total_liabilities + total_equity

    # -------------------------------------------------------------------------
    # 4. ĐÓNG GÓI BẢNG BÁO CÁO
    # -------------------------------------------------------------------------
    bs_structure = [
        # TÀI SẢN
        {"Mã số": "100", "Chỉ tiêu": "A. TÀI SẢN NGẮN HẠN", "Số tiền": total_short_term_assets},
        {"Mã số": "110", "Chỉ tiêu": "  I. Tiền và các khoản tương đương tiền", "Số tiền": cash_and_equiv},
        {"Mã số": "130", "Chỉ tiêu": "  II. Các khoản phải thu ngắn hạn", "Số tiền": trade_receivables},
        {"Mã số": "140", "Chỉ tiêu": "  III. Hàng tồn kho", "Số tiền": inventory},
        {"Mã số": "150", "Chỉ tiêu": "  IV. Tài sản ngắn hạn khác", "Số tiền": other_short_assets},
        {"Mã số": "200", "Chỉ tiêu": "B. TÀI SẢN DÀI HẠN", "Số tiền": total_long_term_assets},
        {"Mã số": "220", "Chỉ tiêu": "  I. Tài sản cố định (Nguyên giá - Hao mòn)", "Số tiền": fixed_assets_net},
        {"Mã số": "270", "Chỉ tiêu": "TỔNG CỘNG TÀI SẢN (270 = 100 + 200)", "Số tiền": total_assets},
        
        # NGUỒN VỐN
        {"Mã số": "300", "Chỉ tiêu": "C. NỢ PHẢI TRẢ", "Số tiền": total_liabilities},
        {"Mã số": "310", "Chỉ tiêu": "  I. Nợ ngắn hạn (Phải trả người bán, Thuế...)", "Số tiền": total_liabilities},
        {"Mã số": "400", "Chỉ tiêu": "D. VỐN CHỦ SỞ HỮU", "Số tiền": total_equity},
        {"Mã số": "411", "Chỉ tiêu": "  I. Vốn góp của chủ sở hữu", "Số tiền": owner_capital},
        {"Mã số": "421", "Chỉ tiêu": "  II. Lợi nhuận sau thuế chưa phân phối", "Số tiền": retained_earnings},
        {"Mã số": "440", "Chỉ tiêu": "TỔNG CỘNG NGUỒN VỐN (440 = 300 + 400)", "Số tiền": total_resources},
    ]

    df_bs = pd.DataFrame(bs_structure)
    diff = total_assets - total_resources
    is_balanced = abs(diff) < 1e-5

    return {
        "df": df_bs,
        "as_of_date": as_of_date,
        "total_assets": total_assets,
        "total_resources": total_resources,
        "is_balanced": is_balanced,
        "difference": diff
    }

4. Chạy Thử Nghiệm & Kiểm Tra Cân Đối

Dưới đây là kịch bản chạy thử nghiệm xuất Bảng Cân đối Kế toán tại ngày 30/09/2026 (sau khi đã thực thi bút toán kết chuyển ở Bài 3.1).

if __name__ == "__main__":
    # Trích xuất Bảng Cân đối Kế toán tại ngày 30/09/2026
    bs_result = get_balance_sheet("2026-09-30")
    df_bs = bs_result["df"]

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

    print("==========================================================================")
    print("                    BẢNG CÂN ĐỐI KẾ TOÁN (BALANCE SHEET)                  ")
    print(f"                         Tại ngày: {bs_result['as_of_date']}                       ")
    print("==========================================================================")
    print(df_bs.to_string(index=False))
    print("--------------------------------------------------------------------------")
    print(f" TỔNG TÀI SẢN  : {bs_result['total_assets']:,.0f} VNĐ")
    print(f" TỔNG NGUỒN VỐN: {bs_result['total_resources']:,.0f} VNĐ")
    print(f" CHÊNH LỆCH    : {bs_result['difference']:,.0f} VNĐ")
    print("--------------------------------------------------------------------------")

    if bs_result["is_balanced"]:
        print("[XÁC NHẬN] BẢNG CÂN ĐỐI KẾ TOÁN HOÀN TOÀN CÂN BẰNG! (Phương trình Kế toán đúng)")
    else:
        print("[CẢNH BÁO LỖI] Bảng Cân đối Kế toán bị MẤT CÂN BẰNG. Kiểm tra lại bút toán!")
    print("==========================================================================")

Kết quả xuất ra màn hình Console:

Plaintext
==========================================================================
                    BẢNG CÂN ĐỐI KẾ TOÁN (BALANCE SHEET)                  
                         Tại ngày: 2026-09-30                       
==========================================================================
Mã số                                                  Chỉ tiêu      Số tiền
  100                                     A. TÀI SẢN NGẮN HẠN   105,000,000
  110                    I. Tiền và các khoản tương đương tiền   105,000,000
  130                          II. Các khoản phải thu ngắn hạn            0
  140                                       III. Hàng tồn kho            0
  150                               IV. Tài sản ngắn hạn khác            0
  200                                     B. TÀI SẢN DÀI HẠN            0
  220                I. Tài sản cố định (Nguyên giá - Hao mòn)            0
  270              TỔNG CỘNG TÀI SẢN (270 = 100 + 200)          105,000,000
  300                                         C. NỢ PHẢI TRẢ    1,000,000
  310          I. Nợ ngắn hạn (Phải trả người bán, Thuế...)    1,000,000
  400                                     D. VỐN CHỦ SỞ HỮU   104,000,000
  411                             I. Vốn góp của chủ sở hữu   100,000,000
  421                 II. Lợi nhuận sau thuế chưa phân phối    4,000,000
  440              TỔNG CỘNG NGUỒN VỐN (440 = 300 + 400)        105,000,000
--------------------------------------------------------------------------
 TỔNG TÀI SẢN  : 105,000,000 VNĐ
 TỔNG NGUỒN VỐN: 105,000,000 VNĐ
 CHÊNH LỆCH    : 0 VNĐ
--------------------------------------------------------------------------
[XÁC NHẬN] BẢNG CÂN ĐỐI KẾ TOÁN HOÀN TOÀN CÂN BẰNG! (Phương trình Kế toán đúng)
==========================================================================

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 thức Tài chính

Nếu doanh nghiệp chưa chạy bút toán kết chuyển xác định kết quả kinh doanh cuối kỳ (Bài 3.1), Bảng Cân đối Kế toán sẽ bị rơi vào trạng thái nào?

A. Bảng Cân đối Kế toán vẫn tự động cân bằng vì SQL tự tính Lợi nhuận.

B. Bảng Cân đối Kế toán bị mất cân bằng đúng bằng giá trị Lợi nhuận sau thuế phát sinh trong kỳ, do Doanh thu và Chi phí chưa được chuyển về TK 421.

C. Tổng Tài sản sẽ bị giảm đi 20%.

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

Hãy bổ sung đoạn mã xử lý trường hợp doanh nghiệp có trích lập Hao mòn Tài sản cố định (TK 2141 dư Có) để hiển thị riêng dòng Nguyên giá (211) và Hao mòn lũy kế (2141) dạng số âm trong nhóm Chỉ tiêu Tài sản cố định.

Đáp án gợi ý:

  • Bài tập 1:

    • Đáp án đúng là B. Nếu chưa kết chuyển, Doanh thu và Chi phí nằm ở TK Loại 5–8 (không thuộc Bảng Cân đối Kế toán) và chưa chảy về TK 421 (Vốn chủ sở hữu), dẫn đến $\text{Tài sản} \ne \text{Nguồn vốn}$.

  • Bài tập 2:

    # Trong hàm get_balance_sheet:
    cost = b("211")
    accum_dep = b("2141") # Giá trị mang dấu âm (do dư Có)
    
    fixed_asset_rows = [
        {"Mã số": "221", "Chỉ tiêu": "  - Nguyên giá", "Số tiền": cost},
        {"Mã số": "222", "Chỉ tiêu": "  - Giá trị hao mòn lũy kế", "Số tiền": accum_dep},
        {"Mã số": "220", "Chỉ tiêu": "  I. Giá trị còn lại TSCĐ", "Số tiền": cost + accum_dep}
    ]