Cuối tháng, chị kế toán mở ba file Excel của ba chi nhánh, chép từng bảng sang một file chung, cộng lại, tô đậm tiêu đề, lưu thành "bao_cao_final_v3.xlsx" rồi đính kèm gửi sếp. Việc này mất hơn một tiếng. Tháng sau lại làm đúng như thế. Một ngày nào đó có người chép thiếu một dòng, và không ai biết.
Việc lặp lại, có quy tắc rõ ràng như vậy là thứ máy làm tốt nhất. Bài này cho bạn thấy một chương trình Python nhỏ làm hết chuỗi đó trong vài giây: đọc Excel, gộp số liệu, xuất báo cáo, gửi mail. Bạn không cần là dev. Chỉ cần chép code, thay tên cột và tên file cho khớp với công việc của bạn.
Chương trình sẽ làm gì
Hình dung một người trợ lý không bao giờ mệt, làm đúng bốn việc theo thứ tự.

Một, mở lần lượt mọi file Excel trong một thư mục. Hai, dán chúng lại thành một bảng dài và thêm cột "Thành tiền". Ba, cộng theo chi nhánh và theo sản phẩm rồi ghi ra một file báo cáo mới có nhiều trang tính. Bốn, soạn một lá thư, đính kèm file đó và gửi đi.

Phần dễ sợ nhất với người ngoài nghề là bước gửi mail. Nhỡ chạy thử mà thư bay thật tới sếp thì sao? Mình giải quyết bằng một "hộp thư giả" chạy ngay trên máy, sẽ nói ở phần sau.
Chuẩn bị
Bạn cần Python và công cụ uv. Nếu chưa có thì làm theo bài Bắt đầu với Python năm 2026: cài đặt, môi trường riêng và chạy chương trình đầu tiên. Sau đó tạo dự án và cài ba thư viện:
mkdir bao-cao && cd bao-cao
uv init --bare --python 3.15
uv add pandas openpyxl aiosmtpd
Ba thư viện này là: pandas (xử lý bảng số liệu), openpyxl (đọc và ghi file .xlsx) và aiosmtpd (làm hộp thư giả để thử). Mình chạy với pandas 3.0.6, openpyxl 3.1.5 và aiosmtpd 1.4.6 trên Python 3.15.0, kiểm ngày 21/10/2026.
Cho bạn nào viết code
Tạo file Excel mẫu
Để thử mà không đụng số liệu thật, mình cho máy tự sinh ba file của ba chi nhánh. Lưu thành tao_mau.py:
import random
from pathlib import Path
import pandas as pd
random.seed(7)
SAN_PHAM = {"Chuột không dây": 190000, "Bàn phím cơ": 650000, "Tai nghe": 480000, "Sạc nhanh": 220000}
Path("du_lieu").mkdir(exist_ok=True)
for ten in ["Quan1", "Quan7", "ThuDuc"]:
dong = []
for ngay in pd.date_range("2026-10-01", periods=10):
sp = random.choice(list(SAN_PHAM))
dong.append({"Ngày": ngay.date(), "Sản phẩm": sp,
"Số lượng": random.randint(1, 6), "Đơn giá": SAN_PHAM[sp]})
pd.DataFrame(dong).to_excel(f"du_lieu/{ten}.xlsx", index=False)
print("Đã tạo", ten)
Chạy uv run tao_mau.py. Thư mục du_lieu sẽ có ba file, mỗi file mười dòng bán hàng.
Gộp và làm báo cáo
Lưu thành bao_cao.py:
from pathlib import Path
import pandas as pd
from openpyxl.styles import Font
bang = []
for file in sorted(Path("du_lieu").glob("*.xlsx")):
df = pd.read_excel(file)
df["Chi nhánh"] = file.stem
bang.append(df)
tat_ca = pd.concat(bang, ignore_index=True)
tat_ca["Thành tiền"] = tat_ca["Số lượng"] * tat_ca["Đơn giá"]
theo_cn = tat_ca.groupby("Chi nhánh")["Thành tiền"].sum().reset_index()
theo_sp = (tat_ca.groupby("Sản phẩm")[["Số lượng", "Thành tiền"]].sum()
.sort_values("Thành tiền", ascending=False).reset_index())
with pd.ExcelWriter("bao_cao.xlsx", engine="openpyxl") as xl:
theo_cn.to_excel(xl, sheet_name="Theo chi nhánh", index=False)
theo_sp.to_excel(xl, sheet_name="Theo sản phẩm", index=False)
tat_ca.to_excel(xl, sheet_name="Chi tiết", index=False)
for ws in xl.book.worksheets:
for cot in ws.columns:
ws.column_dimensions[cot[0].column_letter].width = 18
for o in ws[1]:
o.font = Font(bold=True)
print(theo_cn.to_string(index=False))
print("Tổng:", f"{tat_ca['Thành tiền'].sum():,}".replace(",", "."), "đồng")
Chạy uv run bao_cao.py. Mình nhận được:
Chi nhánh Thành tiền
Quan1 6490000
Quan7 10540000
ThuDuc 13380000
Tổng: 30.410.000 đồng
Mở bao_cao.xlsx lên, bạn sẽ thấy ba trang tính: tổng theo chi nhánh, tổng theo sản phẩm và toàn bộ dòng chi tiết. Muốn dùng cho file thật, bạn chỉ cần sửa tên cột trong code cho đúng với tiêu đề cột của mình. Các file phải có cùng tên cột.
Gửi mail bằng Python
Lưu thành gui_mail.py:
import os, smtplib
from email.message import EmailMessage
from pathlib import Path
MAY_CHU = os.environ.get("SMTP_HOST", "127.0.0.1")
CONG = int(os.environ.get("SMTP_PORT", "8025"))
thu = EmailMessage()
thu["From"] = "bao-cao@congty.example"
thu["To"] = "giam-doc@congty.example"
thu["Subject"] = "Báo cáo doanh thu tháng 10"
thu.set_content("Chào anh,\n\nEm gửi báo cáo doanh thu kèm theo.\n")
file = Path("bao_cao.xlsx")
thu.add_attachment(file.read_bytes(), maintype="application",
subtype="vnd.openxmlformats-officedocument.spreadsheetml.sheet",
filename=file.name)
with smtplib.SMTP(MAY_CHU, CONG) as s:
s.send_message(thu)
print("Đã gửi tới", MAY_CHU, CONG)
Mặc định, chương trình gửi tới địa chỉ 127.0.0.1 cổng 8025, tức là chính máy bạn. Thư không đi đâu cả, trừ khi bạn tự đổi hai biến môi trường SMTP_HOST và SMTP_PORT.
Thử mà không sợ gửi nhầm

Mở một cửa sổ dòng lệnh thứ hai và lưu smtp_gia.py. Nó dựng một máy chủ mail giả ở cổng 8025, nhận thư rồi lưu thành file thay vì chuyển đi:
import time
from pathlib import Path
from aiosmtpd.controller import Controller
Path("hop_thu_gia").mkdir(exist_ok=True)
class LuuThu:
async def handle_DATA(self, server, session, envelope):
ten = Path("hop_thu_gia") / f"thu_{int(time.time() * 1000)}.eml"
ten.write_bytes(envelope.content)
print("Đã nhận thư gửi tới", envelope.rcpt_tos, "->", ten.name, flush=True)
return "250 OK"
Controller(LuuThu(), hostname="127.0.0.1", port=8025).start()
print("Máy chủ mail giả đang nghe ở cổng 8025", flush=True)
time.sleep(3600)
Chạy uv run smtp_gia.py ở cửa sổ này, rồi uv run gui_mail.py ở cửa sổ kia. Cửa sổ đầu in ra:
Máy chủ mail giả đang nghe ở cổng 8025
Đã nhận thư gửi tới ['giam-doc@congty.example'] -> thu_1792560339115.eml
Mình mở file thư đã lưu, tiêu đề đúng "Báo cáo doanh thu tháng 10", tệp đính kèm bao_cao.xlsx dài 7.347 byte, và mở ra bằng openpyxl vẫn thấy đủ ba trang tính, ô đầu tiên của tổng chi nhánh là 6.490.000. Nghĩa là toàn bộ đường đi từ Excel tới thư đều chạy đúng.
Khi mọi thứ ổn, bạn mới chuyển sang gửi thật. Dịch vụ mail thật thường đòi mã hoá và tài khoản, nên thêm hai dòng s.starttls() và s.login(tai_khoan, os.environ["SMTP_MAT_KHAU"]) trước send_message, địa chỉ và cổng theo nhà cung cấp. Với Gmail hay Outlook, thường phải dùng mật khẩu ứng dụng chứ không phải mật khẩu đăng nhập. Phần gửi thật này mình chưa chạy với dịch vụ nào, nên bạn thử với chính địa chỉ của mình trước. Mật khẩu để trong biến môi trường, đừng viết thẳng vào file code.
Lỗi hay gặp
Lỗi đầu tiên mình hay thấy: file Excel đang mở trong Excel khi chương trình chạy, và Windows khoá file không cho đọc hay ghi. Đóng file rồi chạy lại là xong.
Lỗi thứ hai là cột. Một chi nhánh gõ "Số lượng", chi nhánh kia gõ "SL", khi gộp ra cột trống. Thống nhất tên cột, hoặc thêm một dòng đổi tên cột trong code trước khi gộp.
Lỗi thứ ba là gõ số kiểu chữ. Ô trông như số nhưng Excel lưu là văn bản, pandas không cộng được. Đổi định dạng ô sang số, hoặc dùng pd.to_numeric để ép kiểu.
Làm tiếp
Có hai bước để biến bản này thành thói quen. Cho nó chạy tự động vào đúng giờ mỗi tháng (Task Scheduler trên Windows, cron trên Mac và Linux). Và đừng gửi thẳng cho sếp ở những lần đầu: cho nó gửi tới chính bạn một tháng, đối chiếu với cách làm tay, rồi mới đổi người nhận.
Nếu công việc của bạn có những bảng và quy tắc riêng, mình có thể viết giúp công cụ chạy đúng theo quy trình đó. Cứ nhắn mình qua trang liên hệ.



