@k2works/claude-code-booster 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/lib/assets/docs/article/index.md +4 -1
  2. package/lib/assets/docs/article/practical-database-design/index.md +121 -0
  3. package/lib/assets/docs/article/practical-database-design/part1/chapter01.md +288 -0
  4. package/lib/assets/docs/article/practical-database-design/part1/chapter02.md +518 -0
  5. package/lib/assets/docs/article/practical-database-design/part1/chapter03.md +557 -0
  6. package/lib/assets/docs/article/practical-database-design/part2/chapter04.md +924 -0
  7. package/lib/assets/docs/article/practical-database-design/part2/chapter05.md +1627 -0
  8. package/lib/assets/docs/article/practical-database-design/part2/chapter06.md +2716 -0
  9. package/lib/assets/docs/article/practical-database-design/part2/chapter07.md +2082 -0
  10. package/lib/assets/docs/article/practical-database-design/part2/chapter08.md +2105 -0
  11. package/lib/assets/docs/article/practical-database-design/part2/chapter09.md +2031 -0
  12. package/lib/assets/docs/article/practical-database-design/part2/chapter10.md +1387 -0
  13. package/lib/assets/docs/article/practical-database-design/part2/chapter11.md +1677 -0
  14. package/lib/assets/docs/article/practical-database-design/part2/chapter12.md +1417 -0
  15. package/lib/assets/docs/article/practical-database-design/part2/chapter13.md +1434 -0
  16. package/lib/assets/docs/article/practical-database-design/part3/chapter14.md +667 -0
  17. package/lib/assets/docs/article/practical-database-design/part3/chapter15.md +1625 -0
  18. package/lib/assets/docs/article/practical-database-design/part3/chapter16.md +1915 -0
  19. package/lib/assets/docs/article/practical-database-design/part3/chapter17.md +1708 -0
  20. package/lib/assets/docs/article/practical-database-design/part3/chapter18.md +2095 -0
  21. package/lib/assets/docs/article/practical-database-design/part3/chapter19.md +1123 -0
  22. package/lib/assets/docs/article/practical-database-design/part3/chapter20.md +1031 -0
  23. package/lib/assets/docs/article/practical-database-design/part3/chapter21.md +1382 -0
  24. package/lib/assets/docs/article/practical-database-design/part3-orm/chapter14-orm.md +991 -0
  25. package/lib/assets/docs/article/practical-database-design/part3-orm/chapter15-orm.md +1300 -0
  26. package/lib/assets/docs/article/practical-database-design/part3-orm/chapter16-orm.md +1166 -0
  27. package/lib/assets/docs/article/practical-database-design/part3-orm/chapter17-orm.md +1584 -0
  28. package/lib/assets/docs/article/practical-database-design/part3-orm/chapter18-orm.md +1183 -0
  29. package/lib/assets/docs/article/practical-database-design/part3-orm/chapter19-orm.md +1016 -0
  30. package/lib/assets/docs/article/practical-database-design/part3-orm/chapter20-orm.md +1753 -0
  31. package/lib/assets/docs/article/practical-database-design/part3-orm/chapter21-orm.md +1447 -0
  32. package/lib/assets/docs/article/practical-database-design/part3-orm/chapter22-orm.md +1878 -0
  33. package/lib/assets/docs/article/practical-database-design/part4/chapter22.md +965 -0
  34. package/lib/assets/docs/article/practical-database-design/part4/chapter23.md +2069 -0
  35. package/lib/assets/docs/article/practical-database-design/part4/chapter24.md +2439 -0
  36. package/lib/assets/docs/article/practical-database-design/part4/chapter25.md +3661 -0
  37. package/lib/assets/docs/article/practical-database-design/part4/chapter26.md +2916 -0
  38. package/lib/assets/docs/article/practical-database-design/part4/chapter27.md +3105 -0
  39. package/lib/assets/docs/article/practical-database-design/part4/chapter28.md +2697 -0
  40. package/lib/assets/docs/article/practical-database-design/part4/chapter29.md +2544 -0
  41. package/lib/assets/docs/article/practical-database-design/part4/chapter30.md +2180 -0
  42. package/lib/assets/docs/article/practical-database-design/part4/chapter31.md +1192 -0
  43. package/lib/assets/docs/article/practical-database-design/part4/chapter32.md +2101 -0
  44. package/lib/assets/docs/article/practical-database-design/part5/chapter33.md +1032 -0
  45. package/lib/assets/docs/article/practical-database-design/part5/chapter34.md +1609 -0
  46. package/lib/assets/docs/article/practical-database-design/part5/chapter35.md +1453 -0
  47. package/lib/assets/docs/article/practical-database-design/part5/chapter36.md +1292 -0
  48. package/lib/assets/docs/article/practical-database-design/part5/chapter37.md +1470 -0
  49. package/lib/assets/docs/article/practical-database-design/part5/chapter38.md +1698 -0
  50. package/lib/assets/docs/article/practical-database-design/part5/chapter39.md +2334 -0
  51. package/lib/assets/docs/article/practical-database-design/study/study2-1.md +1693 -0
  52. package/lib/assets/docs/article/practical-database-design/study/study2-2.md +1347 -0
  53. package/lib/assets/docs/article/practical-database-design/study/study2-3.md +2044 -0
  54. package/lib/assets/docs/article/practical-database-design/study/study2-4.md +2229 -0
  55. package/lib/assets/docs/article/practical-database-design/study/study2-5.md +2418 -0
  56. package/lib/assets/docs/article/practical-database-design/study/study3-1.md +2205 -0
  57. package/lib/assets/docs/article/practical-database-design/study/study3-2.md +2221 -0
  58. package/lib/assets/docs/article/practical-database-design/study/study3-3.md +2253 -0
  59. package/lib/assets/docs/article/practical-database-design/study/study3-4.md +2106 -0
  60. package/lib/assets/docs/article/practical-database-design/study/study3-5.md +2507 -0
  61. package/lib/assets/docs/article/practical-database-design/study/study4-1.md +2587 -0
  62. package/lib/assets/docs/article/practical-database-design/study/study4-2.md +2075 -0
  63. package/lib/assets/docs/article/practical-database-design/study/study4-3.md +1805 -0
  64. package/lib/assets/docs/article/practical-database-design/study/study4-4.md +1895 -0
  65. package/lib/assets/docs/article/practical-database-design/study/study4-5.md +2878 -0
  66. package/lib/assets/docs/reference//351/201/213/345/226/266/347/256/241/347/220/206.md +131 -39
  67. package/package.json +1 -1
@@ -0,0 +1,3661 @@
1
+ # 第25章:購買管理の設計
2
+
3
+ 本章では、MRP で生成された購買オーダを実際の発注に変換し、入荷・検収までの一連の購買業務を設計します。
4
+
5
+ ---
6
+
7
+ ## 25.1 発注業務の DB 設計
8
+
9
+ 発注業務は、購買オーダを基に取引先への発注を行い、納期管理を行う業務です。
10
+
11
+ ### 発注業務の流れ
12
+
13
+ ```plantuml
14
+ @startuml
15
+
16
+ title 発注から検収までの業務フロー
17
+
18
+ |生産管理部|
19
+ start
20
+ :オーダ情報;
21
+
22
+ |購買部|
23
+ :発注;
24
+ fork
25
+ :検査表;
26
+ fork again
27
+ :現品票;
28
+ fork again
29
+ :受領書;
30
+ fork again
31
+ :納品書;
32
+ fork again
33
+ :注文書;
34
+ fork end
35
+
36
+ |仕入先|
37
+ fork
38
+ :検査表;
39
+ fork again
40
+ :現品票;
41
+ fork again
42
+ :受領書;
43
+ fork again
44
+ :納品書;
45
+ fork again
46
+ :注文書;
47
+ fork end
48
+ :材料;
49
+
50
+ |資材倉庫|
51
+ fork
52
+ :現品表;
53
+ fork again
54
+ :材料;
55
+ fork end
56
+
57
+ |品質管理部|
58
+ fork
59
+ :現品表;
60
+ fork again
61
+ :材料;
62
+ fork again
63
+ :検査表;
64
+ fork end
65
+
66
+ |購買部|
67
+ :検収;
68
+
69
+ |品質管理部|
70
+ :受入検査;
71
+ if (不良品) then
72
+ |仕入先|
73
+ :材料;
74
+ |購買部|
75
+ :検収返品;
76
+ |仕入先|
77
+ :返品通知;
78
+ else (合格品)
79
+ |資材倉庫|
80
+ fork
81
+ :現品表;
82
+ fork again
83
+ :材料;
84
+ fork end
85
+ endif
86
+ stop
87
+
88
+ @enduml
89
+ ```
90
+
91
+ ### 発注業務のデータ関連
92
+
93
+ 発注業務では、オーダ情報を基に発注データを生成し、単価マスタを参照して発注金額を計算します。
94
+
95
+ ```plantuml
96
+ @startuml
97
+
98
+ title 発注業務のデータ関連図
99
+
100
+ database "場所マスタ" as location
101
+ database "オーダ情報" as order
102
+ database "単価マスタ" as price
103
+
104
+ rectangle "購買計画作成" as plan
105
+
106
+ database "購買計画" as purchasePlan
107
+
108
+ rectangle "発注" as po
109
+
110
+ database "発注情報" as poData
111
+
112
+ location --> plan
113
+ order --> plan
114
+ price --> plan
115
+ plan --> purchasePlan
116
+ purchasePlan --> po
117
+ po --> poData
118
+
119
+ @enduml
120
+ ```
121
+
122
+ ### 発注関連のスキーマ設計
123
+
124
+ 発注業務で使用するテーブルを定義します。
125
+
126
+ #### テーブル構造
127
+
128
+ | テーブル名 | 説明 |
129
+ |-----------|------|
130
+ | 単価マスタ | 品目・取引先ごとの単価情報を管理 |
131
+ | 発注データ | 発注ヘッダ情報(発注番号、取引先、ステータス等) |
132
+ | 発注明細データ | 発注明細情報(品目、数量、単価、金額等) |
133
+ | 諸口品目情報 | マスタに登録されていない臨時品目の情報 |
134
+
135
+ #### 発注ステータスの状態遷移
136
+
137
+ ```plantuml
138
+ @startuml
139
+
140
+ title 発注ステータスの状態遷移
141
+
142
+ [*] --> 作成中 : 発注作成
143
+ 作成中 --> 発注済 : 発注確定
144
+ 作成中 --> 取消 : 取消
145
+ 発注済 --> 一部入荷 : 分割入荷
146
+ 発注済 --> 入荷完了 : 全数入荷
147
+ 一部入荷 --> 入荷完了 : 残数入荷
148
+ 入荷完了 --> 検収完了 : 検収処理
149
+ 検収完了 --> [*]
150
+ 取消 --> [*]
151
+
152
+ @enduml
153
+ ```
154
+
155
+ <details>
156
+ <summary>DDL: 発注関連テーブル</summary>
157
+
158
+ ```sql
159
+ -- V009__create_purchasing_tables.sql
160
+
161
+ -- 発注ステータス
162
+ CREATE TYPE 発注ステータス AS ENUM ('作成中', '発注済', '一部入荷', '入荷完了', '検収完了', '取消');
163
+
164
+ -- 入荷受入区分
165
+ CREATE TYPE 入荷受入区分 AS ENUM ('通常入荷', '分割入荷', '返品入荷');
166
+
167
+ -- 単価マスタ
168
+ CREATE TABLE "単価マスタ" (
169
+ "ID" SERIAL PRIMARY KEY,
170
+ "品目コード" VARCHAR(20) NOT NULL,
171
+ "取引先コード" VARCHAR(20) NOT NULL,
172
+ "ロット単位数" DECIMAL(15, 2) DEFAULT 1 NOT NULL,
173
+ "使用開始日" DATE NOT NULL,
174
+ "使用停止日" DATE,
175
+ "単価" DECIMAL(15, 2) NOT NULL,
176
+ "作成日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
177
+ "作成者" VARCHAR(50),
178
+ "更新日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
179
+ "更新者" VARCHAR(50),
180
+ CONSTRAINT "fk_単価マスタ_品目"
181
+ FOREIGN KEY ("品目コード") REFERENCES "品目マスタ"("品目コード"),
182
+ CONSTRAINT "fk_単価マスタ_取引先"
183
+ FOREIGN KEY ("取引先コード") REFERENCES "取引先マスタ"("取引先コード"),
184
+ UNIQUE ("品目コード", "取引先コード", "ロット単位数", "使用開始日")
185
+ );
186
+
187
+ -- 発注データ
188
+ CREATE TABLE "発注データ" (
189
+ "ID" SERIAL PRIMARY KEY,
190
+ "発注番号" VARCHAR(20) UNIQUE NOT NULL,
191
+ "発注日" DATE NOT NULL,
192
+ "取引先コード" VARCHAR(20) NOT NULL,
193
+ "発注担当者コード" VARCHAR(20),
194
+ "発注部門コード" VARCHAR(20),
195
+ "ステータス" 発注ステータス DEFAULT '作成中' NOT NULL,
196
+ "備考" TEXT,
197
+ "作成日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
198
+ "作成者" VARCHAR(50),
199
+ "更新日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
200
+ "更新者" VARCHAR(50),
201
+ CONSTRAINT "fk_発注データ_取引先"
202
+ FOREIGN KEY ("取引先コード") REFERENCES "取引先マスタ"("取引先コード")
203
+ );
204
+
205
+ -- 発注明細データ
206
+ CREATE TABLE "発注明細データ" (
207
+ "ID" SERIAL PRIMARY KEY,
208
+ "発注番号" VARCHAR(20) NOT NULL,
209
+ "発注行番号" INTEGER NOT NULL,
210
+ "オーダNO" VARCHAR(20),
211
+ "納入場所コード" VARCHAR(20),
212
+ "品目コード" VARCHAR(20) NOT NULL,
213
+ "諸口品目区分" BOOLEAN DEFAULT FALSE NOT NULL,
214
+ "受入予定日" DATE NOT NULL,
215
+ "回答納期" DATE,
216
+ "発注単価" DECIMAL(15, 2) NOT NULL,
217
+ "発注数量" DECIMAL(15, 2) NOT NULL,
218
+ "入荷済数量" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
219
+ "検査済数量" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
220
+ "検収済数量" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
221
+ "発注金額" DECIMAL(15, 2) NOT NULL,
222
+ "消費税金額" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
223
+ "完了フラグ" BOOLEAN DEFAULT FALSE NOT NULL,
224
+ "明細備考" TEXT,
225
+ "作成日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
226
+ "作成者" VARCHAR(50),
227
+ "更新日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
228
+ "更新者" VARCHAR(50),
229
+ CONSTRAINT "fk_発注明細_発注"
230
+ FOREIGN KEY ("発注番号") REFERENCES "発注データ"("発注番号"),
231
+ CONSTRAINT "fk_発注明細_品目"
232
+ FOREIGN KEY ("品目コード") REFERENCES "品目マスタ"("品目コード"),
233
+ UNIQUE ("発注番号", "発注行番号")
234
+ );
235
+
236
+ -- 諸口品目情報(マスタに登録されていない臨時品目)
237
+ CREATE TABLE "諸口品目情報" (
238
+ "ID" SERIAL PRIMARY KEY,
239
+ "発注番号" VARCHAR(20) NOT NULL,
240
+ "発注行番号" INTEGER NOT NULL,
241
+ "品目コード" VARCHAR(20) NOT NULL,
242
+ "品名" VARCHAR(100) NOT NULL,
243
+ "規格" VARCHAR(100),
244
+ "図番メーカー" VARCHAR(100),
245
+ "版数" VARCHAR(20),
246
+ "作成日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
247
+ "更新日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
248
+ UNIQUE ("発注番号", "発注行番号", "品目コード")
249
+ );
250
+
251
+ -- インデックス
252
+ CREATE INDEX "idx_発注データ_取引先コード" ON "発注データ"("取引先コード");
253
+ CREATE INDEX "idx_発注データ_発注日" ON "発注データ"("発注日");
254
+ CREATE INDEX "idx_発注明細_発注番号" ON "発注明細データ"("発注番号");
255
+ CREATE INDEX "idx_発注明細_品目コード" ON "発注明細データ"("品目コード");
256
+ CREATE INDEX "idx_単価マスタ_品目取引先" ON "単価マスタ"("品目コード", "取引先コード");
257
+ ```
258
+
259
+ </details>
260
+
261
+ ### オーダ情報・場所マスタ・単価マスタとの連携
262
+
263
+ 発注データは以下のマスタ情報と連携します。
264
+
265
+ ```plantuml
266
+ @startuml
267
+
268
+ title 発注データとマスタの関連
269
+
270
+ entity "発注データ" as po {
271
+ * 発注番号 [PK]
272
+ --
273
+ 取引先コード [FK]
274
+ 発注日
275
+ ステータス
276
+ }
277
+
278
+ entity "発注明細データ" as pod {
279
+ * 発注番号 [PK,FK]
280
+ * 発注行番号 [PK]
281
+ --
282
+ オーダNO [FK]
283
+ 品目コード [FK]
284
+ 納入場所コード [FK]
285
+ 発注数量
286
+ 発注単価
287
+ }
288
+
289
+ entity "オーダ情報" as order {
290
+ * オーダNO [PK]
291
+ --
292
+ 品目コード
293
+ 所要数量
294
+ 所要日
295
+ }
296
+
297
+ entity "単価マスタ" as price {
298
+ * ID [PK]
299
+ --
300
+ 品目コード [FK]
301
+ 取引先コード [FK]
302
+ 単価
303
+ 使用開始日
304
+ 使用停止日
305
+ }
306
+
307
+ entity "場所マスタ" as location {
308
+ * 場所コード [PK]
309
+ --
310
+ 場所名
311
+ }
312
+
313
+ entity "取引先マスタ" as supplier {
314
+ * 取引先コード [PK]
315
+ --
316
+ 取引先名
317
+ }
318
+
319
+ po ||--o{ pod : contains
320
+ po }o--|| supplier : 発注先
321
+ pod }o--o| order : 紐付け
322
+ pod }o--|| price : 単価参照
323
+ pod }o--o| location : 納入先
324
+
325
+ @enduml
326
+ ```
327
+
328
+ ### 諸口品目の扱い
329
+
330
+ マスタに登録されていない臨時品目(諸口品目)を発注する場合、発注明細データの「諸口品目区分」を `TRUE` に設定し、諸口品目情報テーブルに品目の詳細情報を登録します。
331
+
332
+ ```plantuml
333
+ @startuml
334
+
335
+ title 諸口品目の関連
336
+
337
+ entity "発注明細データ" as pod {
338
+ * 発注番号 [PK]
339
+ * 発注行番号 [PK]
340
+ --
341
+ 品目コード
342
+ 諸口品目区分
343
+ }
344
+
345
+ entity "諸口品目情報" as misc {
346
+ * ID [PK]
347
+ --
348
+ 発注番号 [FK]
349
+ 発注行番号 [FK]
350
+ 品目コード
351
+ 品名
352
+ 規格
353
+ 図番メーカー
354
+ 版数
355
+ }
356
+
357
+ pod ||--o| misc : 諸口品目の場合
358
+
359
+ note right of misc
360
+ 品目マスタに登録されていない
361
+ 臨時品目の情報を保持
362
+ end note
363
+
364
+ @enduml
365
+ ```
366
+
367
+ ### 納期管理・分納対応
368
+
369
+ 発注明細には「受入予定日」と「回答納期」の 2 つの日付を持ちます。
370
+
371
+ | フィールド | 説明 |
372
+ |-----------|------|
373
+ | 受入予定日 | 発注時に指定した希望納期 |
374
+ | 回答納期 | 仕入先から回答された実際の納期 |
375
+
376
+ 分納(分割納品)に対応するため、発注明細ごとに「入荷済数量」「検査済数量」「検収済数量」を管理します。
377
+
378
+ ### Java エンティティの定義
379
+
380
+ <details>
381
+ <summary>発注ステータス Enum</summary>
382
+
383
+ ```java
384
+ // src/main/java/com/example/pms/domain/model/purchase/PurchaseOrderStatus.java
385
+ package com.example.pms.domain.model.purchase;
386
+
387
+ import lombok.Getter;
388
+ import lombok.RequiredArgsConstructor;
389
+
390
+ @Getter
391
+ @RequiredArgsConstructor
392
+ public enum PurchaseOrderStatus {
393
+ CREATING("作成中"),
394
+ ORDERED("発注済"),
395
+ PARTIALLY_RECEIVED("一部入荷"),
396
+ RECEIVED("入荷完了"),
397
+ ACCEPTED("検収完了"),
398
+ CANCELLED("取消");
399
+
400
+ private final String displayName;
401
+
402
+ public static PurchaseOrderStatus fromDisplayName(String displayName) {
403
+ for (PurchaseOrderStatus status : values()) {
404
+ if (status.displayName.equals(displayName)) {
405
+ return status;
406
+ }
407
+ }
408
+ throw new IllegalArgumentException("不正な発注ステータス: " + displayName);
409
+ }
410
+ }
411
+ ```
412
+
413
+ </details>
414
+
415
+ <details>
416
+ <summary>入荷受入区分 Enum</summary>
417
+
418
+ ```java
419
+ // src/main/java/com/example/pms/domain/model/purchase/ReceivingType.java
420
+ package com.example.pms.domain.model.purchase;
421
+
422
+ import lombok.Getter;
423
+ import lombok.RequiredArgsConstructor;
424
+
425
+ @Getter
426
+ @RequiredArgsConstructor
427
+ public enum ReceivingType {
428
+ NORMAL("通常入荷"),
429
+ SPLIT("分割入荷"),
430
+ RETURN("返品入荷");
431
+
432
+ private final String displayName;
433
+
434
+ public static ReceivingType fromDisplayName(String displayName) {
435
+ for (ReceivingType type : values()) {
436
+ if (type.displayName.equals(displayName)) {
437
+ return type;
438
+ }
439
+ }
440
+ throw new IllegalArgumentException("不正な入荷受入区分: " + displayName);
441
+ }
442
+ }
443
+ ```
444
+
445
+ </details>
446
+
447
+ <details>
448
+ <summary>単価マスタエンティティ</summary>
449
+
450
+ ```java
451
+ // src/main/java/com/example/pms/domain/model/purchase/UnitPrice.java
452
+ package com.example.pms.domain.model.purchase;
453
+
454
+ import com.example.pms.domain.model.item.Item;
455
+ import com.example.pms.domain.model.supplier.Supplier;
456
+ import lombok.AllArgsConstructor;
457
+ import lombok.Builder;
458
+ import lombok.Data;
459
+ import lombok.NoArgsConstructor;
460
+
461
+ import java.math.BigDecimal;
462
+ import java.time.LocalDate;
463
+ import java.time.LocalDateTime;
464
+
465
+ @Data
466
+ @Builder
467
+ @NoArgsConstructor
468
+ @AllArgsConstructor
469
+ public class UnitPrice {
470
+ private Integer id;
471
+ private String itemCode;
472
+ private String supplierCode;
473
+ private BigDecimal lotUnitQuantity;
474
+ private LocalDate effectiveFrom;
475
+ private LocalDate effectiveTo;
476
+ private BigDecimal unitPrice;
477
+ private LocalDateTime createdAt;
478
+ private String createdBy;
479
+ private LocalDateTime updatedAt;
480
+ private String updatedBy;
481
+
482
+ // リレーション
483
+ private Item item;
484
+ private Supplier supplier;
485
+ }
486
+ ```
487
+
488
+ </details>
489
+
490
+ <details>
491
+ <summary>発注データエンティティ</summary>
492
+
493
+ ```java
494
+ // src/main/java/com/example/pms/domain/model/purchase/PurchaseOrder.java
495
+ package com.example.pms.domain.model.purchase;
496
+
497
+ import com.example.pms.domain.model.supplier.Supplier;
498
+ import lombok.AllArgsConstructor;
499
+ import lombok.Builder;
500
+ import lombok.Data;
501
+ import lombok.NoArgsConstructor;
502
+
503
+ import java.time.LocalDate;
504
+ import java.time.LocalDateTime;
505
+ import java.util.List;
506
+
507
+ @Data
508
+ @Builder
509
+ @NoArgsConstructor
510
+ @AllArgsConstructor
511
+ public class PurchaseOrder {
512
+ private Integer id;
513
+ private String purchaseOrderNumber;
514
+ private LocalDate orderDate;
515
+ private String supplierCode;
516
+ private String ordererCode;
517
+ private String departmentCode;
518
+ private PurchaseOrderStatus status;
519
+ private String remarks;
520
+ private LocalDateTime createdAt;
521
+ private String createdBy;
522
+ private LocalDateTime updatedAt;
523
+ private String updatedBy;
524
+
525
+ // リレーション
526
+ private Supplier supplier;
527
+ private List<PurchaseOrderDetail> details;
528
+ }
529
+ ```
530
+
531
+ </details>
532
+
533
+ <details>
534
+ <summary>発注明細データエンティティ</summary>
535
+
536
+ ```java
537
+ // src/main/java/com/example/pms/domain/model/purchase/PurchaseOrderDetail.java
538
+ package com.example.pms.domain.model.purchase;
539
+
540
+ import com.example.pms.domain.model.item.Item;
541
+ import com.example.pms.domain.model.plan.Order;
542
+ import lombok.AllArgsConstructor;
543
+ import lombok.Builder;
544
+ import lombok.Data;
545
+ import lombok.NoArgsConstructor;
546
+
547
+ import java.math.BigDecimal;
548
+ import java.time.LocalDate;
549
+ import java.time.LocalDateTime;
550
+ import java.util.List;
551
+
552
+ @Data
553
+ @Builder
554
+ @NoArgsConstructor
555
+ @AllArgsConstructor
556
+ public class PurchaseOrderDetail {
557
+ private Integer id;
558
+ private String purchaseOrderNumber;
559
+ private Integer lineNumber;
560
+ private String orderNumber;
561
+ private String deliveryLocationCode;
562
+ private String itemCode;
563
+ private Boolean miscellaneousItemFlag;
564
+ private LocalDate expectedReceivingDate;
565
+ private LocalDate confirmedDeliveryDate;
566
+ private BigDecimal orderUnitPrice;
567
+ private BigDecimal orderQuantity;
568
+ private BigDecimal receivedQuantity;
569
+ private BigDecimal inspectedQuantity;
570
+ private BigDecimal acceptedQuantity;
571
+ private BigDecimal orderAmount;
572
+ private BigDecimal taxAmount;
573
+ private Boolean completedFlag;
574
+ private String detailRemarks;
575
+ private LocalDateTime createdAt;
576
+ private String createdBy;
577
+ private LocalDateTime updatedAt;
578
+ private String updatedBy;
579
+
580
+ // リレーション
581
+ private PurchaseOrder purchaseOrder;
582
+ private Item item;
583
+ private Order order;
584
+ private MiscellaneousItem miscellaneousItem;
585
+ private List<Receiving> receivings;
586
+ private List<Acceptance> acceptances;
587
+ }
588
+ ```
589
+
590
+ </details>
591
+
592
+ <details>
593
+ <summary>諸口品目情報エンティティ</summary>
594
+
595
+ ```java
596
+ // src/main/java/com/example/pms/domain/model/purchase/MiscellaneousItem.java
597
+ package com.example.pms.domain.model.purchase;
598
+
599
+ import lombok.AllArgsConstructor;
600
+ import lombok.Builder;
601
+ import lombok.Data;
602
+ import lombok.NoArgsConstructor;
603
+
604
+ import java.time.LocalDateTime;
605
+
606
+ @Data
607
+ @Builder
608
+ @NoArgsConstructor
609
+ @AllArgsConstructor
610
+ public class MiscellaneousItem {
611
+ private Integer id;
612
+ private String purchaseOrderNumber;
613
+ private Integer lineNumber;
614
+ private String itemCode;
615
+ private String itemName;
616
+ private String specification;
617
+ private String manufacturerDrawingNumber;
618
+ private String revision;
619
+ private LocalDateTime createdAt;
620
+ private LocalDateTime updatedAt;
621
+ }
622
+ ```
623
+
624
+ </details>
625
+
626
+ ### TypeHandler の実装
627
+
628
+ PostgreSQL の日本語 ENUM 型と Java の英語 Enum を相互変換するために TypeHandler を実装します。
629
+
630
+ <details>
631
+ <summary>PurchaseOrderStatusTypeHandler</summary>
632
+
633
+ ```java
634
+ // src/main/java/com/example/pms/infrastructure/out/persistence/typehandler/PurchaseOrderStatusTypeHandler.java
635
+ package com.example.pms.infrastructure.out.persistence.typehandler;
636
+
637
+ import com.example.pms.domain.model.purchase.PurchaseOrderStatus;
638
+ import org.apache.ibatis.type.BaseTypeHandler;
639
+ import org.apache.ibatis.type.JdbcType;
640
+ import org.apache.ibatis.type.MappedTypes;
641
+
642
+ import java.sql.CallableStatement;
643
+ import java.sql.PreparedStatement;
644
+ import java.sql.ResultSet;
645
+ import java.sql.SQLException;
646
+
647
+ @MappedTypes(PurchaseOrderStatus.class)
648
+ public class PurchaseOrderStatusTypeHandler extends BaseTypeHandler<PurchaseOrderStatus> {
649
+
650
+ @Override
651
+ public void setNonNullParameter(PreparedStatement ps, int i, PurchaseOrderStatus parameter, JdbcType jdbcType)
652
+ throws SQLException {
653
+ ps.setString(i, parameter.getDisplayName());
654
+ }
655
+
656
+ @Override
657
+ public PurchaseOrderStatus getNullableResult(ResultSet rs, String columnName) throws SQLException {
658
+ String value = rs.getString(columnName);
659
+ return value == null ? null : PurchaseOrderStatus.fromDisplayName(value);
660
+ }
661
+
662
+ @Override
663
+ public PurchaseOrderStatus getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
664
+ String value = rs.getString(columnIndex);
665
+ return value == null ? null : PurchaseOrderStatus.fromDisplayName(value);
666
+ }
667
+
668
+ @Override
669
+ public PurchaseOrderStatus getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
670
+ String value = cs.getString(columnIndex);
671
+ return value == null ? null : PurchaseOrderStatus.fromDisplayName(value);
672
+ }
673
+ }
674
+ ```
675
+
676
+ </details>
677
+
678
+ <details>
679
+ <summary>ReceivingTypeTypeHandler</summary>
680
+
681
+ ```java
682
+ // src/main/java/com/example/pms/infrastructure/out/persistence/typehandler/ReceivingTypeTypeHandler.java
683
+ package com.example.pms.infrastructure.out.persistence.typehandler;
684
+
685
+ import com.example.pms.domain.model.purchase.ReceivingType;
686
+ import org.apache.ibatis.type.BaseTypeHandler;
687
+ import org.apache.ibatis.type.JdbcType;
688
+ import org.apache.ibatis.type.MappedTypes;
689
+
690
+ import java.sql.CallableStatement;
691
+ import java.sql.PreparedStatement;
692
+ import java.sql.ResultSet;
693
+ import java.sql.SQLException;
694
+
695
+ @MappedTypes(ReceivingType.class)
696
+ public class ReceivingTypeTypeHandler extends BaseTypeHandler<ReceivingType> {
697
+
698
+ @Override
699
+ public void setNonNullParameter(PreparedStatement ps, int i, ReceivingType parameter, JdbcType jdbcType)
700
+ throws SQLException {
701
+ ps.setString(i, parameter.getDisplayName());
702
+ }
703
+
704
+ @Override
705
+ public ReceivingType getNullableResult(ResultSet rs, String columnName) throws SQLException {
706
+ String value = rs.getString(columnName);
707
+ return value == null ? null : ReceivingType.fromDisplayName(value);
708
+ }
709
+
710
+ @Override
711
+ public ReceivingType getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
712
+ String value = rs.getString(columnIndex);
713
+ return value == null ? null : ReceivingType.fromDisplayName(value);
714
+ }
715
+
716
+ @Override
717
+ public ReceivingType getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
718
+ String value = cs.getString(columnIndex);
719
+ return value == null ? null : ReceivingType.fromDisplayName(value);
720
+ }
721
+ }
722
+ ```
723
+
724
+ </details>
725
+
726
+ ### MyBatis Mapper XML
727
+
728
+ <details>
729
+ <summary>UnitPriceMapper.xml</summary>
730
+
731
+ ```xml
732
+ <!-- src/main/resources/mapper/UnitPriceMapper.xml -->
733
+ <?xml version="1.0" encoding="UTF-8" ?>
734
+ <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
735
+ "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
736
+ <mapper namespace="com.example.pms.infrastructure.out.persistence.mapper.UnitPriceMapper">
737
+
738
+ <resultMap id="UnitPriceResultMap" type="com.example.pms.domain.model.purchase.UnitPrice">
739
+ <id property="id" column="ID"/>
740
+ <result property="itemCode" column="品目コード"/>
741
+ <result property="supplierCode" column="取引先コード"/>
742
+ <result property="lotUnitQuantity" column="ロット単位数"/>
743
+ <result property="effectiveFrom" column="使用開始日"/>
744
+ <result property="effectiveTo" column="使用停止日"/>
745
+ <result property="unitPrice" column="単価"/>
746
+ <result property="createdAt" column="作成日時"/>
747
+ <result property="createdBy" column="作成者"/>
748
+ <result property="updatedAt" column="更新日時"/>
749
+ <result property="updatedBy" column="更新者"/>
750
+ </resultMap>
751
+
752
+ <insert id="insert" useGeneratedKeys="true" keyProperty="id" keyColumn="ID">
753
+ INSERT INTO "単価マスタ" (
754
+ "品目コード", "取引先コード", "ロット単位数", "使用開始日",
755
+ "使用停止日", "単価", "作成者"
756
+ ) VALUES (
757
+ #{itemCode},
758
+ #{supplierCode},
759
+ #{lotUnitQuantity},
760
+ #{effectiveFrom},
761
+ #{effectiveTo},
762
+ #{unitPrice},
763
+ #{createdBy}
764
+ )
765
+ </insert>
766
+
767
+ <select id="findEffectiveUnitPrice" resultMap="UnitPriceResultMap">
768
+ SELECT * FROM "単価マスタ"
769
+ WHERE "品目コード" = #{itemCode}
770
+ AND "取引先コード" = #{supplierCode}
771
+ AND "使用開始日" &lt;= #{date}
772
+ AND ("使用停止日" IS NULL OR "使用停止日" &gt;= #{date})
773
+ ORDER BY "使用開始日" DESC
774
+ LIMIT 1
775
+ </select>
776
+
777
+ <delete id="deleteAll">
778
+ DELETE FROM "単価マスタ"
779
+ </delete>
780
+ </mapper>
781
+ ```
782
+
783
+ </details>
784
+
785
+ <details>
786
+ <summary>PurchaseOrderMapper.xml</summary>
787
+
788
+ ```xml
789
+ <!-- src/main/resources/mapper/PurchaseOrderMapper.xml -->
790
+ <?xml version="1.0" encoding="UTF-8" ?>
791
+ <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
792
+ "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
793
+ <mapper namespace="com.example.pms.infrastructure.out.persistence.mapper.PurchaseOrderMapper">
794
+
795
+ <resultMap id="PurchaseOrderResultMap" type="com.example.pms.domain.model.purchase.PurchaseOrder">
796
+ <id property="id" column="ID"/>
797
+ <result property="purchaseOrderNumber" column="発注番号"/>
798
+ <result property="orderDate" column="発注日"/>
799
+ <result property="supplierCode" column="取引先コード"/>
800
+ <result property="ordererCode" column="発注担当者コード"/>
801
+ <result property="departmentCode" column="発注部門コード"/>
802
+ <result property="status" column="ステータス"
803
+ typeHandler="com.example.pms.infrastructure.out.persistence.typehandler.PurchaseOrderStatusTypeHandler"/>
804
+ <result property="remarks" column="備考"/>
805
+ <result property="createdAt" column="作成日時"/>
806
+ <result property="createdBy" column="作成者"/>
807
+ <result property="updatedAt" column="更新日時"/>
808
+ <result property="updatedBy" column="更新者"/>
809
+ </resultMap>
810
+
811
+ <!-- PostgreSQL用 INSERT -->
812
+ <insert id="insert" parameterType="com.example.pms.domain.model.purchase.PurchaseOrder"
813
+ useGeneratedKeys="true" keyProperty="id" keyColumn="ID" databaseId="postgresql">
814
+ INSERT INTO "発注データ" (
815
+ "発注番号", "発注日", "取引先コード", "発注担当者コード", "発注部門コード",
816
+ "ステータス", "備考", "作成者", "更新者"
817
+ ) VALUES (
818
+ #{purchaseOrderNumber},
819
+ #{orderDate},
820
+ #{supplierCode},
821
+ #{ordererCode},
822
+ #{departmentCode},
823
+ #{status, typeHandler=com.example.pms.infrastructure.out.persistence.typehandler.PurchaseOrderStatusTypeHandler}::発注ステータス,
824
+ #{remarks},
825
+ #{createdBy},
826
+ #{updatedBy}
827
+ )
828
+ </insert>
829
+
830
+ <!-- H2用 INSERT -->
831
+ <insert id="insert" parameterType="com.example.pms.domain.model.purchase.PurchaseOrder"
832
+ useGeneratedKeys="true" keyProperty="id" keyColumn="ID" databaseId="h2">
833
+ INSERT INTO "発注データ" (
834
+ "発注番号", "発注日", "取引先コード", "発注担当者コード", "発注部門コード",
835
+ "ステータス", "備考", "作成者", "更新者"
836
+ ) VALUES (
837
+ #{purchaseOrderNumber},
838
+ #{orderDate},
839
+ #{supplierCode},
840
+ #{ordererCode},
841
+ #{departmentCode},
842
+ #{status, typeHandler=com.example.pms.infrastructure.out.persistence.typehandler.PurchaseOrderStatusTypeHandler},
843
+ #{remarks},
844
+ #{createdBy},
845
+ #{updatedBy}
846
+ )
847
+ </insert>
848
+
849
+ <select id="findById" resultMap="PurchaseOrderResultMap">
850
+ SELECT * FROM "発注データ" WHERE "ID" = #{id}
851
+ </select>
852
+
853
+ <select id="findByPurchaseOrderNumber" resultMap="PurchaseOrderResultMap">
854
+ SELECT * FROM "発注データ" WHERE "発注番号" = #{purchaseOrderNumber}
855
+ </select>
856
+
857
+ <select id="findBySupplierCode" resultMap="PurchaseOrderResultMap">
858
+ SELECT * FROM "発注データ" WHERE "取引先コード" = #{supplierCode} ORDER BY "発注日" DESC
859
+ </select>
860
+
861
+ <!-- PostgreSQL用 findByStatus -->
862
+ <select id="findByStatus" resultMap="PurchaseOrderResultMap" databaseId="postgresql">
863
+ SELECT * FROM "発注データ" WHERE "ステータス" = #{status, typeHandler=com.example.pms.infrastructure.out.persistence.typehandler.PurchaseOrderStatusTypeHandler}::発注ステータス ORDER BY "発注日" DESC
864
+ </select>
865
+
866
+ <!-- H2用 findByStatus -->
867
+ <select id="findByStatus" resultMap="PurchaseOrderResultMap" databaseId="h2">
868
+ SELECT * FROM "発注データ" WHERE "ステータス" = #{status, typeHandler=com.example.pms.infrastructure.out.persistence.typehandler.PurchaseOrderStatusTypeHandler} ORDER BY "発注日" DESC
869
+ </select>
870
+
871
+ <select id="findAll" resultMap="PurchaseOrderResultMap">
872
+ SELECT * FROM "発注データ" ORDER BY "発注日" DESC
873
+ </select>
874
+
875
+ <!-- PostgreSQL用 updateStatus -->
876
+ <update id="updateStatus" databaseId="postgresql">
877
+ UPDATE "発注データ"
878
+ SET "ステータス" = #{status, typeHandler=com.example.pms.infrastructure.out.persistence.typehandler.PurchaseOrderStatusTypeHandler}::発注ステータス,
879
+ "更新日時" = CURRENT_TIMESTAMP
880
+ WHERE "ID" = #{id}
881
+ </update>
882
+
883
+ <!-- H2用 updateStatus -->
884
+ <update id="updateStatus" databaseId="h2">
885
+ UPDATE "発注データ"
886
+ SET "ステータス" = #{status, typeHandler=com.example.pms.infrastructure.out.persistence.typehandler.PurchaseOrderStatusTypeHandler},
887
+ "更新日時" = CURRENT_TIMESTAMP
888
+ WHERE "ID" = #{id}
889
+ </update>
890
+
891
+ <!-- PostgreSQL用 DELETE -->
892
+ <delete id="deleteAll" databaseId="postgresql">
893
+ TRUNCATE TABLE "発注データ" CASCADE
894
+ </delete>
895
+
896
+ <!-- H2用 DELETE -->
897
+ <delete id="deleteAll" databaseId="h2">
898
+ DELETE FROM "発注データ"
899
+ </delete>
900
+ </mapper>
901
+ ```
902
+
903
+ </details>
904
+
905
+ <details>
906
+ <summary>PurchaseOrderDetailMapper.xml</summary>
907
+
908
+ ```xml
909
+ <!-- src/main/resources/mapper/PurchaseOrderDetailMapper.xml -->
910
+ <?xml version="1.0" encoding="UTF-8" ?>
911
+ <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
912
+ "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
913
+ <mapper namespace="com.example.pms.infrastructure.out.persistence.mapper.PurchaseOrderDetailMapper">
914
+
915
+ <resultMap id="PurchaseOrderDetailResultMap" type="com.example.pms.domain.model.purchase.PurchaseOrderDetail">
916
+ <id property="id" column="ID"/>
917
+ <result property="purchaseOrderNumber" column="発注番号"/>
918
+ <result property="lineNumber" column="発注行番号"/>
919
+ <result property="orderNumber" column="オーダNO"/>
920
+ <result property="deliveryLocationCode" column="納入場所コード"/>
921
+ <result property="itemCode" column="品目コード"/>
922
+ <result property="miscellaneousItemFlag" column="諸口品目区分"/>
923
+ <result property="expectedReceivingDate" column="受入予定日"/>
924
+ <result property="confirmedDeliveryDate" column="回答納期"/>
925
+ <result property="orderUnitPrice" column="発注単価"/>
926
+ <result property="orderQuantity" column="発注数量"/>
927
+ <result property="receivedQuantity" column="入荷済数量"/>
928
+ <result property="inspectedQuantity" column="検査済数量"/>
929
+ <result property="acceptedQuantity" column="検収済数量"/>
930
+ <result property="orderAmount" column="発注金額"/>
931
+ <result property="taxAmount" column="消費税金額"/>
932
+ <result property="completedFlag" column="完了フラグ"/>
933
+ <result property="detailRemarks" column="明細備考"/>
934
+ <result property="createdAt" column="作成日時"/>
935
+ <result property="createdBy" column="作成者"/>
936
+ <result property="updatedAt" column="更新日時"/>
937
+ <result property="updatedBy" column="更新者"/>
938
+ </resultMap>
939
+
940
+ <insert id="insert" useGeneratedKeys="true" keyProperty="id" keyColumn="ID">
941
+ INSERT INTO "発注明細データ" (
942
+ "発注番号", "発注行番号", "オーダNO", "納入場所コード", "品目コード",
943
+ "諸口品目区分", "受入予定日", "回答納期", "発注単価", "発注数量",
944
+ "発注金額", "消費税金額", "作成者"
945
+ ) VALUES (
946
+ #{purchaseOrderNumber},
947
+ #{lineNumber},
948
+ #{orderNumber},
949
+ #{deliveryLocationCode},
950
+ #{itemCode},
951
+ #{miscellaneousItemFlag},
952
+ #{expectedReceivingDate},
953
+ #{confirmedDeliveryDate},
954
+ #{orderUnitPrice},
955
+ #{orderQuantity},
956
+ #{orderAmount},
957
+ #{taxAmount},
958
+ #{createdBy}
959
+ )
960
+ </insert>
961
+
962
+ <select id="findByPurchaseOrderNumber" resultMap="PurchaseOrderDetailResultMap">
963
+ SELECT * FROM "発注明細データ"
964
+ WHERE "発注番号" = #{purchaseOrderNumber}
965
+ ORDER BY "発注行番号"
966
+ </select>
967
+
968
+ <select id="findByPurchaseOrderNumberAndLineNumber" resultMap="PurchaseOrderDetailResultMap">
969
+ SELECT * FROM "発注明細データ"
970
+ WHERE "発注番号" = #{purchaseOrderNumber} AND "発注行番号" = #{lineNumber}
971
+ </select>
972
+
973
+ <update id="updateReceivedQuantity">
974
+ UPDATE "発注明細データ"
975
+ SET "入荷済数量" = #{receivedQuantity}, "更新日時" = CURRENT_TIMESTAMP
976
+ WHERE "発注番号" = #{purchaseOrderNumber} AND "発注行番号" = #{lineNumber}
977
+ </update>
978
+
979
+ <update id="updateAcceptedQuantity">
980
+ UPDATE "発注明細データ"
981
+ SET "検収済数量" = #{acceptedQuantity}, "更新日時" = CURRENT_TIMESTAMP
982
+ WHERE "発注番号" = #{purchaseOrderNumber} AND "発注行番号" = #{lineNumber}
983
+ </update>
984
+
985
+ <delete id="deleteAll">
986
+ DELETE FROM "発注明細データ"
987
+ </delete>
988
+ </mapper>
989
+ ```
990
+
991
+ </details>
992
+
993
+ ### Mapper インターフェース
994
+
995
+ <details>
996
+ <summary>UnitPriceMapper</summary>
997
+
998
+ ```java
999
+ // src/main/java/com/example/pms/infrastructure/out/persistence/mapper/UnitPriceMapper.java
1000
+ package com.example.pms.infrastructure.out.persistence.mapper;
1001
+
1002
+ import com.example.pms.domain.model.purchase.UnitPrice;
1003
+ import org.apache.ibatis.annotations.Mapper;
1004
+ import org.apache.ibatis.annotations.Param;
1005
+
1006
+ import java.time.LocalDate;
1007
+
1008
+ @Mapper
1009
+ public interface UnitPriceMapper {
1010
+ void insert(UnitPrice unitPrice);
1011
+ UnitPrice findEffectiveUnitPrice(@Param("itemCode") String itemCode,
1012
+ @Param("supplierCode") String supplierCode,
1013
+ @Param("date") LocalDate date);
1014
+ void deleteAll();
1015
+ }
1016
+ ```
1017
+
1018
+ </details>
1019
+
1020
+ <details>
1021
+ <summary>PurchaseOrderMapper</summary>
1022
+
1023
+ ```java
1024
+ // src/main/java/com/example/pms/infrastructure/out/persistence/mapper/PurchaseOrderMapper.java
1025
+ package com.example.pms.infrastructure.out.persistence.mapper;
1026
+
1027
+ import com.example.pms.domain.model.purchase.PurchaseOrder;
1028
+ import com.example.pms.domain.model.purchase.PurchaseOrderStatus;
1029
+ import org.apache.ibatis.annotations.Mapper;
1030
+ import org.apache.ibatis.annotations.Param;
1031
+
1032
+ import java.util.List;
1033
+
1034
+ @Mapper
1035
+ public interface PurchaseOrderMapper {
1036
+ void insert(PurchaseOrder purchaseOrder);
1037
+ PurchaseOrder findById(Integer id);
1038
+ PurchaseOrder findByPurchaseOrderNumber(String purchaseOrderNumber);
1039
+ List<PurchaseOrder> findBySupplierCode(String supplierCode);
1040
+ List<PurchaseOrder> findByStatus(PurchaseOrderStatus status);
1041
+ List<PurchaseOrder> findAll();
1042
+ void updateStatus(@Param("id") Integer id, @Param("status") PurchaseOrderStatus status);
1043
+ void deleteAll();
1044
+ }
1045
+ ```
1046
+
1047
+ </details>
1048
+
1049
+ <details>
1050
+ <summary>PurchaseOrderDetailMapper</summary>
1051
+
1052
+ ```java
1053
+ // src/main/java/com/example/pms/infrastructure/out/persistence/mapper/PurchaseOrderDetailMapper.java
1054
+ package com.example.pms.infrastructure.out.persistence.mapper;
1055
+
1056
+ import com.example.pms.domain.model.purchase.PurchaseOrderDetail;
1057
+ import org.apache.ibatis.annotations.Mapper;
1058
+ import org.apache.ibatis.annotations.Param;
1059
+
1060
+ import java.math.BigDecimal;
1061
+ import java.util.List;
1062
+
1063
+ @Mapper
1064
+ public interface PurchaseOrderDetailMapper {
1065
+ void insert(PurchaseOrderDetail detail);
1066
+ List<PurchaseOrderDetail> findByPurchaseOrderNumber(String purchaseOrderNumber);
1067
+ PurchaseOrderDetail findByPurchaseOrderNumberAndLineNumber(
1068
+ @Param("purchaseOrderNumber") String purchaseOrderNumber,
1069
+ @Param("lineNumber") Integer lineNumber);
1070
+ void updateReceivedQuantity(@Param("purchaseOrderNumber") String purchaseOrderNumber,
1071
+ @Param("lineNumber") Integer lineNumber,
1072
+ @Param("receivedQuantity") BigDecimal receivedQuantity);
1073
+ void updateAcceptedQuantity(@Param("purchaseOrderNumber") String purchaseOrderNumber,
1074
+ @Param("lineNumber") Integer lineNumber,
1075
+ @Param("acceptedQuantity") BigDecimal acceptedQuantity);
1076
+ void deleteAll();
1077
+ }
1078
+ ```
1079
+
1080
+ </details>
1081
+
1082
+ ### 発注サービスの実装
1083
+
1084
+ <details>
1085
+ <summary>PurchaseOrderService</summary>
1086
+
1087
+ ```java
1088
+ // src/main/java/com/example/pms/application/service/PurchaseOrderService.java
1089
+ package com.example.pms.application.service;
1090
+
1091
+ import com.example.pms.domain.model.purchase.*;
1092
+ import com.example.pms.infrastructure.out.persistence.mapper.*;
1093
+ import lombok.RequiredArgsConstructor;
1094
+ import org.springframework.stereotype.Service;
1095
+ import org.springframework.transaction.annotation.Transactional;
1096
+
1097
+ import java.math.BigDecimal;
1098
+ import java.math.RoundingMode;
1099
+ import java.time.LocalDate;
1100
+ import java.time.format.DateTimeFormatter;
1101
+ import java.util.ArrayList;
1102
+ import java.util.List;
1103
+
1104
+ @Service
1105
+ @RequiredArgsConstructor
1106
+ public class PurchaseOrderService {
1107
+
1108
+ private final PurchaseOrderMapper purchaseOrderMapper;
1109
+ private final PurchaseOrderDetailMapper purchaseOrderDetailMapper;
1110
+ private final UnitPriceMapper unitPriceMapper;
1111
+
1112
+ /**
1113
+ * 発注番号を生成する
1114
+ */
1115
+ private String generatePurchaseOrderNumber(LocalDate orderDate) {
1116
+ String prefix = "PO-" + orderDate.format(DateTimeFormatter.ofPattern("yyyyMM")) + "-";
1117
+ String latestNumber = purchaseOrderMapper.findLatestPurchaseOrderNumber(prefix + "%");
1118
+
1119
+ int sequence = 1;
1120
+ if (latestNumber != null) {
1121
+ int currentSequence = Integer.parseInt(latestNumber.substring(latestNumber.length() - 4));
1122
+ sequence = currentSequence + 1;
1123
+ }
1124
+
1125
+ return prefix + String.format("%04d", sequence);
1126
+ }
1127
+
1128
+ /**
1129
+ * 発注を作成する
1130
+ */
1131
+ @Transactional
1132
+ public PurchaseOrder createPurchaseOrder(PurchaseOrderCreateInput input) {
1133
+ String purchaseOrderNumber = generatePurchaseOrderNumber(input.getOrderDate());
1134
+ BigDecimal taxRate = input.getTaxRate() != null ? input.getTaxRate() : new BigDecimal("10");
1135
+
1136
+ // 発注ヘッダを作成
1137
+ PurchaseOrder purchaseOrder = PurchaseOrder.builder()
1138
+ .purchaseOrderNumber(purchaseOrderNumber)
1139
+ .orderDate(input.getOrderDate())
1140
+ .supplierCode(input.getSupplierCode())
1141
+ .ordererCode(input.getOrdererCode())
1142
+ .departmentCode(input.getDepartmentCode())
1143
+ .status(PurchaseOrderStatus.CREATING)
1144
+ .remarks(input.getRemarks())
1145
+ .build();
1146
+ purchaseOrderMapper.insert(purchaseOrder);
1147
+
1148
+ // 発注明細を作成
1149
+ List<PurchaseOrderDetail> details = new ArrayList<>();
1150
+ int lineNumber = 0;
1151
+
1152
+ for (PurchaseOrderDetailInput detailInput : input.getDetails()) {
1153
+ lineNumber++;
1154
+
1155
+ // 単価を取得
1156
+ UnitPrice unitPrice = unitPriceMapper.findEffectiveUnitPrice(
1157
+ detailInput.getItemCode(),
1158
+ input.getSupplierCode(),
1159
+ input.getOrderDate()
1160
+ );
1161
+
1162
+ if (unitPrice == null) {
1163
+ throw new IllegalArgumentException(
1164
+ "Unit price not found: " + detailInput.getItemCode() + " / " + input.getSupplierCode());
1165
+ }
1166
+
1167
+ // 金額計算
1168
+ BigDecimal orderAmount = unitPrice.getUnitPrice().multiply(detailInput.getOrderQuantity());
1169
+ BigDecimal taxAmount = orderAmount.multiply(taxRate)
1170
+ .divide(new BigDecimal("100"), 0, RoundingMode.HALF_UP);
1171
+
1172
+ PurchaseOrderDetail detail = PurchaseOrderDetail.builder()
1173
+ .purchaseOrderNumber(purchaseOrderNumber)
1174
+ .lineNumber(lineNumber)
1175
+ .orderNumber(detailInput.getOrderNumber())
1176
+ .deliveryLocationCode(detailInput.getDeliveryLocationCode())
1177
+ .itemCode(detailInput.getItemCode())
1178
+ .miscellaneousItemFlag(false)
1179
+ .expectedReceivingDate(detailInput.getExpectedReceivingDate())
1180
+ .orderUnitPrice(unitPrice.getUnitPrice())
1181
+ .orderQuantity(detailInput.getOrderQuantity())
1182
+ .orderAmount(orderAmount)
1183
+ .taxAmount(taxAmount)
1184
+ .build();
1185
+ purchaseOrderDetailMapper.insert(detail);
1186
+
1187
+ details.add(detail);
1188
+ }
1189
+
1190
+ purchaseOrder.setDetails(details);
1191
+ return purchaseOrder;
1192
+ }
1193
+
1194
+ /**
1195
+ * 発注を確定する
1196
+ */
1197
+ @Transactional
1198
+ public PurchaseOrder confirmPurchaseOrder(String purchaseOrderNumber) {
1199
+ PurchaseOrder purchaseOrder = purchaseOrderMapper.findByPurchaseOrderNumber(purchaseOrderNumber);
1200
+
1201
+ if (purchaseOrder == null) {
1202
+ throw new IllegalArgumentException("Purchase order not found: " + purchaseOrderNumber);
1203
+ }
1204
+
1205
+ if (purchaseOrder.getStatus() != PurchaseOrderStatus.CREATING) {
1206
+ throw new IllegalStateException("Only creating purchase orders can be confirmed: " + purchaseOrderNumber);
1207
+ }
1208
+
1209
+ purchaseOrderMapper.updateStatus(purchaseOrderNumber, PurchaseOrderStatus.ORDERED);
1210
+ purchaseOrder.setStatus(PurchaseOrderStatus.ORDERED);
1211
+ return purchaseOrder;
1212
+ }
1213
+
1214
+ /**
1215
+ * 発注を取消する
1216
+ */
1217
+ @Transactional
1218
+ public PurchaseOrder cancelPurchaseOrder(String purchaseOrderNumber) {
1219
+ PurchaseOrder purchaseOrder = purchaseOrderMapper.findByPurchaseOrderNumber(purchaseOrderNumber);
1220
+
1221
+ if (purchaseOrder == null) {
1222
+ throw new IllegalArgumentException("Purchase order not found: " + purchaseOrderNumber);
1223
+ }
1224
+
1225
+ List<PurchaseOrderDetail> details = purchaseOrderDetailMapper.findByPurchaseOrderNumber(purchaseOrderNumber);
1226
+ boolean hasReceived = details.stream()
1227
+ .anyMatch(d -> d.getReceivedQuantity() != null &&
1228
+ d.getReceivedQuantity().compareTo(BigDecimal.ZERO) > 0);
1229
+
1230
+ if (hasReceived) {
1231
+ throw new IllegalStateException("Cannot cancel purchase order with received items: " + purchaseOrderNumber);
1232
+ }
1233
+
1234
+ purchaseOrderMapper.updateStatus(purchaseOrderNumber, PurchaseOrderStatus.CANCELLED);
1235
+ purchaseOrder.setStatus(PurchaseOrderStatus.CANCELLED);
1236
+ return purchaseOrder;
1237
+ }
1238
+ }
1239
+ ```
1240
+
1241
+ </details>
1242
+
1243
+ <details>
1244
+ <summary>入力 DTO クラス</summary>
1245
+
1246
+ ```java
1247
+ // src/main/java/com/example/pms/application/service/PurchaseOrderCreateInput.java
1248
+ package com.example.pms.application.service;
1249
+
1250
+ import lombok.Builder;
1251
+ import lombok.Data;
1252
+
1253
+ import java.math.BigDecimal;
1254
+ import java.time.LocalDate;
1255
+ import java.util.List;
1256
+
1257
+ @Data
1258
+ @Builder
1259
+ public class PurchaseOrderCreateInput {
1260
+ private String supplierCode;
1261
+ private LocalDate orderDate;
1262
+ private String ordererCode;
1263
+ private String departmentCode;
1264
+ private BigDecimal taxRate;
1265
+ private String remarks;
1266
+ private List<PurchaseOrderDetailInput> details;
1267
+ }
1268
+ ```
1269
+
1270
+ ```java
1271
+ // src/main/java/com/example/pms/application/service/PurchaseOrderDetailInput.java
1272
+ package com.example.pms.application.service;
1273
+
1274
+ import lombok.Builder;
1275
+ import lombok.Data;
1276
+
1277
+ import java.math.BigDecimal;
1278
+ import java.time.LocalDate;
1279
+
1280
+ @Data
1281
+ @Builder
1282
+ public class PurchaseOrderDetailInput {
1283
+ private String itemCode;
1284
+ private BigDecimal orderQuantity;
1285
+ private LocalDate expectedReceivingDate;
1286
+ private String orderNumber;
1287
+ private String deliveryLocationCode;
1288
+ }
1289
+ ```
1290
+
1291
+ </details>
1292
+
1293
+ ### TDD: 発注データの登録テスト
1294
+
1295
+ <details>
1296
+ <summary>PurchaseOrderServiceTest</summary>
1297
+
1298
+ ```java
1299
+ // src/test/java/com/example/pms/application/service/PurchaseOrderServiceTest.java
1300
+ package com.example.pms.application.service;
1301
+
1302
+ import com.example.pms.domain.model.item.Item;
1303
+ import com.example.pms.domain.model.item.ItemCategory;
1304
+ import com.example.pms.domain.model.master.Supplier;
1305
+ import com.example.pms.domain.model.purchase.*;
1306
+ import com.example.pms.infrastructure.out.persistence.mapper.*;
1307
+ import org.junit.jupiter.api.*;
1308
+ import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
1309
+ import org.springframework.beans.factory.annotation.Autowired;
1310
+ import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
1311
+ import org.springframework.context.annotation.Import;
1312
+ import org.springframework.test.context.DynamicPropertyRegistry;
1313
+ import org.springframework.test.context.DynamicPropertySource;
1314
+ import org.testcontainers.containers.PostgreSQLContainer;
1315
+ import org.testcontainers.junit.jupiter.Container;
1316
+ import org.testcontainers.junit.jupiter.Testcontainers;
1317
+
1318
+ import java.math.BigDecimal;
1319
+ import java.time.LocalDate;
1320
+ import java.util.List;
1321
+
1322
+ import static org.assertj.core.api.Assertions.*;
1323
+
1324
+ @MybatisTest
1325
+ @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
1326
+ @Import(PurchaseOrderService.class)
1327
+ @Testcontainers
1328
+ @DisplayName("発注業務")
1329
+ class PurchaseOrderServiceTest {
1330
+
1331
+ @Container
1332
+ static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
1333
+ .withDatabaseName("testdb")
1334
+ .withUsername("testuser")
1335
+ .withPassword("testpass");
1336
+
1337
+ @DynamicPropertySource
1338
+ static void configureProperties(DynamicPropertyRegistry registry) {
1339
+ registry.add("spring.datasource.url", postgres::getJdbcUrl);
1340
+ registry.add("spring.datasource.username", postgres::getUsername);
1341
+ registry.add("spring.datasource.password", postgres::getPassword);
1342
+ }
1343
+
1344
+ @Autowired
1345
+ private PurchaseOrderService purchaseOrderService;
1346
+
1347
+ @Autowired
1348
+ private PurchaseOrderMapper purchaseOrderMapper;
1349
+
1350
+ @Autowired
1351
+ private PurchaseOrderDetailMapper purchaseOrderDetailMapper;
1352
+
1353
+ @Autowired
1354
+ private ItemMapper itemMapper;
1355
+
1356
+ @Autowired
1357
+ private SupplierMapper supplierMapper;
1358
+
1359
+ @Autowired
1360
+ private UnitPriceMapper unitPriceMapper;
1361
+
1362
+ @BeforeEach
1363
+ void setUp() {
1364
+ purchaseOrderDetailMapper.deleteAll();
1365
+ purchaseOrderMapper.deleteAll();
1366
+ unitPriceMapper.deleteAll();
1367
+ supplierMapper.deleteAll();
1368
+ itemMapper.deleteAll();
1369
+ }
1370
+
1371
+ @Nested
1372
+ @DisplayName("発注データの作成")
1373
+ class PurchaseOrderCreation {
1374
+
1375
+ @Test
1376
+ @DisplayName("購買オーダから発注データを作成できる")
1377
+ void canCreatePurchaseOrderFromOrder() {
1378
+ // Arrange: マスタデータを準備
1379
+ Supplier supplier = Supplier.builder()
1380
+ .supplierCode("SUP-001")
1381
+ .effectiveFrom(LocalDate.of(2025, 1, 1))
1382
+ .supplierName("テスト仕入先株式会社")
1383
+ .supplierNameKana("テストシイレサキカブシキガイシャ")
1384
+ .build();
1385
+ supplierMapper.insert(supplier);
1386
+
1387
+ Item item = Item.builder()
1388
+ .itemCode("MAT-001")
1389
+ .effectiveFrom(LocalDate.of(2025, 1, 1))
1390
+ .itemName("材料A")
1391
+ .itemCategory(ItemCategory.MATERIAL)
1392
+ .build();
1393
+ itemMapper.insert(item);
1394
+
1395
+ UnitPrice unitPrice = UnitPrice.builder()
1396
+ .itemCode("MAT-001")
1397
+ .supplierCode("SUP-001")
1398
+ .effectiveFrom(LocalDate.of(2025, 1, 1))
1399
+ .unitPrice(new BigDecimal("1000"))
1400
+ .build();
1401
+ unitPriceMapper.insert(unitPrice);
1402
+
1403
+ // Act: 発注を作成
1404
+ PurchaseOrderCreateInput input = PurchaseOrderCreateInput.builder()
1405
+ .supplierCode("SUP-001")
1406
+ .orderDate(LocalDate.of(2025, 1, 15))
1407
+ .taxRate(new BigDecimal("10"))
1408
+ .details(List.of(
1409
+ PurchaseOrderDetailInput.builder()
1410
+ .itemCode("MAT-001")
1411
+ .orderQuantity(new BigDecimal("100"))
1412
+ .expectedReceivingDate(LocalDate.of(2025, 1, 25))
1413
+ .build()
1414
+ ))
1415
+ .build();
1416
+
1417
+ PurchaseOrder purchaseOrder = purchaseOrderService.createPurchaseOrder(input);
1418
+
1419
+ // Assert
1420
+ assertThat(purchaseOrder).isNotNull();
1421
+ assertThat(purchaseOrder.getPurchaseOrderNumber()).startsWith("PO-");
1422
+ assertThat(purchaseOrder.getStatus()).isEqualTo(PurchaseOrderStatus.CREATING);
1423
+ assertThat(purchaseOrder.getDetails()).hasSize(1);
1424
+ assertThat(purchaseOrder.getDetails().get(0).getOrderQuantity())
1425
+ .isEqualByComparingTo(new BigDecimal("100"));
1426
+ assertThat(purchaseOrder.getDetails().get(0).getOrderUnitPrice())
1427
+ .isEqualByComparingTo(new BigDecimal("1000"));
1428
+ assertThat(purchaseOrder.getDetails().get(0).getOrderAmount())
1429
+ .isEqualByComparingTo(new BigDecimal("100000"));
1430
+ }
1431
+
1432
+ @Test
1433
+ @DisplayName("発注を発注済ステータスに変更できる")
1434
+ void canConfirmPurchaseOrder() {
1435
+ // Arrange: 発注データを準備
1436
+ Supplier supplier = Supplier.builder()
1437
+ .supplierCode("SUP-003")
1438
+ .effectiveFrom(LocalDate.of(2025, 1, 1))
1439
+ .supplierName("ステータス確認用仕入先")
1440
+ .build();
1441
+ supplierMapper.insert(supplier);
1442
+
1443
+ Item item = Item.builder()
1444
+ .itemCode("MAT-004")
1445
+ .effectiveFrom(LocalDate.of(2025, 1, 1))
1446
+ .itemName("材料D")
1447
+ .itemCategory(ItemCategory.MATERIAL)
1448
+ .build();
1449
+ itemMapper.insert(item);
1450
+
1451
+ unitPriceMapper.insert(UnitPrice.builder()
1452
+ .itemCode("MAT-004")
1453
+ .supplierCode("SUP-003")
1454
+ .effectiveFrom(LocalDate.of(2025, 1, 1))
1455
+ .unitPrice(new BigDecimal("2000"))
1456
+ .build());
1457
+
1458
+ PurchaseOrderCreateInput input = PurchaseOrderCreateInput.builder()
1459
+ .supplierCode("SUP-003")
1460
+ .orderDate(LocalDate.of(2025, 1, 15))
1461
+ .details(List.of(
1462
+ PurchaseOrderDetailInput.builder()
1463
+ .itemCode("MAT-004")
1464
+ .orderQuantity(new BigDecimal("10"))
1465
+ .expectedReceivingDate(LocalDate.of(2025, 1, 25))
1466
+ .build()
1467
+ ))
1468
+ .build();
1469
+
1470
+ PurchaseOrder purchaseOrder = purchaseOrderService.createPurchaseOrder(input);
1471
+
1472
+ // Act: 発注確定
1473
+ PurchaseOrder confirmedOrder = purchaseOrderService.confirmPurchaseOrder(
1474
+ purchaseOrder.getPurchaseOrderNumber());
1475
+
1476
+ // Assert
1477
+ assertThat(confirmedOrder.getStatus()).isEqualTo(PurchaseOrderStatus.ORDERED);
1478
+ }
1479
+ }
1480
+
1481
+ @Nested
1482
+ @DisplayName("消費税計算")
1483
+ class TaxCalculation {
1484
+
1485
+ @Test
1486
+ @DisplayName("消費税額が正しく計算される")
1487
+ void calculatesTaxCorrectly() {
1488
+ // Arrange
1489
+ Supplier supplier = Supplier.builder()
1490
+ .supplierCode("SUP-004")
1491
+ .effectiveFrom(LocalDate.of(2025, 1, 1))
1492
+ .supplierName("消費税確認用仕入先")
1493
+ .build();
1494
+ supplierMapper.insert(supplier);
1495
+
1496
+ Item item = Item.builder()
1497
+ .itemCode("MAT-005")
1498
+ .effectiveFrom(LocalDate.of(2025, 1, 1))
1499
+ .itemName("材料E")
1500
+ .itemCategory(ItemCategory.MATERIAL)
1501
+ .build();
1502
+ itemMapper.insert(item);
1503
+
1504
+ unitPriceMapper.insert(UnitPrice.builder()
1505
+ .itemCode("MAT-005")
1506
+ .supplierCode("SUP-004")
1507
+ .effectiveFrom(LocalDate.of(2025, 1, 1))
1508
+ .unitPrice(new BigDecimal("1000"))
1509
+ .build());
1510
+
1511
+ // Act
1512
+ PurchaseOrderCreateInput input = PurchaseOrderCreateInput.builder()
1513
+ .supplierCode("SUP-004")
1514
+ .orderDate(LocalDate.of(2025, 1, 15))
1515
+ .taxRate(new BigDecimal("10"))
1516
+ .details(List.of(
1517
+ PurchaseOrderDetailInput.builder()
1518
+ .itemCode("MAT-005")
1519
+ .orderQuantity(new BigDecimal("100"))
1520
+ .expectedReceivingDate(LocalDate.of(2025, 1, 25))
1521
+ .build()
1522
+ ))
1523
+ .build();
1524
+
1525
+ PurchaseOrder purchaseOrder = purchaseOrderService.createPurchaseOrder(input);
1526
+
1527
+ // Assert: 100 × 1000 = 100,000円、消費税 10,000円
1528
+ assertThat(purchaseOrder.getDetails().get(0).getOrderAmount())
1529
+ .isEqualByComparingTo(new BigDecimal("100000"));
1530
+ assertThat(purchaseOrder.getDetails().get(0).getTaxAmount())
1531
+ .isEqualByComparingTo(new BigDecimal("10000"));
1532
+ }
1533
+ }
1534
+ }
1535
+ ```
1536
+
1537
+ </details>
1538
+
1539
+ ---
1540
+
1541
+ ## 25.2 入荷・検収業務の DB 設計
1542
+
1543
+ 入荷・検収業務は、発注した品目が納品された後の一連の処理を管理します。
1544
+
1545
+ ### 入荷・受入業務の流れ
1546
+
1547
+ ```plantuml
1548
+ @startuml
1549
+
1550
+ title 入荷から検収までの流れ
1551
+
1552
+ start
1553
+
1554
+ :発注明細;
1555
+
1556
+ :入荷受付;
1557
+ note right
1558
+ ・納品書と現品の確認
1559
+ ・入荷数量の記録
1560
+ end note
1561
+
1562
+ :受入検査;
1563
+ note right
1564
+ ・品質検査の実施
1565
+ ・良品/不良品の判定
1566
+ end note
1567
+
1568
+ if (検査結果) then (合格)
1569
+ :検収処理;
1570
+ note right
1571
+ ・検収数量の確定
1572
+ ・仕入計上
1573
+ end note
1574
+
1575
+ :在庫計上;
1576
+ else (不合格)
1577
+ :検収返品;
1578
+ note right
1579
+ ・返品処理
1580
+ ・仕入先への通知
1581
+ end note
1582
+ endif
1583
+
1584
+ stop
1585
+
1586
+ @enduml
1587
+ ```
1588
+
1589
+ ### 入荷・検収関連のスキーマ設計
1590
+
1591
+ #### テーブル構造
1592
+
1593
+ | テーブル名 | 説明 |
1594
+ |-----------|------|
1595
+ | 入荷受入データ | 入荷情報(入荷番号、数量、担当者等) |
1596
+ | 欠点マスタ | 不良品の欠点コード・内容 |
1597
+ | 受入検査データ | 受入検査情報(良品数、不良品数等) |
1598
+ | 検収データ | 検収情報(検収数、検収金額等) |
1599
+
1600
+ #### データの関連
1601
+
1602
+ ```plantuml
1603
+ @startuml
1604
+
1605
+ title 入荷・検収データの関連
1606
+
1607
+ entity "発注明細データ" as pod {
1608
+ * 発注番号 [PK]
1609
+ * 発注行番号 [PK]
1610
+ --
1611
+ 発注数量
1612
+ 入荷済数量
1613
+ 検収済数量
1614
+ }
1615
+
1616
+ entity "入荷受入データ" as rcv {
1617
+ * 入荷番号 [PK]
1618
+ --
1619
+ 発注番号 [FK]
1620
+ 発注行番号 [FK]
1621
+ 入荷日
1622
+ 入荷数量
1623
+ 入荷受入区分
1624
+ }
1625
+
1626
+ entity "受入検査データ" as ins {
1627
+ * 受入検査番号 [PK]
1628
+ --
1629
+ 入荷番号 [FK]
1630
+ 受入検査日
1631
+ 良品数
1632
+ 不良品数
1633
+ }
1634
+
1635
+ entity "検収データ" as acc {
1636
+ * 検収番号 [PK]
1637
+ --
1638
+ 受入検査番号 [FK]
1639
+ 発注番号 [FK]
1640
+ 発注行番号 [FK]
1641
+ 検収日
1642
+ 検収数
1643
+ 検収金額
1644
+ }
1645
+
1646
+ pod ||--o{ rcv : 入荷
1647
+ rcv ||--o{ ins : 検査
1648
+ ins ||--o{ acc : 検収
1649
+ pod ||--o{ acc : 検収
1650
+
1651
+ @enduml
1652
+ ```
1653
+
1654
+ <details>
1655
+ <summary>DDL: 入荷・検収関連テーブル</summary>
1656
+
1657
+ ```sql
1658
+ -- V010__create_receiving_tables.sql
1659
+
1660
+ -- 入荷受入データ
1661
+ CREATE TABLE "入荷受入データ" (
1662
+ "ID" SERIAL PRIMARY KEY,
1663
+ "入荷番号" VARCHAR(20) UNIQUE NOT NULL,
1664
+ "発注番号" VARCHAR(20) NOT NULL,
1665
+ "発注行番号" INTEGER NOT NULL,
1666
+ "入荷日" DATE NOT NULL,
1667
+ "入荷担当者コード" VARCHAR(20),
1668
+ "入荷受入区分" 入荷受入区分 DEFAULT '通常入荷' NOT NULL,
1669
+ "品目コード" VARCHAR(20) NOT NULL,
1670
+ "諸口品目区分" BOOLEAN DEFAULT FALSE NOT NULL,
1671
+ "入荷数量" DECIMAL(15, 2) NOT NULL,
1672
+ "入荷備考" TEXT,
1673
+ "作成日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
1674
+ "作成者" VARCHAR(50),
1675
+ "更新日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
1676
+ "更新者" VARCHAR(50),
1677
+ CONSTRAINT "fk_入荷受入_発注明細"
1678
+ FOREIGN KEY ("発注番号", "発注行番号") REFERENCES "発注明細データ"("発注番号", "発注行番号"),
1679
+ CONSTRAINT "fk_入荷受入_品目"
1680
+ FOREIGN KEY ("品目コード") REFERENCES "品目マスタ"("品目コード")
1681
+ );
1682
+
1683
+ -- 欠点マスタ
1684
+ CREATE TABLE "欠点マスタ" (
1685
+ "ID" SERIAL PRIMARY KEY,
1686
+ "欠点コード" VARCHAR(20) UNIQUE NOT NULL,
1687
+ "欠点内容" VARCHAR(200) NOT NULL,
1688
+ "作成日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
1689
+ "作成者" VARCHAR(50),
1690
+ "更新日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
1691
+ "更新者" VARCHAR(50)
1692
+ );
1693
+
1694
+ -- 受入検査データ
1695
+ CREATE TABLE "受入検査データ" (
1696
+ "ID" SERIAL PRIMARY KEY,
1697
+ "受入検査番号" VARCHAR(20) UNIQUE NOT NULL,
1698
+ "入荷番号" VARCHAR(20) NOT NULL,
1699
+ "発注番号" VARCHAR(20) NOT NULL,
1700
+ "発注行番号" INTEGER NOT NULL,
1701
+ "受入検査日" DATE NOT NULL,
1702
+ "受入検査担当者コード" VARCHAR(20),
1703
+ "品目コード" VARCHAR(20) NOT NULL,
1704
+ "諸口品目区分" BOOLEAN DEFAULT FALSE NOT NULL,
1705
+ "良品数" DECIMAL(15, 2) NOT NULL,
1706
+ "不良品数" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
1707
+ "受入検査備考" TEXT,
1708
+ "作成日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
1709
+ "作成者" VARCHAR(50),
1710
+ "更新日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
1711
+ "更新者" VARCHAR(50),
1712
+ CONSTRAINT "fk_受入検査_入荷"
1713
+ FOREIGN KEY ("入荷番号") REFERENCES "入荷受入データ"("入荷番号"),
1714
+ CONSTRAINT "fk_受入検査_品目"
1715
+ FOREIGN KEY ("品目コード") REFERENCES "品目マスタ"("品目コード")
1716
+ );
1717
+
1718
+ -- 検収データ
1719
+ CREATE TABLE "検収データ" (
1720
+ "ID" SERIAL PRIMARY KEY,
1721
+ "検収番号" VARCHAR(20) UNIQUE NOT NULL,
1722
+ "受入検査番号" VARCHAR(20) NOT NULL,
1723
+ "発注番号" VARCHAR(20) NOT NULL,
1724
+ "発注行番号" INTEGER NOT NULL,
1725
+ "検収日" DATE NOT NULL,
1726
+ "検収担当者コード" VARCHAR(20),
1727
+ "取引先コード" VARCHAR(20) NOT NULL,
1728
+ "品目コード" VARCHAR(20) NOT NULL,
1729
+ "諸口品目区分" BOOLEAN DEFAULT FALSE NOT NULL,
1730
+ "検収数" DECIMAL(15, 2) NOT NULL,
1731
+ "検収単価" DECIMAL(15, 2) NOT NULL,
1732
+ "検収金額" DECIMAL(15, 2) NOT NULL,
1733
+ "検収消費税額" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
1734
+ "検収備考" TEXT,
1735
+ "作成日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
1736
+ "作成者" VARCHAR(50),
1737
+ "更新日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
1738
+ "更新者" VARCHAR(50),
1739
+ CONSTRAINT "fk_検収_受入検査"
1740
+ FOREIGN KEY ("受入検査番号") REFERENCES "受入検査データ"("受入検査番号"),
1741
+ CONSTRAINT "fk_検収_発注明細"
1742
+ FOREIGN KEY ("発注番号", "発注行番号") REFERENCES "発注明細データ"("発注番号", "発注行番号"),
1743
+ CONSTRAINT "fk_検収_取引先"
1744
+ FOREIGN KEY ("取引先コード") REFERENCES "取引先マスタ"("取引先コード"),
1745
+ CONSTRAINT "fk_検収_品目"
1746
+ FOREIGN KEY ("品目コード") REFERENCES "品目マスタ"("品目コード")
1747
+ );
1748
+
1749
+ -- インデックス
1750
+ CREATE INDEX "idx_入荷受入_発注番号" ON "入荷受入データ"("発注番号", "発注行番号");
1751
+ CREATE INDEX "idx_入荷受入_入荷日" ON "入荷受入データ"("入荷日");
1752
+ CREATE INDEX "idx_受入検査_入荷番号" ON "受入検査データ"("入荷番号");
1753
+ CREATE INDEX "idx_検収_受入検査番号" ON "検収データ"("受入検査番号");
1754
+ CREATE INDEX "idx_検収_発注番号" ON "検収データ"("発注番号", "発注行番号");
1755
+ ```
1756
+
1757
+ </details>
1758
+
1759
+ ### 発注明細との紐付け
1760
+
1761
+ 入荷受入データは発注明細データと紐付けられ、入荷のたびに発注明細の「入荷済数量」が更新されます。これにより、分納への対応が可能になります。
1762
+
1763
+ ### 入荷受入区分(入荷 / 受入返品)
1764
+
1765
+ 入荷受入区分は以下の値を取ります。
1766
+
1767
+ | 区分 | 説明 |
1768
+ |-----|------|
1769
+ | 通常入荷 | 通常の入荷処理 |
1770
+ | 分割入荷 | 発注数量を分割して入荷する場合 |
1771
+ | 返品入荷 | 検収後の返品を受け入れる場合 |
1772
+
1773
+ ### Java エンティティの定義
1774
+
1775
+ <details>
1776
+ <summary>入荷受入データエンティティ</summary>
1777
+
1778
+ ```java
1779
+ // src/main/java/com/example/pms/domain/model/purchase/Receiving.java
1780
+ package com.example.pms.domain.model.purchase;
1781
+
1782
+ import com.example.pms.domain.model.item.Item;
1783
+ import lombok.AllArgsConstructor;
1784
+ import lombok.Builder;
1785
+ import lombok.Data;
1786
+ import lombok.NoArgsConstructor;
1787
+
1788
+ import java.math.BigDecimal;
1789
+ import java.time.LocalDate;
1790
+ import java.time.LocalDateTime;
1791
+ import java.util.List;
1792
+
1793
+ @Data
1794
+ @Builder
1795
+ @NoArgsConstructor
1796
+ @AllArgsConstructor
1797
+ public class Receiving {
1798
+ private Integer id;
1799
+ private String receivingNumber;
1800
+ private String purchaseOrderNumber;
1801
+ private Integer lineNumber;
1802
+ private LocalDate receivingDate;
1803
+ private String receiverCode;
1804
+ private ReceivingType receivingType;
1805
+ private String itemCode;
1806
+ private Boolean miscellaneousItemFlag;
1807
+ private BigDecimal receivingQuantity;
1808
+ private String remarks;
1809
+ private LocalDateTime createdAt;
1810
+ private String createdBy;
1811
+ private LocalDateTime updatedAt;
1812
+ private String updatedBy;
1813
+
1814
+ // リレーション
1815
+ private PurchaseOrderDetail purchaseOrderDetail;
1816
+ private Item item;
1817
+ private List<Inspection> inspections;
1818
+ }
1819
+ ```
1820
+
1821
+ </details>
1822
+
1823
+ <details>
1824
+ <summary>受入検査データエンティティ</summary>
1825
+
1826
+ ```java
1827
+ // src/main/java/com/example/pms/domain/model/purchase/Inspection.java
1828
+ package com.example.pms.domain.model.purchase;
1829
+
1830
+ import com.example.pms.domain.model.item.Item;
1831
+ import lombok.AllArgsConstructor;
1832
+ import lombok.Builder;
1833
+ import lombok.Data;
1834
+ import lombok.NoArgsConstructor;
1835
+
1836
+ import java.math.BigDecimal;
1837
+ import java.time.LocalDate;
1838
+ import java.time.LocalDateTime;
1839
+ import java.util.List;
1840
+
1841
+ @Data
1842
+ @Builder
1843
+ @NoArgsConstructor
1844
+ @AllArgsConstructor
1845
+ public class Inspection {
1846
+ private Integer id;
1847
+ private String inspectionNumber;
1848
+ private String receivingNumber;
1849
+ private String purchaseOrderNumber;
1850
+ private Integer lineNumber;
1851
+ private LocalDate inspectionDate;
1852
+ private String inspectorCode;
1853
+ private String itemCode;
1854
+ private Boolean miscellaneousItemFlag;
1855
+ private BigDecimal goodQuantity;
1856
+ private BigDecimal defectQuantity;
1857
+ private String remarks;
1858
+ private LocalDateTime createdAt;
1859
+ private String createdBy;
1860
+ private LocalDateTime updatedAt;
1861
+ private String updatedBy;
1862
+
1863
+ // リレーション
1864
+ private Receiving receiving;
1865
+ private Item item;
1866
+ private List<Acceptance> acceptances;
1867
+ }
1868
+ ```
1869
+
1870
+ </details>
1871
+
1872
+ <details>
1873
+ <summary>検収データエンティティ</summary>
1874
+
1875
+ ```java
1876
+ // src/main/java/com/example/pms/domain/model/purchase/Acceptance.java
1877
+ package com.example.pms.domain.model.purchase;
1878
+
1879
+ import com.example.pms.domain.model.item.Item;
1880
+ import com.example.pms.domain.model.supplier.Supplier;
1881
+ import lombok.AllArgsConstructor;
1882
+ import lombok.Builder;
1883
+ import lombok.Data;
1884
+ import lombok.NoArgsConstructor;
1885
+
1886
+ import java.math.BigDecimal;
1887
+ import java.time.LocalDate;
1888
+ import java.time.LocalDateTime;
1889
+
1890
+ @Data
1891
+ @Builder
1892
+ @NoArgsConstructor
1893
+ @AllArgsConstructor
1894
+ public class Acceptance {
1895
+ private Integer id;
1896
+ private String acceptanceNumber;
1897
+ private String inspectionNumber;
1898
+ private String purchaseOrderNumber;
1899
+ private Integer lineNumber;
1900
+ private LocalDate acceptanceDate;
1901
+ private String acceptorCode;
1902
+ private String supplierCode;
1903
+ private String itemCode;
1904
+ private Boolean miscellaneousItemFlag;
1905
+ private BigDecimal acceptedQuantity;
1906
+ private BigDecimal unitPrice;
1907
+ private BigDecimal amount;
1908
+ private BigDecimal taxAmount;
1909
+ private String remarks;
1910
+ private LocalDateTime createdAt;
1911
+ private String createdBy;
1912
+ private LocalDateTime updatedAt;
1913
+ private String updatedBy;
1914
+
1915
+ // リレーション
1916
+ private Inspection inspection;
1917
+ private PurchaseOrderDetail purchaseOrderDetail;
1918
+ private Supplier supplier;
1919
+ private Item item;
1920
+ }
1921
+ ```
1922
+
1923
+ </details>
1924
+
1925
+ ### MyBatis Mapper XML(入荷・検収)
1926
+
1927
+ <details>
1928
+ <summary>ReceivingMapper.xml</summary>
1929
+
1930
+ ```xml
1931
+ <!-- src/main/resources/mapper/ReceivingMapper.xml -->
1932
+ <?xml version="1.0" encoding="UTF-8" ?>
1933
+ <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
1934
+ "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
1935
+ <mapper namespace="com.example.pms.infrastructure.out.persistence.mapper.ReceivingMapper">
1936
+
1937
+ <resultMap id="ReceivingResultMap" type="com.example.pms.domain.model.purchase.Receiving">
1938
+ <id property="id" column="ID"/>
1939
+ <result property="receivingNumber" column="入荷番号"/>
1940
+ <result property="purchaseOrderNumber" column="発注番号"/>
1941
+ <result property="lineNumber" column="発注行番号"/>
1942
+ <result property="receivingDate" column="入荷日"/>
1943
+ <result property="receiverCode" column="入荷担当者コード"/>
1944
+ <result property="receivingType" column="入荷受入区分"
1945
+ typeHandler="com.example.pms.infrastructure.out.persistence.typehandler.ReceivingTypeTypeHandler"/>
1946
+ <result property="itemCode" column="品目コード"/>
1947
+ <result property="miscellaneousItemFlag" column="諸口品目区分"/>
1948
+ <result property="receivingQuantity" column="入荷数量"/>
1949
+ <result property="remarks" column="入荷備考"/>
1950
+ <result property="createdAt" column="作成日時"/>
1951
+ <result property="createdBy" column="作成者"/>
1952
+ <result property="updatedAt" column="更新日時"/>
1953
+ <result property="updatedBy" column="更新者"/>
1954
+ </resultMap>
1955
+
1956
+ <!-- PostgreSQL用 INSERT -->
1957
+ <insert id="insert" parameterType="com.example.pms.domain.model.purchase.Receiving"
1958
+ useGeneratedKeys="true" keyProperty="id" keyColumn="ID" databaseId="postgresql">
1959
+ INSERT INTO "入荷受入データ" (
1960
+ "入荷番号", "発注番号", "発注行番号", "入荷日", "入荷担当者コード",
1961
+ "入荷受入区分", "品目コード", "諸口品目区分", "入荷数量", "入荷備考", "作成者", "更新者"
1962
+ ) VALUES (
1963
+ #{receivingNumber},
1964
+ #{purchaseOrderNumber},
1965
+ #{lineNumber},
1966
+ #{receivingDate},
1967
+ #{receiverCode},
1968
+ #{receivingType, typeHandler=com.example.pms.infrastructure.out.persistence.typehandler.ReceivingTypeTypeHandler}::入荷受入区分,
1969
+ #{itemCode},
1970
+ #{miscellaneousItemFlag},
1971
+ #{receivingQuantity},
1972
+ #{remarks},
1973
+ #{createdBy},
1974
+ #{updatedBy}
1975
+ )
1976
+ </insert>
1977
+
1978
+ <!-- H2用 INSERT -->
1979
+ <insert id="insert" parameterType="com.example.pms.domain.model.purchase.Receiving"
1980
+ useGeneratedKeys="true" keyProperty="id" keyColumn="ID" databaseId="h2">
1981
+ INSERT INTO "入荷受入データ" (
1982
+ "入荷番号", "発注番号", "発注行番号", "入荷日", "入荷担当者コード",
1983
+ "入荷受入区分", "品目コード", "諸口品目区分", "入荷数量", "入荷備考", "作成者", "更新者"
1984
+ ) VALUES (
1985
+ #{receivingNumber},
1986
+ #{purchaseOrderNumber},
1987
+ #{lineNumber},
1988
+ #{receivingDate},
1989
+ #{receiverCode},
1990
+ #{receivingType, typeHandler=com.example.pms.infrastructure.out.persistence.typehandler.ReceivingTypeTypeHandler},
1991
+ #{itemCode},
1992
+ #{miscellaneousItemFlag},
1993
+ #{receivingQuantity},
1994
+ #{remarks},
1995
+ #{createdBy},
1996
+ #{updatedBy}
1997
+ )
1998
+ </insert>
1999
+
2000
+ <select id="findById" resultMap="ReceivingResultMap">
2001
+ SELECT * FROM "入荷受入データ" WHERE "ID" = #{id}
2002
+ </select>
2003
+
2004
+ <select id="findByReceivingNumber" resultMap="ReceivingResultMap">
2005
+ SELECT * FROM "入荷受入データ" WHERE "入荷番号" = #{receivingNumber}
2006
+ </select>
2007
+
2008
+ <select id="findByPurchaseOrderNumber" resultMap="ReceivingResultMap">
2009
+ SELECT * FROM "入荷受入データ" WHERE "発注番号" = #{purchaseOrderNumber} ORDER BY "入荷日" DESC
2010
+ </select>
2011
+
2012
+ <select id="findByPurchaseOrderNumberAndLineNumber" resultMap="ReceivingResultMap">
2013
+ SELECT * FROM "入荷受入データ"
2014
+ WHERE "発注番号" = #{purchaseOrderNumber} AND "発注行番号" = #{lineNumber}
2015
+ ORDER BY "入荷日" DESC
2016
+ </select>
2017
+
2018
+ <select id="findAll" resultMap="ReceivingResultMap">
2019
+ SELECT * FROM "入荷受入データ" ORDER BY "入荷日" DESC
2020
+ </select>
2021
+
2022
+ <!-- PostgreSQL用 DELETE -->
2023
+ <delete id="deleteAll" databaseId="postgresql">
2024
+ TRUNCATE TABLE "入荷受入データ" CASCADE
2025
+ </delete>
2026
+
2027
+ <!-- H2用 DELETE -->
2028
+ <delete id="deleteAll" databaseId="h2">
2029
+ DELETE FROM "入荷受入データ"
2030
+ </delete>
2031
+ </mapper>
2032
+ ```
2033
+
2034
+ </details>
2035
+
2036
+ <details>
2037
+ <summary>InspectionMapper.xml</summary>
2038
+
2039
+ ```xml
2040
+ <!-- src/main/resources/mapper/InspectionMapper.xml -->
2041
+ <?xml version="1.0" encoding="UTF-8" ?>
2042
+ <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
2043
+ "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
2044
+ <mapper namespace="com.example.pms.infrastructure.out.persistence.mapper.InspectionMapper">
2045
+
2046
+ <resultMap id="InspectionResultMap" type="com.example.pms.domain.model.purchase.Inspection">
2047
+ <id property="id" column="ID"/>
2048
+ <result property="inspectionNumber" column="受入検査番号"/>
2049
+ <result property="receivingNumber" column="入荷番号"/>
2050
+ <result property="purchaseOrderNumber" column="発注番号"/>
2051
+ <result property="lineNumber" column="発注行番号"/>
2052
+ <result property="inspectionDate" column="受入検査日"/>
2053
+ <result property="inspectorCode" column="受入検査担当者コード"/>
2054
+ <result property="itemCode" column="品目コード"/>
2055
+ <result property="miscellaneousItemFlag" column="諸口品目区分"/>
2056
+ <result property="goodQuantity" column="良品数"/>
2057
+ <result property="defectQuantity" column="不良品数"/>
2058
+ <result property="remarks" column="受入検査備考"/>
2059
+ <result property="createdAt" column="作成日時"/>
2060
+ <result property="createdBy" column="作成者"/>
2061
+ <result property="updatedAt" column="更新日時"/>
2062
+ <result property="updatedBy" column="更新者"/>
2063
+ </resultMap>
2064
+
2065
+ <insert id="insert" parameterType="com.example.pms.domain.model.purchase.Inspection"
2066
+ useGeneratedKeys="true" keyProperty="id" keyColumn="ID">
2067
+ INSERT INTO "受入検査データ" (
2068
+ "受入検査番号", "入荷番号", "発注番号", "発注行番号", "受入検査日",
2069
+ "受入検査担当者コード", "品目コード", "諸口品目区分", "良品数", "不良品数",
2070
+ "受入検査備考", "作成者", "更新者"
2071
+ ) VALUES (
2072
+ #{inspectionNumber},
2073
+ #{receivingNumber},
2074
+ #{purchaseOrderNumber},
2075
+ #{lineNumber},
2076
+ #{inspectionDate},
2077
+ #{inspectorCode},
2078
+ #{itemCode},
2079
+ #{miscellaneousItemFlag},
2080
+ #{goodQuantity},
2081
+ #{defectQuantity},
2082
+ #{remarks},
2083
+ #{createdBy},
2084
+ #{updatedBy}
2085
+ )
2086
+ </insert>
2087
+
2088
+ <select id="findById" resultMap="InspectionResultMap">
2089
+ SELECT * FROM "受入検査データ" WHERE "ID" = #{id}
2090
+ </select>
2091
+
2092
+ <select id="findByInspectionNumber" resultMap="InspectionResultMap">
2093
+ SELECT * FROM "受入検査データ" WHERE "受入検査番号" = #{inspectionNumber}
2094
+ </select>
2095
+
2096
+ <select id="findByReceivingNumber" resultMap="InspectionResultMap">
2097
+ SELECT * FROM "受入検査データ" WHERE "入荷番号" = #{receivingNumber} ORDER BY "受入検査日" DESC
2098
+ </select>
2099
+
2100
+ <select id="findAll" resultMap="InspectionResultMap">
2101
+ SELECT * FROM "受入検査データ" ORDER BY "受入検査日" DESC
2102
+ </select>
2103
+
2104
+ <!-- PostgreSQL用 DELETE -->
2105
+ <delete id="deleteAll" databaseId="postgresql">
2106
+ TRUNCATE TABLE "受入検査データ" CASCADE
2107
+ </delete>
2108
+
2109
+ <!-- H2用 DELETE -->
2110
+ <delete id="deleteAll" databaseId="h2">
2111
+ DELETE FROM "受入検査データ"
2112
+ </delete>
2113
+ </mapper>
2114
+ ```
2115
+
2116
+ </details>
2117
+
2118
+ <details>
2119
+ <summary>AcceptanceMapper.xml</summary>
2120
+
2121
+ ```xml
2122
+ <!-- src/main/resources/mapper/AcceptanceMapper.xml -->
2123
+ <?xml version="1.0" encoding="UTF-8" ?>
2124
+ <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
2125
+ "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
2126
+ <mapper namespace="com.example.pms.infrastructure.out.persistence.mapper.AcceptanceMapper">
2127
+
2128
+ <resultMap id="AcceptanceResultMap" type="com.example.pms.domain.model.purchase.Acceptance">
2129
+ <id property="id" column="ID"/>
2130
+ <result property="acceptanceNumber" column="検収番号"/>
2131
+ <result property="inspectionNumber" column="受入検査番号"/>
2132
+ <result property="purchaseOrderNumber" column="発注番号"/>
2133
+ <result property="lineNumber" column="発注行番号"/>
2134
+ <result property="acceptanceDate" column="検収日"/>
2135
+ <result property="acceptorCode" column="検収担当者コード"/>
2136
+ <result property="supplierCode" column="取引先コード"/>
2137
+ <result property="itemCode" column="品目コード"/>
2138
+ <result property="miscellaneousItemFlag" column="諸口品目区分"/>
2139
+ <result property="acceptedQuantity" column="検収数"/>
2140
+ <result property="unitPrice" column="検収単価"/>
2141
+ <result property="amount" column="検収金額"/>
2142
+ <result property="taxAmount" column="検収消費税額"/>
2143
+ <result property="remarks" column="検収備考"/>
2144
+ <result property="createdAt" column="作成日時"/>
2145
+ <result property="createdBy" column="作成者"/>
2146
+ <result property="updatedAt" column="更新日時"/>
2147
+ <result property="updatedBy" column="更新者"/>
2148
+ </resultMap>
2149
+
2150
+ <insert id="insert" parameterType="com.example.pms.domain.model.purchase.Acceptance"
2151
+ useGeneratedKeys="true" keyProperty="id" keyColumn="ID">
2152
+ INSERT INTO "検収データ" (
2153
+ "検収番号", "受入検査番号", "発注番号", "発注行番号", "検収日",
2154
+ "検収担当者コード", "取引先コード", "品目コード", "諸口品目区分",
2155
+ "検収数", "検収単価", "検収金額", "検収消費税額", "検収備考", "作成者", "更新者"
2156
+ ) VALUES (
2157
+ #{acceptanceNumber},
2158
+ #{inspectionNumber},
2159
+ #{purchaseOrderNumber},
2160
+ #{lineNumber},
2161
+ #{acceptanceDate},
2162
+ #{acceptorCode},
2163
+ #{supplierCode},
2164
+ #{itemCode},
2165
+ #{miscellaneousItemFlag},
2166
+ #{acceptedQuantity},
2167
+ #{unitPrice},
2168
+ #{amount},
2169
+ #{taxAmount},
2170
+ #{remarks},
2171
+ #{createdBy},
2172
+ #{updatedBy}
2173
+ )
2174
+ </insert>
2175
+
2176
+ <select id="findById" resultMap="AcceptanceResultMap">
2177
+ SELECT * FROM "検収データ" WHERE "ID" = #{id}
2178
+ </select>
2179
+
2180
+ <select id="findByAcceptanceNumber" resultMap="AcceptanceResultMap">
2181
+ SELECT * FROM "検収データ" WHERE "検収番号" = #{acceptanceNumber}
2182
+ </select>
2183
+
2184
+ <select id="findByInspectionNumber" resultMap="AcceptanceResultMap">
2185
+ SELECT * FROM "検収データ" WHERE "受入検査番号" = #{inspectionNumber} ORDER BY "検収日" DESC
2186
+ </select>
2187
+
2188
+ <select id="findByPurchaseOrderNumber" resultMap="AcceptanceResultMap">
2189
+ SELECT * FROM "検収データ" WHERE "発注番号" = #{purchaseOrderNumber} ORDER BY "検収日" DESC
2190
+ </select>
2191
+
2192
+ <select id="findAll" resultMap="AcceptanceResultMap">
2193
+ SELECT * FROM "検収データ" ORDER BY "検収日" DESC
2194
+ </select>
2195
+
2196
+ <!-- PostgreSQL用 DELETE -->
2197
+ <delete id="deleteAll" databaseId="postgresql">
2198
+ TRUNCATE TABLE "検収データ" CASCADE
2199
+ </delete>
2200
+
2201
+ <!-- H2用 DELETE -->
2202
+ <delete id="deleteAll" databaseId="h2">
2203
+ DELETE FROM "検収データ"
2204
+ </delete>
2205
+ </mapper>
2206
+ ```
2207
+
2208
+ </details>
2209
+
2210
+ ### Mapper インターフェース(入荷・検収)
2211
+
2212
+ <details>
2213
+ <summary>ReceivingMapper</summary>
2214
+
2215
+ ```java
2216
+ // src/main/java/com/example/pms/infrastructure/out/persistence/mapper/ReceivingMapper.java
2217
+ package com.example.pms.infrastructure.out.persistence.mapper;
2218
+
2219
+ import com.example.pms.domain.model.purchase.Receiving;
2220
+ import org.apache.ibatis.annotations.Mapper;
2221
+ import org.apache.ibatis.annotations.Param;
2222
+
2223
+ import java.util.List;
2224
+
2225
+ @Mapper
2226
+ public interface ReceivingMapper {
2227
+ void insert(Receiving receiving);
2228
+ Receiving findByReceivingNumber(String receivingNumber);
2229
+ String findLatestReceivingNumber(String prefix);
2230
+ List<Receiving> findByPurchaseOrderNumber(String purchaseOrderNumber);
2231
+ void deleteAll();
2232
+ }
2233
+ ```
2234
+
2235
+ </details>
2236
+
2237
+ <details>
2238
+ <summary>InspectionMapper</summary>
2239
+
2240
+ ```java
2241
+ // src/main/java/com/example/pms/infrastructure/out/persistence/mapper/InspectionMapper.java
2242
+ package com.example.pms.infrastructure.out.persistence.mapper;
2243
+
2244
+ import com.example.pms.domain.model.purchase.Inspection;
2245
+ import org.apache.ibatis.annotations.Mapper;
2246
+
2247
+ @Mapper
2248
+ public interface InspectionMapper {
2249
+ void insert(Inspection inspection);
2250
+ Inspection findByInspectionNumber(String inspectionNumber);
2251
+ String findLatestInspectionNumber(String prefix);
2252
+ void deleteAll();
2253
+ }
2254
+ ```
2255
+
2256
+ </details>
2257
+
2258
+ <details>
2259
+ <summary>AcceptanceMapper</summary>
2260
+
2261
+ ```java
2262
+ // src/main/java/com/example/pms/infrastructure/out/persistence/mapper/AcceptanceMapper.java
2263
+ package com.example.pms.infrastructure.out.persistence.mapper;
2264
+
2265
+ import com.example.pms.domain.model.purchase.Acceptance;
2266
+ import org.apache.ibatis.annotations.Mapper;
2267
+
2268
+ import java.util.List;
2269
+
2270
+ @Mapper
2271
+ public interface AcceptanceMapper {
2272
+ void insert(Acceptance acceptance);
2273
+ Acceptance findByAcceptanceNumber(String acceptanceNumber);
2274
+ String findLatestAcceptanceNumber(String prefix);
2275
+ List<Acceptance> findByPurchaseOrderNumber(String purchaseOrderNumber);
2276
+ void deleteAll();
2277
+ }
2278
+ ```
2279
+
2280
+ </details>
2281
+
2282
+ ### コマンドクラス
2283
+
2284
+ ヘキサゴナルアーキテクチャに従い、入力用 DTO は `application/port/in/command` に配置し、命名は `xxxCommand` とします。
2285
+
2286
+ <details>
2287
+ <summary>ReceivingCommand</summary>
2288
+
2289
+ ```java
2290
+ // src/main/java/com/example/pms/application/port/in/command/ReceivingCommand.java
2291
+ package com.example.pms.application.port.in.command;
2292
+
2293
+ import com.example.pms.domain.model.purchase.ReceivingType;
2294
+ import lombok.Builder;
2295
+ import lombok.Data;
2296
+
2297
+ import java.math.BigDecimal;
2298
+ import java.time.LocalDate;
2299
+
2300
+ @Data
2301
+ @Builder
2302
+ public class ReceivingCommand {
2303
+ private String purchaseOrderNumber;
2304
+ private Integer lineNumber;
2305
+ private LocalDate receivingDate;
2306
+ private String receiverCode;
2307
+ private ReceivingType receivingType;
2308
+ private BigDecimal receivingQuantity;
2309
+ private String remarks;
2310
+ }
2311
+ ```
2312
+
2313
+ </details>
2314
+
2315
+ <details>
2316
+ <summary>InspectionCommand</summary>
2317
+
2318
+ ```java
2319
+ // src/main/java/com/example/pms/application/port/in/command/InspectionCommand.java
2320
+ package com.example.pms.application.port.in.command;
2321
+
2322
+ import lombok.Builder;
2323
+ import lombok.Data;
2324
+
2325
+ import java.math.BigDecimal;
2326
+ import java.time.LocalDate;
2327
+
2328
+ @Data
2329
+ @Builder
2330
+ public class InspectionCommand {
2331
+ private String receivingNumber;
2332
+ private LocalDate inspectionDate;
2333
+ private String inspectorCode;
2334
+ private BigDecimal goodQuantity;
2335
+ private BigDecimal defectQuantity;
2336
+ private String remarks;
2337
+ }
2338
+ ```
2339
+
2340
+ </details>
2341
+
2342
+ <details>
2343
+ <summary>AcceptanceCommand</summary>
2344
+
2345
+ ```java
2346
+ // src/main/java/com/example/pms/application/port/in/command/AcceptanceCommand.java
2347
+ package com.example.pms.application.port.in.command;
2348
+
2349
+ import lombok.Builder;
2350
+ import lombok.Data;
2351
+
2352
+ import java.math.BigDecimal;
2353
+ import java.time.LocalDate;
2354
+
2355
+ @Data
2356
+ @Builder
2357
+ public class AcceptanceCommand {
2358
+ private String inspectionNumber;
2359
+ private LocalDate acceptanceDate;
2360
+ private String acceptorCode;
2361
+ private BigDecimal taxRate;
2362
+ private String remarks;
2363
+ }
2364
+ ```
2365
+
2366
+ </details>
2367
+
2368
+ ### 入荷検収サービスの実装
2369
+
2370
+ <details>
2371
+ <summary>ReceivingService</summary>
2372
+
2373
+ ```java
2374
+ // src/main/java/com/example/pms/application/service/ReceivingService.java
2375
+ package com.example.pms.application.service;
2376
+
2377
+ import com.example.pms.domain.model.purchase.*;
2378
+ import com.example.pms.infrastructure.out.persistence.mapper.*;
2379
+ import lombok.RequiredArgsConstructor;
2380
+ import org.springframework.stereotype.Service;
2381
+ import org.springframework.transaction.annotation.Transactional;
2382
+
2383
+ import java.math.BigDecimal;
2384
+ import java.math.RoundingMode;
2385
+ import java.time.LocalDate;
2386
+ import java.time.format.DateTimeFormatter;
2387
+
2388
+ @Service
2389
+ @RequiredArgsConstructor
2390
+ public class ReceivingService {
2391
+
2392
+ private final ReceivingMapper receivingMapper;
2393
+ private final InspectionMapper inspectionMapper;
2394
+ private final AcceptanceMapper acceptanceMapper;
2395
+ private final PurchaseOrderMapper purchaseOrderMapper;
2396
+ private final PurchaseOrderDetailMapper purchaseOrderDetailMapper;
2397
+
2398
+ /**
2399
+ * 入荷を登録する
2400
+ */
2401
+ @Transactional
2402
+ public Receiving registerReceiving(ReceivingCommand command) {
2403
+ PurchaseOrderDetail detail = purchaseOrderDetailMapper.findByPurchaseOrderNumberAndLineNumber(
2404
+ command.getPurchaseOrderNumber(), command.getLineNumber());
2405
+
2406
+ if (detail == null) {
2407
+ throw new IllegalArgumentException(
2408
+ "Purchase order detail not found: " + command.getPurchaseOrderNumber() + "-" + command.getLineNumber());
2409
+ }
2410
+
2411
+ BigDecimal receivedQuantity = detail.getReceivedQuantity() != null ?
2412
+ detail.getReceivedQuantity() : BigDecimal.ZERO;
2413
+ BigDecimal remainingQuantity = detail.getOrderQuantity().subtract(receivedQuantity);
2414
+
2415
+ if (command.getReceivingQuantity().compareTo(remainingQuantity) > 0) {
2416
+ throw new IllegalStateException("Cannot receive more than ordered quantity");
2417
+ }
2418
+
2419
+ String receivingNumber = generateReceivingNumber(command.getReceivingDate());
2420
+
2421
+ Receiving receiving = Receiving.builder()
2422
+ .receivingNumber(receivingNumber)
2423
+ .purchaseOrderNumber(command.getPurchaseOrderNumber())
2424
+ .lineNumber(command.getLineNumber())
2425
+ .receivingDate(command.getReceivingDate())
2426
+ .receiverCode(command.getReceiverCode())
2427
+ .receivingType(command.getReceivingType() != null ? command.getReceivingType() : ReceivingType.NORMAL)
2428
+ .itemCode(detail.getItemCode())
2429
+ .miscellaneousItemFlag(detail.getMiscellaneousItemFlag())
2430
+ .receivingQuantity(command.getReceivingQuantity())
2431
+ .remarks(command.getRemarks())
2432
+ .build();
2433
+ receivingMapper.insert(receiving);
2434
+
2435
+ BigDecimal newReceivedQuantity = receivedQuantity.add(command.getReceivingQuantity());
2436
+ purchaseOrderDetailMapper.updateReceivedQuantity(
2437
+ command.getPurchaseOrderNumber(), command.getLineNumber(), newReceivedQuantity);
2438
+
2439
+ PurchaseOrderStatus newStatus = newReceivedQuantity.compareTo(detail.getOrderQuantity()) >= 0
2440
+ ? PurchaseOrderStatus.RECEIVED : PurchaseOrderStatus.PARTIALLY_RECEIVED;
2441
+ purchaseOrderMapper.updateStatus(command.getPurchaseOrderNumber(), newStatus);
2442
+
2443
+ return receiving;
2444
+ }
2445
+
2446
+ private String generateReceivingNumber(LocalDate date) {
2447
+ String prefix = "RCV-" + date.format(DateTimeFormatter.ofPattern("yyyyMM")) + "-";
2448
+ String latestNumber = receivingMapper.findLatestReceivingNumber(prefix + "%");
2449
+
2450
+ int sequence = 1;
2451
+ if (latestNumber != null) {
2452
+ int currentSequence = Integer.parseInt(latestNumber.substring(latestNumber.length() - 4));
2453
+ sequence = currentSequence + 1;
2454
+ }
2455
+
2456
+ return prefix + String.format("%04d", sequence);
2457
+ }
2458
+
2459
+ /**
2460
+ * 受入検査を登録する
2461
+ */
2462
+ @Transactional
2463
+ public Inspection registerInspection(InspectionCommand command) {
2464
+ Receiving receiving = receivingMapper.findByReceivingNumber(command.getReceivingNumber());
2465
+
2466
+ if (receiving == null) {
2467
+ throw new IllegalArgumentException("Receiving not found: " + command.getReceivingNumber());
2468
+ }
2469
+
2470
+ BigDecimal totalQuantity = command.getGoodQuantity().add(command.getDefectQuantity());
2471
+ if (totalQuantity.compareTo(receiving.getReceivingQuantity()) > 0) {
2472
+ throw new IllegalStateException("Inspection quantity exceeds receiving quantity");
2473
+ }
2474
+
2475
+ String inspectionNumber = generateInspectionNumber(command.getInspectionDate());
2476
+
2477
+ Inspection inspection = Inspection.builder()
2478
+ .inspectionNumber(inspectionNumber)
2479
+ .receivingNumber(command.getReceivingNumber())
2480
+ .purchaseOrderNumber(receiving.getPurchaseOrderNumber())
2481
+ .lineNumber(receiving.getLineNumber())
2482
+ .inspectionDate(command.getInspectionDate())
2483
+ .inspectorCode(command.getInspectorCode())
2484
+ .itemCode(receiving.getItemCode())
2485
+ .miscellaneousItemFlag(receiving.getMiscellaneousItemFlag())
2486
+ .goodQuantity(command.getGoodQuantity())
2487
+ .defectQuantity(command.getDefectQuantity())
2488
+ .remarks(command.getRemarks())
2489
+ .build();
2490
+ inspectionMapper.insert(inspection);
2491
+
2492
+ return inspection;
2493
+ }
2494
+
2495
+ private String generateInspectionNumber(LocalDate date) {
2496
+ String prefix = "INS-" + date.format(DateTimeFormatter.ofPattern("yyyyMM")) + "-";
2497
+ String latestNumber = inspectionMapper.findLatestInspectionNumber(prefix + "%");
2498
+
2499
+ int sequence = 1;
2500
+ if (latestNumber != null) {
2501
+ int currentSequence = Integer.parseInt(latestNumber.substring(latestNumber.length() - 4));
2502
+ sequence = currentSequence + 1;
2503
+ }
2504
+
2505
+ return prefix + String.format("%04d", sequence);
2506
+ }
2507
+
2508
+ /**
2509
+ * 検収処理を行う
2510
+ */
2511
+ @Transactional
2512
+ public Acceptance processAcceptance(AcceptanceCommand command) {
2513
+ Inspection inspection = inspectionMapper.findByInspectionNumber(command.getInspectionNumber());
2514
+
2515
+ if (inspection == null) {
2516
+ throw new IllegalArgumentException("Inspection not found: " + command.getInspectionNumber());
2517
+ }
2518
+
2519
+ PurchaseOrderDetail detail = purchaseOrderDetailMapper.findByPurchaseOrderNumberAndLineNumber(
2520
+ inspection.getPurchaseOrderNumber(), inspection.getLineNumber());
2521
+
2522
+ PurchaseOrder purchaseOrder = purchaseOrderMapper.findByPurchaseOrderNumber(
2523
+ inspection.getPurchaseOrderNumber());
2524
+
2525
+ BigDecimal taxRate = command.getTaxRate() != null ? command.getTaxRate() : new BigDecimal("10");
2526
+
2527
+ BigDecimal amount = detail.getOrderUnitPrice().multiply(inspection.getGoodQuantity());
2528
+ BigDecimal taxAmount = amount.multiply(taxRate)
2529
+ .divide(new BigDecimal("100"), 0, RoundingMode.HALF_UP);
2530
+
2531
+ String acceptanceNumber = generateAcceptanceNumber(command.getAcceptanceDate());
2532
+
2533
+ Acceptance acceptance = Acceptance.builder()
2534
+ .acceptanceNumber(acceptanceNumber)
2535
+ .inspectionNumber(command.getInspectionNumber())
2536
+ .purchaseOrderNumber(inspection.getPurchaseOrderNumber())
2537
+ .lineNumber(inspection.getLineNumber())
2538
+ .acceptanceDate(command.getAcceptanceDate())
2539
+ .acceptorCode(command.getAcceptorCode())
2540
+ .supplierCode(purchaseOrder.getSupplierCode())
2541
+ .itemCode(inspection.getItemCode())
2542
+ .miscellaneousItemFlag(inspection.getMiscellaneousItemFlag())
2543
+ .acceptedQuantity(inspection.getGoodQuantity())
2544
+ .unitPrice(detail.getOrderUnitPrice())
2545
+ .amount(amount)
2546
+ .taxAmount(taxAmount)
2547
+ .remarks(command.getRemarks())
2548
+ .build();
2549
+ acceptanceMapper.insert(acceptance);
2550
+
2551
+ return acceptance;
2552
+ }
2553
+
2554
+ private String generateAcceptanceNumber(LocalDate date) {
2555
+ String prefix = "ACC-" + date.format(DateTimeFormatter.ofPattern("yyyyMM")) + "-";
2556
+ String latestNumber = acceptanceMapper.findLatestAcceptanceNumber(prefix + "%");
2557
+
2558
+ int sequence = 1;
2559
+ if (latestNumber != null) {
2560
+ int currentSequence = Integer.parseInt(latestNumber.substring(latestNumber.length() - 4));
2561
+ sequence = currentSequence + 1;
2562
+ }
2563
+
2564
+ return prefix + String.format("%04d", sequence);
2565
+ }
2566
+ }
2567
+ ```
2568
+
2569
+ </details>
2570
+
2571
+ ### 在庫計上処理
2572
+
2573
+ 検収処理が完了すると、良品数が在庫に計上されます。在庫計上処理の詳細は第28章で解説します。
2574
+
2575
+ ### TDD: 入荷・検収のテスト
2576
+
2577
+ <details>
2578
+ <summary>ReceivingServiceTest</summary>
2579
+
2580
+ ```java
2581
+ // src/test/java/com/example/pms/application/service/ReceivingServiceTest.java
2582
+ package com.example.pms.application.service;
2583
+
2584
+ import com.example.pms.domain.model.item.Item;
2585
+ import com.example.pms.domain.model.item.ItemCategory;
2586
+ import com.example.pms.domain.model.master.Supplier;
2587
+ import com.example.pms.domain.model.purchase.*;
2588
+ import com.example.pms.infrastructure.out.persistence.mapper.*;
2589
+ import org.junit.jupiter.api.*;
2590
+ import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
2591
+ import org.springframework.beans.factory.annotation.Autowired;
2592
+ import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
2593
+ import org.springframework.context.annotation.Import;
2594
+ import org.springframework.test.context.DynamicPropertyRegistry;
2595
+ import org.springframework.test.context.DynamicPropertySource;
2596
+ import org.testcontainers.containers.PostgreSQLContainer;
2597
+ import org.testcontainers.junit.jupiter.Container;
2598
+ import org.testcontainers.junit.jupiter.Testcontainers;
2599
+
2600
+ import java.math.BigDecimal;
2601
+ import java.time.LocalDate;
2602
+ import java.util.List;
2603
+
2604
+ import static org.assertj.core.api.Assertions.*;
2605
+
2606
+ @MybatisTest
2607
+ @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
2608
+ @Import({ReceivingService.class, PurchaseOrderService.class})
2609
+ @Testcontainers
2610
+ @DisplayName("入荷・検収業務")
2611
+ class ReceivingServiceTest {
2612
+
2613
+ @Container
2614
+ static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
2615
+ .withDatabaseName("testdb")
2616
+ .withUsername("testuser")
2617
+ .withPassword("testpass");
2618
+
2619
+ @DynamicPropertySource
2620
+ static void configureProperties(DynamicPropertyRegistry registry) {
2621
+ registry.add("spring.datasource.url", postgres::getJdbcUrl);
2622
+ registry.add("spring.datasource.username", postgres::getUsername);
2623
+ registry.add("spring.datasource.password", postgres::getPassword);
2624
+ }
2625
+
2626
+ @Autowired
2627
+ private ReceivingService receivingService;
2628
+
2629
+ @Autowired
2630
+ private PurchaseOrderService purchaseOrderService;
2631
+
2632
+ @Autowired
2633
+ private ItemMapper itemMapper;
2634
+
2635
+ @Autowired
2636
+ private SupplierMapper supplierMapper;
2637
+
2638
+ @Autowired
2639
+ private UnitPriceMapper unitPriceMapper;
2640
+
2641
+ @Autowired
2642
+ private AcceptanceMapper acceptanceMapper;
2643
+
2644
+ @Autowired
2645
+ private InspectionMapper inspectionMapper;
2646
+
2647
+ @Autowired
2648
+ private ReceivingMapper receivingMapper;
2649
+
2650
+ @Autowired
2651
+ private PurchaseOrderDetailMapper purchaseOrderDetailMapper;
2652
+
2653
+ @Autowired
2654
+ private PurchaseOrderMapper purchaseOrderMapper;
2655
+
2656
+ private PurchaseOrder testPurchaseOrder;
2657
+
2658
+ @BeforeEach
2659
+ void setUp() {
2660
+ // テストデータのクリーンアップ
2661
+ acceptanceMapper.deleteAll();
2662
+ inspectionMapper.deleteAll();
2663
+ receivingMapper.deleteAll();
2664
+ purchaseOrderDetailMapper.deleteAll();
2665
+ purchaseOrderMapper.deleteAll();
2666
+ unitPriceMapper.deleteAll();
2667
+ supplierMapper.deleteAll();
2668
+ itemMapper.deleteAll();
2669
+
2670
+ // マスタデータの準備
2671
+ Supplier supplier = Supplier.builder()
2672
+ .supplierCode("SUP-RCV-001")
2673
+ .effectiveFrom(LocalDate.of(2025, 1, 1))
2674
+ .supplierName("入荷テスト用仕入先")
2675
+ .build();
2676
+ supplierMapper.insert(supplier);
2677
+
2678
+ Item item = Item.builder()
2679
+ .itemCode("MAT-RCV-001")
2680
+ .effectiveFrom(LocalDate.of(2025, 1, 1))
2681
+ .itemName("入荷テスト用材料")
2682
+ .itemCategory(ItemCategory.MATERIAL)
2683
+ .build();
2684
+ itemMapper.insert(item);
2685
+
2686
+ unitPriceMapper.insert(UnitPrice.builder()
2687
+ .itemCode("MAT-RCV-001")
2688
+ .supplierCode("SUP-RCV-001")
2689
+ .effectiveFrom(LocalDate.of(2025, 1, 1))
2690
+ .unitPrice(new BigDecimal("1000"))
2691
+ .build());
2692
+
2693
+ // テスト用発注を作成
2694
+ PurchaseOrderCreateInput poInput = PurchaseOrderCreateInput.builder()
2695
+ .supplierCode("SUP-RCV-001")
2696
+ .orderDate(LocalDate.of(2025, 1, 15))
2697
+ .details(List.of(
2698
+ PurchaseOrderDetailInput.builder()
2699
+ .itemCode("MAT-RCV-001")
2700
+ .orderQuantity(new BigDecimal("100"))
2701
+ .expectedReceivingDate(LocalDate.of(2025, 1, 25))
2702
+ .build()
2703
+ ))
2704
+ .build();
2705
+ testPurchaseOrder = purchaseOrderService.createPurchaseOrder(poInput);
2706
+ purchaseOrderService.confirmPurchaseOrder(testPurchaseOrder.getPurchaseOrderNumber());
2707
+ }
2708
+
2709
+ @Nested
2710
+ @DisplayName("入荷登録")
2711
+ class ReceivingRegistration {
2712
+
2713
+ @Test
2714
+ @DisplayName("発注に対して入荷を登録できる")
2715
+ void canRegisterReceiving() {
2716
+ // Act
2717
+ ReceivingCommand command = ReceivingCommand.builder()
2718
+ .purchaseOrderNumber(testPurchaseOrder.getPurchaseOrderNumber())
2719
+ .lineNumber(1)
2720
+ .receivingDate(LocalDate.of(2025, 1, 25))
2721
+ .receivingQuantity(new BigDecimal("50"))
2722
+ .build();
2723
+
2724
+ Receiving receiving = receivingService.registerReceiving(command);
2725
+
2726
+ // Assert
2727
+ assertThat(receiving).isNotNull();
2728
+ assertThat(receiving.getReceivingNumber()).startsWith("RCV-");
2729
+ assertThat(receiving.getReceivingQuantity()).isEqualByComparingTo(new BigDecimal("50"));
2730
+ assertThat(receiving.getReceivingType()).isEqualTo(ReceivingType.NORMAL);
2731
+ }
2732
+
2733
+ @Test
2734
+ @DisplayName("分割入荷を登録できる")
2735
+ void canRegisterSplitReceiving() {
2736
+ // Arrange: 1回目の入荷
2737
+ receivingService.registerReceiving(ReceivingCommand.builder()
2738
+ .purchaseOrderNumber(testPurchaseOrder.getPurchaseOrderNumber())
2739
+ .lineNumber(1)
2740
+ .receivingDate(LocalDate.of(2025, 1, 25))
2741
+ .receivingQuantity(new BigDecimal("30"))
2742
+ .receivingType(ReceivingType.SPLIT)
2743
+ .build());
2744
+
2745
+ // Act: 2回目の入荷
2746
+ Receiving secondReceiving = receivingService.registerReceiving(ReceivingCommand.builder()
2747
+ .purchaseOrderNumber(testPurchaseOrder.getPurchaseOrderNumber())
2748
+ .lineNumber(1)
2749
+ .receivingDate(LocalDate.of(2025, 1, 28))
2750
+ .receivingQuantity(new BigDecimal("70"))
2751
+ .receivingType(ReceivingType.SPLIT)
2752
+ .build());
2753
+
2754
+ // Assert
2755
+ assertThat(secondReceiving).isNotNull();
2756
+ assertThat(secondReceiving.getReceivingType()).isEqualTo(ReceivingType.SPLIT);
2757
+ }
2758
+
2759
+ @Test
2760
+ @DisplayName("発注数量を超える入荷はエラーになる")
2761
+ void cannotReceiveMoreThanOrdered() {
2762
+ // Act & Assert
2763
+ ReceivingCommand command = ReceivingCommand.builder()
2764
+ .purchaseOrderNumber(testPurchaseOrder.getPurchaseOrderNumber())
2765
+ .lineNumber(1)
2766
+ .receivingDate(LocalDate.of(2025, 1, 25))
2767
+ .receivingQuantity(new BigDecimal("150"))
2768
+ .build();
2769
+
2770
+ assertThatThrownBy(() -> receivingService.registerReceiving(command))
2771
+ .isInstanceOf(IllegalStateException.class)
2772
+ .hasMessageContaining("Cannot receive more than ordered quantity");
2773
+ }
2774
+ }
2775
+
2776
+ @Nested
2777
+ @DisplayName("受入検査")
2778
+ class InspectionRegistration {
2779
+
2780
+ @Test
2781
+ @DisplayName("入荷に対して受入検査を登録できる")
2782
+ void canRegisterInspection() {
2783
+ // Arrange
2784
+ Receiving receiving = receivingService.registerReceiving(ReceivingCommand.builder()
2785
+ .purchaseOrderNumber(testPurchaseOrder.getPurchaseOrderNumber())
2786
+ .lineNumber(1)
2787
+ .receivingDate(LocalDate.of(2025, 1, 25))
2788
+ .receivingQuantity(new BigDecimal("100"))
2789
+ .build());
2790
+
2791
+ // Act
2792
+ InspectionCommand command = InspectionCommand.builder()
2793
+ .receivingNumber(receiving.getReceivingNumber())
2794
+ .inspectionDate(LocalDate.of(2025, 1, 26))
2795
+ .goodQuantity(new BigDecimal("95"))
2796
+ .defectQuantity(new BigDecimal("5"))
2797
+ .build();
2798
+
2799
+ Inspection inspection = receivingService.registerInspection(command);
2800
+
2801
+ // Assert
2802
+ assertThat(inspection).isNotNull();
2803
+ assertThat(inspection.getInspectionNumber()).startsWith("INS-");
2804
+ assertThat(inspection.getGoodQuantity()).isEqualByComparingTo(new BigDecimal("95"));
2805
+ assertThat(inspection.getDefectQuantity()).isEqualByComparingTo(new BigDecimal("5"));
2806
+ }
2807
+ }
2808
+
2809
+ @Nested
2810
+ @DisplayName("検収処理")
2811
+ class AcceptanceProcessing {
2812
+
2813
+ @Test
2814
+ @DisplayName("検査合格品を検収できる")
2815
+ void canProcessAcceptance() {
2816
+ // Arrange
2817
+ Receiving receiving = receivingService.registerReceiving(ReceivingCommand.builder()
2818
+ .purchaseOrderNumber(testPurchaseOrder.getPurchaseOrderNumber())
2819
+ .lineNumber(1)
2820
+ .receivingDate(LocalDate.of(2025, 1, 25))
2821
+ .receivingQuantity(new BigDecimal("100"))
2822
+ .build());
2823
+
2824
+ Inspection inspection = receivingService.registerInspection(InspectionCommand.builder()
2825
+ .receivingNumber(receiving.getReceivingNumber())
2826
+ .inspectionDate(LocalDate.of(2025, 1, 26))
2827
+ .goodQuantity(new BigDecimal("100"))
2828
+ .defectQuantity(BigDecimal.ZERO)
2829
+ .build());
2830
+
2831
+ // Act
2832
+ AcceptanceCommand command = AcceptanceCommand.builder()
2833
+ .inspectionNumber(inspection.getInspectionNumber())
2834
+ .acceptanceDate(LocalDate.of(2025, 1, 27))
2835
+ .taxRate(new BigDecimal("10"))
2836
+ .build();
2837
+
2838
+ Acceptance acceptance = receivingService.processAcceptance(command);
2839
+
2840
+ // Assert
2841
+ assertThat(acceptance).isNotNull();
2842
+ assertThat(acceptance.getAcceptanceNumber()).startsWith("ACC-");
2843
+ assertThat(acceptance.getAcceptedQuantity()).isEqualByComparingTo(new BigDecimal("100"));
2844
+ // 100個 × 1000円 = 100,000円
2845
+ assertThat(acceptance.getAmount()).isEqualByComparingTo(new BigDecimal("100000"));
2846
+ // 消費税 10% = 10,000円
2847
+ assertThat(acceptance.getTaxAmount()).isEqualByComparingTo(new BigDecimal("10000"));
2848
+ }
2849
+ }
2850
+ }
2851
+ ```
2852
+
2853
+ </details>
2854
+
2855
+ ---
2856
+
2857
+ ## 25.3 リレーションと楽観ロックの設計
2858
+
2859
+ ### MyBatis ネストした ResultMap によるリレーション設定
2860
+
2861
+ 購買管理データは、発注→発注明細、入荷→発注明細、検収→入荷 という複数のリレーションを持ちます。MyBatis でこれらの関係を効率的に取得するための設定を実装します。
2862
+
2863
+ #### ネストした ResultMap の定義
2864
+
2865
+ <details>
2866
+ <summary>PurchaseOrderMapper.xml(リレーション設定)</summary>
2867
+
2868
+ ```xml
2869
+ <?xml version="1.0" encoding="UTF-8" ?>
2870
+ <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
2871
+ "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
2872
+
2873
+ <!-- src/main/resources/mapper/PurchaseOrderMapper.xml -->
2874
+ <mapper namespace="com.example.pms.infrastructure.out.persistence.mapper.PurchaseOrderMapper">
2875
+
2876
+ <!-- 発注データ ResultMap(明細込み) -->
2877
+ <resultMap id="purchaseOrderWithDetailsResultMap" type="com.example.pms.domain.model.purchase.PurchaseOrder">
2878
+ <id property="id" column="po_ID"/>
2879
+ <result property="purchaseOrderNumber" column="po_発注番号"/>
2880
+ <result property="orderDate" column="po_発注日"/>
2881
+ <result property="supplierCode" column="po_取引先コード"/>
2882
+ <result property="purchaserCode" column="po_発注担当者コード"/>
2883
+ <result property="departmentCode" column="po_発注部門コード"/>
2884
+ <result property="status" column="po_ステータス"
2885
+ typeHandler="com.example.pms.infrastructure.out.persistence.typehandler.PurchaseOrderStatusTypeHandler"/>
2886
+ <result property="remarks" column="po_備考"/>
2887
+ <result property="version" column="po_バージョン"/>
2888
+ <result property="createdAt" column="po_作成日時"/>
2889
+ <result property="createdBy" column="po_作成者"/>
2890
+ <result property="updatedAt" column="po_更新日時"/>
2891
+ <result property="updatedBy" column="po_更新者"/>
2892
+ <!-- 取引先マスタとの N:1 関連 -->
2893
+ <association property="supplier" javaType="com.example.pms.domain.model.master.Supplier">
2894
+ <id property="supplierCode" column="s_取引先コード"/>
2895
+ <result property="supplierName" column="s_取引先名"/>
2896
+ <result property="supplierType" column="s_取引先区分"/>
2897
+ </association>
2898
+ <!-- 発注明細との 1:N 関連 -->
2899
+ <collection property="details" ofType="com.example.pms.domain.model.purchase.PurchaseOrderDetail"
2900
+ resultMap="purchaseOrderDetailNestedResultMap"/>
2901
+ </resultMap>
2902
+
2903
+ <!-- 発注明細のネスト ResultMap -->
2904
+ <resultMap id="purchaseOrderDetailNestedResultMap" type="com.example.pms.domain.model.purchase.PurchaseOrderDetail">
2905
+ <id property="id" column="pod_ID"/>
2906
+ <result property="purchaseOrderNumber" column="pod_発注番号"/>
2907
+ <result property="lineNumber" column="pod_発注行番号"/>
2908
+ <result property="orderNumber" column="pod_オーダNO"/>
2909
+ <result property="deliveryLocationCode" column="pod_納入場所コード"/>
2910
+ <result property="itemCode" column="pod_品目コード"/>
2911
+ <result property="isMiscellaneous" column="pod_諸口品目区分"/>
2912
+ <result property="expectedReceiveDate" column="pod_受入予定日"/>
2913
+ <result property="confirmedDeliveryDate" column="pod_回答納期"/>
2914
+ <result property="unitPrice" column="pod_発注単価"/>
2915
+ <result property="orderQuantity" column="pod_発注数量"/>
2916
+ <result property="receivedQuantity" column="pod_入荷済数量"/>
2917
+ <result property="inspectedQuantity" column="pod_検査済数量"/>
2918
+ <result property="acceptedQuantity" column="pod_検収済数量"/>
2919
+ <result property="amount" column="pod_発注金額"/>
2920
+ <result property="taxAmount" column="pod_消費税金額"/>
2921
+ <result property="isCompleted" column="pod_完了フラグ"/>
2922
+ <result property="lineRemarks" column="pod_明細備考"/>
2923
+ <result property="version" column="pod_バージョン"/>
2924
+ <!-- 品目マスタとの N:1 関連 -->
2925
+ <association property="item" javaType="com.example.pms.domain.model.item.Item">
2926
+ <id property="itemCode" column="i_品目コード"/>
2927
+ <result property="itemName" column="i_品名"/>
2928
+ <result property="itemCategory" column="i_品目区分"/>
2929
+ <result property="unit" column="i_単位"/>
2930
+ </association>
2931
+ </resultMap>
2932
+
2933
+ <!-- JOIN による一括取得クエリ -->
2934
+ <select id="findWithDetailsByPurchaseOrderNumber" resultMap="purchaseOrderWithDetailsResultMap">
2935
+ SELECT
2936
+ -- 発注データ
2937
+ po."ID" AS po_ID,
2938
+ po."発注番号" AS po_発注番号,
2939
+ po."発注日" AS po_発注日,
2940
+ po."取引先コード" AS po_取引先コード,
2941
+ po."発注担当者コード" AS po_発注担当者コード,
2942
+ po."発注部門コード" AS po_発注部門コード,
2943
+ po."ステータス" AS po_ステータス,
2944
+ po."備考" AS po_備考,
2945
+ po."バージョン" AS po_バージョン,
2946
+ po."作成日時" AS po_作成日時,
2947
+ po."作成者" AS po_作成者,
2948
+ po."更新日時" AS po_更新日時,
2949
+ po."更新者" AS po_更新者,
2950
+ -- 取引先マスタ
2951
+ s."取引先コード" AS s_取引先コード,
2952
+ s."取引先名" AS s_取引先名,
2953
+ s."取引先区分" AS s_取引先区分,
2954
+ -- 発注明細データ
2955
+ pod."ID" AS pod_ID,
2956
+ pod."発注番号" AS pod_発注番号,
2957
+ pod."発注行番号" AS pod_発注行番号,
2958
+ pod."オーダNO" AS pod_オーダNO,
2959
+ pod."納入場所コード" AS pod_納入場所コード,
2960
+ pod."品目コード" AS pod_品目コード,
2961
+ pod."諸口品目区分" AS pod_諸口品目区分,
2962
+ pod."受入予定日" AS pod_受入予定日,
2963
+ pod."回答納期" AS pod_回答納期,
2964
+ pod."発注単価" AS pod_発注単価,
2965
+ pod."発注数量" AS pod_発注数量,
2966
+ pod."入荷済数量" AS pod_入荷済数量,
2967
+ pod."検査済数量" AS pod_検査済数量,
2968
+ pod."検収済数量" AS pod_検収済数量,
2969
+ pod."発注金額" AS pod_発注金額,
2970
+ pod."消費税金額" AS pod_消費税金額,
2971
+ pod."完了フラグ" AS pod_完了フラグ,
2972
+ pod."明細備考" AS pod_明細備考,
2973
+ pod."バージョン" AS pod_バージョン,
2974
+ -- 品目マスタ
2975
+ i."品目コード" AS i_品目コード,
2976
+ i."品名" AS i_品名,
2977
+ i."品目区分" AS i_品目区分,
2978
+ i."単位" AS i_単位
2979
+ FROM "発注データ" po
2980
+ LEFT JOIN "取引先マスタ" s ON po."取引先コード" = s."取引先コード"
2981
+ LEFT JOIN "発注明細データ" pod ON po."発注番号" = pod."発注番号"
2982
+ LEFT JOIN "品目マスタ" i ON pod."品目コード" = i."品目コード"
2983
+ AND i."適用開始日" = (
2984
+ SELECT MAX("適用開始日") FROM "品目マスタ"
2985
+ WHERE "品目コード" = pod."品目コード"
2986
+ AND "適用開始日" <= CURRENT_DATE
2987
+ )
2988
+ WHERE po."発注番号" = #{purchaseOrderNumber}
2989
+ ORDER BY pod."発注行番号"
2990
+ </select>
2991
+
2992
+ </mapper>
2993
+ ```
2994
+
2995
+ </details>
2996
+
2997
+ <details>
2998
+ <summary>ReceivingMapper.xml(リレーション設定)</summary>
2999
+
3000
+ ```xml
3001
+ <?xml version="1.0" encoding="UTF-8" ?>
3002
+ <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
3003
+ "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
3004
+
3005
+ <!-- src/main/resources/mapper/ReceivingMapper.xml -->
3006
+ <mapper namespace="com.example.pms.infrastructure.out.persistence.mapper.ReceivingMapper">
3007
+
3008
+ <!-- 入荷受入データ ResultMap(発注明細・検査・検収込み) -->
3009
+ <resultMap id="receivingWithRelationsResultMap" type="com.example.pms.domain.model.purchase.Receiving">
3010
+ <id property="id" column="r_ID"/>
3011
+ <result property="receivingNumber" column="r_入荷受入番号"/>
3012
+ <result property="purchaseOrderNumber" column="r_発注番号"/>
3013
+ <result property="lineNumber" column="r_発注行番号"/>
3014
+ <result property="receivingDate" column="r_入荷日"/>
3015
+ <result property="receivingType" column="r_入荷受入区分"
3016
+ typeHandler="com.example.pms.infrastructure.out.persistence.typehandler.ReceivingTypeTypeHandler"/>
3017
+ <result property="receivedQuantity" column="r_入荷数量"/>
3018
+ <result property="locationCode" column="r_入荷場所コード"/>
3019
+ <result property="remarks" column="r_備考"/>
3020
+ <result property="version" column="r_バージョン"/>
3021
+ <result property="createdAt" column="r_作成日時"/>
3022
+ <result property="updatedAt" column="r_更新日時"/>
3023
+ <!-- 発注明細との N:1 関連 -->
3024
+ <association property="purchaseOrderDetail" javaType="com.example.pms.domain.model.purchase.PurchaseOrderDetail">
3025
+ <id property="id" column="pod_ID"/>
3026
+ <result property="purchaseOrderNumber" column="pod_発注番号"/>
3027
+ <result property="lineNumber" column="pod_発注行番号"/>
3028
+ <result property="itemCode" column="pod_品目コード"/>
3029
+ <result property="orderQuantity" column="pod_発注数量"/>
3030
+ <result property="unitPrice" column="pod_発注単価"/>
3031
+ </association>
3032
+ <!-- 検査データとの 1:1 関連 -->
3033
+ <association property="inspection" javaType="com.example.pms.domain.model.purchase.Inspection">
3034
+ <id property="id" column="ins_ID"/>
3035
+ <result property="inspectionNumber" column="ins_検査番号"/>
3036
+ <result property="inspectionDate" column="ins_検査日"/>
3037
+ <result property="inspectedQuantity" column="ins_検査数量"/>
3038
+ <result property="goodQuantity" column="ins_良品数量"/>
3039
+ <result property="defectQuantity" column="ins_不良数量"/>
3040
+ </association>
3041
+ <!-- 検収データとの 1:1 関連 -->
3042
+ <association property="acceptance" javaType="com.example.pms.domain.model.purchase.Acceptance">
3043
+ <id property="id" column="acc_ID"/>
3044
+ <result property="acceptanceNumber" column="acc_検収番号"/>
3045
+ <result property="acceptanceDate" column="acc_検収日"/>
3046
+ <result property="acceptedQuantity" column="acc_検収数量"/>
3047
+ <result property="amount" column="acc_検収金額"/>
3048
+ <result property="taxAmount" column="acc_消費税額"/>
3049
+ </association>
3050
+ </resultMap>
3051
+
3052
+ <!-- JOIN による一括取得クエリ -->
3053
+ <select id="findWithRelationsByReceivingNumber" resultMap="receivingWithRelationsResultMap">
3054
+ SELECT
3055
+ -- 入荷受入データ
3056
+ r."ID" AS r_ID,
3057
+ r."入荷受入番号" AS r_入荷受入番号,
3058
+ r."発注番号" AS r_発注番号,
3059
+ r."発注行番号" AS r_発注行番号,
3060
+ r."入荷日" AS r_入荷日,
3061
+ r."入荷受入区分" AS r_入荷受入区分,
3062
+ r."入荷数量" AS r_入荷数量,
3063
+ r."入荷場所コード" AS r_入荷場所コード,
3064
+ r."備考" AS r_備考,
3065
+ r."バージョン" AS r_バージョン,
3066
+ r."作成日時" AS r_作成日時,
3067
+ r."更新日時" AS r_更新日時,
3068
+ -- 発注明細データ
3069
+ pod."ID" AS pod_ID,
3070
+ pod."発注番号" AS pod_発注番号,
3071
+ pod."発注行番号" AS pod_発注行番号,
3072
+ pod."品目コード" AS pod_品目コード,
3073
+ pod."発注数量" AS pod_発注数量,
3074
+ pod."発注単価" AS pod_発注単価,
3075
+ -- 受入検査データ
3076
+ ins."ID" AS ins_ID,
3077
+ ins."検査番号" AS ins_検査番号,
3078
+ ins."検査日" AS ins_検査日,
3079
+ ins."検査数量" AS ins_検査数量,
3080
+ ins."良品数量" AS ins_良品数量,
3081
+ ins."不良数量" AS ins_不良数量,
3082
+ -- 検収データ
3083
+ acc."ID" AS acc_ID,
3084
+ acc."検収番号" AS acc_検収番号,
3085
+ acc."検収日" AS acc_検収日,
3086
+ acc."検収数量" AS acc_検収数量,
3087
+ acc."検収金額" AS acc_検収金額,
3088
+ acc."消費税額" AS acc_消費税額
3089
+ FROM "入荷受入データ" r
3090
+ LEFT JOIN "発注明細データ" pod
3091
+ ON r."発注番号" = pod."発注番号" AND r."発注行番号" = pod."発注行番号"
3092
+ LEFT JOIN "受入検査データ" ins ON r."入荷受入番号" = ins."入荷受入番号"
3093
+ LEFT JOIN "検収データ" acc ON r."入荷受入番号" = acc."入荷受入番号"
3094
+ WHERE r."入荷受入番号" = #{receivingNumber}
3095
+ </select>
3096
+
3097
+ </mapper>
3098
+ ```
3099
+
3100
+ </details>
3101
+
3102
+ #### リレーション設定のポイント
3103
+
3104
+ | 設定項目 | 説明 |
3105
+ |---------|------|
3106
+ | `<collection>` | 発注→発注明細 の 1:N 関連 |
3107
+ | `<association>` | 入荷→発注明細、入荷→検査、入荷→検収 の N:1/1:1 関連 |
3108
+ | エイリアス | `po_`(発注)、`pod_`(発注明細)、`r_`(入荷)、`ins_`(検査)、`acc_`(検収) |
3109
+ | 有効日サブクエリ | 品目マスタの適用開始日を考慮した最新レコード取得 |
3110
+
3111
+ ### 楽観ロックの実装
3112
+
3113
+ 発注から検収までの一連の業務で複数ユーザーが同時に操作する可能性があるため、楽観ロックを実装します。
3114
+
3115
+ #### Flyway マイグレーション: バージョンカラム追加
3116
+
3117
+ <details>
3118
+ <summary>V010__add_purchasing_version_columns.sql</summary>
3119
+
3120
+ ```sql
3121
+ -- src/main/resources/db/migration/V010__add_purchasing_version_columns.sql
3122
+
3123
+ -- 発注データテーブルにバージョンカラムを追加
3124
+ ALTER TABLE "発注データ" ADD COLUMN "バージョン" INTEGER DEFAULT 1 NOT NULL;
3125
+
3126
+ -- 発注明細データテーブルにバージョンカラムを追加
3127
+ ALTER TABLE "発注明細データ" ADD COLUMN "バージョン" INTEGER DEFAULT 1 NOT NULL;
3128
+
3129
+ -- 入荷受入データテーブルにバージョンカラムを追加
3130
+ ALTER TABLE "入荷受入データ" ADD COLUMN "バージョン" INTEGER DEFAULT 1 NOT NULL;
3131
+
3132
+ -- 受入検査データテーブルにバージョンカラムを追加
3133
+ ALTER TABLE "受入検査データ" ADD COLUMN "バージョン" INTEGER DEFAULT 1 NOT NULL;
3134
+
3135
+ -- 検収データテーブルにバージョンカラムを追加
3136
+ ALTER TABLE "検収データ" ADD COLUMN "バージョン" INTEGER DEFAULT 1 NOT NULL;
3137
+
3138
+ -- コメント追加
3139
+ COMMENT ON COLUMN "発注データ"."バージョン" IS '楽観ロック用バージョン番号';
3140
+ COMMENT ON COLUMN "発注明細データ"."バージョン" IS '楽観ロック用バージョン番号';
3141
+ COMMENT ON COLUMN "入荷受入データ"."バージョン" IS '楽観ロック用バージョン番号';
3142
+ COMMENT ON COLUMN "受入検査データ"."バージョン" IS '楽観ロック用バージョン番号';
3143
+ COMMENT ON COLUMN "検収データ"."バージョン" IS '楽観ロック用バージョン番号';
3144
+ ```
3145
+
3146
+ </details>
3147
+
3148
+ #### エンティティへのバージョンフィールド追加
3149
+
3150
+ <details>
3151
+ <summary>PurchaseOrder.java(バージョンフィールド追加)</summary>
3152
+
3153
+ ```java
3154
+ // src/main/java/com/example/pms/domain/model/purchase/PurchaseOrder.java
3155
+ package com.example.pms.domain.model.purchase;
3156
+
3157
+ import com.example.pms.domain.model.master.Supplier;
3158
+ import lombok.Builder;
3159
+ import lombok.Data;
3160
+
3161
+ import java.time.LocalDate;
3162
+ import java.time.LocalDateTime;
3163
+ import java.util.ArrayList;
3164
+ import java.util.List;
3165
+
3166
+ @Data
3167
+ @Builder
3168
+ public class PurchaseOrder {
3169
+ private Integer id;
3170
+ private String purchaseOrderNumber;
3171
+ private LocalDate orderDate;
3172
+ private String supplierCode;
3173
+ private String purchaserCode;
3174
+ private String departmentCode;
3175
+ private PurchaseOrderStatus status;
3176
+ private String remarks;
3177
+ private LocalDateTime createdAt;
3178
+ private String createdBy;
3179
+ private LocalDateTime updatedAt;
3180
+ private String updatedBy;
3181
+
3182
+ // 楽観ロック用バージョン
3183
+ @Builder.Default
3184
+ private Integer version = 1;
3185
+
3186
+ // リレーション
3187
+ private Supplier supplier;
3188
+ @Builder.Default
3189
+ private List<PurchaseOrderDetail> details = new ArrayList<>();
3190
+ }
3191
+ ```
3192
+
3193
+ </details>
3194
+
3195
+ <details>
3196
+ <summary>Receiving.java(バージョンフィールド追加)</summary>
3197
+
3198
+ ```java
3199
+ // src/main/java/com/example/pms/domain/model/purchase/Receiving.java
3200
+ package com.example.pms.domain.model.purchase;
3201
+
3202
+ import lombok.Builder;
3203
+ import lombok.Data;
3204
+
3205
+ import java.math.BigDecimal;
3206
+ import java.time.LocalDate;
3207
+ import java.time.LocalDateTime;
3208
+
3209
+ @Data
3210
+ @Builder
3211
+ public class Receiving {
3212
+ private Integer id;
3213
+ private String receivingNumber;
3214
+ private String purchaseOrderNumber;
3215
+ private Integer lineNumber;
3216
+ private LocalDate receivingDate;
3217
+ private ReceivingType receivingType;
3218
+ private BigDecimal receivedQuantity;
3219
+ private String locationCode;
3220
+ private String remarks;
3221
+ private LocalDateTime createdAt;
3222
+ private LocalDateTime updatedAt;
3223
+
3224
+ // 楽観ロック用バージョン
3225
+ @Builder.Default
3226
+ private Integer version = 1;
3227
+
3228
+ // リレーション
3229
+ private PurchaseOrderDetail purchaseOrderDetail;
3230
+ private Inspection inspection;
3231
+ private Acceptance acceptance;
3232
+ }
3233
+ ```
3234
+
3235
+ </details>
3236
+
3237
+ #### MyBatis Mapper: 楽観ロック対応の更新
3238
+
3239
+ <details>
3240
+ <summary>PurchaseOrderMapper.xml(楽観ロック対応 UPDATE)</summary>
3241
+
3242
+ ```xml
3243
+ <!-- 楽観ロック対応の更新(バージョンチェック付き) -->
3244
+ <update id="updateWithOptimisticLock" parameterType="com.example.pms.domain.model.purchase.PurchaseOrder">
3245
+ UPDATE "発注データ"
3246
+ SET
3247
+ "発注日" = #{orderDate},
3248
+ "取引先コード" = #{supplierCode},
3249
+ "発注担当者コード" = #{purchaserCode},
3250
+ "発注部門コード" = #{departmentCode},
3251
+ "ステータス" = #{status, typeHandler=com.example.pms.infrastructure.out.persistence.typehandler.PurchaseOrderStatusTypeHandler}::発注ステータス,
3252
+ "備考" = #{remarks},
3253
+ "更新日時" = CURRENT_TIMESTAMP,
3254
+ "更新者" = #{updatedBy},
3255
+ "バージョン" = "バージョン" + 1
3256
+ WHERE "ID" = #{id}
3257
+ AND "バージョン" = #{version}
3258
+ </update>
3259
+
3260
+ <!-- ステータス更新(楽観ロック対応) -->
3261
+ <update id="updateStatusWithOptimisticLock">
3262
+ UPDATE "発注データ"
3263
+ SET
3264
+ "ステータス" = #{status, typeHandler=com.example.pms.infrastructure.out.persistence.typehandler.PurchaseOrderStatusTypeHandler}::発注ステータス,
3265
+ "更新日時" = CURRENT_TIMESTAMP,
3266
+ "バージョン" = "バージョン" + 1
3267
+ WHERE "ID" = #{id}
3268
+ AND "バージョン" = #{version}
3269
+ </update>
3270
+
3271
+ <!-- 現在のバージョン取得 -->
3272
+ <select id="findVersionById" resultType="java.lang.Integer">
3273
+ SELECT "バージョン" FROM "発注データ" WHERE "ID" = #{id}
3274
+ </select>
3275
+ ```
3276
+
3277
+ </details>
3278
+
3279
+ <details>
3280
+ <summary>PurchaseOrderDetailMapper.xml(楽観ロック対応 UPDATE)</summary>
3281
+
3282
+ ```xml
3283
+ <!-- 入荷数量更新(楽観ロック対応) -->
3284
+ <update id="updateReceivedQuantityWithOptimisticLock">
3285
+ UPDATE "発注明細データ"
3286
+ SET
3287
+ "入荷済数量" = "入荷済数量" + #{additionalQuantity},
3288
+ "更新日時" = CURRENT_TIMESTAMP,
3289
+ "バージョン" = "バージョン" + 1
3290
+ WHERE "ID" = #{id}
3291
+ AND "バージョン" = #{version}
3292
+ </update>
3293
+
3294
+ <!-- 検収数量更新(楽観ロック対応) -->
3295
+ <update id="updateAcceptedQuantityWithOptimisticLock">
3296
+ UPDATE "発注明細データ"
3297
+ SET
3298
+ "検収済数量" = "検収済数量" + #{additionalQuantity},
3299
+ "完了フラグ" = CASE
3300
+ WHEN "検収済数量" + #{additionalQuantity} >= "発注数量" THEN TRUE
3301
+ ELSE FALSE
3302
+ END,
3303
+ "更新日時" = CURRENT_TIMESTAMP,
3304
+ "バージョン" = "バージョン" + 1
3305
+ WHERE "ID" = #{id}
3306
+ AND "バージョン" = #{version}
3307
+ </update>
3308
+
3309
+ <!-- 現在のバージョン取得 -->
3310
+ <select id="findVersionById" resultType="java.lang.Integer">
3311
+ SELECT "バージョン" FROM "発注明細データ" WHERE "ID" = #{id}
3312
+ </select>
3313
+ ```
3314
+
3315
+ </details>
3316
+
3317
+ #### Repository 実装: 楽観ロック対応
3318
+
3319
+ <details>
3320
+ <summary>PurchaseOrderRepositoryImpl.java(楽観ロック対応)</summary>
3321
+
3322
+ ```java
3323
+ // src/main/java/com/example/pms/infrastructure/persistence/repository/PurchaseOrderRepositoryImpl.java
3324
+ package com.example.pms.infrastructure.out.persistence.typehandler.repository;
3325
+
3326
+ import com.example.pms.application.port.out.PurchaseOrderRepository;
3327
+ import com.example.pms.domain.exception.OptimisticLockException;
3328
+ import com.example.pms.domain.model.purchase.PurchaseOrder;
3329
+ import com.example.pms.domain.model.purchase.PurchaseOrderStatus;
3330
+ import com.example.pms.infrastructure.out.persistence.mapper.PurchaseOrderMapper;
3331
+ import lombok.RequiredArgsConstructor;
3332
+ import org.springframework.stereotype.Repository;
3333
+ import org.springframework.transaction.annotation.Transactional;
3334
+
3335
+ import java.util.Optional;
3336
+
3337
+ @Repository
3338
+ @RequiredArgsConstructor
3339
+ public class PurchaseOrderRepositoryImpl implements PurchaseOrderRepository {
3340
+
3341
+ private final PurchaseOrderMapper mapper;
3342
+
3343
+ @Override
3344
+ @Transactional
3345
+ public void update(PurchaseOrder purchaseOrder) {
3346
+ int updatedCount = mapper.updateWithOptimisticLock(purchaseOrder);
3347
+
3348
+ if (updatedCount == 0) {
3349
+ Integer currentVersion = mapper.findVersionById(purchaseOrder.getId());
3350
+ if (currentVersion == null) {
3351
+ throw new OptimisticLockException("発注", purchaseOrder.getId());
3352
+ } else {
3353
+ throw new OptimisticLockException("発注", purchaseOrder.getId(),
3354
+ purchaseOrder.getVersion(), currentVersion);
3355
+ }
3356
+ }
3357
+ }
3358
+
3359
+ @Override
3360
+ @Transactional
3361
+ public void updateStatus(Integer id, PurchaseOrderStatus status, Integer version) {
3362
+ int updatedCount = mapper.updateStatusWithOptimisticLock(id, status, version);
3363
+
3364
+ if (updatedCount == 0) {
3365
+ Integer currentVersion = mapper.findVersionById(id);
3366
+ if (currentVersion == null) {
3367
+ throw new OptimisticLockException("発注", id);
3368
+ } else {
3369
+ throw new OptimisticLockException("発注", id, version, currentVersion);
3370
+ }
3371
+ }
3372
+ }
3373
+
3374
+ @Override
3375
+ public Optional<PurchaseOrder> findWithDetailsByPurchaseOrderNumber(String purchaseOrderNumber) {
3376
+ return Optional.ofNullable(mapper.findWithDetailsByPurchaseOrderNumber(purchaseOrderNumber));
3377
+ }
3378
+
3379
+ // その他のメソッド...
3380
+ }
3381
+ ```
3382
+
3383
+ </details>
3384
+
3385
+ #### TDD: 楽観ロックのテスト
3386
+
3387
+ <details>
3388
+ <summary>PurchaseOrderRepositoryOptimisticLockTest.java</summary>
3389
+
3390
+ ```java
3391
+ // src/test/java/com/example/pms/infrastructure/persistence/repository/PurchaseOrderRepositoryOptimisticLockTest.java
3392
+ package com.example.pms.infrastructure.out.persistence.typehandler.repository;
3393
+
3394
+ import com.example.pms.application.port.out.PurchaseOrderRepository;
3395
+ import com.example.pms.domain.exception.OptimisticLockException;
3396
+ import com.example.pms.domain.model.purchase.PurchaseOrder;
3397
+ import com.example.pms.domain.model.purchase.PurchaseOrderStatus;
3398
+ import com.example.pms.testsetup.BaseIntegrationTest;
3399
+ import org.junit.jupiter.api.*;
3400
+ import org.springframework.beans.factory.annotation.Autowired;
3401
+
3402
+ import java.time.LocalDate;
3403
+
3404
+ import static org.assertj.core.api.Assertions.*;
3405
+
3406
+ @DisplayName("発注リポジトリ - 楽観ロック")
3407
+ class PurchaseOrderRepositoryOptimisticLockTest extends BaseIntegrationTest {
3408
+
3409
+ @Autowired
3410
+ private PurchaseOrderRepository purchaseOrderRepository;
3411
+
3412
+ @BeforeEach
3413
+ void setUp() {
3414
+ purchaseOrderRepository.deleteAll();
3415
+ }
3416
+
3417
+ @Nested
3418
+ @DisplayName("楽観ロック")
3419
+ class OptimisticLocking {
3420
+
3421
+ @Test
3422
+ @DisplayName("同じバージョンで更新できる")
3423
+ void canUpdateWithSameVersion() {
3424
+ // Arrange
3425
+ var po = PurchaseOrder.builder()
3426
+ .purchaseOrderNumber("PO-2025-0001")
3427
+ .orderDate(LocalDate.of(2025, 1, 20))
3428
+ .supplierCode("SUP-001")
3429
+ .status(PurchaseOrderStatus.CREATING)
3430
+ .build();
3431
+ purchaseOrderRepository.save(po);
3432
+
3433
+ // Act
3434
+ var fetched = purchaseOrderRepository.findByPurchaseOrderNumber("PO-2025-0001").get();
3435
+ fetched.setRemarks("更新テスト");
3436
+ purchaseOrderRepository.update(fetched);
3437
+
3438
+ // Assert
3439
+ var updated = purchaseOrderRepository.findByPurchaseOrderNumber("PO-2025-0001").get();
3440
+ assertThat(updated.getRemarks()).isEqualTo("更新テスト");
3441
+ assertThat(updated.getVersion()).isEqualTo(2);
3442
+ }
3443
+
3444
+ @Test
3445
+ @DisplayName("異なるバージョンで更新すると楽観ロック例外が発生する")
3446
+ void throwsExceptionWhenVersionMismatch() {
3447
+ // Arrange
3448
+ var po = PurchaseOrder.builder()
3449
+ .purchaseOrderNumber("PO-2025-0002")
3450
+ .orderDate(LocalDate.of(2025, 1, 20))
3451
+ .supplierCode("SUP-001")
3452
+ .status(PurchaseOrderStatus.CREATING)
3453
+ .build();
3454
+ purchaseOrderRepository.save(po);
3455
+
3456
+ // ユーザーAが取得
3457
+ var poA = purchaseOrderRepository.findByPurchaseOrderNumber("PO-2025-0002").get();
3458
+ // ユーザーBが取得
3459
+ var poB = purchaseOrderRepository.findByPurchaseOrderNumber("PO-2025-0002").get();
3460
+
3461
+ // ユーザーAが更新(成功)
3462
+ poA.setRemarks("ユーザーAの更新");
3463
+ purchaseOrderRepository.update(poA);
3464
+
3465
+ // Act & Assert: ユーザーBが古いバージョンで更新(失敗)
3466
+ poB.setRemarks("ユーザーBの更新");
3467
+ assertThatThrownBy(() -> purchaseOrderRepository.update(poB))
3468
+ .isInstanceOf(OptimisticLockException.class)
3469
+ .hasMessageContaining("他のユーザーによって更新されています");
3470
+ }
3471
+
3472
+ @Test
3473
+ @DisplayName("ステータス更新も楽観ロックが適用される")
3474
+ void statusUpdateWithOptimisticLock() {
3475
+ // Arrange
3476
+ var po = PurchaseOrder.builder()
3477
+ .purchaseOrderNumber("PO-2025-0003")
3478
+ .orderDate(LocalDate.of(2025, 1, 20))
3479
+ .supplierCode("SUP-001")
3480
+ .status(PurchaseOrderStatus.CREATING)
3481
+ .build();
3482
+ purchaseOrderRepository.save(po);
3483
+
3484
+ // ユーザーAとBが同時に取得
3485
+ var poA = purchaseOrderRepository.findByPurchaseOrderNumber("PO-2025-0003").get();
3486
+ var poB = purchaseOrderRepository.findByPurchaseOrderNumber("PO-2025-0003").get();
3487
+
3488
+ // ユーザーAがステータス更新(成功)
3489
+ purchaseOrderRepository.updateStatus(poA.getId(), PurchaseOrderStatus.ORDERED, poA.getVersion());
3490
+
3491
+ // Act & Assert: ユーザーBが古いバージョンでステータス更新(失敗)
3492
+ assertThatThrownBy(() ->
3493
+ purchaseOrderRepository.updateStatus(poB.getId(), PurchaseOrderStatus.CANCELLED, poB.getVersion()))
3494
+ .isInstanceOf(OptimisticLockException.class);
3495
+ }
3496
+ }
3497
+ }
3498
+ ```
3499
+
3500
+ </details>
3501
+
3502
+ ### 入荷・検収処理における楽観ロックの考慮
3503
+
3504
+ 入荷から検収までの一連の処理では、発注明細の数量を段階的に更新するため、楽観ロックの適切な処理が重要です。
3505
+
3506
+ ```plantuml
3507
+ @startuml
3508
+
3509
+ title 入荷・検収処理と楽観ロック
3510
+
3511
+ participant "入荷サービス" as RecvSvc
3512
+ participant "発注明細リポジトリ" as PODRepo
3513
+ participant "入荷リポジトリ" as RecvRepo
3514
+ database "DB" as DB
3515
+
3516
+ RecvSvc -> PODRepo: findByPurchaseOrderAndLine(poNumber, lineNumber)
3517
+ PODRepo -> DB: SELECT ... WHERE 発注番号 = ? AND 発注行番号 = ?
3518
+ DB --> PODRepo: 発注明細(バージョン込み)
3519
+ PODRepo --> RecvSvc: PurchaseOrderDetail
3520
+
3521
+ RecvSvc -> RecvSvc: 入荷データ作成
3522
+
3523
+ RecvSvc -> RecvRepo: save(receiving)
3524
+ RecvRepo -> DB: INSERT INTO 入荷受入データ
3525
+ DB --> RecvRepo: OK
3526
+
3527
+ RecvSvc -> PODRepo: updateReceivedQuantity(id, quantity, version)
3528
+ PODRepo -> DB: UPDATE ... WHERE ID = ? AND バージョン = ?
3529
+ alt バージョン一致
3530
+ DB --> PODRepo: 更新成功(1件)
3531
+ PODRepo --> RecvSvc: OK
3532
+ else バージョン不一致
3533
+ DB --> PODRepo: 更新失敗(0件)
3534
+ PODRepo --> RecvSvc: OptimisticLockException
3535
+ RecvSvc -> RecvSvc: ロールバック
3536
+ end
3537
+
3538
+ @enduml
3539
+ ```
3540
+
3541
+ #### 分割入荷時の楽観ロック戦略
3542
+
3543
+ <details>
3544
+ <summary>ReceivingService.java(楽観ロック対応)</summary>
3545
+
3546
+ ```java
3547
+ /**
3548
+ * 入荷処理(楽観ロック対応)
3549
+ */
3550
+ @Transactional
3551
+ public Receiving processReceiving(ReceivingRequest request) {
3552
+ // 発注明細を取得
3553
+ var detail = purchaseOrderDetailRepository
3554
+ .findByPurchaseOrderAndLine(request.getPurchaseOrderNumber(), request.getLineNumber())
3555
+ .orElseThrow(() -> new IllegalArgumentException("発注明細が見つかりません"));
3556
+
3557
+ // 入荷可能数量チェック
3558
+ var remainingQuantity = detail.getOrderQuantity()
3559
+ .subtract(detail.getReceivedQuantity());
3560
+ if (request.getReceivedQuantity().compareTo(remainingQuantity) > 0) {
3561
+ throw new IllegalArgumentException(
3562
+ String.format("入荷数量が残数を超えています(残数: %s)", remainingQuantity));
3563
+ }
3564
+
3565
+ // 入荷データ作成
3566
+ var receiving = Receiving.builder()
3567
+ .receivingNumber(generateReceivingNumber())
3568
+ .purchaseOrderNumber(request.getPurchaseOrderNumber())
3569
+ .lineNumber(request.getLineNumber())
3570
+ .receivingDate(request.getReceivingDate())
3571
+ .receivingType(determineReceivingType(detail, request.getReceivedQuantity()))
3572
+ .receivedQuantity(request.getReceivedQuantity())
3573
+ .locationCode(request.getLocationCode())
3574
+ .build();
3575
+ receivingRepository.save(receiving);
3576
+
3577
+ // 発注明細の入荷済数量を更新(楽観ロック)
3578
+ try {
3579
+ purchaseOrderDetailRepository.updateReceivedQuantity(
3580
+ detail.getId(),
3581
+ request.getReceivedQuantity(),
3582
+ detail.getVersion()
3583
+ );
3584
+ } catch (OptimisticLockException e) {
3585
+ // 他ユーザーが先に入荷処理を行った場合
3586
+ throw new ConcurrentUpdateException(
3587
+ "他のユーザーが同時に入荷処理を行いました。画面を更新して再度お試しください。", e);
3588
+ }
3589
+
3590
+ // 発注ステータス更新
3591
+ updatePurchaseOrderStatus(request.getPurchaseOrderNumber());
3592
+
3593
+ return receiving;
3594
+ }
3595
+
3596
+ private ReceivingType determineReceivingType(PurchaseOrderDetail detail, BigDecimal receivedQuantity) {
3597
+ var totalReceived = detail.getReceivedQuantity().add(receivedQuantity);
3598
+ if (totalReceived.compareTo(detail.getOrderQuantity()) < 0) {
3599
+ return ReceivingType.SPLIT; // 分割入荷
3600
+ }
3601
+ return ReceivingType.NORMAL; // 通常入荷(全数)
3602
+ }
3603
+ ```
3604
+
3605
+ </details>
3606
+
3607
+ #### 楽観ロックのベストプラクティス(購買管理向け)
3608
+
3609
+ | ポイント | 説明 |
3610
+ |---------|------|
3611
+ | **発注明細の数量更新** | 入荷・検査・検収それぞれで楽観ロックを適用 |
3612
+ | **分割入荷対応** | 複数回の入荷でも整合性を保証 |
3613
+ | **ステータス連動** | 発注ステータスと明細の完了フラグを連動更新 |
3614
+ | **エラーメッセージ** | ユーザーに再操作を促す明確なメッセージ |
3615
+ | **トランザクション境界** | 入荷→明細更新→ステータス更新を1トランザクションで |
3616
+
3617
+ ---
3618
+
3619
+ ## 25.4 まとめ
3620
+
3621
+ 本章では、購買管理(発注から検収まで)の DB 設計と実装について学びました。
3622
+
3623
+ ### 学んだこと
3624
+
3625
+ 1. **発注業務の設計**
3626
+ - 発注データと発注明細データの親子関係
3627
+ - 単価マスタによる価格管理
3628
+ - 発注ステータスの状態遷移
3629
+ - 諸口品目(マスタ外品目)の扱い
3630
+
3631
+ 2. **入荷・検収業務の設計**
3632
+ - 入荷→検査→検収の業務フロー
3633
+ - 分割入荷への対応
3634
+ - 良品・不良品の管理
3635
+ - 消費税計算
3636
+
3637
+ 3. **命名規則のパターン**
3638
+ - **DB(日本語)**: テーブル名、カラム名、ENUM 型・値は日本語
3639
+ - **Java(英語)**: クラス名、フィールド名、enum 値は英語
3640
+ - **MyBatis resultMap**: 日本語カラムと英語プロパティのマッピング
3641
+ - **TypeHandler**: 日本語 ENUM 値と英語 enum の相互変換
3642
+
3643
+ ### テーブル一覧
3644
+
3645
+ | テーブル名(日本語) | Java エンティティ | 説明 |
3646
+ |---|---|---|
3647
+ | 単価マスタ | UnitPrice | 品目・取引先ごとの単価情報 |
3648
+ | 発注データ | PurchaseOrder | 発注ヘッダ情報 |
3649
+ | 発注明細データ | PurchaseOrderDetail | 発注明細情報 |
3650
+ | 諸口品目情報 | MiscellaneousItem | マスタ外品目の臨時情報 |
3651
+ | 入荷受入データ | Receiving | 入荷情報 |
3652
+ | 欠点マスタ | Defect | 不良品の欠点コード |
3653
+ | 受入検査データ | Inspection | 受入検査情報 |
3654
+ | 検収データ | Acceptance | 検収情報 |
3655
+
3656
+ ### ENUM 一覧
3657
+
3658
+ | DB ENUM 型(日本語) | Java Enum | 値 |
3659
+ |---|---|---|
3660
+ | 発注ステータス | PurchaseOrderStatus | 作成中→CREATING, 発注済→ORDERED, 一部入荷→PARTIALLY_RECEIVED, 入荷完了→RECEIVED, 検収完了→ACCEPTED, 取消→CANCELLED |
3661
+ | 入荷受入区分 | ReceivingType | 通常入荷→NORMAL, 分割入荷→SPLIT, 返品入荷→RETURN |