Lập trình Tự động hóa Bút toán Kết chuyển Doanh thu, Chi phí và Xác định Kết quả Kinh doanh

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 nguyên lý kết chuyển cuối kỳ (Month-end Closing Process): Hiểu rõ bản chất xóa số dư các tài khoản tạm thời (Loại 5 đến Loại 8) về Tài khoản Xác định kết quả kinh doanh (TK 911) và chuyển Lợi nhuận sau thuế vào TK 421.

  • Xây dựng quy trình tự động hóa kết chuyển (Closing Engine): Lập trình thuật toán quét số dư ròng phát sinh trong kỳ, tự động phát sinh các bút toán Nợ/Có tương ứng mà không cần thao tác thủ công.

  • Đảm bảo tính toàn vẹn dữ liệu (Atomic Transactions): Sử dụng cơ chế Transaction (BEGIN, COMMIT, ROLLBACK) trong SQLite/Python để đảm bảo toàn bộ chuỗi bút toán kết chuyển thành công 100% hoặc hoàn tác nếu có lỗi.

  • Cập nhật Bảng Cân đối Tài khoản sau kết chuyển: Kiểm tra số dư các tài khoản Loại 5–9 về đúng bằng 0 sau khi hoàn tất đóng sổ.

2. Nguyên lý Kế toán & Luồng Dữ liệu Kết chuyển Cuối kỳ

Cuối kỳ kế toán (tháng/quý/năm), toàn bộ các tài khoản Doanh thu (Loại 5, 7) và Chi phí (Loại 6, 8) phải được kết chuyển sang Tài khoản 911 - Xác định kết quả kinh doanh để tính Lợi nhuận ròng.

                  SƠ ĐỒ LUỒNG BÚT TOÁN KẾT CHUYỂN CUỐI KỲ

     TK Loại 5, 7                  TK 911                  TK Loại 6, 8
   (Doanh thu, TN)          (Xác định KQKD)             (Chi phí)
 ───────┬─────────        ─────────┬─────────        ───────┬─────────
        │  Kết chuyển              │  Kết chuyển            │
        │  Doanh thu               │  Chi phí               │
        └─────────────────────────►│◄───────────────────────┘
                                   │
                                   ▼
                            TK 421 (Lợi nhuận
                            sau thuế chưa PP)
                          ─────────┬─────────
                                   │
                     Lãi ◄─────────┴─────────► Lỗ
                  (Nợ 911 / Có 421)      (Nợ 421 / Có 911)

Các bước thực hiện kết chuyển tự động:

  1. Bước 1: Kết chuyển Giảm trừ Doanh thu (nếu có): Nợ TK 511 / Có TK 521.

  2. Bước 2: Kết chuyển Doanh thu & Thu nhập ròng sang TK 911: Nợ TK 511, 515, 711 / Có TK 911.

  3. Bước 3: Kết chuyển Chi phí sang TK 911: Nợ TK 911 / Có TK 632, 635, 641, 642, 811.

  4. Bước 4: Tính Thuế TNDN hiện hành (TK 821) & Kết chuyển Thuế:

    • Nếu $\text{EBIT} > 0$: Bút toán Thuế = $\text{EBIT} \times 20\%$.

    • Kết chuyển Chi phí Thuế: Nợ TK 911 / Có TK 821.

  5. Bước 5: Kết chuyển Lợi nhuận sau thuế sang TK 4212:

    • Nếu Lãi ($\text{EBIT} - \text{Thuế} > 0$): Nợ TK 911 / Có TK 4212.

    • Nếu Lỗ ($\text{EBIT} < 0$): Nợ TK 4212 / Có TK 911.

3. Lập trình Python Tự động hóa Bút toán Kết chuyển

Dưới đây là mã nguồn Python đóng đóng gói quy trình kết chuyển tự động trong một Database Transaction.

import sqlite3
import pandas as pd
from datetime import datetime

def run_month_end_closing(period_date: str, db_name: str = "accounting_mini.db") -> dict:
    """
    Tự động kết chuyển Doanh thu, Chi phí, Xác định KQKD và Lợi nhuận sau thuế cho kỳ kế toán.
    period_date: Ngày cuối kỳ (ví dụ: '2026-09-30')
    """
    # Xác định ngày bắt đầu tháng từ period_date
    dt = datetime.strptime(period_date, "%Y-%m-%d")
    from_date = dt.strftime("%Y-%m-01")
    to_date = period_date
    
    conn = sqlite3.connect(db_name)
    cursor = conn.cursor()
    
    closing_entries = [] # Lưu các bút toán sẽ phát sinh
    
    try:
        # Bắt đầu Transaction nguyên tử
        cursor.execute("BEGIN TRANSACTION;")
        
        # 1. TRUY VẤN SỐ DƯ RÒNG TRONG KỲ CỦA CÁC TÀI KHỎAN LOẠI 5, 6, 7, 8
        query = """
            SELECT 
                j.account_code,
                a.account_name,
                COALESCE(SUM(j.debit_amount), 0.0) AS total_debit,
                COALESCE(SUM(j.credit_amount), 0.0) AS total_credit
            FROM journal_entries j
            INNER JOIN transactions t ON j.transaction_id = t.transaction_id
            INNER JOIN accounts a ON j.account_code = a.account_code
            WHERE t.entry_date >= ? AND t.entry_date <= ?
              AND (j.account_code LIKE '5%' OR j.account_code LIKE '6%' 
                   OR j.account_code LIKE '7%' OR j.account_code LIKE '8%')
            GROUP BY j.account_code, a.account_name
        """
        cursor.execute(query, (from_date, to_date))
        rows = cursor.fetchall()
        
        acc_balances = {}
        for code, name, debit, credit in rows:
            acc_balances[code] = {"debit": debit, "credit": credit, "net": debit - credit}

        # ----------------────────────────────────────────────────────
        # BƯỚC 1: KẾT CHUYỂN DOANH THU & THU NHẬP (Loại 5, 7) -> TK 911
        # ------------------------------------------------────────────
        total_revenue = 0.0
        revenue_lines = []
        
        for code, data in acc_balances.items():
            if code.startswith(('511', '515', '711')):
                net_credit = data["credit"] - data["debit"]
                if net_credit > 0:
                    total_revenue += net_credit
                    revenue_lines.append({"account_code": code, "debit": net_credit, "credit": 0.0})
        
        if total_revenue > 0:
            revenue_lines.append({"account_code": "911", "debit": 0.0, "credit": total_revenue})
            closing_entries.append({
                "desc": "Kết chuyển Doanh thu và Thu nhập trong kỳ sang TK 911",
                "lines": revenue_lines
            })

        # ----------------────────────────────────────────────────────
        # BƯỚC 2: KẾT CHUYỂN CHI PHÍ HOẠT ĐỘNG (632, 635, 641, 642, 811) -> TK 911
        # ------------------------------------------------────────────
        total_expense = 0.0
        expense_lines = []
        
        for code, data in acc_balances.items():
            if code.startswith(('632', '635', '641', '642', '811')):
                net_debit = data["debit"] - data["credit"]
                if net_debit > 0:
                    total_expense += net_debit
                    expense_lines.append({"account_code": code, "debit": 0.0, "credit": net_debit})
        
        if total_expense > 0:
            expense_lines.insert(0, {"account_code": "911", "debit": total_expense, "credit": 0.0})
            closing_entries.append({
                "desc": "Kết chuyển Chi phí hoạt động trong kỳ sang TK 911",
                "lines": expense_lines
            })

        # ----------------────────────────────────────────────────────
        # BƯỚC 3: TÍNH THUẾ TNDN HHIỆN HÀNH (20%) & KẾT CHUYỂN THUẾ
        # ------------------------------------------------────────────
        ebit = total_revenue - total_expense
        tax_amount = 0.0
        
        if ebit > 0:
            tax_amount = ebit * 0.20 # Giả định thuế suất TNDN 20%
            
            # Bút toán ghi nhận Chi phí thuế TNDN
            closing_entries.append({
                "desc": f"Ghi nhận Chi phí Thuế TNDN hiện hành (20% trên EBT = {ebit:,.0f})",
                "lines": [
                    {"account_code": "821", "debit": tax_amount, "credit": 0.0},
                    {"account_code": "3334", "debit": 0.0, "credit": tax_amount}
                ]
            })
            
            # Bút toán kết chuyển Chi phí thuế -> TK 911
            closing_entries.append({
                "desc": "Kết chuyển Chi phí Thuế TNDN sang TK 911",
                "lines": [
                    {"account_code": "911", "debit": tax_amount, "credit": 0.0},
                    {"account_code": "821", "debit": 0.0, "credit": tax_amount}
                ]
            })

        # --------------------------------────────────────────────────
        # BƯỚC 4: KẾT CHUYỂN LỢI NHUẬN SAU THUẾ -> TK 4212
        # ------------------------------------------------────────────
        net_profit = ebit - tax_amount
        
        if net_profit > 0:
            # Lãi: Nợ 911 / Có 4212
            closing_entries.append({
                "desc": "Kết chuyển Lãi sau thuế TNDN vào Lợi nhuận chưa phân phối",
                "lines": [
                    {"account_code": "911", "debit": net_profit, "credit": 0.0},
                    {"account_code": "4212", "debit": 0.0, "credit": net_profit}
                ]
            })
        elif net_profit < 0:
            # Lỗ: Nợ 4212 / Có 911
            loss_amount = abs(net_profit)
            closing_entries.append({
                "desc": "Kết chuyển Lỗ trong kỳ vào Lợi nhuận chưa phân phối",
                "lines": [
                    {"account_code": "4212", "debit": loss_amount, "credit": 0.0},
                    {"account_code": "911", "debit": 0.0, "credit": loss_amount}
                ]
            })

        # --------------------------------────────────────────────────
        # THỰC THI GHI CÁC BÚT TOÁN VÀO CƠ SỞ DỮ LIỆU SQL
        # ------------------------------------------------────────────
        posted_count = 0
        for entry in closing_entries:
            voucher_no = f"KC_{dt.strftime('%Y%m')}_{posted_count+1:02d}"
            
            cursor.execute(
                "INSERT INTO transactions (entry_date, voucher_number, description) VALUES (?, ?, ?)",
                (to_date, voucher_no, entry["desc"])
            )
            tx_id = cursor.lastrowid
            
            for line in entry["lines"]:
                cursor.execute(
                    "INSERT INTO journal_entries (transaction_id, account_code, debit_amount, credit_amount) VALUES (?, ?, ?, ?)",
                    (tx_id, line["account_code"], line["debit"], line["credit"])
                )
            posted_count += 1

        # Xác nhận thành công Transaction
        conn.commit()
        conn.close()
        
        return {
            "status": "SUCCESS",
            "entries_posted": posted_count,
            "ebit": ebit,
            "tax": tax_amount,
            "net_profit": net_profit
        }

    except Exception as e:
        conn.rollback()
        conn.close()
        return {"status": "FAILED", "error": str(e)}

4. Chạy Thử Nghiệm Kết Chuyển & Kiểm Tra Số Dư

Thực thi chạy kết chuyển tự động cho tháng 09/2026 và kiểm tra lại trạng thái các tài khoản tạm thời.

if __name__ == "__main__":
    # 1. Thực thi Quy trình Kết chuyển tự động
    result = run_month_end_closing("2026-09-30")
    
    print("==========================================================================")
    print("            KẾT QUẢ QUY TRÌNH KẾT CHUYỂN CUỐI KỲ TỰ ĐỘNG                  ")
    print("==========================================================================")
    print(f" Trạng thái        : {result['status']}")
    print(f" Số bút toán đã tạo: {result['entries_posted']}")
    print(f" Lợi nhuận HĐKD (EBT): {result['ebit']:,.0f} VNĐ")
    print(f" Thuế TNDN (20%)   : {result['tax']:,.0f} VNĐ")
    print(f" Lợi nhuận Sau thuế: {result['net_profit']:,.0f} VNĐ")
    print("==========================================================================")

    # 2. Truy vấn kiểm tra lại số dư các tài khoản Loại 5-9 sau kết chuyển
    conn = sqlite3.connect("accounting_mini.db")
    check_query = """
        SELECT 
            j.account_code,
            a.account_name,
            SUM(j.debit_amount) AS total_debit,
            SUM(j.credit_amount) AS total_credit,
            (SUM(j.debit_amount) - SUM(j.credit_amount)) AS balance
        FROM journal_entries j
        INNER JOIN accounts a ON j.account_code = a.account_code
        WHERE j.account_code LIKE '5%' OR j.account_code LIKE '6%' 
           OR j.account_code LIKE '7%' OR j.account_code LIKE '8%'
           OR j.account_code = '911'
        GROUP BY j.account_code, a.account_name
    """
    df_check = pd.read_sql_query(check_query, conn)
    conn.close()

    print("\n[KIỂM TRA SỐ DƯ TÀI KHỎAN TẠM THỜI SAU KẾT CHUYỂN]")
    print(df_check.to_string(index=False))

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

Plaintext
==========================================================================
            KẾT QUẢ QUY TRÌNH KẾT CHUYỂN CUỐI KỲ TỰ ĐỘNG                  
==========================================================================
 Trạng thái        : SUCCESS
 Số bút toán đã tạo: 4
 Lợi nhuận HĐKD (EBT): 5,000,000 VNĐ
 Thuế TNDN (20%)   : 1,000,000 VNĐ
 Lợi nhuận Sau thuế: 4,000,000 VNĐ
==========================================================================

[KIỂM TRA SỐ DƯ TÀI KHỎAN TẠM THỜI SAU KẾT CHUYỂN]
mã TK               Tên Tài khoản  total_debit  total_credit  balance
  511          Doanh thu bán hàng   15,000,000    15,000,000        0
  632           Giá vốn hàng bán    8,000,000     8,000,000        0
  642 Chi phí quản lý doanh nghiệp    2,000,000     2,000,000        0
  821           Chi phí thuế TNDN    1,000,000     1,000,000        0
  911  Xác định kết quả kinh doanh   15,000,000    15,000,000        0

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 Hệ thống

Điều gì sẽ xảy ra nếu quy trình kết chuyển bị ngắt điện giữa chừng khi đang chạy câu lệnh ghi bút toán thứ 2 trong chuỗi 4 bút toán, nếu ta KHÔNG sử dụng BEGIN TRANSACTION và ROLLBACK?

A. Cơ sở dữ liệu tự động quay về trạng thái cũ.

B. Dữ liệu bị sai lệch nghiêm trọng do một số tài khoản đã kết chuyển còn một số tài khoản chưa kết chuyển, làm mất cân bằng hệ thống.

C. Hàm Python sẽ tự động sửa lỗi và chạy lại từ đầu.

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

Hãy viết hàm rollback_closing_entries(period_date) cho phép hủy toàn bộ các bút toán kết chuyển của một kỳ kế toán (xóa các giao dịch có voucher_number bắt đầu bằng KC_YYYYMM) để kế toán có thể điều chỉnh lại sổ sách trước khi kết chuyển lại.

Đáp án gợi ý:

  • Bài tập 1:

    • Đáp án đúng là B. Đây là lý do tại sao các hệ thống tài chính/kế toán bắt buộc phải áp dụng tính chất ACID (Atomicity, Consistency, Isolation, Durability) của cơ sở dữ liệu giao dịch.

  • Bài tập 2:

def rollback_closing_entries(period_month: str, db_name: str = "accounting_mini.db"):
    """Hủy các bút toán kết chuyển tháng period_month (VD: '202609')"""
    conn = sqlite3.connect(db_name)
    cursor = conn.cursor()

    pattern = f"KC_{period_month}_%"

    # Xóa chi tiết nhật ký
    cursor.execute("""
        DELETE FROM journal_entries 
        WHERE transaction_id IN (
            SELECT transaction_id FROM transactions WHERE voucher_number LIKE ?
        )
    """, (pattern,))

    # Xóa giao dịch gốc
    cursor.execute("DELETE FROM transactions WHERE voucher_number LIKE ?", (pattern,))

    conn.commit()
    deleted_count = cursor.rowcount
    conn.close()
    print(f"Đã hủy thành công các bút toán kết chuyển của kỳ {period_month}.")