Google Forms và Apps Script
Tiện ích bổ sung Sheets tạo tài liệu khi bạn nhấp Tạo. Để tạo tài liệu mà không cần ai nhấp, chẳng hạn một giấy chứng nhận cho mỗi câu trả lời biểu mẫu hoặc một báo giá khi một hàng được đánh dấu, một Apps Script ngắn sẽ gửi dữ liệu tới API tài liệu của AutomaDoc và xử lý tệp theo ý bạn: lưu vào Drive, gửi qua email, hoặc ghi liên kết của tệp vào trang tính.
Script chạy trong tài khoản Google của bạn, theo các quy tắc Apps Script thông thường của Google. AutomaDoc chỉ thấy JSON mà script gửi đi.
Những gì bạn cần
Phần tiêu đề “Những gì bạn cần”- Một mẫu. Mẫu AutomaDoc nào cũng dùng được: JSON mà script gửi chính là bản ghi, nên nguồn dữ liệu riêng của mẫu không được dùng đến. Các biến giữ chỗ của mẫu được đặt theo các khóa mà script gửi (xem Tên).
- ID của mẫu. Trong Cổng thông tin công ty, mở Mẫu, mở menu của mẫu và chọn Sao chép ID mẫu.
- Một khóa API. Một khóa API của công ty, do quản trị viên tạo trong Cổng thông tin công ty tại Khóa API (bắt đầu bằng
amk_và chỉ hiển thị một lần), hoặc, với mẫu Lark Base, khóa riêng của mẫu đó, vốn chỉ mở được mẫu đó. Phạm vi của khóa càng hẹp càng tốt: bất kỳ ai sửa được script đều đọc được khóa.
Công thức: một tài liệu cho mỗi câu trả lời biểu mẫu
Phần tiêu đề “Công thức: một tài liệu cho mỗi câu trả lời biểu mẫu”Script này được gắn với biểu mẫu. Với mỗi câu trả lời, nó tạo một tệp PDF, lưu vào một thư mục Drive và gửi qua email cho người đã trả lời.
- Mở biểu mẫu trong Google Forms, mở menu ⋮ và chọn Apps Script.
- Trong trình soạn thảo Apps Script, mở Project Settings và thêm một script property tên là
AUTOMADOC_API_KEYvới giá trị là khóa của bạn. Nhờ vậy khóa không nằm trong mã. - Thay nội dung của
Code.gsbằng script bên dưới, và điềnTEMPLATE_IDvàFOLDER_ID(phần cuối trong địa chỉ của thư mục Drive, sau/folders/). - Chọn
setUptrong danh sách hàm và nhấp Run một lần. Google sẽ yêu cầu bạn cho phép script kết nối với dịch vụ bên ngoài, dùng Drive của bạn và gửi email dưới tên bạn.
Từ đó trở đi, mỗi lần gửi biểu mẫu sẽ chạy handleSubmit. Các lượt chạy và lỗi của nó được liệt kê trong Executions của trình soạn thảo.
const AUTOMADOC_API = 'https://api.automadoc.cc';const TEMPLATE_ID = 'PASTE-THE-TEMPLATE-ID';const FOLDER_ID = 'PASTE-THE-DRIVE-FOLDER-ID';
/** Run once: creates the trigger that calls handleSubmit on every response. */function setUp() { ScriptApp.newTrigger('handleSubmit') .forForm(FormApp.getActiveForm()) .onFormSubmit() .create();}
function handleSubmit(e) { const response = e.response;
// One key per question: its title is the placeholder's name. const data = {}; response.getItemResponses().forEach(function (itemResponse) { data[itemResponse.getItem().getTitle()] = answerOf(itemResponse.getResponse()); }); data['Submitted'] = Utilities.formatDate( response.getTimestamp(), Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm');
const doc = createDocument(data); const file = DriveApp.getFolderById(FOLDER_ID).createFile(downloadFile(doc)); console.log('Saved ' + file.getName() + ': ' + file.getUrl());
// Needs "Collect email addresses" in the form's settings. const email = response.getRespondentEmail(); if (email) { MailApp.sendEmail({ to: email, subject: doc.filename, body: 'Thank you. Your document is attached.', attachments: [file.getBlob()], }); }}
/** Grid questions answer with a list of lists, which the API refuses: one text instead. */function answerOf(value) { if (Array.isArray(value) && value.some(Array.isArray)) { return value.map(function (row) { return [].concat(row).join(', '); }).join('; '); } return value;}
/** Fills the template and waits until the document is finished. */function createDocument(data) { let doc = callApi(AUTOMADOC_API + '/api/v1/templates/' + TEMPLATE_ID + '/documents?wait=20', { method: 'post', contentType: 'application/json', payload: JSON.stringify({ data: data, format: 'pdf' }), }); // Not finished within the 20 seconds: read its status every 5 seconds, for up to 4 minutes. for (let i = 0; doc.status === 'processing' && i < 48; i++) { Utilities.sleep(5000); doc = callApi(doc.statusUrl); } if (doc.status !== 'completed') { throw new Error(doc.error || 'The document is still being made: ' + doc.statusUrl); } if (doc.warnings) console.log(doc.warnings.join('\n')); return doc;}
/** The finished file, as a blob named like the document. */function downloadFile(doc) { const response = UrlFetchApp.fetch(doc.statusUrl + '/file', { headers: { 'X-Webhook-API-Key': apiKey() }, muteHttpExceptions: true, }); if (response.getResponseCode() !== 200) { throw new Error('Download failed: ' + response.getContentText()); } return response.getBlob().setName(doc.filename);}
function callApi(url, options) { options = options || {}; options.headers = { 'X-Webhook-API-Key': apiKey() }; options.muteHttpExceptions = true; const response = UrlFetchApp.fetch(url, options); const body = JSON.parse(response.getContentText()); if (response.getResponseCode() >= 400) throw new Error(body.code + ': ' + body.error); return body;}
function apiKey() { return PropertiesService.getScriptProperties().getProperty('AUTOMADOC_API_KEY');}Điều xảy ra ở mỗi lệnh gọi:
POST /api/v1/templates/{templateId}/documents?wait=20gửi{ "data": …, "format": "pdf" }. Vớiwait, câu trả lời đến khi tài liệu hoàn tất (200) hoặc sau 20 giây (202, vẫnprocessing).formatcũng có thể là"native"(định dạng Word, Excel hoặc PowerPoint gốc của mẫu) hoặc"png", vàfilenameđặt mẫu tên tệp cho lệnh gọi này.GET /api/v1/documents/{generationId}(statusUrltrong câu trả lời) đọcstatuscủa tài liệu:processing,completedhoặcfailed, với lý do trongerror. Tài liệu đã hoàn tất cófilenamevàdownloadUrl.GET /api/v1/documents/{generationId}/filetrả về chính tệp đó, với cùng khóa.
Nếu bạn chỉ cần một liên kết, hãy bỏ qua downloadFile: doc.downloadUrl mở tệp mà không cần khóa, trong thời gian công ty bạn lưu giữ tài liệu đã tạo (30 ngày nếu tài liệu được lưu giữ vô thời hạn). Mỗi lần đọc trạng thái sẽ cho một liên kết mới.
Công thức: một tài liệu khi một ô được đánh dấu trong trang tính
Phần tiêu đề “Công thức: một tài liệu khi một ô được đánh dấu trong trang tính”Các hàm hỗ trợ trên cũng dùng được từ một bảng tính. Mở bảng tính, chọn Extensions → Apps Script, thêm script property, rồi dán các hàm hỗ trợ ở trên (createDocument, downloadFile, callApi, apiKey và ba hằng số) cùng đoạn dưới đây thay cho setUp và handleSubmit.
Trang tính ở đây tên là Orders. Hàng 1 chứa tên các cột; một cột hộp kiểm tên Generate khởi động một tài liệu, và một cột tên Document nhận liên kết của tài liệu.
const SHEET_NAME = 'Orders';
/** Run once: creates the trigger that calls handleEdit on every edit. */function setUp() { ScriptApp.newTrigger('handleEdit') .forSpreadsheet(SpreadsheetApp.getActive()) .onEdit() .create();}
function handleEdit(e) { const sheet = e.range.getSheet(); // Only a single checkbox ticked on a data row of the right tab if (sheet.getName() !== SHEET_NAME || e.range.getRow() < 2 || e.value !== 'TRUE') return;
const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getDisplayValues()[0]; if (headers[e.range.getColumn() - 1] !== 'Generate') return;
const row = e.range.getRow(); const values = sheet.getRange(row, 1, 1, headers.length).getDisplayValues()[0]; const data = {}; headers.forEach(function (header, i) { if (header && header !== 'Generate' && header !== 'Document') data[header] = values[i]; });
const doc = createDocument(data); const file = DriveApp.getFolderById(FOLDER_ID).createFile(downloadFile(doc)); const linkColumn = headers.indexOf('Document') + 1; if (linkColumn > 0) sheet.getRange(row, linkColumn).setValue(file.getUrl());}Điều kiện kích hoạt phải là loại cài đặt được (installable) mà setUp tạo ra. Một hàm tên onEdit sẽ chạy như một điều kiện kích hoạt đơn giản (simple trigger), loại mà Google không cho phép gọi dịch vụ bên ngoài.
getDisplayValues() gửi mỗi ô đúng như trang tính hiển thị, nên số được gửi đến dưới dạng văn bản đã định dạng theo trang tính. Để dùng các bộ lọc như number, currency hoặc sum trên một cột, hãy gửi giá trị của cột đó lấy từ getValues().
Mỗi khóa trở thành tên biến giữ chỗ theo cùng cách như tên cột hoặc tên trường: khoảng trắng, dấu chấm và các ký hiệu khác trở thành _, chữ cái và chữ số của mọi ngôn ngữ được giữ nguyên, và có phân biệt chữ hoa chữ thường.
| Câu hỏi hoặc cột | Biến giữ chỗ |
|---|---|
Full name |
{Full_name} |
Invoice No. |
{Invoice_No_} |
Submitted (do script thêm vào) |
{Submitted} |
Tiêu đề không có chữ cái hay chữ số nào sẽ bị từ chối (DATA_INVALID_NAME), và hai tiêu đề cho ra cùng một tên cũng bị từ chối (DATA_NAME_COLLISION). Câu hỏi dạng hộp kiểm trả lời bằng một danh sách, được in ra nối bằng dấu phẩy.
Ngày gửi dưới dạng văn bản không có múi giờ, như 2026-10-17 09:30, được đọc theo múi giờ của mẫu, nên bộ lọc date có thể định dạng lại.
Hình ảnh phải là địa chỉ web công khai (https://, hoặc http://), được gửi ở chỗ mẫu có ảnh giữ chỗ. Tệp tải lên qua một câu hỏi của biểu mẫu là tệp riêng tư trong Drive của chủ biểu mẫu: AutomaDoc không tải được chúng, và câu trả lời chỉ là id của các tệp. Hãy để những câu hỏi như vậy ngoài mẫu.
Giới hạn và chi phí
Phần tiêu đề “Giới hạn và chi phí”| Yêu cầu cho mỗi mẫu | 60 mỗi phút |
| Tài liệu cho mỗi công ty | 600 mỗi phút, tính chung mọi quy trình tự động |
| Lần đọc trạng thái cho mỗi tài liệu | 60 mỗi phút; lượt tải tệp có giới hạn 60 riêng |
wait |
Tối đa 25 giây |
| Nội dung yêu cầu | 1 MB |
| Số ký tự trong một giá trị | 100.000 |
Một lần từ chối trả về kèm code và error, và script ở trên dừng lại với cả hai trong nhật ký Executions. RATE_LIMITED (429) nghĩa là có quá nhiều lệnh gọi trong một phút: biểu mẫu có thể nhận nhiều câu trả lời cùng lúc nên bắt lỗi này và thử lại sau một chút. Tài liệu tham khảo API liệt kê mọi mã.
Google dừng một lượt chạy script sau 6 phút, đó là lý do script ngừng đọc trạng thái sau 4 phút. Tài liệu vẫn đang được tạo lúc đó không bị mất: statusUrl của nó trong thông báo lỗi vẫn trả lời.
Tài liệu tạo theo cách này là tài liệu tự động. Chúng được trừ vào hạn mức tự động của các suất Business trước, sau đó vào hạn mức miễn phí hằng tháng, rồi đến các gói tài liệu của bạn; khi cả ba đều hết, API trả lời QUOTA_EXCEEDED. Với gói Miễn phí, tài liệu mang dòng chân trang “Generated with AutomaDoc” trừ khi công ty bạn có một gói tài liệu đã mua.