ข้ามไปยังเนื้อหา

Google Forms และ Apps Script

ส่วนเสริม Sheets สร้างเอกสารเมื่อคุณคลิก สร้าง หากต้องการสร้างเอกสารโดยไม่ต้องมีใครคลิก เช่น ใบประกาศนียบัตรสำหรับคำตอบแต่ละรายการของฟอร์ม หรือใบเสนอราคาเมื่อมีการติ๊กแถว ให้ใช้ Apps Script สั้น ๆ ส่งข้อมูลไปยัง API เอกสาร ของ AutomaDoc แล้วจัดการไฟล์ตามที่ต้องการ: บันทึกลงใน Drive ส่งทางอีเมล หรือเขียนลิงก์ลงในชีต

สคริปต์ทำงานในบัญชี Google ของคุณ ภายใต้กฎ Apps Script ตามปกติของ Google AutomaDoc เห็นเฉพาะ JSON ที่สคริปต์ส่งมาเท่านั้น

  1. เทมเพลต ใช้เทมเพลต AutomaDoc ใดก็ได้: JSON ที่สคริปต์ส่งคือระเบียน จึงไม่ได้ใช้แหล่งข้อมูลของเทมเพลตเอง ตัวแทนค่าตั้งชื่อตามคีย์ที่สคริปต์ส่ง (ดูชื่อ)
  2. ID ของเทมเพลต ในพอร์ทัลบริษัท เปิด เทมเพลต เปิดเมนูของเทมเพลต แล้วเลือก คัดลอก ID เทมเพลต
  3. คีย์ API คีย์ API ของบริษัท ซึ่งผู้ดูแลระบบสร้างในพอร์ทัลบริษัทที่ คีย์ API (ขึ้นต้นด้วย amk_ และแสดงเพียงครั้งเดียว) หรือคีย์ของเทมเพลตเองสำหรับเทมเพลต Lark Base ซึ่งเปิดได้เฉพาะเทมเพลตนั้น ขอบเขตของคีย์ยิ่งแคบยิ่งดี เพราะใครก็ตามที่แก้ไขสคริปต์ได้ก็อ่านคีย์ได้

สคริปต์นี้ผูกอยู่กับฟอร์ม สำหรับแต่ละคำตอบ สคริปต์จะสร้าง PDF บันทึกลงในโฟลเดอร์ Drive และส่งทางอีเมลไปยังผู้ตอบ

  1. เปิดฟอร์มใน Google Forms เปิดเมนู ⋮ แล้วเลือก Apps Script
  2. ในเครื่องมือแก้ไข Apps Script เปิด Project Settings แล้วเพิ่มพร็อพเพอร์ตีของสคริปต์ชื่อ AUTOMADOC_API_KEY โดยใส่คีย์ของคุณเป็นค่า คีย์จะได้ไม่อยู่ในโค้ด
  3. แทนที่เนื้อหาของ Code.gs ด้วยสคริปต์ด้านล่าง แล้วกรอก TEMPLATE_ID และ FOLDER_ID (ส่วนสุดท้ายของที่อยู่โฟลเดอร์ Drive หลัง /folders/)
  4. เลือก setUp ในรายการฟังก์ชันแล้วคลิก Run หนึ่งครั้ง Google จะขอให้คุณอนุญาตให้สคริปต์เชื่อมต่อกับบริการภายนอก ใช้ Drive ของคุณ และส่งอีเมลในนามของคุณ

จากนั้นการส่งฟอร์มทุกครั้งจะเรียก handleSubmit การทำงานและข้อผิดพลาดจะแสดงอยู่ใต้ Executions ในเครื่องมือแก้ไข

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');
}

สิ่งที่เกิดขึ้นในการเรียกแต่ละครั้ง:

  • POST /api/v1/templates/{templateId}/documents?wait=20 ส่ง { "data": …, "format": "pdf" } เมื่อมี wait คำตอบจะมาเมื่อเอกสารเสร็จ (200) หรือหลังจาก 20 วินาที (202 ยังเป็น processing) format ยังเป็น "native" (รูปแบบ Word, Excel หรือ PowerPoint ของเทมเพลตเอง) หรือ "png" ได้ และ filename ใช้ตั้งรูปแบบชื่อไฟล์สำหรับการเรียกครั้งนี้
  • GET /api/v1/documents/{generationId} (คือ statusUrl ในคำตอบ) อ่าน status: processing, completed หรือ failed พร้อมเหตุผลใน error เอกสารที่เสร็จแล้วมี filename และ downloadUrl
  • GET /api/v1/documents/{generationId}/file ส่งคืนตัวไฟล์ ด้วยคีย์เดียวกัน

หากต้องการเพียงลิงก์ ให้ข้าม downloadFile: doc.downloadUrl เปิดไฟล์ได้โดยไม่ต้องใช้คีย์ ตราบเท่าที่บริษัทของคุณเก็บเอกสารที่สร้างไว้ (30 วัน หากตั้งให้เก็บไว้ไม่มีกำหนด) การอ่านสถานะแต่ละครั้งจะให้ลิงก์ใหม่

ฟังก์ชันช่วยชุดเดียวกันใช้ได้จากสเปรดชีต เปิดสเปรดชีต เลือก ส่วนขยาย → Apps Script เพิ่มพร็อพเพอร์ตีของสคริปต์ แล้ววางฟังก์ชันช่วยด้านบน (createDocument, downloadFile, callApi, apiKey และค่าคงที่ทั้งสามตัว) พร้อมโค้ดนี้แทน setUp และ handleSubmit

แท็บในตัวอย่างนี้ชื่อ Orders แถว 1 เก็บชื่อคอลัมน์ คอลัมน์ช่องทำเครื่องหมายชื่อ Generate ใช้เริ่มสร้างเอกสาร และคอลัมน์ชื่อ Document ใช้รับลิงก์

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());
}

ทริกเกอร์ต้องเป็นแบบติดตั้งได้ (installable) ที่ setUp สร้าง ฟังก์ชันที่ชื่อ onEdit จะทำงานเป็นทริกเกอร์แบบง่าย ซึ่ง Google ไม่อนุญาตให้เรียกบริการภายนอก

getDisplayValues() ส่งแต่ละเซลล์ตามที่ชีตแสดง ตัวเลขจึงมาถึงเป็นข้อความที่ชีตจัดรูปแบบแล้ว หากต้องการใช้ตัวกรองอย่าง number, currency หรือ sum กับคอลัมน์ใด ให้ส่งค่าของคอลัมน์นั้นจาก getValues() แทน

แต่ละคีย์จะกลายเป็นชื่อตัวแทนค่าแบบเดียวกับชื่อคอลัมน์หรือชื่อฟิลด์: ช่องว่าง จุด และสัญลักษณ์อื่นจะกลายเป็น _ ตัวอักษรและตัวเลขของทุกภาษาจะคงไว้ และตัวพิมพ์เล็กใหญ่มีผล

คำถามหรือคอลัมน์ ตัวแทนค่า
Full name {Full_name}
Invoice No. {Invoice_No_}
Submitted (สคริปต์เพิ่มให้) {Submitted}

ชื่อคำถามที่ไม่มีตัวอักษรหรือตัวเลขเลยจะถูกปฏิเสธ (DATA_INVALID_NAME) และชื่อคำถามสองชื่อที่กลายเป็นชื่อเดียวกันก็จะถูกปฏิเสธ (DATA_NAME_COLLISION) คำถามแบบช่องทำเครื่องหมายจะตอบเป็นรายการ ซึ่งจะพิมพ์ออกมาคั่นด้วยจุลภาค

วันที่ที่ส่งเป็นข้อความโดยไม่มีเขตเวลา เช่น 2026-10-17 09:30 จะถูกอ่านตามเขตเวลาของเทมเพลต ตัวกรอง date จึงจัดรูปแบบใหม่ได้

รูปภาพ ต้องเป็นที่อยู่เว็บสาธารณะ (https:// หรือ http://) ส่งไปในตำแหน่งที่เทมเพลตมีรูปภาพตัวแทน ไฟล์ที่อัปโหลดผ่านคำถามในฟอร์มเป็นไฟล์ส่วนตัวใน Drive ของเจ้าของฟอร์ม AutomaDoc ดึงไฟล์เหล่านั้นไม่ได้ และคำตอบของคำถามนั้นมีเพียง ID ของไฟล์ จึงไม่ควรใส่คำถามประเภทนี้ในเทมเพลต

คำขอต่อเทมเพลต 60 ครั้งต่อนาที
เอกสารต่อบริษัท 600 ฉบับต่อนาที รวมทุกระบบอัตโนมัติ
การอ่านสถานะต่อเอกสาร 60 ครั้งต่อนาที การดาวน์โหลดไฟล์มีโควตา 60 ครั้งแยกต่างหาก
wait สูงสุด 25 วินาที
ขนาดเนื้อหาคำขอ 1 MB
จำนวนอักขระในค่าหนึ่งค่า 100,000

การปฏิเสธจะส่งกลับมาพร้อม code และ error และสคริปต์ด้านบนจะหยุดทำงานโดยบันทึกทั้งสองค่าไว้ในบันทึก Executions RATE_LIMITED (429) หมายถึงเรียกมากเกินไปในนาทีนั้น ฟอร์มที่อาจได้รับคำตอบจำนวนมากพร้อมกันควรดักข้อผิดพลาดนี้แล้วลองอีกครั้งในอีกสักครู่ ข้อมูลอ้างอิง API แสดงรหัสทั้งหมด

Google หยุดการทำงานของสคริปต์แต่ละครั้งหลังจาก 6 นาที สคริปต์จึงหยุดอ่านสถานะหลังจาก 4 นาที เอกสารที่ยังสร้างไม่เสร็จในตอนนั้นจะไม่สูญหาย: statusUrl ในข้อผิดพลาดยังคงตอบกลับอยู่

เอกสารที่สร้างด้วยวิธีนี้เป็นเอกสารอัตโนมัติ โดยจะนับจากโควตาระบบอัตโนมัติของที่นั่ง Business ก่อน จากนั้นจากโควตาฟรีรายเดือน แล้วจึงจากแพ็กเอกสารของคุณ เมื่อใช้หมดทั้งสามส่วน API จะตอบ QUOTA_EXCEEDED ในแผน Free เอกสารจะมีบรรทัดท้ายกระดาษ “Generated with AutomaDoc” เว้นแต่บริษัทของคุณมีแพ็กที่ซื้อไว้