diff --git a/docs/intro.html b/docs/intro.html
index 7468e20..650f8cf 100644
--- a/docs/intro.html
+++ b/docs/intro.html
@@ -321,7 +321,7 @@
作業指示書印刷 / PDF
-
+
現品票
diff --git a/docs/masters.md b/docs/masters.md
index 7ede9e7..74aad8f 100644
--- a/docs/masters.md
+++ b/docs/masters.md
@@ -61,7 +61,7 @@ next_title: 生産管理
| 単位 | 個、kg など |
| 品目区分 | 製品/半製品・中間品/部材 |
| 規格 | 任意の規格情報 |
-| 標準不良率(%) | 参考値として保持 |
+| 標準不良率(%) | 製造指図を展開したときの予定材料をこの率ぶん割り増します(0〜99.99) |
登録済みの品目は一覧に表示され、**編集**で内容を変更、**無効化**で以後の選択肢から外せます
(過去の実績は残ります)。
@@ -76,6 +76,7 @@ next_title: 生産管理
子品目(使用する部材)を選びます。
必要数量を入力します。これは親1単位あたりの数量です。
内外製区分と、必要なら代替部品グループを指定します。
+消費工程で、その部材を使う工程(工順の工程順序)を選びます。選ばなければ最終工程で使う扱いになります。
明細に追加を押して行を積み上げます。
すべて追加したら MBOMを保存(一括置換) を押します。
@@ -83,11 +84,19 @@ next_title: 生産管理
保存は一括置換です。画面に表示されている明細で既存の構成が丸ごと置き換わります。
1行だけ足したい場合も、既存行を残したまま追加して保存してください。
-
品目自身を子品目にすることはできません。
+
品目自身を子品目にすることはできません。構成が一周する登録(例:製品の子に半製品、半製品の子に製品)もできません。エラーには一周する経路が表示されます。
+
代替部品グループには、主材料(代替部品にチェックしない行)をちょうど1つ入れ、残りを代替部品にします。代替部品にチェックした行にはグループ名が必要です。
MBOMは、実績入力の**バックフラッシュ**(部材の自動消費)で使われます。
バックフラッシュを使う予定があれば必ず登録してください。
+製造指図を展開すると、MBOMから予定材料が作られます。予定数量は **必要数量 × 指図数量 ÷ (1 − 標準不良率)** です。
+不良になる分も部材を使うため、親品目の標準不良率ぶん多めに見込みます(例:必要数量2・指図数量10・標準不良率20%なら予定数量25)。
+バックフラッシュは各部材を**消費工程の作業指示でだけ**引くので、組立の工程で使う部材は消費工程に組立の工程を選んでください。
+
+
+
消費工程に工順に無い工程順序を選んだままだと、製造指図を展開できません。工順を組み直したときはMBOMの消費工程も見直してください(一覧に「(工順に無し)」と表示されます)。
+
---
@@ -293,6 +302,7 @@ MBOMは、実績入力の**バックフラッシュ**(部材の自動消費)
明細を持つマスタは、**同じキーの行をまとめて1件**として扱い、**その明細を丸ごと置き換えます**。
- **MBOM**:`ParentProductCode`(親品目コード)が同じ行が、その親のMBOMになります。
+ `RoutingSequence` に部材を使う工程の工程順序を書きます(空なら最終工程)。代替部品グループのルールと循環の禁止は、画面から登録したときと同じです。
- **工順**:`ProductCode`(品目コード)が同じ行が、その品目の工順になります。
- **チェックリスト**:`Code` が同じ行が、1つのチェックリストの項目になります。
diff --git a/docs/reference.md b/docs/reference.md
index 11d3364..16766e5 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -116,7 +116,7 @@ next_title: 困ったときは
| 承認しないと展開できない | 製造指図は「承認済」でのみ工程展開できます |
| 工程の順序は強制しない | 前工程が終わっていなくても後工程に着手できます。着手順は差立で管理します |
| 最終工程で在庫計上 | 工順の最後の工程の実績登録時に、良品数が産出ロットとして入庫されます |
-| バックフラッシュはMBOM基準 | MBOM × (良品数 + 不良数) を先入れ先出しで自動消費します |
+| バックフラッシュは予定材料基準 | 指図の予定材料のうち、その工程で使う部材を 原単位 × (良品数 + 不良数) だけ先入れ先出しで自動消費します(代替部品は引きません) |
| ピッキングは先入れ先出し | 有効期限を優先して自動引当されます |
| 出荷には出荷判定が必要 | 承認済みの「可」または「特採」判定がないと出荷実行できません |
| 承認は単段階 | 各種承認は1回の承認操作で確定します(多段階ワークフローはありません) |
diff --git a/docs/reports.md b/docs/reports.md
index dfa5434..6d42e4e 100644
--- a/docs/reports.md
+++ b/docs/reports.md
@@ -16,7 +16,7 @@ next_title: ロールと権限
| 帳票 | どこから出すか | 内容 |
|---|---|---|
-| 作業指示書 | 作業指示 一覧/製造指図 詳細 の **指示書** | 指示番号・指図番号・品目・工程・計画数・担当者・設備・構成部品(MBOM) |
+| 作業指示書 | 作業指示 一覧/製造指図 詳細 の **指示書** | 指示番号・指図番号・品目・工程・計画数・担当者・設備・この工程で使う部材(指図の予定材料) |
| 現品票(ラベル) | 受入/在庫照会/トレーサビリティ の **現品票** | 品目・数量・製造日/受入日・有効期限・ステータス・グレードと、**ロット番号のバーコード** |
| 検査成績書 | 検査詳細の **検査成績書** | 検査項目ごとの規格値・測定値・判定 |
| 出荷伝票 | 出荷管理の **出荷伝票** | 出荷先・予定日・明細 |
@@ -75,4 +75,4 @@ next_title: ロールと権限
- **受入したらすぐ現品票を印刷して貼る。** 以降のピッキング照合・検査・トレースがすべてスキャンで進みます。
- **ピッキングは必ず照合してから実行する。** 一致/不一致の表示は目視確認用で、不一致でも実行ボタンは押せてしまいます。
-- **作業指示書を工程に配る。** 構成部品(MBOM)が載っているので、部材の取り違え防止になります。
+- **作業指示書を工程に配る。** その工程で使う部材(代替部品を含む)が載っているので、部材の取り違え防止になります。
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index 65fde13..e27369b 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -46,6 +46,8 @@ next_title: 管理者向け運用
|---|---|
| 最終工程の実績には入庫先ロケーション(outputLocationId)が必要です。 | **入庫先ロケーション**を選んでから登録してください |
| MBOMが未登録のためバックフラッシュできません。 | [マスタ管理](masters.html#mbom)でMBOMを登録するか、バックフラッシュのチェックを外してください |
+| この工程(工程順序 ○○)で消費する予定材料が無いためバックフラッシュできません。 | この工程で使う部材はありません。バックフラッシュのチェックを外してください。部材を使う工程なら[MBOM](masters.html#mbom)の消費工程を直し、指図を作り直してください |
+| MBOMの部材 '○○' の消費工程(工程順序 ○○)が品目 '○○' の工順にありません。 | [MBOM](masters.html#mbom)の消費工程か工順を直してから展開してください |
| 良品数と不良数の合計は0より大きい必要があります。 | どちらかに数量を入力してください |
| 状態 '○○' の作業指示には実績を記録できません。 | すでに承認済み、または取消済みです |
| 状態 '○○' の作業指示は承認できません(完了済みのみ)。 | 実績を登録して「完了」にしてから承認してください |
diff --git a/docs/walkthrough.md b/docs/walkthrough.md
index 239a7d6..8e96acf 100644
--- a/docs/walkthrough.md
+++ b/docs/walkthrough.md
@@ -240,9 +240,9 @@ MBOMと工順の登録方法は、[マスタ管理の品目・MBOM・工順](mas
SMP-FG-001-20PR-40 最終検査 10台
SMP-FG-001-30PR-50 梱包 10台
産出ロット FG1000-S001最終工程の良品が、このロットで入庫する
-予定材料(MBOM×10台で固定)
-SF-2000 10・ギアセット 10・シャフト 10
-Oリング 20(代替可)・ボルト 40・潤滑油 0.5 L
+予定材料(MBOM×10台÷(1−1.5%)で固定)
+SF-2000 10.15・ギアセット 10.15・シャフト 10.15
+Oリング 20.30(代替可)・ボルト 40.61・潤滑油 0.51 L
予定にない品目は投入できない
製造指図の状態
@@ -270,8 +270,8 @@ MBOMと工順の登録方法は、[マスタ管理の品目・MBOM・工順](mas
-
-製造指図の詳細。予定材料と、工程展開でできた作業指示が並ぶ。各行から段取り・実績入力・指示書の印刷へ進める
+
+製造指図の詳細。予定材料と、工程展開でできた作業指示が並ぶ。予定数量はギアポンプの標準不良率 1.5% ぶん割り増され、部材はどれも組立(工程10)で使う。各行から段取り・実績入力・指示書の印刷へ進める
**差立**では、作業指示ごとに作業者・設備・着手順を割り当てます。作業者には工順の必要スキルが照合され、
diff --git a/samples/actual-csv/README.md b/samples/actual-csv/README.md
index c649ed0..62e3bde 100644
--- a/samples/actual-csv/README.md
+++ b/samples/actual-csv/README.md
@@ -57,5 +57,7 @@ GET /api/actuals/csv/receiving/template … ヘッダーだけのCSV
- 出荷実行には、その出荷指示を対象にした**承認済みの出荷判定(可・特採)**が要ります。判定の無い指示・保留の指示を
出荷実行CSVに書くと、そのファイルは取り込めません。
- `DV-03`(校正期限切れ)を `DeviceCode` に書くと、その検査は取り込めません。
-- 部材はバックフラッシュ(`Backflush=true`)でも消費できます。ただし代替部品は消費されない
- (主材料だけを引く)ので、代替部品を使ったときは投入CSVで理由とともに投入してください。
+- 部材はバックフラッシュ(`Backflush=true`)でも消費できます。引くのは**その工程で使う部材だけ**で、
+ サンプルのMBOMでは部材をすべて最初の工程(`Sequence=10`)で使います。部材を使わない工程(20・30)の行を
+ `Backflush=true` にすると、そのファイルは取り込めません。また代替部品は消費されない(主材料だけを引く)ので、
+ 代替部品を使ったときは投入CSVで理由とともに投入してください。
diff --git a/samples/master-csv/15_bom.csv b/samples/master-csv/15_bom.csv
index 9ff4baf..158a42a 100644
--- a/samples/master-csv/15_bom.csv
+++ b/samples/master-csv/15_bom.csv
@@ -1,9 +1,9 @@
-ParentProductCode,ChildProductCode,QuantityPer,MakeOrBuy,AlternativeGroup,IsAlternative
-FG-1000,SF-2000,1,InHouse,,false
-FG-1000,RM-3002,1,InHouse,,false
-FG-1000,RM-3003,1,InHouse,,false
-FG-1000,RM-3004,2,InHouse,ORING-P20,false
-FG-1000,RM-3007,2,InHouse,ORING-P20,true
-FG-1000,RM-3005,4,InHouse,,false
-FG-1000,RM-3006,0.05,InHouse,,false
-SF-2000,RM-3001,1,Outsourced,,false
+ParentProductCode,ChildProductCode,QuantityPer,MakeOrBuy,AlternativeGroup,IsAlternative,RoutingSequence
+FG-1000,SF-2000,1,InHouse,,false,10
+FG-1000,RM-3002,1,InHouse,,false,10
+FG-1000,RM-3003,1,InHouse,,false,10
+FG-1000,RM-3004,2,InHouse,ORING-P20,false,10
+FG-1000,RM-3007,2,InHouse,ORING-P20,true,10
+FG-1000,RM-3005,4,InHouse,,false,10
+FG-1000,RM-3006,0.05,InHouse,,false,10
+SF-2000,RM-3001,1,Outsourced,,false,10
diff --git a/samples/master-csv/README.md b/samples/master-csv/README.md
index a2d9d30..d84b836 100644
--- a/samples/master-csv/README.md
+++ b/samples/master-csv/README.md
@@ -38,7 +38,7 @@ RM-3001 ハウジング鋳造品
| 12 | `12_inspection-items.csv` | 検査項目・基準 | 規格値あり/なし、受入・工程内・完成品・サンプル検査 |
| 13 | `13_control-items.csv` | 工程管理項目 | 指示値と許容範囲(製造条件の逸脱判定に使う) |
| 14 | `14_inspection-devices.csv` | 検査機・測定器 | 校正期限切れの機器を1台含む(システム管理者のみ取込可) |
-| 15 | `15_bom.csv` | 品目・MBOM・工順 | FG-1000 と SF-2000 のMBOM。Oリングに代替部品あり |
+| 15 | `15_bom.csv` | 品目・MBOM・工順 | FG-1000 と SF-2000 のMBOM。Oリングに代替部品あり。部材はどれも最初の工程(工程10)で消費する |
| 16 | `16_work-procedures.csv` | 作業手順書 | 手順を本文に書く例と、所在だけを書く例(システム管理者のみ取込可) |
| 17 | `17_routing.csv` | 品目・MBOM・工順 | 候補設備・作業区・治工具・スキル・チェックリスト・手順書を紐づけた工順 |
| 18 | `18_users.csv` | ユーザー | 各ロールの担当者。作業場所・所属・直つき(システム管理者のみ取込可) |
@@ -66,6 +66,8 @@ RM-3001 ハウジング鋳造品
## 試せる業務のポイント
- `RM-3007`(耐油Oリング)は `RM-3004` の代替部品です。投入するときは理由の入力が求められます
+- `FG-1000` の標準不良率は 1.5%、`SF-2000` は 2% です。製造指図を展開すると、予定材料がこの率ぶん割り増されます
+ (例:ギアポンプ10台の指図で、ギアセットの予定数量は 10 ÷ (1 − 0.015) = 10.152285)
- `14_inspection-devices.csv` の `DV-03` は校正期限切れです。検査実績の測定器に選ぶと登録できません
- `SF-2000` の機械加工は `EQ-01` と `EQ-05` のどちらでも作業できます(候補設備)
- `op003` は夜勤(20:00〜翌05:00)の作業者です。直別の集計を試せます
diff --git a/src/MesApp.Api/Controllers/ManufacturingOrdersController.cs b/src/MesApp.Api/Controllers/ManufacturingOrdersController.cs
index 59aed62..60e3f63 100644
--- a/src/MesApp.Api/Controllers/ManufacturingOrdersController.cs
+++ b/src/MesApp.Api/Controllers/ManufacturingOrdersController.cs
@@ -86,7 +86,7 @@ public async Task
> Get(int id, Ca
order.Materials.OrderBy(m => m.ChildProduct!.Code)
.Select(m => new OrderMaterialResponse(
m.ChildProductId, m.ChildProduct!.Code, m.ChildProduct!.Name,
- m.QuantityPer, m.PlannedQuantity, m.AlternativeGroup)).ToList());
+ m.QuantityPer, m.PlannedQuantity, m.AlternativeGroup, m.RoutingSequence)).ToList());
}
/// 指図登録(A-20-10-01 手動登録、B-10-10-04 突発、B-70-10-01 リワーク)
diff --git a/src/MesApp.Api/Controllers/ProductsController.cs b/src/MesApp.Api/Controllers/ProductsController.cs
index 64b4392..fb708f5 100644
--- a/src/MesApp.Api/Controllers/ProductsController.cs
+++ b/src/MesApp.Api/Controllers/ProductsController.cs
@@ -147,7 +147,7 @@ public async Task>> GetBom(int id, Cancellati
.OrderBy(b => b.ChildProduct!.Code)
.Select(b => new BomItemResponse(
b.Id, b.ChildProductId, b.ChildProduct!.Code, b.ChildProduct!.Name,
- b.QuantityPer, b.MakeOrBuy, b.AlternativeGroup, b.IsAlternative))
+ b.QuantityPer, b.MakeOrBuy, b.AlternativeGroup, b.IsAlternative, b.RoutingSequence))
.ToListAsync(ct);
}
diff --git a/src/MesApp.Api/Localization/ApiText.en.resx b/src/MesApp.Api/Localization/ApiText.en.resx
index 7cca82f..73ff611 100644
--- a/src/MesApp.Api/Localization/ApiText.en.resx
+++ b/src/MesApp.Api/Localization/ApiText.en.resx
@@ -1380,8 +1380,8 @@
Substitute part
-
- true / false. Consuming a true row requires recording a reason
+
+ true / false. A true row requires AlternativeGroup, and consuming it requires recording a reason
Routing (BOP)
@@ -1704,8 +1704,8 @@
Backflush
-
- true consumes planned materials × (good + defect) automatically by FIFO
+
+ true consumes the planned materials used at this step (quantity per × (good + defect)) first-in first-out (substitutes are not consumed)
Defects by reason
@@ -1983,4 +1983,37 @@
A number greater than 0. Together with the quantity already shipped, it cannot exceed the instructed quantity.
+
+ The consuming step (sequence {1}) of MBOM material '{0}' is not in the routing of item '{2}'. Correct the MBOM or the routing.
+
+
+ Cannot backflush because no planned materials are consumed at this step (sequence {0}).
+
+
+ Consuming step (sequence)
+
+
+ Sequence in the parent item's routing. Blank means the final step. Backflush consumes the material at this step
+
+
+ 0 to 99.99. Planned materials of an order are increased by this rate
+
+
+ The MBOM is circular ({0}).
+
+
+ A substitute row needs a substitute group (child item '{0}').
+
+
+ Substitute group '{0}' has no primary material (a row that is not a substitute).
+
+
+ Substitute group '{0}' has more than one primary material ({1}). Keep one primary and mark the others as substitutes.
+
+
+ Parent item '{0}': {1}
+
+
+ Exactly one primary material (IsAlternative=false) per group
+
diff --git a/src/MesApp.Api/Policies/ProductStructurePolicy.cs b/src/MesApp.Api/Policies/ProductStructurePolicy.cs
index 199d484..9e5e843 100644
--- a/src/MesApp.Api/Policies/ProductStructurePolicy.cs
+++ b/src/MesApp.Api/Policies/ProductStructurePolicy.cs
@@ -1,3 +1,4 @@
+using MesApp.Api.Localization;
using MesApp.Core.Entities;
namespace MesApp.Api.Policies;
@@ -35,4 +36,95 @@ public static IEnumerable CandidateEquipmentIds(IEnumerable? candidate
(candidates ?? [])
.Concat(representative is { } id ? [id] : [])
.Distinct();
+
+ ///
+ /// MBOMの代替部品グループの整合(A-40-10-04)。問題があれば日本語の理由を返す。
+ ///
+ /// - 代替部品の行は代替部品グループを持つ(どの主材料の代わりかが決まらないと、投入の照合で意味を持たない)
+ /// - グループの主材料(代替部品でない行)はちょうど1つ。無いとバックフラッシュがそのグループの部材を何も引かず、
+ /// 2つ以上だと代わりになるはずの部材を両方とも原単位ぶん引く(Spec.md 3.9)
+ ///
+ /// グループ名は前後の空白を除いて比べる()
+ ///
+ public static string? CheckAlternativeGroups(IEnumerable<(string ChildCode, string? Group, bool IsAlternative)> lines)
+ {
+ var normalized = lines.Select(l => (l.ChildCode, Group: NormalizeAlternativeGroup(l.Group), l.IsAlternative)).ToList();
+ if (normalized.FirstOrDefault(l => l.IsAlternative && l.Group is null) is { ChildCode: { } orphan })
+ {
+ return ApiText.T("代替部品の行には代替部品グループが必要です(子品目 '{0}')。", orphan);
+ }
+ foreach (var group in normalized.Where(l => l.Group is not null).GroupBy(l => l.Group!, StringComparer.Ordinal))
+ {
+ var primaries = group.Where(l => !l.IsAlternative).Select(l => l.ChildCode).ToList();
+ if (primaries.Count == 0)
+ {
+ return ApiText.T("代替部品グループ '{0}' に主材料(代替部品でない行)がありません。", group.Key);
+ }
+ if (primaries.Count > 1)
+ {
+ return ApiText.T("代替部品グループ '{0}' に主材料が複数あります({1})。主材料は1つにし、他は代替部品にしてください。",
+ group.Key, string.Join(", ", primaries));
+ }
+ }
+ return null;
+ }
+
+ /// 代替部品グループ名の正規化(前後の空白を除き、空なら未指定)。保存とグループの比較で同じ形にする
+ public static string? NormalizeAlternativeGroup(string? group) =>
+ string.IsNullOrWhiteSpace(group) ? null : group.Trim();
+
+ ///
+ /// MBOMの循環(A→B→…→A)を探す(A-40-10-01)。親品目の明細を に置き換えたとき、
+ /// 子品目から既存の明細をたどって親品目へ戻れるなら、その経路(親品目で始まり親品目で終わる品目IDの並び)を返す。
+ /// 循環がなければ null。
+ ///
+ /// 今の展開は1段だけなので循環があっても止まらないが、構成として作れない品目になる。
+ /// 多段の所要量展開や逆展開を足したときに無限ループになるため、登録の時点で止める。
+ /// の親品目自身の明細は、置き換えられるので見ない
+ ///
+ ///
+ public static IReadOnlyList? FindBomCycle(
+ IReadOnlyDictionary> childrenByParent, int parentId, IEnumerable childIds)
+ {
+ // 幅優先でたどり、見つけた品目の手前を覚えておいて経路を組み立てる
+ var previous = new Dictionary();
+ var queue = new Queue();
+ foreach (var child in childIds.Distinct())
+ {
+ if (child == parentId)
+ {
+ return [parentId, parentId];
+ }
+ previous[child] = parentId;
+ queue.Enqueue(child);
+ }
+ while (queue.Count > 0)
+ {
+ var current = queue.Dequeue();
+ if (current == parentId || !childrenByParent.TryGetValue(current, out var next))
+ {
+ continue;
+ }
+ foreach (var child in next)
+ {
+ if (child == parentId)
+ {
+ var path = new List { parentId, current };
+ for (var at = current; previous[at] != parentId; at = previous[at])
+ {
+ path.Add(previous[at]);
+ }
+ path.Add(parentId);
+ // 末尾から組んだので、親品目→子品目→…→親品目の順に並べ直す
+ path.Reverse(1, path.Count - 2);
+ return path;
+ }
+ if (previous.TryAdd(child, current))
+ {
+ queue.Enqueue(child);
+ }
+ }
+ }
+ return null;
+ }
}
diff --git a/src/MesApp.Api/Services/ActualCsvKinds.cs b/src/MesApp.Api/Services/ActualCsvKinds.cs
index e5bf0f5..dca7088 100644
--- a/src/MesApp.Api/Services/ActualCsvKinds.cs
+++ b/src/MesApp.Api/Services/ActualCsvKinds.cs
@@ -101,7 +101,7 @@ public static class ActualCsvKinds
new("StartedAt", "開始日時", true, DateTimeNote + "。この時刻で直を決める"),
new("EndedAt", "終了日時", false, DateTimeNote),
new("OutputLocationCode", "入庫先ロケーションコード", false, "最終工程で良品があるときは必須"),
- new("Backflush", "バックフラッシュ", false, "true で予定材料×(良品+不良)を先入れ先出しで自動消費"),
+ new("Backflush", "バックフラッシュ", false, "true でこの工程で使う予定材料を 原単位×(良品+不良) だけ先入れ先出しで自動消費(代替部品は引かない)"),
new("Defects", "不良理由別の内訳", false, "不良理由コード=数量 をセミコロン区切り(DR-02=2;DR-03=1)。合計は不良数以下"),
]), MesRoleGroups.ShopFloorRecord),
new(new CsvKindInfo(DataRecords, "製造条件データ", false,
diff --git a/src/MesApp.Api/Services/ManufacturingOrderService.cs b/src/MesApp.Api/Services/ManufacturingOrderService.cs
index bfacd10..d0d07e6 100644
--- a/src/MesApp.Api/Services/ManufacturingOrderService.cs
+++ b/src/MesApp.Api/Services/ManufacturingOrderService.cs
@@ -213,6 +213,21 @@ public async Task ExpandAsync(ManufacturingOrder order, string? lo
return OrderOutcome.Invalid(ApiText.T("品目 '{0}' に工順(BOP)が登録されていません。", order.Product!.Code));
}
+ // MBOMの消費工程は工順の工程順序で指す。MBOMと工順は別々に改訂できるため、ずれはここで止める
+ // (見過ごすと、どの作業指示のバックフラッシュでも引かれない部材ができる)
+ var bom = await db.BomItems
+ .Include(b => b.ChildProduct)
+ .Where(b => b.ParentProductId == order.ProductId)
+ .ToListAsync(ct);
+ var sequences = routing.Select(r => r.Sequence).ToHashSet();
+ if (bom.FirstOrDefault(b => b.RoutingSequence is { } s && !sequences.Contains(s)) is { } orphan)
+ {
+ return OrderOutcome.Invalid(ApiText.T(
+ "MBOMの部材 '{0}' の消費工程(工程順序 {1})が品目 '{2}' の工順にありません。MBOMか工順を直してください。",
+ orphan.ChildProduct!.Code, orphan.RoutingSequence!.Value, order.Product!.Code));
+ }
+ var finalSequence = routing[^1].Sequence;
+
// 産出ロット採番(手入力があれば一意性を確認して使用)
if (string.IsNullOrWhiteSpace(lotNumber))
{
@@ -289,10 +304,8 @@ .. controlItems
}
// MBOMも展開時点で予定材料として固定する。以降の投入照合(B-30-20-01)と
- // バックフラッシュ(B-40-10-09)はこの予定材料を基準にする
- var bom = await db.BomItems
- .Where(b => b.ParentProductId == order.ProductId)
- .ToListAsync(ct);
+ // バックフラッシュ(B-40-10-09)はこの予定材料を基準にする。
+ // 消費工程が未指定の行は最終工程に解決して入れ、以降は工順の改訂に左右されないようにする
foreach (var item in bom)
{
db.ManufacturingOrderMaterials.Add(new ManufacturingOrderMaterial
@@ -300,9 +313,10 @@ .. controlItems
ManufacturingOrder = order,
ChildProductId = item.ChildProductId,
QuantityPer = item.QuantityPer,
- PlannedQuantity = item.QuantityPer * order.Quantity,
+ PlannedQuantity = PlannedMaterialQuantity(item.QuantityPer, order.Quantity, order.Product!.StandardDefectRate),
AlternativeGroup = item.AlternativeGroup,
IsAlternative = item.IsAlternative,
+ RoutingSequence = item.RoutingSequence ?? finalSequence,
});
}
@@ -320,4 +334,15 @@ await auditLogger.LogAsync("Production", "Expand", nameof(ManufacturingOrder), o
}, ct: ct);
return OrderOutcome.Ok(order);
}
+
+ ///
+ /// 予定材料の数量(A-40-10-04)。親品目の標準不良率ぶん割り増す:原単位×指図数量÷(1-率)。
+ /// 不良品も部材を消費するため、良品を指図数量だけ得るには不良になる分の部材も要る。
+ /// 割り切れない値は列の精度(小数6桁)で切り上げる(切り捨てると欠品側にずれる)
+ ///
+ internal static decimal PlannedMaterialQuantity(decimal quantityPer, decimal orderQuantity, decimal defectRatePercent)
+ {
+ var quantity = quantityPer * orderQuantity / (1m - defectRatePercent / 100m);
+ return Math.Ceiling(quantity * 1_000_000m) / 1_000_000m;
+ }
}
diff --git a/src/MesApp.Api/Services/MasterCsvKinds.cs b/src/MesApp.Api/Services/MasterCsvKinds.cs
index cf9e31f..2d7fe83 100644
--- a/src/MesApp.Api/Services/MasterCsvKinds.cs
+++ b/src/MesApp.Api/Services/MasterCsvKinds.cs
@@ -40,7 +40,7 @@ public static class MasterCsvKinds
new("Unit", "単位", true, "個・kg・m など"),
new("Specification", "規格", false, null),
new("Type", "品目区分", false, "Product(製品)/ SemiFinished(半製品)/ Material(部材)"),
- new("StandardDefectRate", "標準不良率(%)", false, "0〜100"),
+ new("StandardDefectRate", "標準不良率(%)", false, "0〜99.99。指図の予定材料をこの率ぶん割り増す"),
new("DefaultLocationCode", "既定ロケーション", false, "推奨ロケーションの第一候補。空欄で解除"),
new("IsActive", "有効", false, "true / false。falseで無効化"),
]),
@@ -161,8 +161,9 @@ public static class MasterCsvKinds
new("ChildProductCode", "子品目コード", true, null),
new("QuantityPer", "必要数量(親1単位)", true, "0より大きい数値"),
new("MakeOrBuy", "内外製区分", false, "InHouse(内製)/ Outsourced(外注)"),
- new("AlternativeGroup", "代替部品グループ", false, null),
- new("IsAlternative", "代替部品", false, "true / false。trueの行を投入するには理由の記録が必要"),
+ new("AlternativeGroup", "代替部品グループ", false, "グループの主材料(IsAlternative=false)はちょうど1つ"),
+ new("IsAlternative", "代替部品", false, "true / false。trueの行はAlternativeGroupが必須で、投入するには理由の記録が必要"),
+ new("RoutingSequence", "消費工程(工程順序)", false, "親品目の工順の工程順序。空なら最終工程。バックフラッシュはこの工程で部材を引く"),
]),
new(Routing, "工順(BOP)", false,
[
diff --git a/src/MesApp.Api/Services/MasterCsvService.Import.cs b/src/MesApp.Api/Services/MasterCsvService.Import.cs
index bba9cb7..ef2b417 100644
--- a/src/MesApp.Api/Services/MasterCsvService.Import.cs
+++ b/src/MesApp.Api/Services/MasterCsvService.Import.cs
@@ -20,6 +20,12 @@ private sealed class ImportCounter
/// 取り込めたが伝えたいこと(エラーと違いロールバックしない。CsvImportResult.Warnings)
public List Warnings { get; } = [];
+
+ ///
+ /// 保存の後に書く個別の監査ログ(対象の種類・ID・内容)。監査ログは書くたびに保存するため、
+ /// 取込の途中で書くと検証前の変更まで保存してしまう。全行の検証が通って保存した後にまとめて書く
+ ///
+ public List<(string TargetType, string TargetId, object Detail)> Audits { get; } = [];
}
public async Task ImportAsync(
@@ -113,6 +119,10 @@ private async Task ImportOneAsync(
await db.SaveChangesAsync(ct);
await auditLogger.LogAsync("Master", "CsvImport", kind.Kind, null,
detail: $"rows={table.Rows.Count}, created={counter.Created}, updated={counter.Updated}", ct: ct);
+ foreach (var (targetType, targetId, detail) in counter.Audits)
+ {
+ await auditLogger.LogAsync("Master", "Update", targetType, targetId, detail: detail, ct: ct);
+ }
}
return new CsvFileImportCount(counter.Created, counter.Updated) { Warnings = counter.Warnings };
}
@@ -144,7 +154,7 @@ private async Task ImportProductsAsync(
var unit = reader.RequiredText("Unit", 20);
var specification = reader.Text("Specification", product.Specification);
var type = reader.Enum("Type", product.Type, CsvEnumLabels.ProductTypes);
- var defectRate = reader.Number("StandardDefectRate", product.StandardDefectRate, 0, 100);
+ var defectRate = reader.Number("StandardDefectRate", product.StandardDefectRate, 0, 99.99m);
var defaultLocationId = reader.Reference(
"DefaultLocationCode", product.DefaultLocationId, locationIds, "ロケーション");
var isActive = reader.Bool("IsActive", product.IsActive);
@@ -905,6 +915,10 @@ private async Task ImportBomAsync(
{
var productIds = await ProductIdsAsync(ct);
var existing = await db.BomItems.ToListAsync(ct);
+ // 循環の判定に使う明細。取り込んだ親品目の分はその都度置き換え、後の行が前の行と循環するのも捕まえる
+ var edges = existing.GroupBy(b => b.ParentProductId)
+ .ToDictionary(g => g.Key, g => g.Select(b => b.ChildProductId).ToList());
+ var codeById = productIds.ToDictionary(p => p.Value, p => p.Key);
foreach (var group in GroupRows(table, "ParentProductCode", errors))
{
@@ -928,6 +942,7 @@ private async Task ImportBomAsync(
var makeOrBuy = reader.Enum("MakeOrBuy", MakeOrBuy.InHouse, CsvEnumLabels.MakeOrBuys);
var alternativeGroup = reader.Text("AlternativeGroup", null, 50);
var isAlternative = reader.Bool("IsAlternative", false);
+ var routingSequence = reader.IntOrNull("RoutingSequence", null, 1);
if (quantity is null && !reader.Failed)
{
reader.Fail(ApiText.T("QuantityPer(必要数量)は必須です。"));
@@ -951,16 +966,31 @@ private async Task ImportBomAsync(
ChildProductId = childId!.Value,
QuantityPer = quantity!.Value,
MakeOrBuy = makeOrBuy,
- AlternativeGroup = alternativeGroup,
+ AlternativeGroup = ProductStructurePolicy.NormalizeAlternativeGroup(alternativeGroup),
IsAlternative = isAlternative,
+ RoutingSequence = routingSequence,
});
}
if (failed)
{
continue;
}
+ if (ProductStructurePolicy.CheckAlternativeGroups(
+ lines.Select(l => (codeById[l.ChildProductId], l.AlternativeGroup, l.IsAlternative))) is { } alternativeError)
+ {
+ parentReader.Fail(ApiText.T("親品目 '{0}':{1}", parentCode, alternativeError));
+ continue;
+ }
+ if (ProductStructurePolicy.FindBomCycle(edges, parentId, lines.Select(l => l.ChildProductId)) is { } cycle)
+ {
+ parentReader.Fail(ApiText.T("MBOMが循環します({0})。", string.Join(" → ", cycle.Select(id => codeById[id]))));
+ continue;
+ }
+ edges[parentId] = [.. lines.Select(l => l.ChildProductId)];
var current = existing.Where(b => b.ParentProductId == parentId).ToList();
+ // 設計変更の追跡(A-40-10-05)は単票APIと同じ形で残す。CSVから改訂すると中身が追えない、にしない
+ counter.Audits.Add(("Bom", parentId.ToString(), ProductStructureService.BomChangeDetail(current, lines, codeById)));
db.BomItems.RemoveRange(current);
db.BomItems.AddRange(lines);
if (current.Count > 0)
diff --git a/src/MesApp.Api/Services/MasterCsvService.cs b/src/MesApp.Api/Services/MasterCsvService.cs
index fdb44d9..ba6ac34 100644
--- a/src/MesApp.Api/Services/MasterCsvService.cs
+++ b/src/MesApp.Api/Services/MasterCsvService.cs
@@ -217,6 +217,7 @@ public async Task ExportAsync(CsvKindInfo kind, bool includeInactive, Ca
{
b.ParentProduct!.Code, b.ChildProduct!.Code, Num(b.QuantityPer),
b.MakeOrBuy.ToString(), b.AlternativeGroup, Bool(b.IsAlternative),
+ b.RoutingSequence?.ToString(CultureInfo.InvariantCulture),
})];
}
diff --git a/src/MesApp.Api/Services/ProductStructureService.cs b/src/MesApp.Api/Services/ProductStructureService.cs
index ffad404..63ba9e8 100644
--- a/src/MesApp.Api/Services/ProductStructureService.cs
+++ b/src/MesApp.Api/Services/ProductStructureService.cs
@@ -28,30 +28,76 @@ public sealed class ProductStructureService(MesAppDbContext db, IAuditLogger aud
}
var childIds = items.Select(i => i.ChildProductId).ToList();
- var validChildIds = await db.Products
- .Where(p => childIds.Contains(p.Id)).Select(p => p.Id).ToListAsync(ct);
- if (childIds.Except(validChildIds).Any())
+ var childCodes = await db.Products
+ .Where(p => childIds.Contains(p.Id)).ToDictionaryAsync(p => p.Id, p => p.Code, ct);
+ if (childIds.Except(childCodes.Keys).Any())
{
return ApiText.T("存在しない子品目IDが含まれています。");
}
+ if (ProductStructurePolicy.CheckAlternativeGroups(
+ items.Select(i => (childCodes[i.ChildProductId], i.AlternativeGroup, i.IsAlternative))) is { } alternativeError)
+ {
+ return alternativeError;
+ }
+
+ var edges = (await db.BomItems.AsNoTracking()
+ .Select(b => new { b.ParentProductId, b.ChildProductId })
+ .ToListAsync(ct))
+ .GroupBy(b => b.ParentProductId)
+ .ToDictionary(g => g.Key, g => g.Select(b => b.ChildProductId).ToList());
+ if (ProductStructurePolicy.FindBomCycle(edges, productId, childIds) is { } cycle)
+ {
+ var cycleCodes = await db.Products.AsNoTracking()
+ .Where(p => cycle.Contains(p.Id)).ToDictionaryAsync(p => p.Id, p => p.Code, ct);
+ return ApiText.T("MBOMが循環します({0})。", string.Join(" → ", cycle.Select(id => cycleCodes[id])));
+ }
var existing = await db.BomItems.Where(b => b.ParentProductId == productId).ToListAsync(ct);
- db.BomItems.RemoveRange(existing);
- db.BomItems.AddRange(items.Select(i => new BomItem
+ var lines = items.Select(i => new BomItem
{
ParentProductId = productId,
ChildProductId = i.ChildProductId,
QuantityPer = i.QuantityPer,
MakeOrBuy = i.MakeOrBuy,
- AlternativeGroup = i.AlternativeGroup,
+ AlternativeGroup = ProductStructurePolicy.NormalizeAlternativeGroup(i.AlternativeGroup),
IsAlternative = i.IsAlternative,
- }));
+ RoutingSequence = i.RoutingSequence,
+ }).ToList();
+ var codeIds = existing.Select(b => b.ChildProductId).Except(childCodes.Keys).ToList();
+ var codes = childCodes.Concat(await db.Products.AsNoTracking()
+ .Where(p => codeIds.Contains(p.Id)).ToDictionaryAsync(p => p.Id, p => p.Code, ct))
+ .ToDictionary();
+ var detail = BomChangeDetail(existing, lines, codes);
+ db.BomItems.RemoveRange(existing);
+ db.BomItems.AddRange(lines);
await db.SaveChangesAsync(ct);
- await auditLogger.LogAsync("Master", "Update", "Bom", productId.ToString(),
- detail: $"items={items.Count}", ct: ct);
+ await auditLogger.LogAsync("Master", "Update", "Bom", productId.ToString(), detail: detail, ct: ct);
return null;
}
+ ///
+ /// MBOM改訂の監査ログの内容(変更前後の明細。A-40-10-05 設計変更)。
+ /// 一括置換なので件数だけでは何を変えたか追えない。子品目はIDでなくコードで残す(品目を無効化・改名しても読めるように)。
+ /// 単票APIとマスタCSV取込の両方で同じ形にする
+ ///
+ internal static object BomChangeDetail(
+ IEnumerable before, IEnumerable after, IReadOnlyDictionary codeById)
+ {
+ object[] Snapshot(IEnumerable lines) =>
+ [
+ .. lines.OrderBy(b => codeById[b.ChildProductId], StringComparer.Ordinal).Select(b => new
+ {
+ child = codeById[b.ChildProductId],
+ quantityPer = b.QuantityPer,
+ makeOrBuy = b.MakeOrBuy.ToString(),
+ alternativeGroup = b.AlternativeGroup,
+ isAlternative = b.IsAlternative,
+ routingSequence = b.RoutingSequence,
+ }),
+ ];
+ return new { before = Snapshot(before), after = Snapshot(after), reason = (string?)null };
+ }
+
/// 工順(BOP)の一括置換(工程変更 A-40-20-03、I-50-30 も本処理で反映)
public async Task ReplaceRoutingAsync(int productId, List steps, CancellationToken ct)
{
diff --git a/src/MesApp.Api/Services/WorkOrderExecutionService.cs b/src/MesApp.Api/Services/WorkOrderExecutionService.cs
index 1827705..f9f9346 100644
--- a/src/MesApp.Api/Services/WorkOrderExecutionService.cs
+++ b/src/MesApp.Api/Services/WorkOrderExecutionService.cs
@@ -343,13 +343,20 @@ public async Task> AddProductionRecordAsync(
{
// 消費量の基準は展開時に固定した予定材料の原単位(Spec.md 5.7)。
// 代替部品の行は除く:主材料の代わりに使う部材なので、両方を原単位ぶん引くと二重に減る。
- // 代替の投入には理由が要る(Spec.md 3.9)ため、代替部品は手動の部材投入で理由とともに記録する
+ // 代替の投入には理由が要る(Spec.md 3.9)ため、代替部品は手動の部材投入で理由とともに記録する。
+ // 引くのはこの作業指示の工程で消費する部材だけ。指図の全部材を引くと、工程ごとに
+ // バックフラッシュしたとき工程数ぶん二重に減る。消費工程が null の行(項目追加前の指図)は最終工程で引く
+ var sequence = workOrder.RoutingSequence;
var bom = await db.ManufacturingOrderMaterials
- .Where(m => m.ManufacturingOrderId == workOrder.ManufacturingOrderId && !m.IsAlternative)
+ .Where(m => m.ManufacturingOrderId == workOrder.ManufacturingOrderId && !m.IsAlternative
+ && (m.RoutingSequence == sequence || (m.RoutingSequence == null && isFinalStep)))
.ToListAsync(ct);
if (bom.Count == 0)
{
- return Outcome.Invalid(ApiText.T("予定材料が未登録(展開時にMBOMが未登録)のためバックフラッシュできません。"));
+ return Outcome.Invalid(
+ await db.ManufacturingOrderMaterials.AnyAsync(m => m.ManufacturingOrderId == workOrder.ManufacturingOrderId, ct)
+ ? ApiText.T("この工程(工程順序 {0})で消費する予定材料が無いためバックフラッシュできません。", sequence)
+ : ApiText.T("予定材料が未登録(展開時にMBOMが未登録)のためバックフラッシュできません。"));
}
var totalProduced = request.GoodQuantity + request.DefectQuantity;
foreach (var bomItem in bom)
diff --git a/src/MesApp.Client.Web/Pages/ManufacturingOrderDetail.razor b/src/MesApp.Client.Web/Pages/ManufacturingOrderDetail.razor
index 0d22415..1afcaea 100644
--- a/src/MesApp.Client.Web/Pages/ManufacturingOrderDetail.razor
+++ b/src/MesApp.Client.Web/Pages/ManufacturingOrderDetail.razor
@@ -105,7 +105,7 @@ else
@L["予定材料(展開時のMBOMで確定)"]
- | @L["品目"] | @L["原単位"] | @L["予定数量"] | @L["代替部品グループ"] |
+ | @L["品目"] | @L["原単位"] | @L["予定数量"] | @L["代替部品グループ"] | @L["消費工程"] |
@foreach (var material in _detail.Materials)
@@ -115,6 +115,7 @@ else
@material.QuantityPer |
@material.PlannedQuantity |
@(material.AlternativeGroup ?? "-") |
+ @(material.RoutingSequence?.ToString() ?? L["最終工程"].Value) |
}
diff --git a/src/MesApp.Client.Web/Pages/Masters/Products/ProductBomEditor.razor b/src/MesApp.Client.Web/Pages/Masters/Products/ProductBomEditor.razor
index 3deccf5..efddcd1 100644
--- a/src/MesApp.Client.Web/Pages/Masters/Products/ProductBomEditor.razor
+++ b/src/MesApp.Client.Web/Pages/Masters/Products/ProductBomEditor.razor
@@ -5,7 +5,7 @@
@L["{0} {1} の MBOM(A-40-10)", Product.Code, Product.Name]
- | @L["子品目"] | @L["必要数量(親1単位)"] | @L["内外製"] | @L["代替グループ"] | @L["代替部品"] | |
+ | @L["子品目"] | @L["必要数量(親1単位)"] | @L["内外製"] | @L["代替グループ"] | @L["代替部品"] | @L["消費工程"] | |
@foreach (var line in _lines)
{
@@ -15,6 +15,7 @@
@EnumLabels.Of(line.MakeOrBuy) |
@(line.AlternativeGroup ?? "-") |
@(line.IsAlternative ? L["代替"] : L["主"]) |
+ @StepLabel(line.RoutingSequence) |
|
}
@@ -54,6 +55,17 @@
@L["代替部品(主材料でない)"]
+ @* バックフラッシュはこの工程の作業指示で部材を引く(B-40-10-09) *@
+
+
+
+
@@ -73,6 +85,7 @@
[Parameter] public EventCallback OnSaved { get; set; }
private readonly List _lines = [];
+ private List _routing = [];
private int _loadedProductId;
private int _childId;
@@ -80,6 +93,7 @@
private MakeOrBuy _makeOrBuy = MakeOrBuy.InHouse;
private string? _altGroup;
private bool _isAlternative;
+ private int _routingSequence;
private string? _error;
private string? _message;
@@ -101,7 +115,9 @@
var bom = await Http.GetFromJsonAsync>($"api/products/{Product.Id}/bom") ?? [];
_lines.AddRange(bom.Select(b =>
new BomItemRequest(b.ChildProductId, b.QuantityPer, b.MakeOrBuy, b.AlternativeGroup,
- b.IsAlternative)));
+ b.IsAlternative, b.RoutingSequence)));
+ // 消費工程の選択肢。工順は隣の編集欄で直せるため、MBOMを読み直すたびに取り直す
+ _routing = await Http.GetFromJsonAsync>($"api/products/{Product.Id}/routing") ?? [];
// 親から呼ばれたときはパラメータが変わらないため、自分で再描画を要求する
StateHasChanged();
}
@@ -115,11 +131,20 @@
}
_error = null;
_lines.Add(new BomItemRequest(_childId, _quantity, _makeOrBuy,
- string.IsNullOrWhiteSpace(_altGroup) ? null : _altGroup, _isAlternative));
+ string.IsNullOrWhiteSpace(_altGroup) ? null : _altGroup, _isAlternative,
+ _routingSequence == 0 ? null : _routingSequence));
_childId = 0;
_isAlternative = false;
}
+ /// 消費工程の表示。工順に無い工程順序は指図の展開で拒否されるため、ここで目立たせる
+ private string StepLabel(int? sequence) => sequence switch
+ {
+ null => L["最終工程"],
+ { } s when _routing.FirstOrDefault(r => r.Sequence == s) is { } step => $"{s} {step.ProcessName}",
+ { } s => L["{0}(工順に無し)", s],
+ };
+
private async Task SaveAsync()
{
_busy = true;
diff --git a/src/MesApp.Client.Web/Pages/Masters/ProductsTab.razor b/src/MesApp.Client.Web/Pages/Masters/ProductsTab.razor
index 6dcb774..7d73590 100644
--- a/src/MesApp.Client.Web/Pages/Masters/ProductsTab.razor
+++ b/src/MesApp.Client.Web/Pages/Masters/ProductsTab.razor
@@ -34,7 +34,7 @@
-
+
- @L["構成部品(MBOM)"]
- @if (_bom.Count == 0)
+ @L["この工程で使う部材(予定材料)"]
+ @if (_materials.Count == 0)
{
- @L["MBOMは登録されていません。"]
+ @L["この工程で使う部材はありません。"]
}
else
{
- | @L["子品目"] | @L["品目名"] | @L["必要数量(計画数分)"] | @L["内外製"] |
+ | @L["子品目"] | @L["品目名"] | @L["必要数量(計画数分)"] | @L["代替部品グループ"] |
- @foreach (var line in _bom)
+ @foreach (var line in _materials)
{
- | @line.ChildProductCode |
- @line.ChildProductName |
- @(line.QuantityPer * _workOrder.PlannedQuantity) |
- @EnumLabels.Of(line.MakeOrBuy) |
+ @line.ProductCode |
+ @line.ProductName |
+ @RequiredQuantity(line) |
+ @(line.AlternativeGroup ?? "-") |
}
@@ -76,15 +77,32 @@ else
[Parameter] public int Id { get; set; }
private WorkOrderResponse? _workOrder;
- private List _bom = [];
+ private List _materials = [];
+ private decimal _orderQuantity;
protected override async Task OnParametersSetAsync()
{
_workOrder = await Http.GetFromJsonAsync($"api/work-orders/{Id}");
if (_workOrder is not null)
{
- _bom = await Http.GetFromJsonAsync>(
- $"api/products/{_workOrder.ProductId}/bom") ?? [];
+ var order = await Http.GetFromJsonAsync(
+ $"api/manufacturing-orders/{_workOrder.ManufacturingOrderId}");
+ // 消費工程が空の行は消費工程の追加前に展開した指図のもので、最終工程で使う扱い(バックフラッシュと同じ)
+ var finalSequence = order?.WorkOrders
+ .Where(w => w.Status != WorkOrderStatus.Canceled)
+ .Max(w => (int?)w.RoutingSequence);
+ _orderQuantity = order?.Order.Quantity ?? 0;
+ var sequence = _workOrder.RoutingSequence;
+ _materials = [.. (order?.Materials ?? [])
+ .Where(m => m.RoutingSequence == sequence || (m.RoutingSequence is null && sequence == finalSequence))];
}
}
+
+ ///
+ /// 計画数分の必要数量。予定材料の予定数量(標準不良率で割り増し済み。Spec.md 5.2)を、
+ /// 指図数量に対するこの作業指示の計画数の割合で按分する。原単位×計画数で出すと割り増しが抜け、指図の予定材料と食い違う
+ ///
+ private decimal RequiredQuantity(OrderMaterialResponse line) =>
+ _orderQuantity == 0 ? line.QuantityPer * _workOrder!.PlannedQuantity
+ : line.PlannedQuantity * _workOrder!.PlannedQuantity / _orderQuantity;
}
diff --git a/src/MesApp.Client.Web/Shared/UiText.en.resx b/src/MesApp.Client.Web/Shared/UiText.en.resx
index e307d34..54ce73d 100644
--- a/src/MesApp.Client.Web/Shared/UiText.en.resx
+++ b/src/MesApp.Client.Web/Shared/UiText.en.resx
@@ -2127,11 +2127,11 @@
WORK INSTRUCTION
-
- Components (MBOM)
+
+ Materials used in this step (planned materials)
-
- No MBOM registered.
+
+ No materials are used in this step.
Required Qty (for planned qty)
@@ -4155,4 +4155,16 @@
production date + product code + process code + work center code
+
+ Consuming step
+
+
+ Final step
+
+
+ Final step (not specified)
+
+
+ {0} (not in routing)
+
diff --git a/src/MesApp.Core/Contracts/Masters/MasterContracts.cs b/src/MesApp.Core/Contracts/Masters/MasterContracts.cs
index 83d5416..5cb58bc 100644
--- a/src/MesApp.Core/Contracts/Masters/MasterContracts.cs
+++ b/src/MesApp.Core/Contracts/Masters/MasterContracts.cs
@@ -11,7 +11,8 @@ public record ProductRequest(
[Required, MaxLength(20)] string Unit,
string? Specification,
ProductType Type,
- [Range(0, 100)] decimal StandardDefectRate,
+ /// 標準不良率(%)。予定材料の数量を割り増す(÷(1-率))ため100%は受け付けない
+ [Range(0, 99.99)] decimal StandardDefectRate,
/// 既定の入庫先ロケーション(推奨ロケーション指示の第一候補。D-10-30-03、D-40-40-03)
int? DefaultLocationId = null);
@@ -28,12 +29,14 @@ public record BomItemRequest(
MakeOrBuy MakeOrBuy,
string? AlternativeGroup,
/// 代替部品か(同一グループ内の主材料でない行。投入時に理由の記録を求める)
- bool IsAlternative = false);
+ bool IsAlternative = false,
+ /// 消費する工程の工程順序(未指定なら最終工程。バックフラッシュはこの工程で引く)
+ [Range(1, int.MaxValue)] int? RoutingSequence = null);
public record BomItemResponse(
int Id, int ChildProductId, string ChildProductCode, string ChildProductName,
decimal QuantityPer, MakeOrBuy MakeOrBuy, string? AlternativeGroup,
- bool IsAlternative = false);
+ bool IsAlternative = false, int? RoutingSequence = null);
// ---- 工程(Process)----
diff --git a/src/MesApp.Core/Contracts/Production/ProductionContracts.cs b/src/MesApp.Core/Contracts/Production/ProductionContracts.cs
index 187efd4..ad62071 100644
--- a/src/MesApp.Core/Contracts/Production/ProductionContracts.cs
+++ b/src/MesApp.Core/Contracts/Production/ProductionContracts.cs
@@ -28,7 +28,8 @@ public record ManufacturingOrderResponse(
/// 指図の予定材料(展開時にMBOMから固定。Spec.md 5.7)
public record OrderMaterialResponse(
int ChildProductId, string ProductCode, string ProductName,
- decimal QuantityPer, decimal PlannedQuantity, string? AlternativeGroup);
+ decimal QuantityPer, decimal PlannedQuantity, string? AlternativeGroup,
+ int? RoutingSequence = null);
public record ManufacturingOrderDetailResponse(
ManufacturingOrderResponse Order,
diff --git a/src/MesApp.Core/Entities/Masters.cs b/src/MesApp.Core/Entities/Masters.cs
index b7292a4..a8ed796 100644
--- a/src/MesApp.Core/Entities/Masters.cs
+++ b/src/MesApp.Core/Entities/Masters.cs
@@ -20,7 +20,10 @@ public class Product : IDeactivatableMaster
public ProductType Type { get; set; }
- /// 標準不良率(%。A-40-10-04)
+ ///
+ /// 標準不良率(%。A-40-10-04)。指図の展開で予定材料の数量を 原単位×指図数量÷(1-率) に割り増す。
+ /// 不良品も部材を消費する(バックフラッシュは良品+不良の数で引く)ため、良品を指図数量だけ得るのに要る部材を見込む
+ ///
public decimal StandardDefectRate { get; set; }
///
@@ -61,6 +64,13 @@ public class BomItem
/// 代替部品の投入には理由の記録を求める(Spec.md 3.9 部材投入の照合)
///
public bool IsAlternative { get; set; }
+
+ ///
+ /// この部材を消費する工程(親品目の工順の工程順序 。B-40-10-09)。
+ /// 未設定なら最終工程で消費する。工順は一括置換で行が作り直されるため、IDではなく工程順序で指す。
+ /// 工順に無い工程順序は指図の展開時に拒否する(MBOMと工順はどちらを先に登録してもよいため、登録時には照合しない)
+ ///
+ public int? RoutingSequence { get; set; }
}
/// 工程マスタ(Spec.md 5.1 Process)
diff --git a/src/MesApp.Core/Entities/Production.cs b/src/MesApp.Core/Entities/Production.cs
index b0fb782..fcabb75 100644
--- a/src/MesApp.Core/Entities/Production.cs
+++ b/src/MesApp.Core/Entities/Production.cs
@@ -75,6 +75,13 @@ public class ManufacturingOrderMaterial
/// 代替部品か(展開時点のMBOMの値。投入時に理由の記録を求める)
public bool IsAlternative { get; set; }
+
+ ///
+ /// この部材を消費する工程順序(展開時点のMBOMの値。未設定の行は展開時に最終工程へ解決して入れる)。
+ /// バックフラッシュはこの工程の作業指示でだけ消費する。
+ /// null は本項目の追加前に展開した指図で、最終工程で消費したものとして扱う
+ ///
+ public int? RoutingSequence { get; set; }
}
/// 作業指示(Spec.md 5.2 WorkOrder。製造指図×工程。B-10)
diff --git a/src/MesApp.Infrastructure/Migrations/20260922224608_AddBomItemRoutingSequence.Designer.cs b/src/MesApp.Infrastructure/Migrations/20260922224608_AddBomItemRoutingSequence.Designer.cs
new file mode 100644
index 0000000..7cf7afa
--- /dev/null
+++ b/src/MesApp.Infrastructure/Migrations/20260922224608_AddBomItemRoutingSequence.Designer.cs
@@ -0,0 +1,4753 @@
+//
+using System;
+using MesApp.Infrastructure;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace MesApp.Infrastructure.Migrations
+{
+ [DbContext(typeof(MesAppDbContext))]
+ [Migration("20260922224608_AddBomItemRoutingSequence")]
+ partial class AddBomItemRoutingSequence
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.10");
+
+ modelBuilder.Entity("MesApp.Core.Entities.AppUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Department")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("DisplayName")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("TEXT");
+
+ b.Property("MustChangePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("PasswordHash")
+ .HasColumnType("TEXT");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("TEXT");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("TEXT");
+
+ b.Property("ShiftId")
+ .HasColumnType("INTEGER");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("WorkCenterId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasDatabaseName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasDatabaseName("UserNameIndex");
+
+ b.HasIndex("ShiftId");
+
+ b.HasIndex("WorkCenterId");
+
+ b.ToTable("AspNetUsers", (string)null);
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.AuditLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Action")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("Category")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("Detail")
+ .HasColumnType("TEXT");
+
+ b.Property("IpAddress")
+ .HasColumnType("TEXT");
+
+ b.Property("RecordedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("TargetId")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("TargetType")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("Timestamp")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("UserName")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RecordedOn");
+
+ b.HasIndex("Timestamp");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("Category", "Action");
+
+ b.HasIndex("TargetType", "TargetId");
+
+ b.ToTable("AuditLogs");
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.BomItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AlternativeGroup")
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("ChildProductId")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsAlternative")
+ .HasColumnType("INTEGER");
+
+ b.Property("MakeOrBuy")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("TEXT");
+
+ b.Property("ParentProductId")
+ .HasColumnType("INTEGER");
+
+ b.Property("QuantityPer")
+ .HasColumnType("TEXT");
+
+ b.Property("RoutingSequence")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChildProductId");
+
+ b.HasIndex("ParentProductId", "ChildProductId")
+ .IsUnique();
+
+ b.ToTable("BomItems");
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.Checklist", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Category")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("TEXT");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.ToTable("Checklists");
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.ChecklistItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChecklistId")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsRequired")
+ .HasColumnType("INTEGER");
+
+ b.Property("Sequence")
+ .HasColumnType("INTEGER");
+
+ b.Property("Text")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChecklistId");
+
+ b.ToTable("ChecklistItem");
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.ChecklistRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChecklistId")
+ .HasColumnType("INTEGER");
+
+ b.Property("EquipmentId")
+ .HasColumnType("INTEGER");
+
+ b.Property("PerformedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("PerformedByUserId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("WorkOrderId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChecklistId");
+
+ b.HasIndex("EquipmentId");
+
+ b.HasIndex("PerformedByUserId");
+
+ b.HasIndex("WorkOrderId");
+
+ b.ToTable("ChecklistRecords");
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.ChecklistResultItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChecklistItemId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ChecklistRecordId")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsChecked")
+ .HasColumnType("INTEGER");
+
+ b.Property("Note")
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChecklistItemId");
+
+ b.HasIndex("ChecklistRecordId");
+
+ b.ToTable("ChecklistResultItem");
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.ControlItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property("LowerLimit")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("TEXT");
+
+ b.Property("TargetProcessId")
+ .HasColumnType("INTEGER");
+
+ b.Property("TargetProductId")
+ .HasColumnType("INTEGER");
+
+ b.Property("TargetValue")
+ .HasColumnType("TEXT");
+
+ b.Property("Unit")
+ .HasMaxLength(30)
+ .HasColumnType("TEXT");
+
+ b.Property("UpperLimit")
+ .HasColumnType("TEXT");
+
+ b.Property("Version")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.HasIndex("TargetProcessId");
+
+ b.HasIndex("TargetProductId");
+
+ b.ToTable("ControlItems");
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.DefectReason", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Category")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("TEXT");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.ToTable("DefectReasons");
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.Equipment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AssetNo")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaintenanceParts")
+ .HasMaxLength(1000)
+ .HasColumnType("TEXT");
+
+ b.Property("MaintenanceThreshold")
+ .HasColumnType("TEXT");
+
+ b.Property("MaintenanceType")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("TEXT");
+
+ b.Property("Site")
+ .HasMaxLength(200)
+ .HasColumnType("TEXT");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("TEXT");
+
+ b.Property("WorkCenterId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AssetNo")
+ .IsUnique();
+
+ b.HasIndex("WorkCenterId");
+
+ b.ToTable("Equipments");
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.EquipmentLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("EndedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("EquipmentId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Note")
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("RecordedByUserId")
+ .HasColumnType("TEXT");
+
+ b.Property("StartedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("TEXT");
+
+ b.Property("StopCause")
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("WorkOrderId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EquipmentId");
+
+ b.HasIndex("WorkOrderId");
+
+ b.ToTable("EquipmentLogs");
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.EquipmentPart", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Category")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("TEXT");
+
+ b.Property("EquipmentId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Note")
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("ProductId")
+ .HasColumnType("INTEGER");
+
+ b.Property("QuantityPer")
+ .HasPrecision(18, 4)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ProductId");
+
+ b.HasIndex("EquipmentId", "ProductId")
+ .IsUnique();
+
+ b.ToTable("EquipmentParts");
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.InspectionDevice", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CalibratedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("CalibrationCycleDays")
+ .HasColumnType("INTEGER");
+
+ b.Property("CalibrationDueOn")
+ .HasColumnType("TEXT");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("TEXT");
+
+ b.Property("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property("Location")
+ .HasMaxLength(200)
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("TEXT");
+
+ b.Property("Note")
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("SerialNo")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.ToTable("InspectionDevices");
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.InspectionDeviceCalibration", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CalibratedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("InspectionDeviceId")
+ .HasColumnType("INTEGER");
+
+ b.Property("NextDueOn")
+ .HasColumnType("TEXT");
+
+ b.Property("PerformedByUserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Result")
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("PerformedByUserId");
+
+ b.HasIndex("InspectionDeviceId", "CalibratedOn");
+
+ b.ToTable("InspectionDeviceCalibrations");
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.InspectionItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property("LowerLimit")
+ .HasColumnType("TEXT");
+
+ b.Property("Method")
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("TEXT");
+
+ b.Property("SamplingCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("StandardValue")
+ .HasColumnType("TEXT");
+
+ b.Property("TargetProcessId")
+ .HasColumnType("INTEGER");
+
+ b.Property("TargetProductId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("TEXT");
+
+ b.Property("UpperLimit")
+ .HasColumnType("TEXT");
+
+ b.Property("Version")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.HasIndex("TargetProcessId");
+
+ b.HasIndex("TargetProductId");
+
+ b.ToTable("InspectionItems");
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.InspectionOrder", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ApprovedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("ApprovedByUserId")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("JudgedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("JudgedByUserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Note")
+ .HasMaxLength(1000)
+ .HasColumnType("TEXT");
+
+ b.Property("OrderNo")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("OverallJudgment")
+ .HasMaxLength(30)
+ .HasColumnType("TEXT");
+
+ b.Property("RequestedByUserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("TEXT");
+
+ b.Property("TargetLotId")
+ .HasColumnType("INTEGER");
+
+ b.Property("TargetWorkOrderId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OrderNo")
+ .IsUnique();
+
+ b.HasIndex("RequestedByUserId");
+
+ b.HasIndex("Status");
+
+ b.HasIndex("TargetLotId");
+
+ b.HasIndex("TargetWorkOrderId");
+
+ b.ToTable("InspectionOrders");
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.InspectionOrderItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("InspectionItemId")
+ .HasColumnType("INTEGER");
+
+ b.Property("InspectionOrderId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemCode")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("ItemName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("TEXT");
+
+ b.Property("ItemVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property("LowerLimit")
+ .HasColumnType("TEXT");
+
+ b.Property("Method")
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("SamplingCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("StandardValue")
+ .HasColumnType("TEXT");
+
+ b.Property("UpperLimit")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InspectionItemId");
+
+ b.HasIndex("InspectionOrderId", "InspectionItemId")
+ .IsUnique();
+
+ b.ToTable("InspectionOrderItem");
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.InspectionResult", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CorrectionNote")
+ .HasMaxLength(1000)
+ .HasColumnType("TEXT");
+
+ b.Property("InspectedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("InspectedByUserId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("InspectionDeviceId")
+ .HasColumnType("INTEGER");
+
+ b.Property("InspectionItemId")
+ .HasColumnType("INTEGER");
+
+ b.Property("InspectionOrderId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Judgment")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("TEXT");
+
+ b.Property("MeasuredValue")
+ .HasColumnType("TEXT");
+
+ b.Property("SampleNo")
+ .HasColumnType("INTEGER");
+
+ b.Property("TextValue")
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InspectedByUserId");
+
+ b.HasIndex("InspectionDeviceId");
+
+ b.HasIndex("InspectionItemId");
+
+ b.HasIndex("InspectionOrderId");
+
+ b.ToTable("InspectionResults");
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.InspectionResultCorrection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AfterJudgment")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("TEXT");
+
+ b.Property("AfterMeasuredValue")
+ .HasColumnType("TEXT");
+
+ b.Property("AfterTextValue")
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("BeforeJudgment")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("TEXT");
+
+ b.Property("BeforeMeasuredValue")
+ .HasColumnType("TEXT");
+
+ b.Property("BeforeTextValue")
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("CorrectedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("CorrectedByUserId")
+ .HasMaxLength(450)
+ .HasColumnType("TEXT");
+
+ b.Property("InspectionOrderId")
+ .HasColumnType("INTEGER");
+
+ b.Property("InspectionResultId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Reason")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CorrectedByUserId");
+
+ b.HasIndex("InspectionResultId");
+
+ b.HasIndex("InspectionOrderId", "Id");
+
+ b.ToTable("InspectionResultCorrections");
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.InventoryStock", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("LocationId")
+ .HasColumnType("INTEGER");
+
+ b.Property("LotId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ProductId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Quantity")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LocationId");
+
+ b.HasIndex("LotId", "LocationId")
+ .IsUnique();
+
+ b.HasIndex("ProductId", "LocationId");
+
+ b.ToTable("InventoryStocks");
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.InventoryTransaction", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("FromLocationId")
+ .HasColumnType("INTEGER");
+
+ b.Property("LotId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Note")
+ .HasMaxLength(500)
+ .HasColumnType("TEXT");
+
+ b.Property("PerformedByUserId")
+ .HasColumnType("TEXT");
+
+ b.Property("PickingOrderId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ProductId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Quantity")
+ .HasColumnType("TEXT");
+
+ b.Property("ShippingOrderId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Timestamp")
+ .HasColumnType("TEXT");
+
+ b.Property("ToLocationId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("TEXT");
+
+ b.Property("WorkOrderId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("FromLocationId");
+
+ b.HasIndex("LotId");
+
+ b.HasIndex("ProductId");
+
+ b.HasIndex("Timestamp");
+
+ b.HasIndex("ToLocationId");
+
+ b.HasIndex("WorkOrderId");
+
+ b.ToTable("InventoryTransactions");
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.Location", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AreaType")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("TEXT");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShelfNo")
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("WorkCenterId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.HasIndex("WorkCenterId");
+
+ b.ToTable("Locations");
+ });
+
+ modelBuilder.Entity("MesApp.Core.Entities.Lot", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("ExpiresOn")
+ .HasColumnType("TEXT");
+
+ b.Property("Grade")
+ .HasMaxLength(30)
+ .HasColumnType("TEXT");
+
+ b.Property("InitialQuantity")
+ .HasColumnType("TEXT");
+
+ b.Property("LotNumber")
+ .IsRequired()
+ .HasMaxLength(60)
+ .HasColumnType("TEXT");
+
+ b.Property("ManufacturedOn")
+ .HasColumnType("TEXT");
+
+ b.Property("OriginType")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("TEXT");
+
+ b.Property