feat(asset): 优化调拨单新增页面和后端控制器
- 更新页面标题为新增调拨单 - 添加表单验证样式规则 - 重构基本信息表单布局结构 - 实现资产选择功能替换原来的静态行添加 - 添加调拨明细表格的动态操作功能 - 集成仓库、位置、部门和用户选择下拉框 - 添加资产状态数据字典支持 - 实现调拨单实体类注释完善和字段调整 - 增加可调拨资产选择器接口支持 - 优化控制器方法参数和返回处理 - 添加权限控制和数据验证机制main
parent
ebb710c43c
commit
3bb8cff513
@ -0,0 +1,22 @@
|
||||
package com.ruoyi.asset.constant;
|
||||
|
||||
/**
|
||||
* 调拨单状态常量
|
||||
*
|
||||
* @author Yangk
|
||||
*/
|
||||
public final class TransferOrderStatus
|
||||
{
|
||||
/** 草稿 */
|
||||
public static final String DRAFT = "DRAFT";
|
||||
|
||||
/** 待确认(已提交,等待确认人确认调拨) */
|
||||
public static final String PENDING_CONFIRM = "PENDING_CONFIRM";
|
||||
|
||||
/** 已调拨(确认完成,资产归属已更新) */
|
||||
public static final String TRANSFERRED = "TRANSFERRED";
|
||||
|
||||
private TransferOrderStatus()
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,115 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:include="include :: header('选择可调拨资产')" />
|
||||
</head>
|
||||
<body class="gray-bg">
|
||||
<div class="container-div">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 search-collapse">
|
||||
<form id="formId">
|
||||
<div class="select-list">
|
||||
<ul>
|
||||
<li>
|
||||
<label>资产编码:</label>
|
||||
<input type="text" name="assetCode">
|
||||
</li>
|
||||
<li>
|
||||
<label>资产名称:</label>
|
||||
<input type="text" name="assetName">
|
||||
</li>
|
||||
<li>
|
||||
<label>资产类别:</label>
|
||||
<input type="text" name="categoryName">
|
||||
</li>
|
||||
<li>
|
||||
<label>资产状态:</label>
|
||||
<select name="assetStatus" th:with="type=${@dict.getType('ams_asset_status')}">
|
||||
<option value="">所有</option>
|
||||
<option th:each="dict : ${type}"
|
||||
th:if="${dict.dictValue == 'IN_STOCK' || dict.dictValue == 'IN_USE'}"
|
||||
th:text="${dict.dictLabel}" th:value="${dict.dictValue}"></option>
|
||||
</select>
|
||||
</li>
|
||||
<li>
|
||||
<a class="btn btn-primary btn-rounded btn-sm" onclick="$.table.search()">
|
||||
<i class="fa fa-search"></i> 搜索
|
||||
</a>
|
||||
<a class="btn btn-warning btn-rounded btn-sm" onclick="$.form.reset()">
|
||||
<i class="fa fa-refresh"></i> 重置
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-sm-12 select-table table-striped">
|
||||
<table id="bootstrap-table"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:include="include :: footer" />
|
||||
<script th:inline="javascript">
|
||||
var prefix = ctx + "asset/transfer";
|
||||
var orderId = [[${orderId}]];
|
||||
var assetStatusDatas = [[${@dict.getType('ams_asset_status')}]];
|
||||
|
||||
$(function() {
|
||||
var url = prefix + "/availableAssetList";
|
||||
if (orderId) {
|
||||
url += "?orderId=" + orderId;
|
||||
}
|
||||
$.table.init({
|
||||
url: url,
|
||||
showSearch: false,
|
||||
showRefresh: true,
|
||||
showToggle: false,
|
||||
showColumns: false,
|
||||
modalName: "可调拨资产",
|
||||
columns: [{
|
||||
checkbox: true
|
||||
},
|
||||
{
|
||||
field: "assetCode",
|
||||
title: "资产编码"
|
||||
},
|
||||
{
|
||||
field: "assetName",
|
||||
title: "资产名称"
|
||||
},
|
||||
{
|
||||
field: "categoryName",
|
||||
title: "资产类别"
|
||||
},
|
||||
{
|
||||
field: "assetStatus",
|
||||
title: "资产状态",
|
||||
formatter: function(value) {
|
||||
return $.table.selectDictLabel(assetStatusDatas, value);
|
||||
}
|
||||
},
|
||||
{
|
||||
field: "warehouseName",
|
||||
title: "当前仓库"
|
||||
},
|
||||
{
|
||||
field: "locationName",
|
||||
title: "当前位置"
|
||||
},
|
||||
{
|
||||
field: "useDeptName",
|
||||
title: "当前部门"
|
||||
},
|
||||
{
|
||||
field: "useUserName",
|
||||
title: "当前使用人"
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
function getSelectedAssets() {
|
||||
return $("#bootstrap-table").bootstrapTable("getSelections");
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,327 @@
|
||||
package com.ruoyi.asset.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import com.ruoyi.asset.constant.AssetStatus;
|
||||
import com.ruoyi.asset.constant.TransferOrderStatus;
|
||||
import com.ruoyi.asset.domain.AmsAsset;
|
||||
import com.ruoyi.asset.domain.AmsAssetLocation;
|
||||
import com.ruoyi.asset.domain.AmsTransferOrder;
|
||||
import com.ruoyi.asset.domain.AmsTransferOrderItem;
|
||||
import com.ruoyi.asset.domain.AmsWarehouse;
|
||||
import com.ruoyi.asset.domain.AssetTransitionContext;
|
||||
import com.ruoyi.asset.mapper.AmsAssetMapper;
|
||||
import com.ruoyi.asset.mapper.AmsTransferOrderMapper;
|
||||
import com.ruoyi.asset.service.IAmsAssetLocationService;
|
||||
import com.ruoyi.asset.service.IAmsWarehouseService;
|
||||
import com.ruoyi.asset.service.IAssetStatusTransitionService;
|
||||
import com.ruoyi.common.core.domain.entity.SysDept;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.system.service.ISysCodeRuleService;
|
||||
import com.ruoyi.system.service.ISysDeptService;
|
||||
import com.ruoyi.system.service.ISysUserService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AmsTransferOrderServiceImplTest
|
||||
{
|
||||
@Mock
|
||||
private AmsTransferOrderMapper amsTransferOrderMapper;
|
||||
|
||||
@Mock
|
||||
private AmsAssetMapper amsAssetMapper;
|
||||
|
||||
@Mock
|
||||
private ISysCodeRuleService sysCodeRuleService;
|
||||
|
||||
@Mock
|
||||
private IAmsWarehouseService amsWarehouseService;
|
||||
|
||||
@Mock
|
||||
private IAmsAssetLocationService amsAssetLocationService;
|
||||
|
||||
@Mock
|
||||
private ISysDeptService sysDeptService;
|
||||
|
||||
@Mock
|
||||
private ISysUserService sysUserService;
|
||||
|
||||
@Mock
|
||||
private IAssetStatusTransitionService assetStatusTransitionService;
|
||||
|
||||
@InjectMocks
|
||||
private AmsTransferOrderServiceImpl service;
|
||||
|
||||
/** 在库资产新增调拨草稿时,应生成单号、回填仓位快照并清空目标使用归属。 */
|
||||
@Test
|
||||
void insertStockTransferShouldGenerateCodeAndFillSnapshots()
|
||||
{
|
||||
AmsTransferOrder order = buildRequest(AssetStatus.IN_STOCK);
|
||||
AmsTransferOrderItem item = order.getAmsTransferOrderItemList().get(0);
|
||||
item.setNewDeptId(103L);
|
||||
item.setNewUserId(1L);
|
||||
when(sysCodeRuleService.nextCode("TRANSFER_ORDER")).thenReturn("DB202606120001");
|
||||
stubStockAsset();
|
||||
stubTargetWarehouseLocation();
|
||||
doAnswer(invocation -> {
|
||||
AmsTransferOrder inserted = invocation.getArgument(0);
|
||||
inserted.setOrderId(100L);
|
||||
return 1;
|
||||
}).when(amsTransferOrderMapper).insertAmsTransferOrder(any(AmsTransferOrder.class));
|
||||
when(amsTransferOrderMapper.batchAmsTransferOrderItem(anyList()))
|
||||
.thenAnswer(invocation -> ((List<?>) invocation.getArgument(0)).size());
|
||||
|
||||
assertEquals(1, service.insertAmsTransferOrder(order, 1L, "管理员"));
|
||||
|
||||
assertEquals("DB202606120001", order.getTransferNo());
|
||||
assertEquals(TransferOrderStatus.DRAFT, order.getOrderStatus());
|
||||
assertEquals(1L, order.getApplicantId());
|
||||
assertEquals("WH-001", item.getOldWarehouseCode());
|
||||
assertEquals("WH-002", item.getNewWarehouseCode());
|
||||
assertNull(item.getNewDeptId());
|
||||
assertNull(item.getNewUserId());
|
||||
assertEquals(100L, item.getOrderId());
|
||||
assertNotNull(item.getCreateTime());
|
||||
}
|
||||
|
||||
/** 在用资产调拨必须回填有效的新部门和新使用人快照。 */
|
||||
@Test
|
||||
void insertInUseTransferShouldFillNewUseOwnership()
|
||||
{
|
||||
AmsTransferOrder order = buildRequest(AssetStatus.IN_USE);
|
||||
when(sysCodeRuleService.nextCode("TRANSFER_ORDER")).thenReturn("DB202606120001");
|
||||
stubInUseAsset();
|
||||
stubTargetWarehouseLocation();
|
||||
stubTargetDeptUser();
|
||||
doAnswer(invocation -> {
|
||||
AmsTransferOrder inserted = invocation.getArgument(0);
|
||||
inserted.setOrderId(100L);
|
||||
return 1;
|
||||
}).when(amsTransferOrderMapper).insertAmsTransferOrder(any(AmsTransferOrder.class));
|
||||
when(amsTransferOrderMapper.batchAmsTransferOrderItem(anyList())).thenReturn(1);
|
||||
|
||||
service.insertAmsTransferOrder(order, 1L, "管理员");
|
||||
|
||||
AmsTransferOrderItem item = order.getAmsTransferOrderItemList().get(0);
|
||||
assertEquals("研发部门", item.getNewDeptName());
|
||||
assertEquals("管理员", item.getNewUserName());
|
||||
}
|
||||
|
||||
/** 被其他草稿或待确认调拨单占用的资产不得再次加入调拨单。 */
|
||||
@Test
|
||||
void insertShouldRejectAssetOccupiedByOtherActiveTransfer()
|
||||
{
|
||||
AmsTransferOrder order = buildRequest(AssetStatus.IN_STOCK);
|
||||
when(sysCodeRuleService.nextCode("TRANSFER_ORDER")).thenReturn("DB202606120001");
|
||||
stubStockAsset();
|
||||
when(amsTransferOrderMapper.countOtherActiveTransferOrderByAssetId(1L, null,
|
||||
TransferOrderStatus.DRAFT, TransferOrderStatus.PENDING_CONFIRM)).thenReturn(1);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> service.insertAmsTransferOrder(order, 1L, "管理员"));
|
||||
|
||||
assertTrue(exception.getMessage().contains("其他有效调拨单"));
|
||||
verify(amsTransferOrderMapper, never()).insertAmsTransferOrder(any(AmsTransferOrder.class));
|
||||
}
|
||||
|
||||
/** 草稿提交前校验当前归属未变化,成功后进入待确认状态。 */
|
||||
@Test
|
||||
void submitShouldValidateAndMoveToPendingConfirm()
|
||||
{
|
||||
AmsTransferOrder order = buildPersistedOrder(TransferOrderStatus.DRAFT, AssetStatus.IN_STOCK);
|
||||
when(amsTransferOrderMapper.selectAmsTransferOrderByOrderIdForUpdate(100L)).thenReturn(order);
|
||||
stubStockAsset();
|
||||
stubTargetWarehouseLocation();
|
||||
when(amsTransferOrderMapper.submitAmsTransferOrder(any(AmsTransferOrder.class))).thenReturn(1);
|
||||
|
||||
assertEquals(1, service.submitTransfer(100L, "admin"));
|
||||
|
||||
assertEquals(TransferOrderStatus.PENDING_CONFIRM, order.getOrderStatus());
|
||||
verify(amsTransferOrderMapper).submitAmsTransferOrder(order);
|
||||
}
|
||||
|
||||
/** 待确认调拨单确认时,应逐条调用公共流转服务并写入确认信息。 */
|
||||
@Test
|
||||
void confirmShouldDelegateTransitionAndCompleteOrder()
|
||||
{
|
||||
AmsTransferOrder order = buildPersistedOrder(TransferOrderStatus.PENDING_CONFIRM, AssetStatus.IN_STOCK);
|
||||
when(amsTransferOrderMapper.selectAmsTransferOrderByOrderIdForUpdate(100L)).thenReturn(order);
|
||||
stubStockAsset();
|
||||
stubTargetWarehouseLocation();
|
||||
when(amsTransferOrderMapper.confirmAmsTransferOrder(any(AmsTransferOrder.class))).thenReturn(1);
|
||||
|
||||
assertEquals(1, service.confirmTransfer(100L, 1L, "管理员", "admin"));
|
||||
|
||||
ArgumentCaptor<AssetTransitionContext> contextCaptor = ArgumentCaptor.forClass(AssetTransitionContext.class);
|
||||
verify(assetStatusTransitionService).confirmTransfer(
|
||||
org.mockito.ArgumentMatchers.eq(1L),
|
||||
org.mockito.ArgumentMatchers.eq(2L),
|
||||
org.mockito.ArgumentMatchers.eq(20L),
|
||||
org.mockito.ArgumentMatchers.isNull(),
|
||||
org.mockito.ArgumentMatchers.isNull(),
|
||||
contextCaptor.capture());
|
||||
assertEquals(100L, contextCaptor.getValue().getSourceOrderId());
|
||||
assertEquals(101L, contextCaptor.getValue().getSourceItemId());
|
||||
assertEquals(TransferOrderStatus.TRANSFERRED, order.getOrderStatus());
|
||||
assertEquals("管理员", order.getConfirmUserName());
|
||||
assertNotNull(order.getConfirmTime());
|
||||
}
|
||||
|
||||
/** 调拨草稿保存后的原归属发生变化时,提交必须阻断,避免覆盖新归属。 */
|
||||
@Test
|
||||
void submitShouldRejectStaleOldOwnership()
|
||||
{
|
||||
AmsTransferOrder order = buildPersistedOrder(TransferOrderStatus.DRAFT, AssetStatus.IN_STOCK);
|
||||
order.getAmsTransferOrderItemList().get(0).setOldLocationId(99L);
|
||||
when(amsTransferOrderMapper.selectAmsTransferOrderByOrderIdForUpdate(100L)).thenReturn(order);
|
||||
stubStockAsset();
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> service.submitTransfer(100L, "admin"));
|
||||
|
||||
assertTrue(exception.getMessage().contains("当前归属已变化"));
|
||||
verify(amsTransferOrderMapper, never()).submitAmsTransferOrder(any(AmsTransferOrder.class));
|
||||
}
|
||||
|
||||
/** 非草稿调拨单不得删除。 */
|
||||
@Test
|
||||
void deleteShouldRejectNonDraftOrder()
|
||||
{
|
||||
AmsTransferOrder order = new AmsTransferOrder();
|
||||
order.setOrderId(100L);
|
||||
order.setOrderStatus(TransferOrderStatus.PENDING_CONFIRM);
|
||||
when(amsTransferOrderMapper.selectAmsTransferOrderByOrderIdForUpdate(100L)).thenReturn(order);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> service.deleteAmsTransferOrderByOrderId(100L));
|
||||
|
||||
assertTrue(exception.getMessage().contains("仅草稿"));
|
||||
verify(amsTransferOrderMapper, never()).deleteAmsTransferOrderByOrderId(100L);
|
||||
}
|
||||
|
||||
private AmsTransferOrder buildRequest(String assetStatus)
|
||||
{
|
||||
AmsTransferOrder order = new AmsTransferOrder();
|
||||
order.setCreateBy("admin");
|
||||
order.setTransferReason("调整归属");
|
||||
AmsTransferOrderItem item = new AmsTransferOrderItem();
|
||||
item.setAssetId(1L);
|
||||
item.setAssetStatus(assetStatus);
|
||||
item.setNewWarehouseId(2L);
|
||||
item.setNewLocationId(20L);
|
||||
if (AssetStatus.IN_USE.equals(assetStatus))
|
||||
{
|
||||
item.setNewDeptId(103L);
|
||||
item.setNewUserId(1L);
|
||||
}
|
||||
order.setAmsTransferOrderItemList(Collections.singletonList(item));
|
||||
return order;
|
||||
}
|
||||
|
||||
private AmsTransferOrder buildPersistedOrder(String orderStatus, String assetStatus)
|
||||
{
|
||||
AmsTransferOrder order = buildRequest(assetStatus);
|
||||
order.setOrderId(100L);
|
||||
order.setTransferNo("DB202606120001");
|
||||
order.setOrderStatus(orderStatus);
|
||||
AmsTransferOrderItem item = order.getAmsTransferOrderItemList().get(0);
|
||||
item.setItemId(101L);
|
||||
item.setOldWarehouseId(1L);
|
||||
item.setOldLocationId(10L);
|
||||
if (AssetStatus.IN_USE.equals(assetStatus))
|
||||
{
|
||||
item.setOldDeptId(105L);
|
||||
item.setOldUserId(2L);
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
private void stubStockAsset()
|
||||
{
|
||||
AmsAsset asset = buildAsset(AssetStatus.IN_STOCK);
|
||||
when(amsAssetMapper.selectAmsAssetByAssetIdForUpdate(1L)).thenReturn(asset);
|
||||
}
|
||||
|
||||
private void stubInUseAsset()
|
||||
{
|
||||
AmsAsset asset = buildAsset(AssetStatus.IN_USE);
|
||||
asset.setUseDeptId(105L);
|
||||
asset.setUseDeptName("测试部门");
|
||||
asset.setUseUserId(2L);
|
||||
asset.setUseUserName("测试用户");
|
||||
when(amsAssetMapper.selectAmsAssetByAssetIdForUpdate(1L)).thenReturn(asset);
|
||||
}
|
||||
|
||||
private AmsAsset buildAsset(String status)
|
||||
{
|
||||
AmsAsset asset = new AmsAsset();
|
||||
asset.setAssetId(1L);
|
||||
asset.setAssetCode("ASSET-001");
|
||||
asset.setAssetName("测试资产");
|
||||
asset.setCategoryId(3L);
|
||||
asset.setCategoryCode("CAT-003");
|
||||
asset.setCategoryName("测试类别");
|
||||
asset.setAssetStatus(status);
|
||||
asset.setWarehouseId(1L);
|
||||
asset.setWarehouseCode("WH-001");
|
||||
asset.setWarehouseName("一号仓");
|
||||
asset.setLocationId(10L);
|
||||
asset.setLocationCode("LOC-010");
|
||||
asset.setLocationName("一号仓A区");
|
||||
return asset;
|
||||
}
|
||||
|
||||
private void stubTargetWarehouseLocation()
|
||||
{
|
||||
AmsWarehouse warehouse = new AmsWarehouse();
|
||||
warehouse.setWarehouseId(2L);
|
||||
warehouse.setWarehouseCode("WH-002");
|
||||
warehouse.setWarehouseName("二号仓");
|
||||
warehouse.setEnabled("Y");
|
||||
when(amsWarehouseService.selectAmsWarehouseByWarehouseId(2L)).thenReturn(warehouse);
|
||||
|
||||
AmsAssetLocation location = new AmsAssetLocation();
|
||||
location.setLocationId(20L);
|
||||
location.setLocationCode("LOC-020");
|
||||
location.setLocationName("二号仓A区");
|
||||
location.setWarehouseId(2L);
|
||||
location.setEnabled("Y");
|
||||
when(amsAssetLocationService.selectAmsAssetLocationByLocationId(20L)).thenReturn(location);
|
||||
}
|
||||
|
||||
private void stubTargetDeptUser()
|
||||
{
|
||||
SysDept dept = new SysDept();
|
||||
dept.setDeptId(103L);
|
||||
dept.setDeptName("研发部门");
|
||||
dept.setStatus("0");
|
||||
dept.setDelFlag("0");
|
||||
when(sysDeptService.selectDeptById(103L)).thenReturn(dept);
|
||||
|
||||
SysUser user = new SysUser();
|
||||
user.setUserId(1L);
|
||||
user.setUserName("管理员");
|
||||
user.setDeptId(103L);
|
||||
user.setStatus("0");
|
||||
user.setDelFlag("0");
|
||||
when(sysUserService.selectUserById(1L)).thenReturn(user);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue