MAS-081: 請負契約の売上計上ロジック
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-081 |
| 案件名 | 請負契約の売上計上ロジック |
| カテゴリ | パイプライン |
| Phase | P1 |
| 優先度 | ★★ |
| 所要時間 | 1〜2 時間 |
| 対象ファイル | 400_domain/406_rpa_pipeline.js |
| 前提案件 | MAS-080(21タブ管理IDの早期採番)完了済み |
目的
21_bud_pipeline の「契約形態」に '請負' を選択した案件について、契約完了月に契約総額を一括で売上計上するロジックを追加する。
- 現状、
400_domain/406_rpa_pipeline.jsのgeneratePipelineInvoicesは「スポット売上(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 であれば既存分岐は自動でスキップされる(L168if (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)'を入れ、noteにpipIdと完了月・総額を記録する。より正確な 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 から集計されるため追加変更不要 |
注意事項
100_config/101_sys_config.jsの DDL 変更は不要だが、実装後は念のため サイドバー「🚀 BizLP > 操作パネルを開く」→「DDL適用(setupAllSchemas)」 を実行し、21_bud_pipelineD列のドロップダウンに'請負'が選択肢として表示されることを目視確認すること。21_bud_pipelineの既存'スポット(狩猟)'/'継続(農耕)'等の行は、分岐条件が=== '請負'の完全一致なので挙動に副作用が発生しないこと。特に既存スポット/MRR ループに追加する&& typeStr !== '請負'ガードが「請負以外の既存値」を正しく通過させることを確認する('スポット(狩猟)' !== '請負'→ true のため既存動作維持)。InvoiceRepository.append()は本案件では使用しない(既存writeInvRows_経由)。ただしwriteInvRows_内部(400_rpa_common.jsL367 以降)で科目マスタから諸表区分・大分類が自動付与されるため、科目名='売上高'が11_mst_accountに登録済み・有効フラグ=TRUE であることを事前確認すること。- 請負案件で
スポット売上・初期費用と継続月額(MRR)を両方入力した場合、両者は 完了月の総額に合算 される(スポット単体 INV・MRR 月次 INV は生成されない)。 決済日_計画は既存スポット/MRR と同じcalcSettleDate(completionYm)で計算する(入金ラグ・入金日が適用される)。- 請負案件の
最終起票年月日の扱い: 請負は完了月に 1 度だけ計上されるため、通常のローリング更新の意味は薄い。既存ロジック(L247 のpipeUpdates書き戻し)は請負分岐では実行しない方針とする(もしくは完了月を書き戻して「完了済」を示す運用に寄せる)。実装時は「完了月を書き戻す」方針を推奨とし、2 回目以降は冪等性チェックでスキップされるため副作用はない。
エッジケース
| 条件 | 動作 | 理由 |
|---|---|---|
typeStr === '請負' かつ 継続月数 <= 0 | Utils.logError でログ出力し、当該行をスキップ | 完了月が算出不能。請負契約は期間定義が必須 |
typeStr === '請負' かつ Utils.parseDateToYm(計上開始年月) が空文字列 | 当該行をスキップ(既存 L90 の if (!startYm) continue; で既に捕捉) | 計上月が不定 |
typeStr === '請負' かつ 契約総額 (spot + mrr * dur) === 0 | 当該行をスキップ(ログなし) | ゼロ金額 INV はデータ汚染。既存 0 金額スポットと同じ扱い |
typeStr === '請負' かつ 契約総額 < 0 | Utils.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_pipelineD列(契約形態)のドロップダウンに'請負'が選択肢として含まれること(既に MST_DICT 登録済みのため表示されているはず)。不在の場合はsetupAllSchemasIncrementalを再実行。21_bud_pipelineの既存値分布を確認:=UNIQUE(D2:D)で'スポット(狩猟)'/'継続(農耕)'等の値を目視し、'請負'以外の既存値がtypeStr !== '請負'を満たすことを確認。11_mst_accountに売上高が有効フラグ=TRUE・諸表区分=P/L・大分類=売上高で登録されていること。32_wrk_invoiceのヘッダーがInvoiceDTOの@typedef(000_infra/003_contracts.jsL39-67)と一致しており、特に申請種別(有効値例:'請求書発行(AR)')・請求ステータス("未処理" | "承認済" | "却下")の文字列が期待通りであること。最終起票年月日列が21_bud_pipelineに存在すること(RpaCommon.ensureLastBilledColにより自動追加されるはずだが、念のため目視確認)。
関連ドキュメント
| 仕様書 | 関連箇所 |
|---|---|
| CLAUDE.md | 会計ロジック・科目マスタ(未登録禁止)・Human-in-the-Loop ポリシー |
| dev_mas-080_pipeline_early_id.md | 21タブ管理IDの早期採番(MAS-081 の前提。PIP_NNNN の早期採番により本案件の冪等性キーが安定する) |
| dev_mas-078_pipeline_cf_integration.md | 84タブへのパイプライン売上合流。請負一括計上 INV もこの集計対象になるため、完了月の突出への影響を要確認 |
| dev_mas-079_tab51_data_source.md | 51タブでのシミュレーション。請負案件の完了月集中が月次計画に与える影響を試算可能 |
| dev_mas-083_pipeline_tax.md | パイプライン売上の消費税対応。課税事業者移行後は本案件でも税込換算ロジックが必要 |
人間が検討すべき事項
TODO_future.md(MAS-081 行)から転記: 進行基準 vs 完了基準の選択(会計方針の決定)。本仕様書では 完了基準を採用 する前提で設計しているため、税理士・監査人との擦り合わせを実装前に完了すること。
追加の調査事項:
- 完了基準の採用可否(税理士確認必須): 中小企業会計指針および法人税法上、役務提供完了時点での一括計上が適切か。特に長期請負(6 ヶ月超)では工事進行基準が強制される論点があるため、契約期間・金額規模の条件を確認する。
- 請負案件の
最終起票年月日ハンドリング: 完了月 1 回限りの計上のため、最終起票年月日を完了月で固定するか、書き戻さないかの運用方針。冪等性は摘要一致で担保されるため書き戻し無しでも副作用はないが、UI 上「起票済み」の表示があった方が経理担当者には親切。 - 進行基準(按分計上)への将来拡張可否: 将来、進行基準が必要になった場合の
InvoiceDTO拡張は必要か(現状の MRR ループで概ね代替可能なため、本案件では対象外)。 - 請負案件で取消・返金が発生した場合のロジック: 完了月計上後にキャンセルが発生した場合のマイナス INV 起票運用は別案件(MAS-087 元データ修正→下流タブ再同期)で扱う。本案件では考慮対象外とする。
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>