1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【kintone】レコード追加時に他のアプリへ行った更新を、レコード削除時に取り消す

Posted at

掲題の仕組みの作り方を在庫管理で例示する。

在庫アプリがこのような状態のときに
タイトルなし.png

入出庫アプリで下図のレコードを追加すると
タイトルなしa.png

追加したレコードと在庫アプリで、同数の在庫が記録される。
タイトルなしb.png
タイトルなしc.png

追加したレコードを削除すれば
タイトルなしd.png

在庫アプリの在庫数がレコード追加前に戻る。
タイトルなし.png


入出庫アプリに kintone REST API Client
以下のJavaScriptを適用すれば、上述の仕様を実現できる。

(() => {
  'use strict';

  // 備品コードのルックアップ参照先のアプリID(在庫アプリのID)を取得
  const INVENTORY_APP_ID = kintone.app.getLookupTargetAppId('備品コード');
  // kintone REST API Client のインスタンスを生成
  const client = new KintoneRestAPIClient();

  // テーブル内の備品コード重複をチェックする関数
  const checkDuplicateItemCodes = (tableRows) => {
    // 備品コードをキーとする行番号の配列
    const itemCodeGroups = new Map();

    // 前回のエラーメッセージをクリア
    tableRows.forEach(row => row.value.備品コード.error = null);

    // 備品コードのグループ分けを開始
    tableRows.forEach((row, i) => {
      // 備品コードが空文字でない行のみ処理を続行
      const itemCode = row.value.備品コード.value;
      if (itemCode?.trim()) {
        // 初回出現の備品コードの場合は新しい配列を作成
        if (!itemCodeGroups.has(itemCode)) itemCodeGroups.set(itemCode, []);
        // 該当する備品コードの配列に現在の行番号を追加
        itemCodeGroups.get(itemCode).push(i);
      }
    });
    
    // 重複が発生しているかどうかのフラグ
    let hasDuplicate = false;
    // 各備品コードグループを検査
    for (const [, indices] of itemCodeGroups) {
      // 同一備品コードが2行以上存在したら重複ありと判定
      if (indices.length > 1) {
        // 重複している全ての行にエラーメッセージを設定
        indices.forEach(index => {
          tableRows[index].value.備品コード.error = '備品コードが重複しています。';
        });
        // 重複フラグを真に設定
        hasDuplicate = true;
      }
    }

    return hasDuplicate;
  };

  // 追加画面が表示されたときの処理
  kintone.events.on(['app.record.create.show'], (event) => {
    // 在庫列を非表示に設定
    kintone.app.record.setFieldShown('在庫', false);
    // 入出庫テーブルの全行で繰越在庫列を編集不可にする
    event.record.入出庫.value.forEach(row => row.value.繰越在庫.disabled = true);

    return event;
  });

  // 入庫,出庫の値が変更されたとき、入出庫テーブル全行の入庫,出庫に同じ処理をする
  kintone.events.on(['app.record.create.change.入庫', 'app.record.create.change.出庫'], 
  (event) => {
    event.record.入出庫.value.forEach(row => {
      ['入庫', '出庫'].forEach(field => {
        let value = row.value[field].value;
        
        // 値の全角数字を半角数字に変換
        if (typeof value === 'string') {
          value = value.replace(/[0-9]/g, s => 
            String.fromCharCode(s.charCodeAt(0) - 0xFEE0));
          row.value[field].value = value;
        }
        
        // 前回のエラーメッセージをクリア
        row.value[field].error = null;
        // 値が存在し、かつ自然数でない場合はエラーメッセージを設定
        if (value && (!/^\d+$/.test(value) || Number(value) < 1)) {
          row.value[field].error = '自然数を入力してください。';
        }
      });
    });

    return event;
  });

  // 入出庫テーブルの行数が変わったときの処理
  kintone.events.on(['app.record.create.change.入出庫'], (event) => {
    const tableRows = event.record.入出庫.value;
    // 備品コードの重複チェックを実行
    checkDuplicateItemCodes(tableRows);
    
    // 処理の競合を避けるため、重複チェック完了後に繰越在庫の非活性化を実行
    setTimeout(() => {
      const currentRecord = kintone.app.record.get();
      currentRecord.record.入出庫.value.forEach(row => row.value.繰越在庫.disabled = true);
      kintone.app.record.set(currentRecord);
    }, 0);

    return event;
  });

  // 備品フィールドの値が変更されたときの処理
  kintone.events.on(['app.record.create.change.備品'], (event) => {
    // 変更された行のデータを取得
    const changedRowData = event.changes.row;
    // 変更された行のデータが存在しない場合は処理を終了
    if (!changedRowData?.value) return event;
    
    // 値が変わった行の行番号を特定
    const tableRows = event.record.入出庫.value;
    const rowIndex = tableRows.findIndex(row => 
      row.value.備品.value === changedRowData.value.備品.value);
    // 該当する行が見つからない場合は処理を終了
    if (rowIndex === -1) return event;
    
    // 変更された行の備品の値を取得
    const changedRow = tableRows[rowIndex];
    const selectedItem = changedRow.value.備品.value;
    
    // 備品コードの重複チェックを実行
    checkDuplicateItemCodes(tableRows);
    
    // 備品が選択されていない場合は繰越在庫をクリアして処理を終了
    if (!selectedItem) {
      changedRow.value.繰越在庫.value = '';
      return event;
    }
    
    // 在庫アプリから該当備品の在庫を取得
    const inventoryQuery = `備品 = "${selectedItem}"`;
    client.record.getRecords({
      app: INVENTORY_APP_ID,
      query: inventoryQuery
    // 繰越在庫に取得した在庫の値を設定
    }).then(inventoryResponse => {
      const currentRecord = kintone.app.record.get();
      if (currentRecord.record.入出庫.value[rowIndex]) {
        const stockValue = inventoryResponse.records.length > 0 
          ? inventoryResponse.records[0].在庫.value || '0' 
          : '0';
        currentRecord.record.入出庫.value[rowIndex].value.繰越在庫.value = stockValue;
        kintone.app.record.set(currentRecord);
      }
    }).catch((error) => {
      event.error = error.message;
    });

    return event;
  });

  // レコードを追加したときの処理
  kintone.events.on('app.record.create.submit', async (event) => {
    const record = event.record;
    const stockMovements = record.入出庫.value;

    try {
      // 備品コードの重複チェックを実行
      if (checkDuplicateItemCodes(stockMovements)) {
        // 重複があったらエラーを出して処理を中止
        event.error = '備品コードの重複があります。';
        return event;
      }

      // エラー行を記録するための配列
      const emptyRows = []; // 備品コード未選択の行
      const invalidNumberRows = []; // 入庫か出庫に自然数以外の値がある行
      const noMovementRows = []; // 入庫も出庫も0か空文字の行
      const bulkRequests = []; // 一括処理用のリクエスト配列

      // 入出庫テーブルの全行に処理
      for (let i = 0; i < stockMovements.length; i++) {
        // 処理中の行の値を取得
        const movement = stockMovements[i];
        const itemName = movement.value.備品コード.value;
        const inStockValue = movement.value.入庫.value;
        const outStockValue = movement.value.出庫.value;

        // 備品コードが未入力の場合はエラー行として記録してから次の行の処理へ移行
        if (!itemName?.trim()) {
          emptyRows.push(i + 1); // 1行目の行番号が内部的には0なので+1
          continue;
        }

        // 入庫と出庫の値を数値に変換(空文字は0とする)
        const inStock = parseInt(inStockValue) || 0;
        const outStock = parseInt(outStockValue) || 0;

        // 入庫と出庫のバリデーション
        ['入庫', '出庫'].forEach(field => {
          // 値が存在し、かつ自然数でない場合はエラーにする
          const value = movement.value[field].value;
          if (value && (!/^\d+$/.test(value.toString().trim()) || parseInt(value) < 1)) {
            movement.value[field].error = '自然数を入力してください。';
            invalidNumberRows.push(i + 1); // エラー行の行番号を記録
          // 値が正常な場合はエラーをクリア
          } else {
            movement.value[field].error = null;
          }
        });

        // 入庫も出庫も0の場合はエラー行として記録して次の行の処理へ移行
        if (inStock === 0 && outStock === 0) {
          noMovementRows.push(i + 1);
          continue;
        }

        // 在庫アプリから該当備品の現在の在庫の値を取得
        const inventoryQuery = `備品コード = "${itemName}"`;
        const inventoryResponse = await client.record.getRecords({
          app: INVENTORY_APP_ID,
          query: inventoryQuery
        });
        const inventoryRecord = inventoryResponse.records[0];
        const currentStock = parseInt(inventoryRecord.在庫.value) || 0;

        // 繰越在庫に取得した在庫の値を設定
        movement.value.繰越在庫.value = currentStock.toString();
        // 取得した在庫 + 入庫 - 出庫 で新しい在庫数を計算
        const newStock = currentStock + inStock - outStock;

        // 新しい在庫数がマイナスになる場合はエラーとする
        if (newStock < 0) {
          const itemDisplayName = movement.value.備品.value;
          const currentAvailableStock = currentStock + inStock;
          event.error = `${itemDisplayName}の在庫が不足しています。` +
                       `繰越在庫+入庫: ${currentAvailableStock} 出庫: ${outStock}`;
          return event;
        }

        // 在庫フィールドに新しい在庫数を設定
        movement.value.在庫.value = newStock.toString();

        // 在庫アプリの在庫の値も同数に更新するリクエスト
        bulkRequests.push({
          method: 'PUT',
          api: '/k/v1/record.json',
          payload: {
            app: INVENTORY_APP_ID,
            id: inventoryRecord.$id.value,
            record: { 在庫: { value: newStock.toString() } },
            revision: inventoryRecord.$revision.value // 楽観ロック用のリビジョン
          }
        });
      }

      // 備品コード未選択の行が存在する場合のエラー
      if (emptyRows.length > 0) {
        event.error = `${emptyRows.join(',')}行目の備品コードが選択されていません。`;
        return event;
      }

      // 入庫か出庫に自然数以外が入力された行が存在する場合のエラー
      if (invalidNumberRows.length > 0) {
        event.error = `入庫、出庫には自然数しか入力できません。`;
        return event;
      }

      // 入庫も出庫も0の行が存在する場合のエラー
      if (noMovementRows.length > 0) {
        event.error = `${noMovementRows.join(',')}行目の入庫と出庫が両方とも空になっています。`;
        return event;
      }

      // 入出庫アプリにレコード追加するためのリクエスト
      bulkRequests.push({
        method: 'POST',
        api: '/k/v1/record.json',
        payload: { 
          app: kintone.app.getId(),
          record: record
        }
      });

      // リクエストを一括処理
      const bulkResp = await client.bulkRequest({ requests: bulkRequests });
      // 一括処理完了後、追加されたレコードの詳細画面にリダイレクト
      location.href = '/k/' + kintone.app.getId() + '/show#record=' + 
                     bulkResp.results[bulkResp.results.length - 1].id;
      // デフォルトのレコード追加処理をキャンセル
      return false;

    } catch (error) {
      event.error = error.message;
      return event;
    }
  });

  // レコードを削除したときの処理
  kintone.events.on(['app.record.index.delete.submit', 
                     'app.record.detail.delete.submit'], async (event) => {
    try {
      const record = event.record;
      const stockMovements = record.入出庫.value;

      // 一括処理用のリクエスト配列
      const bulkRequests = [];
      
      // 入出庫テーブルの全行を処理
      for (const movement of stockMovements) {
        const itemCode = movement.value.備品コード.value;
        const inStock = parseInt(movement.value.入庫.value) || 0;
        const outStock = parseInt(movement.value.出庫.value) || 0;
        
        // 在庫アプリから該当備品のレコードを検索
        const inventoryQuery = `備品コード = "${itemCode}"`;
        const inventoryResponse = await client.record.getRecords({
          app: INVENTORY_APP_ID,
          query: inventoryQuery
        });
        
        // 該当備品のレコードが存在しない場合は次の行へ
        if (inventoryResponse.records.length === 0) continue;
        
        // レコード削除後の在庫の値を計算(現在の在庫 - 入庫 + 出庫)
        const inventoryRecord = inventoryResponse.records[0];
        const currentStock = parseInt(inventoryRecord.在庫.value) || 0;
        const restoredStock = currentStock - inStock + outStock;
        
        // レコード削除後の在庫の値がマイナスになる場合はエラー
        if (restoredStock < 0) {
          const itemDisplayName = movement.value.備品.value;
          event.error = `${itemDisplayName}の在庫を元に戻せません。` +
                       `在庫アプリの在庫: ${currentStock}, 出庫-入庫: ${inStock - outStock}`;
          return event;
        }
        
        // 在庫を削除するレコードが追加される前の値に復元するためのリクエスト
        bulkRequests.push({
          method: 'PUT',
          api: '/k/v1/record.json',
          payload: {
            app: INVENTORY_APP_ID,
            id: inventoryRecord.$id.value,
            record: { 在庫: { value: restoredStock.toString() } },
            revision: inventoryRecord.$revision.value
          }
        });
      }
      
      // 入出庫アプリのレコードを削除するためのリクエスト
      bulkRequests.push({
        method: 'DELETE',
        api: '/k/v1/records.json',
        payload: {
          app: kintone.app.getId(),
          ids: [record.$id.value],
          revisions: [record.$revision.value]
        }
      });
      
      // リクエストを一括処理
      if (bulkRequests.length > 0) {
        await client.bulkRequest({ requests: bulkRequests });
        
        // 詳細画面からレコード削除した場合は一覧画面にリダイレクト
        if (location.pathname.includes('/show')) {
          location.href = '/k/' + kintone.app.getId() + '/';
        } else {
          // 一覧画面からレコード削除した場合はリロード
          location.reload();
        }
        
        // デフォルトの削除処理をキャンセル
        return false;
      }
      
    } catch (error) {
      event.error = error.message;
      return event;
    }

  });
})();
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?