2022年6月27日月曜日

ニラを眺めながら考えたこと

無限再生ニラ

今年は家庭菜園で無限再生ニラを栽培しております。食べた端から生えてきます。いいですね、これ。


農業の難易度をつり上げているのって「規格に合わせつつ収量の最大化をはかる」という部分が大きいんじゃないかと思っています。

産業として考えればもちろん逃げてはいけない部分なんですが、かと言ってじゃあ自分がその土俵で戦わなければいけない理由もないわけで、生業としてならばやりようはいくらでもあるんじゃないかという気がしています。6次化とか、兼業化とか、いろいろ。

生業として、というはつまり右肩上がりを目指すのではなく、概ねの着地点に向けて帳尻を合わせるということですね(分かりづらいですね、すみません笑)。


などということをニラを眺めながら考えたのですが、まぁ、焦らず浮つかず無理のないところでたたかうのが一番ですね。頑張りましょう。


Twitter (@nkkmd) 日々更新中です。

2022年6月25日土曜日

徒然じゃない日々(16)

梅雨に入ったと思ったら全然雨が降らなくなっていたので、一昨日はまさに恵みのしとしと雨でした。

今年の夏は猛暑になるようですね。お米や秋から使う椎茸のホダ木は管理を気をつけねばです。


そういえば、おとなりの畑のおばあちゃんから新じゃがいもを頂きました。蒸してバターで食べたらめちゃくちゃ美味しかったです。メークインです。うちでも作ろうかなぁ。

毎年夏は加工品作りに時間をとられて、畑を使い切れていなかったのですが(猫の額程のサイズにもかかわらず)、もう少し有効活用できたらなと思っています。

基本的に畑は加工用の材料を自家生産しつつ、ちょっと販売プラス自家消費用という使い方をしているのですが、今後は販売と自家消費の比率を上げるのもありかなと思ったり。

じゃがいもの場合はたぶん完全に自家消費用になりますね(笑)。


昨今の世の情勢を鑑みるに、これからあらゆる面でなかなか厳しい状況というのが続くものと思われます。しかしながら、そんな時にこそ淡々と普通に続く農園でありたいという思いが強くあります。


クレソン畑に鴨

クレソン畑に鴨が視察にやって来ました。流れがゆるやかな水場は生き物が集まりますね。


Twitter (@nkkmd) 日々更新中です。

2022年6月20日月曜日

無料で簡単に管理できる請求書等の受領システム(電子取引の保存要件に対応)

電子帳簿保存法の改正により、電子取引を電子データとして保存することが義務化されました。しかしながら、電子取引による請求書の発行も受領も月数件なので対応にコストをかけたくないというのが正直なところ……。

ということで、電子取引の保存要件に対応した請求書等の発行システムに続きまして、Google フォーム、スプレッドシート、GAS (Google Apps  Script) を利用して請求書等の受領システムを作成します。


受領システムについても、以下の3つの条件を満たす検索機能が必要となります。
  1. 取引年月日その他の日付、取引金額及び取引先を検索の条件として設定することができること。
  2. 日付又は金額に係る記録項目については、その範囲を指定して条件を設定することができること。
  3. 二以上の任意の記録項目を組み合わせて条件を設定することができること。
(国税庁「電子帳簿保存法一問一答【電子取引関係】」より)


主な機能は2つです。

① 請求書等受領機能

「請求書等(PDFファイル)を Google フォームからドライブへアップロード」(手動)
   ↓
「ファイル名の形式を変更して統一」(自動)
   ↓
「取引先履歴の保存」(自動)


② 検索機能

「取引履歴」を1~3の条件で検索できるようにします。


Google フォームをまるっと共有しようとしたところ、「閲覧者」としての共有はできない仕様みたいなので、以下いちからの作り方です。


▼ フォームの作成



まずは Google フォームを作成します。質問項目は4つです。

① 受領書類のアップロード(ファイルのアップロード)
「特定のファイル形式のみ許可」で PDF のみチェックしておきます。

② 金額(記述式テキスト(短文回答))
回答の検証を入れておきます。「正規表現」の「一致する」で ^(0|[1-9]\d*)$ とか、「数値」の「整数」でいいかと思います。

③ 取引先(プルダウン)
取引先の一覧を選択肢として作っておきます。数が多いようであればスプレッドシートから読み込むようにしてもいいかもしれません(こちら参照)。

④ 種別(ラジオボタン)
請求書、納品書、見積書、領収書等を選択肢として作っておきます。

全て必須項目です。

ファイルのアップロード先となるフォルダが自動的に作成されます。回答先となるスプレッドシートも作成しておきます。


▼ スプレッドシートの作成


フォームの回答先として作成されたスプレッドシートに手を加えます。

自動で作成される「フォームの回答 1」シートの他に「取引履歴」という名前でシートを追加します。

項目は「管理番号(A列)」「受領日(B列)」「金額(C列)」「取引先(D列)」「種別(E列)」「URL(F列)」となります。


▼ ファイルアップロード部分の GAS の説明


トリガーとして「フォーム送信時」を使うため、フォームからスクリプトを作成します。

まずはファイルのアップロード部分の GAS になります。

function getForm(e) {
  //回答を取得
  let itemResponses = e.response.getItemResponses();

  //ファイル名の生成
  let date = new Date();
  date = Utilities.formatDate(date, "JST", "yyyyMMdd");
  let amt = itemResponses[1].getResponse(); //金額
  let sup = itemResponses[2].getResponse(); //取引先
  let typ = itemResponses[3].getResponse(); //種別
  let fileName = date + '_' + sup + '_' + amt + '_' + typ;
  
  //アップロードファイルを取得
  let file = itemResponses[0];

  //ファイル名の変更
  let pdf = DriveApp.getFileById(file.getResponse());
  pdf.setName(fileName);

  //取引履歴の記録
  setTrHis(pdf);

  //マイドライブ下にアップロードされたファイルの削除
  dltMyFile();
}

//取引履歴の記録
function setTrHis(pdf) {
  Utilities.sleep(3 * 1000);

  const sp = SpreadsheetApp.openById("スプレッドシート ID");
  const formSh = sp.getSheetByName("フォームの回答 1");
  const hisSh = sp.getSheetByName("取引履歴");
  let formLastRow = formSh.getLastRow();
  let hisLastRow = hisSh.getLastRow();

  //ファームから回答の取得
  let formData = formSh.getRange(formLastRow, 1, 1, 5).getValues();
  formData = Array.prototype.concat.apply([], formData); //二次元配列を一次元配列へ

  //取引履歴の記録
  let hisData = [];
  let num = "";
  if(hisLastRow == 1) {
    num = 1;
  } else {
    num = hisSh.getRange(hisLastRow, 1).getValue() + 1;
  }
  hisData.push(num);
  hisData.push(Utilities.formatDate(formData[0], "JST", "yyyy/MM/dd"));
  hisData.push(formData[2]);
  hisData.push(formData[3]);
  hisData.push(formData[4]);
  hisData.push(pdf.getDownloadUrl());
  hisSh.getRange(hisLastRow + 1, 1, 1, 6).setValues([hisData]);
}

//マイドライブ下にアップロードされたファイルの削除
function dltMyFile() {
  const myDrive = DriveApp.getRootFolder();
  let file = myDrive.getFiles().next();
  file.setTrashed(true);
}
※ アップロード当日に検索されない不具合を修正しました。(2022-7-14)

31行目の「スプレッドシート ID」を書き換えてください。

アップロードされたファイルの名前は「日付_取引先_金額_種別.pdf」という形式になります。

指定したフォルダ以外にマイドライブ直下にもファイルがアップロードされるようなのでそちらは dltMyFile 関数で削除(ゴミ箱に移動)しています。マイドライブ直下に他のファイルがある場合はあらかじめ調整してください。

トリガーとして「フォーム送信時」を設定します。


これで、請求書等を受領した際にフォーム経由でドライブにアップロードすることでファイル名が統一フォーマットになり、取引履歴が記録されます。


▼ 検索部分の GAS の説明


次に検索部分の GAS です。取引履歴から検索します。

const sp = SpreadsheetApp.openById("スプレッドシート ID");
const hisSh = sp.getSheetByName("取引履歴");

let result = [];

function doGet() {
  let htmlIndex = HtmlService.createTemplateFromFile("index");

  htmlIndex.dataNum = dataNum();
  htmlIndex.dateList = dateList().join("");
  htmlIndex.csmList = csmList().join("");
  htmlIndex.result = result;

  return htmlIndex.evaluate().setTitle("受領済請求書等検索システム");
}

function doPost(e) {
  let y_from = e.parameter.y_from;
  let m_from = e.parameter.m_from;
  let d_from = e.parameter.d_from;
  let y_to = e.parameter.y_to;
  let m_to = e.parameter.m_to;
  let d_to = e.parameter.d_to;
  let amt_low = e.parameters.amt_low;
  let amt_up = e.parameters.amt_up;
  let csm = e.parameter.csm;
  let typ = e.parameter.typ;

  let htmlResult = HtmlService.createTemplateFromFile("index");

  let rowNum = []; //検索結果行

  //全ての行を取得
  for(let i = 2; i <= hisSh.getLastRow(); i++) {
    rowNum.push(i);
  }
  
  //種別→検索順1
  if(typ != "全選択") {
    let typFinder = hisSh.getRange("E2:E").createTextFinder(typ).findAll();
    for(let i in typFinder) {
      rowNum.push(typFinder[i].getRow());
    }
    //重複要素のみ残す
    rowNum = rowNum.filter(function (value, index, self) {
      return self.indexOf(value) === index && index !== self.lastIndexOf(value);
    });
  }

  //取引先→検索順2
  if(csm != "全選択") {
    let csmFinder = hisSh.getRange("D2:D").createTextFinder(csm).findAll();
    for(let i in csmFinder) {
      rowNum.push(csmFinder[i].getRow());
    }
    //重複要素のみ残す
    rowNum = rowNum.filter(function (value, index, self) {
      return self.indexOf(value) === index && index !== self.lastIndexOf(value);
    });
  }

  //金額→検索順3
  let amt = "";
  //下限
  if(amt_low != "") {
    for(let i in rowNum) {
      amt = hisSh.getRange(rowNum[i], 3).getValue();  
      //下限以下の行を削除
      if(amt >= amt_low) {
        rowNum.push(rowNum[i]);
      }
    }
    //重複要素のみ残す
    rowNum = rowNum.filter(function (value, index, self) {
      return self.indexOf(value) === index && index !== self.lastIndexOf(value);
    });
  }
  //上限
  if(amt_up != "") {
    for(let i in rowNum) {
      amt = hisSh.getRange(rowNum[i], 3).getValue();  
      //上限以上の行を削除    
      if(amt <= amt_up) {
        rowNum.push(rowNum[i]);
      }
    }
    //重複要素のみ残す
    rowNum = rowNum.filter(function (value, index, self) {
      return self.indexOf(value) === index && index !== self.lastIndexOf(value);
    });
  }

  //期間→検索順4
  let dtDate = "";
  let date = "";
  //期間・始まり
  date = new Date(y_from, m_from - 1, d_from);
  for(let i in rowNum) {
    dtDate = hisSh.getRange(rowNum[i], 2).getValue();  
    //開始日以前の行を削除    
    if(dtDate.getTime() >= date.getTime()) {
      rowNum.push(rowNum[i]);
    }
  }
  //重複要素のみ残す
  rowNum = rowNum.filter(function (value, index, self) {
    return self.indexOf(value) === index && index !== self.lastIndexOf(value);
  });
  //期間・終わり
  date = new Date(y_to, m_to - 1, d_to);
  for(let i in rowNum) {
    dtDate = hisSh.getRange(rowNum[i], 2).getValue(); 
    //終了日以降の行を削除    
    if(dtDate.getTime() <= date.getTime()) {
      rowNum.push(rowNum[i]);
    }
  }
  //重複要素のみ残す
  rowNum = rowNum.filter(function (value, index, self) {
    return self.indexOf(value) === index && index !== self.lastIndexOf(value);
  });

  rowNum.sort((a, b) => {return a - b;});

  result.push("<p><b>期間: </b>"+ Utilities.formatDate(new Date(y_from, m_from - 1, d_from), "JST", "yyyy/MM/dd") + " から " + Utilities.formatDate(new Date(y_to, m_to - 1, d_to), "JST", "yyyy/MM/dd") + " まで<br><b>金額: </b>" + amt_low + "円 以上 " + amt_up + "円 以下<br><b>取引先: </b>" + csm + "<br><b>種別: </b>" + typ + "</p><br>"); //検索条件
  result.push("<table><tr><th>管理番号</th><th>受領日</th><th>金額</th><th>取引先</th><th>種別</th><th>ファイル</th></tr>");
  for (let i = 0; i < rowNum.length; i++) {
    result.push("<tr><td>" + hisSh.getRange(rowNum[i], 1).getValue() + "</td>");
    result.push("<td>" + Utilities.formatDate(new Date(hisSh.getRange(rowNum[i], 2).getValue()), "JST", "yyyy/MM/dd") + "</td><td>");
    result.push(Array.prototype.concat.apply([], hisSh.getRange(rowNum[i], 3, 1, 3).getValues()).join("</td><td>"));
    result.push("<td><a href='" + hisSh.getRange(rowNum[i], 6).getValue() + "'>ダウンロード</a></td><td>");
    result.push("</td></tr>");
  }
  result.push("</table>");

  htmlResult.dataNum = dataNum();
  htmlResult.dateList = dateList().join("");
  htmlResult.csmList = csmList().join("");
  htmlResult.result = result.join("");

  return htmlResult.evaluate().setTitle("検索結果 - 受領済請求書等検索システム");
}

//登録数の作成
function dataNum() {
  let dataNum = 0;
  let lastRow = hisSh.getLastRow();
  for(let i = 2; i <= lastRow; i++){
    dataNum++;
  }
  return dataNum;
}

//期間の作成
function dateList() {
  let list = [];
  let date = "";
  //始まり(年)
  list.push("<select name='y_from'>");
  for(let i = 0; i < 8; i++) {
    date = new Date();
    date = Utilities.formatDate(new Date(date.setFullYear(date.getFullYear() - i)), "JST", "yyyy");
    if(i == 1) {
      list.push("<option value='" + date + "' selected>" + date + "</option>");
    } else {
      list.push("<option value='" + date + "'>" + date + "</option>");
    }
  }
  list.push("</select>" + "年 ");
  //始まり(月)
  list.push("<select name='m_from'>");
  for(let i = 0; i < 12; i++) {
    date = new Date();
    date = Utilities.formatDate(new Date(date.setFullYear(date.getFullYear() - i)), "JST", "MM");
    if((i + 1) == date) {
      list.push("<option value='" + (i + 1) + "' selected>" + (i + 1) + "</option>");
    } else {
      list.push("<option value='" + (i + 1) + "'>" + (i + 1) + "</option>");
    }
  }
  list.push("</select>" + "月 ");
  //始まり(日)
  list.push("<select name='d_from'>");
  for(let i = 0; i < 31; i++) {
    date = new Date();
    date = Utilities.formatDate(new Date(date.setFullYear(date.getFullYear() - i)), "JST", "dd");
    if((i + 1) == date) {
      list.push("<option value='" + (i + 1) + "' selected>" + (i + 1) + "</option>");
    } else {
      list.push("<option value='" + (i + 1) + "'>" + (i + 1) + "</option>");
    }
  }
  list.push("</select>" + "日 から ");

  //終わり(年)
  list.push("<select name='y_to'>");
  for(let i = 0; i < 8; i++) {
    date = new Date();
    date = Utilities.formatDate(new Date(date.setFullYear(date.getFullYear() - i)), "JST", "yyyy");
    if(i == 0) {
      list.push("<option value='" + date + "' selected>" + date + "</option>");
    } else {
      list.push("<option value='" + date + "'>" + date + "</option>");
    }
  }
  list.push("</select>" + "年 ");
  //終わり(月)
  list.push("<select name='m_to'>");
  for(let i = 0; i < 12; i++) {
    date = new Date();
    date = Utilities.formatDate(new Date(date.setFullYear(date.getFullYear() - i)), "JST", "MM");
    if((i + 1) == date) {
      list.push("<option value='" + (i + 1) + "' selected>" + (i + 1) + "</option>");
    } else {
      list.push("<option value='" + (i + 1) + "'>" + (i + 1) + "</option>");
    }
  }
  list.push("</select>" + "月 ");
  //終わり(日)
  list.push("<select name='d_to'>");
  for(let i = 0; i < 31; i++) {
    date = new Date();
    date = Utilities.formatDate(new Date(date.setFullYear(date.getFullYear() - i)), "JST", "dd");
    if((i + 1) == date) {
      list.push("<option value='" + (i + 1) + "' selected>" + (i + 1) + "</option>");
    } else {
      list.push("<option value='" + (i + 1) + "'>" + (i + 1) + "</option>");
    }
  }
  list.push("</select>" + "日 まで");
  return list;
}

//取引先の作成
function csmList() {
  let list = [];
  let lastRow = hisSh.getLastRow();
  if(lastRow > 1) {
    let csm = hisSh.getRange(2, 4, lastRow -1, 1).getValues(); //全取引先を取得
    csm = Array.prototype.concat.apply([], csm); //二次元配列を一次元配列へ
    //重複要素を削除
    csm = csm.filter(function (value, index, self) {
      return self.indexOf(value) === index;
    });
    for(let i = 0; i < csm.length; i++){
      list.push("<option value='" + csm[i].replace("'", "&#039;") + "'>" + csm[i].replace("'", "&#039;") + "</option>"); //シングルクオーテーションがあった場合はHTML特殊文字に変換
    }
  }
  return list;
}

1行目の「スプレッドシート ID」を書き換えてください。


HTML も作ります。ファイル名は index.html とします。

<!DOCTYPE html>
<html>
  <head>
    <base target="_top"></base>
  </head>
  <body>
    <center><h1><a href="ウェブアプリ URL">受領済請求書等検索システム</a></h1></center>
    <p>
      登録数: 件
    </p>
    <form action="ウェブアプリ URL" method="post">
      <p><b><期間></b><br />
        
      </p>
      <p><b><金額></b><br />
        <input name="amt_low" size="15" type="text" />円 以上 <input name="amt_up" size="15" type="text" />円 以下
      </p>
      <p><b><取引先></b><br />
        <select name="csm">
          <option value="全選択">全選択</option>
          
        </select>
      </p>
      <p><b><種別></b><br />
       <input checked="checked" name="typ" type="radio" value="全選択" />全選択
        <input name="typ" type="radio" value="請求書" />請求書
        <input name="typ" type="radio" value="納品書" />納品書
        <input name="typ" type="radio" value="見積書" />見積書
        <input name="typ" type="radio" value="領収書" />領収書
      </p>
      <p>
        <input type="submit" value="検索" />
      </p>
    </form>
    <p>
      
    </p>
  </body>
</html>

7行目 <a> タグ、11行目 <form> タグの「ウェブアプリ URL」を書き換えてください。

ウェブアプリ URL は GAS をウェブアプリとしてデプロイすると得られます。公開はしないのでアクセスできるユーザーは「自分のみ」としておきます。


検索画面はこんな感じになります。



***

必要があればお使いください。カスタマイズもご自由にどうぞ。


・関連投稿
無料かつ時短になる請求書等の発行システム(電子取引の保存要件に対応)


詳解! Google Apps Script完全入門 [第3版]
詳解! Google Apps Script完全入門 [第3版]

by SimpleImageLink


Twitter(@nkkmd)日々更新中です。

2022年6月18日土曜日

無料かつ時短になる請求書等の発行システム(電子取引の保存要件に対応)

-----
・追記(2022-6-22)
請求書、納品書、見積書のテンプレートを修正しました。
-----

電子帳簿保存法の改正により、電子取引を電子データとして保存することが義務化されました。しかしながら、電子取引による請求書の発行も受領も月数件なので対応にコストをかけたくないというのが正直なところ……。

ということで、電子取引の保存要件に対応した請求書等の発行システムを作成してみました。

私だけではなく少なからずそんな方々がおられると思いますので、わずかながらでもお役に立てば幸いです。


まず、電子取引の保存要件として「真実性の確保」と「可視性の確保」の2つを満たす必要があります。

「真実性の確保」についてはいくつかの選択肢がありますが、導入コストのかからない事務処理規定を定めることで対応します。

問題は「可視性の確保」です。以下の検索機能が必要となります。
  1. 取引年月日その他の日付、取引金額及び取引先を検索の条件として設定することができること。
  2. 日付又は金額に係る記録項目については、その範囲を指定して条件を設定することができること。
  3. 二以上の任意の記録項目を組み合わせて条件を設定することができること。
(国税庁「電子帳簿保存法一問一答【電子取引関係】」より)


素直にスプレッドシートの機能だけですと対応しようとすると、都度手作業が発生するため好ましくありません。

ということで、スプレッドシートと GAS (Google Apps Script) を用いて検索機能を備えた請求書等の発行システムを作成します。

(受領する請求書等についてはまた別に投稿予定です)


主な機能は2つです。

① 請求書等発行機能

「請求書等の作成」(テンプレートに入力)
   ↓
「PDFファイルの作成」(自動)
   ↓
「取引履歴の保存」(自動)

という流れで請求書等を発行。


② 検索機能

「取引履歴」を1~3の条件で検索できるようにします。


▼ スプレッドシートの作成


請求書テンプレート


PC で Google アカウントにログインした状態で、上記リンク先にて「ファイル」から「コピーを作成」で編集可能なファイルが作成されます。

時々、アクセス権のリクエストをいただきますが、↑の方法で編集可能なコピーを作成してご使用ください。

スプレッドシートには請求書等のテンプレート他と GAS のスクリプトファイルが含まれます。GAS は一部書き換えが必要です(後述)。


▼ スプレッドシートの説明


・「請求書」「納品書」「見積書」シート
請求書、納品書、見積書のテンプレートです。 
「宛名(A3:B3)」「敬称(C3)」「日付(F4)」「品名(A13:B13 〜 A24:B24)」「数量(E13 〜 E24)」を入力します(請求書は「振込期日(B9:C9)」と「振込先(B10:C10)」も)。 
H13 〜 H24で商品と取引先の組み合わせが正しいかをチェックするようになっています。 

・「商品」シート
「商品(A列)」「税率(B列)」「単価(C列)」「取引先(D列)」を入力しておきます。 
「請求書」「納品書」「見積書」シートの「品名」をこのシートから読み込みます。

・「取引先」シート
「取引先(A列)」を入力しておきます。 
「請求書」「納品書」「見積書」シートの「宛名」をこのシートから読み込みます。「商品」シートの「取引先」と一致しているかを確認するので表記がブレないようにします。

・「参照」シート
「振込先(B4 〜 D4)」を入力しておきます(デフォルトで3つなので適宜増減してください)。「日付(B2 〜 H2)」は自動で3日前から3日後まで入力されます。 
「請求書」「納品書」「見積書」シートの「日付」と「振込先」をこのシートから読み込みます。

・「取引履歴」シート 
請求書、納品書、見積書のPDFが作成されると「管理番号(A列)」「発行日(B列)」「金額(C列)」「取引先(D列)」「種別(E列)」「URL(F列)」が記録されます。 
このシートから検索を行います。


▼ 請求書等発行部分の GAS の説明


PDF作成.gs の1、2行目の「フォルダ ID」と「スプレッドシート ID」を書き換えてください。

フォルダ ID は作成された PDF を保存する場所のものになります。

請求書のPDF作成の関数が pdfInv、納品書が pdfDn、見積書が pdfQuot となります。実行すると指定したフォルダ内にPDFが作成され取引履歴が記録されます。

マクロをインポートして、ショートカットキーを設定しておくと便利です。


▼ 検索部分の GAS の説明


検索.gs の1行目のスプレッドシート ID、index.html の7行目 <a> タグ、11行目 <form> タグの「ウェブアプリ URL」を書き換えてください。

ウェブアプリ URL は GAS をウェブアプリとしてデプロイすると得られます。公開はしないのでアクセスできるユーザーは「自分のみ」としておきます。


検索画面はこんな感じ。装飾等は一切していないので簡素です。

発行済請求書等検索システム


検索すると条件と結果が表示されます(下の画像では結果は消してあります)。

発行済請求書等検索システム(検索結果)


***

必要があればお使いください。カスタマイズもご自由にどうぞ。


・関連投稿


詳解! Google Apps Script完全入門 [第3版]
詳解! Google Apps Script完全入門 [第3版]

by SimpleImageLink


Twitter(@nkkmd)日々更新中です。

2022年6月6日月曜日

【GAS】2週間気温予報のデータをグラフ化して LINE で画像メッセージとして配信する

最近の天気は寒暖差が激しすぎて訳が分からないですね。

気象庁から提供されている2週間気温予報のデータを視覚的に一目で把握できるようにするため、スプレッドシートでグラフ化 → 画像メッセージとして LINE で配信する GAS(Google Apps Script)を作ってみました。


▼ スプレッドシートの準備

2週間気温予報のデータの入力、及びグラフ作成のためのスプレッドシートを準備します。

グラフの見出し用にB1からM1に順に「平均気温(予報)」「最高気温(予報)」「最低気温(予報)」「平均気温(平年値)」「最高気温(平年値)」「最低気温(平年値)」「平均気温(過去10年)」「最高気温(過去10年)」「最低気温(過去10年)」「平均気温(昨年)」「最高気温(昨年)」「最低気温(昨年)」と入力しておきます。

予報、平年値、過去10年、昨年についてそれぞれ平均気温、最高気温、最低気温なので計12列になります。


▼ スクリプトの作成

まずは2週間気温予報のデータの取得、グラフの作成・保存、LINEへのグラフ画像URLの受け渡し、グラフの削除(ゴミ箱移動)を行うためのスクリプトです。

今回はとりあえず予報についてのみ簡単な折れ線グラフを作成、配信するようになっています。

2週間気温予報のデータの取得には自作の Web API を利用しています。

function getTwoweekData() {
  const spreadsheet = SpreadsheetApp.openById("スプレッドシート ID");
  const twSheet = spreadsheet.getSheetByName("twoweek");

  //JSON データの取得
  let point = "地域(地点)番号";
  let url = "https://api.cultivationdata.net/twoweek?point=";
  url = url + point;
  let response = "";
  try {
    response = UrlFetchApp.fetch(url);
  } catch(e) {
    return response;
  }
  let data = JSON.parse(response.getContentText());

  //スプレッドシートに初期日と予報期間を入力
  let date = new Date([data["date"][0]]);
  let fcDate = new Date(date.setDate(date.getDate() + 2));
  let period = [];
  for(i = 0; i < 13; i++) {    
    period.push([Utilities.formatDate(fcDate, "JST", "MM/dd")]);
    fcDate = new Date(fcDate.setDate(fcDate.getDate() + 1));
  }
  twSheet.getRange(2, 1, 13, 1).setValues(period);

  //スプレッドシートに気温を入力
  let keys = Object.keys(data); 
  let twoweek = [];
  let temp = [];
  for(let x = 0; x < 13; x++) {
    for(let y = 1; y < 5; y++) {
      temp.push([data[keys[y]]["ave" + String(x)][0]]);
      temp.push([data[keys[y]]["hi" + String(x)][0]]);
      temp.push([data[keys[y]]["low" + String(x)][0]]);
    }
    twoweek.push(temp);
    temp = [];
  }
  twSheet.getRange(2, 2, 13, 12).setValues(twoweek);

  //折れ線グラフを作成
  const range = twSheet.getRange(1, 1, 14, 4);
  let chart = twSheet.newChart()
                .addRange(range)
                .asLineChart()
                .setNumHeaders(1)
                .setPosition(16,2,0,0)
                .setOption("title", "2週間気温予報")
                .build();
  twSheet.insertChart(chart);

  //折れ線グラフをドライブに保存
  let graph = twSheet.getCharts();
  let graphImg = graph[0].getBlob();
  let folder = DriveApp.getFolderById("フォルダ ID");
  let file = folder.createFile(graphImg.setName("graph"));
  file.setSharing(DriveApp.Access.ANYONE, DriveApp.Permission.VIEW);

  //LINE 配信
  push(file.getDownloadUrl());

  //折れ線グラフの削除
  twSheet.removeChart(graph[0]);
  folder.getFilesByName("graph").next().setTrashed(true);
}
※ Web API の URL(7行目)が変更になりました。

2行目の「スプレッドシート ID」、6行目の「地域(地点)番号」、56行目の「フォルダ ID」を記載します。フォルダはグラフ画像を一時的に保存する場所になります(LINE 配信後にゴミ箱に入ります)。

また、シート名は「twoweek」、作成するグラフ名は「graph」としています(適宜変更してください)。


つぎに LINE で画像メッセージをプッシュ通知で配信するためのスクリプトです。

const ACCESS_TOKEN = "チャンネルアクセストークン";

const PUSH = "https://api.line.me/v2/bot/message/push";

function push(img) {
  let postData = {
    "to" : "LINE ユーザー ID",
    "messages" : [
      {
        "type" : "image",
        "originalContentUrl" : img,
        "previewImageUrl" : img
      }
    ]
  };
  
  // リクエストヘッダ
  const headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    "Authorization" : "Bearer " + ACCESS_TOKEN
  };
  
  // POSTオプション作成
  const options = {
    "method" : "POST",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(PUSH, options);
}

1行目の「チャンネルアクセストークン」、7行目の「LINE ユーザー ID」を記載します。

LINE のユーザー ID は LINE ID とは異なります。不明な場合はこちら↓を参考に取得してください。



ということで、ひとまず完成です。


▼ 送ってみる


getTwoweekData 関数を実行することで、こんな感じで画像メッセージが配信されます。

今日とても寒いですが、一応今後上がっていく予報ですね。よかった……。

あとは、平年値との差なんかも分かるようにしたいですね。

トリガー設定をして、毎朝送られてくるようにしようかと思います。


***

LINE で画像メッセージをおくるためには JPG もしくは PNG ファイルの URL を指定しなければならないのですが、Google ドライブ内の画像ファイルから LINE で送信可能な URL をどう取得すればいいの分からず迷宮に入り込みました。

getUrl() ではだめ。結局、getDownloadUrl() でいけました。取得できる URL は JPG や PNG の拡張子付きではないのですが、なぜか大丈夫です。

自動でグラフ化からの画像メッセージはけっこう便利そうなので、覚えておくといろいろ使いどころがありそうです。


詳解! Google Apps Script完全入門 [第3版]
詳解! Google Apps Script完全入門 [第3版]

by SimpleImageLink


・関連投稿
【GAS】気象庁 API から取得した天気予報を LINE にプッシュ通知する

2週間気温予報のデータを JSON 形式で取得する Web API を作りました。



Twitter(@nkkmd)日々更新中です。

2022年6月3日金曜日

徒然じゃない日々(15)

肥料の高騰が報道されていましたね。ものによっては90%以上の値上げになっているようです。

肥料が高騰した場合、さわで影響を受けるのは主にお米です。

農水省の資料(18年産)によると、お米の生産コスト(自己資本利子・自作地地代・家族労働費除く)における肥料費の割合は概ね10%です。仮に価格が2倍になった場合、トータルコストは1.1倍になるということですね。その他、光熱動力費等のコストも上がっていきそうです。

1.1倍と言うと大した事なさそうですが、米価は下落していますから、状況はますます厳しく……といったところです。いや、実際10%増はけっこう大変です。100万円だったものが110万円ですからね。

そもそも主食用米については需給バランスが崩れダブついていますから、国内の農業全体の構造も変化させていかなければいけない状況なのですよね。

こうした中で一経営体としてどう舵を取っていくか……。さわでは、直接販売させて頂いているお客様の分は確保しつつ、加工用米(もち米)への転作を進めています。


話が逸れましたが肥料の方に戻りまして……一方で畑は、化成肥料をほぼ使用していませんので影響は限定的です。今後とも身の回りの資材を有効活用していきたいと思っています。具体的には米糠、もみ殻、廃ホダ木、廃菌床、雑草あたりですね。本を引っ張り出して改めて土の勉強しています。


変化というのは起こるときは急激ですね。農業に限らずでしょうが、なかなか難しい時代です。

しかしながら、だからこその面白味もありますし、今になって何年も前からの準備が活きている実感もあります。

できることをコツコツやっていくしかないですね。


施設栽培


Twitter(@nkkmd)日々更新中です。