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ổ.
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)
Bước 1: Kết chuyển Giảm trừ Doanh thu (nếu có): Nợ TK 511 / Có TK 521.
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.
Bước 3: Kết chuyển Chi phí sang TK 911: Nợ TK 911 / Có TK 632, 635, 641, 642, 811.
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.
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.
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)}
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Ả 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
Đ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.
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.
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}.")