Google Forms และ Apps Script
ส่วนเสริม Sheets สร้างเอกสารเมื่อคุณคลิก สร้าง หากต้องการสร้างเอกสารโดยไม่ต้องมีใครคลิก เช่น ใบประกาศนียบัตรสำหรับคำตอบแต่ละรายการของฟอร์ม หรือใบเสนอราคาเมื่อมีการติ๊กแถว ให้ใช้ Apps Script สั้น ๆ ส่งข้อมูลไปยัง API เอกสาร ของ AutomaDoc แล้วจัดการไฟล์ตามที่ต้องการ: บันทึกลงใน Drive ส่งทางอีเมล หรือเขียนลิงก์ลงในชีต
สคริปต์ทำงานในบัญชี Google ของคุณ ภายใต้กฎ Apps Script ตามปกติของ Google AutomaDoc เห็นเฉพาะ JSON ที่สคริปต์ส่งมาเท่านั้น
สิ่งที่ต้องมี
หัวข้อที่มีชื่อว่า “สิ่งที่ต้องมี”- เทมเพลต ใช้เทมเพลต AutomaDoc ใดก็ได้: JSON ที่สคริปต์ส่งคือระเบียน จึงไม่ได้ใช้แหล่งข้อมูลของเทมเพลตเอง ตัวแทนค่าตั้งชื่อตามคีย์ที่สคริปต์ส่ง (ดูชื่อ)
- ID ของเทมเพลต ในพอร์ทัลบริษัท เปิด เทมเพลต เปิดเมนูของเทมเพลต แล้วเลือก คัดลอก ID เทมเพลต
- คีย์ API คีย์ API ของบริษัท ซึ่งผู้ดูแลระบบสร้างในพอร์ทัลบริษัทที่ คีย์ API (ขึ้นต้นด้วย
amk_และแสดงเพียงครั้งเดียว) หรือคีย์ของเทมเพลตเองสำหรับเทมเพลต Lark Base ซึ่งเปิดได้เฉพาะเทมเพลตนั้น ขอบเขตของคีย์ยิ่งแคบยิ่งดี เพราะใครก็ตามที่แก้ไขสคริปต์ได้ก็อ่านคีย์ได้
สูตร: เอกสารสำหรับทุกคำตอบของฟอร์ม
หัวข้อที่มีชื่อว่า “สูตร: เอกสารสำหรับทุกคำตอบของฟอร์ม”สคริปต์นี้ผูกอยู่กับฟอร์ม สำหรับแต่ละคำตอบ สคริปต์จะสร้าง PDF บันทึกลงในโฟลเดอร์ Drive และส่งทางอีเมลไปยังผู้ตอบ
- เปิดฟอร์มใน Google Forms เปิดเมนู ⋮ แล้วเลือก Apps Script
- ในเครื่องมือแก้ไข Apps Script เปิด Project Settings แล้วเพิ่มพร็อพเพอร์ตีของสคริปต์ชื่อ
AUTOMADOC_API_KEYโดยใส่คีย์ของคุณเป็นค่า คีย์จะได้ไม่อยู่ในโค้ด - แทนที่เนื้อหาของ
Code.gsด้วยสคริปต์ด้านล่าง แล้วกรอกTEMPLATE_IDและFOLDER_ID(ส่วนสุดท้ายของที่อยู่โฟลเดอร์ Drive หลัง/folders/) - เลือก
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และdownloadUrlGET /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” เว้นแต่บริษัทของคุณมีแพ็กที่ซื้อไว้