Sau khi hoàn thành bài học này, người học có thể:
Thành thạo kỹ thuật JOIN đa bảng trong SQL: Viết các câu lệnh SQL phức tạp (INNER JOIN) để kết hợp dữ liệu giữa 3 bảng transactions, journal_entries, và accounts.
Chiết xuất Sổ Nhật ký chung (General Journal): Lập trình truy vấn hiển thị toàn bộ các nghiệp vụ phát sinh theo thứ tự thời gian với đầy đủ thông tin Chứng từ, Diễn giải, Tài khoản Nợ/Có và Số tiền.
Chiết xuất Sổ Cái (General Ledger) cho từng Tài khoản: Tính toán tự động Số dư đầu kỳ, Các phát sinh Nợ/Có trong kỳ, Số dư phát sinh lũy kế, và Số dư cuối kỳ của một tài khoản bất kỳ.
Xuất báo cáo dưới dạng Pandas DataFrame: Chuyển đổi kết quả truy vấn SQL thành cấu trúc bảng chuẩn để dễ dàng phân tích hoặc xuất ra file Excel.
Trong cơ sở dữ liệu kế toán chuẩn hóa, dữ liệu bị phân tách ra các bảng khác nhau. Để lập sổ sách kế toán, ta cần ghép nối dữ liệu:
┌─────────────────────────┐ ┌─────────────────────────┐ ┌─────────────────────────┐
│ TRANSACTIONS │ │ JOURNAL_ENTRIES │ │ ACCOUNTS │
├─────────────────────────┤ ├─────────────────────────┤ ├─────────────────────────┤
│ transaction_id (PK) ────┼───────┼─ transaction_id (FK) │ │ account_code (PK) │
│ entry_date │ │ account_code (FK) ─────┼───────┼─ account_name │
│ ref_no │ │ debit_amount │ │ normal_balance │
│ description │ │ credit_amount │ └─────────────────────────┘
└─────────────────────────┘ └─────────────────────────┘
Sổ Nhật ký chung: Ghép transactions + journal_entries + accounts để hiển thị dòng tiền Nợ/Có theo từng chứng từ.
Sổ Cái từng Tài khoản: Lọc toàn bộ các dòng chi tiết trong journal_entries liên quan đến một account_code cụ thể trong một khoảng thời gian [from_date, to_date].
Dưới đây là mã nguồn Python sử dụng thư viện pandas và sqlite3 để truy vấn và định dạng sổ sách kế toán chuyên nghiệp.
import sqlite3
import pandas as pd
# Cấu hình hiển thị cho Pandas DataFrame
pd.set_option('display.max_columns', None)
pd.set_option('display.width', 1000)
pd.set_option('display.float_format', lambda x: '{:,.0f}'.format(x))
def get_general_journal(from_date: str = None, to_date: str = None, db_name: str = "accounting_mini.db") -> pd.DataFrame:
"""
Truy vấn và chiết xuất Sổ Nhật ký chung (General Journal).
"""
conn = sqlite3.connect(db_name)
query = """
SELECT
t.entry_date AS "Ngày ghi sổ",
t.ref_no AS "Số chứng từ",
t.description AS "Diễn giải",
j.account_code AS "Mã TK",
a.account_name AS "Tên Tài khoản",
j.debit_amount AS "Ghi Nợ (VNĐ)",
j.credit_amount AS "Ghi Có (VNĐ)"
FROM transactions t
INNER JOIN journal_entries j ON t.transaction_id = j.transaction_id
INNER JOIN accounts a ON j.account_code = a.account_code
WHERE 1=1
"""
params = []
if from_date:
query += " AND t.entry_date >= ?"
params.append(from_date)
if to_date:
query += " AND t.entry_date <= ?"
params.append(to_date)
query += " ORDER BY t.entry_date ASC, t.transaction_id ASC, j.entry_id ASC"
df = pd.read_sql_query(query, conn, params=params)
conn.close()
return df
def get_general_ledger(account_code: str, from_date: str, to_date: str, db_name: str = "accounting_mini.db") -> dict:
"""
Truy vấn và tính toán Sổ Cái (General Ledger) cho một tài khoản cụ thể.
Bao gồm: Số dư đầu kỳ, Chi tiết phát sinh, và Số dư cuối kỳ.
"""
conn = sqlite3.connect(db_name)
cursor = conn.cursor()
# 1. Lấy thông tin Tài khoản
cursor.execute("SELECT account_name, normal_balance FROM accounts WHERE account_code = ?", (account_code,))
acc_info = cursor.fetchone()
if not acc_info:
conn.close()
raise ValueError(f"Mã tài khoản '{account_code}' không tồn tại!")
account_name, normal_balance = acc_info
# 2. Tính SỐ DƯ ĐẦU KỲ (Tính tổng Nợ/Có trước ngày from_date)
cursor.execute("""
SELECT
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
WHERE j.account_code = ? AND t.entry_date < ?
""", (account_code, from_date))
pre_debit, pre_credit = cursor.fetchone()
if normal_balance == 'DEBIT':
opening_balance = pre_debit - pre_credit
else:
opening_balance = pre_credit - pre_debit
# 3. Truy vấn CÁC PHÁT SINH TRONG KỲ [from_date -> to_date]
query_period = """
SELECT
t.entry_date AS "Ngày GS",
t.ref_no AS "Số CT",
t.description AS "Diễn giải",
j.debit_amount AS "Phát sinh Nợ",
j.credit_amount AS "Phát sinh Có"
FROM journal_entries j
INNER JOIN transactions t ON j.transaction_id = t.transaction_id
WHERE j.account_code = ? AND t.entry_date >= ? AND t.entry_date <= ?
ORDER BY t.entry_date ASC, t.transaction_id ASC
"""
df_period = pd.read_sql_query(query_period, conn, params=[account_code, from_date, to_date])
conn.close()
# 4. Tính toán SỐ DƯ LŨY KẾ cho từng dòng và SỐ DƯ CUỐI KỲ
total_period_debit = df_period["Phát sinh Nợ"].sum() if not df_period.empty else 0.0
total_period_credit = df_period["Phát sinh Có"].sum() if not df_period.empty else 0.0
if normal_balance == 'DEBIT':
closing_balance = opening_balance + total_period_debit - total_period_credit
else:
closing_balance = opening_balance + total_period_credit - total_period_debit
return {
"account_code": account_code,
"account_name": account_name,
"normal_balance": normal_balance,
"from_date": from_date,
"to_date": to_date,
"opening_balance": opening_balance,
"total_debit": total_period_debit,
"total_credit": total_period_credit,
"closing_balance": closing_balance,
"transactions_df": df_period
}
Dưới đây là đoạn script thực thi để nạp dữ liệu mẫu và in ra Sổ Nhật ký chung và Sổ Cái Tài khoản 111 (Tiền mặt).
if __name__ == "__main__":
from add_entry_script import add_journal_entry # Hàm từ Bài 1.2
# 1. Nạp danh sách dữ liệu mẫu trong tháng 09/2026
add_journal_entry("2026-09-01", "GOPVON", "Chủ sở hữu góp vốn tiền mặt", [
{"account_code": "111", "debit": 100000000, "credit": 0},
{"account_code": "411", "debit": 0, "credit": 100000000}
])
add_journal_entry("2026-09-05", "TGNH", "Gửi tiền mặt vào Ngân hàng", [
{"account_code": "112", "debit": 40000000, "credit": 0},
{"account_code": "111", "debit": 0, "credit": 40000000}
])
add_journal_entry("2026-09-10", "PT001", "Thu tiền mặt bán hàng dịch vụ", [
{"account_code": "111", "debit": 15000000, "credit": 0},
{"account_code": "511", "debit": 0, "credit": 15000000}
])
# 2. Xuất SỔ NHẬT KÝ CHUNG
print("==========================================================================")
print(" SỔ NHẬT KÝ CHUNG ")
print("==========================================================================")
df_journal = get_general_journal("2026-09-01", "2026-09-30")
print(df_journal.to_string(index=False))
# 3. Xuất SỔ CÁI TÀI KHỎAN 111 (TIỀN MẶT)
print("\n==========================================================================")
print(" SỔ CÁI TÀI KHỎAN 111 - TIỀN MẶT ")
print("==========================================================================")
ledger_111 = get_general_ledger("111", "2026-09-01", "2026-09-30")
print(f"Từ ngày: {ledger_111['from_date']} Đến ngày: {ledger_111['to_date']}")
print(f"Số dư đầu kỳ: {ledger_111['opening_balance']:,.0f} VNĐ")
print("--------------------------------------------------------------------------")
print(ledger_111['transactions_df'].to_string(index=False))
print("--------------------------------------------------------------------------")
print(f"Tổng phát sinh NỢ : {ledger_111['total_debit']:,.0f} VNĐ")
print(f"Tổng phát sinh CÓ : {ledger_111['total_credit']:,.0f} VNĐ")
print(f"SỐ DƯ CUỐI KỲ : {ledger_111['closing_balance']:,.0f} VNĐ")
print("==========================================================================")
Khi tính Số dư Đầu kỳ cho một Tài khoản thuộc nhóm NỢ PHẢI TRẢ (LIABILITY) có normal_balance = 'CREDIT', công thức SQL / Python đúng để tính opening_balance từ tổng Nợ (pre_debit) và tổng Có (pre_credit) trước ngày bắt đầu là gì?
A. opening_balance = pre_debit - pre_credit
B. opening_balance = pre_credit - pre_debit
C. opening_balance = pre_debit + pre_credit
Sử dụng hàm get_general_ledger() đã viết ở trên để chiết xuất Sổ Cái cho TK 511 (Doanh thu bán hàng) từ ngày 2026-09-01 đến 2026-09-30. Cho biết Số dư cuối kỳ của TK 511 là bao nhiêu.
Bài tập 1:
Đáp án đúng là B. Tài khoản Nguồn vốn / Nợ phải trả tăng bên Có, giảm bên Nợ, do đó Số dư = Tổng Có - Tổng Nợ.
Bài tập 2:
Chạy câu lệnh: ledger_511 = get_general_ledger("511", "2026-09-01", "2026-09-30")
Dựa trên các bút toán mẫu ở kịch bản trên, TK 511 phát sinh Có $15.000.000 \text{ VNĐ}$. Vì đây là tài khoản Doanh thu (số dư bình thường bên Có), nên Số dư cuối kỳ của TK 511 = 15.000.000 VNĐ.