Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
M
mini-wms
概览
Overview
Details
Activity
Cycle Analytics
版本库
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
问题
0
Issues
0
列表
Board
标记
里程碑
合并请求
0
Merge Requests
0
CI / CD
CI / CD
流水线
作业
日程表
图表
维基
Wiki
代码片段
Snippets
成员
Members
Collapse sidebar
Close sidebar
活动
图像
聊天
创建新问题
作业
提交
Issue Boards
Open sidebar
周海峰
mini-wms
Commits
c7cf6243
Commit
c7cf6243
authored
Dec 16, 2025
by
yubin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
组件双击 去选框 弹窗修改高度 物料根据库存排序反显 列表id修改
parent
278338ea
显示空白字符变更
内嵌
并排
正在显示
12 个修改的文件
包含
514 行增加
和
197 行删除
+514
-197
ruoyi-admin-vue/src/components/materialsSeletor.vue
+82
-2
ruoyi-admin-vue/src/views/inventory/locations/index.vue
+12
-12
ruoyi-admin-vue/src/views/inventory/orders/OutboundOrderFormWithItems.vue
+10
-15
ruoyi-admin/src/main/resources/application-druid.yml
+1
-1
ruoyi-common/src/main/java/com/ruoyi/common/config/WarehouseConfig.java
+2
-1
ruoyi-inventory/src/main/java/com/ruoyi/inventory/domain/OutboundOrderItems.java
+2
-1
ruoyi-inventory/src/main/java/com/ruoyi/inventory/mapper/OutboundOrderItemsMapper.java
+6
-0
ruoyi-inventory/src/main/java/com/ruoyi/inventory/service/impl/InventoryServiceImpl.java
+27
-15
ruoyi-inventory/src/main/java/com/ruoyi/inventory/service/impl/OutboundOrdersServiceImpl.java
+318
-150
ruoyi-inventory/src/main/java/com/ruoyi/inventory/service/impl/StorageLocationsServiceImpl.java
+1
-0
ruoyi-inventory/src/main/resources/mapper/inventory/InventoryMapper.xml
+14
-0
ruoyi-inventory/src/main/resources/mapper/inventory/OutboundOrderItemsMapper.xml
+39
-0
没有找到文件。
ruoyi-admin-vue/src/components/materialsSeletor.vue
View file @
c7cf6243
...
...
@@ -56,6 +56,7 @@
</el-form-item>
</el-form>
<div
class=
"table-container"
style=
"flex: 1; min-height: 400px; max-height: 600px; overflow: auto; margin: 10px 0;"
>
<!-- 物料表格(恢复所有字段显示) -->
<el-table
ref=
"materialTable"
...
...
@@ -67,11 +68,14 @@
@
row-click=
"handleRowClick"
:select-on-indeterminate=
"false"
@
select=
"handleTableSelect"
@
row-dblclick=
"handleRowDblClick"
>
<!-- 单选模式下隐藏选择框 -->
<el-table-column
type=
"selection"
width=
"55"
align=
"center"
v-if=
"multiple"
/>
<el-table-column
type=
"index"
label=
"序号"
align=
"center"
/>
<el-table-column
label=
"SAP物料号"
align=
"center"
prop=
"sapNo"
/>
...
...
@@ -79,7 +83,7 @@
<el-table-column
label=
"TS Code"
align=
"center"
prop=
"tsCode"
/>
<el-table-column
label=
"物料分类"
align=
"center"
prop=
"categoryCode"
>
<
template
slot-scope=
"scope"
>
{{
scope
.
row
.
displayCategory
||
categoryMap
[
scope
.
row
.
categoryCode
]
||
scope
.
row
.
categoryCode
||
'-'
}}
{{
scope
.
row
.
displayCategory
}}
</
template
>
</el-table-column>
<el-table-column
label=
"规格型号"
align=
"center"
prop=
"specification"
/>
...
...
@@ -92,6 +96,7 @@
</
template
>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<pagination
...
...
@@ -221,6 +226,55 @@ export default {
this
.
getList
()
},
methods
:
{
// 新增:双击行事件处理
handleRowDblClick
(
row
)
{
if
(
this
.
isSelecting
||
!
this
.
$refs
.
materialTable
)
return
this
.
isSelecting
=
true
try
{
// 单选模式下双击直接选择并关闭组件
if
(
!
this
.
multiple
)
{
// 选中当前行
this
.
$refs
.
materialTable
.
clearSelection
()
this
.
$refs
.
materialTable
.
toggleRowSelection
(
row
,
true
)
this
.
singleSelectedId
=
row
.
id
this
.
selectedRows
=
[
row
]
// 构造返回数据
const
selectedData
=
{
id
:
row
.
id
,
sapNo
:
row
.
sapNo
,
materialName
:
row
.
materialName
,
tsCode
:
row
.
tsCode
,
categoryCode
:
row
.
categoryCode
,
categoryName
:
this
.
categoryMap
[
row
.
categoryCode
]
||
row
.
categoryCode
,
specification
:
row
.
specification
,
materialUnit
:
row
.
materialUnit
,
isBatchManaged
:
row
.
isBatchManaged
}
// 触发事件
this
.
$emit
(
'input'
,
row
.
id
)
this
.
$emit
(
'change'
,
selectedData
)
this
.
$emit
(
'selection-change'
,
{
materialIds
:
[
row
.
id
],
materials
:
[
selectedData
],
names
:
[
row
.
materialName
],
categoryIds
:
[
row
.
categoryCode
||
''
]
})
// 触发关闭事件(需要父组件监听此事件并关闭弹窗)
this
.
$emit
(
'confirm'
,
selectedData
)
this
.
$emit
(
'close'
)
}
else
{
// 多选模式下双击仅切换选择状态
this
.
$refs
.
materialTable
.
toggleRowSelection
(
row
)
}
}
finally
{
this
.
isSelecting
=
false
}
},
// 恢复分类列表加载(分类名称映射)
async
getCategoryList
()
{
try
{
...
...
@@ -246,6 +300,7 @@ export default {
console
.
error
(
'获取分类列表失败:'
,
error
)
}
},
handleValueChange
(
val
)
{
if
(
this
.
isSelecting
)
return
...
...
@@ -276,6 +331,7 @@ export default {
this
.
handleValueSync
()
}
},
async
getCategoryTreeData
()
{
this
.
loadingTree
=
true
try
{
...
...
@@ -291,6 +347,7 @@ export default {
this
.
loadingTree
=
false
}
},
buildTreeData
(
list
,
parentId
=
null
)
{
return
list
.
filter
(
item
=>
parentId
===
null
...
...
@@ -306,6 +363,7 @@ export default {
:
undefined
}))
},
buildCategoryCodeToSidMap
(
treeData
)
{
treeData
.
forEach
(
node
=>
{
if
(
node
.
categoryCode
)
{
...
...
@@ -320,6 +378,7 @@ export default {
}
})
},
selectCategoryNodes
(
categoryCodes
)
{
if
(
!
this
.
$refs
.
treeComponent
||
!
this
.
$refs
.
treeComponent
.
$refs
.
tree
)
return
const
tree
=
this
.
$refs
.
treeComponent
.
$refs
.
tree
...
...
@@ -334,6 +393,7 @@ export default {
}
})
},
handleTreeClick
(
data
)
{
this
.
currentNodeId
=
data
.
sid
this
.
queryParams
.
categoryCode
=
data
.
categoryCode
...
...
@@ -341,13 +401,14 @@ export default {
this
.
queryParams
.
pageNum
=
1
this
.
getList
()
},
getList
()
{
this
.
loading
=
true
listMaterials
(
this
.
queryParams
).
then
(
response
=>
{
// 恢复所有字段映射(显示用)
this
.
materialsList
=
(
response
.
rows
||
[]).
filter
(
item
=>
item
.
isUsed
!==
0
&&
item
.
isUsed
!==
'0'
).
map
(
item
=>
({
...
item
,
displayCategory
:
this
.
categoryMap
[
item
.
categoryCode
]
||
`
${
item
.
categoryCode
}
(未匹配分类)`
displayCategory
:
this
.
categoryMap
[
item
.
categoryCode
]
||
'未匹配分类'
}))
this
.
total
=
response
.
total
||
0
this
.
$nextTick
(()
=>
{
...
...
@@ -361,6 +422,7 @@ export default {
this
.
loading
=
false
})
},
handleQuery
()
{
// 恢复分类名称查询逻辑
const
inputName
=
this
.
queryParams
.
categoryNameInput
...
...
@@ -380,6 +442,7 @@ export default {
this
.
queryParams
.
pageNum
=
1
this
.
getList
()
},
resetQuery
()
{
// 恢复所有查询参数重置
this
.
queryParams
=
{
...
...
@@ -399,6 +462,7 @@ export default {
this
.
clearSelection
()
this
.
getList
()
},
// 核心:基于 ID 的选择事件(保留所有字段返回)
handleSelectionChange
(
selection
)
{
if
(
this
.
isSelecting
||
!
this
.
$refs
.
materialTable
)
return
...
...
@@ -454,6 +518,7 @@ export default {
this
.
isSelecting
=
false
}
},
// 单选模式下的选择事件(纯 ID 逻辑,保留字段返回)
handleTableSelect
(
selection
,
row
)
{
if
(
this
.
isSelecting
||
this
.
multiple
)
return
...
...
@@ -502,6 +567,7 @@ export default {
this
.
isSelecting
=
false
}
},
// 行点击事件(纯 ID 逻辑,保留字段返回)
handleRowClick
(
row
)
{
if
(
this
.
isSelecting
||
!
this
.
$refs
.
materialTable
)
return
...
...
@@ -555,6 +621,7 @@ export default {
this
.
isSelecting
=
false
}
},
// 清空选择(纯 ID 逻辑)
clearSelection
()
{
if
(
this
.
isSelecting
||
!
this
.
$refs
.
materialTable
)
return
...
...
@@ -576,6 +643,7 @@ export default {
this
.
isSelecting
=
false
}
},
// 核心:基于 ID 的反显逻辑(保留所有字段显示)
handleValueSync
(
isRetry
=
false
)
{
if
(
this
.
loading
||
this
.
isSelecting
||
!
this
.
$refs
.
materialTable
)
return
...
...
@@ -646,6 +714,7 @@ export default {
this
.
isRetrySync
=
false
}
},
// 外部设置选中 ID 的方法
setSelectedIds
(
ids
)
{
if
(
this
.
isSelecting
)
return
...
...
@@ -665,6 +734,7 @@ export default {
this
.
isSelecting
=
false
}
},
// 获取选中物料(返回完整字段,核心为ID)
getSelectedMaterials
()
{
if
(
this
.
multiple
)
{
...
...
@@ -693,6 +763,7 @@ export default {
}
:
null
}
},
// 单选模式下设置选中 ID
setSingleSelection
(
id
)
{
if
(
this
.
isSelecting
||
this
.
multiple
)
return
...
...
@@ -758,9 +829,17 @@ export default {
}
/
deep
/
.el-table--enable-row-hover
.el-table__body
tr
:hover
>
td
{
background-color
:
#f5f7fa
;
cursor
:
pointer
;
/* 新增:鼠标悬停显示指针 */
}
/
deep
/
.el-table__fixed-right
,
/
deep
/
.el-table__fixed-left
{
pointer-events
:
auto
!important
;
}
/* 单选模式下表格行样式优化 */
/
deep
/
.el-table__body
tr
.el-table__row--striped
td
{
background-color
:
#fafafa
;
}
/
deep
/
.el-table__body
tr
.current-row
td
{
background-color
:
#e8f4fd
!important
;
}
</
style
>
\ No newline at end of file
ruoyi-admin-vue/src/views/inventory/locations/index.vue
View file @
c7cf6243
...
...
@@ -71,7 +71,7 @@
@
keyup
.
enter
.
native=
"handleQuery"
/>
</el-form-item>
<el-form-item
label=
"仓库"
prop=
"warehouseId"
>
<
!-- <
el-form-item label="仓库" prop="warehouseId">
<el-input
v-model="queryWarehouseName"
placeholder="请选择仓库"
...
...
@@ -89,7 +89,7 @@
></i>
</template>
</el-input>
</el-form-item>
</el-form-item>
-->
<el-form-item
label=
"层"
prop=
"layerCode"
>
<el-input
v-model=
"queryParams.layerCode"
...
...
@@ -230,11 +230,11 @@
<el-table-column
type=
"selection"
width=
"55"
align=
"center"
fixed
/>
<el-table-column
label=
"库位编码"
align=
"center"
prop=
"locationCode"
width=
"120"
fixed
/>
<el-table-column
label=
"库位名称"
align=
"center"
prop=
"locationName"
width=
"150"
/>
<el-table-column
label=
"仓库"
align=
"center"
prop=
"warehousesName"
width=
"180"
>
<
!-- <
el-table-column label="仓库" align="center" prop="warehousesName" width="180">
<template slot-scope="scope">
{{ scope.row.warehousesName }}
</template>
</el-table-column>
</el-table-column>
-->
<el-table-column
label=
"库位类型"
align=
"center"
prop=
"locationType"
width=
"100"
>
<
template
slot-scope=
"scope"
>
<dict-tag
:options=
"dict.type.location_type"
:value=
"scope.row.locationType"
/>
...
...
@@ -357,7 +357,7 @@
</el-form-item>
</el-col>
</el-row>
<el-form-item
label=
"仓库"
prop=
"warehouseId"
>
<
!-- <
el-form-item label="仓库" prop="warehouseId">
<el-input
v-model="form.warehouseName"
placeholder="请选择仓库"
...
...
@@ -375,7 +375,7 @@
></i>
</template>
</el-input>
</el-form-item>
</el-form-item>
-->
<el-row
:gutter=
"20"
>
<el-col
:span=
"12"
>
<el-form-item
label=
"库位类型"
prop=
"locationType"
>
...
...
@@ -583,10 +583,10 @@
/>
<!-- 仓库选择器组件 -->
<WarehouseSelector
<
!-- <
WarehouseSelector
v-model="warehouseSelectorVisible"
@selected="handleWarehouseSelected"
/>
/>
-->
</div>
</template>
...
...
@@ -594,7 +594,7 @@
import
{
listLocations
,
getLocations
,
delLocations
,
addLocations
,
updateLocations
}
from
"@/api/inventory/locations"
import
{
listWarehouses
}
from
"@/api/inventory/warehouses"
import
materialsSeletor
from
"../../../components/materialsSeletor.vue"
import
WarehouseSelector
from
"@/views/compononents/WarehouseSelector.vue"
//
import WarehouseSelector from "@/views/compononents/WarehouseSelector.vue"
import
ImportExcel
from
"@/components/ImportExcel/index"
import
{
listMaterials
}
from
"@/api/inventory/materials"
...
...
@@ -670,9 +670,9 @@ export default {
{
required
:
true
,
message
:
'库位名称不能为空'
,
trigger
:
'blur'
},
{
min
:
1
,
max
:
100
,
message
:
'库位名称长度不能超过100个字符'
,
trigger
:
'blur'
}
],
warehouseId
:
[
{
required
:
true
,
message
:
'仓库不能为空'
,
trigger
:
'change'
}
],
//
warehouseId: [
//
{ required: true, message: '仓库不能为空', trigger: 'change' }
//
],
locationType
:
[
{
required
:
true
,
message
:
'库位类型不能为空'
,
trigger
:
'change'
}
],
...
...
ruoyi-admin-vue/src/views/inventory/orders/OutboundOrderFormWithItems.vue
View file @
c7cf6243
...
...
@@ -49,15 +49,12 @@
<div
style=
"height: 70vh; overflow: auto; padding: 0 10px;"
>
<MaterialSelector
ref=
"materialsSeletor"
@
selection-change=
"
handleMaterialSelectionChange
"
@
selection-change=
"
confirmMaterialSelect
"
:selected-material-codes=
"form.materialUuids ? [form.materialUuids] : []"
:multiple=
"false"
/>
</div>
<div
slot=
"footer"
class=
"dialog-footer"
>
<el-button
@
click
.
native=
"openMaterialSelector = false"
>
取消
</el-button>
<el-button
type=
"primary"
@
click
.
native=
"confirmMaterialSelect"
>
确认选择
</el-button>
</div>
</el-dialog>
<!-- 库存信息列表 -->
...
...
@@ -335,7 +332,8 @@ export default {
selectedMaterialId
:
''
,
selectedMaterialInfo
:
null
,
currentSelectedRowId
:
null
,
isInitEcho
:
false
isInitEcho
:
false
,
openMaterialSelector
:
false
,
// 确保初始为关闭状态
};
},
computed
:
{
...
...
@@ -799,6 +797,9 @@ handleSubmit() {
},
// 修复:增强校验逻辑,增加组件存在性检查和异步同步
confirmMaterialSelect
()
{
// 标记为“用户主动操作”
this
.
isUserInitiatedSelect
=
true
;
// 1. 检查选择器组件是否存在
if
(
!
this
.
$refs
.
materialsSeletor
)
{
this
.
$message
.
error
(
'物料选择器组件加载失败,请重试'
);
...
...
@@ -821,18 +822,9 @@ handleSubmit() {
// 触发数据同步(覆盖事件未触发的场景)
this
.
handleMaterialSelectionChange
(
selectedData
);
// 4. 增强空值校验
if
(
!
this
.
selectedMaterialInfo
||
!
this
.
selectedMaterialId
)
{
this
.
$message
.
warning
(
'请选择物料后再确认'
);
return
;
}
// 5. 容错处理:确保物料ID有效
const
materialId
=
this
.
selectedMaterialInfo
.
id
||
this
.
selectedMaterialInfo
.
materialId
||
this
.
selectedMaterialInfo
.
uuid
||
''
;
if
(
!
materialId
)
{
this
.
$message
.
error
(
'选中的物料缺少有效ID,请重新选择'
);
return
;
}
// 6. 赋值并关闭弹窗
this
.
$set
(
this
.
form
,
'materialId'
,
materialId
);
...
...
@@ -844,6 +836,9 @@ handleSubmit() {
this
.
$nextTick
(()
=>
{
this
.
$refs
.
detailForm
?.
validateField
(
'materialId'
);
});
// 重置标记
this
.
isUserInitiatedSelect
=
false
;
});
}
},
...
...
ruoyi-admin/src/main/resources/application-druid.yml
View file @
c7cf6243
...
...
@@ -13,7 +13,7 @@ spring:
#username: root # 数据库用户名
#password: 'Aa123456'
#測試
url
:
jdbc:mysql://demo.docmis.cn:23500/inventory_manager?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&useSSL=false
url
:
jdbc:mysql://demo.docmis.cn:23500/inventory_manager?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&useSSL=false
&allowMultiQueries=true
username
:
root
password
:
'
!QAZ2wsx#EDC2022'
# 从库数据源
...
...
ruoyi-common/src/main/java/com/ruoyi/common/config/WarehouseConfig.java
View file @
c7cf6243
...
...
@@ -7,6 +7,6 @@ public class WarehouseConfig {
/**
* 默认出库仓库ID(核心默认值)
*/
public
static
final
String
DEFAULT_WAREHOUSE_ID
=
"
572ba484-199c-45d9-9735-610928ed5c70
"
;
public
static
final
String
DEFAULT_WAREHOUSE_ID
=
"
local
"
;
}
\ No newline at end of file
ruoyi-inventory/src/main/java/com/ruoyi/inventory/domain/OutboundOrderItems.java
View file @
c7cf6243
...
...
@@ -117,7 +117,7 @@ public class OutboundOrderItems extends BaseEntity
@Excel
(
name
=
"排序号"
)
private
String
updateUserCode
;
private
String
InventoryType
;
private
int
InventoryType
;
}
\ No newline at end of file
ruoyi-inventory/src/main/java/com/ruoyi/inventory/mapper/OutboundOrderItemsMapper.java
View file @
c7cf6243
...
...
@@ -74,5 +74,11 @@ public interface OutboundOrderItemsMapper
public
int
batchInsertOutboundOrderItems
(
List
<
OutboundOrderItems
>
inboundOrderItems
);
public
int
batchUpdateOutboundOrderItems
(
List
<
OutboundOrderItems
>
inboundOrderItems
);
int
batchDeleteOutboundOrderItems
(
String
[]
ids
);
public
int
deleteOutboundOrderItemsByOrderId
(
String
orderId
);
}
ruoyi-inventory/src/main/java/com/ruoyi/inventory/service/impl/InventoryServiceImpl.java
View file @
c7cf6243
...
...
@@ -156,33 +156,45 @@ public class InventoryServiceImpl implements IInventoryService
}
updateInventory
(
inventory
);
// 库存操作表插入数据
createInventoryOutboundLog
(
inventory
,
outboundOrderItem
,
Long
.
valueOf
(
quantity
),
SystemUtils
.
getUserName
(),
new
Date
());
}
RefreshInventory
(
inventoryIds
);
}
return
1
;
}
private
void
createInventoryOutboundLog
(
Inventory
inventory
,
OutboundOrderItems
outboundOrderItem
,
Long
deductQty
,
String
updateUser
,
Date
updateTime
)
{
InventoryTransactions
transactions
=
new
InventoryTransactions
();
transactions
.
setId
(
IdUtils
.
simpleUUID
());
transactions
.
setTransactionType
(
2L
);
// 事务类型-出库
transactions
.
setId
(
IdUtils
.
simpleUUID
());
// 确保IdUtils工具类存在,若无则替换为UUID.randomUUID().toString()
transactions
.
setTransactionType
(
2L
);
// 事务类型-出库
transactions
.
setBatchCode
(
outboundOrderItem
.
getBatchCode
());
transactions
.
setUnitPrice
(
String
.
valueOf
(
outboundOrderItem
.
getUnitPrice
()));
transactions
.
setInventoryId
(
inventory
.
getId
());
// 库存表Id
transactions
.
setReferenceId
(
outboundOrderItem
.
getOutboundOrderId
());
//关联单号,相当于主记录-盘点主表
transactions
.
setReferenceItemId
(
outboundOrderItem
.
getId
());
// 盘点子表id
transactions
.
setReferenceId
(
outboundOrderItem
.
getOutboundOrderId
());
// 关联出库单主表ID
transactions
.
setReferenceItemId
(
outboundOrderItem
.
getId
());
// 关联出库单明细ID
transactions
.
setMaterialId
(
outboundOrderItem
.
getMaterialId
());
transactions
.
setWarehouseId
(
outboundOrderItem
.
getWarehouseId
());
transactions
.
setLocationId
(
outboundOrderItem
.
getLocationId
());
// 补充货主ID(从出库单主表查询)
OutboundOrders
outboundOrders
=
outboundOrderMapper
.
selectOutboundOrdersById
(
outboundOrderItem
.
getOutboundOrderId
());
if
(
outboundOrders
!=
null
)
{
transactions
.
setOwnerId
(
outboundOrders
.
getOwnerId
());
transactions
.
setQuantityBefore
(
Long
.
valueOf
(
quantity
));
// 变更前数量
transactions
.
setQuantityAfter
(
inventory
.
getQuantity
());
// 变更后数量
transactions
.
setQuantityChange
(
outboundOrderItem
.
getActualQuantity
());
Date
nowDate
=
new
Date
();
transactions
.
setTransactionTime
(
nowDate
);
}
// 变更前后数量
Long
beforeQty
=
Optional
.
ofNullable
(
inventory
.
getQuantity
()).
orElse
(
0L
)
+
deductQty
;
// 扣减前数量 = 扣减后 + 扣减量
transactions
.
setQuantityBefore
(
beforeQty
);
transactions
.
setQuantityAfter
(
inventory
.
getQuantity
());
// 扣减后数量
transactions
.
setQuantityChange
(
deductQty
);
// 变更量(出库为正数)
transactions
.
setTransactionTime
(
updateTime
);
transactions
.
setOperatedBy
(
updateUser
);
transactions
.
setOperatedBy
(
SystemUtils
.
getUserName
());
// 插入日志
insertInventoryTransactions
.
insertInventoryTransactions
(
transactions
);
}
RefreshInventory
(
inventoryIds
);
}
return
1
;
}
@SerialExecution
(
group
=
"inventoryRefresh"
,
fair
=
true
)
@Override
...
...
ruoyi-inventory/src/main/java/com/ruoyi/inventory/service/impl/OutboundOrdersServiceImpl.java
View file @
c7cf6243
...
...
@@ -30,9 +30,12 @@ import org.springframework.util.CollectionUtils;
/**
* 出库单主Service业务层处理
* 核心修正:
* 1. 库存匹配Key统一为
仓库ID_物料ID_库位ID_库存类型
* 1. 库存匹配Key统一为
物料ID_库位ID_库存类型(移除仓库维度)
* 2. 确保inventoryType完整参与匹配
* 3. 库存扣减后≤0时强制设置inventory_status=0
* 4. 无库位库存不足时,自动扣减同物料同库存类型的有库位库存
* 5. 导入场景下仅保留新增逻辑,移除更新相关处理
* 6. 移除所有仓库相关逻辑,仅保留物料+库位维度匹配
*
* @author ruoyi
* @date 2025-12-03
...
...
@@ -52,8 +55,6 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
private
OwnersServiceImpl
ownersService
;
@Autowired
private
WarehousesServiceImpl
warehousesService
;
@Autowired
private
InventoryServiceImpl
inventoryService
;
@Autowired
private
MaterialsServiceImpl
materialsService
;
...
...
@@ -108,7 +109,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
}
/**
* 修改出库单主
* 修改出库单主
(仅保留方法定义,移除更新逻辑)
*
* @param outboundOrders 出库单主
* @return 结果
...
...
@@ -117,13 +118,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
@Override
public
int
updateOutboundOrders
(
OutboundOrders
outboundOrders
)
{
outboundOrdersMapper
.
deleteOutboundOrderItemsByOrderId
(
outboundOrders
.
getId
());
outboundOrderLogMapper
.
deleteOutboundOrderLogByOrdersId
(
outboundOrders
.
getId
());
outboundOrders
.
setUpdateUserCode
(
SystemUtils
.
getUserName
());
outboundOrders
.
setUpdateTime
(
DateUtils
.
getNowDate
());
insertOutboundOrderItems
(
outboundOrders
);
return
outboundOrdersMapper
.
updateOutboundOrders
(
outboundOrders
);
throw
new
ServiceException
(
"当前系统仅支持新增出库单,不支持修改操作"
);
}
/**
...
...
@@ -184,7 +179,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
outboundOrders
.
setUpdateUserCode
(
updateUser
);
outboundOrdersMapper
.
updateOutboundOrders
(
outboundOrders
);
// 3. 执行库存扣减(
包含inventoryType匹配 + 0值状态更新
)
// 3. 执行库存扣减(
无库位不足时扣有库位
)
this
.
deductInventory
(
outboundOrderItems
,
updateUser
,
updateTime
);
return
1
;
...
...
@@ -192,47 +187,186 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
/**
* 核心库存扣减逻辑
* 1. 按 仓库ID_物料ID_库位ID_库存类型 匹配库存
* 2. 扣减后数量≤0时设置inventory_status=0
* 1. 有库位:直接扣减指定库位库存
* 2. 无库位:先扣无库位库存,不足则扣同物料同库存类型的有库位库存
* 3. 扣减后数量≤0时设置inventory_status=0
* 4. 无库位扣减有库位时,记录扣减的库位和数量(用于生成明细)
* 5. 移除仓库维度,仅按物料+库位+库存类型匹配
* @param outboundOrderItems 出库明细
* @param updateUser 操作人
* @param updateTime 操作时间
* @return 无库位明细扣减的有库位库存记录 Map<无库位明细ID, List<扣减的库位库存信息Map>>
*/
private
void
deductInventory
(
List
<
OutboundOrderItems
>
outboundOrderItems
,
String
updateUser
,
Date
updateTime
)
{
private
Map
<
String
,
List
<
Map
<
String
,
Object
>>>
deductInventory
(
List
<
OutboundOrderItems
>
outboundOrderItems
,
String
updateUser
,
Date
updateTime
)
{
if
(
CollectionUtils
.
isEmpty
(
outboundOrderItems
))
{
return
;
return
Collections
.
emptyMap
()
;
}
// 1. 预加载库存映射:Key=仓库ID_物料ID_库位ID_库存类型 Value=库存对象
Map
<
String
,
Inventory
>
inventoryMap
=
this
.
loadInventoryMap
();
// 2. 构建扣减数量Map:Key=仓库ID_物料ID_库位ID_库存类型 Value=总扣减数量
Map
<
String
,
List
<
Map
<
String
,
Object
>>>
deductRecordMap
=
new
HashMap
<>();
Map
<
String
,
List
<
Inventory
>>
inventoryGroupMap
=
this
.
loadInventoryGroupMap
();
Map
<
String
,
Long
>
deductQtyMap
=
this
.
buildDeductQtyMap
(
outboundOrderItems
);
// 3. 遍历扣减Map,执行扣减+状态更新
List
<
Inventory
>
needUpdateList
=
new
ArrayList
<>();
for
(
Map
.
Entry
<
String
,
Long
>
entry
:
deductQtyMap
.
entrySet
())
{
String
key
=
entry
.
getKey
();
Long
deductQty
=
entry
.
getValue
();
Long
totalDeductQty
=
entry
.
getValue
();
if
(
totalDeductQty
<=
0
)
continue
;
// 匹配库存
Inventory
inventory
=
inventoryMap
.
get
(
key
);
if
(
inventory
==
null
)
{
String
[]
keyParts
=
key
.
split
(
"_"
);
String
warehouseId
=
keyParts
.
length
>
0
?
keyParts
[
0
]
:
""
;
String
materialId
=
keyParts
.
length
>
1
?
keyParts
[
1
]
:
""
;
String
locationId
=
keyParts
.
length
>
2
?
keyParts
[
2
]
:
""
;
String
inventoryType
=
keyParts
.
length
>
3
?
keyParts
[
3
]
:
""
;
String
materialId
=
keyParts
.
length
>
0
?
keyParts
[
0
]
:
""
;
String
locationId
=
keyParts
.
length
>
1
?
keyParts
[
1
]
:
""
;
String
inventoryType
=
keyParts
.
length
>
2
?
keyParts
[
2
]
:
""
;
String
groupKey
=
String
.
join
(
"_"
,
materialId
,
inventoryType
);
List
<
Inventory
>
inventoryList
=
inventoryGroupMap
.
get
(
groupKey
);
if
(
CollectionUtils
.
isEmpty
(
inventoryList
))
{
throw
new
ServiceException
(
String
.
format
(
"仓库[%s]物料[%s]库位[%s]库存类型[%s]的库存不存在,无法扣减"
,
warehouseId
,
materialId
,
locationId
,
inventoryType
));
"物料[%s]库存类型[%s]无可用库存,无法扣减"
,
materialId
,
inventoryType
));
}
Long
remainDeductQty
=
totalDeductQty
;
List
<
Map
<
String
,
Object
>>
deductRecords
=
new
ArrayList
<>();
String
itemId
=
outboundOrderItems
.
stream
()
.
filter
(
item
->
key
.
equals
(
buildDeductKey
(
item
)))
.
map
(
OutboundOrderItems:
:
getId
)
.
findFirst
()
.
orElse
(
null
);
// 区分无库位/有库位逻辑
if
(
StringUtils
.
isBlank
(
locationId
))
{
// 无库位:先扣无库位库存,再扣有库位库存
// 第一步:扣无库位库存
List
<
Inventory
>
noLocationInvList
=
inventoryList
.
stream
()
.
filter
(
inv
->
StringUtils
.
isBlank
(
inv
.
getLocationId
()))
.
collect
(
Collectors
.
toList
());
for
(
Inventory
inv
:
noLocationInvList
)
{
if
(
remainDeductQty
<=
0
)
break
;
Long
currentQty
=
Optional
.
ofNullable
(
inv
.
getQuantity
()).
orElse
(
0L
);
Long
deductQty
=
Math
.
min
(
remainDeductQty
,
currentQty
);
// 不扣负数,只扣现有库存
inv
.
setQuantity
(
currentQty
-
deductQty
);
if
(
inv
.
getQuantity
()
<=
0
)
{
inv
.
setInventoryStatus
(
0L
);
}
inv
.
setUpdateBy
(
updateUser
);
inv
.
setUpdateTime
(
updateTime
);
needUpdateList
.
add
(
inv
);
// 记录无库位扣减
deductRecords
.
add
(
buildDeductRecord
(
inv
,
inventoryType
,
deductQty
));
remainDeductQty
-=
deductQty
;
}
// 第二步:无库位不足,扣有库位库存
if
(
remainDeductQty
>
0
)
{
List
<
Inventory
>
hasLocationInvList
=
inventoryList
.
stream
()
.
filter
(
inv
->
StringUtils
.
isNotBlank
(
inv
.
getLocationId
()))
.
collect
(
Collectors
.
toList
());
for
(
Inventory
inv
:
hasLocationInvList
)
{
if
(
remainDeductQty
<=
0
)
break
;
Long
currentQty
=
Optional
.
ofNullable
(
inv
.
getQuantity
()).
orElse
(
0L
);
Long
deductQty
=
Math
.
min
(
remainDeductQty
,
currentQty
);
// 不扣负数
inv
.
setQuantity
(
currentQty
-
deductQty
);
if
(
inv
.
getQuantity
()
<=
0
)
{
inv
.
setInventoryStatus
(
0L
);
}
inv
.
setUpdateBy
(
updateUser
);
inv
.
setUpdateTime
(
updateTime
);
needUpdateList
.
add
(
inv
);
// 记录有库位扣减
deductRecords
.
add
(
buildDeductRecord
(
inv
,
inventoryType
,
deductQty
));
remainDeductQty
-=
deductQty
;
}
}
// 最后仍有剩余(所有库存耗尽),允许扣最后一个库存为负数
if
(
remainDeductQty
>
0
&&
!
inventoryList
.
isEmpty
())
{
Inventory
lastInv
=
inventoryList
.
get
(
inventoryList
.
size
()
-
1
);
Long
deductQty
=
remainDeductQty
;
lastInv
.
setQuantity
(
Optional
.
ofNullable
(
lastInv
.
getQuantity
()).
orElse
(
0L
)
-
deductQty
);
lastInv
.
setInventoryStatus
(
0L
);
lastInv
.
setUpdateBy
(
updateUser
);
lastInv
.
setUpdateTime
(
updateTime
);
needUpdateList
.
add
(
lastInv
);
deductRecords
.
add
(
buildDeductRecord
(
lastInv
,
inventoryType
,
deductQty
));
remainDeductQty
=
0L
;
}
}
else
{
// 有库位:直接扣指定库位库存
Inventory
targetInv
=
inventoryList
.
stream
()
.
filter
(
inv
->
locationId
.
equals
(
inv
.
getLocationId
()))
.
findFirst
()
.
orElseThrow
(()
->
new
ServiceException
(
String
.
format
(
"物料[%s]库位[%s]无库存"
,
materialId
,
locationId
)));
Long
currentQty
=
Optional
.
ofNullable
(
targetInv
.
getQuantity
()).
orElse
(
0L
);
Long
deductQty
=
Math
.
min
(
remainDeductQty
,
currentQty
);
targetInv
.
setQuantity
(
currentQty
-
deductQty
);
if
(
targetInv
.
getQuantity
()
<=
0
)
{
targetInv
.
setInventoryStatus
(
0L
);
}
targetInv
.
setUpdateBy
(
updateUser
);
targetInv
.
setUpdateTime
(
updateTime
);
needUpdateList
.
add
(
targetInv
);
deductRecords
.
add
(
buildDeductRecord
(
targetInv
,
inventoryType
,
deductQty
));
remainDeductQty
-=
deductQty
;
// 执行数量扣减
// 剩余部分扣为负数
if
(
remainDeductQty
>
0
)
{
Long
finalDeduct
=
remainDeductQty
;
targetInv
.
setQuantity
(
targetInv
.
getQuantity
()
-
finalDeduct
);
targetInv
.
setInventoryStatus
(
0L
);
deductRecords
.
add
(
buildDeductRecord
(
targetInv
,
inventoryType
,
finalDeduct
));
remainDeductQty
=
0L
;
}
}
// 关联明细与扣减记录
if
(
itemId
!=
null
&&
!
deductRecords
.
isEmpty
())
{
deductRecordMap
.
put
(
itemId
,
deductRecords
);
}
}
// 批量更新库存
if
(!
needUpdateList
.
isEmpty
())
{
inventoryMapper
.
batchUpdateInventory
(
needUpdateList
);
inventoryService
.
RefreshInventory
(
needUpdateList
.
stream
().
map
(
Inventory:
:
getId
).
distinct
().
collect
(
Collectors
.
toList
()));
}
return
deductRecordMap
;
}
private
Map
<
String
,
Object
>
buildDeductRecord
(
Inventory
inv
,
String
inventoryType
,
Long
deductQty
)
{
Map
<
String
,
Object
>
record
=
new
HashMap
<>();
record
.
put
(
"inventoryId"
,
inv
.
getId
());
record
.
put
(
"locationId"
,
inv
.
getLocationId
());
record
.
put
(
"materialId"
,
inv
.
getMaterialId
());
record
.
put
(
"inventoryType"
,
inventoryType
);
record
.
put
(
"deductQty"
,
deductQty
);
return
record
;
}
/**
* 扣减单个库存的数量
* @param inventory 库存对象
* @param deductQty 待扣减数量
* @param updateUser 操作人
* @param updateTime 操作时间
* @return 剩余未扣减数量
*/
private
Long
deductSingleInventory
(
Inventory
inventory
,
Long
deductQty
,
String
updateUser
,
Date
updateTime
)
{
Long
currentQty
=
Optional
.
ofNullable
(
inventory
.
getQuantity
()).
orElse
(
0L
);
Long
newQty
=
currentQty
-
deductQty
;
// 关键修改:不再限制扣减数量,直接扣减(允许库存变为负数)
Long
canDeductQty
=
deductQty
;
// 原逻辑:Math.min(deductQty, currentQty)
// 扣减数量(允许负数)
Long
newQty
=
currentQty
-
canDeductQty
;
inventory
.
setQuantity
(
newQty
);
// 核心规则:扣减后数量≤0 → 状态置0
// 扣减后≤0,设置状态为0(即使是负数也设置)
if
(
newQty
<=
0
)
{
inventory
.
setInventoryStatus
(
0L
);
}
...
...
@@ -241,26 +375,16 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
inventory
.
setUpdateBy
(
updateUser
);
inventory
.
setUpdateTime
(
updateTime
);
needUpdateList
.
add
(
inventory
);
}
// 4. 批量更新库存
if
(!
needUpdateList
.
isEmpty
())
{
inventoryMapper
.
batchUpdateInventory
(
needUpdateList
);
// 刷新库存缓存
List
<
String
>
inventoryIds
=
needUpdateList
.
stream
()
.
map
(
Inventory:
:
getId
)
.
distinct
()
.
collect
(
Collectors
.
toList
());
inventoryService
.
RefreshInventory
(
inventoryIds
);
}
// 返回剩余未扣减数量(扣减全部,剩余为0)
return
0L
;
// 原逻辑:deductQty - canDeductQty
}
/**
* 预加载库存Map
* Key规则:仓库ID_物料ID_库位ID_库存类型
* 预加载库存分组Map
* Key=物料ID_库存类型 Value=该维度下所有库位的库存列表(含无库位)
* 移除仓库维度
*/
private
Map
<
String
,
Inventory
>
loadInventory
Map
()
{
private
Map
<
String
,
List
<
Inventory
>>
loadInventoryGroup
Map
()
{
Inventory
query
=
new
Inventory
();
query
.
setInventoryStatus
(
1L
);
query
.
setIsUsed
(
1L
);
...
...
@@ -270,35 +394,26 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
return
Collections
.
emptyMap
();
}
Map
<
String
,
Inventory
>
inventoryMap
=
new
HashMap
<>();
for
(
Inventory
inv
:
inventoryList
)
{
String
key
=
String
.
join
(
"_"
,
Optional
.
ofNullable
(
inv
.
getWarehousesId
()).
orElse
(
""
)
,
// 按「物料ID_库存类型」分组(移除仓库维度)
return
inventoryList
.
stream
()
.
collect
(
Collectors
.
groupingBy
(
inv
->
String
.
join
(
"_"
,
Optional
.
ofNullable
(
inv
.
getMaterialId
()).
orElse
(
""
),
Optional
.
ofNullable
(
inv
.
getLocationId
()).
orElse
(
""
),
Optional
.
ofNullable
(
inv
.
getInventoryType
()).
map
(
String:
:
valueOf
).
orElse
(
""
)
);
// 重复key保留第一个(避免覆盖)
if
(!
inventoryMap
.
containsKey
(
key
))
{
inventoryMap
.
put
(
key
,
inv
);
}
}
return
inventoryMap
;
),
HashMap:
:
new
,
Collectors
.
toList
()
));
}
/**
* 构建扣减数量Map
* Key规则:
仓库ID_物料ID_库位ID_库存类型(和库存Map完全对齐
)
* Key规则:
物料ID_库位ID_库存类型(移除仓库维度
)
*/
private
Map
<
String
,
Long
>
buildDeductQtyMap
(
List
<
OutboundOrderItems
>
items
)
{
Map
<
String
,
Long
>
deductQtyMap
=
new
HashMap
<>();
for
(
OutboundOrderItems
item
:
items
)
{
String
key
=
String
.
join
(
"_"
,
Optional
.
ofNullable
(
item
.
getWarehouseId
()).
orElse
(
""
),
Optional
.
ofNullable
(
item
.
getMaterialId
()).
orElse
(
""
),
Optional
.
ofNullable
(
item
.
getLocationId
()).
orElse
(
""
),
Optional
.
ofNullable
(
item
.
getInventoryType
()).
map
(
String:
:
valueOf
).
orElse
(
""
)
);
String
key
=
buildDeductKey
(
item
);
// 累加扣减数量
Long
qty
=
Optional
.
ofNullable
(
item
.
getActualQuantity
()).
orElse
(
0L
);
deductQtyMap
.
put
(
key
,
deductQtyMap
.
getOrDefault
(
key
,
0L
)
+
qty
);
...
...
@@ -306,6 +421,18 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
return
deductQtyMap
;
}
/**
* 构建明细的扣减Key
* 移除仓库维度,Key规则:物料ID_库位ID_库存类型
*/
private
String
buildDeductKey
(
OutboundOrderItems
item
)
{
return
String
.
join
(
"_"
,
Optional
.
ofNullable
(
item
.
getMaterialId
()).
orElse
(
""
),
Optional
.
ofNullable
(
item
.
getLocationId
()).
orElse
(
""
),
Optional
.
ofNullable
(
item
.
getInventoryType
()).
map
(
String:
:
valueOf
).
orElse
(
""
)
);
}
@Override
public
List
<
Map
<
String
,
String
>>
outboundOrdersTopTenByQuantity
()
{
return
outboundOrdersMapper
.
SelectOutboundOrdersMaterialsTopTenByQuantity
();
...
...
@@ -392,10 +519,9 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
Map
<
String
,
List
<
OutboundOrderItems
>>
validItemMap
=
new
HashMap
<>();
boolean
hasValidateError
=
false
;
// 3. 预加载映射缓存
// 3. 预加载映射缓存
(移除仓库相关映射)
Map
<
String
,
String
>
sapToMaterialIdMap
=
loadSapToMaterialIdMap
();
Map
<
String
,
String
>
locationNameToIdMap
=
loadLocationNameToIdMap
();
Map
<
String
,
String
>
warehouseNameToIdMap
=
loadWarehouseNameToIdMap
();
Map
<
String
,
String
>
ownerNameToIdMap
=
loadOwnerNameToIdMap
();
Map
<
String
,
AbstractMap
.
SimpleEntry
<
String
,
Long
>>
inventoryTOIdMap
=
loadInventoryTOIdMap
();
...
...
@@ -404,7 +530,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
.
filter
(
vo
->
StringUtils
.
isNotBlank
(
vo
.
getOrderId
()))
.
collect
(
Collectors
.
groupingBy
(
OutboundTemplateVO:
:
getOrderId
));
// 5. 数据验证
// 5. 数据验证
(仅保留新增逻辑,检测到已存在则抛异常)
for
(
Map
.
Entry
<
String
,
List
<
OutboundTemplateVO
>>
entry
:
orderGroupMap
.
entrySet
())
{
String
orderId
=
entry
.
getKey
();
List
<
OutboundTemplateVO
>
voList
=
entry
.
getValue
();
...
...
@@ -413,31 +539,20 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
try
{
OutboundTemplateVO
firstVO
=
voList
.
get
(
0
);
// 检查出库单是否已存在
// 检查出库单是否已存在
(仅新增,存在则抛异常)
OutboundOrders
outboundOrdersQuery
=
new
OutboundOrders
();
outboundOrdersQuery
.
setOrderId
(
orderId
);
List
<
OutboundOrders
>
existMains
=
outboundOrdersMapper
.
selectOutboundOrdersList
(
outboundOrdersQuery
);
if
(
existMains
!=
null
&&
existMains
.
size
()
>
1
)
{
throw
new
ServiceException
(
String
.
format
(
"入库单号【%s】
存在多条重复主表数据
"
,
orderId
));
if
(
existMains
!=
null
&&
!
existMains
.
isEmpty
()
)
{
throw
new
ServiceException
(
String
.
format
(
"入库单号【%s】
已存在,当前系统仅支持新增,不支持更新
"
,
orderId
));
}
OutboundOrders
existMain
=
CollectionUtils
.
isEmpty
(
existMains
)
?
null
:
existMains
.
get
(
0
);
if
(
existMain
!=
null
)
{
if
(!
Boolean
.
TRUE
.
equals
(
isUpdateSupport
))
{
throw
new
ServiceException
(
String
.
format
(
"入库单号【%s】已存在,且不支持更新"
,
orderId
));
}
mainDO
=
existMain
;
BeanUtils
.
copyProperties
(
firstVO
,
mainDO
,
"id"
,
"createBy"
,
"createTime"
);
mainDO
.
setUpdateBy
(
operId
);
mainDO
.
setWarehouseId
(
WarehouseConfig
.
DEFAULT_WAREHOUSE_ID
);
mainDO
.
setUpdateTime
(
now
);
mainDO
.
setUpdateUserCode
(
operId
);
}
else
{
// 仅新增逻辑:构建新出库单主数据
mainDO
=
new
OutboundOrders
();
BeanUtils
.
copyProperties
(
firstVO
,
mainDO
,
"sapNo"
,
"materialName"
,
"plannedQuantity"
,
"actualQuantity"
,
"plannedPackages"
,
"materialUnit"
,
"materialRemark
"
);
"plannedPackages"
,
"materialUnit"
,
"materialRemark"
,
"warehouseName"
,
"warehouseId
"
);
// 货主校验
String
ownerName
=
firstVO
.
getOwnerName
();
...
...
@@ -476,7 +591,6 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
mainDO
.
setUpdateTime
(
now
);
mainDO
.
setUpdateUserCode
(
operId
);
mainDO
.
setSortNo
(
Optional
.
ofNullable
(
mainDO
.
getSortNo
()).
orElse
(
0L
));
}
// 明细校验
for
(
int
i
=
0
;
i
<
voList
.
size
();
i
++)
{
...
...
@@ -485,7 +599,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
OutboundOrderItems
itemDO
=
new
OutboundOrderItems
();
BeanUtils
.
copyProperties
(
vo
,
itemDO
,
"orderId"
,
"systemNo"
,
"orderTypeId"
,
"batchId"
);
"orderId"
,
"systemNo"
,
"orderTypeId"
,
"batchId"
,
"warehouseName"
,
"warehouseId"
);
// 填充明细必填字段
itemDO
.
setId
(
UUID
.
randomUUID
().
toString
());
...
...
@@ -496,6 +610,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
itemDO
.
setCreateTime
(
now
);
itemDO
.
setCreateUserCode
(
operId
);
itemDO
.
setSortNo
(
0L
);
itemDO
.
setItemStatus
(
3L
);
// 设置为已出库状态
// 物料SAP校验
String
sapNo
=
vo
.
getSapNo
()
!=
null
?
vo
.
getSapNo
().
trim
()
:
""
;
...
...
@@ -512,38 +627,35 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
// 库位校验
String
locationName
=
vo
.
getLocationName
()
!=
null
?
vo
.
getLocationName
().
trim
()
:
""
;
if
(
StringUtils
.
isBlank
(
locationName
))
{
throw
new
ServiceException
(
String
.
format
(
"入库单号【%s】第%d条明细的库位名称不能为空"
,
orderId
,
lineNo
));
}
String
locationId
=
locationNameToIdMap
.
get
(
locationName
);
if
(
StringUtils
.
isNotBlank
(
locationName
))
{
if
(
StringUtils
.
isBlank
(
locationId
))
{
throw
new
ServiceException
(
String
.
format
(
"入库单号【%s】第%d条明细的库位【%s】不存在"
,
orderId
,
lineNo
,
locationName
));
}
itemDO
.
setLocationId
(
locationId
);
// 仓库校验
String
warehouseName
=
vo
.
getWarehouseName
()
!=
null
?
vo
.
getWarehouseName
().
trim
()
:
""
;
String
warehouseId
=
warehouseNameToIdMap
.
get
(
warehouseName
);
if
(
StringUtils
.
isBlank
(
warehouseId
))
{
throw
new
ServiceException
(
String
.
format
(
"系统未配置仓库【%s】,无法导入入库单号【%s】的第%d条明细"
,
warehouseName
,
orderId
,
lineNo
));
}
itemDO
.
setWarehouseId
(
warehouseId
);
// 库存校验(包含inventoryType)
// 库存类型设置
itemDO
.
setInventoryType
(
orderType
);
// 实际出库数量(使用计划数量)
itemDO
.
setActualQuantity
(
Optional
.
ofNullable
(
vo
.
getActualQuantity
()).
orElse
(
0L
));
// 库存校验(包含inventoryType)- 有库位才校验,无库位直接跳过
if
(
StringUtils
.
isNotBlank
(
locationName
))
{
String
inventoryTypeStr
=
Optional
.
ofNullable
(
orderType
).
map
(
String:
:
valueOf
).
orElse
(
""
);
String
inventoryMatchKey
=
String
.
join
(
"_"
,
warehouseId
,
materialId
,
locationId
,
inventoryTypeStr
);
String
inventoryMatchKey
=
String
.
join
(
"_"
,
materialId
,
locationId
,
inventoryTypeStr
);
AbstractMap
.
SimpleEntry
<
String
,
Long
>
inventoryEntry
=
inventoryTOIdMap
.
get
(
inventoryMatchKey
);
if
(
inventoryEntry
==
null
)
{
throw
new
ServiceException
(
String
.
format
(
"入库单号【%s】第%d条明细:仓库【%s】+
物料【%s】+库位【%s】+库存类型【%s】组合的库存记录不存在"
,
orderId
,
lineNo
,
vo
.
getWarehouseName
()
,
vo
.
getMaterialName
(),
vo
.
getLocationName
(),
inventoryTypeStr
));
"入库单号【%s】第%d条明细:
物料【%s】+库位【%s】+库存类型【%s】组合的库存记录不存在"
,
orderId
,
lineNo
,
vo
.
getMaterialName
(),
vo
.
getLocationName
(),
inventoryTypeStr
));
}
itemDO
.
setInventoryId
(
inventoryEntry
.
getKey
());
itemDO
.
setInventoryType
(
inventoryTypeStr
);
// 填充库存类型
itemDO
.
setItemStatus
(
3L
);
}
// 无库位时不校验库存,也不设置inventoryId
itemDOList
.
add
(
itemDO
);
}
...
...
@@ -563,18 +675,21 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
throw
new
ServiceException
(
String
.
format
(
"验证失败,导入终止!失败详情:%s"
,
failureMsg
.
toString
()));
}
// 7. 执行入库/更新操作
// 7. 执行新增操作(仅新增,无更新)
Map
<
String
,
List
<
OutboundOrderItems
>>
allItemListMap
=
new
HashMap
<>();
for
(
Map
.
Entry
<
String
,
OutboundOrders
>
entry
:
validMainMap
.
entrySet
())
{
String
orderId
=
entry
.
getKey
();
OutboundOrders
mainDO
=
entry
.
getValue
();
List
<
OutboundOrderItems
>
itemDOList
=
validItemMap
.
get
(
orderId
);
// 仅新增主单
outboundOrdersMapper
.
insertOutboundOrders
(
mainDO
);
totalMainSuccess
++;
successMsg
.
append
(
String
.
format
(
"入库单号【%s】已新增;\n"
,
orderId
));
// 插入明细(仅新增,无删除操作)
if
(!
CollectionUtils
.
isEmpty
(
itemDOList
))
{
// 批量插入新明细
int
itemSuccess
=
outboundOrderItemsMapper
.
batchInsertOutboundOrderItems
(
itemDOList
);
totalItemSuccess
+=
itemSuccess
;
int
itemFail
=
itemDOList
.
size
()
-
itemSuccess
;
...
...
@@ -584,46 +699,115 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
if
(
itemFail
>
0
)
{
failureMsg
.
append
(
String
.
format
(
"入库单号【%s】有%d条物料明细导入失败;\n"
,
orderId
,
itemFail
));
}
allItemListMap
.
put
(
orderId
,
itemDOList
);
}
}
// 8.
事务提交后执行库存扣减
if
(
TransactionSynchronizationManager
.
isSynchronizationActive
())
{
Map
<
String
,
List
<
OutboundOrderItems
>>
finalValidItemMap
=
new
HashMap
<>(
validItemMap
);
String
finalOperId
=
operId
;
Date
finalNow
=
now
;
// 8.
执行库存扣减并处理无库位明细拆分
for
(
Map
.
Entry
<
String
,
List
<
OutboundOrderItems
>>
entry
:
allItemListMap
.
entrySet
())
{
List
<
OutboundOrderItems
>
itemList
=
entry
.
getValue
(
);
// 执行库存扣减,获取无库位明细的扣减记录
Map
<
String
,
List
<
Map
<
String
,
Object
>>>
deductRecordMap
=
deductInventory
(
itemList
,
operId
,
now
)
;
TransactionSynchronizationManager
.
registerSynchronization
(
new
TransactionSynchronization
()
{
@Override
public
void
afterCommit
()
{
for
(
List
<
OutboundOrderItems
>
itemList
:
finalValidItemMap
.
values
())
{
deductInventory
(
itemList
,
finalOperId
,
finalNow
);
}
}
});
}
else
{
for
(
List
<
OutboundOrderItems
>
itemList
:
validItemMap
.
values
())
{
deductInventory
(
itemList
,
operId
,
now
);
// 处理无库位明细拆分:整理有效明细后统一插入
if
(!
deductRecordMap
.
isEmpty
())
{
handleNoLocationItemSplit
(
itemList
,
deductRecordMap
,
operId
,
now
);
}
}
// 9. 结果汇总
if
(
totalMainFailure
>
0
||
totalItemFailure
>
0
)
{
String
finalFailureMsg
=
String
.
format
(
"导入结果:成功新增
/更新
%d个入库单,失败%d个;成功导入%d条明细,失败%d条。失败详情:%s"
,
"导入结果:成功新增%d个入库单,失败%d个;成功导入%d条明细,失败%d条。失败详情:%s"
,
totalMainSuccess
,
totalMainFailure
,
totalItemSuccess
,
totalItemFailure
,
failureMsg
.
toString
()
);
throw
new
ServiceException
(
finalFailureMsg
);
}
else
{
String
finalSuccessMsg
=
String
.
format
(
"恭喜您,数据已全部导入成功!共
处理
%d个入库单,成功导入%d条物料明细。详情:%s"
,
"恭喜您,数据已全部导入成功!共
新增
%d个入库单,成功导入%d条物料明细。详情:%s"
,
totalMainSuccess
,
totalItemSuccess
,
successMsg
.
toString
()
);
return
finalSuccessMsg
;
}
}
// ========== 预加载映射辅助方法 ==========
/**
* 处理无库位明细拆分:
* 1. 过滤无效扣减(≤0),按库位合并有效扣减数量
* 2. 仅新增场景:删除临时明细,插入整理后的有效明细
* 3. 严格保留原逻辑,仅适配新增场景
*/
private
void
handleNoLocationItemSplit
(
List
<
OutboundOrderItems
>
itemList
,
Map
<
String
,
List
<
Map
<
String
,
Object
>>>
deductRecordMap
,
String
operId
,
Date
now
)
{
List
<
OutboundOrderItems
>
newValidItemList
=
new
ArrayList
<>();
Set
<
String
>
orderIdSet
=
new
HashSet
<>();
// 第一步:遍历扣减记录,整理有效明细(过滤≤0,合并同库位)
for
(
OutboundOrderItems
item
:
itemList
)
{
String
itemId
=
item
.
getId
();
List
<
Map
<
String
,
Object
>>
deductRecords
=
deductRecordMap
.
get
(
itemId
);
if
(
CollectionUtils
.
isEmpty
(
deductRecords
))
continue
;
// 收集订单ID(用于删除临时明细)
orderIdSet
.
add
(
item
.
getOutboundOrderId
());
// 按库位合并扣减数量,过滤≤0的无效记录
Map
<
String
,
Long
>
locationQtyMap
=
new
HashMap
<>();
for
(
Map
<
String
,
Object
>
rec
:
deductRecords
)
{
String
locId
=
(
String
)
rec
.
get
(
"locationId"
);
Long
deductQty
=
(
Long
)
rec
.
get
(
"deductQty"
);
if
(
deductQty
<=
0
)
continue
;
// 过滤无效扣减
locationQtyMap
.
put
(
locId
,
locationQtyMap
.
getOrDefault
(
locId
,
0L
)
+
deductQty
);
}
// 生成有效明细
for
(
Map
.
Entry
<
String
,
Long
>
entry
:
locationQtyMap
.
entrySet
())
{
String
locId
=
entry
.
getKey
();
Long
validQty
=
entry
.
getValue
();
OutboundOrderItems
newItem
=
new
OutboundOrderItems
();
BeanUtils
.
copyProperties
(
item
,
newItem
);
newItem
.
setId
(
UUID
.
randomUUID
().
toString
().
replace
(
"-"
,
""
));
newItem
.
setLocationId
(
locId
);
newItem
.
setActualQuantity
(
validQty
);
newItem
.
setInventoryId
(
deductRecords
.
stream
()
.
filter
(
r
->
locId
.
equals
(
r
.
get
(
"locationId"
)))
.
findFirst
()
.
map
(
r
->
(
String
)
r
.
get
(
"inventoryId"
))
.
orElse
(
""
));
newItem
.
setCreateBy
(
operId
);
newItem
.
setCreateTime
(
now
);
newItem
.
setUpdateBy
(
operId
);
newItem
.
setUpdateTime
(
now
);
newValidItemList
.
add
(
newItem
);
}
}
// 第二步:删除临时明细(导入时插入的原始明细)
for
(
String
orderId
:
orderIdSet
)
{
outboundOrderItemsMapper
.
deleteOutboundOrderItemsByOrderId
(
orderId
);
}
// 第三步:批量插入整理后的有效明细
if
(!
newValidItemList
.
isEmpty
())
{
outboundOrderItemsMapper
.
batchInsertOutboundOrderItems
(
newValidItemList
);
// 同步生成日志(保留原日志逻辑)
List
<
OutboundOrderLog
>
logList
=
new
ArrayList
<>();
for
(
OutboundOrderItems
newItem
:
newValidItemList
)
{
OutboundOrderLog
log
=
new
OutboundOrderLog
();
BeanUtils
.
copyProperties
(
newItem
,
log
);
log
.
setOrderId
(
newItem
.
getOutboundOrderId
());
log
.
setItemStatus
(
3L
);
logList
.
add
(
log
);
}
outboundOrderLogMapper
.
batchOutboundOrderLog
(
logList
);
}
}
// ========== 预加载映射辅助方法(移除仓库相关) ==========
private
Map
<
String
,
String
>
loadSapToMaterialIdMap
()
{
List
<
Materials
>
materialsList
=
materialsService
.
selectMaterialsList
(
new
Materials
());
if
(
CollectionUtils
.
isEmpty
(
materialsList
))
{
...
...
@@ -654,22 +838,6 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
));
}
private
Map
<
String
,
String
>
loadWarehouseNameToIdMap
()
{
Warehouses
query
=
new
Warehouses
();
query
.
setIsUsed
(
1L
);
List
<
Warehouses
>
warehouseList
=
warehousesService
.
selectWarehousesList
(
query
);
if
(
CollectionUtils
.
isEmpty
(
warehouseList
))
{
return
Collections
.
emptyMap
();
}
return
warehouseList
.
stream
()
.
filter
(
w
->
StringUtils
.
isNotBlank
(
w
.
getWarehousesName
()))
.
collect
(
Collectors
.
toMap
(
w
->
w
.
getWarehousesName
().
trim
(),
Warehouses:
:
getId
,
(
k1
,
k2
)
->
k1
));
}
private
Map
<
String
,
String
>
loadOwnerNameToIdMap
()
{
List
<
Owners
>
ownerList
=
ownersService
.
selectOwnersList
(
new
Owners
());
if
(
CollectionUtils
.
isEmpty
(
ownerList
))
{
...
...
@@ -684,6 +852,10 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
));
}
/**
* 加载库存映射Map(移除仓库维度)
* Key=物料ID_库位ID_库存类型
*/
private
Map
<
String
,
AbstractMap
.
SimpleEntry
<
String
,
Long
>>
loadInventoryTOIdMap
()
{
Inventory
inventory
=
new
Inventory
();
inventory
.
setInventoryStatus
(
1L
);
...
...
@@ -695,14 +867,12 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
}
Map
<
String
,
AbstractMap
.
SimpleEntry
<
String
,
Long
>>
emptyLocationMap
=
inventoryList
.
stream
()
.
filter
(
inv
->
StringUtils
.
isNotBlank
(
inv
.
getWarehousesId
())
&&
StringUtils
.
isNotBlank
(
inv
.
getMaterialId
())
.
filter
(
inv
->
StringUtils
.
isNotBlank
(
inv
.
getMaterialId
())
&&
inv
.
getInventoryType
()
!=
null
&&
StringUtils
.
isNotBlank
(
inv
.
getId
())
&&
StringUtils
.
isBlank
(
inv
.
getLocationId
()))
.
collect
(
Collectors
.
toMap
(
inv
->
String
.
join
(
"_"
,
inv
.
getWarehousesId
().
trim
(),
inv
.
getMaterialId
().
trim
(),
""
,
inv
.
getInventoryType
().
toString
()),
inv
->
new
AbstractMap
.
SimpleEntry
<>(
inv
.
getId
().
trim
(),
Optional
.
ofNullable
(
inv
.
getQuantity
()).
orElse
(
0L
)),
...
...
@@ -711,14 +881,12 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
));
Map
<
String
,
AbstractMap
.
SimpleEntry
<
String
,
Long
>>
nonEmptyLocationMap
=
inventoryList
.
stream
()
.
filter
(
inv
->
StringUtils
.
isNotBlank
(
inv
.
getWarehousesId
())
&&
StringUtils
.
isNotBlank
(
inv
.
getMaterialId
())
.
filter
(
inv
->
StringUtils
.
isNotBlank
(
inv
.
getMaterialId
())
&&
StringUtils
.
isNotBlank
(
inv
.
getLocationId
())
&&
StringUtils
.
isNotBlank
(
inv
.
getId
())
&&
inv
.
getInventoryType
()
!=
null
)
.
collect
(
Collectors
.
toMap
(
inv
->
String
.
join
(
"_"
,
inv
.
getWarehousesId
().
trim
(),
inv
.
getMaterialId
().
trim
(),
inv
.
getLocationId
().
trim
(),
inv
.
getInventoryType
().
toString
()),
...
...
ruoyi-inventory/src/main/java/com/ruoyi/inventory/service/impl/StorageLocationsServiceImpl.java
View file @
c7cf6243
...
...
@@ -128,6 +128,7 @@ public class StorageLocationsServiceImpl implements IStorageLocationsService
storageLocations
.
setId
(
LocationsID
);
storageLocations
.
setCreateTime
(
DateUtils
.
getNowDate
());
storageLocations
.
setCreateUserCode
(
String
.
valueOf
(
SecurityUtils
.
getUserId
()));
storageLocations
.
setWarehousesId
(
WarehouseConfig
.
DEFAULT_WAREHOUSE_ID
);
storageLocationsCategory
.
setLocationCode
(
LocationsID
);
...
...
ruoyi-inventory/src/main/resources/mapper/inventory/InventoryMapper.xml
View file @
c7cf6243
...
...
@@ -586,4 +586,17 @@ and inventory_status = '1'
group by m.id, m.material_name
order by value desc, m.material_name asc
</select>
<update
id=
"batchUpdateInventory"
>
<foreach
collection=
"list"
item=
"item"
separator=
";"
>
UPDATE inventory
<set>
quantity = #{item.quantity},
inventory_status = #{item.inventoryStatus},
update_user_code = #{item.updateBy},
update_time = #{item.updateTime}
</set>
WHERE id = #{item.id}
</foreach>
</update>
</mapper>
\ No newline at end of file
ruoyi-inventory/src/main/resources/mapper/inventory/OutboundOrderItemsMapper.xml
View file @
c7cf6243
...
...
@@ -367,6 +367,9 @@
</foreach>
</update>
<delete
id=
"deleteOutboundOrderItemsByOrderId"
parameterType=
"String"
>
delete from outbound_order_items where outbound_order_id = #{orderId}
</delete>
<!-- 单条删除:逻辑删除 -->
<update
id=
"deleteOutboundOrderItemsById"
parameterType=
"String"
>
update outbound_order_items
...
...
@@ -432,4 +435,39 @@
)
</foreach>
</insert>
<update
id=
"batchUpdateOutboundOrderItems"
parameterType=
"java.util.List"
>
<foreach
collection=
"list"
item=
"item"
separator=
";"
>
UPDATE outbound_order_items
SET
inventory_id =
<if
test=
"item.inventoryId != null"
>
#{item.inventoryId}
</if><if
test=
"item.inventoryId == null"
>
null
</if>
,
material_id =
<if
test=
"item.materialId != null"
>
#{item.materialId}
</if><if
test=
"item.materialId == null"
>
null
</if>
,
batch_code =
<if
test=
"item.batchCode != null"
>
#{item.batchCode}
</if><if
test=
"item.batchCode == null"
>
null
</if>
,
warehouse_id =
<if
test=
"item.warehouseId != null"
>
#{item.warehouseId}
</if><if
test=
"item.warehouseId == null"
>
null
</if>
,
location_id =
<if
test=
"item.locationId != null"
>
#{item.locationId}
</if><if
test=
"item.locationId == null"
>
null
</if>
,
unit_price =
<if
test=
"item.unitPrice != null"
>
#{item.unitPrice}
</if><if
test=
"item.unitPrice == null"
>
null
</if>
,
planned_quantity =
<if
test=
"item.plannedQuantity != null"
>
#{item.plannedQuantity}
</if><if
test=
"item.plannedQuantity == null"
>
null
</if>
,
actual_quantity =
<if
test=
"item.actualQuantity != null"
>
#{item.actualQuantity}
</if><if
test=
"item.actualQuantity == null"
>
null
</if>
,
divisor =
<if
test=
"item.divisor != null"
>
#{item.divisor}
</if><if
test=
"item.divisor == null"
>
null
</if>
,
label_color =
<if
test=
"item.labelColor != null"
>
#{item.labelColor}
</if><if
test=
"item.labelColor == null"
>
null
</if>
,
voucher_number =
<if
test=
"item.voucherNumber != null"
>
#{item.voucherNumber}
</if><if
test=
"item.voucherNumber == null"
>
null
</if>
,
item_status =
<if
test=
"item.itemStatus != null"
>
#{item.itemStatus}
</if><if
test=
"item.itemStatus == null"
>
null
</if>
,
shipped_at =
<if
test=
"item.shippedAt != null"
>
#{item.shippedAt}
</if><if
test=
"item.shippedAt == null"
>
null
</if>
,
shipped_by =
<if
test=
"item.shippedBy != null"
>
#{item.shippedBy}
</if><if
test=
"item.shippedBy == null"
>
null
</if>
,
remark =
<if
test=
"item.remark != null"
>
#{item.remark}
</if><if
test=
"item.remark == null"
>
null
</if>
,
is_used =
<if
test=
"item.isUsed != null"
>
#{item.isUsed}
</if><if
test=
"item.isUsed == null"
>
1
</if>
,
sort_no =
<if
test=
"item.sortNo != null"
>
#{item.sortNo}
</if><if
test=
"item.sortNo == null"
>
null
</if>
,
update_time =
<if
test=
"item.updateTime != null"
>
#{item.updateTime}
</if><if
test=
"item.updateTime == null"
>
NOW()
</if>
,
update_user_code =
<if
test=
"item.updateUserCode != null"
>
#{item.updateUserCode}
</if><if
test=
"item.updateUserCode == null"
>
null
</if>
WHERE id = #{item.id}
</foreach>
</update>
<delete
id=
"batchDeleteOutboundOrderItems"
>
DELETE FROM outbound_order_items
WHERE id IN
<foreach
collection=
"array"
item=
"id"
open=
"("
separator=
","
close=
")"
>
#{id}
</foreach>
</delete>
</mapper>
\ No newline at end of file
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论