本篇適合:

  • 小企業、組織,有「大量發通知」、「印收據」的需求,且需要便於管理的工具。
  • 對 生成式 AI 應用有興趣的朋友
  • 就算你不會寫程式,也有機會快速上手!

本篇目標:

  1. 學習如何與AI互動解決內心真正的問題
  2. 製作出一個自動化收據開立系統!

筆者雖然過去寫過一些小程式,但比起真正企業的工程師大概還差一大截。這幾個月來生成式 AI (Generative Artifical Intelligence,後文簡稱 GAI)的快速發展,實在令不少人跌破眼鏡。

一直都知道「Google Apps Script」的存在,是一個可以允許你控制和自動化Google文件、試算表、簡報工具,但一來不知道應用場景,一來更不知道怎麼使用,要重新學習一個東西也相當需要門檻(仿間也出了不少工具書、付費課程)。

現在有了 GAI,叫 AI 教你一步一步做吧!以下比較不像是教你怎麼做,而是教你怎麼請AI教你做

初始指令

首先,我們需要定義任務。切記,這個「任務」你需要好好審慎思考。

錯誤指令:「我想用 Google 做一個自動化收據系統」

當然,AI 可能也會給你一個答案,但它可能並不完全符合你的想像情境。

對,「想像情境」。有想像,且試圖去「描述想像」,才會有機會讓 AI 正確教你,且做出來符合使用者要求的東西。

以下給你參考我的指令(很多人會稱之為「Prompt」,而這個動作叫做「Prompt Engineering」):

1
2
3
4
5
6
7
8
9
10
11
我想使用 Google App Script  搭配google spreadsheet, google doc 做自動生成收據系統,
現在我有一份 spreadsheet 表單和google doc 收據(名為「收據模板」,儲存在同一層中的「收據製作」資料夾內),

請使用 app script 達成以下情況:

1. 當 spreadsheet 中偵測到新增一筆數據的「收據製作(J欄)」呈現「未處理」,則開始準備製作收據。切記,未處理改成已處理的動作,並不會觸發收據製作。
2. 使用我的 google doc(可以先副本一份) 將相對應的欄位填入,並將檔案轉成 PDF , PDF 文件名稱:改成收據 + 姓名 + _ + 序號,PDF名稱與doc 副本檔案名稱一致,PDF 檔案儲存到「收據製作」資料夾裡面的「收據」資料夾
3. 製作完成後,spreadsheet 在 K 欄對應該筆完成收據製作的資料,寫「自動程式已完成收據製作」

請你一步一步教我怎麼操作,使用繁體中文回覆

同時,現在大部分的 AI 都已經支援圖片上傳,也請你上傳幾個相關的圖片,讓 AI 自己去辨識揣摩。

本文使用Claude為例

Google Apps Script 使用

先寫在前面,AI 認識的「Google Apps Script」,可能是幾年前的 Google Apps Script,因此他對你的指示是有可能有所落差,不過應該會大同小異,你可以切記這點。

而這邊簡單介紹一下這個工具,可以從 Google 試算表中上層選單「擴充功能 > Apps Script」進入畫面。

Apps Script 介面

進入畫面後,就會看到輸入程式碼的區塊,此時你可以把 AI 給你的程式碼直接挑到主要區塊當中,把預設的「funciton myFunction()」覆蓋掉。如果你從來沒寫過程式,請勇敢做這件事,它不會被你玩壞的!儘管玩!(很重要XD)

接著帶你介紹介面,左邊欄位的時鐘號誌是「觸發器(Trigger)」,當你做的自動化工具是「因為某個動作」或「到了某個時間」就要觸發,通常會使用這個工具,有時候我知道了我需要有這樣的功能,會直接在 Prompt 裡面告訴 AI 說我需要使用觸發器。

左邊欄位有「觸發器」和「執行項目」

觸發器:如果有設定觸發器,這邊就會顯示。

(當然,AI 有時候也會自己判斷是否需要觸發器)

觸發器下面是「執行項目」,也就是可以看到所有程式碼執行情況的地方。比起其他程式碼,我覺得 Apps Script 更難去針對錯誤解決問題。因此你的 Prompt 可以多加「能否幫我把程式碼做一些log讓我可以看?」

這裡的 log 不是數學的對數,在程式語言中指的是日誌(logging),通常會有視窗(或檔案)記錄程式執行的過程,幫助開發者理解程式的運行狀況,診斷錯誤和問題。因此,我在 Prompt 裡增加「能否幫我把程式碼做一些log讓我可以看?」後,我就可以在「執行項目」裏看到他執行的過程,讓我更好理解發生什麼事、程式執行到哪。

執行項目裡,如果你有請 AI 輸出 log,則會有一些執行過後的文字放在這邊。

調整細部工作內容、調整錯誤

雷點 1 - 把自己當白癡,跟著 AI 一步一步做

「切記,跟著 AI 一步一步做,不要漏掉!」

AI 可能出錯,但常常我自己的錯是漏掉 AI 給的步驟,因此請記得鉅細靡遺的嘗試一步一步來,不會的話,也可以反覆詢問AI。

雷點 2 - AI 可能出錯,貼錯誤訊息給他!

貼錯誤訊息,或說「無法成功,請重新思考一次」,都是一種方式,不過,如果能有錯誤訊息,會更能讓 AI 知道怎麼改善!

我的任務

最後,直接附上我的成果。

首先準備好一份已經處理好的 Spreadsheet 表格,以及一份收據模板:

Spreadsheet

Doc 收據模板

在這邊,根據 Claude 最後給我的 code,設定的觸發條件是當 J 欄(收據製作)文字是「未電子開立」且 K欄沒有任何值,我就會開始進行收據製作,並也會轉成 PDF 檔存放到指定的位址!

如果想要直接使用我的程式碼,你需要準備上面兩份資料,搭配我的 Prompt 就可以使用!(記得相關的資料夾也要設定喔!詳見我給 AI 的指令)

Claude 回覆與程式碼

  1. 設置步驟:
    a. 將上面的代碼添加到您的腳本中。
    b. 運行 createEditTrigger 函數來創建安裝的觸發器。
    c. 在腳本編輯器中,轉到 “觸發器” 頁面(左側邊欄時鐘圖標)。
    d. 您應該能看到一個新的觸發器被創建。
  2. 授予權限:
    a. 在觸發器頁面,點擊新創建的觸發器。
    b. 在彈出的窗口中,您可能需要再次授予權限。
    c. 仔細閱讀權限請求,然後點擊 “允許”。
  3. 測試:現在,當您在試算表中編輯 J 列時,應該會觸發 onEditTrigger 函數,而不是原來的 onEdit。這個函數應該有足夠的權限來訪問 Drive。
    錯誤處理和日誌
  4. 為了更好地捕獲可能的錯誤,我們可以在 generateReceipt 函數中添加更多的錯誤處理和日誌記錄

這些更改應該能幫助解決權限問題,並提供更詳細的錯誤信息。如果您仍然遇到問題:

  1. 檢查觸發器是否正確創建和授權。
  2. 運行 testDrivePermissions 函數(如之前提供的),確保基本的 Drive 操作正常。
  3. 如果問題持續,請查看執行日誌並分享詳細的錯誤信息。

如果您需要進一步的幫助或有任何疑問,請告訴我。我們會繼續調試直到問題解決。

程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
//需先執行 createEditTrigger
function createEditTrigger() {
var ss = SpreadsheetApp.getActive();
ScriptApp.newTrigger('onEditTrigger')
.forSpreadsheet(ss)
.onEdit()
.create();
Logger.log('Edit trigger created successfully');
}


// 全局變量,用於存儲待處理的行
var pendingRows = [];

function onEditTrigger(e) {
Logger.log('onEditTrigger executed');
var sheet = e.source.getActiveSheet();
var range = e.range;

// 檢查是否有多行被編輯
var startRow = range.getRow();
var numRows = range.getNumRows();

for (var i = 0; i < numRows; i++) {
var currentRow = startRow + i;
var jCellValue = sheet.getRange(currentRow, 10).getValue().toString().trim();
var kCellValue = sheet.getRange(currentRow, 11).getValue();

if (jCellValue === "未電子開立" && kCellValue === "") {
Logger.log('Row ' + currentRow + ' needs processing');
sheet.getRange(currentRow, 11).setValue("等待處理...");
pendingRows.push(currentRow);
}
}

// 如果有待處理的行,開始處理
if (pendingRows.length > 0) {
Logger.log('Starting to process ' + pendingRows.length + ' rows');
processPendingRows(sheet);
}
}

function processPendingRows(sheet) {
while (pendingRows.length > 0) {
var row = pendingRows.shift(); // 從隊列中取出第一個行
Logger.log('Processing row: ' + row);
sheet.getRange(row, 11).setValue("製作中...");

try {
generateReceipt(sheet, row);
sheet.getRange(row, 11).setValue("自動程式已完成收據製作");
Logger.log('Receipt generated successfully for row ' + row);
} catch (error) {
Logger.log('Error processing row ' + row + ': ' + error.toString());
sheet.getRange(row, 11).setValue("錯誤: " + error.toString());
}
}
}

// 輔助函數:格式化金額
function formatAmount(amount) {
// 檢查 amount 是否為數字或可以轉換為數字
var num = parseFloat(amount);
if (isNaN(num)) {
return amount; // 如果不是有效數字,返回原始值
}

// 將數字轉換為字符串,並用正則表達式添加逗號
return num.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}

// Main Functions
function generateReceipt(sheet, row) {
Logger.log('Starting generateReceipt for row: ' + row);

try {
var data = sheet.getRange(row, 1, 1, 11).getValues()[0]; // 注意:現在獲取11列數據
Logger.log('Retrieved data: ' + JSON.stringify(data));

var [序號, 姓名, 開立日期, 證號, 項目, 用途, 總額, 地址, 電話, 收據狀態] = data;

// 格式化日期
var formattedDate = Utilities.formatDate(new Date(開立日期), Session.getScriptTimeZone(), "yyyy/MM/dd");
Logger.log('Formatted date: ' + formattedDate);

// 格式化金額
var formattedAmount = formatAmount(總額);
Logger.log('Formatted amount: ' + formattedAmount);

// 生成中文大寫金額
// var chineseAmount = numberToChinese(總額);
// Logger.log('Chinese amount: ' + chineseAmount);

// 開啟模板文件
var templateFile = DriveApp.getFilesByName("收據模板").next();
Logger.log('Template file found: ' + templateFile.getName());

var newDoc = templateFile.makeCopy("收據_" + 姓名 + "_" + 序號, DriveApp.getFoldersByName("收據製作").next());
Logger.log('New document created: ' + newDoc.getName());

var doc = DocumentApp.openById(newDoc.getId());
var body = doc.getBody();

// 替換文檔中的佔位符
Logger.log('Replacing placeholders in the document');
body.replaceText("{{序號}}", 序號);
body.replaceText("{{姓名}}", 姓名);
body.replaceText("{{開立日期}}", formattedDate);
body.replaceText("{{證號}}", 證號);
body.replaceText("{{項目}}", 項目);
body.replaceText("{{用途}}", 用途);
body.replaceText("{{總額}}", formattedAmount);
// body.replaceText("{{中文總額}}", chineseAmount); // 新增的中文大寫金額
body.replaceText("{{地址}}", 地址);
body.replaceText("{{電話}}", 電話);

doc.saveAndClose();
Logger.log('Document saved and closed');

// Convert to PDF
var pdf = DriveApp.getFileById(newDoc.getId()).getAs("application/pdf");
// Find the "收據製作" folder
var receiptFolder = DriveApp.getFoldersByName("收據製作").next();

// Find or create the "收據" subfolder within "收據製作"
var pdfFolder;
var subFolders = receiptFolder.getFoldersByName("收據");
if (subFolders.hasNext()) {
pdfFolder = subFolders.next();
} else {
pdfFolder = receiptFolder.createFolder("收據");
}

Logger.log('PDF will be saved in folder: ' + pdfFolder.getName());

var pdfFile = pdfFolder.createFile(pdf);
pdfFile.setName("收據_" + 姓名 + "_" + 序號 + ".pdf");
Logger.log('PDF created: ' + pdfFile.getName());

// 在成功生成 PDF 後,更新 K 欄的狀態
sheet.getRange(row, 11).setValue("自動程式已完成收據製作");
Logger.log('Receipt generated successfully, spreadsheet updated');

} catch (error) {
Logger.log('Error in generateReceipt: ' + error.toString());
Logger.log('Error stack: ' + error.stack);
// 如果出錯,更新 K 欄的狀態
sheet.getRange(row, 11).setValue("錯誤: " + error.toString());
}
}

// 手動測試函數
function manualTest() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var testRow = 2; // 假設要測試第二行
Logger.log('Starting manual test for row: ' + testRow);
generateReceipt(sheet, testRow);
Logger.log('Manual test completed');
}