Kon's DX Lab - Case Study

Day 58|PDF出力+Logiless連携!配送ストップFAX業務を一歩先へ

Published on 2025-05-29

🔬 Case Study Summary
Problem

(ここに課題を記述)

Result

(ここに具体的な成果を記述)


Tech & Process

(ここに採用技術とプロセスを記述) コードを詳しく見る »

こんにちは、こんです🦊

今日は、昨日に続き「配送ストップ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で業務効率化 #レガシー業務の自動化