feat(asset): 添加资产入库管理功能

- 新增入库单页面实现资产入库流程
- 添加入库订单实体类及明细项目实体类
- 实现入库管理控制器提供完整的CRUD操作
- 创建入库订单数据访问层及映射文件
- 开发入库服务业务逻辑处理
- 集成仓库、资产位置和资产数据联动功能
- 实现入库单状态管理和确认入库功能
- 添加入库单导出和权限控制功能
main
yangk 2 weeks ago
parent f79d622544
commit d67da452a2

@ -0,0 +1,19 @@
package com.ruoyi.asset.constant;
/**
*
*
* @author Yangk
*/
public final class InboundOrderStatus
{
/** 草稿 */
public static final String DRAFT = "DRAFT";
/** 已入库 */
public static final String INBOUND_DONE = "INBOUND_DONE";
private InboundOrderStatus()
{
}
}

@ -0,0 +1,196 @@
package com.ruoyi.asset.controller;
import java.util.List;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.asset.domain.AmsInboundOrder;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsAssetLocation;
import com.ruoyi.asset.domain.AmsWarehouse;
import com.ruoyi.asset.constant.AssetStatus;
import com.ruoyi.asset.service.IAmsAssetLocationService;
import com.ruoyi.asset.service.IAmsAssetService;
import com.ruoyi.asset.service.IAmsInboundOrderService;
import com.ruoyi.asset.service.IAmsWarehouseService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;
/**
* Controller
*
* @author Yangk
* @date 2026-06-10
*/
@Controller
@RequestMapping("/asset/inbound")
public class AmsInboundOrderController extends BaseController
{
private static final String ENABLED_YES = "Y";
private String prefix = "asset/inbound";
@Autowired
private IAmsInboundOrderService amsInboundOrderService;
@Autowired
private IAmsWarehouseService amsWarehouseService;
@Autowired
private IAmsAssetLocationService amsAssetLocationService;
@Autowired
private IAmsAssetService amsAssetService;
@RequiresPermissions("asset:inbound:view")
@GetMapping()
public String inbound(ModelMap mmap)
{
mmap.put("warehouseList", selectEnabledWarehouseList());
return prefix + "/inbound";
}
/**
*
*/
@RequiresPermissions("asset:inbound:list")
@PostMapping("/list")
@ResponseBody
public TableDataInfo list(AmsInboundOrder amsInboundOrder)
{
startPage();
List<AmsInboundOrder> list = amsInboundOrderService.selectAmsInboundOrderList(amsInboundOrder);
return getDataTable(list);
}
/**
*
*/
@RequiresPermissions("asset:inbound:export")
@Log(title = "入库管理", businessType = BusinessType.EXPORT)
@PostMapping("/export")
@ResponseBody
public AjaxResult export(AmsInboundOrder amsInboundOrder)
{
List<AmsInboundOrder> list = amsInboundOrderService.selectAmsInboundOrderList(amsInboundOrder);
ExcelUtil<AmsInboundOrder> util = new ExcelUtil<AmsInboundOrder>(AmsInboundOrder.class);
return util.exportExcel(list, "入库管理数据");
}
/**
*
*/
@RequiresPermissions("asset:inbound:view")
@GetMapping("/view/{orderId}")
public String view(@PathVariable("orderId") Long orderId, ModelMap mmap)
{
AmsInboundOrder amsInboundOrder = amsInboundOrderService.selectAmsInboundOrderByOrderId(orderId);
mmap.put("amsInboundOrder", amsInboundOrder);
return prefix + "/view";
}
/**
*
*/
@RequiresPermissions("asset:inbound:add")
@GetMapping("/add")
public String add(ModelMap mmap)
{
putInboundOptions(mmap);
return prefix + "/add";
}
/**
*
*/
@RequiresPermissions("asset:inbound:add")
@Log(title = "入库管理", businessType = BusinessType.INSERT)
@PostMapping("/add")
@ResponseBody
public AjaxResult addSave(AmsInboundOrder amsInboundOrder)
{
amsInboundOrder.setCreateBy(getLoginName());
return toAjax(amsInboundOrderService.insertAmsInboundOrder(amsInboundOrder));
}
/**
*
*/
@RequiresPermissions("asset:inbound:edit")
@GetMapping("/edit/{orderId}")
public String edit(@PathVariable("orderId") Long orderId, ModelMap mmap)
{
AmsInboundOrder amsInboundOrder = amsInboundOrderService.selectAmsInboundOrderByOrderId(orderId);
mmap.put("amsInboundOrder", amsInboundOrder);
putInboundOptions(mmap);
return prefix + "/edit";
}
/**
*
*/
@RequiresPermissions("asset:inbound:edit")
@Log(title = "入库管理", businessType = BusinessType.UPDATE)
@PostMapping("/edit")
@ResponseBody
public AjaxResult editSave(AmsInboundOrder amsInboundOrder)
{
amsInboundOrder.setUpdateBy(getLoginName());
return toAjax(amsInboundOrderService.updateAmsInboundOrder(amsInboundOrder));
}
/**
*
*/
@RequiresPermissions("asset:inbound:confirm")
@Log(title = "入库管理", businessType = BusinessType.UPDATE)
@PostMapping("/confirm/{orderId}")
@ResponseBody
public AjaxResult confirm(@PathVariable("orderId") Long orderId)
{
return toAjax(amsInboundOrderService.confirmInbound(orderId, getUserId(),
getSysUser().getUserName(), getLoginName()));
}
/**
*
*/
@RequiresPermissions("asset:inbound:remove")
@Log(title = "入库管理", businessType = BusinessType.DELETE)
@PostMapping( "/remove")
@ResponseBody
public AjaxResult remove(String ids)
{
return toAjax(amsInboundOrderService.deleteAmsInboundOrderByOrderIds(ids));
}
private void putInboundOptions(ModelMap mmap)
{
mmap.put("warehouseList", selectEnabledWarehouseList());
AmsAssetLocation location = new AmsAssetLocation();
location.setEnabled(ENABLED_YES);
mmap.put("locationList", amsAssetLocationService.selectAmsAssetLocationList(location));
AmsAsset asset = new AmsAsset();
asset.setAssetStatus(AssetStatus.IN_STOCK);
mmap.put("assetList", amsAssetService.selectAmsAssetList(asset));
}
private List<AmsWarehouse> selectEnabledWarehouseList()
{
AmsWarehouse warehouse = new AmsWarehouse();
warehouse.setEnabled(ENABLED_YES);
return amsWarehouseService.selectAmsWarehouseList(warehouse);
}
}

@ -0,0 +1,205 @@
package com.ruoyi.asset.domain;
import java.util.List;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
/**
* ams_inbound_order
*
* @author Yangk
* @date 2026-06-10
*/
public class AmsInboundOrder extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 单据ID */
private Long orderId;
/** 入库单号 */
@Excel(name = "入库单号")
private String inboundNo;
/** 入库仓库ID */
private Long warehouseId;
/** 入库仓库编码快照 */
private String warehouseCode;
/** 入库仓库名称快照 */
@Excel(name = "入库仓库名称快照")
private String warehouseName;
/** 入库人ID */
private Long inboundUserId;
/** 入库人名称快照 */
@Excel(name = "入库人名称快照")
private String inboundUserName;
/** 入库时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Excel(name = "入库时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date inboundTime;
/** 单据状态 */
@Excel(name = "单据状态", dictType = "ams_inbound_status")
private String orderStatus;
/** 资产编码查询条件,不属于入库单主表字段 */
private String assetCode;
/** 删除标志0存在1删除 */
private String delFlag;
/** 入库单明细信息 */
private List<AmsInboundOrderItem> amsInboundOrderItemList;
public void setOrderId(Long orderId)
{
this.orderId = orderId;
}
public Long getOrderId()
{
return orderId;
}
public void setInboundNo(String inboundNo)
{
this.inboundNo = inboundNo;
}
public String getInboundNo()
{
return inboundNo;
}
public void setWarehouseId(Long warehouseId)
{
this.warehouseId = warehouseId;
}
public Long getWarehouseId()
{
return warehouseId;
}
public void setWarehouseCode(String warehouseCode)
{
this.warehouseCode = warehouseCode;
}
public String getWarehouseCode()
{
return warehouseCode;
}
public void setWarehouseName(String warehouseName)
{
this.warehouseName = warehouseName;
}
public String getWarehouseName()
{
return warehouseName;
}
public void setInboundUserId(Long inboundUserId)
{
this.inboundUserId = inboundUserId;
}
public Long getInboundUserId()
{
return inboundUserId;
}
public void setInboundUserName(String inboundUserName)
{
this.inboundUserName = inboundUserName;
}
public String getInboundUserName()
{
return inboundUserName;
}
public void setInboundTime(Date inboundTime)
{
this.inboundTime = inboundTime;
}
public Date getInboundTime()
{
return inboundTime;
}
public void setOrderStatus(String orderStatus)
{
this.orderStatus = orderStatus;
}
public String getOrderStatus()
{
return orderStatus;
}
public void setAssetCode(String assetCode)
{
this.assetCode = assetCode;
}
public String getAssetCode()
{
return assetCode;
}
public void setDelFlag(String delFlag)
{
this.delFlag = delFlag;
}
public String getDelFlag()
{
return delFlag;
}
public List<AmsInboundOrderItem> getAmsInboundOrderItemList()
{
return amsInboundOrderItemList;
}
public void setAmsInboundOrderItemList(List<AmsInboundOrderItem> amsInboundOrderItemList)
{
this.amsInboundOrderItemList = amsInboundOrderItemList;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("orderId", getOrderId())
.append("inboundNo", getInboundNo())
.append("warehouseId", getWarehouseId())
.append("warehouseCode", getWarehouseCode())
.append("warehouseName", getWarehouseName())
.append("inboundUserId", getInboundUserId())
.append("inboundUserName", getInboundUserName())
.append("inboundTime", getInboundTime())
.append("orderStatus", getOrderStatus())
.append("assetCode", getAssetCode())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.append("delFlag", getDelFlag())
.append("amsInboundOrderItemList", getAmsInboundOrderItemList())
.toString();
}
}

@ -0,0 +1,251 @@
package com.ruoyi.asset.domain;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
/**
* ams_inbound_order_item
*
* @author Yangk
* @date 2026-06-10
*/
public class AmsInboundOrderItem extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 明细ID */
private Long itemId;
/** 入库单ID */
@Excel(name = "入库单ID")
private Long orderId;
/** 入库单号快照 */
@Excel(name = "入库单号快照")
private String inboundNo;
/** 资产ID */
@Excel(name = "资产ID")
private Long assetId;
/** 资产编码快照 */
@Excel(name = "资产编码快照")
private String assetCode;
/** 资产名称快照 */
@Excel(name = "资产名称快照")
private String assetName;
/** 资产类别ID快照 */
@Excel(name = "资产类别ID快照")
private Long categoryId;
/** 类别编码快照 */
@Excel(name = "类别编码快照")
private String categoryCode;
/** 类别名称快照 */
@Excel(name = "类别名称快照")
private String categoryName;
/** 规格型号快照 */
@Excel(name = "规格型号快照")
private String specModel;
/** 品牌快照 */
@Excel(name = "品牌快照")
private String brand;
/** 入库位置ID */
@Excel(name = "入库位置ID")
private Long locationId;
/** 入库位置编码快照 */
@Excel(name = "入库位置编码快照")
private String locationCode;
/** 入库位置名称快照 */
@Excel(name = "入库位置名称快照")
private String locationName;
/** 入库数量一期资产按件管理默认1 */
@Excel(name = "入库数量一期资产按件管理默认1")
private Integer inboundQuantity;
/** 删除标志0存在1删除 */
private String delFlag;
public void setItemId(Long itemId)
{
this.itemId = itemId;
}
public Long getItemId()
{
return itemId;
}
public void setOrderId(Long orderId)
{
this.orderId = orderId;
}
public Long getOrderId()
{
return orderId;
}
public void setInboundNo(String inboundNo)
{
this.inboundNo = inboundNo;
}
public String getInboundNo()
{
return inboundNo;
}
public void setAssetId(Long assetId)
{
this.assetId = assetId;
}
public Long getAssetId()
{
return assetId;
}
public void setAssetCode(String assetCode)
{
this.assetCode = assetCode;
}
public String getAssetCode()
{
return assetCode;
}
public void setAssetName(String assetName)
{
this.assetName = assetName;
}
public String getAssetName()
{
return assetName;
}
public void setCategoryId(Long categoryId)
{
this.categoryId = categoryId;
}
public Long getCategoryId()
{
return categoryId;
}
public void setCategoryCode(String categoryCode)
{
this.categoryCode = categoryCode;
}
public String getCategoryCode()
{
return categoryCode;
}
public void setCategoryName(String categoryName)
{
this.categoryName = categoryName;
}
public String getCategoryName()
{
return categoryName;
}
public void setSpecModel(String specModel)
{
this.specModel = specModel;
}
public String getSpecModel()
{
return specModel;
}
public void setBrand(String brand)
{
this.brand = brand;
}
public String getBrand()
{
return brand;
}
public void setLocationId(Long locationId)
{
this.locationId = locationId;
}
public Long getLocationId()
{
return locationId;
}
public void setLocationCode(String locationCode)
{
this.locationCode = locationCode;
}
public String getLocationCode()
{
return locationCode;
}
public void setLocationName(String locationName)
{
this.locationName = locationName;
}
public String getLocationName()
{
return locationName;
}
public void setInboundQuantity(Integer inboundQuantity)
{
this.inboundQuantity = inboundQuantity;
}
public Integer getInboundQuantity()
{
return inboundQuantity;
}
public void setDelFlag(String delFlag)
{
this.delFlag = delFlag;
}
public String getDelFlag()
{
return delFlag;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("itemId", getItemId())
.append("orderId", getOrderId())
.append("inboundNo", getInboundNo())
.append("assetId", getAssetId())
.append("assetCode", getAssetCode())
.append("assetName", getAssetName())
.append("categoryId", getCategoryId())
.append("categoryCode", getCategoryCode())
.append("categoryName", getCategoryName())
.append("specModel", getSpecModel())
.append("brand", getBrand())
.append("locationId", getLocationId())
.append("locationCode", getLocationCode())
.append("locationName", getLocationName())
.append("inboundQuantity", getInboundQuantity())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.append("delFlag", getDelFlag())
.toString();
}
}

@ -0,0 +1,103 @@
package com.ruoyi.asset.mapper;
import java.util.List;
import com.ruoyi.asset.domain.AmsInboundOrder;
import com.ruoyi.asset.domain.AmsInboundOrderItem;
/**
* Mapper
*
* @author Yangk
* @date 2026-06-10
*/
public interface AmsInboundOrderMapper
{
/**
*
*
* @param orderId
* @return
*/
public AmsInboundOrder selectAmsInboundOrderByOrderId(Long orderId);
/**
*
*
* @param orderId ID
* @return
*/
public AmsInboundOrder selectAmsInboundOrderByOrderIdForUpdate(Long orderId);
/**
*
*
* @param amsInboundOrder
* @return
*/
public List<AmsInboundOrder> selectAmsInboundOrderList(AmsInboundOrder amsInboundOrder);
/**
*
*
* @param amsInboundOrder
* @return
*/
public int insertAmsInboundOrder(AmsInboundOrder amsInboundOrder);
/**
*
*
* @param amsInboundOrder
* @return
*/
public int updateAmsInboundOrder(AmsInboundOrder amsInboundOrder);
/**
*
*
* @param amsInboundOrder
* @return
*/
public int confirmAmsInboundOrder(AmsInboundOrder amsInboundOrder);
/**
*
*
* @param orderId
* @return
*/
public int deleteAmsInboundOrderByOrderId(Long orderId);
/**
*
*
* @param orderIds
* @return
*/
public int deleteAmsInboundOrderByOrderIds(String[] orderIds);
/**
*
*
* @param orderIds
* @return
*/
public int deleteAmsInboundOrderItemByOrderIds(String[] orderIds);
/**
*
*
* @param amsInboundOrderItemList
* @return
*/
public int batchAmsInboundOrderItem(List<AmsInboundOrderItem> amsInboundOrderItemList);
/**
*
*
* @param orderId ID
* @return
*/
public int deleteAmsInboundOrderItemByOrderId(Long orderId);
}

@ -0,0 +1,72 @@
package com.ruoyi.asset.service;
import java.util.List;
import com.ruoyi.asset.domain.AmsInboundOrder;
/**
* Service
*
* @author Yangk
* @date 2026-06-10
*/
public interface IAmsInboundOrderService
{
/**
*
*
* @param orderId
* @return
*/
public AmsInboundOrder selectAmsInboundOrderByOrderId(Long orderId);
/**
*
*
* @param amsInboundOrder
* @return
*/
public List<AmsInboundOrder> selectAmsInboundOrderList(AmsInboundOrder amsInboundOrder);
/**
*
*
* @param amsInboundOrder
* @return
*/
public int insertAmsInboundOrder(AmsInboundOrder amsInboundOrder);
/**
*
*
* @param amsInboundOrder
* @return
*/
public int updateAmsInboundOrder(AmsInboundOrder amsInboundOrder);
/**
*
*
* @param orderId ID
* @param operateUserId ID
* @param operateUserName
* @param operateLoginName
* @return
*/
public int confirmInbound(Long orderId, Long operateUserId, String operateUserName, String operateLoginName);
/**
*
*
* @param orderIds
* @return
*/
public int deleteAmsInboundOrderByOrderIds(String orderIds);
/**
*
*
* @param orderId
* @return
*/
public int deleteAmsInboundOrderByOrderId(Long orderId);
}

@ -0,0 +1,329 @@
package com.ruoyi.asset.service.impl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.ruoyi.asset.constant.AssetStatus;
import com.ruoyi.asset.constant.InboundOrderStatus;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsAssetLocation;
import com.ruoyi.asset.domain.AmsInboundOrder;
import com.ruoyi.asset.domain.AmsInboundOrderItem;
import com.ruoyi.asset.domain.AmsWarehouse;
import com.ruoyi.asset.domain.AssetTransitionContext;
import com.ruoyi.asset.mapper.AmsInboundOrderMapper;
import com.ruoyi.asset.service.IAmsAssetLocationService;
import com.ruoyi.asset.service.IAmsAssetService;
import com.ruoyi.asset.service.IAmsInboundOrderService;
import com.ruoyi.asset.service.IAmsWarehouseService;
import com.ruoyi.asset.service.IAssetStatusTransitionService;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysCodeRuleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Service
*
* @author Yangk
*/
@Service
public class AmsInboundOrderServiceImpl implements IAmsInboundOrderService
{
private static final String INBOUND_ORDER_RULE = "INBOUND_ORDER";
private static final String ENABLED_YES = "Y";
private static final String DEL_FLAG_NORMAL = "0";
@Autowired
private AmsInboundOrderMapper amsInboundOrderMapper;
@Autowired
private ISysCodeRuleService sysCodeRuleService;
@Autowired
private IAmsWarehouseService amsWarehouseService;
@Autowired
private IAmsAssetLocationService amsAssetLocationService;
@Autowired
private IAmsAssetService amsAssetService;
@Autowired
private IAssetStatusTransitionService assetStatusTransitionService;
@Override
public AmsInboundOrder selectAmsInboundOrderByOrderId(Long orderId)
{
return amsInboundOrderMapper.selectAmsInboundOrderByOrderId(orderId);
}
@Override
public List<AmsInboundOrder> selectAmsInboundOrderList(AmsInboundOrder amsInboundOrder)
{
return amsInboundOrderMapper.selectAmsInboundOrderList(amsInboundOrder);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int insertAmsInboundOrder(AmsInboundOrder amsInboundOrder)
{
validateOrderRequest(amsInboundOrder);
amsInboundOrder.setInboundNo(sysCodeRuleService.nextCode(INBOUND_ORDER_RULE));
amsInboundOrder.setOrderStatus(InboundOrderStatus.DRAFT);
amsInboundOrder.setInboundUserId(null);
amsInboundOrder.setInboundUserName(null);
amsInboundOrder.setInboundTime(null);
amsInboundOrder.setDelFlag(DEL_FLAG_NORMAL);
amsInboundOrder.setCreateTime(DateUtils.getNowDate());
fillOrderSnapshots(amsInboundOrder);
int rows = amsInboundOrderMapper.insertAmsInboundOrder(amsInboundOrder);
if (rows != 1 || StringUtils.isNull(amsInboundOrder.getOrderId()))
{
throw new ServiceException("入库单保存失败");
}
insertAmsInboundOrderItem(amsInboundOrder);
return rows;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int updateAmsInboundOrder(AmsInboundOrder amsInboundOrder)
{
if (StringUtils.isNull(amsInboundOrder) || StringUtils.isNull(amsInboundOrder.getOrderId()))
{
throw new ServiceException("入库单ID不能为空");
}
AmsInboundOrder current = requireDraftOrderForUpdate(amsInboundOrder.getOrderId());
validateOrderRequest(amsInboundOrder);
// 单据号、状态和确认信息只能由服务层维护,防止前端篡改生命周期状态。
amsInboundOrder.setInboundNo(current.getInboundNo());
amsInboundOrder.setOrderStatus(current.getOrderStatus());
amsInboundOrder.setInboundUserId(current.getInboundUserId());
amsInboundOrder.setInboundUserName(current.getInboundUserName());
amsInboundOrder.setInboundTime(current.getInboundTime());
amsInboundOrder.setUpdateTime(DateUtils.getNowDate());
fillOrderSnapshots(amsInboundOrder);
amsInboundOrderMapper.deleteAmsInboundOrderItemByOrderId(amsInboundOrder.getOrderId());
insertAmsInboundOrderItem(amsInboundOrder);
if (amsInboundOrderMapper.updateAmsInboundOrder(amsInboundOrder) != 1)
{
throw new ServiceException("入库单状态已变化,保存失败");
}
return 1;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int confirmInbound(Long orderId, Long operateUserId, String operateUserName, String operateLoginName)
{
AmsInboundOrder order = requireDraftOrderForUpdate(orderId);
List<AmsInboundOrderItem> itemList = order.getAmsInboundOrderItemList();
if (itemList == null || itemList.isEmpty())
{
throw new ServiceException("入库单明细不能为空");
}
for (AmsInboundOrderItem item : itemList)
{
AssetTransitionContext context = new AssetTransitionContext();
context.setSourceOrderId(order.getOrderId());
context.setSourceOrderNo(order.getInboundNo());
context.setSourceItemId(item.getItemId());
context.setOperateUserId(operateUserId);
context.setOperateUserName(operateUserName);
context.setOperateLoginName(operateLoginName);
context.setRemark(order.getRemark());
assetStatusTransitionService.confirmInbound(item.getAssetId(), order.getWarehouseId(),
item.getLocationId(), context);
}
order.setOrderStatus(InboundOrderStatus.INBOUND_DONE);
order.setInboundUserId(operateUserId);
order.setInboundUserName(operateUserName);
Date now = DateUtils.getNowDate();
order.setInboundTime(now);
order.setUpdateBy(operateLoginName);
order.setUpdateTime(now);
if (amsInboundOrderMapper.confirmAmsInboundOrder(order) != 1)
{
throw new ServiceException("入库单状态已变化,确认入库失败");
}
return 1;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteAmsInboundOrderByOrderIds(String orderIds)
{
Long[] sortedIds = Arrays.stream(Convert.toStrArray(orderIds))
.map(Long::valueOf).sorted().toArray(Long[]::new);
int rows = 0;
for (Long orderId : sortedIds)
{
AmsInboundOrder order = amsInboundOrderMapper.selectAmsInboundOrderByOrderIdForUpdate(orderId);
if (StringUtils.isNull(order))
{
continue;
}
requireDraft(order);
amsInboundOrderMapper.deleteAmsInboundOrderItemByOrderId(orderId);
rows += amsInboundOrderMapper.deleteAmsInboundOrderByOrderId(orderId);
}
return rows;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteAmsInboundOrderByOrderId(Long orderId)
{
AmsInboundOrder order = amsInboundOrderMapper.selectAmsInboundOrderByOrderIdForUpdate(orderId);
if (StringUtils.isNull(order))
{
return 0;
}
requireDraft(order);
amsInboundOrderMapper.deleteAmsInboundOrderItemByOrderId(orderId);
return amsInboundOrderMapper.deleteAmsInboundOrderByOrderId(orderId);
}
private AmsInboundOrder requireDraftOrderForUpdate(Long orderId)
{
if (StringUtils.isNull(orderId))
{
throw new ServiceException("入库单ID不能为空");
}
AmsInboundOrder order = amsInboundOrderMapper.selectAmsInboundOrderByOrderIdForUpdate(orderId);
if (StringUtils.isNull(order))
{
throw new ServiceException("入库单不存在或已删除");
}
requireDraft(order);
return order;
}
private void requireDraft(AmsInboundOrder order)
{
if (!StringUtils.equals(InboundOrderStatus.DRAFT, order.getOrderStatus()))
{
throw new ServiceException("仅草稿入库单允许修改、删除或确认");
}
}
private void validateOrderRequest(AmsInboundOrder order)
{
if (StringUtils.isNull(order))
{
throw new ServiceException("入库单不能为空");
}
if (StringUtils.isNull(order.getWarehouseId()))
{
throw new ServiceException("入库仓库不能为空");
}
if (order.getAmsInboundOrderItemList() == null || order.getAmsInboundOrderItemList().isEmpty())
{
throw new ServiceException("入库单明细不能为空");
}
validateLength(order.getRemark(), 500, "备注");
}
/**
* ID
*/
private void fillOrderSnapshots(AmsInboundOrder order)
{
AmsWarehouse warehouse = amsWarehouseService.selectAmsWarehouseByWarehouseId(order.getWarehouseId());
if (StringUtils.isNull(warehouse) || !StringUtils.equals(ENABLED_YES, warehouse.getEnabled()))
{
throw new ServiceException("入库仓库不存在或已停用");
}
order.setWarehouseCode(warehouse.getWarehouseCode());
order.setWarehouseName(warehouse.getWarehouseName());
Set<Long> assetIds = new HashSet<>();
for (AmsInboundOrderItem item : order.getAmsInboundOrderItemList())
{
if (StringUtils.isNull(item.getAssetId()))
{
throw new ServiceException("入库资产不能为空");
}
if (!assetIds.add(item.getAssetId()))
{
throw new ServiceException("同一入库单不能重复选择资产");
}
AmsAsset asset = amsAssetService.selectAmsAssetByAssetId(item.getAssetId());
if (StringUtils.isNull(asset))
{
throw new ServiceException("入库资产不存在或已删除");
}
if (!StringUtils.equals(AssetStatus.IN_STOCK, asset.getAssetStatus()))
{
throw new ServiceException(StringUtils.format("资产【{}】当前状态不允许入库", asset.getAssetCode()));
}
if (StringUtils.isNull(item.getLocationId()))
{
throw new ServiceException(StringUtils.format("资产【{}】的入库位置不能为空", asset.getAssetCode()));
}
AmsAssetLocation location = amsAssetLocationService.selectAmsAssetLocationByLocationId(item.getLocationId());
if (StringUtils.isNull(location) || !StringUtils.equals(ENABLED_YES, location.getEnabled()))
{
throw new ServiceException(StringUtils.format("资产【{}】的入库位置不存在或已停用", asset.getAssetCode()));
}
if (!order.getWarehouseId().equals(location.getWarehouseId()))
{
throw new ServiceException(StringUtils.format("资产【{}】的入库位置不属于所选仓库", asset.getAssetCode()));
}
item.setAssetCode(asset.getAssetCode());
item.setAssetName(asset.getAssetName());
item.setCategoryId(asset.getCategoryId());
item.setCategoryCode(asset.getCategoryCode());
item.setCategoryName(asset.getCategoryName());
item.setSpecModel(asset.getSpecModel());
item.setBrand(asset.getBrand());
item.setLocationCode(location.getLocationCode());
item.setLocationName(location.getLocationName());
item.setInboundQuantity(1);
item.setInboundNo(order.getInboundNo());
item.setDelFlag(DEL_FLAG_NORMAL);
}
}
private void insertAmsInboundOrderItem(AmsInboundOrder order)
{
List<AmsInboundOrderItem> list = new ArrayList<>();
Date now = DateUtils.getNowDate();
for (AmsInboundOrderItem item : order.getAmsInboundOrderItemList())
{
item.setItemId(null);
item.setOrderId(order.getOrderId());
item.setCreateBy(StringUtils.isNotEmpty(order.getCreateBy()) ? order.getCreateBy() : order.getUpdateBy());
item.setCreateTime(now);
item.setUpdateBy(null);
item.setUpdateTime(null);
list.add(item);
}
if (amsInboundOrderMapper.batchAmsInboundOrderItem(list) != list.size())
{
throw new ServiceException("入库单明细保存失败");
}
}
private void validateLength(String value, int maxLength, String fieldName)
{
if (StringUtils.isNotEmpty(value) && value.length() > maxLength)
{
throw new ServiceException(fieldName + "长度不能超过" + maxLength + "个字符");
}
}
}

@ -0,0 +1,188 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.asset.mapper.AmsInboundOrderMapper">
<resultMap type="AmsInboundOrder" id="AmsInboundOrderResult">
<result property="orderId" column="order_id" />
<result property="inboundNo" column="inbound_no" />
<result property="warehouseId" column="warehouse_id" />
<result property="warehouseCode" column="warehouse_code" />
<result property="warehouseName" column="warehouse_name" />
<result property="inboundUserId" column="inbound_user_id" />
<result property="inboundUserName" column="inbound_user_name" />
<result property="inboundTime" column="inbound_time" />
<result property="orderStatus" column="order_status" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
<result property="delFlag" column="del_flag" />
</resultMap>
<resultMap id="AmsInboundOrderItemCollectionResult" type="AmsInboundOrder" extends="AmsInboundOrderResult">
<collection property="amsInboundOrderItemList" ofType="AmsInboundOrderItem"
column="order_id" select="selectAmsInboundOrderItemList" />
</resultMap>
<resultMap type="AmsInboundOrderItem" id="AmsInboundOrderItemResult">
<result property="itemId" column="item_id" />
<result property="orderId" column="order_id" />
<result property="inboundNo" column="inbound_no" />
<result property="assetId" column="asset_id" />
<result property="assetCode" column="asset_code" />
<result property="assetName" column="asset_name" />
<result property="categoryId" column="category_id" />
<result property="categoryCode" column="category_code" />
<result property="categoryName" column="category_name" />
<result property="specModel" column="spec_model" />
<result property="brand" column="brand" />
<result property="locationId" column="location_id" />
<result property="locationCode" column="location_code" />
<result property="locationName" column="location_name" />
<result property="inboundQuantity" column="inbound_quantity" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
<result property="delFlag" column="del_flag" />
</resultMap>
<sql id="selectAmsInboundOrderVo">
select order_id, inbound_no, warehouse_id, warehouse_code, warehouse_name,
inbound_user_id, inbound_user_name, inbound_time, order_status,
create_by, create_time, update_by, update_time, remark, del_flag
from ams_inbound_order
</sql>
<select id="selectAmsInboundOrderList" parameterType="AmsInboundOrder" resultMap="AmsInboundOrderResult">
<include refid="selectAmsInboundOrderVo"/>
<where>
del_flag = '0'
<if test="inboundNo != null and inboundNo != ''">and inbound_no = #{inboundNo}</if>
<if test="warehouseId != null">and warehouse_id = #{warehouseId}</if>
<if test="inboundUserName != null and inboundUserName != ''">
and inbound_user_name like concat('%', #{inboundUserName}, '%')
</if>
<if test="params.beginInboundTime != null and params.beginInboundTime != ''">
and inbound_time &gt;= #{params.beginInboundTime}
</if>
<if test="params.endInboundTime != null and params.endInboundTime != ''">
and inbound_time &lt; date_add(#{params.endInboundTime}, interval 1 day)
</if>
<if test="orderStatus != null and orderStatus != ''">and order_status = #{orderStatus}</if>
<if test="assetCode != null and assetCode != ''">
and exists (
select 1 from ams_inbound_order_item item
where item.order_id = ams_inbound_order.order_id
and item.del_flag = '0'
and item.asset_code like concat(#{assetCode}, '%')
)
</if>
</where>
order by create_time desc, order_id desc
</select>
<select id="selectAmsInboundOrderByOrderId" parameterType="Long" resultMap="AmsInboundOrderItemCollectionResult">
<include refid="selectAmsInboundOrderVo"/>
where order_id = #{orderId} and del_flag = '0'
</select>
<select id="selectAmsInboundOrderByOrderIdForUpdate" parameterType="Long"
resultMap="AmsInboundOrderItemCollectionResult">
<include refid="selectAmsInboundOrderVo"/>
where order_id = #{orderId} and del_flag = '0'
for update
</select>
<select id="selectAmsInboundOrderItemList" resultMap="AmsInboundOrderItemResult">
select item_id, order_id, inbound_no, asset_id, asset_code, asset_name, category_id,
category_code, category_name, spec_model, brand, location_id, location_code,
location_name, inbound_quantity, create_by, create_time, update_by, update_time,
remark, del_flag
from ams_inbound_order_item
where order_id = #{order_id} and del_flag = '0'
order by item_id
</select>
<insert id="insertAmsInboundOrder" parameterType="AmsInboundOrder" useGeneratedKeys="true" keyProperty="orderId">
insert into ams_inbound_order (
inbound_no, warehouse_id, warehouse_code, warehouse_name, order_status,
create_by, create_time, remark, del_flag
) values (
#{inboundNo}, #{warehouseId}, #{warehouseCode}, #{warehouseName}, #{orderStatus},
#{createBy}, #{createTime}, #{remark}, #{delFlag}
)
</insert>
<update id="updateAmsInboundOrder" parameterType="AmsInboundOrder">
update ams_inbound_order
set warehouse_id = #{warehouseId},
warehouse_code = #{warehouseCode},
warehouse_name = #{warehouseName},
update_by = #{updateBy},
update_time = #{updateTime},
remark = #{remark}
where order_id = #{orderId} and del_flag = '0' and order_status = 'DRAFT'
</update>
<update id="confirmAmsInboundOrder" parameterType="AmsInboundOrder">
update ams_inbound_order
set order_status = #{orderStatus},
inbound_user_id = #{inboundUserId},
inbound_user_name = #{inboundUserName},
inbound_time = #{inboundTime},
update_by = #{updateBy},
update_time = #{updateTime}
where order_id = #{orderId} and del_flag = '0' and order_status = 'DRAFT'
</update>
<update id="deleteAmsInboundOrderByOrderId" parameterType="Long">
update ams_inbound_order
set del_flag = '1'
where order_id = #{orderId} and del_flag = '0' and order_status = 'DRAFT'
</update>
<update id="deleteAmsInboundOrderByOrderIds" parameterType="String">
update ams_inbound_order set del_flag = '1' where order_id in
<foreach item="orderId" collection="array" open="(" separator="," close=")">
#{orderId}
</foreach>
and del_flag = '0' and order_status = 'DRAFT'
</update>
<update id="deleteAmsInboundOrderItemByOrderIds" parameterType="String">
update ams_inbound_order_item set del_flag = '1' where order_id in
<foreach item="orderId" collection="array" open="(" separator="," close=")">
#{orderId}
</foreach>
and del_flag = '0'
</update>
<update id="deleteAmsInboundOrderItemByOrderId" parameterType="Long">
update ams_inbound_order_item
set del_flag = '1'
where order_id = #{orderId} and del_flag = '0'
</update>
<insert id="batchAmsInboundOrderItem">
insert into ams_inbound_order_item (
order_id, inbound_no, asset_id, asset_code, asset_name, category_id, category_code,
category_name, spec_model, brand, location_id, location_code, location_name,
inbound_quantity, create_by, create_time, remark, del_flag
) values
<foreach item="item" collection="list" separator=",">
(
#{item.orderId}, #{item.inboundNo}, #{item.assetId}, #{item.assetCode},
#{item.assetName}, #{item.categoryId}, #{item.categoryCode}, #{item.categoryName},
#{item.specModel}, #{item.brand}, #{item.locationId}, #{item.locationCode},
#{item.locationName}, #{item.inboundQuantity}, #{item.createBy}, #{item.createTime},
#{item.remark}, #{item.delFlag}
)
</foreach>
</insert>
</mapper>

@ -0,0 +1,211 @@
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:include="include :: header('新增入库单')" />
<style type="text/css">
table label.error { position: inherit; }
select + label.error { z-index: 1; right: 40px; }
</style>
</head>
<body class="white-bg">
<div class="wrapper wrapper-content animated fadeInRight ibox-content">
<form class="form-horizontal m" id="form-inbound-add">
<h4 class="form-header h4">基本信息</h4>
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label class="col-sm-4 control-label is-required">入库仓库:</label>
<div class="col-sm-8">
<select id="warehouseId" name="warehouseId" class="form-control" required>
<option value="">请选择入库仓库</option>
<option th:each="warehouse : ${warehouseList}"
th:value="${warehouse.warehouseId}"
th:text="${warehouse.warehouseCode + ' - ' + warehouse.warehouseName}"></option>
</select>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label class="col-sm-4 control-label">备注:</label>
<div class="col-sm-8">
<textarea name="remark" maxlength="500" class="form-control" rows="3"></textarea>
</div>
</div>
</div>
</div>
<h4 class="form-header h4">入库明细</h4>
<div class="row">
<div class="col-sm-12">
<button type="button" class="btn btn-white btn-sm" onclick="addRow()">
<i class="fa fa-plus"> 增加</i>
</button>
<button type="button" class="btn btn-white btn-sm" onclick="sub.delRow()">
<i class="fa fa-minus"> 删除</i>
</button>
<div class="col-sm-12 select-table table-striped">
<table id="bootstrap-table"></table>
</div>
</div>
</div>
</form>
</div>
<th:block th:include="include :: footer" />
<script th:inline="javascript">
var prefix = ctx + "asset/inbound";
var assetList = [[${assetList}]];
var locationList = [[${locationList}]];
$("#form-inbound-add").validate({
focusCleanup: true
});
function submitHandler() {
if ($("#bootstrap-table").bootstrapTable('getData').length === 0) {
$.modal.alertWarning("请至少添加一条入库明细");
return;
}
if ($.validate.form()) {
$.operate.save(prefix + "/add", $('#form-inbound-add').serialize());
}
}
$(function() {
initDetailTable([]);
$("#warehouseId").change(function() {
if ($("#bootstrap-table").bootstrapTable('getData').length > 0) {
$("#bootstrap-table").bootstrapTable('removeAll');
$.modal.alertWarning("入库仓库已变更,请重新添加入库明细");
}
});
});
function initDetailTable(data) {
var options = {
data: data,
pagination: false,
showSearch: false,
showRefresh: false,
showToggle: false,
showColumns: false,
sidePagination: "client",
columns: [{
checkbox: true
},
{
field: 'index',
align: 'center',
title: "序号",
formatter: function(value, row, index) {
return $.table.serialNumber(index);
}
},
{
field: 'assetId',
title: '资产',
formatter: function(value, row, index) {
return buildAssetSelect(value, index);
}
},
{
field: 'locationId',
title: '入库位置',
formatter: function(value, row, index) {
return buildLocationSelect(value, index);
}
},
{
field: 'inboundQuantity',
align: 'center',
title: '数量',
formatter: function(value, row, index) {
return '<input class="form-control" type="text" readonly name="amsInboundOrderItemList['
+ index + '].inboundQuantity" value="1">';
}
},
{
field: 'remark',
title: '明细备注',
formatter: function(value, row, index) {
return buildInput("amsInboundOrderItemList[" + index + "].remark", value, 500);
}
},
{
title: '操作',
align: 'center',
formatter: function(value, row, index) {
var rowIndex = $.common.isNotEmpty(row.index) ? row.index : $.table.serialNumber(index);
return '<a class="btn btn-danger btn-xs" href="javascript:void(0)" onclick="sub.delRowByIndex(\''
+ rowIndex + '\')"><i class="fa fa-remove"></i>删除</a>';
}
}]
};
$.table.init(options);
}
function addRow() {
if ($.common.isEmpty($("#warehouseId").val())) {
$.modal.alertWarning("请先选择入库仓库");
return;
}
var count = $("#bootstrap-table").bootstrapTable('getData').length;
sub.addRow({
index: $.table.serialNumber(count),
assetId: "",
locationId: "",
inboundQuantity: 1,
remark: ""
});
}
function buildAssetSelect(value, index) {
var select = $("<select>").addClass("form-control").attr({
name: "amsInboundOrderItemList[" + index + "].assetId",
required: true
});
select.append($("<option>").val("").text("请选择资产"));
$.each(assetList, function(i, asset) {
var label = asset.assetCode + " - " + asset.assetName;
if ($.common.isNotEmpty(asset.categoryName)) {
label += " - " + asset.categoryName;
}
var option = $("<option>").val(asset.assetId).text(label);
if (String(asset.assetId) === String(value)) {
option.attr("selected", "selected");
}
select.append(option);
});
return select.prop("outerHTML");
}
function buildLocationSelect(value, index) {
var warehouseId = $("#warehouseId").val();
var select = $("<select>").addClass("form-control").attr({
name: "amsInboundOrderItemList[" + index + "].locationId",
required: true
});
select.append($("<option>").val("").text("请选择入库位置"));
$.each(locationList, function(i, location) {
if (String(location.warehouseId) === String(warehouseId)) {
var label = location.locationCode + " - " + location.locationName;
var option = $("<option>").val(location.locationId).text(label);
if (String(location.locationId) === String(value)) {
option.attr("selected", "selected");
}
select.append(option);
}
});
return select.prop("outerHTML");
}
function buildInput(name, value, maxLength) {
return $("<input>").addClass("form-control").attr({
type: "text",
name: name,
maxlength: maxLength
}).val(value || "").prop("outerHTML");
}
</script>
</body>
</html>

@ -0,0 +1,224 @@
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:include="include :: header('修改入库单')" />
<style type="text/css">
table label.error { position: inherit; }
select + label.error { z-index: 1; right: 40px; }
</style>
</head>
<body class="white-bg">
<div class="wrapper wrapper-content animated fadeInRight ibox-content">
<form class="form-horizontal m" id="form-inbound-edit" th:object="${amsInboundOrder}">
<input name="orderId" th:field="*{orderId}" type="hidden">
<h4 class="form-header h4">基本信息</h4>
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label class="col-sm-4 control-label">入库单号:</label>
<div class="col-sm-8">
<input th:value="*{inboundNo}" class="form-control" type="text" readonly>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label class="col-sm-4 control-label is-required">入库仓库:</label>
<div class="col-sm-8">
<select id="warehouseId" name="warehouseId" class="form-control" required>
<option value="">请选择入库仓库</option>
<option th:each="warehouse : ${warehouseList}"
th:value="${warehouse.warehouseId}"
th:text="${warehouse.warehouseCode + ' - ' + warehouse.warehouseName}"
th:selected="${warehouse.warehouseId == amsInboundOrder.warehouseId}"></option>
</select>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<label class="col-sm-2 control-label">备注:</label>
<div class="col-sm-10">
<textarea name="remark" maxlength="500" class="form-control" rows="3">[[*{remark}]]</textarea>
</div>
</div>
</div>
</div>
<h4 class="form-header h4">入库明细</h4>
<div class="row">
<div class="col-sm-12">
<button type="button" class="btn btn-white btn-sm" onclick="addRow()">
<i class="fa fa-plus"> 增加</i>
</button>
<button type="button" class="btn btn-white btn-sm" onclick="sub.delRow()">
<i class="fa fa-minus"> 删除</i>
</button>
<div class="col-sm-12 select-table table-striped">
<table id="bootstrap-table"></table>
</div>
</div>
</div>
</form>
</div>
<th:block th:include="include :: footer" />
<script th:inline="javascript">
var prefix = ctx + "asset/inbound";
var assetList = [[${assetList}]];
var locationList = [[${locationList}]];
var detailList = [[${amsInboundOrder.amsInboundOrderItemList}]];
$("#form-inbound-edit").validate({
focusCleanup: true
});
function submitHandler() {
if ($("#bootstrap-table").bootstrapTable('getData').length === 0) {
$.modal.alertWarning("请至少保留一条入库明细");
return;
}
if ($.validate.form()) {
$.operate.save(prefix + "/edit", $('#form-inbound-edit').serialize());
}
}
$(function() {
initDetailTable(detailList);
$("#warehouseId").change(function() {
if ($("#bootstrap-table").bootstrapTable('getData').length > 0) {
$("#bootstrap-table").bootstrapTable('removeAll');
$.modal.alertWarning("入库仓库已变更,请重新添加入库明细");
}
});
});
function initDetailTable(data) {
var options = {
data: data,
pagination: false,
showSearch: false,
showRefresh: false,
showToggle: false,
showColumns: false,
sidePagination: "client",
columns: [{
checkbox: true
},
{
field: 'index',
align: 'center',
title: "序号",
formatter: function(value, row, index) {
return $.table.serialNumber(index);
}
},
{
field: 'assetId',
title: '资产',
formatter: function(value, row, index) {
return buildAssetSelect(value, index);
}
},
{
field: 'locationId',
title: '入库位置',
formatter: function(value, row, index) {
return buildLocationSelect(value, index);
}
},
{
field: 'inboundQuantity',
align: 'center',
title: '数量',
formatter: function(value, row, index) {
return '<input class="form-control" type="text" readonly name="amsInboundOrderItemList['
+ index + '].inboundQuantity" value="1">';
}
},
{
field: 'remark',
title: '明细备注',
formatter: function(value, row, index) {
return buildInput("amsInboundOrderItemList[" + index + "].remark", value, 500);
}
},
{
title: '操作',
align: 'center',
formatter: function(value, row, index) {
var rowIndex = $.common.isNotEmpty(row.index) ? row.index : $.table.serialNumber(index);
return '<a class="btn btn-danger btn-xs" href="javascript:void(0)" onclick="sub.delRowByIndex(\''
+ rowIndex + '\')"><i class="fa fa-remove"></i>删除</a>';
}
}]
};
$.table.init(options);
}
function addRow() {
if ($.common.isEmpty($("#warehouseId").val())) {
$.modal.alertWarning("请先选择入库仓库");
return;
}
var count = $("#bootstrap-table").bootstrapTable('getData').length;
sub.addRow({
index: $.table.serialNumber(count),
assetId: "",
locationId: "",
inboundQuantity: 1,
remark: ""
});
}
function buildAssetSelect(value, index) {
var select = $("<select>").addClass("form-control").attr({
name: "amsInboundOrderItemList[" + index + "].assetId",
required: true
});
select.append($("<option>").val("").text("请选择资产"));
$.each(assetList, function(i, asset) {
var label = asset.assetCode + " - " + asset.assetName;
if ($.common.isNotEmpty(asset.categoryName)) {
label += " - " + asset.categoryName;
}
var option = $("<option>").val(asset.assetId).text(label);
if (String(asset.assetId) === String(value)) {
option.attr("selected", "selected");
}
select.append(option);
});
return select.prop("outerHTML");
}
function buildLocationSelect(value, index) {
var warehouseId = $("#warehouseId").val();
var select = $("<select>").addClass("form-control").attr({
name: "amsInboundOrderItemList[" + index + "].locationId",
required: true
});
select.append($("<option>").val("").text("请选择入库位置"));
$.each(locationList, function(i, location) {
if (String(location.warehouseId) === String(warehouseId)) {
var label = location.locationCode + " - " + location.locationName;
var option = $("<option>").val(location.locationId).text(label);
if (String(location.locationId) === String(value)) {
option.attr("selected", "selected");
}
select.append(option);
}
});
return select.prop("outerHTML");
}
function buildInput(name, value, maxLength) {
return $("<input>").addClass("form-control").attr({
type: "text",
name: name,
maxlength: maxLength
}).val(value || "").prop("outerHTML");
}
</script>
</body>
</html>

@ -0,0 +1,152 @@
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<th:block th:include="include :: header('入库管理列表')" />
<th:block th:include="include :: datetimepicker-css" />
</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="inboundNo"/>
</li>
<li>
<label>入库仓库:</label>
<select name="warehouseId">
<option value="">所有</option>
<option th:each="warehouse : ${warehouseList}"
th:value="${warehouse.warehouseId}"
th:text="${warehouse.warehouseCode + ' - ' + warehouse.warehouseName}"></option>
</select>
</li>
<li>
<label>入库人:</label>
<input type="text" name="inboundUserName"/>
</li>
<li class="select-time">
<label>入库时间:</label>
<input type="text" class="time-input" id="startTime" placeholder="开始时间"
name="params[beginInboundTime]"/>
<span>-</span>
<input type="text" class="time-input" id="endTime" placeholder="结束时间"
name="params[endInboundTime]"/>
</li>
<li>
<label>单据状态:</label>
<select name="orderStatus" th:with="type=${@dict.getType('ams_inbound_status')}">
<option value="">所有</option>
<option th:each="dict : ${type}" th:text="${dict.dictLabel}"
th:value="${dict.dictValue}"></option>
</select>
</li>
<li>
<label>资产编码:</label>
<input type="text" name="assetCode"/>
</li>
<li>
<a class="btn btn-primary btn-rounded btn-sm" onclick="$.table.search()">
<i class="fa fa-search"></i>&nbsp;搜索
</a>
<a class="btn btn-warning btn-rounded btn-sm" onclick="$.form.reset()">
<i class="fa fa-refresh"></i>&nbsp;重置
</a>
</li>
</ul>
</div>
</form>
</div>
<div class="btn-group-sm" id="toolbar" role="group">
<a class="btn btn-success" onclick="$.operate.add()" shiro:hasPermission="asset:inbound:add">
<i class="fa fa-plus"></i> 添加
</a>
<a class="btn btn-warning" onclick="$.table.exportExcel()" shiro:hasPermission="asset:inbound:export">
<i class="fa fa-download"></i> 导出
</a>
</div>
<div class="col-sm-12 select-table table-striped">
<table id="bootstrap-table"></table>
</div>
</div>
</div>
<th:block th:include="include :: footer" />
<th:block th:include="include :: datetimepicker-js" />
<script th:inline="javascript">
var editFlag = [[${@permission.hasPermi('asset:inbound:edit')}]];
var removeFlag = [[${@permission.hasPermi('asset:inbound:remove')}]];
var confirmFlag = [[${@permission.hasPermi('asset:inbound:confirm')}]];
var orderStatusDatas = [[${@dict.getType('ams_inbound_status')}]];
var prefix = ctx + "asset/inbound";
$(function() {
var options = {
url: prefix + "/list",
viewUrl: prefix + "/view/{id}",
createUrl: prefix + "/add",
updateUrl: prefix + "/edit/{id}",
removeUrl: prefix + "/remove",
exportUrl: prefix + "/export",
modalName: "入库单",
columns: [{
field: 'orderId',
title: '单据ID',
visible: false
},
{
field: 'inboundNo',
title: '入库单号'
},
{
field: 'warehouseName',
title: '入库仓库'
},
{
field: 'inboundUserName',
title: '入库人'
},
{
field: 'inboundTime',
title: '入库时间'
},
{
field: 'orderStatus',
title: '单据状态',
formatter: function(value) {
return $.table.selectDictLabel(orderStatusDatas, value);
}
},
{
field: 'remark',
title: '备注'
},
{
title: '操作',
align: 'center',
formatter: function(value, row) {
var actions = [];
actions.push('<a class="btn btn-info btn-xs" href="javascript:void(0)" onclick="$.operate.view(\'' + row.orderId + '\')"><i class="fa fa-eye"></i>查看</a> ');
if (row.orderStatus === "DRAFT") {
actions.push('<a class="btn btn-success btn-xs ' + editFlag + '" href="javascript:void(0)" onclick="$.operate.edit(\'' + row.orderId + '\')"><i class="fa fa-edit"></i>编辑</a> ');
actions.push('<a class="btn btn-primary btn-xs ' + confirmFlag + '" href="javascript:void(0)" onclick="confirmInbound(\'' + row.orderId + '\')"><i class="fa fa-check"></i>确认入库</a> ');
actions.push('<a class="btn btn-danger btn-xs ' + removeFlag + '" href="javascript:void(0)" onclick="$.operate.remove(\'' + row.orderId + '\')"><i class="fa fa-remove"></i>删除</a>');
}
return actions.join('');
}
}]
};
$.table.init(options);
});
function confirmInbound(orderId) {
$.modal.confirm("确认入库后将更新资产仓库与位置,且单据不可再修改。是否继续?", function() {
$.operate.post(prefix + "/confirm/" + orderId, {});
});
}
</script>
</body>
</html>

@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:include="include :: header('入库单详情')" />
</head>
<body>
<div class="main-content">
<form class="form-horizontal" th:object="${amsInboundOrder}">
<h4 class="form-header h4">基本信息</h4>
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label class="col-sm-4 control-label">入库单号:</label>
<div class="col-sm-8">
<p class="form-control-plaintext" th:text="*{inboundNo}"></p>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label class="col-sm-4 control-label">入库仓库:</label>
<div class="col-sm-8">
<p class="form-control-plaintext" th:text="*{warehouseCode + ' - ' + warehouseName}"></p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label class="col-sm-4 control-label">单据状态:</label>
<div class="col-sm-8">
<p class="form-control-plaintext" th:text="*{@dict.getLabel('ams_inbound_status', orderStatus)}"></p>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label class="col-sm-4 control-label">入库人:</label>
<div class="col-sm-8">
<p class="form-control-plaintext" th:text="*{inboundUserName}"></p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label class="col-sm-4 control-label">入库时间:</label>
<div class="col-sm-8">
<p class="form-control-plaintext"
th:text="*{inboundTime == null ? '' : #dates.format(inboundTime, 'yyyy-MM-dd HH:mm:ss')}"></p>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label class="col-sm-4 control-label">备注:</label>
<div class="col-sm-8">
<p class="form-control-plaintext" th:text="*{remark}"></p>
</div>
</div>
</div>
</div>
<h4 class="form-header h4">入库明细</h4>
<div class="row">
<div class="col-sm-12 select-table table-striped">
<table class="table table-bordered">
<thead>
<tr>
<th>序号</th>
<th>资产编码</th>
<th>资产名称</th>
<th>资产类别</th>
<th>规格型号</th>
<th>品牌</th>
<th>入库位置</th>
<th>数量</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr th:each="item, stat : *{amsInboundOrderItemList}">
<td th:text="${stat.count}"></td>
<td th:text="${item.assetCode}"></td>
<td th:text="${item.assetName}"></td>
<td th:text="${item.categoryName}"></td>
<td th:text="${item.specModel}"></td>
<td th:text="${item.brand}"></td>
<td th:text="${item.locationCode + ' - ' + item.locationName}"></td>
<td th:text="${item.inboundQuantity}"></td>
<td th:text="${item.remark}"></td>
</tr>
</tbody>
</table>
</div>
</div>
</form>
</div>
<th:block th:include="include :: footer" />
</body>
</html>

@ -0,0 +1,205 @@
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.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.InboundOrderStatus;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsAssetLocation;
import com.ruoyi.asset.domain.AmsInboundOrder;
import com.ruoyi.asset.domain.AmsInboundOrderItem;
import com.ruoyi.asset.domain.AmsWarehouse;
import com.ruoyi.asset.domain.AssetTransitionContext;
import com.ruoyi.asset.mapper.AmsInboundOrderMapper;
import com.ruoyi.asset.service.IAmsAssetLocationService;
import com.ruoyi.asset.service.IAmsAssetService;
import com.ruoyi.asset.service.IAmsWarehouseService;
import com.ruoyi.asset.service.IAssetStatusTransitionService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.system.service.ISysCodeRuleService;
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 AmsInboundOrderServiceImplTest
{
@Mock
private AmsInboundOrderMapper amsInboundOrderMapper;
@Mock
private ISysCodeRuleService sysCodeRuleService;
@Mock
private IAmsWarehouseService amsWarehouseService;
@Mock
private IAmsAssetLocationService amsAssetLocationService;
@Mock
private IAmsAssetService amsAssetService;
@Mock
private IAssetStatusTransitionService assetStatusTransitionService;
@InjectMocks
private AmsInboundOrderServiceImpl service;
/** 新增草稿时由服务层生成单号,并从主数据回填所有快照。 */
@Test
void insertShouldGenerateCodeAndFillSnapshots()
{
AmsInboundOrder order = buildDraftRequest();
when(sysCodeRuleService.nextCode("INBOUND_ORDER")).thenReturn("RK202606100001");
stubMasterData();
doAnswer(invocation -> {
AmsInboundOrder inserted = invocation.getArgument(0);
inserted.setOrderId(100L);
return 1;
}).when(amsInboundOrderMapper).insertAmsInboundOrder(any(AmsInboundOrder.class));
when(amsInboundOrderMapper.batchAmsInboundOrderItem(anyList()))
.thenAnswer(invocation -> ((List<?>) invocation.getArgument(0)).size());
int rows = service.insertAmsInboundOrder(order);
assertEquals(1, rows);
assertEquals(100L, order.getOrderId());
assertEquals("RK202606100001", order.getInboundNo());
assertEquals(InboundOrderStatus.DRAFT, order.getOrderStatus());
assertEquals("WH-002", order.getWarehouseCode());
assertEquals("二号仓", order.getWarehouseName());
AmsInboundOrderItem item = order.getAmsInboundOrderItemList().get(0);
assertEquals(100L, item.getOrderId());
assertEquals("ASSET-001", item.getAssetCode());
assertEquals("测试资产", item.getAssetName());
assertEquals("LOC-020", item.getLocationCode());
assertEquals(1, item.getInboundQuantity());
assertEquals("0", item.getDelFlag());
assertNotNull(item.getCreateTime());
}
/** 已完成入库单必须拒绝修改,避免绕过页面直接篡改单据。 */
@Test
void updateShouldRejectCompletedOrder()
{
AmsInboundOrder completed = new AmsInboundOrder();
completed.setOrderId(100L);
completed.setOrderStatus(InboundOrderStatus.INBOUND_DONE);
when(amsInboundOrderMapper.selectAmsInboundOrderByOrderIdForUpdate(100L)).thenReturn(completed);
AmsInboundOrder request = new AmsInboundOrder();
request.setOrderId(100L);
ServiceException exception = assertThrows(ServiceException.class,
() -> service.updateAmsInboundOrder(request));
assertTrue(exception.getMessage().contains("仅草稿"));
verify(amsInboundOrderMapper, never()).updateAmsInboundOrder(any(AmsInboundOrder.class));
}
/** 确认入库必须逐条调用公共状态流转服务,并完成单据确认信息。 */
@Test
void confirmShouldDelegateTransitionAndCompleteOrder()
{
AmsInboundOrder order = new AmsInboundOrder();
order.setOrderId(100L);
order.setInboundNo("RK202606100001");
order.setWarehouseId(2L);
order.setOrderStatus(InboundOrderStatus.DRAFT);
AmsInboundOrderItem item = new AmsInboundOrderItem();
item.setItemId(101L);
item.setAssetId(1L);
item.setLocationId(20L);
order.setAmsInboundOrderItemList(Collections.singletonList(item));
when(amsInboundOrderMapper.selectAmsInboundOrderByOrderIdForUpdate(100L)).thenReturn(order);
when(amsInboundOrderMapper.confirmAmsInboundOrder(any(AmsInboundOrder.class))).thenReturn(1);
int rows = service.confirmInbound(100L, 1L, "管理员", "admin");
assertEquals(1, rows);
ArgumentCaptor<AssetTransitionContext> contextCaptor = ArgumentCaptor.forClass(AssetTransitionContext.class);
verify(assetStatusTransitionService).confirmInbound(
org.mockito.ArgumentMatchers.eq(1L),
org.mockito.ArgumentMatchers.eq(2L),
org.mockito.ArgumentMatchers.eq(20L),
contextCaptor.capture());
assertEquals(100L, contextCaptor.getValue().getSourceOrderId());
assertEquals(101L, contextCaptor.getValue().getSourceItemId());
assertEquals("admin", contextCaptor.getValue().getOperateLoginName());
assertEquals(InboundOrderStatus.INBOUND_DONE, order.getOrderStatus());
assertEquals(1L, order.getInboundUserId());
assertEquals("管理员", order.getInboundUserName());
assertNotNull(order.getInboundTime());
}
/** 已完成入库单必须拒绝删除。 */
@Test
void deleteShouldRejectCompletedOrder()
{
AmsInboundOrder completed = new AmsInboundOrder();
completed.setOrderId(100L);
completed.setOrderStatus(InboundOrderStatus.INBOUND_DONE);
when(amsInboundOrderMapper.selectAmsInboundOrderByOrderIdForUpdate(100L)).thenReturn(completed);
ServiceException exception = assertThrows(ServiceException.class,
() -> service.deleteAmsInboundOrderByOrderId(100L));
assertTrue(exception.getMessage().contains("仅草稿"));
verify(amsInboundOrderMapper, never()).deleteAmsInboundOrderByOrderId(100L);
}
private AmsInboundOrder buildDraftRequest()
{
AmsInboundOrder order = new AmsInboundOrder();
order.setWarehouseId(2L);
order.setCreateBy("admin");
AmsInboundOrderItem item = new AmsInboundOrderItem();
item.setAssetId(1L);
item.setLocationId(20L);
order.setAmsInboundOrderItemList(Collections.singletonList(item));
return order;
}
private void stubMasterData()
{
AmsWarehouse warehouse = new AmsWarehouse();
warehouse.setWarehouseId(2L);
warehouse.setWarehouseCode("WH-002");
warehouse.setWarehouseName("二号仓");
warehouse.setEnabled("Y");
when(amsWarehouseService.selectAmsWarehouseByWarehouseId(2L)).thenReturn(warehouse);
AmsAsset asset = new AmsAsset();
asset.setAssetId(1L);
asset.setAssetCode("ASSET-001");
asset.setAssetName("测试资产");
asset.setCategoryId(3L);
asset.setCategoryCode("CAT-003");
asset.setCategoryName("测试类别");
asset.setSpecModel("SPEC-001");
asset.setBrand("测试品牌");
asset.setAssetStatus(AssetStatus.IN_STOCK);
when(amsAssetService.selectAmsAssetByAssetId(1L)).thenReturn(asset);
AmsAssetLocation location = new AmsAssetLocation();
location.setLocationId(20L);
location.setWarehouseId(2L);
location.setLocationCode("LOC-020");
location.setLocationName("二号仓A区");
location.setEnabled("Y");
when(amsAssetLocationService.selectAmsAssetLocationByLocationId(20L)).thenReturn(location);
}
}
Loading…
Cancel
Save