---
title: "Tự động hoá việc văn phòng bằng Python: gộp Excel, làm báo cáo và gửi mail"
url: https://phatdev.net/vi/blog/python-office-automation
lang: vi
updated: 2026-10-21
alternate:
  en: https://phatdev.net/en/blog/python-office-automation
markdown: https://phatdev.net/vi/blog/python-office-automation.md
---

# Tự động hoá việc văn phòng bằng Python: gộp Excel, làm báo cáo và gửi mail

> Cuối tháng chép số liệu ba chi nhánh mất cả tiếng. Một chương trình Python nhỏ gộp Excel, làm báo cáo và gửi mail trong vài giây, thử an toàn bằng hộp thư giả.

Chuyên mục: Backend | Đăng: 2026-10-21 | Cấp độ: Cơ bản | 8 phút đọc

Tags: python, excel, pandas, openpyxl, smtp, automation

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ự.

![Ba bảng của ba chi nhánh gộp thành một bảng tổng và biểu đồ](https://phatdev.net/uploads/blog/python-office-automation/merge-1920.webp)

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.

![Dòng chảy từ bảng tính qua bộ xử lý ra báo cáo rồi thành thư](https://phatdev.net/uploads/blog/python-office-automation/report-1920.webp)

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](https://phatdev.net/vi/blog/python-start-2026). Sau đó tạo dự án và cài ba thư viện:

```bash
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`:

```python
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`:

```python
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:

```text
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`:

```python
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

![Phong bì đi vào hộp thư thử kín, đường ra internet bị chặn](https://phatdev.net/uploads/blog/python-office-automation/mailbox-1920.webp)

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:

```python
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:

```text
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ệ.

## Nguồn tham khảo

- [pandas documentation](https://pandas.pydata.org/docs/)
- [openpyxl documentation](https://openpyxl.readthedocs.io/)
- [smtplib and email.message (Python docs)](https://docs.python.org/3/library/smtplib.html)
- [aiosmtpd documentation](https://aiosmtpd.aio-libs.org/)
- [uv projects guide (Astral)](https://docs.astral.sh/uv/guides/projects/)

## Bài liên quan

- [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](https://phatdev.net/vi/blog/python-start-2026.md)
- [Đồng bộ tồn kho giữa web, sàn thương mại điện tử và file Excel](https://phatdev.net/vi/blog/sync-inventory-web-marketplace.md)
- [Viết API đầu tiên với FastAPI: quản lý sản phẩm, kiểm dữ liệu và tài liệu tự sinh](https://phatdev.net/vi/blog/python-fastapi-first-api.md)
