Tự Động Gửi Email Nhắc Nhở Thanh Toán Từ Google Sheets [Apps Script]
![Tự Động Gửi Email Nhắc Nhở Thanh Toán Từ Google Sheets [Apps Script]](/images/blog/tu-dong-gui-email-nhac-thanh-toan-google-sheets.png)
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 | 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 đã 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. Mở Google Sheets chứa bảng "Công nợ"
- 2. Vào menu Extensions (Tiện ích mở rộng) > Apps Script
- 3. Xóa nội dung mặc định trong
Code.gs - 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ợ:
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..."
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..."
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) | Có |
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. Trong Apps Script Editor, click biểu tượng đồng hồ (Triggers) ở thanh bên trái
- 2. Click "+ Add Trigger" ở góc phải dưới
- 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. 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ê và 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 | 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. Sau khi lưu code, reload lại Google Sheets (F5 hoặc Ctrl+R)
- 2. Bạn sẽ thấy menu mới "Nhắc Thanh Toán" xuất hiện trên thanh menu
- 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. 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. Dùng email cá nhân của bạn ở cột C để tự kiểm tra
- 3. Trong Apps Script, chọn hàm sendAdvancedReminders > click Run
- 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. 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() và 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 cc và bcc 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:
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.
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.
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.
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.
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í.
📚 Bài Viết Liên Quan
- Template Google Sheets Báo Cáo Bán Hàng Theo Vùng và Đại Lý 2027: Phân Tích Đa Chiều
- Google Sheets Nâng Cao Bài 9: Bảo Mật, Phân Quyền và Chia Sẻ Chuyên Nghiệp
- Google Sheets Nâng Cao Bài 4: Hàm QUERY - Lọc và Phân Tích Dữ Liệu Chuyên Nghiệp
- Template Google Sheets Quản Lý Phòng Khám và Bệnh Viện Nhỏ 2027
Chia sẻ bài viết:
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.