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áo cáo P&L (Profit & Loss / Income Statement): Hiểu rõ cơ chế tập hợp Doanh thu (TK loại 5, 7) và Chi phí (TK loại 6, 8) để tính toán các chỉ tiêu tài chính then chốt.
Xây dựng cấu trúc chỉ tiêu (Financial Line Items Framework): Thiết lập mô hình mapping mã tài khoản SQL vào các dòng báo cáo chuẩn (Doanh thu thuần, Giá vốn, Lợi nhuận gộp, Chi phí quản lý, Lợi nhuận thuần).
Lập trình SQL chiết xuất chỉ tiêu kinh doanh: Viết các câu lệnh truy vấn phân loại dòng tiền theo tính chất doanh thu / chi phí trong khoảng thời gian [from_date, to_date].
Đóng gói Python API Báo cáo P&L: Xây dựng hàm Python tính toán tự động Lợi nhuận gộp, Lợi nhuận trước thuế và Lợi nhuận sau thuế, xuất kết quả ra Pandas DataFrame chuẩn mực.
Khác với Bảng Cân đối Kế toán (phản ánh trạng thái tài sản tại một thời điểm), Báo cáo P&L phản ánh hiệu quả hoạt động trong một khoảng thời gian.
┌─────────────────────────────────────────────────────────────────────────┐
│ DOANH THU THUẦN (Doanh thu bán hàng - Giảm trừ) │
└────────────────────────────────────┬────────────────────────────────────┘
│ (-) Giá vốn hàng bán (TK 632)
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ LỢI NHUẬN GỘP (Gross Profit) │
└────────────────────────────────────┬────────────────────────────────────┘
│ (+) Doanh thu tài chính (TK 515)
│ (-) Chi phí tài chính (TK 635)
│ (-) Chi phí bán hàng & QLDN (TK 641, 642)
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ LỢI NHUẬN THUẦN TỪ HOẠT ĐỘNG KINH DOANH │
└────────────────────────────────────┬────────────────────────────────────┘
│ (+) Lợi nhuận khác (TK 711 - TK 811)
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ LỢI NHUẬN TRƯỚC THUẾ (EBIT / EBT) │
└────────────────────────────────────┬────────────────────────────────────┘
│ (-) Chi phí thuế TNDN (TK 821)
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ LỢI NHUẬN SAU THUẾ (Net Income) │
└─────────────────────────────────────────────────────────────────────────┘
Tài khoản Doanh thu (Loại 5, 7): Giá trị ghi nhận = $\sum \text{credit\_amount} - \sum \text{debit\_amount}$
Tài khoản Chi phí (Loại 6, 8): Giá trị ghi nhận = $\sum \text{debit\_amount} - \sum \text{credit\_amount}$
Dưới đây là mã nguồn Python hoàn chỉnh truy vấn cơ sở dữ liệu và tổng hợp Báo cáo Kết quả Hoạt động Kinh doanh.
import sqlite3
import pandas as pd
def get_income_statement(from_date: str, to_date: str, db_name: str = "accounting_mini.db") -> dict:
"""
Trích xuất Báo cáo Kết quả Hoạt động Kinh doanh (P&L) trong khoảng thời gian [from_date, to_date].
"""
conn = sqlite3.connect(db_name)
# 1. Truy vấn phát sinh của các Tài khoản Doanh thu & Chi phí (Loại 5, 6, 7, 8, 9)
query = """
SELECT
j.account_code,
a.account_name,
a.normal_balance,
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, a.normal_balance
"""
df_raw = pd.read_sql_query(query, conn, params=[from_date, to_date])
conn.close()
# Tạo dictionary tra cứu nhanh tổng số tiền ròng theo mã TK
acc_summary = {}
for _, row in df_raw.iterrows():
code = str(row["account_code"])
debit = row["total_debit"]
credit = row["total_credit"]
# TK Doanh thu (Loại 5, 7): Số tiền = Có - Nợ
if code.startswith(('5', '7')):
acc_summary[code] = credit - debit
# TK Chi phí (Loại 6, 8): Số tiền = Nợ - Có
else:
acc_summary[code] = debit - credit
def get_val(prefix: str) -> float:
"""Hàm trợ giúp cộng dồn số tiền của các tài khoản bắt đầu bằng prefix"""
return sum(amount for code, amount in acc_summary.items() if code.startswith(prefix))
# 2. TÍNH TOÁN CÁC CHỈ TIÊU P&L
revenue_gross = get_val("511") # Doanh thu bán hàng
revenue_deductions = get_val("521") # Các khoản giảm trừ doanh thu
net_revenue = revenue_gross - revenue_deductions # 1. Doanh thu thuần
cogs = get_val("632") # 2. Giá vốn hàng bán
gross_profit = net_revenue - cogs # 3. Lợi nhuận gộp
fin_revenue = get_val("515") # Doanh thu tài chính
fin_expense = get_val("635") # Chi phí tài chính
selling_expense = get_val("641") # Chi phí bán hàng
admin_expense = get_val("642") # Chi phí quản lý doanh nghiệp
# 4. Lợi nhuận thuần từ hoạt động kinh doanh
operating_profit = gross_profit + fin_revenue - fin_expense - selling_expense - admin_expense
other_income = get_val("711") # Thu nhập khác
other_expense = get_val("811") # Chi phí khác
other_profit = other_income - other_expense # 5. Lợi nhuận khác
profit_before_tax = operating_profit + other_profit # 6. Lợi nhuận trước thuế (EBT)
tax_expense = get_val("821") # Chi phí thuế TNDN
net_profit = profit_before_tax - tax_expense # 7. LỢI NHUẬN SAU THUẾ
# 3. BẢNG TỔNG HỢP KẾT QUẢ
pnl_structure = [
{"Mã số": "01", "Chỉ tiêu": "1. Doanh thu bán hàng và cung cấp dịch vụ", "Số tiền": revenue_gross},
{"Mã số": "02", "Chỉ tiêu": "2. Các khoản giảm trừ doanh thu", "Số tiền": revenue_deductions},
{"Mã số": "10", "Chỉ tiêu": "3. DOANH THU THUẦN (10 = 01 - 02)", "Số tiền": net_revenue},
{"Mã số": "11", "Chỉ tiêu": "4. Giá vốn hàng bán", "Số tiền": cogs},
{"Mã số": "20", "Chỉ tiêu": "5. LỢI NHUẬN GỘP (20 = 10 - 11)", "Số tiền": gross_profit},
{"Mã số": "21", "Chỉ tiêu": "6. Doanh thu hoạt động tài chính", "Số tiền": fin_revenue},
{"Mã số": "22", "Chỉ tiêu": "7. Chi phí tài chính", "Số tiền": fin_expense},
{"Mã số": "25", "Chỉ tiêu": "8. Chi phí bán hàng", "Số tiền": selling_expense},
{"Mã số": "26", "Chỉ tiêu": "9. Chi phí quản lý doanh nghiệp", "Số tiền": admin_expense},
{"Mã số": "30", "Chỉ tiêu": "10. LỢI NHUẬN THUẦN TỪ HĐKD [30 = 20 + (21-22) - (25+26)]", "Số tiền": operating_profit},
{"Mã số": "31", "Chỉ tiêu": "11. Thu nhập khác", "Số tiền": other_income},
{"Mã số": "32", "Chỉ tiêu": "12. Chi phí khác", "Số tiền": other_expense},
{"Mã số": "40", "Chỉ tiêu": "13. Lợi nhuận khác (40 = 31 - 32)", "Số tiền": other_profit},
{"Mã số": "50", "Chỉ tiêu": "14. TỔNG LỢI NHUẬN TRƯỚC THUẾ (50 = 30 + 40)", "Số tiền": profit_before_tax},
{"Mã số": "51", "Chỉ tiêu": "15. Chi phí thuế TNDN hiện hành", "Số tiền": tax_expense},
{"Mã số": "60", "Chỉ tiêu": "16. LỢI NHUẬN SAU THUẾ TNDN (60 = 50 - 51)", "Số tiền": net_profit},
]
df_pnl = pd.DataFrame(pnl_structure)
return {
"df": df_pnl,
"from_date": from_date,
"to_date": to_date,
"net_profit": net_profit
}
Dưới đây là kịch bản nạp thêm một số giao dịch doanh thu/chi phí và chạy xuất báo cáo P&L.
if __name__ == "__main__":
from add_entry_script import add_journal_entry # Hàm từ Bài 1.2
# Nạp bổ sung chi phí phát sinh trong tháng 09/2026
add_journal_entry("2026-09-15", "PKX01", "Xuất kho hàng hóa bán (Giá vốn)", [
{"account_code": "632", "debit": 8000000, "credit": 0},
{"account_code": "156", "debit": 0, "credit": 8000000}
])
add_journal_entry("2026-09-20", "PC003", "Chi tiền mặt cho chi phí quản lý", [
{"account_code": "642", "debit": 2000000, "credit": 0},
{"account_code": "111", "debit": 0, "credit": 2000000}
])
# Thực thi xuất Báo cáo P&L
pnl_result = get_income_statement("2026-09-01", "2026-09-30")
df_pnl = pnl_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 KẾT QUẢ HOẠT ĐỘNG KINH DOANH (P&L) ")
print(f" Kỳ: {pnl_result['from_date']} đến {pnl_result['to_date']} ")
print("==========================================================================")
print(df_pnl.to_string(index=False))
print("==========================================================================")
print(f"-> LỢI NHUẬN SAU THUẾ TRONG KỲ: {pnl_result['net_profit']:,.0f} VNĐ")
print("==========================================================================")
==========================================================================
BÁO CÁO KẾT QUẢ HOẠT ĐỘNG KINH DOANH (P&L)
Kỳ: 2026-09-01 đến 2026-09-30
==========================================================================
Mã số Chỉ tiêu Số tiền
01 1. Doanh thu bán hàng và cung cấp dịch vụ 15,000,000
02 2. Các khoản giảm trừ doanh thu 0
10 3. DOANH THU THUẦN (10 = 01 - 02) 15,000,000
11 4. Giá vốn hàng bán 8,000,000
20 5. LỢI NHUẬN GỘP (20 = 10 - 11) 7,000,000
21 6. Doanh thu hoạt động tài chính 0
22 7. Chi phí tài chính 0
25 8. Chi phí bán hàng 0
26 9. Chi phí quản lý doanh nghiệp 2,000,000
30 10. LỢI NHUẬN THUẦN TỪ HĐKD
[30 = 20 + (21-22) - (25+26)] 5,000,000
31 11. Thu nhập khác 0
32 12. Chi phí khác 0
40 13. Lợi nhuận khác (40 = 31 - 32) 0
50 14. TỔNG LỢI NHUẬN TRƯỚC THUẾ (50 = 30 + 40) 5,000,000
51 15. Chi phí thuế TNDN hiện hành 0
60 16. LỢI NHUẬN SAU THUẾ TNDN (60 = 50 - 51) 5,000,000
==========================================================================
-> LỢI NHUẬN SAU THUẾ TRONG KỲ: 5,000,000 VNĐ
==========================================================================
Tại sao Báo cáo P&L chỉ lấy phát sinh trong kỳ [from_date, to_date] mà không cộng số dư đầu kỳ như Bảng Cân đối Kế toán?
A. Vì các tài khoản Doanh thu và Chi phí (Loại 5 đến Loại 9) là tài khoản tạm thời, đã được kết chuyển về 0 vào cuối mỗi kỳ kế toán.
B. Vì cơ sở dữ liệu SQLite tự động xóa dữ liệu doanh thu cũ.
C. Vì doanh thu và chi phí chỉ phát sinh vào ngày cuối tháng.
Hãy viết thêm hàm tính tỷ suất Biên lợi nhuận gộp (Gross Margin %) và Biên lợi nhuận ròng (Net Margin %) từ kết quả trả về của hàm get_income_statement().
(Gợi ý: $\text{Gross Margin \%} = \frac{\text{Gross Profit}}{\text{Net Revenue}} \times 100$)
Bài tập 1:
Đáp án đúng là A. Tất cả tài khoản thuộc Báo cáo Kết quả Kinh doanh không có số dư chuyển sang kỳ sau; toàn bộ số dư cuối kỳ sẽ được kết chuyển sang TK 911 để xác định kết quả kinh doanh.
Bài tập 2:
def calculate_margins(pnl_result: dict) -> dict:
df = pnl_result["df"]
net_rev = df.loc[df["Mã số"] == "10", "Số tiền"].values[0]
gross_p = df.loc[df["Mã số"] == "20", "Số tiền"].values[0]
net_p = df.loc[df["Mã số"] == "60", "Số tiền"].values[0]
gross_margin = (gross_p / net_rev * 100) if net_rev > 0 else 0.0
net_margin = (net_p / net_rev * 100) if net_rev > 0 else 0.0
return {
"gross_margin_pct": round(gross_margin, 2),
"net_margin_pct": round(net_margin, 2)
}