[實作筆記] 用 AI 寫一套自動開單系統

GAS WORKFLOW

[實作筆記] 用 AI 寫一套自動開單系統

不想再手動打 Word 和記帳了!
分享如何用 Google 免費工具 + AI,打造整合收據與自動歸檔的行政系統。

01
為什麼要自己做?系統架構

以前開收據的流程是:打開 Word 範本 ➞ 填資料 ➞ 另存新檔 ➞ 打開 Excel ➞ 登記帳務。只要一忙起來,不是忘記存檔就是記錯帳。

所以我希望做一個系統,只要在手機網頁上填一次資料,後面所有事情自動完成。

⚡ 資料自動化流向
📱 網頁填單
⚙️ GAS 處理
📄 Word 變數
+
📂 Drive 歸檔
+
📊 Sheet 記帳
02
準備工作:三個關鍵檔案

這套系統不需要租伺服器,只需要你的 Google 雲端硬碟。請先建立這三個檔案,並把 ID 記下來。

1. Doc 範本

XX 工作室 收據

日期:{{date}}

茲收到:{{name}} 先生/小姐

金額:新台幣 {{price_chinese}} (NT$ {{price}})

(紅字是用 {{ }} 包起來的變數)
💡 ID 在網址列這裡:
https://docs.google.com/.../d/1rZSBQUyFX...你的ID...dM4/edit
  • Google Sheet: 建立新試算表,分頁名稱改為「收據紀錄」。
  • Google Drive 資料夾: 建立一個空資料夾,用來存生成的 PDF。
03
後端程式 (Code.gs)

這段是系統的大腦。請在 Google Sheet 點選「擴充功能」 > 「Apps Script」,將原本的內容清空,貼上這段代碼。

Code.gs
const CONFIG = {
  TEMPLATE_ID: '請填入_你的DOC範本_ID', 
  FOLDER_ID: '請填入_資料夾_ID',     
  SHEET_NAME: '收據紀錄'
};

function doGet() {
  return HtmlService.createHtmlOutputFromFile('index')
      .setTitle('自動開單系統')
      .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
      .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

// 核心處理函式
function processForm(data) {
  const lock = LockService.getScriptLock();
  if (!lock.tryLock(10000)) return { success: false, error: "系統忙碌中" };

  try {
    // 1. 準備資料
    const docFile = DriveApp.getFileById(CONFIG.TEMPLATE_ID);
    const folder = DriveApp.getFolderById(CONFIG.FOLDER_ID);
    const dateStr = data.date.replace(/-/g, '/'); 
    const priceInt = parseInt(data.price);
    const chinesePrice = toChineseMoney(priceInt); // 轉國字大寫

    // 2. 複製範本
    const filename = `收據_${data.name}_${dateStr}`;
    const newFile = docFile.makeCopy(filename, folder);
    const doc = DocumentApp.openById(newFile.getId());
    const body = doc.getBody();

    // 3. 替換變數 (對應 Word 裡的 {{變數}})
    body.replaceText("{{name}}", data.name);
    body.replaceText("{{date}}", dateStr);
    body.replaceText("{{item}}", data.item);
    body.replaceText("{{price}}", data.price);
    body.replaceText("{{price_chinese}}", chinesePrice);

    doc.saveAndClose(); 

    // 4. 寫入 Google Sheet
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    let sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
    if (!sheet) sheet = ss.insertSheet(CONFIG.SHEET_NAME);
    
    sheet.appendRow([
      new Date(),       // 建立時間
      data.name,        // 姓名
      data.item,        // 項目
      priceInt,         // 金額
      newFile.getUrl()  // 檔案連結
    ]);

    return { success: true, url: newFile.getUrl() };

  } catch (e) {
    return { success: false, error: e.toString() };
  } finally {
    lock.releaseLock();
  }
}

// 數字轉中文大寫工具
function toChineseMoney(n) {
  if (isNaN(n)) return "";
  const digit = ['零', '壹', '貳', '參', '肆', '伍', '陸', '柒', '捌', '玖'];
  const unit = [['元', '萬', '億'], ['', '拾', '佰', '仟']];
  let s = '';
  n = Math.abs(n);
  let integerPart = Math.floor(n);
  for (let i = 0; i < unit[0].length && integerPart > 0; i++) {
    let p = '';
    for (let j = 0; j < unit[1].length && integerPart > 0; j++) {
      p = digit[integerPart % 10] + unit[1][j] + p;
      integerPart = Math.floor(integerPart / 10);
    }
    s = p.replace(/(零.)*零$/, '').replace(/^$/, '零') + unit[0][i] + s;
  }
  return s.replace(/(零.)*零元/, '元').replace(/(零.)+/g, '零').replace(/^整$/, '零元整') + "整";
}
04
前端介面 (index.html)

在 Apps Script 左側新增一個 index.html 檔案。我加入了一點「玻璃擬態」風格,並預留了上方分頁籤的位置,讓系統看起來比較不呆板。

收據
繳費證明
治療紀錄

🧾 快速開立

index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
      body { 
        background: linear-gradient(135deg, #dbeafe, #eff6ff); 
        font-family: 'Noto Sans TC', sans-serif; padding: 20px;
        min-height: 100vh; display: flex; justify-content: center;
      }
      .container { width: 100%; max-width: 450px; }
      
      /* 玻璃擬態卡片 */
      .glass-card {
        background: rgba(255, 255, 255, 0.75);
        backdrop-filter: blur(12px);
        border: 1px solid rgba(255, 255, 255, 0.5);
        border-radius: 20px; padding: 30px;
        box-shadow: 0 10px 25px rgba(0,0,0,0.05);
      }
      
      input {
        width: 100%; padding: 12px; margin-bottom: 15px;
        border: 1px solid #fff; border-radius: 10px;
        background: rgba(255,255,255,0.6); font-size: 16px;
        box-sizing: border-box; transition: 0.3s;
      }
      input:focus { outline: none; border-color: #3b82f6; background: #fff; }
      
      button {
        width: 100%; padding: 14px; background: #2563eb; color: white;
        border: none; border-radius: 10px; font-size: 1.1rem; font-weight: bold;
        cursor: pointer; transition: transform 0.1s;
      }
      button:active { transform: scale(0.98); }
      
      .status-msg { margin-top: 15px; text-align: center; font-size: 0.9rem; }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="glass-card">
        <h2 style="text-align:center; color:#1e3a8a; margin-top:0;">🧾 收據開立</h2>
        <form id="mainForm">
          <label>姓名</label>
          <input type="text" name="name" required placeholder="請輸入姓名">
          
          <label>日期</label>
          <input type="date" name="date" required>
          
          <label>項目</label>
          <input type="text" name="item" required placeholder="例如:服務費">
          
          <label>金額</label>
          <input type="number" name="price" required placeholder="例如:2000">
          
          <button type="button" onclick="submitData(this)">確認開立</button>
        </form>
        <div id="status" class="status-msg"></div>
      </div>
    </div>

    <script>
      document.querySelector('input[type="date"]').valueAsDate = new Date();

      function submitData(btn) {
        btn.textContent = "處理中..."; btn.disabled = true;
        const form = document.getElementById('mainForm');
        
        google.script.run
          .withSuccessHandler(res => {
            btn.textContent = "確認開立"; btn.disabled = false;
            if (res.success) {
              document.getElementById('status').innerHTML = 
                `<span style="color:green">✅ 成功!<a href="${res.url}" target="_blank">下載文件</a></span>`;
              form.reset();
              document.querySelector('input[type="date"]').valueAsDate = new Date();
            } else {
              document.getElementById('status').innerHTML = `<span style="color:red">❌ 錯誤:${res.error}</span>`;
            }
          })
          .processForm(Object.fromEntries(new FormData(form)));
      }
    </script>
  </body>
</html>
05
進階篇:AI 詠唱指令 (Prompt)

上面的程式碼只是基礎版。如果你想要更完整的功能,不需要自己學寫程式!這裡提供我用來請 AI 修改的 Prompt (指令),你可以複製覺得好用的功能,直接去問 AI:

不想手打?輸入名字帶出資料
適合懶人:輸入姓名後,自動把身分證和電話填好。
我想要在 index.html 增加「搜尋建議」功能。 1. 請修改後端 Code.gs:新增 `getCustomerData()` 讀取 Sheet 中的姓名與身分證。 2. 請修改前端:當我在「姓名」輸入文字時,顯示下拉選單。 3. 點選後自動帶入身分證。
檔案自動按年份分類
避免所有 PDF 堆在同一個資料夾,讓系統自己整理。
請修改存檔邏輯。 我希望生成的檔案不要全部堆在同一個資料夾。 請依照今天的日期,自動建立「YYYY年」的資料夾。 如果該年份資料夾已存在,就直接存進去;不存在就建立新的。
開單後自動寄 Email 給客戶
超實用!產生 PDF 後直接寄到客戶信箱。
請修改 Code.gs 的 processForm 函式。 表單會多傳入一個 `email` 欄位。 請使用 MailApp.sendEmail,將生成的 PDF 檔案作為附件,自動寄送給該 email。 信件標題:「您的收據 - (日期)」。
傳送 LINE 通知給老闆
當有人開單時,自動用 LINE Notify 通知管理者。
請增加 LINE Notify 功能。 當收據開立成功後,呼叫 LINE Notify API。 傳送訊息:「有一筆新收據!金額:(金額),經手人:(姓名)」。 請教我如何申請 Token 並寫入程式碼中。
建立營收統計儀表板
在 Google Sheet 中自動畫出每月收入圖表。
請給我一段 Google Apps Script。 功能是讀取「收據紀錄」分頁的資料,並在一個新的分頁「Dashboard」中,建立一個長條圖。 統計每個月的總收入,並自動更新圖表。
06
寫在最後:為什麼要自己做系統?

這套系統目前對我來說很好用,但它絕對不是完美的。我整理了一些這個系統的「亮點」與「可以改進的地方」,希望能給你更多靈感去修改出專屬於你的版本:

👍 這套系統的巧思
👁️
隱私保護模式 我在介面上做了一個按鈕,可以把病患資料「模糊化」。這樣就算螢幕開著,也不怕被旁邊客人看到隱私。
🧮
自動邏輯計算 電腦最擅長算數。例如輸入 2000,自動轉成「貳仟元整」。能讓電腦算的,就不要用人腦算。
🔍
搜尋建議 輸入名字就自動帶出身分證,這省去了 80% 的打字時間。
🤔 你還可以怎麼改?(思考題)
📅
跟 Google Calendar 連動? 如果你是預約制,或許可以改成:選完日期後,自動讀取日曆的行程,把預約好的病人名字帶進來?
📊
數據分析儀表板? 除了記流水帳,或許可以讓 Sheet 自動畫出「每月營收趨勢圖」,讓你一眼看出淡旺季?
🔐
安全性升級? 如果要公開使用,建議加上簡易的密碼登入頁面,或是限制只有公司的 Google 帳號能存取。
🧑‍💻
一點點過來人的心裡話...

其實在做這個系統之前,我也想過:「直接買現成的 POS 系統不是比較快嗎?」

但現成的軟體,往往是「功能多到用不到」,或者「少了一個我最想要的關鍵功能」。自己用 AI 寫程式的好處,就是你可以100% 貼合自己的工作習慣

寫程式最難的一步,往往是「不知道從何問起」。希望這篇筆記提供的程式碼骨架和 Prompt,能成為你的起跑點。

不用怕把程式改壞,因為有 AI 在,隨時都能修好它。試著動手做做看,那種「電腦幫我省下 1 小時」的成就感,真的很棒!

如果你改出了更有趣的功能,歡迎留言跟我分享,我也很想知道這套系統還能進化成什麼樣子!✨

留言

這個網誌中的熱門文章

從 USPH 到 Upstream:物理治療企業化的啟示與思考

🚇捷運紅線物理治療所地圖懶人包|從北投到信義,讓復健更近一點

當物理治療變成一門「大生意」:我們該開心還是擔心?