
こんにちは、こんです🦊
今日は、昨日に続き「配送ストップFAXの業務自動化」です。前回は、GASでPDF生成機能を実装し、FAX依頼書を1クリックで出力できるようになりました。今回はそこからさらに一歩進めて、「Logilessと連携して配送情報を自動で取得」「生成したPDFファイルのURLを一覧に自動反映する」といった実装に取り組みました!
実験のきっかけ:「結局これ、誰が送ったの?」問題
FAXを出したかどうかは、PDFを見ないと分からない
配送済みかどうかは、Logilessを開かないと分からない
進捗管理がスプレッドシートと連携していない
こうした“情報の分断”が現場で頻繁に起きていたことがきっかけです。「一覧シートがあるのに、FAX送信状況も配送状況も結局別の場所で確認するしかないの?」という疑問からスタートしました。
作ってみたもの(システムの概要)
今回は、以下の2ステップの改善を行いました:
✅ 1.PDF出力後にURLをスプレッドシートに自動反映
「受注番号あり、かつ、PDF未生成」の行のみ抽出対象
PDF出力後、一覧の該当行に「PDF生成日」「PDFファイルURL」を自動入力
URLはGoogle Drive上のPDFファイル直リンクなので、いつでも確認可能
✅ 2.Logilessから配送情報をAPI経由で自動取得
「受注番号あり、かつ、出荷日・伝票番号・氏名が未入力」の行のみを対象
Logiless APIの /sales_orders/search エンドポイントに、受注番号の配列をPOST
取得した finished_at、delivery_tracking_numbers、recipient_name1 をスプレッドシートに反映
どちらの処理も、Google Apps Scriptのメニューからボタンで実行できる形に整えました!
実感したメリット
1. PDFファイルのトラッキングが一元管理できるように
これまで「誰が、どこまで出したのか?」が不透明だったのが、PDF出力と同時に「PDF生成日」「ファイルURL」を一覧に書き込むことで、進捗が“見える化”されました。
しかも、URLから直接PDFファイルを確認できるため、再出力や確認作業も楽ちん。
2. Logiless連携で「手動コピペ」がゼロに
「出荷日は?」「伝票番号は?」「宛名は?」といった情報を、毎回Logiless管理画面からコピペしていた作業が完全になくなりました。API連携により、1クリックで最大100件まで一括取得できるのが最高です。
3. 対象行だけに絞った“限定処理”で無駄がない
毎回すべての行を処理するのではなく、「PDF未生成の受注分」「Logiless未取得の受注分」に限って実行するようにしたことで、処理時間も短く、誤操作も防げる設計になっています。
シートイメージ
【配送停止依頼一覧】:スプレッドシートで、PDFステータスや出荷状況を一元管理
【テンプレート】:1枚に15件の依頼情報を載せたFAX送信用テンプレート
【PDF URL列】:出力されたPDFファイルへのDriveリンクを自動挿入
【Logiless連携ボタン】:氏名・出荷日・伝票番号をLogilessから自動取得
技術構成
使用技術:Google Apps Script、Logiless API(OAuth2認証)
主な関数:
exportStopRequestsAsPDF():PDFを自動生成し、一覧に反映
updateShippingInfoFromLogiless():Logilessから配送情報を取得して一覧に反映
onOpen():メニューUI構築(認証関連メニュー付き)
PDFの命名やレイアウト制御には UrlFetchApp による直接PDFエクスポートURLを使い、テンプレートを複数ページ分複製して統合出力しています。
exportStopRequestsAsPDF()
function exportStopRequestsAsPDF() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const listSheet = ss.getSheetByName("配送停止依頼一覧");
const templateSheet = ss.getSheetByName("テンプレート");
const data = listSheet.getDataRange().getValues();
const rows = data.slice(1);
const today = new Date();
const yymmdd = Utilities.formatDate(today, "Asia/Tokyo", "yyMMdd");
const todayStr = Utilities.formatDate(today, "Asia/Tokyo", "yyyy/MM/dd");
// 📌 「受注番号あり」かつ「PDF生成日なし」の行のみ対象
const targetRows = rows
.map((row, i) => ({ row, index: i + 1 }))
.filter(({ row }) => row[1] && !row[7]);
if (targetRows.length === 0) {
SpreadsheetApp.getUi().alert("PDF生成対象のデータがありません。");
return;
}
const maxRowsPerPage = 15;
const totalPages = Math.ceil(targetRows.length / maxRowsPerPage);
const tempSpreadsheet = SpreadsheetApp.create("一時PDFファイル");
const defaultSheet = tempSpreadsheet.getSheets()[0];
for (let p = 0; p < totalPages; p++) {
const sheet = templateSheet.copyTo(tempSpreadsheet);
sheet.setName(`依頼_${p + 1}`);
sheet.getRange("I3").setValue(`${p + 1}/${totalPages}枚目`);
sheet.getRange("I4").setValue(todayStr);
const pageRows = targetRows.slice(p * maxRowsPerPage, (p + 1) * maxRowsPerPage);
pageRows.forEach(({ row }, i) => {
const bRow = 33 + i;
sheet.getRange(`C${bRow}`).setValue(row[3]); // 出荷日
sheet.getRange(`E${bRow}`).setValue(row[4]); // 伝票番号
sheet.getRange(`G${bRow}`).setValue(row[5]); // 氏名
});
}
tempSpreadsheet.deleteSheet(defaultSheet);
const exportUrl = `https://docs.google.com/spreadsheets/d/${tempSpreadsheet.getId()}/export?exportFormat=pdf&format=pdf` +
`&size=A4&portrait=true&fitw=true&scale=4&sheetnames=false&printtitle=false&gridlines=false` +
`&top_margin=0.5&bottom_margin=0.5&left_margin=0.5&right_margin=0.5`;
const token = ScriptApp.getOAuthToken();
const response = UrlFetchApp.fetch(exportUrl, {
headers: { Authorization: 'Bearer ' + token }
});
const blob = response.getBlob().setName(`${yymmdd}_配送停止依頼FAX_${targetRows.length}件.pdf`);
const folder = DriveApp.getFolderById("YOUR_FOLDER_ID");
const file = folder.createFile(blob);
const pdfUrl = file.getUrl();
targetRows.forEach(({ index }) => {
listSheet.getRange(index + 1, 7).setValue('PDF生成済');
listSheet.getRange(index + 1, 8).setValue(todayStr);
listSheet.getRange(index + 1, 11).setValue(pdfUrl);
});
DriveApp.getFileById(tempSpreadsheet.getId()).setTrashed(true);
SpreadsheetApp.getUi().alert("PDF出力と一覧更新が完了しました。");
}
updateShippingInfoFromLogiless()
function updateShippingInfoFromLogiless() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("配送停止依頼一覧");
const rows = sheet.getDataRange().getValues().slice(1);
const service = getOAuthService();
if (!service.hasAccess()) {
showDialog("LOGILESS APIの認証が必要です。", "認証エラー");
return;
}
const codeCol = 1, shipCol = 3, trackCol = 4, nameCol = 5;
const target = rows
.map((row, i) => ({ code: row[codeCol], index: i + 2, row }))
.filter(({ code, row }) => code && !row[shipCol] && !row[trackCol] && !row[nameCol])
.slice(0, 100);
if (target.length === 0) {
SpreadsheetApp.getUi().alert("取得対象のデータがありません。");
return;
}
const payload = JSON.stringify({ codes: target.map(t => t.code) });
const apiUrl = `https://app2.logiless.com/api/v1/merchant/YOUR_MERCHANT_ID/sales_orders/search`;
const headers = {
Authorization: `Bearer ${service.getAccessToken()}`,
'Content-Type': 'application/json'
};
const res = UrlFetchApp.fetch(apiUrl, { method: 'post', headers, payload });
const data = JSON.parse(res.getContentText()).data || [];
const map = {};
data.forEach(o => map[o.code] = {
finished: o.finished_at ? formatLogilessDate(o.finished_at) : "",
track: o.outbound_deliveries?.[0]?.delivery_tracking_numbers?.[0] || "",
name: o.recipient_name1 || o.recipient_name_1 || o.buyer_name1 || ""
});
target.forEach(({ code, index }) => {
const r = map[code];
if (r) {
sheet.getRange(index, shipCol + 1).setValue(r.finished);
sheet.getRange(index, trackCol + 1).setValue(r.track);
sheet.getRange(index, nameCol + 1).setValue(r.name);
}
});
SpreadsheetApp.getUi().alert(`${target.length}件の配送データを更新しました。`);
}
function formatLogilessDate(str) {
try {
return Utilities.formatDate(new Date(str.replace(" ", "T")), "Asia/Tokyo", "yyyy/MM/dd");
} catch {
return str;
}
}
onOpen()
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu("マクロ")
.addItem("配送ストップPDF生成(受注番号・PDF生成日を条件)", "exportStopRequestsAsPDF")
.addItem("ロジレスから関連データを取得", "updateShippingInfoFromLogiless")
.addSubMenu(
ui.createMenu("LOGILESS API認証")
.addItem("初回認証を行う", "authorize")
.addItem("アクセストークンを更新", "refreshAccessTokenFromMenu")
.addItem("認証状況を確認", "checkAccess")
.addItem("認証をリセット", "reset")
)
.addToUi();
}
次回予告:FAX送信の自動化にも挑戦!
今回で「PDF生成」までは完了しましたが、次はその先——つまり「FAX送信」自体の自動化にも踏み込んでみたいと思います!
eFaxやKDDIペーパーレスFAXなどのAPI調査
PDF出力後に自動送信 → ステータス更新の構築
送信履歴やエラーのログ収集
FAX文化をすぐに変えることは難しくても、その“運用負荷”は減らせるはず。引き続き実験していきます!
まとめ:Logiless×GASで“紙業務”がデジタル化!
配送ストップFAXという、一見すると「システム化なんて無理そう」なアナログ業務でも、
テンプレート+スプレッドシート+API連携+GASがあれば、
人の手を動かさずにPDF出力と進捗管理を自動化することが可能です。
ちょっとしたひと工夫とChatGPTのサポートで、現場の負担をグッと減らせる。
そんな手応えを感じた1日でした。
それでは、また次の実験で!🦊
#業務改善 #GAS #ノーコード #ChatGPTで業務効率化 #レガシー業務の自動化