@k2works/claude-code-booster 3.2.1 → 3.3.1
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/.claude/skills/analyzing-business/SKILL.md +2 -2
- package/lib/assets/.claude/skills/analyzing-inception-deck/SKILL.md +5 -5
- package/lib/assets/.claude/skills/analyzing-requirements/SKILL.md +2 -2
- package/lib/assets/.claude/skills/generating-slides/SKILL.md +7 -7
- 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,2716 @@
|
|
|
1
|
+
# 第6章:受注・出荷・売上の設計
|
|
2
|
+
|
|
3
|
+
販売管理システムの中核となるトランザクションデータを設計していきます。本章では、受注から出荷、売上計上までの一連の業務フローに対応したデータベース設計と実装を行います。
|
|
4
|
+
|
|
5
|
+
## 販売業務の全体フロー
|
|
6
|
+
|
|
7
|
+
販売業務は「見積 → 受注 → 出荷 → 売上」という一連のフローで構成されます。
|
|
8
|
+
|
|
9
|
+
```plantuml
|
|
10
|
+
@startuml
|
|
11
|
+
|
|
12
|
+
title 販売業務フロー
|
|
13
|
+
|
|
14
|
+
|営業部門|
|
|
15
|
+
start
|
|
16
|
+
:見積作成;
|
|
17
|
+
note right
|
|
18
|
+
顧客からの問い合わせに
|
|
19
|
+
対して見積書を作成
|
|
20
|
+
end note
|
|
21
|
+
|
|
22
|
+
:見積提出;
|
|
23
|
+
|
|
24
|
+
if (受注確定?) then (yes)
|
|
25
|
+
:受注登録;
|
|
26
|
+
else (no)
|
|
27
|
+
:失注処理;
|
|
28
|
+
stop
|
|
29
|
+
endif
|
|
30
|
+
|
|
31
|
+
|倉庫部門|
|
|
32
|
+
:出荷指示;
|
|
33
|
+
note right
|
|
34
|
+
受注データを元に
|
|
35
|
+
出荷指示を作成
|
|
36
|
+
end note
|
|
37
|
+
|
|
38
|
+
:ピッキング;
|
|
39
|
+
:検品・梱包;
|
|
40
|
+
:出荷;
|
|
41
|
+
|
|
42
|
+
|営業部門|
|
|
43
|
+
:売上計上;
|
|
44
|
+
note right
|
|
45
|
+
出荷完了時点で
|
|
46
|
+
売上を計上
|
|
47
|
+
end note
|
|
48
|
+
|
|
49
|
+
:納品書発行;
|
|
50
|
+
stop
|
|
51
|
+
|
|
52
|
+
@enduml
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### トランザクションデータの特徴
|
|
56
|
+
|
|
57
|
+
トランザクションデータには以下の共通した特徴があります。
|
|
58
|
+
|
|
59
|
+
| 特徴 | 説明 |
|
|
60
|
+
|-----|------|
|
|
61
|
+
| **ヘッダ・明細構造** | 1つの取引に複数の商品が含まれるため、ヘッダ(親)と明細(子)の構造を持つ |
|
|
62
|
+
| **ステータス管理** | 業務の進捗状態を管理するステータスを持つ |
|
|
63
|
+
| **連番管理** | 伝票番号などの連番を自動採番する |
|
|
64
|
+
| **日時管理** | 作成日時、更新日時など監査証跡を残す |
|
|
65
|
+
| **マスタ参照** | 顧客・商品・社員などのマスタを参照する |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 6.1 受注業務の DB 設計
|
|
70
|
+
|
|
71
|
+
### 受注業務フローの理解
|
|
72
|
+
|
|
73
|
+
受注業務は、顧客からの注文を受け付け、システムに登録する業務です。
|
|
74
|
+
|
|
75
|
+
```plantuml
|
|
76
|
+
@startuml
|
|
77
|
+
|
|
78
|
+
title 受注業務フロー
|
|
79
|
+
|
|
80
|
+
|営業担当|
|
|
81
|
+
start
|
|
82
|
+
:見積書を確認;
|
|
83
|
+
|
|
84
|
+
if (見積あり?) then (yes)
|
|
85
|
+
:見積から受注へ変換;
|
|
86
|
+
else (no)
|
|
87
|
+
:新規受注入力;
|
|
88
|
+
endif
|
|
89
|
+
|
|
90
|
+
:受注データ登録;
|
|
91
|
+
note right
|
|
92
|
+
顧客情報
|
|
93
|
+
商品・数量・単価
|
|
94
|
+
納期
|
|
95
|
+
配送先
|
|
96
|
+
end note
|
|
97
|
+
|
|
98
|
+
:受注明細登録;
|
|
99
|
+
|
|
100
|
+
:受注確認書発行;
|
|
101
|
+
|
|
102
|
+
|倉庫担当|
|
|
103
|
+
:在庫引当;
|
|
104
|
+
note right
|
|
105
|
+
受注数量分の
|
|
106
|
+
在庫を確保
|
|
107
|
+
end note
|
|
108
|
+
|
|
109
|
+
if (在庫あり?) then (yes)
|
|
110
|
+
:引当完了;
|
|
111
|
+
else (no)
|
|
112
|
+
:入荷待ち;
|
|
113
|
+
endif
|
|
114
|
+
|
|
115
|
+
stop
|
|
116
|
+
|
|
117
|
+
@enduml
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 受注データ・受注明細データの構造
|
|
121
|
+
|
|
122
|
+
#### 受注データの ER 図
|
|
123
|
+
|
|
124
|
+
```plantuml
|
|
125
|
+
@startuml
|
|
126
|
+
|
|
127
|
+
title 受注関連テーブル
|
|
128
|
+
|
|
129
|
+
entity 見積データ {
|
|
130
|
+
ID <<PK>>
|
|
131
|
+
--
|
|
132
|
+
見積番号 <<UK>>
|
|
133
|
+
見積日
|
|
134
|
+
見積有効期限
|
|
135
|
+
顧客コード <<FK>>
|
|
136
|
+
顧客枝番 <<FK>>
|
|
137
|
+
担当者コード
|
|
138
|
+
件名
|
|
139
|
+
見積金額
|
|
140
|
+
消費税額
|
|
141
|
+
見積合計
|
|
142
|
+
ステータス
|
|
143
|
+
備考
|
|
144
|
+
作成日時
|
|
145
|
+
更新日時
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
entity 見積明細 {
|
|
149
|
+
ID <<PK>>
|
|
150
|
+
--
|
|
151
|
+
見積ID <<FK>>
|
|
152
|
+
行番号
|
|
153
|
+
商品コード <<FK>>
|
|
154
|
+
商品名
|
|
155
|
+
数量
|
|
156
|
+
単位
|
|
157
|
+
単価
|
|
158
|
+
金額
|
|
159
|
+
税区分
|
|
160
|
+
消費税率
|
|
161
|
+
消費税額
|
|
162
|
+
備考
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
entity 受注データ {
|
|
166
|
+
ID <<PK>>
|
|
167
|
+
--
|
|
168
|
+
受注番号 <<UK>>
|
|
169
|
+
受注日
|
|
170
|
+
顧客コード <<FK>>
|
|
171
|
+
顧客枝番 <<FK>>
|
|
172
|
+
出荷先番号 <<FK>>
|
|
173
|
+
担当者コード
|
|
174
|
+
希望納期
|
|
175
|
+
出荷予定日
|
|
176
|
+
受注金額
|
|
177
|
+
消費税額
|
|
178
|
+
受注合計
|
|
179
|
+
ステータス
|
|
180
|
+
見積ID <<FK>>
|
|
181
|
+
顧客注文番号
|
|
182
|
+
備考
|
|
183
|
+
作成日時
|
|
184
|
+
更新日時
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
entity 受注明細 {
|
|
188
|
+
ID <<PK>>
|
|
189
|
+
--
|
|
190
|
+
受注ID <<FK>>
|
|
191
|
+
行番号
|
|
192
|
+
商品コード <<FK>>
|
|
193
|
+
商品名
|
|
194
|
+
受注数量
|
|
195
|
+
引当数量
|
|
196
|
+
出荷数量
|
|
197
|
+
残数量
|
|
198
|
+
単位
|
|
199
|
+
単価
|
|
200
|
+
金額
|
|
201
|
+
税区分
|
|
202
|
+
消費税率
|
|
203
|
+
消費税額
|
|
204
|
+
倉庫コード
|
|
205
|
+
希望納期
|
|
206
|
+
備考
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
見積データ ||--o{ 見積明細
|
|
210
|
+
見積データ ||--o| 受注データ : "受注化"
|
|
211
|
+
受注データ ||--o{ 受注明細
|
|
212
|
+
|
|
213
|
+
@enduml
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### 受注ステータスの定義
|
|
217
|
+
|
|
218
|
+
```plantuml
|
|
219
|
+
@startuml
|
|
220
|
+
|
|
221
|
+
title 受注ステータス遷移図
|
|
222
|
+
|
|
223
|
+
[*] --> 受付済
|
|
224
|
+
|
|
225
|
+
受付済 --> 引当済 : 在庫引当完了
|
|
226
|
+
受付済 --> 出荷指示済 : 出荷指示作成
|
|
227
|
+
|
|
228
|
+
引当済 --> 出荷指示済 : 出荷指示作成
|
|
229
|
+
|
|
230
|
+
出荷指示済 --> 出荷済 : 出荷完了
|
|
231
|
+
|
|
232
|
+
出荷済 --> [*]
|
|
233
|
+
|
|
234
|
+
受付済 --> キャンセル
|
|
235
|
+
引当済 --> キャンセル
|
|
236
|
+
出荷指示済 --> キャンセル
|
|
237
|
+
|
|
238
|
+
キャンセル --> [*]
|
|
239
|
+
|
|
240
|
+
@enduml
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
| ステータス | 説明 |
|
|
244
|
+
|-----------|------|
|
|
245
|
+
| **受付済** | 受注登録が完了した状態 |
|
|
246
|
+
| **引当済** | 在庫引当が完了した状態 |
|
|
247
|
+
| **出荷指示済** | 出荷指示が作成された状態 |
|
|
248
|
+
| **出荷済** | 出荷が完了した状態 |
|
|
249
|
+
| **キャンセル** | 受注がキャンセルされた状態 |
|
|
250
|
+
|
|
251
|
+
### マイグレーション:受注関連テーブルの作成
|
|
252
|
+
|
|
253
|
+
<details>
|
|
254
|
+
<summary>V007__create_quotation_order_tables.sql</summary>
|
|
255
|
+
|
|
256
|
+
```sql
|
|
257
|
+
-- src/main/resources/db/migration/V007__create_quotation_order_tables.sql
|
|
258
|
+
|
|
259
|
+
-- 見積ステータス
|
|
260
|
+
CREATE TYPE 見積ステータス AS ENUM ('商談中', '受注確定', '失注', '期限切れ');
|
|
261
|
+
|
|
262
|
+
-- 受注ステータス
|
|
263
|
+
CREATE TYPE 受注ステータス AS ENUM ('受付済', '引当済', '出荷指示済', '出荷済', 'キャンセル');
|
|
264
|
+
|
|
265
|
+
-- 見積データ(ヘッダ)
|
|
266
|
+
CREATE TABLE "見積データ" (
|
|
267
|
+
"ID" SERIAL PRIMARY KEY,
|
|
268
|
+
"見積番号" VARCHAR(20) UNIQUE NOT NULL,
|
|
269
|
+
"見積日" DATE NOT NULL,
|
|
270
|
+
"見積有効期限" DATE,
|
|
271
|
+
"顧客コード" VARCHAR(20) NOT NULL,
|
|
272
|
+
"顧客枝番" VARCHAR(10) DEFAULT '00',
|
|
273
|
+
"担当者コード" VARCHAR(20),
|
|
274
|
+
"件名" VARCHAR(200),
|
|
275
|
+
"見積金額" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
|
|
276
|
+
"消費税額" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
|
|
277
|
+
"見積合計" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
|
|
278
|
+
"ステータス" 見積ステータス DEFAULT '商談中' NOT NULL,
|
|
279
|
+
"備考" TEXT,
|
|
280
|
+
"作成日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
281
|
+
"作成者" VARCHAR(50),
|
|
282
|
+
"更新日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
283
|
+
"更新者" VARCHAR(50),
|
|
284
|
+
CONSTRAINT "fk_見積データ_顧客"
|
|
285
|
+
FOREIGN KEY ("顧客コード", "顧客枝番") REFERENCES "顧客マスタ"("顧客コード", "顧客枝番")
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
-- 見積明細
|
|
289
|
+
CREATE TABLE "見積明細" (
|
|
290
|
+
"ID" SERIAL PRIMARY KEY,
|
|
291
|
+
"見積ID" INTEGER NOT NULL,
|
|
292
|
+
"行番号" INTEGER NOT NULL,
|
|
293
|
+
"商品コード" VARCHAR(20) NOT NULL,
|
|
294
|
+
"商品名" VARCHAR(100) NOT NULL,
|
|
295
|
+
"数量" DECIMAL(15, 2) NOT NULL,
|
|
296
|
+
"単位" VARCHAR(10),
|
|
297
|
+
"単価" DECIMAL(15, 2) NOT NULL,
|
|
298
|
+
"金額" DECIMAL(15, 2) NOT NULL,
|
|
299
|
+
"税区分" 税区分 DEFAULT '外税' NOT NULL,
|
|
300
|
+
"消費税率" DECIMAL(5, 2) DEFAULT 10.00 NOT NULL,
|
|
301
|
+
"消費税額" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
|
|
302
|
+
"備考" TEXT,
|
|
303
|
+
CONSTRAINT "fk_見積明細_見積"
|
|
304
|
+
FOREIGN KEY ("見積ID") REFERENCES "見積データ"("ID") ON DELETE CASCADE,
|
|
305
|
+
CONSTRAINT "fk_見積明細_商品"
|
|
306
|
+
FOREIGN KEY ("商品コード") REFERENCES "商品マスタ"("商品コード"),
|
|
307
|
+
CONSTRAINT "uq_見積明細_行番号" UNIQUE ("見積ID", "行番号")
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
-- 受注データ(ヘッダ)
|
|
311
|
+
CREATE TABLE "受注データ" (
|
|
312
|
+
"ID" SERIAL PRIMARY KEY,
|
|
313
|
+
"受注番号" VARCHAR(20) UNIQUE NOT NULL,
|
|
314
|
+
"受注日" DATE NOT NULL,
|
|
315
|
+
"顧客コード" VARCHAR(20) NOT NULL,
|
|
316
|
+
"顧客枝番" VARCHAR(10) DEFAULT '00',
|
|
317
|
+
"出荷先番号" VARCHAR(10),
|
|
318
|
+
"担当者コード" VARCHAR(20),
|
|
319
|
+
"希望納期" DATE,
|
|
320
|
+
"出荷予定日" DATE,
|
|
321
|
+
"受注金額" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
|
|
322
|
+
"消費税額" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
|
|
323
|
+
"受注合計" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
|
|
324
|
+
"ステータス" 受注ステータス DEFAULT '受付済' NOT NULL,
|
|
325
|
+
"見積ID" INTEGER,
|
|
326
|
+
"顧客注文番号" VARCHAR(50),
|
|
327
|
+
"備考" TEXT,
|
|
328
|
+
"作成日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
329
|
+
"作成者" VARCHAR(50),
|
|
330
|
+
"更新日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
331
|
+
"更新者" VARCHAR(50),
|
|
332
|
+
CONSTRAINT "fk_受注データ_顧客"
|
|
333
|
+
FOREIGN KEY ("顧客コード", "顧客枝番") REFERENCES "顧客マスタ"("顧客コード", "顧客枝番"),
|
|
334
|
+
CONSTRAINT "fk_受注データ_出荷先"
|
|
335
|
+
FOREIGN KEY ("顧客コード", "顧客枝番", "出荷先番号") REFERENCES "出荷先マスタ"("取引先コード", "顧客枝番", "出荷先番号"),
|
|
336
|
+
CONSTRAINT "fk_受注データ_見積"
|
|
337
|
+
FOREIGN KEY ("見積ID") REFERENCES "見積データ"("ID")
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
-- 受注明細
|
|
341
|
+
CREATE TABLE "受注明細" (
|
|
342
|
+
"ID" SERIAL PRIMARY KEY,
|
|
343
|
+
"受注ID" INTEGER NOT NULL,
|
|
344
|
+
"行番号" INTEGER NOT NULL,
|
|
345
|
+
"商品コード" VARCHAR(20) NOT NULL,
|
|
346
|
+
"商品名" VARCHAR(100) NOT NULL,
|
|
347
|
+
"受注数量" DECIMAL(15, 2) NOT NULL,
|
|
348
|
+
"引当数量" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
|
|
349
|
+
"出荷数量" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
|
|
350
|
+
"残数量" DECIMAL(15, 2) NOT NULL,
|
|
351
|
+
"単位" VARCHAR(10),
|
|
352
|
+
"単価" DECIMAL(15, 2) NOT NULL,
|
|
353
|
+
"金額" DECIMAL(15, 2) NOT NULL,
|
|
354
|
+
"税区分" 税区分 DEFAULT '外税' NOT NULL,
|
|
355
|
+
"消費税率" DECIMAL(5, 2) DEFAULT 10.00 NOT NULL,
|
|
356
|
+
"消費税額" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
|
|
357
|
+
"倉庫コード" VARCHAR(20),
|
|
358
|
+
"希望納期" DATE,
|
|
359
|
+
"備考" TEXT,
|
|
360
|
+
CONSTRAINT "fk_受注明細_受注"
|
|
361
|
+
FOREIGN KEY ("受注ID") REFERENCES "受注データ"("ID") ON DELETE CASCADE,
|
|
362
|
+
CONSTRAINT "fk_受注明細_商品"
|
|
363
|
+
FOREIGN KEY ("商品コード") REFERENCES "商品マスタ"("商品コード"),
|
|
364
|
+
CONSTRAINT "uq_受注明細_行番号" UNIQUE ("受注ID", "行番号")
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
-- インデックス
|
|
368
|
+
CREATE INDEX "idx_見積データ_顧客コード" ON "見積データ"("顧客コード");
|
|
369
|
+
CREATE INDEX "idx_見積データ_見積日" ON "見積データ"("見積日");
|
|
370
|
+
CREATE INDEX "idx_見積データ_ステータス" ON "見積データ"("ステータス");
|
|
371
|
+
CREATE INDEX "idx_受注データ_顧客コード" ON "受注データ"("顧客コード");
|
|
372
|
+
CREATE INDEX "idx_受注データ_受注日" ON "受注データ"("受注日");
|
|
373
|
+
CREATE INDEX "idx_受注データ_ステータス" ON "受注データ"("ステータス");
|
|
374
|
+
CREATE INDEX "idx_受注データ_希望納期" ON "受注データ"("希望納期");
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
</details>
|
|
378
|
+
|
|
379
|
+
### 受注ステータス Enum
|
|
380
|
+
|
|
381
|
+
<details>
|
|
382
|
+
<summary>OrderStatus.java</summary>
|
|
383
|
+
|
|
384
|
+
```java
|
|
385
|
+
// src/main/java/com/example/sms/domain/model/sales/OrderStatus.java
|
|
386
|
+
package com.example.sms.domain.model.sales;
|
|
387
|
+
|
|
388
|
+
import lombok.Getter;
|
|
389
|
+
import lombok.RequiredArgsConstructor;
|
|
390
|
+
|
|
391
|
+
@Getter
|
|
392
|
+
@RequiredArgsConstructor
|
|
393
|
+
public enum OrderStatus {
|
|
394
|
+
RECEIVED("受付済"),
|
|
395
|
+
ALLOCATED("引当済"),
|
|
396
|
+
SHIPMENT_INSTRUCTED("出荷指示済"),
|
|
397
|
+
SHIPPED("出荷済"),
|
|
398
|
+
CANCELLED("キャンセル");
|
|
399
|
+
|
|
400
|
+
private final String displayName;
|
|
401
|
+
|
|
402
|
+
public static OrderStatus fromDisplayName(String displayName) {
|
|
403
|
+
for (OrderStatus status : values()) {
|
|
404
|
+
if (status.displayName.equals(displayName)) {
|
|
405
|
+
return status;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
throw new IllegalArgumentException("Unknown order status: " + displayName);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
</details>
|
|
414
|
+
|
|
415
|
+
### 受注エンティティ
|
|
416
|
+
|
|
417
|
+
<details>
|
|
418
|
+
<summary>SalesOrder.java</summary>
|
|
419
|
+
|
|
420
|
+
```java
|
|
421
|
+
// src/main/java/com/example/sms/domain/model/sales/SalesOrder.java
|
|
422
|
+
package com.example.sms.domain.model.sales;
|
|
423
|
+
|
|
424
|
+
import lombok.AllArgsConstructor;
|
|
425
|
+
import lombok.Builder;
|
|
426
|
+
import lombok.Data;
|
|
427
|
+
import lombok.NoArgsConstructor;
|
|
428
|
+
|
|
429
|
+
import java.math.BigDecimal;
|
|
430
|
+
import java.time.LocalDate;
|
|
431
|
+
import java.time.LocalDateTime;
|
|
432
|
+
import java.util.ArrayList;
|
|
433
|
+
import java.util.List;
|
|
434
|
+
|
|
435
|
+
@Data
|
|
436
|
+
@Builder
|
|
437
|
+
@NoArgsConstructor
|
|
438
|
+
@AllArgsConstructor
|
|
439
|
+
@SuppressWarnings("PMD.RedundantFieldInitializer")
|
|
440
|
+
public class SalesOrder {
|
|
441
|
+
private Integer id;
|
|
442
|
+
private String orderNumber;
|
|
443
|
+
private LocalDate orderDate;
|
|
444
|
+
private String customerCode;
|
|
445
|
+
private String customerBranchNumber;
|
|
446
|
+
private String shippingDestinationNumber;
|
|
447
|
+
private String representativeCode;
|
|
448
|
+
private LocalDate requestedDeliveryDate;
|
|
449
|
+
private LocalDate scheduledShippingDate;
|
|
450
|
+
@Builder.Default
|
|
451
|
+
private BigDecimal orderAmount = BigDecimal.ZERO;
|
|
452
|
+
@Builder.Default
|
|
453
|
+
private BigDecimal taxAmount = BigDecimal.ZERO;
|
|
454
|
+
@Builder.Default
|
|
455
|
+
private BigDecimal totalAmount = BigDecimal.ZERO;
|
|
456
|
+
@Builder.Default
|
|
457
|
+
private OrderStatus status = OrderStatus.RECEIVED;
|
|
458
|
+
private Integer quotationId;
|
|
459
|
+
private String customerOrderNumber;
|
|
460
|
+
private String remarks;
|
|
461
|
+
private LocalDateTime createdAt;
|
|
462
|
+
private String createdBy;
|
|
463
|
+
private LocalDateTime updatedAt;
|
|
464
|
+
private String updatedBy;
|
|
465
|
+
|
|
466
|
+
@Builder.Default
|
|
467
|
+
private List<SalesOrderDetail> details = new ArrayList<>();
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
</details>
|
|
472
|
+
|
|
473
|
+
<details>
|
|
474
|
+
<summary>SalesOrderDetail.java</summary>
|
|
475
|
+
|
|
476
|
+
```java
|
|
477
|
+
// src/main/java/com/example/sms/domain/model/sales/SalesOrderDetail.java
|
|
478
|
+
package com.example.sms.domain.model.sales;
|
|
479
|
+
|
|
480
|
+
import com.example.sms.domain.model.product.TaxCategory;
|
|
481
|
+
import lombok.AllArgsConstructor;
|
|
482
|
+
import lombok.Builder;
|
|
483
|
+
import lombok.Data;
|
|
484
|
+
import lombok.NoArgsConstructor;
|
|
485
|
+
|
|
486
|
+
import java.math.BigDecimal;
|
|
487
|
+
import java.time.LocalDate;
|
|
488
|
+
|
|
489
|
+
@Data
|
|
490
|
+
@Builder
|
|
491
|
+
@NoArgsConstructor
|
|
492
|
+
@AllArgsConstructor
|
|
493
|
+
public class SalesOrderDetail {
|
|
494
|
+
private Integer id;
|
|
495
|
+
private Integer orderId;
|
|
496
|
+
private Integer lineNumber;
|
|
497
|
+
private String productCode;
|
|
498
|
+
private String productName;
|
|
499
|
+
private BigDecimal orderQuantity;
|
|
500
|
+
private BigDecimal allocatedQuantity;
|
|
501
|
+
private BigDecimal shippedQuantity;
|
|
502
|
+
private BigDecimal remainingQuantity;
|
|
503
|
+
private String unit;
|
|
504
|
+
private BigDecimal unitPrice;
|
|
505
|
+
private BigDecimal amount;
|
|
506
|
+
private TaxCategory taxCategory;
|
|
507
|
+
private BigDecimal taxRate;
|
|
508
|
+
private BigDecimal taxAmount;
|
|
509
|
+
private String warehouseCode;
|
|
510
|
+
private LocalDate requestedDeliveryDate;
|
|
511
|
+
private String remarks;
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
</details>
|
|
516
|
+
|
|
517
|
+
### TypeHandler の実装
|
|
518
|
+
|
|
519
|
+
日本語の ENUM 値を Java の Enum に変換するための TypeHandler を実装します。
|
|
520
|
+
|
|
521
|
+
<details>
|
|
522
|
+
<summary>OrderStatusTypeHandler.java</summary>
|
|
523
|
+
|
|
524
|
+
```java
|
|
525
|
+
// src/main/java/com/example/sms/infrastructure/out/persistence/typehandler/OrderStatusTypeHandler.java
|
|
526
|
+
package com.example.sms.infrastructure.out.persistence.typehandler;
|
|
527
|
+
|
|
528
|
+
import com.example.sms.domain.model.sales.OrderStatus;
|
|
529
|
+
import org.apache.ibatis.type.BaseTypeHandler;
|
|
530
|
+
import org.apache.ibatis.type.JdbcType;
|
|
531
|
+
import org.apache.ibatis.type.MappedTypes;
|
|
532
|
+
|
|
533
|
+
import java.sql.*;
|
|
534
|
+
|
|
535
|
+
@MappedTypes(OrderStatus.class)
|
|
536
|
+
public class OrderStatusTypeHandler extends BaseTypeHandler<OrderStatus> {
|
|
537
|
+
|
|
538
|
+
@Override
|
|
539
|
+
public void setNonNullParameter(PreparedStatement ps, int i,
|
|
540
|
+
OrderStatus parameter, JdbcType jdbcType) throws SQLException {
|
|
541
|
+
ps.setObject(i, parameter.getDisplayName(), Types.OTHER);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
@Override
|
|
545
|
+
public OrderStatus getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
|
546
|
+
String value = rs.getString(columnName);
|
|
547
|
+
return value == null ? null : OrderStatus.fromDisplayName(value);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
@Override
|
|
551
|
+
public OrderStatus getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
|
552
|
+
String value = rs.getString(columnIndex);
|
|
553
|
+
return value == null ? null : OrderStatus.fromDisplayName(value);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
@Override
|
|
557
|
+
public OrderStatus getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
|
558
|
+
String value = cs.getString(columnIndex);
|
|
559
|
+
return value == null ? null : OrderStatus.fromDisplayName(value);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
</details>
|
|
565
|
+
|
|
566
|
+
### TDD:受注の登録と取得
|
|
567
|
+
|
|
568
|
+
#### Red: 失敗するテストを書く
|
|
569
|
+
|
|
570
|
+
<details>
|
|
571
|
+
<summary>SalesOrderRepositoryTest.java</summary>
|
|
572
|
+
|
|
573
|
+
```java
|
|
574
|
+
// src/test/java/com/example/sms/infrastructure/persistence/repository/SalesOrderRepositoryTest.java
|
|
575
|
+
package com.example.sms.infrastructure.persistence.repository;
|
|
576
|
+
|
|
577
|
+
import com.example.sms.application.port.out.SalesOrderRepository;
|
|
578
|
+
import com.example.sms.domain.model.product.TaxCategory;
|
|
579
|
+
import com.example.sms.domain.model.sales.*;
|
|
580
|
+
import com.example.sms.testsetup.BaseIntegrationTest;
|
|
581
|
+
import org.junit.jupiter.api.*;
|
|
582
|
+
import org.springframework.beans.factory.annotation.Autowired;
|
|
583
|
+
|
|
584
|
+
import java.math.BigDecimal;
|
|
585
|
+
import java.time.LocalDate;
|
|
586
|
+
|
|
587
|
+
import static org.assertj.core.api.Assertions.*;
|
|
588
|
+
|
|
589
|
+
@DisplayName("受注リポジトリ")
|
|
590
|
+
class SalesOrderRepositoryTest extends BaseIntegrationTest {
|
|
591
|
+
|
|
592
|
+
@Autowired
|
|
593
|
+
private SalesOrderRepository salesOrderRepository;
|
|
594
|
+
|
|
595
|
+
@BeforeEach
|
|
596
|
+
void setUp() {
|
|
597
|
+
salesOrderRepository.deleteAll();
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
@Nested
|
|
601
|
+
@DisplayName("受注の登録")
|
|
602
|
+
class OrderRegistration {
|
|
603
|
+
|
|
604
|
+
@Test
|
|
605
|
+
@DisplayName("受注を登録できる")
|
|
606
|
+
void canRegisterOrder() {
|
|
607
|
+
// Arrange
|
|
608
|
+
var order = SalesOrder.builder()
|
|
609
|
+
.orderNumber("SO-2025-0001")
|
|
610
|
+
.orderDate(LocalDate.of(2025, 1, 20))
|
|
611
|
+
.customerCode("CUST-001")
|
|
612
|
+
.requestedDeliveryDate(LocalDate.of(2025, 1, 30))
|
|
613
|
+
.subtotal(new BigDecimal("50000"))
|
|
614
|
+
.taxAmount(new BigDecimal("5000"))
|
|
615
|
+
.totalAmount(new BigDecimal("55000"))
|
|
616
|
+
.status(OrderStatus.RECEIVED)
|
|
617
|
+
.build();
|
|
618
|
+
|
|
619
|
+
// Act
|
|
620
|
+
salesOrderRepository.save(order);
|
|
621
|
+
|
|
622
|
+
// Assert
|
|
623
|
+
var result = salesOrderRepository.findByOrderNumber("SO-2025-0001");
|
|
624
|
+
assertThat(result).isPresent();
|
|
625
|
+
assertThat(result.get().getOrderNumber()).isEqualTo("SO-2025-0001");
|
|
626
|
+
assertThat(result.get().getCustomerCode()).isEqualTo("CUST-001");
|
|
627
|
+
assertThat(result.get().getTotalAmount()).isEqualByComparingTo(new BigDecimal("55000"));
|
|
628
|
+
assertThat(result.get().getStatus()).isEqualTo(OrderStatus.RECEIVED);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
@Test
|
|
632
|
+
@DisplayName("受注明細を登録できる")
|
|
633
|
+
void canRegisterOrderWithDetails() {
|
|
634
|
+
// Arrange
|
|
635
|
+
var order = SalesOrder.builder()
|
|
636
|
+
.orderNumber("SO-2025-0002")
|
|
637
|
+
.orderDate(LocalDate.of(2025, 1, 20))
|
|
638
|
+
.customerCode("CUST-001")
|
|
639
|
+
.subtotal(new BigDecimal("10000"))
|
|
640
|
+
.taxAmount(new BigDecimal("1000"))
|
|
641
|
+
.totalAmount(new BigDecimal("11000"))
|
|
642
|
+
.status(OrderStatus.RECEIVED)
|
|
643
|
+
.build();
|
|
644
|
+
salesOrderRepository.save(order);
|
|
645
|
+
|
|
646
|
+
var detail = SalesOrderDetail.builder()
|
|
647
|
+
.orderId(order.getId())
|
|
648
|
+
.lineNumber(1)
|
|
649
|
+
.productCode("PROD-001")
|
|
650
|
+
.productName("テスト商品A")
|
|
651
|
+
.orderQuantity(new BigDecimal("10"))
|
|
652
|
+
.allocatedQuantity(BigDecimal.ZERO)
|
|
653
|
+
.shippedQuantity(BigDecimal.ZERO)
|
|
654
|
+
.remainingQuantity(new BigDecimal("10"))
|
|
655
|
+
.unit("個")
|
|
656
|
+
.unitPrice(new BigDecimal("1000"))
|
|
657
|
+
.amount(new BigDecimal("10000"))
|
|
658
|
+
.taxCategory(TaxCategory.EXCLUSIVE)
|
|
659
|
+
.taxRate(new BigDecimal("10.00"))
|
|
660
|
+
.taxAmount(new BigDecimal("1000"))
|
|
661
|
+
.warehouseCode("WH-001")
|
|
662
|
+
.build();
|
|
663
|
+
|
|
664
|
+
// Act
|
|
665
|
+
salesOrderRepository.saveDetail(detail);
|
|
666
|
+
|
|
667
|
+
// Assert
|
|
668
|
+
var result = salesOrderRepository.findByIdWithDetails(order.getId());
|
|
669
|
+
assertThat(result).isPresent();
|
|
670
|
+
assertThat(result.get().getDetails()).hasSize(1);
|
|
671
|
+
assertThat(result.get().getDetails().get(0).getProductCode()).isEqualTo("PROD-001");
|
|
672
|
+
assertThat(result.get().getDetails().get(0).getOrderQuantity())
|
|
673
|
+
.isEqualByComparingTo(new BigDecimal("10"));
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
@Test
|
|
677
|
+
@DisplayName("受注明細の数量を更新できる")
|
|
678
|
+
void canUpdateDetailQuantities() {
|
|
679
|
+
// Arrange
|
|
680
|
+
var order = SalesOrder.builder()
|
|
681
|
+
.orderNumber("SO-2025-0003")
|
|
682
|
+
.orderDate(LocalDate.of(2025, 1, 20))
|
|
683
|
+
.customerCode("CUST-001")
|
|
684
|
+
.subtotal(new BigDecimal("10000"))
|
|
685
|
+
.taxAmount(new BigDecimal("1000"))
|
|
686
|
+
.totalAmount(new BigDecimal("11000"))
|
|
687
|
+
.status(OrderStatus.RECEIVED)
|
|
688
|
+
.build();
|
|
689
|
+
salesOrderRepository.save(order);
|
|
690
|
+
|
|
691
|
+
var detail = SalesOrderDetail.builder()
|
|
692
|
+
.orderId(order.getId())
|
|
693
|
+
.lineNumber(1)
|
|
694
|
+
.productCode("PROD-001")
|
|
695
|
+
.productName("テスト商品A")
|
|
696
|
+
.orderQuantity(new BigDecimal("10"))
|
|
697
|
+
.allocatedQuantity(BigDecimal.ZERO)
|
|
698
|
+
.shippedQuantity(BigDecimal.ZERO)
|
|
699
|
+
.remainingQuantity(new BigDecimal("10"))
|
|
700
|
+
.unit("個")
|
|
701
|
+
.unitPrice(new BigDecimal("1000"))
|
|
702
|
+
.amount(new BigDecimal("10000"))
|
|
703
|
+
.taxCategory(TaxCategory.EXCLUSIVE)
|
|
704
|
+
.taxRate(new BigDecimal("10.00"))
|
|
705
|
+
.taxAmount(new BigDecimal("1000"))
|
|
706
|
+
.build();
|
|
707
|
+
salesOrderRepository.saveDetail(detail);
|
|
708
|
+
|
|
709
|
+
// Act: 5個引当
|
|
710
|
+
salesOrderRepository.updateDetailQuantities(
|
|
711
|
+
detail.getId(),
|
|
712
|
+
new BigDecimal("5"), // 引当数量
|
|
713
|
+
BigDecimal.ZERO, // 出荷数量
|
|
714
|
+
new BigDecimal("5") // 残数量
|
|
715
|
+
);
|
|
716
|
+
|
|
717
|
+
// Assert
|
|
718
|
+
var result = salesOrderRepository.findByIdWithDetails(order.getId());
|
|
719
|
+
assertThat(result).isPresent();
|
|
720
|
+
var updatedDetail = result.get().getDetails().get(0);
|
|
721
|
+
assertThat(updatedDetail.getAllocatedQuantity()).isEqualByComparingTo(new BigDecimal("5"));
|
|
722
|
+
assertThat(updatedDetail.getRemainingQuantity()).isEqualByComparingTo(new BigDecimal("5"));
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
@Nested
|
|
727
|
+
@DisplayName("受注の検索")
|
|
728
|
+
class OrderSearch {
|
|
729
|
+
|
|
730
|
+
@Test
|
|
731
|
+
@DisplayName("納期範囲で受注を検索できる")
|
|
732
|
+
void canSearchByDeliveryDateRange() {
|
|
733
|
+
// Arrange
|
|
734
|
+
var order1 = SalesOrder.builder()
|
|
735
|
+
.orderNumber("SO-2025-0004")
|
|
736
|
+
.orderDate(LocalDate.of(2025, 1, 15))
|
|
737
|
+
.customerCode("CUST-001")
|
|
738
|
+
.requestedDeliveryDate(LocalDate.of(2025, 1, 25))
|
|
739
|
+
.subtotal(BigDecimal.ZERO)
|
|
740
|
+
.taxAmount(BigDecimal.ZERO)
|
|
741
|
+
.totalAmount(BigDecimal.ZERO)
|
|
742
|
+
.status(OrderStatus.RECEIVED)
|
|
743
|
+
.build();
|
|
744
|
+
salesOrderRepository.save(order1);
|
|
745
|
+
|
|
746
|
+
var order2 = SalesOrder.builder()
|
|
747
|
+
.orderNumber("SO-2025-0005")
|
|
748
|
+
.orderDate(LocalDate.of(2025, 1, 20))
|
|
749
|
+
.customerCode("CUST-001")
|
|
750
|
+
.requestedDeliveryDate(LocalDate.of(2025, 2, 15))
|
|
751
|
+
.subtotal(BigDecimal.ZERO)
|
|
752
|
+
.taxAmount(BigDecimal.ZERO)
|
|
753
|
+
.totalAmount(BigDecimal.ZERO)
|
|
754
|
+
.status(OrderStatus.RECEIVED)
|
|
755
|
+
.build();
|
|
756
|
+
salesOrderRepository.save(order2);
|
|
757
|
+
|
|
758
|
+
// Act: 1月中の納期で検索
|
|
759
|
+
var januaryOrders = salesOrderRepository.findByDeliveryDateRange(
|
|
760
|
+
LocalDate.of(2025, 1, 1),
|
|
761
|
+
LocalDate.of(2025, 1, 31)
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
// Assert
|
|
765
|
+
assertThat(januaryOrders).hasSize(1);
|
|
766
|
+
assertThat(januaryOrders.get(0).getOrderNumber()).isEqualTo("SO-2025-0004");
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
</details>
|
|
773
|
+
|
|
774
|
+
#### Green: テストを通す実装
|
|
775
|
+
|
|
776
|
+
<details>
|
|
777
|
+
<summary>SalesOrderRepository.java</summary>
|
|
778
|
+
|
|
779
|
+
```java
|
|
780
|
+
// src/main/java/com/example/sms/application/port/out/SalesOrderRepository.java
|
|
781
|
+
package com.example.sms.application.port.out;
|
|
782
|
+
|
|
783
|
+
import com.example.sms.domain.model.sales.OrderStatus;
|
|
784
|
+
import com.example.sms.domain.model.sales.SalesOrder;
|
|
785
|
+
import com.example.sms.domain.model.sales.SalesOrderDetail;
|
|
786
|
+
|
|
787
|
+
import java.math.BigDecimal;
|
|
788
|
+
import java.time.LocalDate;
|
|
789
|
+
import java.util.List;
|
|
790
|
+
import java.util.Optional;
|
|
791
|
+
|
|
792
|
+
public interface SalesOrderRepository {
|
|
793
|
+
|
|
794
|
+
void save(SalesOrder order);
|
|
795
|
+
|
|
796
|
+
void saveDetail(SalesOrderDetail detail);
|
|
797
|
+
|
|
798
|
+
Optional<SalesOrder> findById(Integer id);
|
|
799
|
+
|
|
800
|
+
Optional<SalesOrder> findByOrderNumber(String orderNumber);
|
|
801
|
+
|
|
802
|
+
Optional<SalesOrder> findByIdWithDetails(Integer id);
|
|
803
|
+
|
|
804
|
+
List<SalesOrder> findByDeliveryDateRange(LocalDate from, LocalDate to);
|
|
805
|
+
|
|
806
|
+
void updateStatus(Integer id, OrderStatus status);
|
|
807
|
+
|
|
808
|
+
void updateDetailQuantities(Integer detailId,
|
|
809
|
+
BigDecimal allocatedQuantity,
|
|
810
|
+
BigDecimal shippedQuantity,
|
|
811
|
+
BigDecimal remainingQuantity);
|
|
812
|
+
|
|
813
|
+
void deleteAll();
|
|
814
|
+
}
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
</details>
|
|
818
|
+
|
|
819
|
+
<details>
|
|
820
|
+
<summary>SalesOrderMapper.xml</summary>
|
|
821
|
+
|
|
822
|
+
```xml
|
|
823
|
+
<?xml version="1.0" encoding="UTF-8" ?>
|
|
824
|
+
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|
825
|
+
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
826
|
+
|
|
827
|
+
<!-- src/main/resources/mapper/SalesOrderMapper.xml -->
|
|
828
|
+
<mapper namespace="com.example.sms.infrastructure.persistence.mapper.SalesOrderMapper">
|
|
829
|
+
|
|
830
|
+
<resultMap id="SalesOrderResultMap" type="com.example.sms.domain.model.sales.SalesOrder">
|
|
831
|
+
<id property="id" column="ID"/>
|
|
832
|
+
<result property="orderNumber" column="受注番号"/>
|
|
833
|
+
<result property="orderDate" column="受注日"/>
|
|
834
|
+
<result property="customerCode" column="顧客コード"/>
|
|
835
|
+
<result property="customerBranchNumber" column="顧客枝番"/>
|
|
836
|
+
<result property="shippingDestinationNumber" column="出荷先番号"/>
|
|
837
|
+
<result property="representativeCode" column="担当者コード"/>
|
|
838
|
+
<result property="requestedDeliveryDate" column="希望納期"/>
|
|
839
|
+
<result property="scheduledShippingDate" column="出荷予定日"/>
|
|
840
|
+
<result property="orderAmount" column="受注金額"/>
|
|
841
|
+
<result property="taxAmount" column="消費税額"/>
|
|
842
|
+
<result property="totalAmount" column="受注合計"/>
|
|
843
|
+
<result property="status" column="ステータス"
|
|
844
|
+
typeHandler="com.example.sms.infrastructure.out.persistence.typehandler.OrderStatusTypeHandler"/>
|
|
845
|
+
<result property="quotationId" column="見積ID"/>
|
|
846
|
+
<result property="customerOrderNumber" column="顧客注文番号"/>
|
|
847
|
+
<result property="remarks" column="備考"/>
|
|
848
|
+
<result property="createdAt" column="作成日時"/>
|
|
849
|
+
<result property="createdBy" column="作成者"/>
|
|
850
|
+
<result property="updatedAt" column="更新日時"/>
|
|
851
|
+
<result property="updatedBy" column="更新者"/>
|
|
852
|
+
<collection property="details" ofType="com.example.sms.domain.model.sales.SalesOrderDetail">
|
|
853
|
+
<id property="id" column="明細ID"/>
|
|
854
|
+
<result property="orderId" column="受注ID"/>
|
|
855
|
+
<result property="lineNumber" column="行番号"/>
|
|
856
|
+
<result property="productCode" column="商品コード"/>
|
|
857
|
+
<result property="productName" column="商品名"/>
|
|
858
|
+
<result property="orderQuantity" column="受注数量"/>
|
|
859
|
+
<result property="allocatedQuantity" column="引当数量"/>
|
|
860
|
+
<result property="shippedQuantity" column="出荷数量"/>
|
|
861
|
+
<result property="remainingQuantity" column="残数量"/>
|
|
862
|
+
<result property="unit" column="単位"/>
|
|
863
|
+
<result property="unitPrice" column="単価"/>
|
|
864
|
+
<result property="amount" column="金額"/>
|
|
865
|
+
<result property="taxCategory" column="税区分"
|
|
866
|
+
typeHandler="com.example.sms.infrastructure.out.persistence.typehandler.TaxCategoryTypeHandler"/>
|
|
867
|
+
<result property="taxRate" column="消費税率"/>
|
|
868
|
+
<result property="taxAmount" column="明細消費税額"/>
|
|
869
|
+
<result property="warehouseCode" column="倉庫コード"/>
|
|
870
|
+
<result property="requestedDeliveryDate" column="明細希望納期"/>
|
|
871
|
+
</collection>
|
|
872
|
+
</resultMap>
|
|
873
|
+
|
|
874
|
+
<insert id="insertHeader" parameterType="com.example.sms.domain.model.sales.SalesOrder"
|
|
875
|
+
useGeneratedKeys="true" keyProperty="id" keyColumn="ID">
|
|
876
|
+
INSERT INTO "受注データ" (
|
|
877
|
+
"受注番号", "受注日", "顧客コード", "顧客枝番", "出荷先番号",
|
|
878
|
+
"担当者コード", "希望納期", "出荷予定日", "受注金額", "消費税額",
|
|
879
|
+
"受注合計", "ステータス", "見積ID", "顧客注文番号", "備考",
|
|
880
|
+
"作成日時", "作成者", "更新日時", "更新者"
|
|
881
|
+
) VALUES (
|
|
882
|
+
#{orderNumber}, #{orderDate}, #{customerCode}, #{customerBranchNumber}, #{shippingDestinationNumber},
|
|
883
|
+
#{representativeCode}, #{requestedDeliveryDate}, #{scheduledShippingDate}, #{orderAmount}, #{taxAmount},
|
|
884
|
+
#{totalAmount},
|
|
885
|
+
#{status, typeHandler=com.example.sms.infrastructure.out.persistence.typehandler.OrderStatusTypeHandler}::受注ステータス,
|
|
886
|
+
#{quotationId}, #{customerOrderNumber}, #{remarks},
|
|
887
|
+
CURRENT_TIMESTAMP, #{createdBy}, CURRENT_TIMESTAMP, #{updatedBy}
|
|
888
|
+
)
|
|
889
|
+
</insert>
|
|
890
|
+
|
|
891
|
+
<insert id="insertDetail" useGeneratedKeys="true" keyProperty="id" keyColumn="ID">
|
|
892
|
+
INSERT INTO "受注明細" (
|
|
893
|
+
"受注ID", "行番号", "商品コード", "商品名", "受注数量",
|
|
894
|
+
"引当数量", "出荷数量", "残数量", "単位", "単価", "金額",
|
|
895
|
+
"税区分", "消費税率", "消費税額", "倉庫コード", "希望納期"
|
|
896
|
+
) VALUES (
|
|
897
|
+
#{orderId}, #{lineNumber}, #{productCode}, #{productName}, #{orderQuantity},
|
|
898
|
+
#{allocatedQuantity}, #{shippedQuantity}, #{remainingQuantity}, #{unit},
|
|
899
|
+
#{unitPrice}, #{amount},
|
|
900
|
+
#{taxCategory, typeHandler=com.example.sms.infrastructure.out.persistence.typehandler.TaxCategoryTypeHandler},
|
|
901
|
+
#{taxRate}, #{taxAmount}, #{warehouseCode}, #{requestedDeliveryDate}
|
|
902
|
+
)
|
|
903
|
+
</insert>
|
|
904
|
+
|
|
905
|
+
<select id="findByOrderNumber" resultMap="SalesOrderResultMap">
|
|
906
|
+
SELECT * FROM "受注データ" WHERE "受注番号" = #{orderNumber}
|
|
907
|
+
</select>
|
|
908
|
+
|
|
909
|
+
<select id="findDetailsByOrderId" resultMap="SalesOrderDetailResultMap">
|
|
910
|
+
SELECT * FROM "受注明細"
|
|
911
|
+
WHERE "受注ID" = #{orderId}
|
|
912
|
+
ORDER BY "行番号"
|
|
913
|
+
</select>
|
|
914
|
+
|
|
915
|
+
<select id="findByDeliveryDateRange" resultMap="SalesOrderResultMap">
|
|
916
|
+
SELECT * FROM "受注データ"
|
|
917
|
+
WHERE "希望納期" BETWEEN #{from} AND #{to}
|
|
918
|
+
ORDER BY "希望納期"
|
|
919
|
+
</select>
|
|
920
|
+
|
|
921
|
+
<update id="updateStatus">
|
|
922
|
+
UPDATE "受注データ"
|
|
923
|
+
SET "ステータス" = #{status, typeHandler=com.example.sms.infrastructure.out.persistence.typehandler.OrderStatusTypeHandler},
|
|
924
|
+
"更新日時" = CURRENT_TIMESTAMP
|
|
925
|
+
WHERE "ID" = #{id}
|
|
926
|
+
</update>
|
|
927
|
+
|
|
928
|
+
<update id="updateDetailQuantities">
|
|
929
|
+
UPDATE "受注明細"
|
|
930
|
+
SET "引当数量" = #{allocatedQuantity},
|
|
931
|
+
"出荷数量" = #{shippedQuantity},
|
|
932
|
+
"残数量" = #{remainingQuantity}
|
|
933
|
+
WHERE "ID" = #{detailId}
|
|
934
|
+
</update>
|
|
935
|
+
|
|
936
|
+
<delete id="deleteAll">
|
|
937
|
+
DELETE FROM "受注明細";
|
|
938
|
+
DELETE FROM "受注データ";
|
|
939
|
+
</delete>
|
|
940
|
+
|
|
941
|
+
</mapper>
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
</details>
|
|
945
|
+
|
|
946
|
+
---
|
|
947
|
+
|
|
948
|
+
## 6.2 出荷業務の DB 設計
|
|
949
|
+
|
|
950
|
+
### 出荷業務の流れ
|
|
951
|
+
|
|
952
|
+
出荷業務は、受注データを起点として商品を顧客に届けるまでのプロセスです。
|
|
953
|
+
|
|
954
|
+
```plantuml
|
|
955
|
+
@startuml
|
|
956
|
+
|
|
957
|
+
title 出荷業務フロー
|
|
958
|
+
|
|
959
|
+
|倉庫部門|
|
|
960
|
+
start
|
|
961
|
+
:受注データ確認;
|
|
962
|
+
:出荷指示作成;
|
|
963
|
+
|
|
964
|
+
:ピッキングリスト発行;
|
|
965
|
+
|
|
966
|
+
:ピッキング作業;
|
|
967
|
+
note right
|
|
968
|
+
倉庫から商品を
|
|
969
|
+
ピックアップ
|
|
970
|
+
end note
|
|
971
|
+
|
|
972
|
+
:検品・梱包;
|
|
973
|
+
|
|
974
|
+
:出荷;
|
|
975
|
+
note right
|
|
976
|
+
配送業者に引渡し
|
|
977
|
+
追跡番号取得
|
|
978
|
+
end note
|
|
979
|
+
|
|
980
|
+
|営業部門|
|
|
981
|
+
:売上計上;
|
|
982
|
+
:納品書発行;
|
|
983
|
+
|
|
984
|
+
stop
|
|
985
|
+
|
|
986
|
+
@enduml
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
### 出荷指示データ・出荷明細データ
|
|
990
|
+
|
|
991
|
+
#### 出荷関連テーブルの ER 図
|
|
992
|
+
|
|
993
|
+
```plantuml
|
|
994
|
+
@startuml
|
|
995
|
+
|
|
996
|
+
title 出荷関連テーブル
|
|
997
|
+
|
|
998
|
+
entity 受注データ {
|
|
999
|
+
ID <<PK>>
|
|
1000
|
+
--
|
|
1001
|
+
受注番号
|
|
1002
|
+
受注日
|
|
1003
|
+
顧客コード
|
|
1004
|
+
ステータス
|
|
1005
|
+
...
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
entity 受注明細 {
|
|
1009
|
+
ID <<PK>>
|
|
1010
|
+
--
|
|
1011
|
+
受注ID <<FK>>
|
|
1012
|
+
商品コード
|
|
1013
|
+
受注数量
|
|
1014
|
+
出荷数量
|
|
1015
|
+
残数量
|
|
1016
|
+
...
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
entity 出荷データ {
|
|
1020
|
+
ID <<PK>>
|
|
1021
|
+
--
|
|
1022
|
+
出荷番号 <<UK>>
|
|
1023
|
+
出荷日
|
|
1024
|
+
受注ID <<FK>>
|
|
1025
|
+
顧客コード <<FK>>
|
|
1026
|
+
顧客枝番 <<FK>>
|
|
1027
|
+
出荷先番号 <<FK>>
|
|
1028
|
+
出荷先名
|
|
1029
|
+
出荷先郵便番号
|
|
1030
|
+
出荷先住所1
|
|
1031
|
+
出荷先住所2
|
|
1032
|
+
担当者コード
|
|
1033
|
+
倉庫コード
|
|
1034
|
+
ステータス
|
|
1035
|
+
備考
|
|
1036
|
+
作成日時
|
|
1037
|
+
更新日時
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
entity 出荷明細 {
|
|
1041
|
+
ID <<PK>>
|
|
1042
|
+
--
|
|
1043
|
+
出荷ID <<FK>>
|
|
1044
|
+
行番号
|
|
1045
|
+
受注明細ID <<FK>>
|
|
1046
|
+
商品コード <<FK>>
|
|
1047
|
+
商品名
|
|
1048
|
+
出荷数量
|
|
1049
|
+
単位
|
|
1050
|
+
単価
|
|
1051
|
+
金額
|
|
1052
|
+
税区分
|
|
1053
|
+
消費税率
|
|
1054
|
+
消費税額
|
|
1055
|
+
倉庫コード
|
|
1056
|
+
備考
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
受注データ ||--o{ 受注明細
|
|
1060
|
+
受注データ ||--o{ 出荷データ
|
|
1061
|
+
出荷データ ||--o{ 出荷明細
|
|
1062
|
+
受注明細 ||--o{ 出荷明細
|
|
1063
|
+
|
|
1064
|
+
@enduml
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
### 出荷ステータスの定義
|
|
1068
|
+
|
|
1069
|
+
```plantuml
|
|
1070
|
+
@startuml
|
|
1071
|
+
|
|
1072
|
+
title 出荷ステータス遷移図
|
|
1073
|
+
|
|
1074
|
+
[*] --> 出荷指示済
|
|
1075
|
+
|
|
1076
|
+
出荷指示済 --> 出荷準備中 : 出荷準備開始
|
|
1077
|
+
出荷準備中 --> 出荷済 : 出荷完了
|
|
1078
|
+
|
|
1079
|
+
出荷済 --> [*]
|
|
1080
|
+
|
|
1081
|
+
出荷指示済 --> キャンセル
|
|
1082
|
+
出荷準備中 --> キャンセル
|
|
1083
|
+
|
|
1084
|
+
キャンセル --> [*]
|
|
1085
|
+
|
|
1086
|
+
@enduml
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
| ステータス | 説明 |
|
|
1090
|
+
|-----------|------|
|
|
1091
|
+
| **出荷指示済** | 出荷指示が作成された状態 |
|
|
1092
|
+
| **出荷準備中** | 出荷準備作業中の状態 |
|
|
1093
|
+
| **出荷済** | 配送業者に引き渡した状態 |
|
|
1094
|
+
| **キャンセル** | 出荷指示がキャンセルされた状態 |
|
|
1095
|
+
|
|
1096
|
+
### マイグレーション:出荷関連テーブルの作成
|
|
1097
|
+
|
|
1098
|
+
<details>
|
|
1099
|
+
<summary>V008__create_shipment_tables.sql</summary>
|
|
1100
|
+
|
|
1101
|
+
```sql
|
|
1102
|
+
-- src/main/resources/db/migration/V008__create_shipment_tables.sql
|
|
1103
|
+
|
|
1104
|
+
-- 出荷ステータス
|
|
1105
|
+
CREATE TYPE 出荷ステータス AS ENUM ('出荷指示済', '出荷準備中', '出荷済', 'キャンセル');
|
|
1106
|
+
|
|
1107
|
+
-- 出荷データ(ヘッダ)
|
|
1108
|
+
CREATE TABLE "出荷データ" (
|
|
1109
|
+
"ID" SERIAL PRIMARY KEY,
|
|
1110
|
+
"出荷番号" VARCHAR(20) UNIQUE NOT NULL,
|
|
1111
|
+
"出荷日" DATE NOT NULL,
|
|
1112
|
+
"受注ID" INTEGER NOT NULL,
|
|
1113
|
+
"顧客コード" VARCHAR(20) NOT NULL,
|
|
1114
|
+
"顧客枝番" VARCHAR(10) DEFAULT '00',
|
|
1115
|
+
"出荷先番号" VARCHAR(10),
|
|
1116
|
+
"出荷先名" VARCHAR(100),
|
|
1117
|
+
"出荷先郵便番号" VARCHAR(10),
|
|
1118
|
+
"出荷先住所1" VARCHAR(100),
|
|
1119
|
+
"出荷先住所2" VARCHAR(100),
|
|
1120
|
+
"担当者コード" VARCHAR(20),
|
|
1121
|
+
"倉庫コード" VARCHAR(20),
|
|
1122
|
+
"ステータス" 出荷ステータス DEFAULT '出荷指示済' NOT NULL,
|
|
1123
|
+
"備考" TEXT,
|
|
1124
|
+
"作成日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
1125
|
+
"作成者" VARCHAR(50),
|
|
1126
|
+
"更新日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
1127
|
+
"更新者" VARCHAR(50),
|
|
1128
|
+
CONSTRAINT "fk_出荷データ_受注"
|
|
1129
|
+
FOREIGN KEY ("受注ID") REFERENCES "受注データ"("ID"),
|
|
1130
|
+
CONSTRAINT "fk_出荷データ_顧客"
|
|
1131
|
+
FOREIGN KEY ("顧客コード", "顧客枝番") REFERENCES "顧客マスタ"("顧客コード", "顧客枝番"),
|
|
1132
|
+
CONSTRAINT "fk_出荷データ_出荷先"
|
|
1133
|
+
FOREIGN KEY ("顧客コード", "顧客枝番", "出荷先番号") REFERENCES "出荷先マスタ"("取引先コード", "顧客枝番", "出荷先番号")
|
|
1134
|
+
);
|
|
1135
|
+
|
|
1136
|
+
-- 出荷明細
|
|
1137
|
+
CREATE TABLE "出荷明細" (
|
|
1138
|
+
"ID" SERIAL PRIMARY KEY,
|
|
1139
|
+
"出荷ID" INTEGER NOT NULL,
|
|
1140
|
+
"行番号" INTEGER NOT NULL,
|
|
1141
|
+
"受注明細ID" INTEGER NOT NULL,
|
|
1142
|
+
"商品コード" VARCHAR(20) NOT NULL,
|
|
1143
|
+
"商品名" VARCHAR(100) NOT NULL,
|
|
1144
|
+
"出荷数量" DECIMAL(15, 2) NOT NULL,
|
|
1145
|
+
"単位" VARCHAR(10),
|
|
1146
|
+
"単価" DECIMAL(15, 2) NOT NULL,
|
|
1147
|
+
"金額" DECIMAL(15, 2) NOT NULL,
|
|
1148
|
+
"税区分" 税区分 DEFAULT '外税' NOT NULL,
|
|
1149
|
+
"消費税率" DECIMAL(5, 2) DEFAULT 10.00 NOT NULL,
|
|
1150
|
+
"消費税額" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
|
|
1151
|
+
"倉庫コード" VARCHAR(20),
|
|
1152
|
+
"備考" TEXT,
|
|
1153
|
+
CONSTRAINT "fk_出荷明細_出荷"
|
|
1154
|
+
FOREIGN KEY ("出荷ID") REFERENCES "出荷データ"("ID") ON DELETE CASCADE,
|
|
1155
|
+
CONSTRAINT "fk_出荷明細_受注明細"
|
|
1156
|
+
FOREIGN KEY ("受注明細ID") REFERENCES "受注明細"("ID"),
|
|
1157
|
+
CONSTRAINT "fk_出荷明細_商品"
|
|
1158
|
+
FOREIGN KEY ("商品コード") REFERENCES "商品マスタ"("商品コード"),
|
|
1159
|
+
CONSTRAINT "uq_出荷明細_行番号" UNIQUE ("出荷ID", "行番号")
|
|
1160
|
+
);
|
|
1161
|
+
|
|
1162
|
+
-- インデックス
|
|
1163
|
+
CREATE INDEX "idx_出荷データ_受注ID" ON "出荷データ"("受注ID");
|
|
1164
|
+
CREATE INDEX "idx_出荷データ_顧客コード" ON "出荷データ"("顧客コード");
|
|
1165
|
+
CREATE INDEX "idx_出荷データ_出荷日" ON "出荷データ"("出荷日");
|
|
1166
|
+
CREATE INDEX "idx_出荷データ_ステータス" ON "出荷データ"("ステータス");
|
|
1167
|
+
```
|
|
1168
|
+
|
|
1169
|
+
</details>
|
|
1170
|
+
|
|
1171
|
+
### 出荷ステータス Enum
|
|
1172
|
+
|
|
1173
|
+
<details>
|
|
1174
|
+
<summary>ShipmentStatus.java</summary>
|
|
1175
|
+
|
|
1176
|
+
```java
|
|
1177
|
+
// src/main/java/com/example/sms/domain/model/shipping/ShipmentStatus.java
|
|
1178
|
+
package com.example.sms.domain.model.shipping;
|
|
1179
|
+
|
|
1180
|
+
import lombok.Getter;
|
|
1181
|
+
import lombok.RequiredArgsConstructor;
|
|
1182
|
+
|
|
1183
|
+
@Getter
|
|
1184
|
+
@RequiredArgsConstructor
|
|
1185
|
+
public enum ShipmentStatus {
|
|
1186
|
+
INSTRUCTED("出荷指示済"),
|
|
1187
|
+
PREPARING("出荷準備中"),
|
|
1188
|
+
SHIPPED("出荷済"),
|
|
1189
|
+
CANCELLED("キャンセル");
|
|
1190
|
+
|
|
1191
|
+
private final String displayName;
|
|
1192
|
+
|
|
1193
|
+
public static ShipmentStatus fromDisplayName(String displayName) {
|
|
1194
|
+
for (ShipmentStatus status : values()) {
|
|
1195
|
+
if (status.displayName.equals(displayName)) {
|
|
1196
|
+
return status;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
throw new IllegalArgumentException("Unknown shipment status: " + displayName);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
```
|
|
1203
|
+
|
|
1204
|
+
</details>
|
|
1205
|
+
|
|
1206
|
+
### 出荷エンティティ
|
|
1207
|
+
|
|
1208
|
+
<details>
|
|
1209
|
+
<summary>Shipment.java</summary>
|
|
1210
|
+
|
|
1211
|
+
```java
|
|
1212
|
+
// src/main/java/com/example/sms/domain/model/shipping/Shipment.java
|
|
1213
|
+
package com.example.sms.domain.model.shipping;
|
|
1214
|
+
|
|
1215
|
+
import lombok.AllArgsConstructor;
|
|
1216
|
+
import lombok.Builder;
|
|
1217
|
+
import lombok.Data;
|
|
1218
|
+
import lombok.NoArgsConstructor;
|
|
1219
|
+
|
|
1220
|
+
import java.time.LocalDate;
|
|
1221
|
+
import java.time.LocalDateTime;
|
|
1222
|
+
import java.util.List;
|
|
1223
|
+
|
|
1224
|
+
@Data
|
|
1225
|
+
@Builder
|
|
1226
|
+
@NoArgsConstructor
|
|
1227
|
+
@AllArgsConstructor
|
|
1228
|
+
@SuppressWarnings("PMD.RedundantFieldInitializer")
|
|
1229
|
+
public class Shipment {
|
|
1230
|
+
private Integer id;
|
|
1231
|
+
private String shipmentNumber;
|
|
1232
|
+
private LocalDate shipmentDate;
|
|
1233
|
+
private Integer orderId;
|
|
1234
|
+
private String customerCode;
|
|
1235
|
+
private String customerBranchNumber;
|
|
1236
|
+
private String shippingDestinationNumber;
|
|
1237
|
+
private String shippingDestinationName;
|
|
1238
|
+
private String shippingDestinationPostalCode;
|
|
1239
|
+
private String shippingDestinationAddress1;
|
|
1240
|
+
private String shippingDestinationAddress2;
|
|
1241
|
+
private String representativeCode;
|
|
1242
|
+
private String warehouseCode;
|
|
1243
|
+
@Builder.Default
|
|
1244
|
+
private ShipmentStatus status = ShipmentStatus.INSTRUCTED;
|
|
1245
|
+
private String remarks;
|
|
1246
|
+
private LocalDateTime createdAt;
|
|
1247
|
+
private String createdBy;
|
|
1248
|
+
private LocalDateTime updatedAt;
|
|
1249
|
+
private String updatedBy;
|
|
1250
|
+
private List<ShipmentDetail> details;
|
|
1251
|
+
}
|
|
1252
|
+
```
|
|
1253
|
+
|
|
1254
|
+
</details>
|
|
1255
|
+
|
|
1256
|
+
<details>
|
|
1257
|
+
<summary>ShipmentDetail.java</summary>
|
|
1258
|
+
|
|
1259
|
+
```java
|
|
1260
|
+
// src/main/java/com/example/sms/domain/model/shipping/ShipmentDetail.java
|
|
1261
|
+
package com.example.sms.domain.model.shipping;
|
|
1262
|
+
|
|
1263
|
+
import com.example.sms.domain.model.product.TaxCategory;
|
|
1264
|
+
import lombok.AllArgsConstructor;
|
|
1265
|
+
import lombok.Builder;
|
|
1266
|
+
import lombok.Data;
|
|
1267
|
+
import lombok.NoArgsConstructor;
|
|
1268
|
+
|
|
1269
|
+
import java.math.BigDecimal;
|
|
1270
|
+
|
|
1271
|
+
@Data
|
|
1272
|
+
@Builder
|
|
1273
|
+
@NoArgsConstructor
|
|
1274
|
+
@AllArgsConstructor
|
|
1275
|
+
public class ShipmentDetail {
|
|
1276
|
+
private Integer id;
|
|
1277
|
+
private Integer shipmentId;
|
|
1278
|
+
private Integer lineNumber;
|
|
1279
|
+
private Integer orderDetailId;
|
|
1280
|
+
private String productCode;
|
|
1281
|
+
private String productName;
|
|
1282
|
+
private BigDecimal shippedQuantity;
|
|
1283
|
+
private String unit;
|
|
1284
|
+
private BigDecimal unitPrice;
|
|
1285
|
+
private BigDecimal amount;
|
|
1286
|
+
private TaxCategory taxCategory;
|
|
1287
|
+
private BigDecimal taxRate;
|
|
1288
|
+
private BigDecimal taxAmount;
|
|
1289
|
+
private String warehouseCode;
|
|
1290
|
+
private String remarks;
|
|
1291
|
+
}
|
|
1292
|
+
```
|
|
1293
|
+
|
|
1294
|
+
</details>
|
|
1295
|
+
|
|
1296
|
+
### TDD:出荷指示の登録と取得
|
|
1297
|
+
|
|
1298
|
+
<details>
|
|
1299
|
+
<summary>ShipmentInstructionRepositoryTest.java</summary>
|
|
1300
|
+
|
|
1301
|
+
```java
|
|
1302
|
+
// src/test/java/com/example/sms/infrastructure/persistence/repository/ShipmentInstructionRepositoryTest.java
|
|
1303
|
+
package com.example.sms.infrastructure.persistence.repository;
|
|
1304
|
+
|
|
1305
|
+
import com.example.sms.application.port.out.SalesOrderRepository;
|
|
1306
|
+
import com.example.sms.application.port.out.ShipmentInstructionRepository;
|
|
1307
|
+
import com.example.sms.domain.model.sales.*;
|
|
1308
|
+
import com.example.sms.testsetup.BaseIntegrationTest;
|
|
1309
|
+
import org.junit.jupiter.api.*;
|
|
1310
|
+
import org.springframework.beans.factory.annotation.Autowired;
|
|
1311
|
+
|
|
1312
|
+
import java.math.BigDecimal;
|
|
1313
|
+
import java.time.LocalDate;
|
|
1314
|
+
|
|
1315
|
+
import static org.assertj.core.api.Assertions.*;
|
|
1316
|
+
|
|
1317
|
+
@DisplayName("出荷指示リポジトリ")
|
|
1318
|
+
class ShipmentInstructionRepositoryTest extends BaseIntegrationTest {
|
|
1319
|
+
|
|
1320
|
+
@Autowired
|
|
1321
|
+
private ShipmentInstructionRepository shipmentInstructionRepository;
|
|
1322
|
+
|
|
1323
|
+
@Autowired
|
|
1324
|
+
private SalesOrderRepository salesOrderRepository;
|
|
1325
|
+
|
|
1326
|
+
@BeforeEach
|
|
1327
|
+
void setUp() {
|
|
1328
|
+
shipmentInstructionRepository.deleteAll();
|
|
1329
|
+
salesOrderRepository.deleteAll();
|
|
1330
|
+
setupOrderData();
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
private void setupOrderData() {
|
|
1334
|
+
// 受注データ準備
|
|
1335
|
+
var order = SalesOrder.builder()
|
|
1336
|
+
.orderNumber("SO-2025-0001")
|
|
1337
|
+
.orderDate(LocalDate.of(2025, 1, 20))
|
|
1338
|
+
.customerCode("CUST-001")
|
|
1339
|
+
.requestedDeliveryDate(LocalDate.of(2025, 1, 30))
|
|
1340
|
+
.subtotal(new BigDecimal("50000"))
|
|
1341
|
+
.taxAmount(new BigDecimal("5000"))
|
|
1342
|
+
.totalAmount(new BigDecimal("55000"))
|
|
1343
|
+
.status(OrderStatus.RECEIVED)
|
|
1344
|
+
.build();
|
|
1345
|
+
salesOrderRepository.save(order);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
@Nested
|
|
1349
|
+
@DisplayName("出荷指示の登録")
|
|
1350
|
+
class ShipmentInstructionRegistration {
|
|
1351
|
+
|
|
1352
|
+
@Test
|
|
1353
|
+
@DisplayName("出荷指示を登録できる")
|
|
1354
|
+
void canRegisterShipmentInstruction() {
|
|
1355
|
+
// Arrange
|
|
1356
|
+
var order = salesOrderRepository.findByOrderNumber("SO-2025-0001").get();
|
|
1357
|
+
var instruction = ShipmentInstruction.builder()
|
|
1358
|
+
.instructionNumber("SI-2025-0001")
|
|
1359
|
+
.instructionDate(LocalDate.of(2025, 1, 25))
|
|
1360
|
+
.orderId(order.getId())
|
|
1361
|
+
.customerCode("CUST-001")
|
|
1362
|
+
.scheduledShipDate(LocalDate.of(2025, 1, 28))
|
|
1363
|
+
.warehouseCode("WH-001")
|
|
1364
|
+
.status(ShipmentStatus.INSTRUCTED)
|
|
1365
|
+
.build();
|
|
1366
|
+
|
|
1367
|
+
// Act
|
|
1368
|
+
shipmentInstructionRepository.save(instruction);
|
|
1369
|
+
|
|
1370
|
+
// Assert
|
|
1371
|
+
var result = shipmentInstructionRepository.findByInstructionNumber("SI-2025-0001");
|
|
1372
|
+
assertThat(result).isPresent();
|
|
1373
|
+
assertThat(result.get().getOrderId()).isEqualTo(order.getId());
|
|
1374
|
+
assertThat(result.get().getStatus()).isEqualTo(ShipmentStatus.INSTRUCTED);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
@Test
|
|
1378
|
+
@DisplayName("出荷指示ステータスを更新できる")
|
|
1379
|
+
void canUpdateShipmentStatus() {
|
|
1380
|
+
// Arrange
|
|
1381
|
+
var order = salesOrderRepository.findByOrderNumber("SO-2025-0001").get();
|
|
1382
|
+
var instruction = ShipmentInstruction.builder()
|
|
1383
|
+
.instructionNumber("SI-2025-0002")
|
|
1384
|
+
.instructionDate(LocalDate.of(2025, 1, 25))
|
|
1385
|
+
.orderId(order.getId())
|
|
1386
|
+
.customerCode("CUST-001")
|
|
1387
|
+
.scheduledShipDate(LocalDate.of(2025, 1, 28))
|
|
1388
|
+
.warehouseCode("WH-001")
|
|
1389
|
+
.status(ShipmentStatus.INSTRUCTED)
|
|
1390
|
+
.build();
|
|
1391
|
+
shipmentInstructionRepository.save(instruction);
|
|
1392
|
+
|
|
1393
|
+
// Act: ピッキング開始
|
|
1394
|
+
shipmentInstructionRepository.updateStatus(instruction.getId(), ShipmentStatus.PICKING);
|
|
1395
|
+
|
|
1396
|
+
// Assert
|
|
1397
|
+
var result = shipmentInstructionRepository.findById(instruction.getId());
|
|
1398
|
+
assertThat(result).isPresent();
|
|
1399
|
+
assertThat(result.get().getStatus()).isEqualTo(ShipmentStatus.PICKING);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
@Test
|
|
1403
|
+
@DisplayName("追跡番号を登録できる")
|
|
1404
|
+
void canRegisterTrackingNumber() {
|
|
1405
|
+
// Arrange
|
|
1406
|
+
var order = salesOrderRepository.findByOrderNumber("SO-2025-0001").get();
|
|
1407
|
+
var instruction = ShipmentInstruction.builder()
|
|
1408
|
+
.instructionNumber("SI-2025-0003")
|
|
1409
|
+
.instructionDate(LocalDate.of(2025, 1, 25))
|
|
1410
|
+
.orderId(order.getId())
|
|
1411
|
+
.customerCode("CUST-001")
|
|
1412
|
+
.scheduledShipDate(LocalDate.of(2025, 1, 28))
|
|
1413
|
+
.warehouseCode("WH-001")
|
|
1414
|
+
.status(ShipmentStatus.SHIPPED)
|
|
1415
|
+
.carrierCode("YAMATO")
|
|
1416
|
+
.trackingNumber("1234-5678-9012")
|
|
1417
|
+
.build();
|
|
1418
|
+
|
|
1419
|
+
// Act
|
|
1420
|
+
shipmentInstructionRepository.save(instruction);
|
|
1421
|
+
|
|
1422
|
+
// Assert
|
|
1423
|
+
var result = shipmentInstructionRepository.findByInstructionNumber("SI-2025-0003");
|
|
1424
|
+
assertThat(result).isPresent();
|
|
1425
|
+
assertThat(result.get().getCarrierCode()).isEqualTo("YAMATO");
|
|
1426
|
+
assertThat(result.get().getTrackingNumber()).isEqualTo("1234-5678-9012");
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
```
|
|
1431
|
+
|
|
1432
|
+
</details>
|
|
1433
|
+
|
|
1434
|
+
### 受注から出荷指示を作成するサービス
|
|
1435
|
+
|
|
1436
|
+
<details>
|
|
1437
|
+
<summary>ShipmentService.java</summary>
|
|
1438
|
+
|
|
1439
|
+
```java
|
|
1440
|
+
// src/main/java/com/example/sms/application/service/ShipmentService.java
|
|
1441
|
+
package com.example.sms.application.service;
|
|
1442
|
+
|
|
1443
|
+
import com.example.sms.application.port.out.*;
|
|
1444
|
+
import com.example.sms.domain.model.sales.*;
|
|
1445
|
+
import lombok.RequiredArgsConstructor;
|
|
1446
|
+
import org.springframework.stereotype.Service;
|
|
1447
|
+
import org.springframework.transaction.annotation.Transactional;
|
|
1448
|
+
|
|
1449
|
+
import java.math.BigDecimal;
|
|
1450
|
+
import java.time.LocalDate;
|
|
1451
|
+
|
|
1452
|
+
@Service
|
|
1453
|
+
@RequiredArgsConstructor
|
|
1454
|
+
public class ShipmentService {
|
|
1455
|
+
|
|
1456
|
+
private final SalesOrderRepository salesOrderRepository;
|
|
1457
|
+
private final ShipmentInstructionRepository shipmentInstructionRepository;
|
|
1458
|
+
|
|
1459
|
+
/**
|
|
1460
|
+
* 受注から出荷指示を作成する
|
|
1461
|
+
*/
|
|
1462
|
+
@Transactional
|
|
1463
|
+
public ShipmentInstruction createShipmentInstruction(Integer orderId,
|
|
1464
|
+
LocalDate scheduledShipDate,
|
|
1465
|
+
String warehouseCode) {
|
|
1466
|
+
var order = salesOrderRepository.findByIdWithDetails(orderId)
|
|
1467
|
+
.orElseThrow(() -> new IllegalArgumentException("Order not found: " + orderId));
|
|
1468
|
+
|
|
1469
|
+
if (order.getStatus() != OrderStatus.RECEIVED && order.getStatus() != OrderStatus.ALLOCATED) {
|
|
1470
|
+
throw new IllegalStateException("Cannot create shipment instruction for order with status: "
|
|
1471
|
+
+ order.getStatus());
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// 出荷指示ヘッダ作成
|
|
1475
|
+
var instruction = ShipmentInstruction.builder()
|
|
1476
|
+
.instructionNumber(generateInstructionNumber(LocalDate.now()))
|
|
1477
|
+
.instructionDate(LocalDate.now())
|
|
1478
|
+
.orderId(orderId)
|
|
1479
|
+
.customerCode(order.getCustomerCode())
|
|
1480
|
+
.shipToCode(order.getShipToCode())
|
|
1481
|
+
.scheduledShipDate(scheduledShipDate)
|
|
1482
|
+
.warehouseCode(warehouseCode)
|
|
1483
|
+
.status(ShipmentStatus.INSTRUCTED)
|
|
1484
|
+
.build();
|
|
1485
|
+
shipmentInstructionRepository.save(instruction);
|
|
1486
|
+
|
|
1487
|
+
// 出荷指示明細作成
|
|
1488
|
+
int lineNumber = 1;
|
|
1489
|
+
for (var orderDetail : order.getDetails()) {
|
|
1490
|
+
if (orderDetail.getRemainingQuantity().compareTo(BigDecimal.ZERO) > 0) {
|
|
1491
|
+
var instructionDetail = ShipmentInstructionDetail.builder()
|
|
1492
|
+
.shipmentInstructionId(instruction.getId())
|
|
1493
|
+
.lineNumber(lineNumber++)
|
|
1494
|
+
.orderDetailId(orderDetail.getId())
|
|
1495
|
+
.productCode(orderDetail.getProductCode())
|
|
1496
|
+
.productName(orderDetail.getProductName())
|
|
1497
|
+
.instructedQuantity(orderDetail.getRemainingQuantity())
|
|
1498
|
+
.shippedQuantity(BigDecimal.ZERO)
|
|
1499
|
+
.unit(orderDetail.getUnit())
|
|
1500
|
+
.build();
|
|
1501
|
+
shipmentInstructionRepository.saveDetail(instructionDetail);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// 受注ステータスを更新
|
|
1506
|
+
salesOrderRepository.updateStatus(orderId, OrderStatus.SHIPMENT_INSTRUCTED);
|
|
1507
|
+
|
|
1508
|
+
return instruction;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
private String generateInstructionNumber(LocalDate date) {
|
|
1512
|
+
return String.format("SI-%d-%04d", date.getYear(), System.currentTimeMillis() % 10000);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
```
|
|
1516
|
+
|
|
1517
|
+
</details>
|
|
1518
|
+
|
|
1519
|
+
---
|
|
1520
|
+
|
|
1521
|
+
## 6.3 売上業務の DB 設計
|
|
1522
|
+
|
|
1523
|
+
### 売上データ・売上明細データの構造
|
|
1524
|
+
|
|
1525
|
+
#### 売上関連テーブルの ER 図
|
|
1526
|
+
|
|
1527
|
+
```plantuml
|
|
1528
|
+
@startuml
|
|
1529
|
+
|
|
1530
|
+
title 売上関連テーブル
|
|
1531
|
+
|
|
1532
|
+
entity 出荷データ {
|
|
1533
|
+
ID <<PK>>
|
|
1534
|
+
--
|
|
1535
|
+
出荷番号
|
|
1536
|
+
受注ID <<FK>>
|
|
1537
|
+
ステータス
|
|
1538
|
+
...
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
entity 売上データ {
|
|
1542
|
+
ID <<PK>>
|
|
1543
|
+
--
|
|
1544
|
+
売上番号 <<UK>>
|
|
1545
|
+
売上日
|
|
1546
|
+
受注ID <<FK>>
|
|
1547
|
+
出荷ID <<FK>>
|
|
1548
|
+
顧客コード <<FK>>
|
|
1549
|
+
顧客枝番 <<FK>>
|
|
1550
|
+
担当者コード
|
|
1551
|
+
売上金額
|
|
1552
|
+
消費税額
|
|
1553
|
+
売上合計
|
|
1554
|
+
ステータス
|
|
1555
|
+
請求ID
|
|
1556
|
+
備考
|
|
1557
|
+
作成日時
|
|
1558
|
+
更新日時
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
entity 売上明細 {
|
|
1562
|
+
ID <<PK>>
|
|
1563
|
+
--
|
|
1564
|
+
売上ID <<FK>>
|
|
1565
|
+
行番号
|
|
1566
|
+
受注明細ID <<FK>>
|
|
1567
|
+
出荷明細ID <<FK>>
|
|
1568
|
+
商品コード <<FK>>
|
|
1569
|
+
商品名
|
|
1570
|
+
売上数量
|
|
1571
|
+
単位
|
|
1572
|
+
単価
|
|
1573
|
+
金額
|
|
1574
|
+
税区分
|
|
1575
|
+
消費税率
|
|
1576
|
+
消費税額
|
|
1577
|
+
備考
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
entity 返品データ {
|
|
1581
|
+
ID <<PK>>
|
|
1582
|
+
--
|
|
1583
|
+
返品番号 <<UK>>
|
|
1584
|
+
返品日
|
|
1585
|
+
売上ID <<FK>>
|
|
1586
|
+
顧客コード <<FK>>
|
|
1587
|
+
返品理由
|
|
1588
|
+
返品金額
|
|
1589
|
+
消費税額
|
|
1590
|
+
返品合計
|
|
1591
|
+
倉庫コード
|
|
1592
|
+
備考
|
|
1593
|
+
作成日時
|
|
1594
|
+
更新日時
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
entity 返品明細 {
|
|
1598
|
+
ID <<PK>>
|
|
1599
|
+
--
|
|
1600
|
+
返品ID <<FK>>
|
|
1601
|
+
行番号
|
|
1602
|
+
売上明細ID <<FK>>
|
|
1603
|
+
商品コード <<FK>>
|
|
1604
|
+
商品名
|
|
1605
|
+
返品数量
|
|
1606
|
+
単位
|
|
1607
|
+
単価
|
|
1608
|
+
金額
|
|
1609
|
+
税区分
|
|
1610
|
+
消費税率
|
|
1611
|
+
消費税額
|
|
1612
|
+
備考
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
出荷データ ||--o| 売上データ
|
|
1616
|
+
売上データ ||--o{ 売上明細
|
|
1617
|
+
売上データ ||--o{ 返品データ
|
|
1618
|
+
返品データ ||--o{ 返品明細
|
|
1619
|
+
|
|
1620
|
+
@enduml
|
|
1621
|
+
```
|
|
1622
|
+
|
|
1623
|
+
### 出荷完了から売上計上への流れ
|
|
1624
|
+
|
|
1625
|
+
```plantuml
|
|
1626
|
+
@startuml
|
|
1627
|
+
|
|
1628
|
+
title 出荷完了から売上計上への流れ
|
|
1629
|
+
|
|
1630
|
+
|倉庫部門|
|
|
1631
|
+
start
|
|
1632
|
+
:出荷完了報告;
|
|
1633
|
+
note right
|
|
1634
|
+
配送業者に引渡し
|
|
1635
|
+
追跡番号登録
|
|
1636
|
+
end note
|
|
1637
|
+
|
|
1638
|
+
|システム|
|
|
1639
|
+
:出荷ステータス更新;
|
|
1640
|
+
note right
|
|
1641
|
+
出荷済に変更
|
|
1642
|
+
end note
|
|
1643
|
+
|
|
1644
|
+
:売上データ自動生成;
|
|
1645
|
+
note right
|
|
1646
|
+
出荷指示データを元に
|
|
1647
|
+
売上ヘッダ・明細を作成
|
|
1648
|
+
end note
|
|
1649
|
+
|
|
1650
|
+
:受注明細の出荷数量更新;
|
|
1651
|
+
|
|
1652
|
+
if (全明細出荷完了?) then (yes)
|
|
1653
|
+
:受注ステータスを出荷済に更新;
|
|
1654
|
+
else (no)
|
|
1655
|
+
:一部出荷として継続;
|
|
1656
|
+
endif
|
|
1657
|
+
|
|
1658
|
+
|営業部門|
|
|
1659
|
+
:納品書発行;
|
|
1660
|
+
|
|
1661
|
+
stop
|
|
1662
|
+
|
|
1663
|
+
@enduml
|
|
1664
|
+
```
|
|
1665
|
+
|
|
1666
|
+
### 売上ステータスの定義
|
|
1667
|
+
|
|
1668
|
+
| ステータス | 説明 |
|
|
1669
|
+
|-----------|------|
|
|
1670
|
+
| **計上済** | 売上が計上された状態 |
|
|
1671
|
+
| **請求済** | 請求書が発行された状態 |
|
|
1672
|
+
| **入金済** | 入金が確認された状態 |
|
|
1673
|
+
| **取消** | 売上が取り消された状態 |
|
|
1674
|
+
|
|
1675
|
+
### マイグレーション:売上関連テーブルの作成
|
|
1676
|
+
|
|
1677
|
+
<details>
|
|
1678
|
+
<summary>V009__create_sales_tables.sql</summary>
|
|
1679
|
+
|
|
1680
|
+
```sql
|
|
1681
|
+
-- src/main/resources/db/migration/V009__create_sales_tables.sql
|
|
1682
|
+
|
|
1683
|
+
-- 売上ステータス
|
|
1684
|
+
CREATE TYPE 売上ステータス AS ENUM ('計上済', '請求済', '入金済', 'キャンセル');
|
|
1685
|
+
|
|
1686
|
+
-- 売上データ(ヘッダ)
|
|
1687
|
+
CREATE TABLE "売上データ" (
|
|
1688
|
+
"ID" SERIAL PRIMARY KEY,
|
|
1689
|
+
"売上番号" VARCHAR(20) UNIQUE NOT NULL,
|
|
1690
|
+
"売上日" DATE NOT NULL,
|
|
1691
|
+
"受注ID" INTEGER NOT NULL,
|
|
1692
|
+
"出荷ID" INTEGER,
|
|
1693
|
+
"顧客コード" VARCHAR(20) NOT NULL,
|
|
1694
|
+
"顧客枝番" VARCHAR(10) DEFAULT '00',
|
|
1695
|
+
"担当者コード" VARCHAR(20),
|
|
1696
|
+
"売上金額" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
|
|
1697
|
+
"消費税額" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
|
|
1698
|
+
"売上合計" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
|
|
1699
|
+
"ステータス" 売上ステータス DEFAULT '計上済' NOT NULL,
|
|
1700
|
+
"請求ID" INTEGER,
|
|
1701
|
+
"備考" TEXT,
|
|
1702
|
+
"作成日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
1703
|
+
"作成者" VARCHAR(50),
|
|
1704
|
+
"更新日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
1705
|
+
"更新者" VARCHAR(50),
|
|
1706
|
+
CONSTRAINT "fk_売上データ_受注"
|
|
1707
|
+
FOREIGN KEY ("受注ID") REFERENCES "受注データ"("ID"),
|
|
1708
|
+
CONSTRAINT "fk_売上データ_出荷"
|
|
1709
|
+
FOREIGN KEY ("出荷ID") REFERENCES "出荷データ"("ID"),
|
|
1710
|
+
CONSTRAINT "fk_売上データ_顧客"
|
|
1711
|
+
FOREIGN KEY ("顧客コード", "顧客枝番") REFERENCES "顧客マスタ"("顧客コード", "顧客枝番")
|
|
1712
|
+
);
|
|
1713
|
+
|
|
1714
|
+
-- 売上明細
|
|
1715
|
+
CREATE TABLE "売上明細" (
|
|
1716
|
+
"ID" SERIAL PRIMARY KEY,
|
|
1717
|
+
"売上ID" INTEGER NOT NULL,
|
|
1718
|
+
"行番号" INTEGER NOT NULL,
|
|
1719
|
+
"受注明細ID" INTEGER NOT NULL,
|
|
1720
|
+
"出荷明細ID" INTEGER,
|
|
1721
|
+
"商品コード" VARCHAR(20) NOT NULL,
|
|
1722
|
+
"商品名" VARCHAR(100) NOT NULL,
|
|
1723
|
+
"売上数量" DECIMAL(15, 2) NOT NULL,
|
|
1724
|
+
"単位" VARCHAR(10),
|
|
1725
|
+
"単価" DECIMAL(15, 2) NOT NULL,
|
|
1726
|
+
"金額" DECIMAL(15, 2) NOT NULL,
|
|
1727
|
+
"税区分" 税区分 DEFAULT '外税' NOT NULL,
|
|
1728
|
+
"消費税率" DECIMAL(5, 2) DEFAULT 10.00 NOT NULL,
|
|
1729
|
+
"消費税額" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
|
|
1730
|
+
"備考" TEXT,
|
|
1731
|
+
CONSTRAINT "fk_売上明細_売上"
|
|
1732
|
+
FOREIGN KEY ("売上ID") REFERENCES "売上データ"("ID") ON DELETE CASCADE,
|
|
1733
|
+
CONSTRAINT "fk_売上明細_受注明細"
|
|
1734
|
+
FOREIGN KEY ("受注明細ID") REFERENCES "受注明細"("ID"),
|
|
1735
|
+
CONSTRAINT "fk_売上明細_出荷明細"
|
|
1736
|
+
FOREIGN KEY ("出荷明細ID") REFERENCES "出荷明細"("ID"),
|
|
1737
|
+
CONSTRAINT "fk_売上明細_商品"
|
|
1738
|
+
FOREIGN KEY ("商品コード") REFERENCES "商品マスタ"("商品コード"),
|
|
1739
|
+
CONSTRAINT "uq_売上明細_行番号" UNIQUE ("売上ID", "行番号")
|
|
1740
|
+
);
|
|
1741
|
+
|
|
1742
|
+
-- インデックス
|
|
1743
|
+
CREATE INDEX "idx_売上データ_受注ID" ON "売上データ"("受注ID");
|
|
1744
|
+
CREATE INDEX "idx_売上データ_出荷ID" ON "売上データ"("出荷ID");
|
|
1745
|
+
CREATE INDEX "idx_売上データ_顧客コード" ON "売上データ"("顧客コード");
|
|
1746
|
+
CREATE INDEX "idx_売上データ_売上日" ON "売上データ"("売上日");
|
|
1747
|
+
CREATE INDEX "idx_売上データ_ステータス" ON "売上データ"("ステータス");
|
|
1748
|
+
```
|
|
1749
|
+
|
|
1750
|
+
</details>
|
|
1751
|
+
|
|
1752
|
+
### 売上ステータス Enum
|
|
1753
|
+
|
|
1754
|
+
<details>
|
|
1755
|
+
<summary>SalesStatus.java</summary>
|
|
1756
|
+
|
|
1757
|
+
```java
|
|
1758
|
+
// src/main/java/com/example/sms/domain/model/sales/SalesStatus.java
|
|
1759
|
+
package com.example.sms.domain.model.sales;
|
|
1760
|
+
|
|
1761
|
+
import lombok.Getter;
|
|
1762
|
+
import lombok.RequiredArgsConstructor;
|
|
1763
|
+
|
|
1764
|
+
@Getter
|
|
1765
|
+
@RequiredArgsConstructor
|
|
1766
|
+
public enum SalesStatus {
|
|
1767
|
+
RECORDED("計上済"),
|
|
1768
|
+
INVOICED("請求済"),
|
|
1769
|
+
COLLECTED("入金済"),
|
|
1770
|
+
CANCELLED("キャンセル");
|
|
1771
|
+
|
|
1772
|
+
private final String displayName;
|
|
1773
|
+
|
|
1774
|
+
public static SalesStatus fromDisplayName(String displayName) {
|
|
1775
|
+
for (SalesStatus status : values()) {
|
|
1776
|
+
if (status.displayName.equals(displayName)) {
|
|
1777
|
+
return status;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
throw new IllegalArgumentException("Unknown sales status: " + displayName);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
```
|
|
1784
|
+
|
|
1785
|
+
</details>
|
|
1786
|
+
|
|
1787
|
+
### 売上エンティティ
|
|
1788
|
+
|
|
1789
|
+
<details>
|
|
1790
|
+
<summary>Sales.java</summary>
|
|
1791
|
+
|
|
1792
|
+
```java
|
|
1793
|
+
// src/main/java/com/example/sms/domain/model/sales/Sales.java
|
|
1794
|
+
package com.example.sms.domain.model.sales;
|
|
1795
|
+
|
|
1796
|
+
import lombok.AllArgsConstructor;
|
|
1797
|
+
import lombok.Builder;
|
|
1798
|
+
import lombok.Data;
|
|
1799
|
+
import lombok.NoArgsConstructor;
|
|
1800
|
+
|
|
1801
|
+
import java.math.BigDecimal;
|
|
1802
|
+
import java.time.LocalDate;
|
|
1803
|
+
import java.time.LocalDateTime;
|
|
1804
|
+
import java.util.ArrayList;
|
|
1805
|
+
import java.util.List;
|
|
1806
|
+
|
|
1807
|
+
@Data
|
|
1808
|
+
@Builder
|
|
1809
|
+
@NoArgsConstructor
|
|
1810
|
+
@AllArgsConstructor
|
|
1811
|
+
@SuppressWarnings("PMD.RedundantFieldInitializer")
|
|
1812
|
+
public class Sales {
|
|
1813
|
+
private Integer id;
|
|
1814
|
+
private String salesNumber;
|
|
1815
|
+
private LocalDate salesDate;
|
|
1816
|
+
private Integer orderId;
|
|
1817
|
+
private Integer shipmentId;
|
|
1818
|
+
private String customerCode;
|
|
1819
|
+
private String customerBranchNumber;
|
|
1820
|
+
private String representativeCode;
|
|
1821
|
+
@Builder.Default
|
|
1822
|
+
private BigDecimal salesAmount = BigDecimal.ZERO;
|
|
1823
|
+
@Builder.Default
|
|
1824
|
+
private BigDecimal taxAmount = BigDecimal.ZERO;
|
|
1825
|
+
@Builder.Default
|
|
1826
|
+
private BigDecimal totalAmount = BigDecimal.ZERO;
|
|
1827
|
+
@Builder.Default
|
|
1828
|
+
private SalesStatus status = SalesStatus.RECORDED;
|
|
1829
|
+
private Integer invoiceId;
|
|
1830
|
+
private String remarks;
|
|
1831
|
+
private LocalDateTime createdAt;
|
|
1832
|
+
private String createdBy;
|
|
1833
|
+
private LocalDateTime updatedAt;
|
|
1834
|
+
private String updatedBy;
|
|
1835
|
+
|
|
1836
|
+
@Builder.Default
|
|
1837
|
+
private List<SalesDetail> details = new ArrayList<>();
|
|
1838
|
+
}
|
|
1839
|
+
```
|
|
1840
|
+
|
|
1841
|
+
</details>
|
|
1842
|
+
|
|
1843
|
+
<details>
|
|
1844
|
+
<summary>SalesDetail.java</summary>
|
|
1845
|
+
|
|
1846
|
+
```java
|
|
1847
|
+
// src/main/java/com/example/sms/domain/model/sales/SalesDetail.java
|
|
1848
|
+
package com.example.sms.domain.model.sales;
|
|
1849
|
+
|
|
1850
|
+
import com.example.sms.domain.model.product.TaxCategory;
|
|
1851
|
+
import lombok.AllArgsConstructor;
|
|
1852
|
+
import lombok.Builder;
|
|
1853
|
+
import lombok.Data;
|
|
1854
|
+
import lombok.NoArgsConstructor;
|
|
1855
|
+
|
|
1856
|
+
import java.math.BigDecimal;
|
|
1857
|
+
|
|
1858
|
+
@Data
|
|
1859
|
+
@Builder
|
|
1860
|
+
@NoArgsConstructor
|
|
1861
|
+
@AllArgsConstructor
|
|
1862
|
+
public class SalesDetail {
|
|
1863
|
+
private Integer id;
|
|
1864
|
+
private Integer salesId;
|
|
1865
|
+
private Integer lineNumber;
|
|
1866
|
+
private Integer orderDetailId;
|
|
1867
|
+
private Integer shipmentDetailId;
|
|
1868
|
+
private String productCode;
|
|
1869
|
+
private String productName;
|
|
1870
|
+
private BigDecimal salesQuantity;
|
|
1871
|
+
private String unit;
|
|
1872
|
+
private BigDecimal unitPrice;
|
|
1873
|
+
private BigDecimal amount;
|
|
1874
|
+
private TaxCategory taxCategory;
|
|
1875
|
+
private BigDecimal taxRate;
|
|
1876
|
+
private BigDecimal taxAmount;
|
|
1877
|
+
private String remarks;
|
|
1878
|
+
}
|
|
1879
|
+
```
|
|
1880
|
+
|
|
1881
|
+
</details>
|
|
1882
|
+
|
|
1883
|
+
### 返品処理の設計
|
|
1884
|
+
|
|
1885
|
+
返品処理は、売上データに対してマイナスの取引を記録する処理です。
|
|
1886
|
+
|
|
1887
|
+
#### 返品業務の流れ
|
|
1888
|
+
|
|
1889
|
+
```plantuml
|
|
1890
|
+
@startuml
|
|
1891
|
+
|
|
1892
|
+
title 返品業務フロー
|
|
1893
|
+
|
|
1894
|
+
|営業担当|
|
|
1895
|
+
start
|
|
1896
|
+
:返品依頼受付;
|
|
1897
|
+
note right
|
|
1898
|
+
顧客からの返品依頼
|
|
1899
|
+
理由の確認
|
|
1900
|
+
end note
|
|
1901
|
+
|
|
1902
|
+
:返品データ登録;
|
|
1903
|
+
|
|
1904
|
+
|倉庫担当|
|
|
1905
|
+
:返品商品受入;
|
|
1906
|
+
:検品;
|
|
1907
|
+
|
|
1908
|
+
if (商品状態OK?) then (yes)
|
|
1909
|
+
:在庫戻入;
|
|
1910
|
+
else (no)
|
|
1911
|
+
:廃棄処理;
|
|
1912
|
+
endif
|
|
1913
|
+
|
|
1914
|
+
|経理担当|
|
|
1915
|
+
:売上取消処理;
|
|
1916
|
+
note right
|
|
1917
|
+
返品金額分の
|
|
1918
|
+
売上をマイナス計上
|
|
1919
|
+
end note
|
|
1920
|
+
|
|
1921
|
+
stop
|
|
1922
|
+
|
|
1923
|
+
@enduml
|
|
1924
|
+
```
|
|
1925
|
+
|
|
1926
|
+
### マイグレーション:返品関連テーブルの作成
|
|
1927
|
+
|
|
1928
|
+
<details>
|
|
1929
|
+
<summary>V009__create_return_tables.sql</summary>
|
|
1930
|
+
|
|
1931
|
+
```sql
|
|
1932
|
+
-- src/main/resources/db/migration/V009__create_return_tables.sql
|
|
1933
|
+
|
|
1934
|
+
-- 返品データ
|
|
1935
|
+
CREATE TABLE "返品データ" (
|
|
1936
|
+
"ID" SERIAL PRIMARY KEY,
|
|
1937
|
+
"返品番号" VARCHAR(20) UNIQUE NOT NULL,
|
|
1938
|
+
"返品日" DATE NOT NULL,
|
|
1939
|
+
"売上ID" INTEGER NOT NULL,
|
|
1940
|
+
"顧客コード" VARCHAR(20) NOT NULL,
|
|
1941
|
+
"返品理由" VARCHAR(200),
|
|
1942
|
+
"返品金額" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
|
|
1943
|
+
"消費税額" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
|
|
1944
|
+
"返品合計" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
|
|
1945
|
+
"倉庫コード" VARCHAR(20),
|
|
1946
|
+
"備考" TEXT,
|
|
1947
|
+
"作成日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
1948
|
+
"作成者" VARCHAR(50),
|
|
1949
|
+
"更新日時" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
1950
|
+
"更新者" VARCHAR(50),
|
|
1951
|
+
CONSTRAINT "fk_返品データ_売上"
|
|
1952
|
+
FOREIGN KEY ("売上ID") REFERENCES "売上データ"("ID"),
|
|
1953
|
+
CONSTRAINT "fk_返品データ_顧客"
|
|
1954
|
+
FOREIGN KEY ("顧客コード") REFERENCES "顧客マスタ"("顧客コード")
|
|
1955
|
+
);
|
|
1956
|
+
|
|
1957
|
+
-- 返品明細
|
|
1958
|
+
CREATE TABLE "返品明細" (
|
|
1959
|
+
"ID" SERIAL PRIMARY KEY,
|
|
1960
|
+
"返品ID" INTEGER NOT NULL,
|
|
1961
|
+
"行番号" INTEGER NOT NULL,
|
|
1962
|
+
"売上明細ID" INTEGER NOT NULL,
|
|
1963
|
+
"商品コード" VARCHAR(20) NOT NULL,
|
|
1964
|
+
"商品名" VARCHAR(100) NOT NULL,
|
|
1965
|
+
"返品数量" DECIMAL(15, 2) NOT NULL,
|
|
1966
|
+
"単位" VARCHAR(10),
|
|
1967
|
+
"単価" DECIMAL(15, 2) NOT NULL,
|
|
1968
|
+
"金額" DECIMAL(15, 2) NOT NULL,
|
|
1969
|
+
"税区分" 税区分 DEFAULT '外税' NOT NULL,
|
|
1970
|
+
"消費税率" DECIMAL(5, 2) DEFAULT 10.00 NOT NULL,
|
|
1971
|
+
"消費税額" DECIMAL(15, 2) DEFAULT 0 NOT NULL,
|
|
1972
|
+
"備考" TEXT,
|
|
1973
|
+
CONSTRAINT "fk_返品明細_返品"
|
|
1974
|
+
FOREIGN KEY ("返品ID") REFERENCES "返品データ"("ID") ON DELETE CASCADE,
|
|
1975
|
+
CONSTRAINT "fk_返品明細_売上明細"
|
|
1976
|
+
FOREIGN KEY ("売上明細ID") REFERENCES "売上明細"("ID"),
|
|
1977
|
+
CONSTRAINT "fk_返品明細_商品"
|
|
1978
|
+
FOREIGN KEY ("商品コード") REFERENCES "商品マスタ"("商品コード"),
|
|
1979
|
+
CONSTRAINT "uq_返品明細_行番号" UNIQUE ("返品ID", "行番号")
|
|
1980
|
+
);
|
|
1981
|
+
|
|
1982
|
+
-- インデックス
|
|
1983
|
+
CREATE INDEX "idx_返品データ_売上ID" ON "返品データ"("売上ID");
|
|
1984
|
+
CREATE INDEX "idx_返品データ_返品日" ON "返品データ"("返品日");
|
|
1985
|
+
```
|
|
1986
|
+
|
|
1987
|
+
</details>
|
|
1988
|
+
|
|
1989
|
+
### 赤黒処理の考え方
|
|
1990
|
+
|
|
1991
|
+
赤黒処理とは、誤った伝票を訂正するための会計処理手法です。
|
|
1992
|
+
|
|
1993
|
+
```plantuml
|
|
1994
|
+
@startuml
|
|
1995
|
+
|
|
1996
|
+
title 赤黒処理の流れ
|
|
1997
|
+
|
|
1998
|
+
participant "元伝票" as original
|
|
1999
|
+
participant "赤伝票" as red
|
|
2000
|
+
participant "黒伝票" as black
|
|
2001
|
+
|
|
2002
|
+
note over original
|
|
2003
|
+
誤った内容の伝票
|
|
2004
|
+
金額: +10,000円
|
|
2005
|
+
end note
|
|
2006
|
+
|
|
2007
|
+
original -> red : 取消処理
|
|
2008
|
+
note over red
|
|
2009
|
+
赤伝票(マイナス)
|
|
2010
|
+
金額: -10,000円
|
|
2011
|
+
end note
|
|
2012
|
+
|
|
2013
|
+
red -> black : 訂正処理
|
|
2014
|
+
note over black
|
|
2015
|
+
黒伝票(正しい内容)
|
|
2016
|
+
金額: +12,000円
|
|
2017
|
+
end note
|
|
2018
|
+
|
|
2019
|
+
note over original, black
|
|
2020
|
+
最終結果:
|
|
2021
|
+
10,000 - 10,000 + 12,000 = 12,000円
|
|
2022
|
+
end note
|
|
2023
|
+
|
|
2024
|
+
@enduml
|
|
2025
|
+
```
|
|
2026
|
+
|
|
2027
|
+
#### 赤黒処理の特徴
|
|
2028
|
+
|
|
2029
|
+
| 項目 | 説明 |
|
|
2030
|
+
|-----|------|
|
|
2031
|
+
| **元伝票** | 訂正対象となる誤った伝票 |
|
|
2032
|
+
| **赤伝票** | 元伝票をマイナスで打ち消す伝票 |
|
|
2033
|
+
| **黒伝票** | 正しい内容を記載した新しい伝票 |
|
|
2034
|
+
|
|
2035
|
+
#### 赤黒処理のメリット
|
|
2036
|
+
|
|
2037
|
+
- **監査証跡の保持**: 元の伝票を削除せず、訂正の履歴が残る
|
|
2038
|
+
- **会計基準への準拠**: 伝票の修正ではなく、新規伝票で訂正
|
|
2039
|
+
- **トレーサビリティ**: いつ、誰が、なぜ訂正したかを追跡可能
|
|
2040
|
+
|
|
2041
|
+
---
|
|
2042
|
+
|
|
2043
|
+
## 6.4 全体の ER 図
|
|
2044
|
+
|
|
2045
|
+
受注から売上までの全体的な関連を示します。
|
|
2046
|
+
|
|
2047
|
+
```plantuml
|
|
2048
|
+
@startuml
|
|
2049
|
+
|
|
2050
|
+
title 受注・出荷・売上の全体ER図
|
|
2051
|
+
|
|
2052
|
+
entity 顧客マスタ {
|
|
2053
|
+
顧客コード <<PK>>
|
|
2054
|
+
--
|
|
2055
|
+
顧客名
|
|
2056
|
+
...
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
entity 商品マスタ {
|
|
2060
|
+
商品コード <<PK>>
|
|
2061
|
+
--
|
|
2062
|
+
商品名
|
|
2063
|
+
単価
|
|
2064
|
+
...
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
entity 見積データ {
|
|
2068
|
+
ID <<PK>>
|
|
2069
|
+
見積番号 <<UK>>
|
|
2070
|
+
--
|
|
2071
|
+
見積日
|
|
2072
|
+
顧客コード <<FK>>
|
|
2073
|
+
ステータス
|
|
2074
|
+
...
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
entity 見積明細 {
|
|
2078
|
+
ID <<PK>>
|
|
2079
|
+
--
|
|
2080
|
+
見積ID <<FK>>
|
|
2081
|
+
商品コード <<FK>>
|
|
2082
|
+
数量
|
|
2083
|
+
単価
|
|
2084
|
+
...
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
entity 受注データ {
|
|
2088
|
+
ID <<PK>>
|
|
2089
|
+
受注番号 <<UK>>
|
|
2090
|
+
--
|
|
2091
|
+
受注日
|
|
2092
|
+
顧客コード <<FK>>
|
|
2093
|
+
見積ID <<FK>>
|
|
2094
|
+
ステータス
|
|
2095
|
+
...
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
entity 受注明細 {
|
|
2099
|
+
ID <<PK>>
|
|
2100
|
+
--
|
|
2101
|
+
受注ID <<FK>>
|
|
2102
|
+
商品コード <<FK>>
|
|
2103
|
+
受注数量
|
|
2104
|
+
引当数量
|
|
2105
|
+
出荷数量
|
|
2106
|
+
残数量
|
|
2107
|
+
...
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
entity 出荷指示データ {
|
|
2111
|
+
ID <<PK>>
|
|
2112
|
+
出荷指示番号 <<UK>>
|
|
2113
|
+
--
|
|
2114
|
+
受注ID <<FK>>
|
|
2115
|
+
顧客コード <<FK>>
|
|
2116
|
+
ステータス
|
|
2117
|
+
...
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
entity 出荷指示明細 {
|
|
2121
|
+
ID <<PK>>
|
|
2122
|
+
--
|
|
2123
|
+
出荷指示ID <<FK>>
|
|
2124
|
+
受注明細ID <<FK>>
|
|
2125
|
+
商品コード <<FK>>
|
|
2126
|
+
指示数量
|
|
2127
|
+
出荷数量
|
|
2128
|
+
...
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
entity 売上データ {
|
|
2132
|
+
ID <<PK>>
|
|
2133
|
+
売上番号 <<UK>>
|
|
2134
|
+
--
|
|
2135
|
+
受注ID <<FK>>
|
|
2136
|
+
出荷指示ID <<FK>>
|
|
2137
|
+
顧客コード <<FK>>
|
|
2138
|
+
ステータス
|
|
2139
|
+
...
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
entity 売上明細 {
|
|
2143
|
+
ID <<PK>>
|
|
2144
|
+
--
|
|
2145
|
+
売上ID <<FK>>
|
|
2146
|
+
受注明細ID <<FK>>
|
|
2147
|
+
商品コード <<FK>>
|
|
2148
|
+
売上数量
|
|
2149
|
+
原価
|
|
2150
|
+
粗利
|
|
2151
|
+
...
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
entity 返品データ {
|
|
2155
|
+
ID <<PK>>
|
|
2156
|
+
返品番号 <<UK>>
|
|
2157
|
+
--
|
|
2158
|
+
売上ID <<FK>>
|
|
2159
|
+
顧客コード <<FK>>
|
|
2160
|
+
返品理由
|
|
2161
|
+
...
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
entity 返品明細 {
|
|
2165
|
+
ID <<PK>>
|
|
2166
|
+
--
|
|
2167
|
+
返品ID <<FK>>
|
|
2168
|
+
売上明細ID <<FK>>
|
|
2169
|
+
商品コード <<FK>>
|
|
2170
|
+
返品数量
|
|
2171
|
+
...
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
顧客マスタ ||--o{ 見積データ
|
|
2175
|
+
顧客マスタ ||--o{ 受注データ
|
|
2176
|
+
顧客マスタ ||--o{ 出荷指示データ
|
|
2177
|
+
顧客マスタ ||--o{ 売上データ
|
|
2178
|
+
顧客マスタ ||--o{ 返品データ
|
|
2179
|
+
|
|
2180
|
+
商品マスタ ||--o{ 見積明細
|
|
2181
|
+
商品マスタ ||--o{ 受注明細
|
|
2182
|
+
商品マスタ ||--o{ 出荷指示明細
|
|
2183
|
+
商品マスタ ||--o{ 売上明細
|
|
2184
|
+
商品マスタ ||--o{ 返品明細
|
|
2185
|
+
|
|
2186
|
+
見積データ ||--o{ 見積明細
|
|
2187
|
+
見積データ ||--o| 受注データ : "受注化"
|
|
2188
|
+
|
|
2189
|
+
受注データ ||--o{ 受注明細
|
|
2190
|
+
受注データ ||--o{ 出荷指示データ
|
|
2191
|
+
受注データ ||--o{ 売上データ
|
|
2192
|
+
|
|
2193
|
+
受注明細 ||--o{ 出荷指示明細
|
|
2194
|
+
受注明細 ||--o{ 売上明細
|
|
2195
|
+
|
|
2196
|
+
出荷指示データ ||--o{ 出荷指示明細
|
|
2197
|
+
出荷指示データ ||--o| 売上データ
|
|
2198
|
+
|
|
2199
|
+
売上データ ||--o{ 売上明細
|
|
2200
|
+
売上データ ||--o{ 返品データ
|
|
2201
|
+
|
|
2202
|
+
売上明細 ||--o{ 返品明細
|
|
2203
|
+
|
|
2204
|
+
返品データ ||--o{ 返品明細
|
|
2205
|
+
|
|
2206
|
+
@enduml
|
|
2207
|
+
```
|
|
2208
|
+
|
|
2209
|
+
---
|
|
2210
|
+
|
|
2211
|
+
## 6.5 リレーションと楽観ロックの設計
|
|
2212
|
+
|
|
2213
|
+
### MyBatis ネストした ResultMap によるリレーション設定
|
|
2214
|
+
|
|
2215
|
+
受注データは、受注(ヘッダ)→ 受注明細の2層構造を持ちます。MyBatis でこの親子関係を効率的に取得するためのリレーション設定を実装します。
|
|
2216
|
+
|
|
2217
|
+
#### ネストした ResultMap の定義
|
|
2218
|
+
|
|
2219
|
+
<details>
|
|
2220
|
+
<summary>SalesOrderMapper.xml(リレーション設定)</summary>
|
|
2221
|
+
|
|
2222
|
+
```xml
|
|
2223
|
+
<?xml version="1.0" encoding="UTF-8" ?>
|
|
2224
|
+
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|
2225
|
+
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
2226
|
+
|
|
2227
|
+
<!-- src/main/resources/mapper/SalesOrderMapper.xml -->
|
|
2228
|
+
<mapper namespace="com.example.sms.infrastructure.persistence.mapper.SalesOrderMapper">
|
|
2229
|
+
|
|
2230
|
+
<!-- 受注(ヘッダ)の ResultMap -->
|
|
2231
|
+
<resultMap id="salesOrderWithDetailsResultMap" type="com.example.sms.domain.model.sales.SalesOrder">
|
|
2232
|
+
<id property="id" column="o_id"/>
|
|
2233
|
+
<result property="orderNumber" column="o_受注番号"/>
|
|
2234
|
+
<result property="orderDate" column="o_受注日"/>
|
|
2235
|
+
<result property="customerCode" column="o_顧客コード"/>
|
|
2236
|
+
<result property="shipToCode" column="o_出荷先コード"/>
|
|
2237
|
+
<result property="salesRepCode" column="o_担当者コード"/>
|
|
2238
|
+
<result property="requestedDeliveryDate" column="o_希望納期"/>
|
|
2239
|
+
<result property="scheduledShipDate" column="o_出荷予定日"/>
|
|
2240
|
+
<result property="subtotal" column="o_受注金額"/>
|
|
2241
|
+
<result property="taxAmount" column="o_消費税額"/>
|
|
2242
|
+
<result property="totalAmount" column="o_受注合計"/>
|
|
2243
|
+
<result property="status" column="o_ステータス"
|
|
2244
|
+
typeHandler="com.example.sms.infrastructure.out.persistence.typehandler.OrderStatusTypeHandler"/>
|
|
2245
|
+
<result property="version" column="o_バージョン"/>
|
|
2246
|
+
<result property="createdAt" column="o_作成日時"/>
|
|
2247
|
+
<result property="updatedAt" column="o_更新日時"/>
|
|
2248
|
+
<!-- 受注明細との1:N関連 -->
|
|
2249
|
+
<collection property="details" ofType="com.example.sms.domain.model.sales.SalesOrderDetail"
|
|
2250
|
+
resultMap="salesOrderDetailNestedResultMap"/>
|
|
2251
|
+
</resultMap>
|
|
2252
|
+
|
|
2253
|
+
<!-- 受注明細のネスト ResultMap -->
|
|
2254
|
+
<resultMap id="salesOrderDetailNestedResultMap" type="com.example.sms.domain.model.sales.SalesOrderDetail">
|
|
2255
|
+
<id property="id" column="d_id"/>
|
|
2256
|
+
<result property="orderId" column="d_受注ID"/>
|
|
2257
|
+
<result property="lineNumber" column="d_行番号"/>
|
|
2258
|
+
<result property="productCode" column="d_商品コード"/>
|
|
2259
|
+
<result property="productName" column="d_商品名"/>
|
|
2260
|
+
<result property="orderQuantity" column="d_受注数量"/>
|
|
2261
|
+
<result property="allocatedQuantity" column="d_引当数量"/>
|
|
2262
|
+
<result property="shippedQuantity" column="d_出荷数量"/>
|
|
2263
|
+
<result property="remainingQuantity" column="d_残数量"/>
|
|
2264
|
+
<result property="unit" column="d_単位"/>
|
|
2265
|
+
<result property="unitPrice" column="d_単価"/>
|
|
2266
|
+
<result property="amount" column="d_金額"/>
|
|
2267
|
+
<result property="taxCategory" column="d_税区分"
|
|
2268
|
+
typeHandler="com.example.sms.infrastructure.out.persistence.typehandler.TaxCategoryTypeHandler"/>
|
|
2269
|
+
<result property="taxRate" column="d_消費税率"/>
|
|
2270
|
+
<result property="taxAmount" column="d_消費税額"/>
|
|
2271
|
+
<result property="version" column="d_バージョン"/>
|
|
2272
|
+
</resultMap>
|
|
2273
|
+
|
|
2274
|
+
<!-- JOIN による一括取得クエリ -->
|
|
2275
|
+
<select id="findWithDetailsByOrderNumber" resultMap="salesOrderWithDetailsResultMap">
|
|
2276
|
+
SELECT
|
|
2277
|
+
o."ID" AS o_id,
|
|
2278
|
+
o."受注番号" AS o_受注番号,
|
|
2279
|
+
o."受注日" AS o_受注日,
|
|
2280
|
+
o."顧客コード" AS o_顧客コード,
|
|
2281
|
+
o."出荷先コード" AS o_出荷先コード,
|
|
2282
|
+
o."担当者コード" AS o_担当者コード,
|
|
2283
|
+
o."希望納期" AS o_希望納期,
|
|
2284
|
+
o."出荷予定日" AS o_出荷予定日,
|
|
2285
|
+
o."受注金額" AS o_受注金額,
|
|
2286
|
+
o."消費税額" AS o_消費税額,
|
|
2287
|
+
o."受注合計" AS o_受注合計,
|
|
2288
|
+
o."ステータス" AS o_ステータス,
|
|
2289
|
+
o."バージョン" AS o_バージョン,
|
|
2290
|
+
o."作成日時" AS o_作成日時,
|
|
2291
|
+
o."更新日時" AS o_更新日時,
|
|
2292
|
+
d."ID" AS d_id,
|
|
2293
|
+
d."受注ID" AS d_受注ID,
|
|
2294
|
+
d."行番号" AS d_行番号,
|
|
2295
|
+
d."商品コード" AS d_商品コード,
|
|
2296
|
+
d."商品名" AS d_商品名,
|
|
2297
|
+
d."受注数量" AS d_受注数量,
|
|
2298
|
+
d."引当数量" AS d_引当数量,
|
|
2299
|
+
d."出荷数量" AS d_出荷数量,
|
|
2300
|
+
d."残数量" AS d_残数量,
|
|
2301
|
+
d."単位" AS d_単位,
|
|
2302
|
+
d."単価" AS d_単価,
|
|
2303
|
+
d."金額" AS d_金額,
|
|
2304
|
+
d."税区分" AS d_税区分,
|
|
2305
|
+
d."消費税率" AS d_消費税率,
|
|
2306
|
+
d."消費税額" AS d_消費税額,
|
|
2307
|
+
d."バージョン" AS d_バージョン
|
|
2308
|
+
FROM "受注データ" o
|
|
2309
|
+
LEFT JOIN "受注明細" d
|
|
2310
|
+
ON o."ID" = d."受注ID"
|
|
2311
|
+
WHERE o."受注番号" = #{orderNumber}
|
|
2312
|
+
ORDER BY d."行番号"
|
|
2313
|
+
</select>
|
|
2314
|
+
|
|
2315
|
+
</mapper>
|
|
2316
|
+
```
|
|
2317
|
+
|
|
2318
|
+
</details>
|
|
2319
|
+
|
|
2320
|
+
#### リレーション設定のポイント
|
|
2321
|
+
|
|
2322
|
+
| 設定項目 | 説明 |
|
|
2323
|
+
|---------|------|
|
|
2324
|
+
| `<collection>` | 1:N 関連のマッピング |
|
|
2325
|
+
| `<id>` | 主キーの識別(MyBatis が重複排除に使用) |
|
|
2326
|
+
| `resultMap` | ネストした ResultMap の参照 |
|
|
2327
|
+
| エイリアス(AS) | カラム名の重複を避けるためのプレフィックス |
|
|
2328
|
+
| `ORDER BY` | コレクションの順序を保証 |
|
|
2329
|
+
|
|
2330
|
+
### 楽観ロックの実装
|
|
2331
|
+
|
|
2332
|
+
複数ユーザーが同時に受注データを編集する場合、データの整合性を保つために楽観ロック(Optimistic Locking)を実装します。
|
|
2333
|
+
|
|
2334
|
+
#### Flyway マイグレーション: バージョンカラム追加
|
|
2335
|
+
|
|
2336
|
+
<details>
|
|
2337
|
+
<summary>V010__add_version_columns.sql</summary>
|
|
2338
|
+
|
|
2339
|
+
```sql
|
|
2340
|
+
-- src/main/resources/db/migration/V010__add_version_columns.sql
|
|
2341
|
+
|
|
2342
|
+
-- 受注データテーブルにバージョンカラムを追加
|
|
2343
|
+
ALTER TABLE "受注データ" ADD COLUMN "バージョン" INTEGER DEFAULT 1 NOT NULL;
|
|
2344
|
+
|
|
2345
|
+
-- 受注明細テーブルにバージョンカラムを追加
|
|
2346
|
+
ALTER TABLE "受注明細" ADD COLUMN "バージョン" INTEGER DEFAULT 1 NOT NULL;
|
|
2347
|
+
|
|
2348
|
+
-- 出荷指示データテーブルにバージョンカラムを追加
|
|
2349
|
+
ALTER TABLE "出荷指示データ" ADD COLUMN "バージョン" INTEGER DEFAULT 1 NOT NULL;
|
|
2350
|
+
|
|
2351
|
+
-- 出荷指示明細テーブルにバージョンカラムを追加
|
|
2352
|
+
ALTER TABLE "出荷指示明細" ADD COLUMN "バージョン" INTEGER DEFAULT 1 NOT NULL;
|
|
2353
|
+
|
|
2354
|
+
-- 売上データテーブルにバージョンカラムを追加
|
|
2355
|
+
ALTER TABLE "売上データ" ADD COLUMN "バージョン" INTEGER DEFAULT 1 NOT NULL;
|
|
2356
|
+
|
|
2357
|
+
-- 売上明細テーブルにバージョンカラムを追加
|
|
2358
|
+
ALTER TABLE "売上明細" ADD COLUMN "バージョン" INTEGER DEFAULT 1 NOT NULL;
|
|
2359
|
+
|
|
2360
|
+
-- 返品データテーブルにバージョンカラムを追加
|
|
2361
|
+
ALTER TABLE "返品データ" ADD COLUMN "バージョン" INTEGER DEFAULT 1 NOT NULL;
|
|
2362
|
+
|
|
2363
|
+
-- 返品明細テーブルにバージョンカラムを追加
|
|
2364
|
+
ALTER TABLE "返品明細" ADD COLUMN "バージョン" INTEGER DEFAULT 1 NOT NULL;
|
|
2365
|
+
|
|
2366
|
+
-- コメント追加
|
|
2367
|
+
COMMENT ON COLUMN "受注データ"."バージョン" IS '楽観ロック用バージョン番号';
|
|
2368
|
+
COMMENT ON COLUMN "受注明細"."バージョン" IS '楽観ロック用バージョン番号';
|
|
2369
|
+
COMMENT ON COLUMN "出荷指示データ"."バージョン" IS '楽観ロック用バージョン番号';
|
|
2370
|
+
COMMENT ON COLUMN "出荷指示明細"."バージョン" IS '楽観ロック用バージョン番号';
|
|
2371
|
+
COMMENT ON COLUMN "売上データ"."バージョン" IS '楽観ロック用バージョン番号';
|
|
2372
|
+
COMMENT ON COLUMN "売上明細"."バージョン" IS '楽観ロック用バージョン番号';
|
|
2373
|
+
COMMENT ON COLUMN "返品データ"."バージョン" IS '楽観ロック用バージョン番号';
|
|
2374
|
+
COMMENT ON COLUMN "返品明細"."バージョン" IS '楽観ロック用バージョン番号';
|
|
2375
|
+
```
|
|
2376
|
+
|
|
2377
|
+
</details>
|
|
2378
|
+
|
|
2379
|
+
#### エンティティへのバージョンフィールド追加
|
|
2380
|
+
|
|
2381
|
+
<details>
|
|
2382
|
+
<summary>SalesOrder.java(バージョンフィールド追加)</summary>
|
|
2383
|
+
|
|
2384
|
+
```java
|
|
2385
|
+
// src/main/java/com/example/sms/domain/model/sales/SalesOrder.java
|
|
2386
|
+
package com.example.sms.domain.model.sales;
|
|
2387
|
+
|
|
2388
|
+
import lombok.AllArgsConstructor;
|
|
2389
|
+
import lombok.Builder;
|
|
2390
|
+
import lombok.Data;
|
|
2391
|
+
import lombok.NoArgsConstructor;
|
|
2392
|
+
|
|
2393
|
+
import java.math.BigDecimal;
|
|
2394
|
+
import java.time.LocalDate;
|
|
2395
|
+
import java.time.LocalDateTime;
|
|
2396
|
+
import java.util.ArrayList;
|
|
2397
|
+
import java.util.List;
|
|
2398
|
+
|
|
2399
|
+
@Data
|
|
2400
|
+
@Builder
|
|
2401
|
+
@NoArgsConstructor
|
|
2402
|
+
@AllArgsConstructor
|
|
2403
|
+
public class SalesOrder {
|
|
2404
|
+
private Integer id;
|
|
2405
|
+
private String orderNumber;
|
|
2406
|
+
private LocalDate orderDate;
|
|
2407
|
+
private String customerCode;
|
|
2408
|
+
private String shipToCode;
|
|
2409
|
+
private String salesRepCode;
|
|
2410
|
+
private LocalDate requestedDeliveryDate;
|
|
2411
|
+
private LocalDate scheduledShipDate;
|
|
2412
|
+
private BigDecimal subtotal;
|
|
2413
|
+
private BigDecimal taxAmount;
|
|
2414
|
+
private BigDecimal totalAmount;
|
|
2415
|
+
private OrderStatus status;
|
|
2416
|
+
private Integer quotationId;
|
|
2417
|
+
private String customerOrderNumber;
|
|
2418
|
+
private String remarks;
|
|
2419
|
+
private LocalDateTime createdAt;
|
|
2420
|
+
private String createdBy;
|
|
2421
|
+
private LocalDateTime updatedAt;
|
|
2422
|
+
private String updatedBy;
|
|
2423
|
+
|
|
2424
|
+
// 楽観ロック用バージョン
|
|
2425
|
+
@Builder.Default
|
|
2426
|
+
private Integer version = 1;
|
|
2427
|
+
|
|
2428
|
+
// リレーション
|
|
2429
|
+
@Builder.Default
|
|
2430
|
+
private List<SalesOrderDetail> details = new ArrayList<>();
|
|
2431
|
+
}
|
|
2432
|
+
```
|
|
2433
|
+
|
|
2434
|
+
</details>
|
|
2435
|
+
|
|
2436
|
+
#### MyBatis Mapper: 楽観ロック対応の更新
|
|
2437
|
+
|
|
2438
|
+
<details>
|
|
2439
|
+
<summary>SalesOrderMapper.xml(楽観ロック対応 UPDATE)</summary>
|
|
2440
|
+
|
|
2441
|
+
```xml
|
|
2442
|
+
<!-- 楽観ロック対応の更新(バージョンチェック付き) -->
|
|
2443
|
+
<update id="updateWithOptimisticLock" parameterType="com.example.sms.domain.model.sales.SalesOrder">
|
|
2444
|
+
UPDATE "受注データ"
|
|
2445
|
+
SET
|
|
2446
|
+
"受注日" = #{orderDate},
|
|
2447
|
+
"顧客コード" = #{customerCode},
|
|
2448
|
+
"出荷先コード" = #{shipToCode},
|
|
2449
|
+
"担当者コード" = #{salesRepCode},
|
|
2450
|
+
"希望納期" = #{requestedDeliveryDate},
|
|
2451
|
+
"出荷予定日" = #{scheduledShipDate},
|
|
2452
|
+
"受注金額" = #{subtotal},
|
|
2453
|
+
"消費税額" = #{taxAmount},
|
|
2454
|
+
"受注合計" = #{totalAmount},
|
|
2455
|
+
"ステータス" = #{status, typeHandler=com.example.sms.infrastructure.out.persistence.typehandler.OrderStatusTypeHandler},
|
|
2456
|
+
"更新日時" = CURRENT_TIMESTAMP,
|
|
2457
|
+
"バージョン" = "バージョン" + 1
|
|
2458
|
+
WHERE "ID" = #{id}
|
|
2459
|
+
AND "バージョン" = #{version}
|
|
2460
|
+
</update>
|
|
2461
|
+
```
|
|
2462
|
+
|
|
2463
|
+
</details>
|
|
2464
|
+
|
|
2465
|
+
#### 楽観ロック例外クラス
|
|
2466
|
+
|
|
2467
|
+
<details>
|
|
2468
|
+
<summary>OptimisticLockException.java</summary>
|
|
2469
|
+
|
|
2470
|
+
```java
|
|
2471
|
+
// src/main/java/com/example/sms/domain/exception/OptimisticLockException.java
|
|
2472
|
+
package com.example.sms.domain.exception;
|
|
2473
|
+
|
|
2474
|
+
public class OptimisticLockException extends RuntimeException {
|
|
2475
|
+
|
|
2476
|
+
private final String entityName;
|
|
2477
|
+
private final Integer entityId;
|
|
2478
|
+
private final Integer expectedVersion;
|
|
2479
|
+
private final Integer actualVersion;
|
|
2480
|
+
|
|
2481
|
+
public OptimisticLockException(String entityName, Integer entityId) {
|
|
2482
|
+
super(String.format("%s (ID: %d) は既に削除されています", entityName, entityId));
|
|
2483
|
+
this.entityName = entityName;
|
|
2484
|
+
this.entityId = entityId;
|
|
2485
|
+
this.expectedVersion = null;
|
|
2486
|
+
this.actualVersion = null;
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
public OptimisticLockException(String entityName, Integer entityId,
|
|
2490
|
+
Integer expectedVersion, Integer actualVersion) {
|
|
2491
|
+
super(String.format("%s (ID: %d) は他のユーザーによって更新されています。" +
|
|
2492
|
+
"期待バージョン: %d, 実際のバージョン: %d",
|
|
2493
|
+
entityName, entityId, expectedVersion, actualVersion));
|
|
2494
|
+
this.entityName = entityName;
|
|
2495
|
+
this.entityId = entityId;
|
|
2496
|
+
this.expectedVersion = expectedVersion;
|
|
2497
|
+
this.actualVersion = actualVersion;
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
public String getEntityName() {
|
|
2501
|
+
return entityName;
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
public Integer getEntityId() {
|
|
2505
|
+
return entityId;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
public Integer getExpectedVersion() {
|
|
2509
|
+
return expectedVersion;
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
public Integer getActualVersion() {
|
|
2513
|
+
return actualVersion;
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
```
|
|
2517
|
+
|
|
2518
|
+
</details>
|
|
2519
|
+
|
|
2520
|
+
#### Repository 実装: 楽観ロック対応
|
|
2521
|
+
|
|
2522
|
+
<details>
|
|
2523
|
+
<summary>SalesOrderRepositoryImpl.java(楽観ロック対応)</summary>
|
|
2524
|
+
|
|
2525
|
+
```java
|
|
2526
|
+
// src/main/java/com/example/sms/infrastructure/persistence/repository/SalesOrderRepositoryImpl.java
|
|
2527
|
+
package com.example.sms.infrastructure.persistence.repository;
|
|
2528
|
+
|
|
2529
|
+
import com.example.sms.application.port.out.SalesOrderRepository;
|
|
2530
|
+
import com.example.sms.domain.exception.OptimisticLockException;
|
|
2531
|
+
import com.example.sms.domain.model.sales.OrderStatus;
|
|
2532
|
+
import com.example.sms.domain.model.sales.SalesOrder;
|
|
2533
|
+
import com.example.sms.domain.model.sales.SalesOrderDetail;
|
|
2534
|
+
import com.example.sms.infrastructure.persistence.mapper.SalesOrderMapper;
|
|
2535
|
+
import lombok.RequiredArgsConstructor;
|
|
2536
|
+
import org.springframework.stereotype.Repository;
|
|
2537
|
+
import org.springframework.transaction.annotation.Transactional;
|
|
2538
|
+
|
|
2539
|
+
import java.math.BigDecimal;
|
|
2540
|
+
import java.time.LocalDate;
|
|
2541
|
+
import java.util.List;
|
|
2542
|
+
import java.util.Optional;
|
|
2543
|
+
|
|
2544
|
+
@Repository
|
|
2545
|
+
@RequiredArgsConstructor
|
|
2546
|
+
public class SalesOrderRepositoryImpl implements SalesOrderRepository {
|
|
2547
|
+
|
|
2548
|
+
private final SalesOrderMapper mapper;
|
|
2549
|
+
|
|
2550
|
+
@Override
|
|
2551
|
+
@Transactional
|
|
2552
|
+
public void update(SalesOrder order) {
|
|
2553
|
+
int updatedCount = mapper.updateWithOptimisticLock(order);
|
|
2554
|
+
|
|
2555
|
+
if (updatedCount == 0) {
|
|
2556
|
+
// バージョン不一致または削除済み
|
|
2557
|
+
Integer currentVersion = mapper.findVersionById(order.getId());
|
|
2558
|
+
if (currentVersion == null) {
|
|
2559
|
+
throw new OptimisticLockException("受注", order.getId());
|
|
2560
|
+
} else {
|
|
2561
|
+
throw new OptimisticLockException("受注", order.getId(),
|
|
2562
|
+
order.getVersion(), currentVersion);
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
@Override
|
|
2568
|
+
public Optional<SalesOrder> findByIdWithDetails(Integer id) {
|
|
2569
|
+
return Optional.ofNullable(mapper.findByIdWithDetails(id));
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
@Override
|
|
2573
|
+
public Optional<SalesOrder> findByOrderNumber(String orderNumber) {
|
|
2574
|
+
return Optional.ofNullable(mapper.findByOrderNumber(orderNumber));
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
// その他のメソッド...
|
|
2578
|
+
}
|
|
2579
|
+
```
|
|
2580
|
+
|
|
2581
|
+
</details>
|
|
2582
|
+
|
|
2583
|
+
#### TDD: 楽観ロックのテスト
|
|
2584
|
+
|
|
2585
|
+
<details>
|
|
2586
|
+
<summary>SalesOrderRepositoryOptimisticLockTest.java</summary>
|
|
2587
|
+
|
|
2588
|
+
```java
|
|
2589
|
+
// src/test/java/com/example/sms/infrastructure/persistence/repository/SalesOrderRepositoryOptimisticLockTest.java
|
|
2590
|
+
package com.example.sms.infrastructure.persistence.repository;
|
|
2591
|
+
|
|
2592
|
+
import com.example.sms.application.port.out.SalesOrderRepository;
|
|
2593
|
+
import com.example.sms.domain.exception.OptimisticLockException;
|
|
2594
|
+
import com.example.sms.domain.model.sales.OrderStatus;
|
|
2595
|
+
import com.example.sms.domain.model.sales.SalesOrder;
|
|
2596
|
+
import com.example.sms.testsetup.BaseIntegrationTest;
|
|
2597
|
+
import org.junit.jupiter.api.*;
|
|
2598
|
+
import org.springframework.beans.factory.annotation.Autowired;
|
|
2599
|
+
|
|
2600
|
+
import java.math.BigDecimal;
|
|
2601
|
+
import java.time.LocalDate;
|
|
2602
|
+
|
|
2603
|
+
import static org.assertj.core.api.Assertions.*;
|
|
2604
|
+
|
|
2605
|
+
@DisplayName("受注リポジトリ - 楽観ロック")
|
|
2606
|
+
class SalesOrderRepositoryOptimisticLockTest extends BaseIntegrationTest {
|
|
2607
|
+
|
|
2608
|
+
@Autowired
|
|
2609
|
+
private SalesOrderRepository salesOrderRepository;
|
|
2610
|
+
|
|
2611
|
+
@BeforeEach
|
|
2612
|
+
void setUp() {
|
|
2613
|
+
salesOrderRepository.deleteAll();
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
@Nested
|
|
2617
|
+
@DisplayName("楽観ロック")
|
|
2618
|
+
class OptimisticLocking {
|
|
2619
|
+
|
|
2620
|
+
@Test
|
|
2621
|
+
@DisplayName("同じバージョンで更新できる")
|
|
2622
|
+
void canUpdateWithSameVersion() {
|
|
2623
|
+
// Arrange
|
|
2624
|
+
var order = SalesOrder.builder()
|
|
2625
|
+
.orderNumber("SO-2025-0001")
|
|
2626
|
+
.orderDate(LocalDate.of(2025, 1, 20))
|
|
2627
|
+
.customerCode("CUST-001")
|
|
2628
|
+
.subtotal(new BigDecimal("50000"))
|
|
2629
|
+
.taxAmount(new BigDecimal("5000"))
|
|
2630
|
+
.totalAmount(new BigDecimal("55000"))
|
|
2631
|
+
.status(OrderStatus.RECEIVED)
|
|
2632
|
+
.build();
|
|
2633
|
+
salesOrderRepository.save(order);
|
|
2634
|
+
|
|
2635
|
+
// Act
|
|
2636
|
+
var fetched = salesOrderRepository.findByOrderNumber("SO-2025-0001").get();
|
|
2637
|
+
fetched.setSubtotal(new BigDecimal("60000"));
|
|
2638
|
+
salesOrderRepository.update(fetched);
|
|
2639
|
+
|
|
2640
|
+
// Assert
|
|
2641
|
+
var updated = salesOrderRepository.findByOrderNumber("SO-2025-0001").get();
|
|
2642
|
+
assertThat(updated.getSubtotal()).isEqualByComparingTo(new BigDecimal("60000"));
|
|
2643
|
+
assertThat(updated.getVersion()).isEqualTo(2); // バージョンがインクリメント
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
@Test
|
|
2647
|
+
@DisplayName("異なるバージョンで更新すると楽観ロック例外が発生する")
|
|
2648
|
+
void throwsExceptionWhenVersionMismatch() {
|
|
2649
|
+
// Arrange
|
|
2650
|
+
var order = SalesOrder.builder()
|
|
2651
|
+
.orderNumber("SO-2025-0002")
|
|
2652
|
+
.orderDate(LocalDate.of(2025, 1, 20))
|
|
2653
|
+
.customerCode("CUST-001")
|
|
2654
|
+
.subtotal(new BigDecimal("50000"))
|
|
2655
|
+
.taxAmount(new BigDecimal("5000"))
|
|
2656
|
+
.totalAmount(new BigDecimal("55000"))
|
|
2657
|
+
.status(OrderStatus.RECEIVED)
|
|
2658
|
+
.build();
|
|
2659
|
+
salesOrderRepository.save(order);
|
|
2660
|
+
|
|
2661
|
+
// ユーザーAが取得
|
|
2662
|
+
var orderA = salesOrderRepository.findByOrderNumber("SO-2025-0002").get();
|
|
2663
|
+
// ユーザーBが取得
|
|
2664
|
+
var orderB = salesOrderRepository.findByOrderNumber("SO-2025-0002").get();
|
|
2665
|
+
|
|
2666
|
+
// ユーザーAが更新(成功)
|
|
2667
|
+
orderA.setSubtotal(new BigDecimal("60000"));
|
|
2668
|
+
salesOrderRepository.update(orderA);
|
|
2669
|
+
|
|
2670
|
+
// Act & Assert: ユーザーBが古いバージョンで更新(失敗)
|
|
2671
|
+
orderB.setSubtotal(new BigDecimal("70000"));
|
|
2672
|
+
assertThatThrownBy(() -> salesOrderRepository.update(orderB))
|
|
2673
|
+
.isInstanceOf(OptimisticLockException.class)
|
|
2674
|
+
.hasMessageContaining("他のユーザーによって更新されています");
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
```
|
|
2679
|
+
|
|
2680
|
+
</details>
|
|
2681
|
+
|
|
2682
|
+
#### 楽観ロックのベストプラクティス
|
|
2683
|
+
|
|
2684
|
+
| ポイント | 説明 |
|
|
2685
|
+
|---------|------|
|
|
2686
|
+
| **バージョンカラム** | INTEGER 型で十分(オーバーフローは実用上問題なし) |
|
|
2687
|
+
| **WHERE 条件** | 必ず `AND "バージョン" = #{version}` を含める |
|
|
2688
|
+
| **インクリメント** | `"バージョン" = "バージョン" + 1` でアトミックに更新 |
|
|
2689
|
+
| **例外処理** | 更新件数が0の場合は楽観ロック例外をスロー |
|
|
2690
|
+
| **リトライ** | 必要に応じて再取得・再更新のリトライロジックを実装 |
|
|
2691
|
+
|
|
2692
|
+
---
|
|
2693
|
+
|
|
2694
|
+
## まとめ
|
|
2695
|
+
|
|
2696
|
+
本章では、販売管理システムの中核となるトランザクションデータの設計を行いました。
|
|
2697
|
+
|
|
2698
|
+
### 設計のポイント
|
|
2699
|
+
|
|
2700
|
+
1. **ヘッダ・明細構造**: すべてのトランザクションデータは、ヘッダ(親)と明細(子)の1対多構造を持つ
|
|
2701
|
+
|
|
2702
|
+
2. **ステータス管理**: 各トランザクションは状態遷移を持ち、PostgreSQL の ENUM 型で管理
|
|
2703
|
+
|
|
2704
|
+
3. **トレーサビリティ**: 見積→受注→出荷→売上の流れを追跡可能な外部キー設計
|
|
2705
|
+
|
|
2706
|
+
4. **数量管理**: 受注明細では受注数量・引当数量・出荷数量・残数量を分離管理
|
|
2707
|
+
|
|
2708
|
+
5. **返品・赤黒処理**: 売上の訂正は削除ではなく、返品データによる追記で対応
|
|
2709
|
+
|
|
2710
|
+
6. **リレーション設定**: MyBatis のネスト ResultMap で親子関係を効率的に取得
|
|
2711
|
+
|
|
2712
|
+
7. **楽観ロック**: バージョンカラムによる同時更新制御で整合性を保証
|
|
2713
|
+
|
|
2714
|
+
### 次章の予告
|
|
2715
|
+
|
|
2716
|
+
第7章では、売上データを起点とした債権管理(請求・入金)の設計を行います。
|