Lập trình Trích xuất Báo cáo Lưu chuyển Tiền tệ (Statement of Cash Flows) theo Phương pháp Trực tiếp

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 Báo cáo Lưu chuyển Tiền tệ (Direct Cash Flow Statement): Nắm vững nguyên lý phản ánh dòng tiền thực thu (Cash Inflows) và thực chi (Cash Outflows) trong kỳ từ 3 hoạt động: Kinh doanh, Đầu tư, và Tài chính.

  • Xây dựng Thuật toán SQL Đối ứng Bút toán (Counterpart Accounting Entry Algorithm): Lập trình truy vấn SQL phân tích các cặp tài khoản đối ứng với Tài khoản Tiền (111, 112) để tự động gắn nhãn mã dòng tiền.

  • Quy chuẩn hóa Mã chỉ tiêu Lưu chuyển Tiền tệ: Thiết lập bảng định ánh xạ (mapping matrix) giữa cặp tài khoản Nợ/Có với các chỉ tiêu tiền tệ theo chuẩn mực kế toán.

  • Đóng gói Python API Báo cáo Tiền tệ: Lập trình hàm Python tính toán Dòng tiền thuần trong kỳ, tự động đối soát với biến động tiền trên Bảng Cân đối Kế toán:

    $$\text{Tiền cuối kỳ} - \text{Tiền đầu kỳ} = \text{Lưu chuyển tiền thuần trong kỳ}$$

2. Nguyên lý Kế toán: Phương pháp Trực tiếp & Thuật toán Đối ứng

Báo cáo Lưu chuyển Tiền tệ theo phương pháp trực tiếp xác định dòng tiền vào/ra bằng cách phân tích trực tiếp các bút toán ghi Nợ hoặc ghi Có vào tài khoản Tiền tệ (TK 111 - Tiền mặt, TK 112 - Tiền gửi Ngân hàng).

                            SƠ ĐỒ THUẬT TOÁN ĐỐI ỨNG DÒNG TIỀN

               ┌────────────────────────────────────────────────────────┐
               │  TÀI KHỎAN TIỀN TỆ (TK 111, 112)                       │
               │  - Ghi Nợ (Debit)  : Dòng tiền VÀO (+)                 │
               │  - Ghi Có (Credit) : Dòng tiền RA  (-)                 │
               └───────────────────────────┬────────────────────────────┘
                                           │
                        Tìm Tài khoản Đối ứng (Counterpart Account)
                                           │
         ┌─────────────────────────────────┼─────────────────────────────────┐
         ▼                                 ▼                                 ▼
┌─────────────────────────┐   ┌─────────────────────────┐   ┌─────────────────────────┐
│ HOẠT ĐỘNG KINH DOANH    │   │   HOẠT ĐỘNG ĐẦU TƯ      │   │   HOẠT ĐỘNG TÀI CHÍNH   │
├─────────────────────────┤   ├─────────────────────────┤   ├─────────────────────────┤
│ • Thu từ bán hàng (511) │   │ • Mua sắm TSCĐ (211)    │   │ • Thu từ góp vốn (411)  │
│ • Chi trả người bán(331)│   │ • Chi cho vay/đầu tư    │   │ • Thu từ vay nợ (341)   │
│ • Chi trả lương (334)   │   │ • Thu hồi đầu tư, lãi   │   │ • Chi trả nợ gốc vay    │
│ • Chi nộp thuế (333)    │   │   cho vay (515)         │   │ • Chi trả cổ tức/lãi    │
└─────────────────────────┘   └─────────────────────────┘   └─────────────────────────┘

Bảng Định ánh xạ Bút toán Tiền với Chỉ tiêu Lưu chuyển Tiền tệ:

Mã STT Chỉ tiêu Cặp Bút toán (Tiền vs TK Đối ứng) Bản chất Dòng tiền
01 Tiền thu từ bán hàng, cung cấp dịch vụ Nợ 111, 112 / Có 511, 131, 515 + (Kinh doanh)
02 Tiền chi trả cho người cung cấp Có 111, 112 / Nợ 331, 152, 156, 627 - (Kinh doanh)
03 Tiền chi trả cho người lao động Có 111, 112 / Nợ 334 - (Kinh doanh)
04 Tiền chi trả lãi vay Có 111, 112 / Nợ 635 - (Kinh doanh)
05 Tiền chi nộp thuế TNDN Có 111, 112 / Nợ 3334 - (Kinh doanh)
21 Tiền chi mua sắm, xây dựng TSCĐ Có 111, 112 / Nợ 211, 241 - (Đầu tư)
31 Tiền thu từ phát hành cổ phiếu, nhận vốn góp Nợ 111, 112 / Có 411 + (Tài chính)
33 Tiền thu từ đi vay Nợ 111, 112 / Có 341 + (Tài chính)
34 Tiền chi trả nợ gốc vay Có 111, 112 / Nợ 341 - (Tài chính)

3. Lập trình Python & SQL Trích xuất Báo cáo Lưu chuyển Tiền tệ

Dưới đây là mã nguồn Python hoàn chỉnh sử dụng SQL để quét các cặp bút toán đối ứng của tài khoản tiền và tổng hợp Báo cáo Lưu chuyển Tiền tệ.

import sqlite3
import pandas as pd

def get_direct_cash_flow(from_date: str, to_date: str, db_name: str = "accounting_mini.db") -> dict:
    """
    Trích xuất Báo cáo Lưu chuyển Tiền tệ theo phương pháp trực tiếp trong khoảng [from_date, to_date].
    """
    conn = sqlite3.connect(db_name)
    
    # 1. Truy vấn các dòng nhật ký dính dán tới TK Tiền (111, 112) cùng TK Đối ứng trong từng giao dịch
    # Sử dụng Self-Join để tìm tài khoản đối ứng trong cùng một transaction_id
    query_cash_transactions = """
        SELECT 
            t.transaction_id,
            t.entry_date,
            j_cash.account_code AS cash_account,
            j_cash.debit_amount AS cash_debit,
            j_cash.credit_amount AS cash_credit,
            j_counter.account_code AS counterpart_account,
            j_counter.debit_amount AS counterpart_debit,
            j_counter.credit_amount AS counterpart_credit
        FROM journal_entries j_cash
        INNER JOIN transactions t ON j_cash.transaction_id = t.transaction_id
        INNER JOIN journal_entries j_counter ON j_cash.transaction_id = j_counter.transaction_id 
               AND j_cash.account_code != j_counter.account_code
        WHERE (j_cash.account_code LIKE '111%' OR j_cash.account_code LIKE '112%')
          AND t.entry_date >= ? AND t.entry_date <= ?
    """
    
    df_raw = pd.read_sql_query(query_cash_transactions, conn, params=[from_date, to_date])
    
    # 2. Truy vấn số dư Tiền ĐẦU KỲ và CUỐI KỲ để kiểm tra đối soát
    query_cash_balance = """
        SELECT 
            COALESCE(SUM(CASE WHEN t.entry_date < ? THEN j.debit_amount - j.credit_amount ELSE 0 END), 0.0) AS open_cash,
            COALESCE(SUM(CASE WHEN t.entry_date <= ? THEN j.debit_amount - j.credit_amount ELSE 0 END), 0.0) AS close_cash
        FROM journal_entries j
        INNER JOIN transactions t ON j.transaction_id = t.transaction_id
        WHERE j.account_code LIKE '111%' OR j.account_code LIKE '112%'
    """
    cursor = conn.cursor()
    cursor.execute(query_cash_balance, (from_date, to_date))
    open_cash, close_cash = cursor.fetchone()
    conn.close()

    # 3. Phân loại dòng tiền dựa vào tài khoản đối ứng
    cf_data = {
        "01": 0.0, # Thu từ bán hàng, dịch vụ
        "02": 0.0, # Chi trả cho người cung cấp
        "03": 0.0, # Chi trả cho người lao động
        "04": 0.0, # Chi trả lãi vay
        "05": 0.0, # Chi nộp thuế TNDN
        "07": 0.0, # Chi khác cho hoạt động kinh doanh
        "21": 0.0, # Chi mua sắm TSCĐ
        "31": 0.0, # Thu từ nhận vốn góp
        "33": 0.0, # Thu từ đi vay
        "34": 0.0  # Chi trả nợ gốc vay
    }

    for _, row in df_raw.iterrows():
        c_debit = row["cash_debit"]
        c_credit = row["cash_credit"]
        counter_code = str(row["counterpart_account"])

        # DÒNG TIỀN VÀO (Cash Debit > 0)
        if c_debit > 0:
            if counter_code.startswith(('511', '131', '521')):
                cf_data["01"] += c_debit
            elif counter_code.startswith('411'):
                cf_data["31"] += c_debit
            elif counter_code.startswith('341'):
                cf_data["33"] += c_debit

        # DÒNG TIỀN RA (Cash Credit > 0)
        elif c_credit > 0:
            if counter_code.startswith(('331', '152', '156', '632')):
                cf_data["02"] -= c_credit
            elif counter_code.startswith(('334', '622')):
                cf_data["03"] -= c_credit
            elif counter_code.startswith('635'):
                cf_data["04"] -= c_credit
            elif counter_code.startswith(('3334', '821')):
                cf_data["05"] -= c_credit
            elif counter_code.startswith(('211', '241')):
                cf_data["21"] -= c_credit
            elif counter_code.startswith('341'):
                cf_data["34"] -= c_credit
            elif counter_code.startswith(('641', '642')):
                cf_data["07"] -= c_credit

    # 4. TỔNG HỢP CÁC LƯU CHUYỂN TIỀN THUẦN
    # I. Lưu chuyển tiền từ Hoạt động Kinh doanh
    net_cf_operating = (cf_data["01"] + cf_data["02"] + cf_data["03"] + 
                        cf_data["04"] + cf_data["05"] + cf_data["07"])

    # II. Lưu chuyển tiền từ Hoạt động Đầu tư
    net_cf_investing = cf_data["21"]

    # III. Lưu chuyển tiền từ Hoạt động Tài chính
    net_cf_financing = cf_data["31"] + cf_data["33"] + cf_data["34"]

    # IV. Lưu chuyển tiền thuần trong kỳ (50 = 20 + 30 + 40)
    total_net_cash_flow = net_cf_operating + net_cf_investing + net_cf_financing

    # 5. ĐÓNG GÓI BÁO CÁO
    cf_structure = [
        {"Mã số": "01", "Chỉ tiêu": "1. Tiền thu từ bán hàng, cung cấp dịch vụ", "Số tiền": cf_data["01"]},
        {"Mã số": "02", "Chỉ tiêu": "2. Tiền chi trả cho người cung cấp hàng hóa, dịch vụ", "Số tiền": cf_data["02"]},
        {"Mã số": "03", "Chỉ tiêu": "3. Tiền chi trả cho người lao động", "Số tiền": cf_data["03"]},
        {"Mã số": "04", "Chỉ tiêu": "4. Tiền chi trả lãi vay", "Số tiền": cf_data["04"]},
        {"Mã số": "05", "Chỉ tiêu": "5. Tiền chi nộp thuế TNDN", "Số tiền": cf_data["05"]},
        {"Mã số": "07", "Chỉ tiêu": "6. Tiền chi khác cho hoạt động kinh doanh", "Số tiền": cf_data["07"]},
        {"Mã số": "20", "Chỉ tiêu": "I. LƯU CHUYỂN TIỀN THUẦN TỪ HOẠT ĐỘNG KINH DOANH", "Số tiền": net_cf_operating},
        
        {"Mã số": "21", "Chỉ tiêu": "1. Tiền chi mua sắm, xây dựng TSCĐ", "Số tiền": cf_data["21"]},
        {"Mã số": "30", "Chỉ tiêu": "II. LƯU CHUYỂN TIỀN THUẦN TỪ HOẠT ĐỘNG ĐẦU TƯ", "Số tiền": net_cf_investing},
        
        {"Mã số": "31", "Chỉ tiêu": "1. Tiền thu từ phát hành cổ phiếu, nhận vốn góp", "Số tiền": cf_data["31"]},
        {"Mã số": "33", "Chỉ tiêu": "2. Tiền thu từ đi vay", "Số tiền": cf_data["33"]},
        {"Mã số": "34", "Chỉ tiêu": "3. Tiền chi trả nợ gốc vay", "Số tiền": cf_data["34"]},
        {"Mã số": "40", "Chỉ tiêu": "III. LƯU CHUYỂN TIỀN THUẦN TỪ HOẠT ĐỘNG TÀI CHÍNH", "Số tiền": net_cf_financing},
        
        {"Mã số": "50", "Chỉ tiêu": "LƯU CHUYỂN TIỀN THUẦN TRONG KỲ (50 = 20 + 30 + 40)", "Số tiền": total_net_cash_flow},
        {"Mã số": "60", "Chỉ tiêu": "Tiền và tương đương tiền đầu kỳ", "Số tiền": open_cash},
        {"Mã số": "70", "Chỉ tiêu": "TIỀN VÀ TƯƠNG ĐƯƠNG TIỀN CUỐI KỲ (70 = 50 + 60)", "Số tiền": total_net_cash_flow + open_cash},
    ]

    df_cf = pd.DataFrame(cf_structure)
    
    # Đối soát kiểm tra tính nhất quán dữ liệu
    calc_closing_cash = open_cash + total_net_cash_flow
    is_reconciled = abs(calc_closing_cash - close_cash) < 1e-5

    return {
        "df": df_cf,
        "from_date": from_date,
        "to_date": to_date,
        "net_cash_flow": total_net_cash_flow,
        "open_cash": open_cash,
        "close_cash": close_cash,
        "is_reconciled": is_reconciled
    }

4. Chạy Thử Nghiệm & Kiểm Tra Đối Soát Dòng Tiền

Dưới đây là kịch bản chạy trích xuất Báo cáo Lưu chuyển Tiền tệ tháng 09/2026 và kiểm tra tính nhất quán đối soát với số dư tài khoản Tiền.

if __name__ == "__main__":
    cf_result = get_direct_cash_flow("2026-09-01", "2026-09-30")
    df_cf = cf_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ÁO CÁO LƯU CHUYỂN TIỀN TỆ (PHƯƠNG PHÁP TRỰC TIẾP)                ")
    print(f"               Kỳ: {cf_result['from_date']} đến {cf_result['to_date']}               ")
    print("==========================================================================")
    print(df_cf.to_string(index=False))
    print("--------------------------------------------------------------------------")
    print(f" Tiền đầu kỳ (Sổ kế toán) : {cf_result['open_cash']:,.0f} VNĐ")
    print(f" Lưu chuyển tiền thuần     : {cf_result['net_cash_flow']:,.0f} VNĐ")
    print(f" Tiền cuối kỳ (Tính toán)  : {cf_result['open_cash'] + cf_result['net_cash_flow']:,.0f} VNĐ")
    print(f" Tiền cuối kỳ (Sổ kế toán) : {cf_result['close_cash']:,.0f} VNĐ")
    print("--------------------------------------------------------------------------")

    if cf_result["is_reconciled"]:
        print("[XÁC NHẬN] ĐỐI SOÁT DÒNG TIỀN HOÀN TOÀN CHÍNH XÁC! (Khớp số dư Sổ cái Tiền)")
    else:
        print("[CẢNH BÁO LỖI] Có chênh lệch giữa Dòng tiền thuần và Biến động tài khoản Tiền!")
    print("==========================================================================")

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

Plaintext
==========================================================================
        BÁO CÁO LƯU CHUYỂN TIỀN TỆ (PHƯƠNG PHÁP TRỰC TIẾP)                
               Kỳ: 2026-09-01 đến 2026-09-30               
=====================================================================
Mã số                                           Chỉ tiêu      Số tiền
   01  1. Tiền thu từ bán hàng, cung cấp dịch vụ          115,000,000
   02  2. Tiền chi trả cho người cung cấp hàng hóa...               0
   03  3. Tiền chi trả cho người lao động                           0
   04  4. Tiền chi trả lãi vay                                      0
   05  5. Tiền chi nộp thuế TNDN                                    0
   07  6. Tiền chi khác cho hoạt động kinh doanh     -      2,000,000
   20  I. LƯU CHUYỂN TIỀN THUẦN TỪ HOẠT ĐỘNG KINH...      113,000,000
   21  1. Tiền chi mua sắm, xây dựng TSCĐ                           0
   30  II. LƯU CHUYỂN TIỀN THUẦN TỪ HOẠT ĐỘNG ĐẦU TƯ                0
   31  1. Tiền thu từ phát hành cổ phiếu, nhận vốn...               0
   33  2. Tiền thu từ đi vay                                        0
   34  3. Tiền chi trả nợ gốc vay                                   0
   40  II. LƯU CHUYỂN TIỀN THUẦN TỪ HOẠT ĐỘNG TÀI...                0
   50  LƯU CHUYỂN TIỀN THUẦN TRONG KỲ (50 = 20+30+40)     113,000,000
   60  Tiền và tương đương tiền đầu kỳ                              0
   70  TIỀN VÀ TƯƠNG ĐƯƠNG TIỀN CUỐI KỲ (70 = 50 + 60)    113,000,000
---------------------------------------------------------------------
 Tiền đầu kỳ (Sổ kế toán) : 0 VNĐ
 Lưu chuyển tiền thuần     : 113,000,000 VNĐ
 Tiền cuối kỳ (Tính toán)  : 113,000,000 VNĐ
 Tiền cuối kỳ (Sổ kế toán) : 113,000,000 VNĐ
--------------------------------------------------------------------------
[XÁC NHẬN] ĐỐI SOÁT DÒNG TIỀN HOÀN TOÀN CHÍNH XÁC! (Khớp số dư Sổ cái Tiền)
==========================================================================

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 Kỹ thuật Truy vấn SQL

Giao dịch rút tiền mặt từ Ngân hàng về quỹ ($Nợ\ TK\ 111 / Có\ TK\ 112$) sẽ làm ảnh hưởng như thế nào đến Báo cáo Lưu chuyển Tiền tệ theo phương pháp trực tiếp?

A. Làm tăng Dòng tiền Kinh doanh 111 và giảm Dòng tiền Kinh doanh 112.

B. Không làm thay đổi Lưu chuyển tiền thuần trong kỳ (chỉ là chuyển dịch nội bộ giữa các khoản tương đương tiền).

C. Làm tăng Dòng tiền từ Hoạt động Tài chính.

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

Hãy cải tiến câu lệnh SQL trong hàm get_direct_cash_flow() để loại trừ các bút toán chuyển tiền nội bộ ($Nợ\ TK\ 111 / Có\ TK\ 112$ hoặc ngược lại $Nợ\ TK\ 112 / Có\ TK\ 111$), đảm bảo dòng tiền vào và dòng tiền ra không bị ghi nhận trùng lặp.

Đáp án gợi ý:

  • Bài tập 1:

    • Đáp án đúng là B. Chuyển tiền giữa Tiền mặt và Tiền gửi Ngân hàng không tạo ra dòng tiền vào/ra ròng khỏi doanh nghiệp.

  • Bài tập 2:

    -- Bổ sung điều kiện loại trừ tài khoản đối ứng cũng thuộc nhóm Tiền tệ
    WHERE (j_cash.account_code LIKE '111%' OR j_cash.account_code LIKE '112%')
      AND NOT (j_counter.account_code LIKE '111%' OR j_counter.account_code LIKE '112%')
      AND t.entry_date >= ? AND t.entry_date <= ?