Hướng dẫn

Tự Động Gửi Email Nhắc Nhở Thanh Toán Từ Google Sheets [Apps Script]

Tuân HoangTuân Hoang
27 tháng 2, 2026
Cập nhật: 25 tháng 3, 2026
31 phút đọc
Tự Động Gửi Email Nhắc Nhở Thanh Toán Từ Google Sheets [Apps Script]

Tại Sao Cần Tự Động Nhắc Nhở Thanh Toán?

Quản lý công nợ là một trong những bài toán đau đầu nhất của mọi doanh nghiệp. Theo thống kê, trung bình 30-40% hóa đơn bị thanh toán trễ hạn, không phải vì khách hàng không muốn trả, mà đơn giản vì họ... quên. Một hệ thống nhắc nhở tự động có thể giúp bạn tăng tỷ lệ thu hồi công nợ lên 30-50% mà không cần gọi điện hay gửi email thủ công.

Lợi ích của hệ thống nhắc thanh toán tự động

  • Tiết kiệm 5-10 giờ/tuần - Không cần kiểm tra từng hóa đơn và gửi email thủ công
  • Tăng tỷ lệ thu hồi 30-50% - Nhắc đúng thời điểm, đúng tone giọng
  • Không bỏ sót hóa đơn nào - Script quét toàn bộ danh sách mỗi ngày
  • Chuyên nghiệp hơn - Email HTML đẹp, có logo, thông tin hóa đơn rõ ràng
  • Theo dõi lịch sử - Ghi log mọi email đã gửi, biết khách nhận bao nhiêu lần nhắc
  • Hoàn toàn miễn phí - Chỉ cần Google Sheets + Apps Script (Gmail miễn phí 100 email/ngày)

Bài viết này dành cho ai?

Kế toán viên theo dõi công nợ khách hàng

Chủ doanh nghiệp nhỏ (SME) muốn tự động hóa thu hồi

Freelancer cần nhắc khách trả tiền dự án

Quản lý tài chính muốn giảm nợ xấu

Bước 1: Chuẩn Bị Google Sheets Theo Dõi Công Nợ

Trước khi viết script, bạn cần thiết lập bảng tính với cấu trúc chuẩn. Tạo một Google Sheets mới với 2 sheet: "Công nợ" (dữ liệu chính) và "Log" (lịch sử gửi email).

Sheet "Công nợ" - Cấu trúc bảng

Cột Tên cột Kiểu dữ liệu Ví dụ Ghi chú
A Mã KH Text KH001 Mã khách hàng duy nhất
B Tên khách hàng Text Công ty ABC Tên đầy đủ
C Email Email ketoan@abc.vn Email người phụ trách
D Số hóa đơn Text HD-2026-001 Mã hóa đơn
E Số tiền (VNĐ) Number 15,000,000 Format: #,##0
F Ngày đáo hạn Date 15/03/2026 Format: dd/mm/yyyy
G Trạng thái Text Chưa thanh toán Dropdown: Chưa TT / Đã TT / Quá hạn
H Lần nhắc cuối Date 12/03/2026 Script tự điền
I Số lần nhắc Number 2 Script tự đếm
J Ghi chú Text Đã hẹn trả 20/03 Ghi chú thêm

Sheet "Log" - Lịch sử gửi email

Cột Tên cột Mô tả
A Thời gian gửi Timestamp khi gửi email
B Mã KH Mã khách hàng
C Email Email đã gửi đến
D Số hóa đơn Hóa đơn liên quan
E Mức nhắc Trước hạn / Đúng hạn / Quá hạn
F Trạng thái gửi Thành công / Lỗi

Mẹo thiết lập nhanh:

  • Dùng Data Validation cho cột "Trạng thái" (G) để tạo dropdown: Chưa thanh toán, Đã thanh toán, Quá hạn
  • Dùng Conditional Formatting để tô màu đỏ cho hóa đơn quá hạn, vàng cho sắp đến hạn
  • Freeze hàng đầu tiên (View > Freeze > 1 row) để dễ cuộn

Bước 2: Script Cơ Bản - Gửi Email Text Đơn Giản

Chúng ta sẽ bắt đầu với phiên bản đơn giản nhất: quét danh sách công nợ, tìm hóa đơn quá hạn, và gửi email nhắc nhở dạng text thuần. Sau đó sẽ nâng cấp dần.

Mở Apps Script Editor

  1. 1. Mở Google Sheets chứa bảng "Công nợ"
  2. 2. Vào menu Extensions (Tiện ích mở rộng) > Apps Script
  3. 3. Xóa nội dung mặc định trong Code.gs
  4. 4. Dán code bên dưới vào
/**
 * Script cơ bản: Gửi email nhắc thanh toán cho hóa đơn quá hạn
 * Sheet: "Công nợ" - Cột A:J
 */
function sendBasicReminders() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Công nợ");
  var data = sheet.getDataRange().getValues();
  var today = new Date();
  today.setHours(0, 0, 0, 0); // Reset về đầu ngày

  var sentCount = 0;
  var errorCount = 0;

  // Bắt đầu từ dòng 2 (bỏ header)
  for (var i = 1; i < data.length; i++) {
    var maKH = data[i][0];        // Cột A: Mã KH
    var tenKH = data[i][1];       // Cột B: Tên khách hàng
    var email = data[i][2];       // Cột C: Email
    var soHD = data[i][3];        // Cột D: Số hóa đơn
    var soTien = data[i][4];      // Cột E: Số tiền
    var ngayDaoHan = new Date(data[i][5]); // Cột F: Ngày đáo hạn
    var trangThai = data[i][6];   // Cột G: Trạng thái

    // Bỏ qua nếu đã thanh toán hoặc không có email
    if (trangThai === "Đã thanh toán" || !email) continue;

    // Tính số ngày chênh lệch
    ngayDaoHan.setHours(0, 0, 0, 0);
    var diffDays = Math.floor((ngayDaoHan - today) / (1000 * 60 * 60 * 24));

    // Chỉ gửi nếu quá hạn (diffDays < 0)
    if (diffDays < 0) {
      try {
        var subject = "Nhắc nhở thanh toán hóa đơn " + soHD;
        var body = "Kính gửi " + tenKH + ",\n\n" +
          "Chúng tôi xin nhắc nhở về hóa đơn " + soHD +
          " với số tiền " + formatCurrency(soTien) +
          " đã quá hạn thanh toán " + Math.abs(diffDays) + " ngày.\n\n" +
          "Vui lòng thanh toán sớm nhất có thể.\n\n" +
          "Trân trọng,\n" +
          "Phòng Kế toán";

        GmailApp.sendEmail(email, subject, body);

        // Cập nhật lần nhắc cuối và số lần nhắc
        sheet.getRange(i + 1, 8).setValue(new Date()); // Cột H
        var soLanNhac = data[i][8] || 0;
        sheet.getRange(i + 1, 9).setValue(soLanNhac + 1); // Cột I

        sentCount++;
      } catch (e) {
        Logger.log("Lỗi gửi email cho " + tenKH + ": " + e.message);
        errorCount++;
      }
    }
  }

  Logger.log("Đã gửi: " + sentCount + " email. Lỗi: " + errorCount);
}

// Hàm format số tiền VNĐ
function formatCurrency(amount) {
  return new Intl.NumberFormat('vi-VN').format(amount) + " VNĐ";
}

Lưu ý quan trọng:

  • Lần đầu chạy, Google sẽ yêu cầu bạn cấp quyền (Authorization). Click "Review Permissions" > chọn tài khoản > "Allow"
  • Script này chỉ gửi email cho hóa đơn đã quá hạn - phiên bản nâng cao sẽ gửi cả nhắc trước hạn
  • Mỗi lần chạy, cột H và I sẽ được cập nhật tự động

Bước 3: Script Nâng Cao - 3 Mức Nhắc Nhở Thông Minh

Phiên bản nâng cao sẽ chia thành 3 mức nhắc nhở với tone giọng và nội dung khác nhau, phù hợp với từng tình huống. Đây là cách các công ty lớn xử lý thu hồi công nợ:

1

Nhắc trước 3 ngày

Tone: Nhẹ nhàng, thân thiện

"Xin nhắc nhở hóa đơn sắp đến hạn thanh toán trong 3 ngày tới..."

2

Nhắc đúng ngày

Tone: Chuyên nghiệp, rõ ràng

"Hóa đơn đến hạn thanh toán hôm nay. Vui lòng xác nhận..."

3

Nhắc quá hạn

Tone: Nghiêm túc + CC manager

"Hóa đơn đã quá hạn X ngày. Đề nghị thanh toán ngay để tránh ảnh hưởng..."

/**
 * Script nâng cao: 3 mức nhắc nhở thanh toán
 * - Mức 1: Trước hạn 3 ngày (nhẹ nhàng)
 * - Mức 2: Đúng ngày đáo hạn (chuyên nghiệp)
 * - Mức 3: Quá hạn (nghiêm túc + CC manager)
 */

// ===== CẤU HÌNH =====
var CONFIG = {
  SHEET_NAME: "Công nợ",
  LOG_SHEET: "Log",
  COMPANY_NAME: "Công Ty TNHH ABC",
  COMPANY_PHONE: "028 1234 5678",
  COMPANY_EMAIL: "ketoan@congtyabc.vn",
  MANAGER_EMAIL: "manager@congtyabc.vn",  // CC khi quá hạn
  DAYS_BEFORE: 3,      // Nhắc trước bao nhiêu ngày
  DAYS_OVERDUE_CC: 0,  // Quá hạn bao nhiêu ngày thì CC manager
  BANK_NAME: "Vietcombank",
  BANK_ACCOUNT: "001 100 1234567",
  BANK_HOLDER: "CONG TY TNHH ABC"
};

function sendAdvancedReminders() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
  var logSheet = ss.getSheetByName(CONFIG.LOG_SHEET);

  // Tạo sheet Log nếu chưa có
  if (!logSheet) {
    logSheet = ss.insertSheet(CONFIG.LOG_SHEET);
    logSheet.appendRow([
      "Thời gian gửi", "Mã KH", "Email",
      "Số hóa đơn", "Mức nhắc", "Trạng thái gửi"
    ]);
    // Format header
    logSheet.getRange(1, 1, 1, 6).setFontWeight("bold")
      .setBackground("#E8EAF6");
  }

  var data = sheet.getDataRange().getValues();
  var today = new Date();
  today.setHours(0, 0, 0, 0);

  var stats = { sent: 0, skipped: 0, errors: 0 };

  for (var i = 1; i < data.length; i++) {
    var row = {
      maKH: data[i][0],
      tenKH: data[i][1],
      email: data[i][2],
      soHD: data[i][3],
      soTien: data[i][4],
      ngayDaoHan: new Date(data[i][5]),
      trangThai: data[i][6],
      lanNhacCuoi: data[i][7],
      soLanNhac: data[i][8] || 0
    };

    // Bỏ qua: đã thanh toán, không có email, hoặc thiếu dữ liệu
    if (row.trangThai === "Đã thanh toán" || !row.email || !row.soHD) {
      stats.skipped++;
      continue;
    }

    // Tránh gửi trùng: nếu đã nhắc hôm nay thì bỏ qua
    if (row.lanNhacCuoi) {
      var lastRemind = new Date(row.lanNhacCuoi);
      lastRemind.setHours(0, 0, 0, 0);
      if (lastRemind.getTime() === today.getTime()) {
        stats.skipped++;
        continue;
      }
    }

    row.ngayDaoHan.setHours(0, 0, 0, 0);
    var diffDays = Math.floor(
      (row.ngayDaoHan - today) / (1000 * 60 * 60 * 24)
    );

    var reminderLevel = getReminderLevel(diffDays);
    if (!reminderLevel) {
      stats.skipped++;
      continue;
    }

    // Gửi email
    var result = sendReminderEmail(row, reminderLevel, diffDays);

    // Ghi log
    logSheet.appendRow([
      new Date(),
      row.maKH,
      row.email,
      row.soHD,
      reminderLevel.name,
      result.success ? "Thành công" : "Lỗi: " + result.error
    ]);

    // Cập nhật sheet chính
    if (result.success) {
      sheet.getRange(i + 1, 8).setValue(new Date());
      sheet.getRange(i + 1, 9).setValue(row.soLanNhac + 1);
      if (diffDays < 0) {
        sheet.getRange(i + 1, 7).setValue("Quá hạn");
      }
      stats.sent++;
    } else {
      stats.errors++;
    }
  }

  // Log tổng kết
  Logger.log("=== KẾT QUẢ GỬI EMAIL ===");
  Logger.log("Đã gửi: " + stats.sent);
  Logger.log("Bỏ qua: " + stats.skipped);
  Logger.log("Lỗi: " + stats.errors);
}

/**
 * Xác định mức nhắc nhở dựa trên số ngày
 */
function getReminderLevel(diffDays) {
  if (diffDays > 0 && diffDays <= CONFIG.DAYS_BEFORE) {
    return {
      level: 1,
      name: "Trước hạn",
      color: "#059669",       // Xanh lá
      bgColor: "#ECFDF5",
      subject: "Nhắc nhở: Hóa đơn sắp đến hạn thanh toán",
      tone: "friendly"
    };
  } else if (diffDays === 0) {
    return {
      level: 2,
      name: "Đúng hạn",
      color: "#2563EB",       // Xanh dương
      bgColor: "#EFF6FF",
      subject: "Hóa đơn đến hạn thanh toán hôm nay",
      tone: "professional"
    };
  } else if (diffDays < 0) {
    return {
      level: 3,
      name: "Quá hạn",
      color: "#DC2626",       // Đỏ
      bgColor: "#FEF2F2",
      subject: "KHẨN: Hóa đơn quá hạn thanh toán",
      tone: "serious"
    };
  }
  return null; // Chưa đến thời điểm nhắc
}

/**
 * Gửi email nhắc nhở với HTML template
 */
function sendReminderEmail(row, level, diffDays) {
  try {
    var htmlBody = buildEmailHTML(row, level, diffDays);
    var options = {
      htmlBody: htmlBody,
      name: CONFIG.COMPANY_NAME + " - Phòng Kế toán"
    };

    // CC manager nếu quá hạn
    if (level.level === 3 && Math.abs(diffDays) >= CONFIG.DAYS_OVERDUE_CC) {
      options.cc = CONFIG.MANAGER_EMAIL;
    }

    GmailApp.sendEmail(
      row.email,
      level.subject + " - " + row.soHD,
      "Vui lòng xem email này trên trình duyệt hỗ trợ HTML.",
      options
    );

    return { success: true };
  } catch (e) {
    Logger.log("Lỗi gửi email " + row.email + ": " + e.message);
    return { success: false, error: e.message };
  }
}

Giải thích logic 3 mức nhắc:

  • diffDays > 0 && <= 3: Hóa đơn còn 1-3 ngày nữa mới đến hạn → Nhắc nhẹ nhàng
  • diffDays === 0: Đúng ngày đáo hạn → Nhắc chuyên nghiệp
  • diffDays < 0: Đã quá hạn → Nhắc nghiêm túc, CC cho quản lý
  • Nếu diffDays > 3: Chưa cần nhắc → return null (bỏ qua)

Bước 4: Template Email HTML Chuyên Nghiệp

Email text thuần trông rất thiếu chuyên nghiệp. Chúng ta sẽ tạo 3 template HTML tương ứng với 3 mức nhắc, với thiết kế đẹp mắt và đầy đủ thông tin thanh toán.

Hàm tạo email HTML

/**
 * Tạo nội dung email HTML dựa trên mức nhắc nhở
 */
function buildEmailHTML(row, level, diffDays) {
  var dueDate = Utilities.formatDate(
    row.ngayDaoHan, "Asia/Ho_Chi_Minh", "dd/MM/yyyy"
  );
  var amount = formatCurrency(row.soTien);
  var greeting = getGreeting(level, row.tenKH);
  var message = getMessage(level, diffDays, row.soHD, amount, dueDate);
  var footer = getFooterMessage(level);

  var html = '<!DOCTYPE html>' +
    '<html><head><meta charset="utf-8"></head>' +
    '<body style="margin:0;padding:0;font-family:Arial,sans-serif;' +
    'background-color:#f5f5f5;">' +
    '<table width="100%" cellpadding="0" cellspacing="0" ' +
    'style="max-width:600px;margin:20px auto;background:#fff;' +
    'border-radius:12px;overflow:hidden;' +
    'box-shadow:0 2px 8px rgba(0,0,0,0.1);">' +

    // Header với màu theo mức nhắc
    '<tr><td style="background:' + level.color + ';' +
    'padding:24px 30px;text-align:center;">' +
    '<h1 style="color:#fff;margin:0;font-size:20px;">' +
    CONFIG.COMPANY_NAME + '</h1>' +
    '<p style="color:rgba(255,255,255,0.85);margin:5px 0 0;' +
    'font-size:14px;">Thông Báo Thanh Toán</p>' +
    '</td></tr>' +

    // Nội dung chính
    '<tr><td style="padding:30px;">' +
    greeting + message +

    // Bảng thông tin hóa đơn
    '<table width="100%" cellpadding="0" cellspacing="0" ' +
    'style="margin:20px 0;border:1px solid #e5e7eb;' +
    'border-radius:8px;overflow:hidden;">' +
    '<tr style="background:' + level.bgColor + ';">' +
    '<td colspan="2" style="padding:12px 16px;font-weight:bold;' +
    'color:' + level.color + ';font-size:15px;">' +
    'Chi Tiết Hóa Đơn</td></tr>' +

    buildInfoRow("Số hóa đơn", row.soHD) +
    buildInfoRow("Khách hàng", row.tenKH) +
    buildInfoRow("Số tiền", '<strong style="color:' +
      level.color + ';font-size:16px;">' + amount + '</strong>') +
    buildInfoRow("Ngày đáo hạn", dueDate) +
    buildInfoRow("Trạng thái",
      diffDays < 0
        ? '<span style="color:#DC2626;font-weight:bold;">' +
          'Quá hạn ' + Math.abs(diffDays) + ' ngày</span>'
        : diffDays === 0
          ? '<span style="color:#2563EB;font-weight:bold;">' +
            'Đến hạn hôm nay</span>'
          : '<span style="color:#059669;font-weight:bold;">' +
            'Còn ' + diffDays + ' ngày</span>'
    ) +
    '</table>' +

    // Thông tin chuyển khoản
    '<div style="background:#F0FDF4;border:1px solid #BBF7D0;' +
    'border-radius:8px;padding:16px;margin:20px 0;">' +
    '<p style="font-weight:bold;color:#166534;margin:0 0 10px;' +
    'font-size:14px;">Thông Tin Chuyển Khoản:</p>' +
    '<p style="margin:4px 0;color:#333;font-size:13px;">' +
    'Ngân hàng: <strong>' + CONFIG.BANK_NAME + '</strong></p>' +
    '<p style="margin:4px 0;color:#333;font-size:13px;">' +
    'Số TK: <strong>' + CONFIG.BANK_ACCOUNT + '</strong></p>' +
    '<p style="margin:4px 0;color:#333;font-size:13px;">' +
    'Chủ TK: <strong>' + CONFIG.BANK_HOLDER + '</strong></p>' +
    '<p style="margin:4px 0;color:#333;font-size:13px;">' +
    'Nội dung CK: <strong>' + row.soHD + ' - ' +
    row.maKH + '</strong></p>' +
    '</div>' +

    footer +
    '</td></tr>' +

    // Footer
    '<tr><td style="background:#f9fafb;padding:20px 30px;' +
    'text-align:center;border-top:1px solid #e5e7eb;">' +
    '<p style="margin:0;color:#6b7280;font-size:12px;">' +
    CONFIG.COMPANY_NAME + ' | ' + CONFIG.COMPANY_PHONE +
    ' | ' + CONFIG.COMPANY_EMAIL + '</p>' +
    '<p style="margin:5px 0 0;color:#9ca3af;font-size:11px;">' +
    'Email này được gửi tự động. Nếu đã thanh toán, ' +
    'vui lòng bỏ qua.</p>' +
    '</td></tr>' +

    '</table></body></html>';

  return html;
}

function buildInfoRow(label, value) {
  return '<tr><td style="padding:10px 16px;border-bottom:' +
    '1px solid #f3f4f6;color:#6b7280;width:40%;font-size:13px;">' +
    label + '</td>' +
    '<td style="padding:10px 16px;border-bottom:1px solid #f3f4f6;' +
    'color:#111827;font-size:13px;">' + value + '</td></tr>';
}

function formatCurrency(amount) {
  return new Intl.NumberFormat('vi-VN').format(amount) + " VNĐ";
}

3 bộ nội dung theo mức nhắc

/**
 * Lời chào phù hợp với tone giọng
 */
function getGreeting(level, tenKH) {
  switch (level.level) {
    case 1: // Trước hạn - nhẹ nhàng
      return '<p style="color:#333;font-size:15px;line-height:1.6;">' +
        'Kính gửi <strong>' + tenKH + '</strong>,</p>' +
        '<p style="color:#555;font-size:14px;line-height:1.6;">' +
        'Cảm ơn Quý khách đã tin tưởng sử dụng dịch vụ của chúng tôi. ' +
        'Chúng tôi xin gửi lời nhắc nhở nhẹ nhàng về hóa đơn sắp ' +
        'đến hạn thanh toán:</p>';

    case 2: // Đúng hạn - chuyên nghiệp
      return '<p style="color:#333;font-size:15px;line-height:1.6;">' +
        'Kính gửi <strong>' + tenKH + '</strong>,</p>' +
        '<p style="color:#555;font-size:14px;line-height:1.6;">' +
        'Chúng tôi trân trọng thông báo rằng hóa đơn dưới đây ' +
        '<strong>đến hạn thanh toán ngày hôm nay</strong>. ' +
        'Kính mong Quý khách sắp xếp thanh toán để đảm bảo ' +
        'tiến độ hợp tác:</p>';

    case 3: // Quá hạn - nghiêm túc
      return '<p style="color:#333;font-size:15px;line-height:1.6;">' +
        'Kính gửi <strong>' + tenKH + '</strong>,</p>' +
        '<p style="color:#555;font-size:14px;line-height:1.6;">' +
        'Chúng tôi nhận thấy hóa đơn dưới đây ' +
        '<strong style="color:#DC2626;">đã quá hạn thanh toán</strong>. ' +
        'Đề nghị Quý khách vui lòng thanh toán trong thời gian ' +
        'sớm nhất để tránh ảnh hưởng đến việc hợp tác:</p>';
  }
}

/**
 * Nội dung bổ sung theo mức nhắc
 */
function getMessage(level, diffDays, soHD, amount, dueDate) {
  if (level.level === 3) {
    return '<div style="background:#FEF2F2;border-left:4px solid #DC2626;' +
      'padding:12px 16px;margin:15px 0;border-radius:0 8px 8px 0;">' +
      '<p style="color:#991B1B;margin:0;font-size:14px;">' +
      '<strong>Hóa đơn ' + soHD + '</strong> trị giá ' + amount +
      ' đã quá hạn <strong>' + Math.abs(diffDays) +
      ' ngày</strong> (hạn thanh toán: ' + dueDate + ').</p></div>';
  }
  return '';
}

/**
 * Lời kết theo mức nhắc
 */
function getFooterMessage(level) {
  switch (level.level) {
    case 1:
      return '<p style="color:#555;font-size:14px;line-height:1.6;' +
        'margin-top:20px;">Nếu Quý khách đã thanh toán, xin vui lòng ' +
        'bỏ qua email này. Mọi thắc mắc vui lòng liên hệ phòng kế toán.' +
        '</p><p style="color:#333;font-size:14px;margin-top:15px;">' +
        'Trân trọng,<br><strong>Phòng Kế toán</strong><br>' +
        CONFIG.COMPANY_NAME + '</p>';

    case 2:
      return '<p style="color:#555;font-size:14px;line-height:1.6;' +
        'margin-top:20px;">Kính mong Quý khách xác nhận thanh toán ' +
        'trong ngày hôm nay hoặc liên hệ với chúng tôi nếu cần hỗ trợ.' +
        '</p><p style="color:#333;font-size:14px;margin-top:15px;">' +
        'Trân trọng,<br><strong>Phòng Kế toán</strong><br>' +
        CONFIG.COMPANY_NAME + '</p>';

    case 3:
      return '<div style="background:#FFFBEB;border:1px solid #FDE68A;' +
        'border-radius:8px;padding:12px 16px;margin:20px 0;">' +
        '<p style="color:#92400E;margin:0;font-size:13px;">' +
        '<strong>Lưu ý:</strong> Nếu không nhận được phản hồi trong ' +
        '7 ngày, chúng tôi sẽ tạm ngưng cung cấp dịch vụ theo điều khoản ' +
        'hợp đồng.</p></div>' +
        '<p style="color:#333;font-size:14px;margin-top:15px;">' +
        'Trân trọng,<br><strong>Phòng Kế toán</strong><br>' +
        CONFIG.COMPANY_NAME + '</p>';
  }
}

Preview email cho từng mức

Mức Subject Header color CC Manager
Trước hạn Nhắc nhở: Hóa đơn sắp đến hạn... Xanh lá (#059669) Không
Đúng hạn Hóa đơn đến hạn thanh toán hôm nay... Xanh dương (#2563EB) Không
Quá hạn KHẨN: Hóa đơn quá hạn thanh toán... Đỏ (#DC2626)

Bước 5: Trigger Tự Động Chạy Mỗi Ngày 9h Sáng

Thay vì chạy thủ công, bạn có thể đặt Time-driven trigger để script tự động quét và gửi email mỗi ngày vào lúc 9h sáng (giờ Việt Nam). Đây là thời điểm lý tưởng vì khách hàng thường kiểm tra email đầu giờ sáng.

Cách 1: Tạo trigger bằng code (khuyến nghị)

/**
 * Tạo trigger chạy tự động mỗi ngày lúc 9h sáng
 * CHỈ CẦN CHẠY HÀM NÀY 1 LẦN DUY NHẤT
 */
function createDailyTrigger() {
  // Xóa trigger cũ (nếu có) để tránh trùng lặp
  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    if (triggers[i].getHandlerFunction() === "sendAdvancedReminders") {
      ScriptApp.deleteTrigger(triggers[i]);
      Logger.log("Đã xóa trigger cũ");
    }
  }

  // Tạo trigger mới
  ScriptApp.newTrigger("sendAdvancedReminders")
    .timeBased()
    .everyDays(1)
    .atHour(9)           // 9h sáng
    .nearMinute(0)       // Khoảng phút 0 (có thể lệch 0-15 phút)
    .inTimezone("Asia/Ho_Chi_Minh")  // Múi giờ VN
    .create();

  Logger.log("Đã tạo trigger chạy mỗi ngày lúc 9:00 AM (VN)");
}

/**
 * Xem danh sách trigger đang hoạt động
 */
function listTriggers() {
  var triggers = ScriptApp.getProjectTriggers();
  if (triggers.length === 0) {
    Logger.log("Không có trigger nào.");
    return;
  }
  for (var i = 0; i < triggers.length; i++) {
    Logger.log(
      "Trigger " + (i + 1) + ": " +
      triggers[i].getHandlerFunction() + " - " +
      triggers[i].getTriggerSource()
    );
  }
}

/**
 * Xóa tất cả trigger
 */
function deleteAllTriggers() {
  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    ScriptApp.deleteTrigger(triggers[i]);
  }
  Logger.log("Đã xóa " + triggers.length + " trigger(s)");
}

Cách 2: Tạo trigger qua giao diện

  1. 1. Trong Apps Script Editor, click biểu tượng đồng hồ (Triggers) ở thanh bên trái
  2. 2. Click "+ Add Trigger" ở góc phải dưới
  3. 3. Cấu hình:
    • Choose which function to run: sendAdvancedReminders
    • Choose which deployment should run: Head
    • Select event source: Time-driven
    • Select type of time based trigger: Day timer
    • Select time of day: 9am to 10am
  4. 4. Click "Save"

Lưu ý về trigger:

  • Trigger có thể lệch 0-15 phút so với giờ đặt (đây là hạn chế của Google)
  • Không tạo nhiều trigger cho cùng 1 hàm - sẽ bị gửi email trùng
  • Nếu script bị lỗi, Google sẽ gửi email thông báo cho bạn
  • Trigger chạy kể cả khi bạn không mở Google Sheets

Bước 6: Ghi Log Lịch Sử Gửi Email

Trong script nâng cao ở Bước 3, chúng ta đã tích hợp sẵn việc ghi log vào sheet "Log". Bây giờ hãy nâng cấp thêm với hàm xem thống kêdọn dẹp log cũ.

/**
 * Xem thống kê email đã gửi
 */
function viewEmailStats() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var logSheet = ss.getSheetByName(CONFIG.LOG_SHEET);
  if (!logSheet) {
    Logger.log("Chưa có sheet Log. Chạy sendAdvancedReminders trước.");
    return;
  }

  var data = logSheet.getDataRange().getValues();
  var stats = {
    total: data.length - 1, // Trừ header
    success: 0,
    error: 0,
    byLevel: { "Trước hạn": 0, "Đúng hạn": 0, "Quá hạn": 0 },
    today: 0
  };

  var today = new Date();
  today.setHours(0, 0, 0, 0);

  for (var i = 1; i < data.length; i++) {
    var sendDate = new Date(data[i][0]);
    sendDate.setHours(0, 0, 0, 0);
    var level = data[i][4];
    var status = data[i][5];

    if (status === "Thành công") {
      stats.success++;
    } else {
      stats.error++;
    }

    if (stats.byLevel[level] !== undefined) {
      stats.byLevel[level]++;
    }

    if (sendDate.getTime() === today.getTime()) {
      stats.today++;
    }
  }

  Logger.log("=== THỐNG KÊ EMAIL NHẮC NHỞ ===");
  Logger.log("Tổng email đã gửi: " + stats.total);
  Logger.log("Thành công: " + stats.success);
  Logger.log("Lỗi: " + stats.error);
  Logger.log("Gửi hôm nay: " + stats.today);
  Logger.log("--- Theo mức nhắc ---");
  Logger.log("Trước hạn: " + stats.byLevel["Trước hạn"]);
  Logger.log("Đúng hạn: " + stats.byLevel["Đúng hạn"]);
  Logger.log("Quá hạn: " + stats.byLevel["Quá hạn"]);
}

/**
 * Xóa log cũ hơn 90 ngày (chạy định kỳ để giảm dung lượng)
 */
function cleanOldLogs() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var logSheet = ss.getSheetByName(CONFIG.LOG_SHEET);
  if (!logSheet) return;

  var data = logSheet.getDataRange().getValues();
  var cutoffDate = new Date();
  cutoffDate.setDate(cutoffDate.getDate() - 90); // 90 ngày trước

  var rowsToDelete = [];
  for (var i = data.length - 1; i >= 1; i--) {
    var logDate = new Date(data[i][0]);
    if (logDate < cutoffDate) {
      rowsToDelete.push(i + 1); // 1-indexed
    }
  }

  // Xóa từ dưới lên để không bị lệch index
  for (var j = 0; j < rowsToDelete.length; j++) {
    logSheet.deleteRow(rowsToDelete[j]);
  }

  Logger.log("Đã xóa " + rowsToDelete.length + " dòng log cũ hơn 90 ngày");
}

Ví dụ dữ liệu trong sheet Log

Thời gian Mã KH Email Hóa đơn Mức Trạng thái
15/02/2026 09:02 KH001 a@abc.vn HD-001 Trước hạn Thành công
15/02/2026 09:02 KH005 b@xyz.vn HD-012 Quá hạn Thành công
15/02/2026 09:03 KH008 invalid-email HD-020 Đúng hạn Lỗi: Invalid email

Bước 7: Tùy Chỉnh Nâng Cao

Bổ sung thêm các tính năng tiện ích: menu tùy chỉnh trong Google Sheets để gửi thủ công, và logic thông minh hơn để xử lý các trường hợp đặc biệt.

Thêm menu tùy chỉnh vào Google Sheets

/**
 * Tạo menu tùy chỉnh khi mở Spreadsheet
 */
function onOpen() {
  var ui = SpreadsheetApp.getUi();
  ui.createMenu("Nhắc Thanh Toán")
    .addItem("Gửi nhắc nhở tự động", "sendAdvancedReminders")
    .addItem("Gửi nhắc cho dòng đang chọn", "sendManualReminder")
    .addSeparator()
    .addItem("Xem thống kê", "viewEmailStats")
    .addItem("Dọn dẹp log cũ (90 ngày)", "cleanOldLogs")
    .addSeparator()
    .addItem("Tạo trigger hàng ngày 9h", "createDailyTrigger")
    .addItem("Xem trigger hiện tại", "listTriggers")
    .addItem("Xóa tất cả trigger", "deleteAllTriggers")
    .addToUi();
}

/**
 * Gửi email nhắc nhở cho dòng đang chọn (gửi thủ công)
 */
function sendManualReminder() {
  var ui = SpreadsheetApp.getUi();
  var sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName(CONFIG.SHEET_NAME);
  var activeRange = sheet.getActiveRange();

  if (!activeRange) {
    ui.alert("Vui lòng chọn 1 dòng trong sheet Công nợ.");
    return;
  }

  var row = activeRange.getRow();
  if (row <= 1) {
    ui.alert("Không thể gửi cho dòng header. Chọn dòng dữ liệu.");
    return;
  }

  var data = sheet.getRange(row, 1, 1, 10).getValues()[0];
  var rowData = {
    maKH: data[0],
    tenKH: data[1],
    email: data[2],
    soHD: data[3],
    soTien: data[4],
    ngayDaoHan: new Date(data[5]),
    trangThai: data[6],
    soLanNhac: data[8] || 0
  };

  // Kiểm tra dữ liệu
  if (!rowData.email) {
    ui.alert("Dòng này không có email.");
    return;
  }
  if (rowData.trangThai === "Đã thanh toán") {
    var confirm = ui.alert(
      "Xác nhận",
      "Hóa đơn này đã được đánh dấu 'Đã thanh toán'. " +
      "Vẫn muốn gửi email?",
      ui.ButtonSet.YES_NO
    );
    if (confirm !== ui.Button.YES) return;
  }

  // Tính mức nhắc
  var today = new Date();
  today.setHours(0, 0, 0, 0);
  rowData.ngayDaoHan.setHours(0, 0, 0, 0);
  var diffDays = Math.floor(
    (rowData.ngayDaoHan - today) / (1000 * 60 * 60 * 24)
  );

  // Cho phép chọn mức nhắc khi gửi thủ công
  var level = getReminderLevel(diffDays);
  if (!level) {
    // Nếu chưa đến thời điểm nhắc, dùng mức "Trước hạn"
    level = {
      level: 1, name: "Trước hạn", color: "#059669",
      bgColor: "#ECFDF5",
      subject: "Nhắc nhở: Hóa đơn sắp đến hạn thanh toán",
      tone: "friendly"
    };
  }

  var result = sendReminderEmail(rowData, level, diffDays);

  if (result.success) {
    // Cập nhật sheet
    sheet.getRange(row, 8).setValue(new Date());
    sheet.getRange(row, 9).setValue(rowData.soLanNhac + 1);

    // Ghi log
    var logSheet = SpreadsheetApp.getActiveSpreadsheet()
      .getSheetByName(CONFIG.LOG_SHEET);
    if (logSheet) {
      logSheet.appendRow([
        new Date(), rowData.maKH, rowData.email,
        rowData.soHD, level.name + " (Thủ công)", "Thành công"
      ]);
    }

    ui.alert(
      "Thành công!",
      "Đã gửi email nhắc nhở đến " + rowData.email +
      "\nMức nhắc: " + level.name,
      ui.ButtonSet.OK
    );
  } else {
    ui.alert("Lỗi", "Không thể gửi email: " + result.error, ui.ButtonSet.OK);
  }
}

Cách sử dụng menu tùy chỉnh

  1. 1. Sau khi lưu code, reload lại Google Sheets (F5 hoặc Ctrl+R)
  2. 2. Bạn sẽ thấy menu mới "Nhắc Thanh Toán" xuất hiện trên thanh menu
  3. 3. Các tùy chọn:
    • Gửi nhắc nhở tự động: Quét toàn bộ danh sách và gửi cho các hóa đơn cần nhắc
    • Gửi nhắc cho dòng đang chọn: Click vào dòng cần gửi → chọn option này
    • Xem thống kê: Hiển thị tổng kết trong Logs (View > Execution log)
    • Tạo trigger / Xóa trigger: Quản lý chạy tự động

Logic thông minh: Tránh gửi spam

Để tránh gửi quá nhiều email cho cùng 1 khách hàng (gây phản cảm), thêm các điều kiện lọc thông minh:

/**
 * Kiểm tra xem có nên gửi email cho dòng này không
 * Quy tắc:
 * - Không gửi nếu đã nhắc hôm nay
 * - Mức 1 (trước hạn): Chỉ gửi 1 lần duy nhất
 * - Mức 2 (đúng hạn): Chỉ gửi 1 lần duy nhất
 * - Mức 3 (quá hạn): Gửi lại mỗi 3 ngày, tối đa 5 lần
 */
function shouldSendReminder(row, level, diffDays) {
  var today = new Date();
  today.setHours(0, 0, 0, 0);

  // Đã nhắc hôm nay rồi?
  if (row.lanNhacCuoi) {
    var lastRemind = new Date(row.lanNhacCuoi);
    lastRemind.setHours(0, 0, 0, 0);
    if (lastRemind.getTime() === today.getTime()) {
      return false; // Đã nhắc hôm nay
    }

    // Mức 3: Chỉ nhắc lại sau 3 ngày
    if (level.level === 3) {
      var daysSinceLastRemind = Math.floor(
        (today - lastRemind) / (1000 * 60 * 60 * 24)
      );
      if (daysSinceLastRemind < 3) return false;
      if (row.soLanNhac >= 5) return false; // Tối đa 5 lần
    }

    // Mức 1 & 2: Đã nhắc rồi thì không nhắc lại
    if (level.level <= 2 && row.soLanNhac > 0) {
      return false;
    }
  }

  return true;
}

Mẹo hay:

Thêm hàm shouldSendReminder() vào trong vòng lặp chính của sendAdvancedReminders(), ngay sau khi tính được reminderLevel. Nếu return false thì skip và tăng stats.skipped.

Bước 8: Mẫu Email HTML Hoàn Chỉnh (Copy-Paste)

Dưới đây là bản tổng hợp toàn bộ code hoàn chỉnh mà bạn có thể copy và paste trực tiếp vào Apps Script Editor. Code này bao gồm tất cả các tính năng đã hướng dẫn ở trên.

Checklist trước khi sử dụng:

  • Thay thông tin CONFIG (tên công ty, SĐT, email, tài khoản ngân hàng)

  • Tạo sheet "Công nợ" với đúng cấu trúc cột A-J như bảng ở Bước 1

  • Nhập dữ liệu test (2-3 dòng) với ngày đáo hạn khác nhau

  • Chạy thử sendAdvancedReminders() và kiểm tra email nhận được

  • Kiểm tra sheet "Log" đã ghi đúng lịch sử

  • Chạy createDailyTrigger() để bật tự động hóa

Cách chạy test nhanh

  1. 1. Nhập 3 dòng dữ liệu test trong sheet "Công nợ":
    • Dòng 1: Ngày đáo hạn = ngày mai (sẽ nhận email "Trước hạn")
    • Dòng 2: Ngày đáo hạn = hôm nay (sẽ nhận email "Đúng hạn")
    • Dòng 3: Ngày đáo hạn = 5 ngày trước (sẽ nhận email "Quá hạn")
  2. 2. Dùng email cá nhân của bạn ở cột C để tự kiểm tra
  3. 3. Trong Apps Script, chọn hàm sendAdvancedReminders > click Run
  4. 4. Kiểm tra hộp mail - bạn sẽ nhận được 3 email với 3 thiết kế khác nhau
  5. 5. Kiểm tra sheet "Log" - phải có 3 dòng log mới

Giới Hạn Gmail Và Cách Xử Lý

Google có giới hạn về số lượng email bạn có thể gửi qua Apps Script. Biết rõ giới hạn này giúp bạn lập kế hoạch phù hợp và tránh bị block.

Loại tài khoản Giới hạn email/ngày Giới hạn người nhận/email Phù hợp cho
Gmail miễn phí (@gmail.com) 100 email/ngày 50 người Freelancer, cá nhân
Google Workspace (doanh nghiệp) 1,500 email/ngày 500 người SME, công ty

Cảnh báo quan trọng:

  • Nếu vượt quá giới hạn, Google sẽ block gửi email trong 24 giờ
  • Giới hạn tính theo ngày lịch (calendar day), reset vào 12:00 AM theo timezone Pacific Time
  • Mỗi người nhận CC/BCC cũng tính là 1 email
  • Email qua GmailApp.sendEmail()MailApp.sendEmail() tính chung quota

Cách xử lý khi có nhiều hóa đơn

/**
 * Kiểm tra quota email còn lại trước khi gửi
 */
function checkEmailQuota() {
  var remaining = MailApp.getRemainingDailyQuota();
  Logger.log("Email quota còn lại hôm nay: " + remaining);
  return remaining;
}

/**
 * Gửi nhắc nhở có kiểm soát quota
 * Nếu gần hết quota, ưu tiên gửi cho hóa đơn quá hạn trước
 */
function sendRemindersWithQuotaCheck() {
  var remaining = MailApp.getRemainingDailyQuota();

  if (remaining < 5) {
    Logger.log("Quota còn lại quá ít (" + remaining +
      "). Dừng gửi email.");
    return;
  }

  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
  var data = sheet.getDataRange().getValues();
  var today = new Date();
  today.setHours(0, 0, 0, 0);

  // Phân loại hóa đơn theo mức ưu tiên
  var overdue = [];   // Quá hạn - ưu tiên cao nhất
  var dueToday = [];  // Đúng hạn
  var upcoming = [];  // Sắp đến hạn

  for (var i = 1; i < data.length; i++) {
    var trangThai = data[i][6];
    var email = data[i][2];
    if (trangThai === "Đã thanh toán" || !email) continue;

    var ngayDaoHan = new Date(data[i][5]);
    ngayDaoHan.setHours(0, 0, 0, 0);
    var diffDays = Math.floor(
      (ngayDaoHan - today) / (1000 * 60 * 60 * 24)
    );

    var item = { rowIndex: i, data: data[i], diffDays: diffDays };

    if (diffDays < 0) overdue.push(item);
    else if (diffDays === 0) dueToday.push(item);
    else if (diffDays <= CONFIG.DAYS_BEFORE) upcoming.push(item);
  }

  // Sắp xếp quá hạn theo số ngày (nhiều nhất trước)
  overdue.sort(function(a, b) { return a.diffDays - b.diffDays; });

  // Gửi theo thứ tự ưu tiên
  var allItems = overdue.concat(dueToday).concat(upcoming);
  var sentCount = 0;

  for (var j = 0; j < allItems.length; j++) {
    if (MailApp.getRemainingDailyQuota() < 2) {
      Logger.log("Hết quota email. Đã gửi: " + sentCount +
        ". Còn lại: " + (allItems.length - j));
      break;
    }
    // ... gửi email cho allItems[j] ...
    sentCount++;
  }

  Logger.log("Hoàn tất: " + sentCount + "/" + allItems.length + " email");
}

Mẹo tối ưu khi có nhiều khách hàng

Gom nhiều hóa đơn của 1 KH

Nếu 1 khách hàng có nhiều hóa đơn quá hạn, gom thành 1 email duy nhất thay vì gửi riêng từng hóa đơn. Tiết kiệm quota và đỡ phiền khách.

Chia ra gửi nhiều ngày

Nếu có 200+ hóa đơn cần nhắc, chia ra: Thứ 2 gửi quá hạn, Thứ 4 gửi đúng hạn, Thứ 6 gửi trước hạn.

Dùng MailApp thay GmailApp

MailApp.sendEmail() có thể dùng cùng lúc với GmailApp và chung quota, nhưng MailApp.getRemainingDailyQuota() cho biết số lượng còn lại.

Nâng cấp Google Workspace

Từ 100 lên 1,500 email/ngày. Chi phí khoảng $6/user/tháng - xứng đáng nếu bạn có 100+ khách hàng cần theo dõi công nợ.

Câu Hỏi Thường Gặp (FAQ)

Script có chạy khi tôi không mở Google Sheets không?

Có. Khi bạn đã tạo Time-driven trigger, script sẽ tự động chạy theo lịch đã đặt, bất kể bạn có mở Google Sheets hay không. Đó là sức mạnh của Apps Script - nó chạy trên cloud server của Google, hoàn toàn độc lập với trình duyệt của bạn. Tuy nhiên, nếu script gặp lỗi (ví dụ bạn đổi tên sheet), Google sẽ gửi email thông báo cho bạn.

Email có vào Spam không? Làm sao để tránh?

Email gửi qua GmailApp thường không vào spam vì được gửi từ server Gmail chính thức (có SPF, DKIM sẵn). Tuy nhiên, để đảm bảo tốt nhất:

  • Sử dụng email Google Workspace với domain riêng (ví dụ: ketoan@congtyabc.vn)
  • Không dùng ALL CAPS trong tiêu đề email
  • Thêm link "Unsubscribe" hoặc câu "Nếu đã thanh toán, vui lòng bỏ qua"
  • Tránh gửi quá nhiều email cùng lúc (dùng Utilities.sleep() để delay)

Có thể gửi email cho nhiều người cùng lúc (CC/BCC) không?

Có. Trong hàm GmailApp.sendEmail(), bạn có thể thêm ccbcc vào object options. Ví dụ: CC cho kế toán trưởng khi hóa đơn quá hạn, hoặc BCC cho sếp để theo dõi. Trong script mẫu, chúng ta đã CC manager khi hóa đơn quá hạn.

Tôi muốn gửi kèm file PDF hóa đơn được không?

Có. Nếu file PDF hóa đơn nằm trên Google Drive, bạn có thể đính kèm bằng cách thêm option attachments:

var file = DriveApp.getFileById("FILE_ID_ON_DRIVE");
GmailApp.sendEmail(email, subject, body, {
  htmlBody: htmlContent,
  attachments: [file.getAs(MimeType.PDF)]
});

Bạn có thể lưu file ID vào 1 cột phụ trong sheet để script tự tìm file đính kèm cho mỗi hóa đơn.

Script có bị chạy sai khi có nhiều người cùng edit Google Sheets?

Không. Apps Script đọc dữ liệu tại thời điểm chạy (snapshot), nên không bị ảnh hưởng bởi việc nhiều người edit cùng lúc. Tuy nhiên, nếu 2 người cùng chạy hàm sendAdvancedReminders() cùng lúc, có thể bị gửi trùng. Logic "kiểm tra đã nhắc hôm nay" trong script sẽ hạn chế vấn đề này, nhưng tốt nhất chỉ nên dùng trigger tự động thay vì chạy thủ công cùng lúc.

Workflow Hoàn Chỉnh: Từ Bảng Tính Đến Thu Hồi Công Nợ

Tổng hợp lại, đây là quy trình hoàn chỉnh bạn sẽ có sau khi setup xong:

1

Nhập dữ liệu công nợ

Mỗi khi phát sinh hóa đơn mới, nhập vào sheet "Công nợ" với đầy đủ thông tin: mã KH, email, số tiền, ngày đáo hạn.

2

Script tự động quét mỗi sáng

9h sáng mỗi ngày, trigger chạy: quét toàn bộ danh sách, xác định mức nhắc, gửi email HTML đẹp cho từng hóa đơn cần nhắc.

3

Khách hàng nhận email nhắc nhở

Email chuyên nghiệp với thông tin hóa đơn rõ ràng, thông tin chuyển khoản đầy đủ. Tone phù hợp với tình huống.

4

Theo dõi và cập nhật

Khi khách thanh toán, cập nhật cột "Trạng thái" thành "Đã thanh toán". Script sẽ tự bỏ qua trong lần chạy tiếp theo.

5

Xem báo cáo thống kê

Bất cứ lúc nào, dùng menu "Nhắc Thanh Toán" > "Xem thống kê" để biết đã gửi bao nhiêu email, bao nhiêu thành công/lỗi.

Mở Rộng: Kết Hợp Với Google Form

Bạn có thể nâng cấp hệ thống bằng cách tạo Google Form để khách hàng xác nhận thanh toán. Khi khách nhận email nhắc nhở, họ click vào link Form và xác nhận đã chuyển khoản. Thông tin tự động cập nhật vào Google Sheets.

/**
 * Thêm link xác nhận thanh toán vào email
 * Tạo Google Form với pre-filled URL
 */
function getPaymentConfirmLink(soHD, maKH) {
  var formUrl = "https://docs.google.com/forms/d/e/YOUR_FORM_ID/viewform";
  var preFilled = formUrl +
    "?entry.111111=" + encodeURIComponent(soHD) +
    "&entry.222222=" + encodeURIComponent(maKH);
  return preFilled;
}

/**
 * Khi nhận response từ Form, tự động cập nhật sheet
 * Đặt trigger: On form submit
 */
function onFormSubmit(e) {
  var responses = e.namedValues;
  var soHD = responses["Số hóa đơn"][0];
  var maKH = responses["Mã khách hàng"][0];

  var sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName(CONFIG.SHEET_NAME);
  var data = sheet.getDataRange().getValues();

  for (var i = 1; i < data.length; i++) {
    if (data[i][0] === maKH && data[i][3] === soHD) {
      // Đánh dấu "Đang xác nhận" (chờ kế toán verify)
      sheet.getRange(i + 1, 7).setValue("Đang xác nhận");
      sheet.getRange(i + 1, 10).setValue(
        "KH xác nhận đã TT qua Form lúc " +
        Utilities.formatDate(new Date(), "Asia/Ho_Chi_Minh",
          "dd/MM/yyyy HH:mm")
      );
      break;
    }
  }
}

Tổng Kết

Với hệ thống nhắc nhở thanh toán tự động bằng Google Sheets + Apps Script, bạn đã có trong tay công cụ miễn phí nhưng cực kỳ hiệu quả để quản lý công nợ:

  • 3 mức nhắc nhở thông minh - Nhẹ nhàng trước hạn, chuyên nghiệp đúng hạn, nghiêm túc khi quá hạn

  • Email HTML đẹp - Template chuyên nghiệp với thông tin hóa đơn, tài khoản chuyển khoản đầy đủ

  • Trigger tự động - Chạy mỗi ngày 9h sáng, không cần can thiệp

  • Ghi log đầy đủ - Biết chính xác đã gửi bao nhiêu email, cho ai, khi nào

  • Anti-spam thông minh - Không gửi trùng, giới hạn số lần nhắc, kiểm soát quota

  • Menu tùy chỉnh - Gửi thủ công cho từng dòng, xem thống kê ngay trong Sheets

Hãy bắt đầu với script cơ bản (Bước 2), test với 2-3 hóa đơn, rồi nâng cấp dần lên phiên bản nâng cao. Chỉ cần 30 phút setup, bạn sẽ tiết kiệm hàng chục giờ mỗi tháng và không bao giờ bỏ sót hóa đơn nào nữa!

Muốn quản lý công nợ chuyên nghiệp hơn?

Khám phá SheetStore - Phần mềm quản lý bán hàng tích hợp Google Sheets, với hệ thống theo dõi công nợ tự động, dashboard báo cáo realtime và nhiều tính năng mạnh mẽ khác.

Truy cập sheet.com.vn để tìm hiểu thêm và dùng thử miễn phí.

Chia sẻ bài viết:

Tuân Hoang

Tuân Hoang

Đội ngũ SheetStore

Bạn thấy bài viết hữu ích?

Đăng ký nhận thông báo khi có bài viết mới.

Nhận thông báo khi có bài viết mới. Không spam, hứa luôn! 😊

Bình luận (0)

Vui lòng đăng nhập để tham gia thảo luận