Bỏ qua để đến nội dung chính
Điền NguyễnAZ

dịch vụ, google-sheets

Tạo PDF hàng loạt từ Google Sheets bằng Apps Script và tự gửi email

31/10/2026· Điền Nguyễn AZ
Tạo PDF hàng loạt từ Google Sheets bằng Apps Script và tự gửi email

Muốn mỗi dòng dữ liệu trong Google Sheets thành một file PDF riêng (phiếu lương, phiếu báo điểm, giấy báo nợ) rồi tự gửi email cho đúng người, cách gọn nhất là: dựng một sheet mẫu lấy dữ liệu theo một ô mã, sau đó dùng Apps Script lần lượt đổi mã, xuất vùng mẫu ra PDF, lưu vào Drive và gửi email đính kèm. Bài này có mã chạy được, giải thích từng phần và có chế độ chạy thử để không gửi nhầm.

Ví dụ xuyên suốt là gửi phiếu lương tháng cho nhân viên, nhưng bạn có thể áp dụng y hệt cho phiếu báo điểm học sinh gửi phụ huynh, thông báo học phí, xác nhận đơn hàng.

Cách này khác gì cách tạo hợp đồng từ Google Docs?

Trên blog đã có bài tự động tạo hợp đồng Word từ Excel, Google Sheets, dùng file mẫu Google Docs và thay các chữ {{TenCot}}. Cách đó hợp với văn bản nhiều chữ.

Bài này dùng chính một sheet làm mẫu. Ưu điểm:

  • Phiếu có nhiều con số, bảng tính toán (lương, khấu trừ, tổng điểm) thì làm trên Sheets dễ hơn nhiều so với Docs.
  • Công thức trên phiếu tự tính, định dạng số tiền, ngày tháng giữ nguyên như bạn thấy trên màn hình.
  • Sửa mẫu không cần đụng vào code.

Bước 1: Chuẩn bị sheet dữ liệu

Tạo sheet tên BangLuong, dòng 1 là tiêu đề không dấu, không khoảng trắng để script tìm cột theo tên:

A B C D E F G H I
1 MaNV HoTen Email PhongBan LuongCoBan PhuCap NgayCong ThucLinh TrangThai
2 NV01 Nguyễn Thị Hoa hoa@... Kế toán 7.000.000 500.000 26 7.500.000
3 NV02 Trần Văn Minh minh@... Kinh doanh 6.500.000 800.000 24 6.800.000
  • MaNV phải duy nhất, không trùng.
  • ThucLinh là công thức tính theo quy chế lương của đơn vị bạn. Ví dụ minh họa với 26 ngày công chuẩn: =ROUND(E2/26*G2, 0) + F2.
  • TrangThai để trống. Script sẽ ghi "Đã gửi" kèm thời gian vào đây, nhờ vậy chạy lại không bị gửi trùng.

Kiểm tra kỹ cột Email: một địa chỉ gõ sai là phiếu lương của người này đến tay người khác.

Bước 2: Dựng sheet mẫu phiếu lương

Tạo sheet PhieuLuong và thiết kế phiếu trong vùng A1:F25 như một tờ giấy: tên công ty, tiêu đề "PHIẾU LƯƠNG THÁNG 10/2026", bảng các khoản. Bí quyết là mọi ô đều lấy dữ liệu từ một ô mã duy nhất:

  • Ô B3: gõ tạm NV01. Đây là ô script sẽ đổi lần lượt.
  • Ô họ tên: =VLOOKUP($B$3, BangLuong!$A:$I, 2, FALSE)
  • Ô phòng ban: =VLOOKUP($B$3, BangLuong!$A:$I, 4, FALSE)
  • Ô lương cơ bản: =VLOOKUP($B$3, BangLuong!$A:$I, 5, FALSE), tương tự với phụ cấp (cột 6), ngày công (cột 7), thực lĩnh (cột 8).

Số 2, 4, 5... là thứ tự cột trong vùng A:I (A=1, B=2...). FALSE nghĩa là tìm khớp chính xác mã.

Định dạng ô tiền bằng Format → Number → Custom number format, nhập #,##0 "đ". Ẩn đường lưới bằng View → Show → Gridlines (bỏ tích) để nhìn giống bản in.

Thử đổi B3 thành NV02: toàn bộ phiếu phải đổi sang thông tin của Minh. Nếu được, phần khó nhất đã xong.

Bước 3: Tạo thư mục lưu PDF

  1. Trên Google Drive, tạo thư mục, ví dụ PhieuLuong_2026-10.
  2. Mở thư mục, copy ID: phần chuỗi sau /folders/ trên thanh địa chỉ.
  3. Không chia sẻ thư mục này cho ai ngoài người phụ trách lương.

Bước 4: Dán mã Apps Script

Trong file Sheets, chọn Extensions (Tiện ích mở rộng) → Apps Script, xóa mã mặc định, dán toàn bộ đoạn sau, sửa FOLDER_ID và KY_LUONG, bấm Save (biểu tượng đĩa mềm).

const CAU_HINH = {
  SHEET_DU_LIEU: 'BangLuong',
  SHEET_MAU: 'PhieuLuong',
  O_MA: 'B3',              // ô chứa mã NV trên phiếu mẫu
  VUNG_IN: 'A1:F25',       // vùng phiếu xuất ra PDF
  FOLDER_ID: 'DAN_ID_THU_MUC_VAO_DAY',
  KY_LUONG: 'tháng 10/2026',
  CHAY_THU: true,          // true: chỉ làm 3 dòng đầu, gửi về email của bạn
  SO_DONG_THU: 3
};

function onOpen() {
  SpreadsheetApp.getUi().createMenu('Phiếu lương')
    .addItem('Tạo PDF và gửi email', 'guiPhieuLuong')
    .addToUi();
}

function guiPhieuLuong() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const shDuLieu = ss.getSheetByName(CAU_HINH.SHEET_DU_LIEU);
  const shMau = ss.getSheetByName(CAU_HINH.SHEET_MAU);
  const thuMuc = DriveApp.getFolderById(CAU_HINH.FOLDER_ID);
  const tz = ss.getSpreadsheetTimeZone();
  const emailCuaToi = Session.getActiveUser().getEmail();

  const data = shDuLieu.getDataRange().getValues();
  const tieuDe = data[0];
  const cMa = tieuDe.indexOf('MaNV');
  const cTen = tieuDe.indexOf('HoTen');
  const cEmail = tieuDe.indexOf('Email');
  const cTT = tieuDe.indexOf('TrangThai');
  if ([cMa, cTen, cEmail, cTT].includes(-1)) {
    throw new Error('Thiếu cột MaNV, HoTen, Email hoặc TrangThai ở dòng 1');
  }

  const batDau = Date.now();
  let soDaLam = 0;

  for (let i = 1; i < data.length; i++) {
    const dong = data[i];
    if (!dong[cMa] || dong[cTT]) continue;              // dòng trống hoặc đã xử lý
    if (CAU_HINH.CHAY_THU && soDaLam >= CAU_HINH.SO_DONG_THU) break;
    if (Date.now() - batDau > 5 * 60 * 1000) {          // tránh giới hạn 6 phút
      SpreadsheetApp.getUi().alert('Đã chạy gần 5 phút. Bấm chạy lại để làm tiếp phần còn lại.');
      return;
    }

    const oTrangThai = shDuLieu.getRange(i + 1, cTT + 1);
    try {
      shMau.getRange(CAU_HINH.O_MA).setValue(dong[cMa]);
      SpreadsheetApp.flush();                           // chờ công thức trên phiếu tính xong

      const tenFile = ('PhieuLuong_' + dong[cMa] + '_' + dong[cTen])
        .replace(/[\\/:*?"<>|]/g, '-') + '.pdf';
      const pdf = xuatPdf(ss.getId(), shMau.getSheetId(), CAU_HINH.VUNG_IN);
      const file = thuMuc.createFile(pdf.setName(tenFile));

      MailApp.sendEmail({
        to: CAU_HINH.CHAY_THU ? emailCuaToi : dong[cEmail],
        subject: 'Phiếu lương ' + CAU_HINH.KY_LUONG + ' - ' + dong[cTen],
        body: 'Chào ' + dong[cTen] + ',\n\n' +
              'Phòng kế toán gửi bạn phiếu lương ' + CAU_HINH.KY_LUONG +
              ' trong file đính kèm. Nếu có sai sót, vui lòng phản hồi email này.\n\nTrân trọng.',
        attachments: [file.getBlob()]
      });

      const thoiGian = Utilities.formatDate(new Date(), tz, 'dd/MM/yyyy HH:mm');
      oTrangThai.setValue((CAU_HINH.CHAY_THU ? 'Chạy thử - ' : 'Đã gửi - ') + thoiGian);
    } catch (e) {
      oTrangThai.setValue('Lỗi: ' + e.message);
    }
    soDaLam++;
    Utilities.sleep(1500);                              // nghỉ giữa các lần xuất PDF
  }
  SpreadsheetApp.getUi().alert('Xong ' + soDaLam + ' dòng.');
}

function xuatPdf(ssId, gid, vung) {
  const thamSo = {
    format: 'pdf', gid: gid, range: vung,
    size: 'A4', portrait: 'true', fitw: 'true',
    gridlines: 'false', printtitle: 'false', sheetnames: 'false', pagenum: 'UNDEFINED',
    top_margin: '0.5', bottom_margin: '0.5', left_margin: '0.5', right_margin: '0.5'
  };
  const query = Object.keys(thamSo)
    .map(k => k + '=' + encodeURIComponent(thamSo[k])).join('&');
  const url = 'https://docs.google.com/spreadsheets/d/' + ssId + '/export?' + query;

  for (let lan = 1; lan <= 3; lan++) {
    const res = UrlFetchApp.fetch(url, {
      headers: { Authorization: 'Bearer ' + ScriptApp.getOAuthToken() },
      muteHttpExceptions: true
    });
    if (res.getResponseCode() === 200) return res.getBlob();
    Utilities.sleep(5000 * lan);                        // bị từ chối thì chờ lâu dần rồi thử lại
  }
  throw new Error('Không xuất được PDF sau 3 lần thử');
}

Giải thích các phần quan trọng

CAU_HINH gom mọi thứ có thể thay đổi lên đầu: tên sheet, ô mã, vùng in, thư mục. Tháng sau chỉ cần sửa KY_LUONG và FOLDER_ID, không phải đọc lại cả đoạn mã.

onOpen tạo menu Phiếu lương trên thanh công cụ mỗi khi mở file, để người phụ trách bấm chạy mà không phải vào trình soạn thảo mã.

Tìm cột theo tên (indexOf('MaNV')): bạn chèn thêm cột giữa bảng vẫn không làm script sai.

SpreadsheetApp.flush() rất quan trọng. Apps Script gom các thay đổi lại để ghi một lần; nếu không có dòng này, có thể xuất PDF khi công thức VLOOKUP chưa kịp cập nhật, kết quả là phiếu của NV02 lại mang số liệu của NV01.

xuatPdf gọi địa chỉ xuất file của chính Google Sheets, giống thao tác File → Download → PDF, kèm các tham số:

  • gid và range: chỉ xuất sheet PhieuLuong, vùng A1:F25, không xuất cả file.
  • size=A4, portrait=true: khổ A4 dọc. Đổi portrait thành false để in ngang.
  • fitw=true: co vừa chiều ngang trang.
  • gridlines=false, sheetnames=false, printtitle=false: bỏ đường lưới, tên sheet, tiêu đề file khỏi bản in.
  • top_margin...: lề trang, đơn vị inch.
  • ScriptApp.getOAuthToken(): dùng chính quyền của bạn để tải file, nên chỉ xuất được file bạn có quyền.

Lưu ý: địa chỉ xuất này không nằm trong tài liệu API chính thức của Apps Script, dù được dùng rất phổ biến. Nếu một ngày Google thay đổi, cách dự phòng đơn giản là ss.getAs('application/pdf'), nhưng cách đó xuất toàn bộ các sheet nên bạn phải tách phiếu sang một file riêng.

Vòng thử lại 3 lần trong xuatPdf: khi xuất quá dày, Google có thể tạm từ chối (thường là mã lỗi 429). Script chờ 5 giây, 10 giây rồi thử lại, kết hợp Utilities.sleep(1500) giữa các nhân viên để hạn chế lỗi này.

Kiểm tra 5 phút: một lần chạy Apps Script bị giới hạn khoảng 6 phút. Danh sách dài thì script tự dừng, báo bạn bấm chạy lại; nhờ cột TrangThai, lần sau nó làm tiếp từ người chưa gửi.

Bước 5: Chạy thử, rồi mới gửi thật

  1. Tải lại trang Google Sheets. Menu Phiếu lương xuất hiện sau vài giây.
  2. Bấm Phiếu lương → Tạo PDF và gửi email. Lần đầu Google hỏi cấp quyền: chọn tài khoản, nếu thấy cảnh báo "Google hasn't verified this app" thì bấm Advanced → Go to ... (unsafe), rồi Allow. Cảnh báo này xuất hiện vì đây là script bạn tự viết, chưa đăng ký với Google.
  3. Với CHAY_THU: true, script chỉ làm 3 dòng đầu và gửi về email của bạn. Mở từng PDF kiểm tra: đúng tên, đúng số liệu, không lệch trang.
  4. Kiểm tra xong: xóa nội dung cột TrangThai (các dòng ghi "Chạy thử"), xóa các PDF thử trong thư mục, sửa thành CHAY_THU: false, lưu và chạy lại.

Phiếu bị tràn sang trang thứ hai? Thu nhỏ vùng phiếu, giảm lề, hoặc kiểm tra VUNG_IN có thừa cột trống không.

Phần nâng cao: những điều nên biết trước khi gửi lương qua email

Giới hạn số email mỗi ngày. Google giới hạn số người nhận email gửi bằng Apps Script mỗi ngày, tài khoản Gmail cá nhân thấp hơn nhiều so với Google Workspace. Thêm dòng Logger.log(MailApp.getRemainingDailyQuota()); vào một hàm riêng và chạy để xem còn gửi được bao nhiêu. Chi tiết trong bài gửi email tự động từ Google Sheets.

Bảo mật. Phiếu lương là thông tin nhạy cảm:

  • Không chia sẻ file Sheets gốc cho nhân viên; ai có quyền xem đều thấy lương của mọi người.
  • Thư mục PDF chỉ để người phụ trách truy cập.
  • Apps Script không có sẵn chức năng đặt mật khẩu cho file PDF. Nếu đơn vị bắt buộc PDF có mật khẩu, cần giải pháp khác thay vì cố làm bằng đoạn mã này.

Không đặt lịch chạy tự động cho việc gửi lương. Lương cần người kiểm tra số liệu trước khi gửi, nên bấm menu thủ công an toàn hơn trigger theo giờ.

Áp dụng cho trường học: đổi BangLuong thành bảng điểm (MaHS, HoTen, EmailPhuHuynh, các cột điểm), phiếu mẫu thành "Phiếu báo kết quả học tập", tiêu đề email thành "Kết quả học tập học kỳ I". Phần code không cần đổi ngoài tên cột trong indexOf và nội dung email.

Lỗi thường gặp

Lỗi dạng "Unexpected error while getting the method or property getFolderById". Nguyên nhân: FOLDER_ID sai hoặc còn chữ mẫu DAN_ID_THU_MUC_VAO_DAY. Cách sửa: copy lại đúng ID sau /folders/.

PDF của người sau mang số liệu người trước. Nguyên nhân: thiếu SpreadsheetApp.flush() hoặc phiếu dùng công thức tham chiếu chậm. Cách sửa: giữ dòng flush(), tăng Utilities.sleep lên nếu phiếu có nhiều công thức nặng.

Cột TrangThai ghi "Lỗi: Không xuất được PDF sau 3 lần thử". Nguyên nhân: Google tạm chặn vì xuất quá nhanh, hoặc VUNG_IN sai cú pháp. Cách sửa: chờ vài phút, xóa ô lỗi rồi chạy lại; kiểm tra vùng in dạng A1:F25.

Phiếu hiện #N/A. Nguyên nhân: mã trong B3 không tìm thấy trong BangLuong (thừa dấu cách, khác ký tự). Cách sửa: dùng TRIM khi nhập mã hoặc chuẩn hóa cột MaNV.

Email không đi, báo "Service invoked too many times for one day: email". Nguyên nhân: hết hạn mức gửi trong ngày. Cách sửa: chờ hôm sau chạy tiếp (script tự bỏ qua người đã gửi), hoặc dùng tài khoản Google Workspace.

Menu "Phiếu lương" không hiện. Nguyên nhân: chưa lưu mã hoặc chưa tải lại trang. Cách sửa: bấm Save trong Apps Script, tải lại Sheets; nếu vẫn không có, chạy thử hàm onOpen một lần từ trình soạn thảo.

Muốn gửi lại cho một người. Xóa ô TrangThai của dòng đó rồi chạy lại.

Bảng tóm tắt

Bước Việc cần làm Ghi chú
1 Sheet dữ liệu Tiêu đề không dấu, MaNV duy nhất, có cột TrangThai
2 Sheet mẫu Mọi ô lấy dữ liệu bằng VLOOKUP theo ô B3
3 Thư mục Drive Lấy ID, không chia sẻ
4 Dán mã Sửa FOLDER_ID, KY_LUONG, VUNG_IN
5 Chạy thử CHAY_THU: true, kiểm tra 3 PDF gửi về mình
6 Gửi thật Xóa cột TrangThai, CHAY_THU: false, chạy lại
— Danh sách dài Script dừng ở 5 phút, bấm chạy tiếp

Khi nào nên nhờ viết công cụ riêng?

Đoạn mã trên đủ dùng cho một phiếu, một danh sách. Bạn nên cân nhắc nhờ người viết công cụ riêng khi:

  • Phiếu có nhiều mẫu khác nhau theo phòng ban, loại hợp đồng, hoặc cần nhiều trang.
  • Cần lấy dữ liệu từ nhiều file, phần mềm chấm công, hoặc file Excel gửi lên.
  • Cần phân quyền: mỗi trưởng bộ phận chỉ xem và gửi phiếu cho người của mình.
  • Cần gửi qua kênh khác email, lưu lịch sử gửi, báo cáo ai đã mở.
  • Không có người trong đơn vị đủ thời gian sửa khi mẫu hoặc dữ liệu thay đổi.

Trước khi thuê, hãy chuẩn bị sẵn: file dữ liệu mẫu (đã che thông tin thật), mẫu phiếu mong muốn, số lượng phiếu mỗi kỳ, và ai sẽ là người bấm chạy. Có những thứ này, việc trao đổi và báo giá sẽ rõ ràng hơn.

Bài viết liên quan

Cần công cụ tạo và gửi phiếu tự động cho đơn vị?

Điền Nguyễn AZ nhận viết công cụ VBA và Google Apps Script theo quy trình thật của văn phòng, trường học, cửa hàng: tạo phiếu lương, phiếu báo điểm, hợp đồng, báo cáo PDF hàng loạt, gửi email tự động. Bạn gửi mô tả công việc và file mẫu; chúng tôi trao đổi để chọn cách làm phù hợp trước khi báo giá. Nếu muốn tự làm, khóa học Google Sheets có học trực tiếp tại TP Vinh, Hưng Nguyên, Đô Lương hoặc online qua Zoom.

📞 Hotline / Zalo: 0362 465 361

👉 Xem chi tiết: Dịch vụ viết VBA / Apps Script · hoặc gửi yêu cầu tư vấn

#tạo pdf hàng loạt google sheets#apps script xuất pdf#gửi phiếu lương qua email#tự động hóa google sheets

Tài liệu miễn phí

30 phím tắt Excel hay dùng nhất

Tổng hợp phím tắt giúp thao tác Excel nhanh hơn — điền thông tin để tải file PDF miễn phí về dùng ngay.