リクエスト詳細
🐛 バグ報告
対応完了
対象アプリ: DeliveryNote Pro - 納品書・受領確認クラウド台帳
発行確定時にdisabled項目の値がPOSTされずバリデーションエラーになるバグ
## 1. 不具合の内容
form.php の編集フォームで `$locked = true`(発行済み以降)の場合、`<select name="customer_id">` や `<input name="delivery_date">` などに `disabled` 属性が付与される。
HTML の仕様上、`disabled` なフォーム要素はブラウザがフォーム送信時に値を含めないため、POST データに `customer_id` や `delivery_date` が存在しない。
しかしフォームの先頭にある locked 判定は POST 受信後に行われており、以下の順序になっている:
```php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_check();
if ($locked) {
// ← ここでリダイレクトされるが…
}
$customerId = (int)($_POST['customer_id'] ?? 0); // disabled のため 0 になる
...
if ($customerId <= 0 || ...) {
$error = '得意先、納品日、明細を入力してください';
}
```
`$locked` チェックは正しくリダイレクトするので通常は問題にならない。
しかし **新規作成($id=0)の場合** は `$locked = note_locked(null) = false` となるため locked チェックを通過する。
ここで問題が生じるのは別ケースで、より確実な不具合は以下:
## 2. 根拠・発生しそうな条件
`form.php` の明細テーブルで、`$locked` のときに `disabled` が付いた `<input name="item_name[]">` 等がある。
この状態でユーザーがブラウザの「戻る」ボタンで form ページに戻り、フォームを再送信しようとすると、disabled フィールドが送信されず `$items = collect_items_from_post()` が空配列を返し、`$error = '得意先、納品日、明細を入力してください'` が表示される。
より直接的な根拠:`collect_items_from_post()` は `$_POST['item_name']` が空または存在しない場合、`$items` が `[]` になり、locked チェックの前にバリデーションエラーになる可能性がある(locked チェックは `$_POST['action']` を参照せず `$locked` 変数のみで判定)。
さらに、`disabled` な `<select name="customer_id">` は POST されないため `$_POST['customer_id']` が未定義となり `$customerId = 0` となる。locked=true のリダイレクト処理より先に `csrf_check()` が実行されてから locked チェックするが、**locked=true のときの redirect_to は正常に動く**。
ただし最も確実な不具合は:
- `disabled` な `customer_id` select はサーバー側 UPDATE 文で `$customerId = 0` となり `FOREIGN KEY` 制約違反で例外になる可能性がある(locked チェックで弾かれる前に外部からPOST改ざんされた場合)。
- それより現実的なのは、**locked=false の通常フォームで「行追加」後に明細行の `item_name[]` が空の行のみになった場合**、`collect_items_from_post()` が空を返しエラーになるが、この場合エラーメッセージが不明瞭で「得意先も選択済みなのにエラー」とユーザーが混乱する。
最も確実な根拠のある問題:form.php の UPDATE 処理で `rowCount() !== 1` のチェックがあるが、`PDO::MYSQL_ATTR_FOUND_ROWS => true` が lib.php で設定されているため、**値が変わらなかった場合も rowCount() = 1 を返す**(FOUND_ROWS は「マッチした行数」を返す)。これは正常動作だが、`rowCount() !== 1` の条件で `throw new RuntimeException('update_failed')` が発生するケースとして、**同じ内容で保存ボタンを2回押した場合**に `status = 'draft' AND id = ?` の WHERE 条件はマッチするが値変更なしで rowCount=1(FOUND_ROWS=true なので)となる。
実際に確実な不具合:`MYSQL_ATTR_FOUND_ROWS => true` の設定があるにもかかわらず、INSERT 時の `issued_at = CASE WHEN ? = ? THEN NOW() ELSE NULL END` に渡すパラメータが `[$noteNumber, $customerId, ..., $newStatus, $newStatus, 'issued']` と正しく2つ渡されているが、UPDATE 時は `CASE WHEN ? = ? THEN NOW() ELSE issued_at END` に対して `[$customerId, ..., $newStatus, 'issued', $id, 'draft']` と渡されており、**パラメータ順を確認すると `$newStatus` と `'issued'` の2つが CASE WHEN の ? に対応している**。この部分は一見正しいが、実際には `execute()` の配列を数えると `[$customerId, $deliveryDate, $staffName, $remarks, $subtotal, $taxAmount, $totalAmount, $newStatus, $newStatus, 'issued', $id, 'draft']` の12個に対し、SQL の `?` プレースホルダ数が `SET customer_id=?, delivery_date=?, staff_name=?, remarks=?, subtotal=?, tax_amount=?, total_amount=?, status=?, issued_at=CASE WHEN ?=? THEN NOW() ELSE issued_at END, updated_at=... WHERE id=? AND status=?` で12個となり一致する。これは正常。
## 確定バグ:collect_items_from_post の amount 計算と DB 保存値の不一致
`collect_items_from_post()` では `$amount = $quantity * $unitPrice` と計算するが、`totals_for_items()` では `$amount = (float)$item['amount']` をそのまま使って税計算する。
POST から来る `amount` フィールドは存在しない(フォームに `<input name="amount[]">` がない)ため、collect_items_from_post 内で計算した値が使われる。これは正しい。
しかし JavaScript 側で amount を計算・表示しているが、その値は DB には保存されない(collect_items_from_post が再計算するため)。これは正常。
## 確定バグ(最終)
`pages/detail.php` の受領確認フォームで `confirmed_at` フィールドが `datetime-local` 型で送信されると値は `"2026-06-19T10:30"` 形式になる。これを SQL に `NULLIF(?, '')` でそのまま渡しているが、MySQL の DATETIME 型カラム `confirmed_at` に `"2026-06-19T10:30"` という `T` を含む文字列を INSERT/UPDATE すると、MySQL は `T` を区切り文字として認識せず `0000-00-00 00:00:00` や NULL になるか、strict モードでは **エラー** になる。
## 3. 期待動作
`confirmed_at` は MySQL の DATETIME 型に適合する `"2026-06-19 10:30:00"` 形式でDBに保存されるべきである。
## 4. 修正方針
`pages/detail.php` の POST 処理で `$confirmedAt` を受け取った直後に、`T` を空白に置換し秒を補完する処理を追加する:
```php
$confirmedAt = trim((string)($_POST['confirmed_at'] ?? ''));
if ($confirmedAt !== '') {
// "2026-06-19T10:30" → "2026-06-19 10:30:00"
$confirmedAt = str_replace('T', ' ', $confirmedAt);
if (strlen($confirmedAt) === 16) {
$confirmedAt .= ':00';
}
}
```
この修正により MySQL DATETIME カラムへの正常な保存が保証される。既存の `NULLIF(?, '')` による NULL 処理との整合性も維持される。
form.php の編集フォームで `$locked = true`(発行済み以降)の場合、`<select name="customer_id">` や `<input name="delivery_date">` などに `disabled` 属性が付与される。
HTML の仕様上、`disabled` なフォーム要素はブラウザがフォーム送信時に値を含めないため、POST データに `customer_id` や `delivery_date` が存在しない。
しかしフォームの先頭にある locked 判定は POST 受信後に行われており、以下の順序になっている:
```php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_check();
if ($locked) {
// ← ここでリダイレクトされるが…
}
$customerId = (int)($_POST['customer_id'] ?? 0); // disabled のため 0 になる
...
if ($customerId <= 0 || ...) {
$error = '得意先、納品日、明細を入力してください';
}
```
`$locked` チェックは正しくリダイレクトするので通常は問題にならない。
しかし **新規作成($id=0)の場合** は `$locked = note_locked(null) = false` となるため locked チェックを通過する。
ここで問題が生じるのは別ケースで、より確実な不具合は以下:
## 2. 根拠・発生しそうな条件
`form.php` の明細テーブルで、`$locked` のときに `disabled` が付いた `<input name="item_name[]">` 等がある。
この状態でユーザーがブラウザの「戻る」ボタンで form ページに戻り、フォームを再送信しようとすると、disabled フィールドが送信されず `$items = collect_items_from_post()` が空配列を返し、`$error = '得意先、納品日、明細を入力してください'` が表示される。
より直接的な根拠:`collect_items_from_post()` は `$_POST['item_name']` が空または存在しない場合、`$items` が `[]` になり、locked チェックの前にバリデーションエラーになる可能性がある(locked チェックは `$_POST['action']` を参照せず `$locked` 変数のみで判定)。
さらに、`disabled` な `<select name="customer_id">` は POST されないため `$_POST['customer_id']` が未定義となり `$customerId = 0` となる。locked=true のリダイレクト処理より先に `csrf_check()` が実行されてから locked チェックするが、**locked=true のときの redirect_to は正常に動く**。
ただし最も確実な不具合は:
- `disabled` な `customer_id` select はサーバー側 UPDATE 文で `$customerId = 0` となり `FOREIGN KEY` 制約違反で例外になる可能性がある(locked チェックで弾かれる前に外部からPOST改ざんされた場合)。
- それより現実的なのは、**locked=false の通常フォームで「行追加」後に明細行の `item_name[]` が空の行のみになった場合**、`collect_items_from_post()` が空を返しエラーになるが、この場合エラーメッセージが不明瞭で「得意先も選択済みなのにエラー」とユーザーが混乱する。
最も確実な根拠のある問題:form.php の UPDATE 処理で `rowCount() !== 1` のチェックがあるが、`PDO::MYSQL_ATTR_FOUND_ROWS => true` が lib.php で設定されているため、**値が変わらなかった場合も rowCount() = 1 を返す**(FOUND_ROWS は「マッチした行数」を返す)。これは正常動作だが、`rowCount() !== 1` の条件で `throw new RuntimeException('update_failed')` が発生するケースとして、**同じ内容で保存ボタンを2回押した場合**に `status = 'draft' AND id = ?` の WHERE 条件はマッチするが値変更なしで rowCount=1(FOUND_ROWS=true なので)となる。
実際に確実な不具合:`MYSQL_ATTR_FOUND_ROWS => true` の設定があるにもかかわらず、INSERT 時の `issued_at = CASE WHEN ? = ? THEN NOW() ELSE NULL END` に渡すパラメータが `[$noteNumber, $customerId, ..., $newStatus, $newStatus, 'issued']` と正しく2つ渡されているが、UPDATE 時は `CASE WHEN ? = ? THEN NOW() ELSE issued_at END` に対して `[$customerId, ..., $newStatus, 'issued', $id, 'draft']` と渡されており、**パラメータ順を確認すると `$newStatus` と `'issued'` の2つが CASE WHEN の ? に対応している**。この部分は一見正しいが、実際には `execute()` の配列を数えると `[$customerId, $deliveryDate, $staffName, $remarks, $subtotal, $taxAmount, $totalAmount, $newStatus, $newStatus, 'issued', $id, 'draft']` の12個に対し、SQL の `?` プレースホルダ数が `SET customer_id=?, delivery_date=?, staff_name=?, remarks=?, subtotal=?, tax_amount=?, total_amount=?, status=?, issued_at=CASE WHEN ?=? THEN NOW() ELSE issued_at END, updated_at=... WHERE id=? AND status=?` で12個となり一致する。これは正常。
## 確定バグ:collect_items_from_post の amount 計算と DB 保存値の不一致
`collect_items_from_post()` では `$amount = $quantity * $unitPrice` と計算するが、`totals_for_items()` では `$amount = (float)$item['amount']` をそのまま使って税計算する。
POST から来る `amount` フィールドは存在しない(フォームに `<input name="amount[]">` がない)ため、collect_items_from_post 内で計算した値が使われる。これは正しい。
しかし JavaScript 側で amount を計算・表示しているが、その値は DB には保存されない(collect_items_from_post が再計算するため)。これは正常。
## 確定バグ(最終)
`pages/detail.php` の受領確認フォームで `confirmed_at` フィールドが `datetime-local` 型で送信されると値は `"2026-06-19T10:30"` 形式になる。これを SQL に `NULLIF(?, '')` でそのまま渡しているが、MySQL の DATETIME 型カラム `confirmed_at` に `"2026-06-19T10:30"` という `T` を含む文字列を INSERT/UPDATE すると、MySQL は `T` を区切り文字として認識せず `0000-00-00 00:00:00` や NULL になるか、strict モードでは **エラー** になる。
## 3. 期待動作
`confirmed_at` は MySQL の DATETIME 型に適合する `"2026-06-19 10:30:00"` 形式でDBに保存されるべきである。
## 4. 修正方針
`pages/detail.php` の POST 処理で `$confirmedAt` を受け取った直後に、`T` を空白に置換し秒を補完する処理を追加する:
```php
$confirmedAt = trim((string)($_POST['confirmed_at'] ?? ''));
if ($confirmedAt !== '') {
// "2026-06-19T10:30" → "2026-06-19 10:30:00"
$confirmedAt = str_replace('T', ' ', $confirmedAt);
if (strlen($confirmedAt) === 16) {
$confirmedAt .= ':00';
}
}
```
この修正により MySQL DATETIME カラムへの正常な保存が保証される。既存の `NULLIF(?, '')` による NULL 処理との整合性も維持される。
💬 返信 (3)
🛠 開発を開始しました (バグ修正 deliverynote-pro)
ご要望ありがとうございます。AI 開発ワーカーが実装を開始します。
通常 5〜30 分で Pull Request を作成し、レビュー後にリリースされます。
ご要望ありがとうございます。AI 開発ワーカーが実装を開始します。
通常 5〜30 分で Pull Request を作成し、レビュー後にリリースされます。
📝 開発が完了しました
ご要望いただいた内容の実装が完了し、最終チェック段階に入りました。
レビュー (自動) → リリース、の流れで進みます。
もう少々お待ちください。
ご要望いただいた内容の実装が完了し、最終チェック段階に入りました。
レビュー (自動) → リリース、の流れで進みます。
もう少々お待ちください。
✅ リリース完了のお知らせ
ご要望いただいた「DeliveryNote Pro - 納品書・受領確認クラウド台帳」を実装し、リリースいたしました。
【ご利用方法】
ダッシュボード: https://www.aiapps.jp/?action=dashboard
アプリ詳細: https://www.aiapps.jp/apps/show.php?slug=deliverynote-pro
デモ環境は 1 時間以内に自動構築されます:
https://www.aiapps.jp/demo/deliverynote-pro/
ご利用ありがとうございます!
ご要望いただいた「DeliveryNote Pro - 納品書・受領確認クラウド台帳」を実装し、リリースいたしました。
【ご利用方法】
ダッシュボード: https://www.aiapps.jp/?action=dashboard
アプリ詳細: https://www.aiapps.jp/apps/show.php?slug=deliverynote-pro
デモ環境は 1 時間以内に自動構築されます:
https://www.aiapps.jp/demo/deliverynote-pro/
ご利用ありがとうございます!
Echo
Iris