概要

項目内容
案件IDMAS-081
案件名請負契約の売上計上ロジック
カテゴリパイプライン
PhaseP1
優先度★★
所要時間1〜2 時間
対象ファイル400_domain/406_rpa_pipeline.js
前提案件MAS-080(21タブ管理IDの早期採番)完了済み

目的

21_bud_pipeline の「契約形態」に '請負' を選択した案件について、契約完了月に契約総額を一括で売上計上するロジックを追加する。

  • 現状、400_domain/406_rpa_pipeline.jsgeneratePipelineInvoices は「スポット売上(1回)」と「継続月額MRR(月次ループ)」の2系統のみを生成しており、「請負」区分に特化した分岐は存在しない(既存コードで typeStr.includes('継続') で拾う挙動は 継続月数 0 以下時のフォールバック上限判定のみであり、売上認識ロジックは無い)。
  • 収益認識基準に準拠し、以下の2方式のうち 完了基準 を採用する(進行基準は本案件の対象外):
    • 完了基準(本案件で採用): 契約完了月(計上開始年月 + 継続月数 - 1)に スポット売上・初期費用 + 継続月額(MRR) × 継続月数 の総額を一度だけ計上。
    • 進行基準(対象外): 継続月数にわたって按分計上する(※既存の継続(農耕)が類似挙動で代替可能)。

現在のコード

400_domain/406_rpa_pipeline.js(契約形態の取得と分岐)

L93 で契約形態を取得するが、請負 (=== '請負') の分岐は存在しない:

// 400_domain/406_rpa_pipeline.js L86-98
// 受注確定の行のみ対象
var probStr = col['確度(ヨミ)'] !== -1 ? String(row[col['確度(ヨミ)']]) : '';
if (!probStr.includes('受注')) continue;

const startYm = Utils.parseDateToYm(row[col['計上開始年月']]);
if (!startYm) continue;

const pjName = col['PJ・案件名'] !== -1 ? String(row[col['PJ・案件名']]).trim() : '';
const typeStr = col['契約形態'] !== -1 ? String(row[col['契約形態']]).trim() : '';

スポット売上(L166-197)と MRR(L199-238)が実際の INV 生成ロジック。typeStr は L203 で 継続月数 既定値の判定 (typeStr.includes('継続') ? 120 : 1) に使われるのみ。

100_config/101_sys_config.js — 契約形態ドロップダウンの定義

L1039 で MST_DICT に契約形態カテゴリが定義されており、既に '請負' (CTR_CTR) が登録済み である:

// 100_config/101_sys_config.js L1039
['契約形態', [['CTR_SPT','スポット'], ['CTR_MON','月額'], ['CTR_CON','継続'],
              ['CTR_QUI','準委任'], ['CTR_CTR','請負'], ['CTR_ADV','顧問'],
              ['CTR_EXE','役員'], ['CTR_FIN','財務取引'], ['CTR_CAP','設備投資'],
              ['CTR_NA','該当なし']]],

また L1129 で 21_bud_pipeline の契約形態列 (D列=4列目) は MST_DICT フィルタ(UI契約形態, X列)にバリデーションが張られている:

// 100_config/101_sys_config.js L1129
setVali('BUD_PIPE', 4, 'X', '21_bud_pipeline');

MST_DICT に '請負' は既に存在するため、本案件における DDL 変更は不要。ただし setupAllSchemas の再実行は「既存の 21_bud_pipeline シートにドロップダウン不整合があった場合の再適用」の目的で推奨する。

000_infra/002_constants.js — SHEET_DEFAULTS

L77 で 21_bud_pipeline のデフォルト値が定義されている。契約形態のデフォルトは 'スポット(狩猟)'(MST_DICT の選択肢とは文言が異なる既知の不整合、本案件対象外):

// 000_infra/002_constants.js L77
{ pattern: '21_bud_pipeline', prefix: 'PIP_', defaults: {
  '契約形態': 'スポット(狩猟)', '売上科目': '売上高',
  '確度(ヨミ)': 'Aヨミ (確度80%)', '継続月数': 1, ...
  'スポット売上・初期費用': 0, '継続月額(MRR)': 0,
  _dynamic: { '計上開始年月': 'nextYm' }
}}

修正方針

1. DDL 変更

結論: 変更不要。調査の結果、100_config/101_sys_config.js L1039 の MST_DICT カテゴリ '契約形態' には既に ['CTR_CTR','請負'] が登録されており、21_bud_pipeline の契約形態列(D列)にも setVali('BUD_PIPE', 4, 'X', '21_bud_pipeline') 経由で同プルダウンが適用済みである。

実装時は setupAllSchemas(サイドバー「🚀 BizLP > 操作パネルを開く」配下の DDL 適用メニュー、または setupAllSchemasIncremental 関数)を念のため再実行し、ドロップダウンに '請負' が選択肢として表示されることを確認する。

2. RPA ロジック変更 (400_domain/406_rpa_pipeline.js)

generatePipelineInvoices 内(L81-239 のメインループ)の 既存 MRR ループ(L199-238)の直後 に「契約形態が '請負' の場合の完了月一括計上ブロック」を追加する。既存のスポット売上・MRR ループは触らない(既存動作を保全)。

追加する分岐ロジック(擬似コード):

// 請負契約: 完了月に一括計上(S-09)
if (typeStr === '請負') {
  var dur = col['継続月数'] !== -1 ? (parseInt(String(row[col['継続月数']]), 10) || 0) : 0;
  if (dur <= 0) {
    Utils.logError('generatePipelineInvoices',
      new Error('請負契約の継続月数が0以下: pipId=' + pipeMgrId));
    continue;  // スキップ
  }

  var completionYm = Utils.addMonths(startYm, dur - 1);  // 計上開始年月 + (継続月数-1)
  if (!completionYm) continue;                           // パース失敗時スキップ
  if (completionYm > rowTargetYm) continue;              // まだ完了月が未到来
  var totalAmt = spot + mrr * dur;                       // 契約総額
  if (totalAmt === 0) continue;                          // ゼロ金額はスキップ
  if (totalAmt < 0) {
    Utils.logError('generatePipelineInvoices',
      new Error('請負契約総額がマイナス: pipId=' + pipeMgrId + ' total=' + totalAmt));
    continue;
  }

  var contractMemo = '【RPA:PIPE】' + completionYm + ' ' + pjName + ' 請負一括計上';
  if (isDuplicate_(invData, invHeaders, contractMemo)) continue;

  var cmpParts = completionYm.split('-').map(Number);
  drafts.push(buildInvRow_(invHeaders, {
    '有効フラグ': true,
    '親発注ID(ORD)': ordId,
    '請求ID(INV)': RpaCommon.generateInvId(dateStr, idOffset++),
    '起票日時': now,
    '起票者': 'RPA自動起票',
    '申請種別': '請求書発行(AR)',  // 既存スポット/MRRと同じ
    '発生日(P/L計上日)': new Date(cmpParts[0], cmpParts[1], 0),  // 完了月末
    '決済日_計画': calcSettleDate(completionYm),
    '請求ステータス': '未処理',       // InvoiceDTO 有効値: "未処理"|"承認済"|"却下"
    '収支区分': '収入',
    '取引先名': vendorName,
    'PJ名': pjName || '指定なし_共通費など',
    '科目名': acc,                   // '売上高' or パイプライン行で指定
    '税区分': '対象外',
    '通貨': 'JPY',
    '税抜金額_計画': totalAmt,
    '消費税額_計画': 0,
    '税込金額_計画': totalAmt,
    '未決済残高(自動計算)': totalAmt,
    '決済手段': payMethod,
    '組織名': orgName,
    '摘要': contractMemo
  }));

  Utils.auditLog('CREATE', '32_wrk_invoice', '(will be assigned on write)',
    '', 'generatePipelineInvoices', null,
    { completionYm: completionYm, totalAmt: totalAmt, pipId: pipeMgrId },
    'S-09 請負一括計上');
}

設計上の重要ポイント:

  • 既存 drafts 配列に push するだけで、書き込みは既存の writeInvRows_(invSheet, invHeaders, drafts)(L246)が一括実行する。InvoiceRepository.append() は呼び出さず、既存パイプラインと書き込み経路を統一する(科目マスタからの諸表区分・大分類付与は writeInvRows_ 内で既に実施される)。
  • スポット売上ブロック (L166-197) と MRR ブロック (L199-238) は無改変。typeStr === '請負' の行であっても、スポット売上の金額が 0 であれば既存分岐は自動でスキップされる(L168 if (spot > 0 && ...))。ただし請負案件で スポット売上・初期費用 が入力されるとスポット売上 INV と請負 INV の二重計上が発生しうるため、請負分岐の先頭で「スポット売上を完了月計上分に含めた上で、スポット売上単体の INV は生成しない」構造を取り、MRR ループも同様にスキップする
  • そのため、正確には typeStr === '請負' の場合は スポットループと MRR ループをスキップして請負分岐のみ実行する構造に変更する。具体的には、既存のスポットループを if (spot > 0 && startYm <= rowTargetYm && typeStr !== '請負') に、MRR ループを if (mrr > 0 && typeStr !== '請負') に変更する。
  • 冪等性摘要 列の完全一致で判定する(既存パターン踏襲)。【RPA:PIPE】YYYY-MM PJ名 請負一括計上 を固定フォーマットとし、同一パイプライン ID・同一完了月では 1 件しか生成されない。
  • 監査ログ: INV_ID は writeInvRows_ 書き込み後に確定するため、auditLog の targetId は仮値 '(will be assigned on write)' を入れ、notepipId と完了月・総額を記録する。より正確な ID 追跡が必要になった場合は、書き込み後に generated INV_ID を渡して auditLog を再発行する構造に拡張する(本案件では簡易版で十分)。

3. 冪等性担保

  • isDuplicate_(invData, invHeaders, contractMemo) を呼び、同一 【RPA:PIPE】YYYY-MM PJ名 請負一括計上 の摘要を持つ既存 INV が 32_wrk_invoice にあればスキップする。
  • invData は関数冒頭 L48 で invSheet.getDataRange().getValues() から取得済みであり、同一 RPA 実行内で複数行を処理する間は drafts 側にも追加されるが、drafts のメモは contractMemo と同形式なので 同一 RPA 内の重複生成防止は別途 drafts を走査するか、pipeMgrId 単位の事前フィルタで対応する必要がある(請負案件が同シートに2行以上並ぶ通常運用はないが、エッジケースとして注意)。
  • 最終起票年月日 列は既に RpaCommon.ensureLastBilledCol(pipeSheet, h) で確保済み(L32)。請負案件の起票ターゲット月は 完了月 となるため、完了月を書き戻す運用とするかは「人間が検討すべき事項」で確認する(本実装では完了月に 1 度だけ計上するため、通常のローリング更新とは意味が異なる)。

4. Human-in-the-Loop

  • 自動生成 INV の 請求ステータス'未処理'InvoiceDTO の有効値は "未処理" | "承認済" | "却下" であり '要確認' は無効なため使用しない)。経理担当が 32_wrk_invoice で内容確認後に '承認済' に更新する運用。
  • Utils.auditLog('CREATE', '32_wrk_invoice', invId, '', 'generatePipelineInvoices', null, invDto, 'S-09 請負一括計上 PIP=' + pipeMgrId) を計上後に呼び出し、誰が・いつ・どの PIP に紐づく INV を自動生成したかをトレース可能にする。

影響範囲

項目内容
変更ファイル400_domain/406_rpa_pipeline.js のみ
変更量新規分岐ブロック約 50 行 + 既存スポット/MRR ループのガード条件 2 箇所追加
DDL 変更不要(MST_DICT に '請負' 登録済み)
既存機能への影響typeStr === '請負' の行だけが新規分岐に入る。'スポット(狩猟)' / '継続(農耕)' / 'スポット' / '継続' 等の既存値は typeStr !== '請負' を満たすため挙動変更なし
連動タブ32_wrk_invoice(INV が追記される)/84_cf_daily_plan 他マート類は既存通り INV から集計されるため追加変更不要

注意事項

  1. 100_config/101_sys_config.js の DDL 変更は不要だが、実装後は念のため サイドバー「🚀 BizLP > 操作パネルを開く」→「DDL適用(setupAllSchemas)」 を実行し、21_bud_pipeline D列のドロップダウンに '請負' が選択肢として表示されることを目視確認すること。
  2. 21_bud_pipeline の既存 'スポット(狩猟)' / '継続(農耕)' 等の行は、分岐条件が === '請負' の完全一致なので挙動に副作用が発生しないこと。特に既存スポット/MRR ループに追加する && typeStr !== '請負' ガードが「請負以外の既存値」を正しく通過させることを確認する('スポット(狩猟)' !== '請負' → true のため既存動作維持)。
  3. InvoiceRepository.append() は本案件では使用しない(既存 writeInvRows_ 経由)。ただし writeInvRows_ 内部(400_rpa_common.js L367 以降)で科目マスタから諸表区分・大分類が自動付与されるため、科目名='売上高'11_mst_account に登録済み・有効フラグ=TRUE であることを事前確認すること。
  4. 請負案件で スポット売上・初期費用継続月額(MRR) を両方入力した場合、両者は 完了月の総額に合算 される(スポット単体 INV・MRR 月次 INV は生成されない)。
  5. 決済日_計画 は既存スポット/MRR と同じ calcSettleDate(completionYm) で計算する(入金ラグ・入金日が適用される)。
  6. 請負案件の 最終起票年月日 の扱い: 請負は完了月に 1 度だけ計上されるため、通常のローリング更新の意味は薄い。既存ロジック(L247 の pipeUpdates 書き戻し)は請負分岐では実行しない方針とする(もしくは完了月を書き戻して「完了済」を示す運用に寄せる)。実装時は「完了月を書き戻す」方針を推奨とし、2 回目以降は冪等性チェックでスキップされるため副作用はない。

エッジケース

条件動作理由
typeStr === '請負' かつ 継続月数 <= 0Utils.logError でログ出力し、当該行をスキップ完了月が算出不能。請負契約は期間定義が必須
typeStr === '請負' かつ Utils.parseDateToYm(計上開始年月) が空文字列当該行をスキップ(既存 L90 の if (!startYm) continue; で既に捕捉)計上月が不定
typeStr === '請負' かつ 契約総額 (spot + mrr * dur) === 0当該行をスキップ(ログなし)ゼロ金額 INV はデータ汚染。既存 0 金額スポットと同じ扱い
typeStr === '請負' かつ 契約総額 < 0Utils.logError でログ出力し、スキップマイナス売上は会計上異常値
typeStr === '請負' かつ 完了月 (Utils.addMonths(startYm, dur-1)) > rowTargetYm当該行をスキップ(まだ完了していない)計上基準日に未到達。次回以降の RPA 実行で計上
同一 pipeMgrId・同一完了月の請負 INV が 32_wrk_invoice に既に存在(摘要一致)isDuplicate_ により新規 INV 生成をスキップ二重計上防止(冪等性)
科目名11_mst_account に未登録または無効writeInvRows_ 内の AccountRepository.findAsMap() が空マッピングを返し、諸表区分・大分類が空欄になる(現状の既存動作と同じ)科目マスタ未登録は別案件(運用側で事前登録を担保)
typeStr === '請負' かつ 確度(ヨミ)'受注' を含まない既存ガード (L87 if (!probStr.includes('受注')) continue;) によりスキップ受注確定前案件は自動起票対象外
スポット売上・初期費用継続月額(MRR) の両方が入力されている請負案件完了月に spot + mrr * dur の総額を 1 本の INV で計上(スポット単体 INV・MRR 月次 INV は生成しない)完了基準で契約総額を一括認識するため

実データ検証

実装前に GAS エディタまたはスプレッドシート UI で以下を確認する:

  • 21_bud_pipeline D列(契約形態)のドロップダウンに '請負' が選択肢として含まれること(既に MST_DICT 登録済みのため表示されているはず)。不在の場合は setupAllSchemasIncremental を再実行。
  • 21_bud_pipeline の既存値分布を確認: =UNIQUE(D2:D)'スポット(狩猟)' / '継続(農耕)' 等の値を目視し、'請負' 以外の既存値が typeStr !== '請負' を満たすことを確認。
  • 11_mst_account売上高 が有効フラグ=TRUE・諸表区分=P/L・大分類=売上高 で登録されていること。
  • 32_wrk_invoice のヘッダーが InvoiceDTO@typedef000_infra/003_contracts.js L39-67)と一致しており、特に 申請種別(有効値例: '請求書発行(AR)')・請求ステータス"未処理" | "承認済" | "却下")の文字列が期待通りであること。
  • 最終起票年月日 列が 21_bud_pipeline に存在すること(RpaCommon.ensureLastBilledCol により自動追加されるはずだが、念のため目視確認)。

関連ドキュメント

仕様書関連箇所
CLAUDE.md会計ロジック・科目マスタ(未登録禁止)・Human-in-the-Loop ポリシー
dev_mas-080_pipeline_early_id.md21タブ管理IDの早期採番(MAS-081 の前提。PIP_NNNN の早期採番により本案件の冪等性キーが安定する)
dev_mas-078_pipeline_cf_integration.md84タブへのパイプライン売上合流。請負一括計上 INV もこの集計対象になるため、完了月の突出への影響を要確認
dev_mas-079_tab51_data_source.md51タブでのシミュレーション。請負案件の完了月集中が月次計画に与える影響を試算可能
dev_mas-083_pipeline_tax.mdパイプライン売上の消費税対応。課税事業者移行後は本案件でも税込換算ロジックが必要

人間が検討すべき事項

TODO_future.md(MAS-081 行)から転記: 進行基準 vs 完了基準の選択(会計方針の決定)。本仕様書では 完了基準を採用 する前提で設計しているため、税理士・監査人との擦り合わせを実装前に完了すること。

追加の調査事項:

  1. 完了基準の採用可否(税理士確認必須): 中小企業会計指針および法人税法上、役務提供完了時点での一括計上が適切か。特に長期請負(6 ヶ月超)では工事進行基準が強制される論点があるため、契約期間・金額規模の条件を確認する。
  2. 請負案件の 最終起票年月日 ハンドリング: 完了月 1 回限りの計上のため、最終起票年月日 を完了月で固定するか、書き戻さないかの運用方針。冪等性は 摘要 一致で担保されるため書き戻し無しでも副作用はないが、UI 上「起票済み」の表示があった方が経理担当者には親切。
  3. 進行基準(按分計上)への将来拡張可否: 将来、進行基準が必要になった場合の InvoiceDTO 拡張は必要か(現状の MRR ループで概ね代替可能なため、本案件では対象外)。
  4. 請負案件で取消・返金が発生した場合のロジック: 完了月計上後にキャンセルが発生した場合のマイナス INV 起票運用は別案件(MAS-087 元データ修正→下流タブ再同期)で扱う。本案件では考慮対象外とする。
  5. SHEET_DEFAULTS'スポット(狩猟)' vs MST_DICT の 'スポット' の文言不整合: 本案件の範囲外だが、将来的に両者を統一する案件化が必要(現状は MST_DICT プルダウンで選択した値が最終的に入るため実害は限定的)。

実装プロンプト(Claude Code 用)

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-081「請負契約の売上計上ロジック」を実装してください。

## 実行前タスク(Read で実物を確認し、名前・行番号を推測しない)

1. `000_infra/003_contracts.js` — InvoiceDTO の全フィールドを確認。
   - `請求ステータス` の有効値は `"未処理" | "承認済" | "却下"` のみ(`'要確認'` は無効)。
   - `参照元ID` フィールドは InvoiceDTO には **存在しない**。冪等性は `摘要` 列で担保する。
2. `000_infra/002_constants.js` — `SHEET_DEFAULTS` の `21_bud_pipeline` エントリ(L77 付近)と `ID_PREFIX_MAP` の `PIP_` 定義を確認。
3. `400_domain/406_rpa_pipeline.js` — `generatePipelineInvoices` のメインループ(L81-239)、スポット売上ブロック(L166-197)、MRR ブロック(L199-238)、書き込み `writeInvRows_(invSheet, invHeaders, drafts)`(L246)を確認。
4. `400_domain/400_rpa_common.js` — `isDuplicate_(invData, headers, memoPattern)` の完全一致ロジック(L337-344)、`buildInvRow_`(L331-334)、`RpaCommon.generateInvId`、`RpaCommon.ensureLastBilledCol` のシグネチャを確認。
5. `100_config/101_sys_config.js` — MST_DICT の `契約形態` エントリ(L1039)に **既に `'請負'` が登録済み** であること、`setVali('BUD_PIPE', 4, 'X', '21_bud_pipeline')`(L1129)で D列にプルダウンが適用されていることを確認。**DDL 変更は不要**。
6. `200_data/202_repository.js` — `InvoiceRepository.findAll()` / `append()` のシグネチャを参考までに確認(本案件では append() は使わず、既存の `writeInvRows_` 経由で書き込む)。

## 修正対象ファイル

- `400_domain/406_rpa_pipeline.js` のみ(請負分岐ロジックを追加)

## 実装内容

### 1. 既存スポット/MRR ループのガード追加

- スポットループ (L168): `if (spot > 0 && startYm <= rowTargetYm)` を
  `if (spot > 0 && startYm <= rowTargetYm && typeStr !== '請負')` に変更。
- MRR ループ (L201): `if (mrr > 0) {` を
  `if (mrr > 0 && typeStr !== '請負') {` に変更。

### 2. 請負分岐の新規追加

MRR ループの直後(L238-239 の閉じカッコ後)に、以下の新規ブロックを追加する:

```js
// S-09: 請負契約 — 完了月に契約総額を一括計上(完了基準)
if (typeStr === '請負') {
  var dur = col['継続月数'] !== -1 ? (parseInt(String(row[col['継続月数']]), 10) || 0) : 0;
  if (dur <= 0) {
    Utils.logError('generatePipelineInvoices',
      new Error('請負契約の継続月数が0以下: pipId=' + pipeMgrId));
  } else {
    var completionYm = Utils.addMonths(startYm, dur - 1);
    if (completionYm && completionYm <= rowTargetYm) {
      var totalAmt = spot + mrr * dur;
      if (totalAmt < 0) {
        Utils.logError('generatePipelineInvoices',
          new Error('請負契約総額がマイナス: pipId=' + pipeMgrId + ' total=' + totalAmt));
      } else if (totalAmt > 0) {
        var contractMemo = '\u3010RPA:PIPE\u3011' + completionYm + ' ' + pjName + ' \u8acb\u8ca0\u4e00\u62ec\u8a08\u4e0a';
        if (!isDuplicate_(invData, invHeaders, contractMemo)) {
          var cmpParts = completionYm.split('-').map(Number);
          drafts.push(buildInvRow_(invHeaders, {
            '有効フラグ': true,
            '親発注ID(ORD)': ordId,
            '請求ID(INV)': RpaCommon.generateInvId(dateStr, idOffset++),
            '起票日時': now,
            '起票者': 'RPA自動起票',
            '申請種別': '請求書発行(AR)',
            '発生日(P/L計上日)': new Date(cmpParts[0], cmpParts[1], 0),
            '決済日_計画': calcSettleDate(completionYm),
            '請求ステータス': '未処理',
            '収支区分': '収入',
            '取引先名': vendorName,
            'PJ名': pjName || '指定なし_共通費など',
            '科目名': acc,
            '税区分': '対象外',
            '通貨': 'JPY',
            '税抜金額_計画': totalAmt,
            '消費税額_計画': 0,
            '税込金額_計画': totalAmt,
            '未決済残高(自動計算)': totalAmt,
            '決済手段': payMethod,
            '組織名': orgName,
            '摘要': contractMemo
          }));
          Utils.auditLog('CREATE', '32_wrk_invoice', '',
            '', 'generatePipelineInvoices', null,
            { completionYm: completionYm, totalAmt: totalAmt, pipId: pipeMgrId },
            'S-09 請負一括計上');
        }
      }
    }
  }
}
```

## 制約

- 既存のスポット売上ループ・MRR ループのロジックは **ガード条件 `&& typeStr !== '請負'` の追加のみ**。ブロック内部の処理は変更しない。
- `InvoiceDTO` に存在しないフィールド名(`参照元ID` 等)を使用しない。
- `請求ステータス` に `'要確認'` を使用しない(有効値は `"未処理" | "承認済" | "却下"` のみ)。
- 科目マスタ未登録の科目名をキーワード推測で自動分類しない(`科目名` は `21_bud_pipeline` の `売上科目` 列からそのまま取得し、デフォルトは `'売上高'`)。
- 既存の書き込み経路 `writeInvRows_(invSheet, invHeaders, drafts)` を使用し、`InvoiceRepository.append()` は呼ばない(書き込みロジックの二重化を避け、科目マスタ自動付与も既存経路内で実施されているため)。
- ORD 発注レコードの扱いは既存のまま(L117-153 の `ordMap` / `newOrdRows` ロジックで契約総額 = `spot + mrr * dur` を発注総額として既に正しく計上済み。追加変更不要)。

## エッジケース(仕様書「## エッジケース」セクション参照)

| 条件 | 動作 |
|------|------|
| 継続月数 <= 0 | logError + スキップ |
| 計上開始年月がパース失敗 | スキップ(既存 `if (!startYm) continue;` で捕捉) |
| 契約総額 = 0 | 無音スキップ |
| 契約総額 < 0 | logError + スキップ |
| 完了月 > rowTargetYm(未到来) | スキップ |
| 摘要重複(同一 PIP・同一完了月) | `isDuplicate_` で検出しスキップ |

## 実データ検証(実装直前に実施)

- `21_bud_pipeline` D列ドロップダウンに `'請負'` が表示されること(MST_DICT 登録済みのため)
- `11_mst_account` に `売上高` が有効フラグ=TRUE・諸表区分=P/L・大分類=売上高 で登録されていること
- `32_wrk_invoice` のヘッダーが `InvoiceDTO` 定義と一致していること

## 動作確認

1. `npm run push:dev` で開発環境にデプロイ
2. サイドバー「🚀 BizLP > 操作パネルを開く」→「DDL適用(setupAllSchemas / setupAllSchemasIncremental)」を実行し、`21_bud_pipeline` D列のプルダウンに `'請負'` が選択肢として表示されることを確認
3. `21_bud_pipeline` に次のテスト行を 1 件追加:
   - 有効フラグ=TRUE, PJ・案件名=`テスト請負案件`, 契約形態=`請負`, 売上科目=`売上高`, 確度(ヨミ)=`受注確定`, 計上開始年月=`2026-04`, 継続月数=`3`, スポット売上・初期費用=`100000`, 継続月額(MRR)=`50000`, 取引先名=`テスト取引先`
4. パイプライン RPA(`generatePipelineInvoices`)を `targetYm='2026-06'` で実行
5. `32_wrk_invoice` を確認: 1 件の新規 INV が追記され、以下の状態であること
   - 請求ステータス=`未処理`, 収支区分=`収入`, 科目名=`売上高`, 税込金額_計画=`250000` (`100000 + 50000 × 3`), 発生日=`2026-06-30`(完了月=`2026-06` の月末), 摘要=`【RPA:PIPE】2026-06 テスト請負案件 請負一括計上`
   - スポット単体 INV・MRR 月次 INV(計 3 件)は **生成されていない** こと(請負分岐のみ実行されたこと)
6. 同じ RPA を再実行し、INV が重複追加されないこと(冪等性)を確認
7. `98_audit_log` に `operation=CREATE`, `targetSheet=32_wrk_invoice`, `funcName=generatePipelineInvoices`, `note='S-09 請負一括計上'` のログが記録されていることを確認
8. `targetYm='2026-05'` で再実行し、**完了月(2026-06)未到来のためスキップされる**ことを確認(INV 新規生成 0 件)
9. エッジケース検証: 継続月数=0 の請負行、契約総額=0 の請負行、契約総額がマイナスの請負行を追加して RPA 実行し、それぞれスキップされ error ログが出ることを確認
10. `900_test/901_test_runner.js` の既存テストを実行し、既存機能(スポット/MRR 分岐)に回帰がないことを確認

### 拡張思考の使用状況

| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| Phase 1(調査・設計) | あり | DDL 変更要否の判断、完了基準 vs 進行基準の方針確定、冪等性キー設計 |
| Phase 2(実装) | なし | 設計確定済みコードの書き下し |

推奨実行モデル

工程推奨モデル理由
仕様書作成(本ドキュメント)Claude Sonnet 4.6複数ファイル横断の調査・既存 RPA パターンの理解・会計方針の判断が必要
実装(既存ループのガード追加)Claude Haiku 4.5既存行の微修正。条件式 && typeStr !== '請負' の追加のみ
実装(請負分岐の新規追加)Claude Sonnet 4.6既存パターン(スポット/MRR)の踏襲だが、挿入位置の特定・冪等性ロジック・エッジケースガードの実装判断が必要

変更履歴

日付変更内容
2026-04-19初版作成。完了基準での一括計上方針・冪等性担保(摘要 【RPA:PIPE】 プレフィックス)・Human-in-the-Loop(請求ステータス=未処理)を設計。MST_DICT に '請負' が既存登録済みであることを調査で確認し、DDL 変更不要と判定。

仕様書作成プロンプト

仕様書作成プロンプト(展開して表示)
<instruction>
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
1. **拡張思考の使い分け**: Phase 1(設計)では拡張思考をフル活用し、関数名・列名・行番号・エッジケース一覧・Step 分割粒度を完全に確定させる。Phase 2(清書)の各 Step 内では拡張思考を最小限に抑え、Phase 1 で確定済みの内容の書き下しに徹する。出力途中で再考しない。
2. **テキスト報告の禁止**: 「〜を作成します」等の text のみで tool_use なしに turn を終了しない。説明は 1 文以内。直ちに tool を呼ぶ。
3. **4-5 分割の Write/Edit 実行**: 仕様書作成は Step 2-1(骨格)/ 2-2(前半)/ 2-3a(後半a)/ 2-3b(実装プロンプト〜変更履歴)/ 2-4(`<details>` プロンプト全文記録)に分割して実行する。1 回の Write/Edit は約 300 行以内。
4. **各 Step で何を書くかを具体指示**: 設計判断を Phase 2 実行時に持ち込まない。各 Step の内容は Phase 1 で確定させてから書き出す。

======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
CLIエージェント「Claude Code」として、案件 S-09「請負契約の売上計上ロジック」の開発仕様書を作成してください。
開発仕様書を新規作成した場合は、`docs/_config.json` の `nav` 配列の適切なセクションにも必ず追記してください。

---

## Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)

以下をすべて **Read** で確認する。Grep は「どこにあるか」の発見まで、「どう書くか」の判断は必ず Read。名前・記憶から推測した瞬間に手を止めて Read すること。

### 1-A: 案件定義の読み込み
- `docs/_internal/TODO_future.md` — S-09 の案件名・概要・人間が検討すべき事項を取得

### 1-B: プロジェクト規約
- `CLAUDE.md` — コーディング規約・ファイル番号体系・会計ロジックルールを把握

### 1-C: 既存仕様書テンプレート
- `docs/dev/dev_mas-093_cf_actual_only.md` または `docs/dev/dev_mas-080_pipeline_early_id.md` を 1 件 Read し、フォーマットを把握

### 1-D: 関連コードの調査(以下を全て Read で確認)

1. **`000_infra/003_contracts.js`**
   - `InvoiceDTO` の全フィールドとその型・有効値を確認する
   - 特に冪等性キーとして使える列(`親発注ID(ORD)`・`摘要` 等)を特定する
   - `OrderDTO` の `参照元区分` / `参照元ID` フィールドと InvoiceDTO の対応関係を確認する
   - **注意**: `参照元ID` という列が InvoiceDTO に存在するかを必ず確認すること。存在しない場合は後述の冪等性担保方針を「要調査」に変更する

2. **`000_infra/002_constants.js`**
   - `SHEET_DEFAULTS` の `21_bud_pipeline` エントリを確認し、「契約形態」の現行デフォルト値(`'スポット(狩猟)'`)と全フィールド名を把握する
   - `21_bud_pipeline` の既存フィールド名(`スポット売上・初期費用`・`継続月額(MRR)`・`継続月数`・`計上開始年月`)が実際に存在することを確認する
   - `ID_PREFIX_MAP` の `21_bud_pipeline` エントリ(prefix `PIP_`)を確認する

3. **`000_infra/004_utils.js`**
   - `Utils.addMonths(ymStr, months)` のシグネチャと返り値形式(`"YYYY-MM"`)を確認する
   - `Utils.parseDateToYm(val)` のシグネチャを確認する
   - `Utils.auditLog(operation, targetSheet, targetId, targetCol, funcName, beforeValue, afterValue, note)` のシグネチャを確認する
   - `Utils.logInfo` / `Utils.logError` のシグネチャを確認する

4. **`200_data/202_repository.js`**
   - `InvoiceRepository.findAll()` の戻り値型 `{ headers, dtos }` を確認する
   - `InvoiceRepository.append(dtos)` の引数型と返り値を確認する

5. **`400_domain/406_rpa_pipeline.js`**
   - ファイルが存在することを Bash `ls` で確認した上で Read する
   - 既存の「契約形態」分岐処理(`スポット(狩猟)` 等)の実装パターンを把握する
   - 冪等性担保の実装方法(チェック対象フィールド・ロジック)を確認する
   - `21_bud_pipeline` シートの列のうち「ステータス管理」に使われている列名(`最終起票年月` 相当の列が存在するかどうか)を確認する。**存在しない場合は「要DDL追加」として仕様書に記載する**

6. **`100_config/101_sys_config.js`**
   - `setupAllSchemas` 内の `21_bud_pipeline` スキーマ定義(列定義・バリデーション・ドロップダウン)を確認し、「契約形態」列のドロップダウン値一覧を取得する
   - DDL 変更(`'請負'` 追加)が必要な箇所の行番号を特定する
   - メニュー定義(`onOpen` 内)に動作確認用の実行トリガーが存在するか確認する

---

## Phase 2: 仕様書の分割作成

出力先: `docs/dev/dev_mas-081_contract_revenue_logic.md`

**【重要】絶対に 1 回の tool_use で全内容を出力せず、以下の Step に厳密に分割して実行すること。**

---

### Step 2-1: 骨格の作成(File Write・約 20 行)

以下の見出しのみを持つ骨格ファイルを新規作成する。本文は空で可。

(各見出しリスト省略。本ドキュメントの全体構造がそれ)

---

### Step 2-2: 前半セクションの追記(File Edit または Bash heredoc・約 300 行)

(指示内容は本ドキュメント本文参照)

---

### Step 2-3a: エッジケース〜人間検討事項の追記(File Edit または Bash・約 200 行)

(指示内容は本ドキュメント本文参照)

---

### Step 2-3b: 実装プロンプト〜変更履歴の追記(File Edit または Bash・約 250 行)

(指示内容は本ドキュメント本文参照)

---

### Step 2-4: 仕様書作成プロンプトの記録(File Edit または Bash・最後に独立実行)

ファイル末尾の `## 仕様書作成プロンプト` セクションに、この `<instruction>` 全文を `<details>` で追記する。

---

## Phase 3: 後処理(全 Step 完了後に実行)

### 3-A: `_config.json` にナビゲーション登録(必須)
`docs/_config.json` の `nav` 配列 §E.2(バグ修正・バリデーション)または §E.4(データマート・財務諸表)の適切なセクションに追加:
    { "file": "dev/dev_mas-081_contract_revenue_logic.md", "title": "E.2.X S-09 請負契約の売上計上ロジック" }

### 3-B: changelog 追記
`docs/_internal/changelog.md` の先頭(ヘッダー直後)に追記。

### 3-C: コミット&プッシュ
    git add docs/dev/dev_mas-081_contract_revenue_logic.md docs/_internal/changelog.md docs/_config.json
    git commit -m "docs: S-09 ..."
    git push -u origin docs/dev-S-09
</instruction>