@k2works/claude-code-booster 3.2.1 → 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.
- package/lib/assets/docs/article/index.md +4 -1
- package/lib/assets/docs/article/practical-database-design/index.md +121 -0
- package/lib/assets/docs/article/practical-database-design/part1/chapter01.md +288 -0
- package/lib/assets/docs/article/practical-database-design/part1/chapter02.md +518 -0
- package/lib/assets/docs/article/practical-database-design/part1/chapter03.md +557 -0
- package/lib/assets/docs/article/practical-database-design/part2/chapter04.md +924 -0
- package/lib/assets/docs/article/practical-database-design/part2/chapter05.md +1627 -0
- package/lib/assets/docs/article/practical-database-design/part2/chapter06.md +2716 -0
- package/lib/assets/docs/article/practical-database-design/part2/chapter07.md +2082 -0
- package/lib/assets/docs/article/practical-database-design/part2/chapter08.md +2105 -0
- package/lib/assets/docs/article/practical-database-design/part2/chapter09.md +2031 -0
- package/lib/assets/docs/article/practical-database-design/part2/chapter10.md +1387 -0
- package/lib/assets/docs/article/practical-database-design/part2/chapter11.md +1677 -0
- package/lib/assets/docs/article/practical-database-design/part2/chapter12.md +1417 -0
- package/lib/assets/docs/article/practical-database-design/part2/chapter13.md +1434 -0
- package/lib/assets/docs/article/practical-database-design/part3/chapter14.md +667 -0
- package/lib/assets/docs/article/practical-database-design/part3/chapter15.md +1625 -0
- package/lib/assets/docs/article/practical-database-design/part3/chapter16.md +1915 -0
- package/lib/assets/docs/article/practical-database-design/part3/chapter17.md +1708 -0
- package/lib/assets/docs/article/practical-database-design/part3/chapter18.md +2095 -0
- package/lib/assets/docs/article/practical-database-design/part3/chapter19.md +1123 -0
- package/lib/assets/docs/article/practical-database-design/part3/chapter20.md +1031 -0
- package/lib/assets/docs/article/practical-database-design/part3/chapter21.md +1382 -0
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter14-orm.md +991 -0
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter15-orm.md +1300 -0
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter16-orm.md +1166 -0
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter17-orm.md +1584 -0
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter18-orm.md +1183 -0
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter19-orm.md +1016 -0
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter20-orm.md +1753 -0
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter21-orm.md +1447 -0
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter22-orm.md +1878 -0
- package/lib/assets/docs/article/practical-database-design/part4/chapter22.md +965 -0
- package/lib/assets/docs/article/practical-database-design/part4/chapter23.md +2069 -0
- package/lib/assets/docs/article/practical-database-design/part4/chapter24.md +2439 -0
- package/lib/assets/docs/article/practical-database-design/part4/chapter25.md +3661 -0
- package/lib/assets/docs/article/practical-database-design/part4/chapter26.md +2916 -0
- package/lib/assets/docs/article/practical-database-design/part4/chapter27.md +3105 -0
- package/lib/assets/docs/article/practical-database-design/part4/chapter28.md +2697 -0
- package/lib/assets/docs/article/practical-database-design/part4/chapter29.md +2544 -0
- package/lib/assets/docs/article/practical-database-design/part4/chapter30.md +2180 -0
- package/lib/assets/docs/article/practical-database-design/part4/chapter31.md +1192 -0
- package/lib/assets/docs/article/practical-database-design/part4/chapter32.md +2101 -0
- package/lib/assets/docs/article/practical-database-design/part5/chapter33.md +1032 -0
- package/lib/assets/docs/article/practical-database-design/part5/chapter34.md +1609 -0
- package/lib/assets/docs/article/practical-database-design/part5/chapter35.md +1453 -0
- package/lib/assets/docs/article/practical-database-design/part5/chapter36.md +1292 -0
- package/lib/assets/docs/article/practical-database-design/part5/chapter37.md +1470 -0
- package/lib/assets/docs/article/practical-database-design/part5/chapter38.md +1698 -0
- package/lib/assets/docs/article/practical-database-design/part5/chapter39.md +2334 -0
- package/lib/assets/docs/article/practical-database-design/study/study2-1.md +1693 -0
- package/lib/assets/docs/article/practical-database-design/study/study2-2.md +1347 -0
- package/lib/assets/docs/article/practical-database-design/study/study2-3.md +2044 -0
- package/lib/assets/docs/article/practical-database-design/study/study2-4.md +2229 -0
- package/lib/assets/docs/article/practical-database-design/study/study2-5.md +2418 -0
- package/lib/assets/docs/article/practical-database-design/study/study3-1.md +2205 -0
- package/lib/assets/docs/article/practical-database-design/study/study3-2.md +2221 -0
- package/lib/assets/docs/article/practical-database-design/study/study3-3.md +2253 -0
- package/lib/assets/docs/article/practical-database-design/study/study3-4.md +2106 -0
- package/lib/assets/docs/article/practical-database-design/study/study3-5.md +2507 -0
- package/lib/assets/docs/article/practical-database-design/study/study4-1.md +2587 -0
- package/lib/assets/docs/article/practical-database-design/study/study4-2.md +2075 -0
- package/lib/assets/docs/article/practical-database-design/study/study4-3.md +1805 -0
- package/lib/assets/docs/article/practical-database-design/study/study4-4.md +1895 -0
- package/lib/assets/docs/article/practical-database-design/study/study4-5.md +2878 -0
- 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 "使用開始日" <= #{date}
|
|
772
|
+
AND ("使用停止日" IS NULL OR "使用停止日" >= #{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 |
|