🍊

Teratail/370876 (別シートから検索・コピーした値を蓄積し続けるGASを書きたい)

2021/12/09に公開2

derived from

https://teratail.com/questions/370876

requirement

  • CRM から Google SpreadSheet に出力が行なわれるデータを IMPORTRANGE で参照するシートをマスターとする
  • マスターから 1 列目の担当ごとに顧客情報を分配する
  • 参照元の変更に応じて随時更新する

sample

  • マスターとなるシート
A列 B列 C列 D列 E列
1行 担当者 顧客名 フリガナ 性別 顧客登録日
2行 担当A 田中 タナカ 09/21
3行 担当B 鈴木 スズキ 10/01
4行 担当A 佐藤 サトウ 10/04
5行 担当B 山田 ヤマダ 11/12
  • 分配後のシート
A列 B列 C列 D列 E列
1行 担当者名: 担当A ▼顧客登録日
2行 担当A 田中 タナカ 09/21
3行 担当A 佐藤 サトウ 10/04

hypothesis

  • 部分的な入力はない(A-E 列のうち A 列だけが入力されることは起きない)
  • 顧客名はユニーク
  • 担当の変更がある
  • 担当と顧客名以外が変更され得る
  • マスターの記載順は変更されず、末尾に追記が起こる
    • ただし担当は変更され得る
  • マスターに空行や表外の行・列への入力は存在しない
  • 担当がいなくなったり、新たな担当が出現したりする
  • 分配後のシートで A-E 列のデータの中間に空欄が存在し得る

code

const inputSheetName = `検索用シート`;
const invalidColour = `#444444`;
const resetColour = null;
const inChargeSheetName = (name) => `${name}用の蓄積シート`;
const isInChargeSheetName = (name) => name.endsWith(`用の蓄積シート`);
const whoInChargeFromSheetName = (name) => name.replace(`用の蓄積シート`,``);

const onChange = (e) => {
    if(e.changeType !== "OTHER") return;
    const spreadsheet = SpreadsheetApp.getActive();
    const mappedData = MapToWhoInCharge(spreadsheet.getSheetByName(inputSheetName).getDataRange().getValues().slice(1));
    const sheetNames = spreadsheet.getSheets().reduce((a,c)=>a.concat(fillInBySheet(c,mappedData)),[]).filter(e=> e !== undefined);
    [...mappedData.keys()].filter(e=>!sheetNames.includes(e)).forEach(e=>fillInByName(e,mappedData.get(e)));
}
const MapToWhoInCharge = (data) => new Map([...new Set(data.map(([e])=>e)).values()].reduce((a,c)=> [...a,[c,data.filter(([e])=>e===c)]],[]));
const fillInBySheet = (sheet, map) => {
    const [isProperSheet, person] = tryGetWhoInCharge(sheet,map);
    if(!isProperSheet) return;
    fillIn(sheet, map.get(person));
    return person;
}
const fillIn = (sheet, current) => {
    sheet.setTabColor(resetColour);
    const toRowNumberOffset = 2;
    const existing = sheet.getDataRange().getValues();
    const tail = existing.reduce((a,[c],i)=>c !== "" ? i+1 : a,existing.length);
    sheet.getRange(1,1,tail,5).setBackground(resetColour);
    const previous = existing.slice(1,tail).map(e=>e.slice(0,5));
    const previousClients = new Set(previous.filter(([e])=> e !== "").map(([,e])=>e));
    const currentClients = new Map([...new Set(current.filter(([e])=> e !== "").map(([,e])=>e)).values()].reduce((a,c)=> [...a,[c,current.filter(([,e])=>e===c)]],[]));
    previous.reduce((a,[,c],i)=>currentClients.has(c)?a:a.concat(i+toRowNumberOffset),[]).forEach(r=>sheet.getRange(r,1,1,5).setBackground(invalidColour));
    const appendRows = current.reduce((a,c)=>previousClients.has(c[1])?a:a.concat([c]),[]);
    if(appendRows.length > 0) {sheet.insertRowsBefore(tail+1,appendRows.length);} // insertRowsAfter だと最終行を invalidColour で塗り潰したとき前の行の色を引き継いでしまう。逆に下端外に色があると insertRowsBefore でも引き継ぐ。下端外に余分な入力があるパターン以外は考慮しないから、先に insertRowsAfter してから、塗り潰しをすべきかも。
    const willBe = previous.map(e=>currentClients.has(e[1])?currentClients.get(e[1]).flat():e).concat(appendRows);
    sheet.getRange(2,1,willBe.length,5).setValues(willBe);
}
const tryGetWhoInCharge = (sheet, map) => {
    const sheetName = sheet.getSheetName();
    if(!isInChargeSheetName(sheetName)) return [false,undefined];
    const person = whoInChargeFromSheetName(sheetName);
    if(!map.has(person)) {
        sheet.setTabColor(invalidColour);
        return [false, undefined];
    }
    return [true, person];
}
const fillInByName = (name, itsData) => {
    const sheet = prepareInChargeSheetByName(name);
    fillIn(sheet, itsData);
    return name;
}
const prepareInChargeSheetByName = (name) => {
    const sheetName = inChargeSheetName(name);
    const existingSheet = SpreadsheetApp.getActive().getSheetByName(sheetName);
    if(existingSheet !== null) {
        return existingSheet;
    }
    const newSheet = SpreadsheetApp.getActive().insertSheet(sheetName);
    newSheet.appendRow(`担当者名:\t${name}\t\t\t▼顧客登録日`.split(`\t`))
    return newSheet;
}
const setTrigger = () => {
    ScriptApp.newTrigger("onChange").forSpreadsheet(SpreadsheetApp.getActive()).onChange().create();
}
const initializer = () => {
    const dat = `担当者\t顧客名\tフリガナ\t性別\t顧客登録日
担当A\t田中\tタナカ\t男\t09/21
担当B\t鈴木\tスズキ\t女\t10/01
担当A\t佐藤\tサトウ\t女\t10/04
担当B\t山田\tヤマダ\t男\t11/12`;
    const inputSheet = SpreadsheetApp.getActive().getSheetByName(inputSheetName)?.clear() ?? SpreadsheetApp.getActive().insertSheet(inputSheetName);
    const value = dat.split(`\n`).map(e=>e.split(`\t`));
    inputSheet.getRange(1,1,value.length,value[0].length).setValues(value);
    SpreadsheetApp.getActive().getSheets().forEach(s=> {if(s.getSheetName() !== inputSheetName) {SpreadsheetApp.getActive().deleteSheet(s);}});
}

next action

  • 業務の詳細な確認
    プログラム的な難点はない。
    仮定が適切であるか、業務に適応できるかの確認が必要
  • そもそも CRM で解決できないか
    kintone などアプリが作れる CRM がある。単にメモ項目が用意されるだけでもよいのかもしれない。
    想定手法の妥当性。例えば CRM から SpreadSheet に連携されるなら、そもそも Google Apps Script でそのシートを読めば良いので、IMPORTRANGE するだけの "検索用シート” は不要。そのようなことをする合理的な理由があるのだとすれば未知の業務要件があるのかもしれない。
    Google Apps Script の限界として、IMPORTRANGE の変更は onChange でしかキャッチできない。これは編集行為のたびにスクリプトが起動することを意味する。動的に更新とのことだが、顧客がそんなに頻繁に追加されるのか、定期的な実行や別のシステムでは満たせない要件は何か。

Discussion

astroryyyastroryyy

ご丁寧な記載をいただいてありがとうございます!
数点ご質問と、NextActionへの回答がございますので、ご確認ください。

ご質問

いただいたScriptをコピー・チェックしたところ、下記処理が確認されました。

  • 日付が更新されない

    • 旧verでは記載のあったdatecolumn等の記載がなく、更新がなされておりませんでした
    • こちらは意図的に更新をなくしたものでしょうか?(必要というわけではなく、ご確認です)
  • タブ名(inChargeSheetName)を編集した時以外、Scriptが実行されない

    • importrangeのimport元を編集した時はもちろん、「検索用シート」の値を編集した時も更新が一切起こりません
    • トリガーの設定を「onChange」に対して「スプレッドシートの変更時」にしたのですが、こちらの指定が間違っていますでしょうか?
    • また、nextaction内に「IMPORTRANGE の変更は onChange でしかキャッチできない」とありますが、他のサイトなどを見ている限りでは、onChangeであってもimportrangeの変更はキャッチできなさそうという認識でおりました。
      • 参考①:https://teratail.com/questions/127009
      • 参考②:https://stackoverflow.com/questions/62525176
        • 特に参考②より、eに依存する形がimportrangeの変更をキャッチしない要因になるのかと考えていたのですが、そういうわけではなさそうでしょうか?
      • いずれのパターンであれ、現状処理が実行されないため、トリガーの設定など事前に設定すべき条件があればお教えください。

nextactionに対して

  • 例えば CRM から SpreadSheet に連携されるなら、そもそも Google Apps Script でそのシートを読めば良いので、IMPORTRANGE するだけの "検索用シート” は不要。そのようなことをする合理的な理由があるのだとすれば未知の業務要件があるのかもしれない。

    • 上記に関して、非常にややこしいのですが、CRM→SpreadSheetに連携されたシート(下記・CRM連携シート)は限られた人間しか編集権限がなく、個人情報も多いものになっています
    • しかし顧客管理をせねばならない+その拠点が複数に分かれるため、CRM連携シートから必要な情報を抜き出し、拠点ごとの顧客管理シートを作成、そのシート上で個人個人が顧客管理・メモなどを記載する必要があります
  • これは編集行為のたびにスクリプトが起動することを意味する。動的に更新とのことだが、顧客がそんなに頻繁に追加されるのか、定期的な実行や別のシステムでは満たせない要件は何か。

    • 顧客の追加は1日あたり10件-20件ほど(すでに3000人以上蓄積している)、担当者は10拠点それぞれ10名ずつ、という想定です。
    • コスト観点からkintoneの導入がまだ検討できない段階、かつSpreadSheetで管理している他のシートとの連携が多いため来年末まではSpreadsheetを使用せねばならない状態です。
    • 顧客が追加されるたびに実行されたほうが、顧客管理をする担当者側はスムーズに操作ができます
    • 最悪の場合、定期実行のScriptに変更することも可能です

以上、一度ご回答させていただきました。
ご確認のほど、よろしくお願いいたします。

ulpianusulpianus

前提

  • next action はあなたの action です。
  • 業務に基づく仕様は明かされていないので、仮定を置く必要があります。

質問

  • 日付が更新されない
    仮定を置いています。仕様変更により仮定は変わりました。変更した結果どういう仮定を置いているか記載しました。
    この確認をすることで解決できる課題は何でしょうか。
  • Scriptが実行されない
    おそらくコメントを書く、シートを移動するといった方法でもトリガーを発火させることは可能ではないかと想像します。手動編集では発火しないよう抑制しています。無駄に重くなるのを避けるためです(changeType の判定を削除すれば全ての編集で発動するようにできます)。
    こちらでは、import されるシートを開いて、更新が反映されたときに動作しています。ファイルの更新それ自体を検知しようとしていないです。そもそもそういう前提でなかったので(CMS からファイルが連携されることは仕様の後出しでしたし "CRM から連携される" の部分は調査さえできないので、ファイルの更新を監視できるかは未知数です)。そのシートを開いていて表が書きかわっても動作しないのであれば、コードのバグでのエラーかもしれないですが、こちらが実環境を見られない以上自力で切り分けをして、再現する最小条件を共有いただく必要があります。

お話を伺っても今やろうとしていることが最適解だとは思えませんでした。もちろん内実を知らないので、合理的な選択肢であること自体は認めますが、こちらが中の人ではない以上、対応できる内容もリソースも限界があります。というか技術的な難点ではないので、どこであれ公開形式の質問では解決できないと思います。