feat(asset): 新增资产退库功能模块

- 创建退库单新增页面,支持选择接收仓库和添加退库资产明细
- 实现可退库资产候选对象,包含原领用仓库和位置信息追踪功能
- 开发退库单主对象和明细对象,支持退库流程的状态管理和信息快照
- 构建退库单控制器,提供退库申请、编辑、提交、确认等完整业务流程
- 实现退库资产选择器,支持按原仓库来源筛选和防重复选择校验
- 添加退库单提交和确认功能,包含资产状态变更和使用人归属清理
- 集成仓库和位置服务,确保退库后资产仓位信息的准确更新
main
yangk 2 weeks ago
parent e4b77612e0
commit 0c7cd2b3df

@ -0,0 +1,22 @@
package com.ruoyi.asset.constant;
/**
* 退
*
* @author Yangk
*/
public final class ReturnOrderStatus
{
/** 草稿 */
public static final String DRAFT = "DRAFT";
/** 待确认 */
public static final String PENDING_CONFIRM = "PENDING_CONFIRM";
/** 已退库 */
public static final String RETURNED = "RETURNED";
private ReturnOrderStatus()
{
}
}

@ -0,0 +1,242 @@
package com.ruoyi.asset.controller;
import java.util.List;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsAssetLocation;
import com.ruoyi.asset.domain.AmsReturnOrder;
import com.ruoyi.asset.domain.AmsWarehouse;
import com.ruoyi.asset.service.IAmsAssetLocationService;
import com.ruoyi.asset.service.IAmsReturnOrderService;
import com.ruoyi.asset.service.IAmsWarehouseService;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.service.ISysDeptService;
import org.apache.shiro.authz.annotation.Logical;
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.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 退Controller
*
* @author Yangk
*/
@Controller
@RequestMapping("/asset/return")
public class AmsReturnOrderController extends BaseController
{
private static final String ENABLED_YES = "Y";
private String prefix = "asset/return";
@Autowired
private IAmsReturnOrderService amsReturnOrderService;
@Autowired
private IAmsWarehouseService amsWarehouseService;
@Autowired
private IAmsAssetLocationService amsAssetLocationService;
@Autowired
private ISysDeptService sysDeptService;
@RequiresPermissions("asset:return:view")
@GetMapping()
public String returnPage(ModelMap mmap)
{
mmap.put("warehouseList", selectEnabledWarehouseList());
mmap.put("deptList", selectNormalDeptList());
return prefix + "/return";
}
@RequiresPermissions("asset:return:list")
@PostMapping("/list")
@ResponseBody
public TableDataInfo list(AmsReturnOrder order)
{
startPage();
return getDataTable(amsReturnOrderService.selectAmsReturnOrderList(order));
}
/**
*
* <p>
*
* 使退
* `originWarehouseId` `missingOriginOnly`
* ID
* </p>
*/
@RequiresPermissions(value = { "asset:return:add", "asset:return:edit" }, logical = Logical.OR)
@GetMapping("/selectAsset")
public String selectAsset(@RequestParam(value = "orderId", required = false) Long orderId,
@RequestParam(value = "originWarehouseId", required = false) Long originWarehouseId,
@RequestParam(value = "missingOriginOnly", required = false) Boolean missingOriginOnly,
ModelMap mmap)
{
mmap.put("orderId", orderId);
mmap.put("originWarehouseId", originWarehouseId);
mmap.put("missingOriginOnly", missingOriginOnly);
mmap.put("deptList", selectNormalDeptList());
return prefix + "/selectAsset";
}
/**
* 退
* <p>
*
* 1. (IN_USE)退
* 2. 稿退退
* 3. `orderId` 退
* </p>
*/
@RequiresPermissions(value = { "asset:return:add", "asset:return:edit" }, logical = Logical.OR)
@PostMapping("/availableAssetList")
@ResponseBody
public TableDataInfo availableAssetList(AmsAsset asset,
@RequestParam(value = "orderId", required = false) Long orderId,
@RequestParam(value = "originWarehouseId", required = false) Long originWarehouseId,
@RequestParam(value = "missingOriginOnly", required = false) Boolean missingOriginOnly)
{
startPage();
return getDataTable(amsReturnOrderService.selectAvailableReturnAssetList(asset, orderId,
originWarehouseId, missingOriginOnly));
}
@RequiresPermissions("asset:return:export")
@Log(title = "退库管理", businessType = BusinessType.EXPORT)
@PostMapping("/export")
@ResponseBody
public AjaxResult export(AmsReturnOrder order)
{
List<AmsReturnOrder> list = amsReturnOrderService.selectAmsReturnOrderList(order);
return new ExcelUtil<AmsReturnOrder>(AmsReturnOrder.class).exportExcel(list, "退库管理数据");
}
@RequiresPermissions("asset:return:view")
@GetMapping("/view/{orderId}")
public String view(@PathVariable("orderId") Long orderId, ModelMap mmap)
{
mmap.put("amsReturnOrder", amsReturnOrderService.selectAmsReturnOrderByOrderId(orderId));
return prefix + "/view";
}
@RequiresPermissions("asset:return:add")
@GetMapping("/add")
public String add(ModelMap mmap)
{
putReturnOptions(mmap);
return prefix + "/add";
}
@RequiresPermissions("asset:return:add")
@Log(title = "退库管理", businessType = BusinessType.INSERT)
@PostMapping("/add")
@ResponseBody
public AjaxResult addSave(AmsReturnOrder order)
{
order.setCreateBy(getLoginName());
return toAjax(amsReturnOrderService.insertAmsReturnOrder(order, getUserId(),
getSysUser().getUserName(), getSysUser().getDeptId()));
}
@RequiresPermissions("asset:return:edit")
@GetMapping("/edit/{orderId}")
public String edit(@PathVariable("orderId") Long orderId, ModelMap mmap)
{
mmap.put("amsReturnOrder", amsReturnOrderService.selectAmsReturnOrderByOrderId(orderId));
putReturnOptions(mmap);
return prefix + "/edit";
}
@RequiresPermissions("asset:return:edit")
@Log(title = "退库管理", businessType = BusinessType.UPDATE)
@PostMapping("/edit")
@ResponseBody
public AjaxResult editSave(AmsReturnOrder order)
{
order.setUpdateBy(getLoginName());
return toAjax(amsReturnOrderService.updateAmsReturnOrder(order));
}
/**
* 退
* <p>
*
* 稿(DRAFT)(PENDING_CONFIRM)
* 使
* </p>
*/
@RequiresPermissions("asset:return:submit")
@Log(title = "退库管理", businessType = BusinessType.UPDATE)
@PostMapping("/submit/{orderId}")
@ResponseBody
public AjaxResult submit(@PathVariable("orderId") Long orderId)
{
return toAjax(amsReturnOrderService.submitReturn(orderId, getLoginName()));
}
/**
* 退
* <p>
*
* (PENDING_CONFIRM)退(RETURNED)
* 使使退
*
* </p>
*/
@RequiresPermissions("asset:return:confirm")
@Log(title = "退库管理", businessType = BusinessType.UPDATE)
@PostMapping("/confirm/{orderId}")
@ResponseBody
public AjaxResult confirm(@PathVariable("orderId") Long orderId)
{
return toAjax(amsReturnOrderService.confirmReturn(orderId, getUserId(),
getSysUser().getUserName(), getLoginName()));
}
@RequiresPermissions("asset:return:remove")
@Log(title = "退库管理", businessType = BusinessType.DELETE)
@PostMapping("/remove")
@ResponseBody
public AjaxResult remove(String ids)
{
return toAjax(amsReturnOrderService.deleteAmsReturnOrderByOrderIds(ids));
}
private void putReturnOptions(ModelMap mmap)
{
mmap.put("warehouseList", selectEnabledWarehouseList());
AmsAssetLocation location = new AmsAssetLocation();
location.setEnabled(ENABLED_YES);
mmap.put("locationList", amsAssetLocationService.selectAmsAssetLocationList(location));
}
private List<AmsWarehouse> selectEnabledWarehouseList()
{
AmsWarehouse warehouse = new AmsWarehouse();
warehouse.setEnabled(ENABLED_YES);
return amsWarehouseService.selectAmsWarehouseList(warehouse);
}
private List<SysDept> selectNormalDeptList()
{
SysDept dept = new SysDept();
dept.setStatus(UserConstants.DEPT_NORMAL);
return sysDeptService.selectDeptList(dept);
}
}

@ -0,0 +1,184 @@
package com.ruoyi.asset.domain;
/**
* 退
* <p>
* 退
* </p>
*
* @author Yangk
*/
public class AmsReturnAssetCandidate extends AmsAsset
{
private static final long serialVersionUID = 1L;
/**
* ID
* 退
*/
private Long sourceReceiveOrderId;
/**
* ID
* 退ID
*/
private Long sourceReceiveItemId;
/**
* ID
* (IN_USE)
* `before_warehouse_id`
*/
private Long originWarehouseId;
/** 最近一次已完成领用时的原仓库编码 */
private String originWarehouseCode;
/** 最近一次已完成领用时的原仓库名称 */
private String originWarehouseName;
/**
* YN
* 退退
*/
private String originWarehouseEnabled;
/**
* ID
* 退退
*/
private Long originLocationId;
/** 最近一次已完成领用时的原资产位置编码 */
private String originLocationCode;
/** 最近一次已完成领用时的原资产位置名称 */
private String originLocationName;
/** 最近一次已完成领用时的原资产位置是否启用Y表示启用N表示停用 */
private String originLocationEnabled;
public Long getSourceReceiveOrderId()
{
return sourceReceiveOrderId;
}
public void setSourceReceiveOrderId(Long sourceReceiveOrderId)
{
this.sourceReceiveOrderId = sourceReceiveOrderId;
}
public Long getSourceReceiveItemId()
{
return sourceReceiveItemId;
}
public void setSourceReceiveItemId(Long sourceReceiveItemId)
{
this.sourceReceiveItemId = sourceReceiveItemId;
}
public Long getOriginWarehouseId()
{
return originWarehouseId;
}
public void setOriginWarehouseId(Long originWarehouseId)
{
this.originWarehouseId = originWarehouseId;
}
public String getOriginWarehouseCode()
{
return originWarehouseCode;
}
public void setOriginWarehouseCode(String originWarehouseCode)
{
this.originWarehouseCode = originWarehouseCode;
}
public String getOriginWarehouseName()
{
return originWarehouseName;
}
public void setOriginWarehouseName(String originWarehouseName)
{
this.originWarehouseName = originWarehouseName;
}
public String getOriginWarehouseEnabled()
{
return originWarehouseEnabled;
}
public void setOriginWarehouseEnabled(String originWarehouseEnabled)
{
this.originWarehouseEnabled = originWarehouseEnabled;
}
public Long getOriginLocationId()
{
return originLocationId;
}
public void setOriginLocationId(Long originLocationId)
{
this.originLocationId = originLocationId;
}
public String getOriginLocationCode()
{
return originLocationCode;
}
public void setOriginLocationCode(String originLocationCode)
{
this.originLocationCode = originLocationCode;
}
public String getOriginLocationName()
{
return originLocationName;
}
public void setOriginLocationName(String originLocationName)
{
this.originLocationName = originLocationName;
}
public String getOriginLocationEnabled()
{
return originLocationEnabled;
}
public void setOriginLocationEnabled(String originLocationEnabled)
{
this.originLocationEnabled = originLocationEnabled;
}
/**
*
* <p>
*
* 退
* </p>
*/
public boolean isOriginTraceable()
{
return originWarehouseId != null && originLocationId != null;
}
/**
*
* <p>
* 退"Y"
*
* </p>
*/
public boolean isOriginEnabled()
{
return isOriginTraceable() && "Y".equals(originWarehouseEnabled) && "Y".equals(originLocationEnabled);
}
}

@ -0,0 +1,257 @@
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_return_order
*
* @author Yangk
* @date 2026-06-15
*/
public class AmsReturnOrder extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 单据ID */
private Long orderId;
/** 退库单号 */
@Excel(name = "退库单号")
private String returnNo;
/** 申请人ID */
@Excel(name = "申请人ID")
private Long applicantId;
/** 申请人名称快照 */
@Excel(name = "申请人名称快照")
private String applicantName;
/** 申请部门ID */
@Excel(name = "申请部门ID")
private Long applyDeptId;
/** 申请部门名称快照 */
@Excel(name = "申请部门名称快照")
private String applyDeptName;
/** 接收仓库ID */
@Excel(name = "接收仓库ID")
private Long receiveWarehouseId;
/** 接收仓库编码快照 */
@Excel(name = "接收仓库编码快照")
private String receiveWarehouseCode;
/** 接收仓库名称快照 */
@Excel(name = "接收仓库名称快照")
private String receiveWarehouseName;
/** 确认人ID */
@Excel(name = "确认人ID")
private Long confirmUserId;
/** 确认人名称快照 */
@Excel(name = "确认人名称快照")
private String confirmUserName;
/** 确认时间 */
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@Excel(name = "确认时间", width = 30, dateFormat = "yyyy-MM-dd")
private Date confirmTime;
/** 单据状态 */
@Excel(name = "单据状态")
private String orderStatus;
/** 删除标志0存在1删除 */
private String delFlag;
/**
* 退
* 退退
*/
private List<AmsReturnOrderItem> amsReturnOrderItemList;
public void setOrderId(Long orderId)
{
this.orderId = orderId;
}
public Long getOrderId()
{
return orderId;
}
public void setReturnNo(String returnNo)
{
this.returnNo = returnNo;
}
public String getReturnNo()
{
return returnNo;
}
public void setApplicantId(Long applicantId)
{
this.applicantId = applicantId;
}
public Long getApplicantId()
{
return applicantId;
}
public void setApplicantName(String applicantName)
{
this.applicantName = applicantName;
}
public String getApplicantName()
{
return applicantName;
}
public void setApplyDeptId(Long applyDeptId)
{
this.applyDeptId = applyDeptId;
}
public Long getApplyDeptId()
{
return applyDeptId;
}
public void setApplyDeptName(String applyDeptName)
{
this.applyDeptName = applyDeptName;
}
public String getApplyDeptName()
{
return applyDeptName;
}
public void setReceiveWarehouseId(Long receiveWarehouseId)
{
this.receiveWarehouseId = receiveWarehouseId;
}
public Long getReceiveWarehouseId()
{
return receiveWarehouseId;
}
public void setReceiveWarehouseCode(String receiveWarehouseCode)
{
this.receiveWarehouseCode = receiveWarehouseCode;
}
public String getReceiveWarehouseCode()
{
return receiveWarehouseCode;
}
public void setReceiveWarehouseName(String receiveWarehouseName)
{
this.receiveWarehouseName = receiveWarehouseName;
}
public String getReceiveWarehouseName()
{
return receiveWarehouseName;
}
public void setConfirmUserId(Long confirmUserId)
{
this.confirmUserId = confirmUserId;
}
public Long getConfirmUserId()
{
return confirmUserId;
}
public void setConfirmUserName(String confirmUserName)
{
this.confirmUserName = confirmUserName;
}
public String getConfirmUserName()
{
return confirmUserName;
}
public void setConfirmTime(Date confirmTime)
{
this.confirmTime = confirmTime;
}
public Date getConfirmTime()
{
return confirmTime;
}
public void setOrderStatus(String orderStatus)
{
this.orderStatus = orderStatus;
}
public String getOrderStatus()
{
return orderStatus;
}
public void setDelFlag(String delFlag)
{
this.delFlag = delFlag;
}
public String getDelFlag()
{
return delFlag;
}
public List<AmsReturnOrderItem> getAmsReturnOrderItemList()
{
return amsReturnOrderItemList;
}
public void setAmsReturnOrderItemList(List<AmsReturnOrderItem> amsReturnOrderItemList)
{
this.amsReturnOrderItemList = amsReturnOrderItemList;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("orderId", getOrderId())
.append("returnNo", getReturnNo())
.append("applicantId", getApplicantId())
.append("applicantName", getApplicantName())
.append("applyDeptId", getApplyDeptId())
.append("applyDeptName", getApplyDeptName())
.append("receiveWarehouseId", getReceiveWarehouseId())
.append("receiveWarehouseCode", getReceiveWarehouseCode())
.append("receiveWarehouseName", getReceiveWarehouseName())
.append("confirmUserId", getConfirmUserId())
.append("confirmUserName", getConfirmUserName())
.append("confirmTime", getConfirmTime())
.append("orderStatus", getOrderStatus())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.append("delFlag", getDelFlag())
.append("amsReturnOrderItemList", getAmsReturnOrderItemList())
.toString();
}
}

@ -0,0 +1,479 @@
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_return_order_item
*
* @author Yangk
* @date 2026-06-15
*/
public class AmsReturnOrderItem extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 明细ID */
private Long itemId;
/** 退库单ID */
@Excel(name = "退库单ID")
private Long orderId;
/** 退库单号快照 */
@Excel(name = "退库单号快照")
private String returnNo;
/** 资产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;
/**
*
* 退/
*/
private String assetStatus;
/**
* ID
* 退
* 1. 退
* 2. 退退
*/
private Long originWarehouseId;
/** 最近一次已完成领用的原仓库编码(仅用于前端展示与比对,不持久化) */
private String originWarehouseCode;
/** 最近一次已完成领用的原仓库名称(仅用于前端展示与比对,不持久化) */
private String originWarehouseName;
/** 最近一次已完成领用的原仓库启用状态(仅用于页面原路退库合法性校验,不持久化) */
private String originWarehouseEnabled;
/**
* ID
* 退
*/
private Long originLocationId;
/** 最近一次已完成领用的原位置编码(仅用于前端展示,不持久化) */
private String originLocationCode;
/** 最近一次已完成领用的原位置名称(仅用于前端展示,不持久化) */
private String originLocationName;
/** 最近一次已完成领用的原位置启用状态(仅用于页面原路退库合法性校验,不持久化) */
private String originLocationEnabled;
/**
* 退使ID
* 稿退
* `ams_asset`使
*/
@Excel(name = "退库前使用人ID")
private Long beforeUserId;
/** 退库前使用人名称快照。记录资产退库前那一刻的责任人名称以备审计 */
@Excel(name = "退库前使用人名称快照")
private String beforeUserName;
/**
* 退ID
* /
*/
@Excel(name = "退库前部门ID")
private Long beforeDeptId;
/** 退库前部门名称快照。记录资产退库前那一刻的所属部门以备审计 */
@Excel(name = "退库前部门名称快照")
private String beforeDeptName;
/**
* 退ID
* 退IDIN_STOCK
*/
@Excel(name = "退库后仓库ID")
private Long afterWarehouseId;
/** 退库后仓库编码快照 */
@Excel(name = "退库后仓库编码快照")
private String afterWarehouseCode;
/** 退库后仓库名称快照 */
@Excel(name = "退库后仓库名称快照")
private String afterWarehouseName;
/**
* 退ID
* /
*/
@Excel(name = "退库后位置ID")
private Long afterLocationId;
/** 退库后位置编码快照 */
@Excel(name = "退库后位置编码快照")
private String afterLocationCode;
/** 退库后位置名称快照 */
@Excel(name = "退库后位置名称快照")
private String afterLocationName;
/** 删除标志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 setReturnNo(String returnNo)
{
this.returnNo = returnNo;
}
public String getReturnNo()
{
return returnNo;
}
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 setAssetStatus(String assetStatus)
{
this.assetStatus = assetStatus;
}
public String getAssetStatus()
{
return assetStatus;
}
public Long getOriginWarehouseId()
{
return originWarehouseId;
}
public void setOriginWarehouseId(Long originWarehouseId)
{
this.originWarehouseId = originWarehouseId;
}
public String getOriginWarehouseCode()
{
return originWarehouseCode;
}
public void setOriginWarehouseCode(String originWarehouseCode)
{
this.originWarehouseCode = originWarehouseCode;
}
public String getOriginWarehouseName()
{
return originWarehouseName;
}
public void setOriginWarehouseName(String originWarehouseName)
{
this.originWarehouseName = originWarehouseName;
}
public String getOriginWarehouseEnabled()
{
return originWarehouseEnabled;
}
public void setOriginWarehouseEnabled(String originWarehouseEnabled)
{
this.originWarehouseEnabled = originWarehouseEnabled;
}
public Long getOriginLocationId()
{
return originLocationId;
}
public void setOriginLocationId(Long originLocationId)
{
this.originLocationId = originLocationId;
}
public String getOriginLocationCode()
{
return originLocationCode;
}
public void setOriginLocationCode(String originLocationCode)
{
this.originLocationCode = originLocationCode;
}
public String getOriginLocationName()
{
return originLocationName;
}
public void setOriginLocationName(String originLocationName)
{
this.originLocationName = originLocationName;
}
public String getOriginLocationEnabled()
{
return originLocationEnabled;
}
public void setOriginLocationEnabled(String originLocationEnabled)
{
this.originLocationEnabled = originLocationEnabled;
}
public void setBeforeUserId(Long beforeUserId)
{
this.beforeUserId = beforeUserId;
}
public Long getBeforeUserId()
{
return beforeUserId;
}
public void setBeforeUserName(String beforeUserName)
{
this.beforeUserName = beforeUserName;
}
public String getBeforeUserName()
{
return beforeUserName;
}
public void setBeforeDeptId(Long beforeDeptId)
{
this.beforeDeptId = beforeDeptId;
}
public Long getBeforeDeptId()
{
return beforeDeptId;
}
public void setBeforeDeptName(String beforeDeptName)
{
this.beforeDeptName = beforeDeptName;
}
public String getBeforeDeptName()
{
return beforeDeptName;
}
public void setAfterWarehouseId(Long afterWarehouseId)
{
this.afterWarehouseId = afterWarehouseId;
}
public Long getAfterWarehouseId()
{
return afterWarehouseId;
}
public void setAfterWarehouseCode(String afterWarehouseCode)
{
this.afterWarehouseCode = afterWarehouseCode;
}
public String getAfterWarehouseCode()
{
return afterWarehouseCode;
}
public void setAfterWarehouseName(String afterWarehouseName)
{
this.afterWarehouseName = afterWarehouseName;
}
public String getAfterWarehouseName()
{
return afterWarehouseName;
}
public void setAfterLocationId(Long afterLocationId)
{
this.afterLocationId = afterLocationId;
}
public Long getAfterLocationId()
{
return afterLocationId;
}
public void setAfterLocationCode(String afterLocationCode)
{
this.afterLocationCode = afterLocationCode;
}
public String getAfterLocationCode()
{
return afterLocationCode;
}
public void setAfterLocationName(String afterLocationName)
{
this.afterLocationName = afterLocationName;
}
public String getAfterLocationName()
{
return afterLocationName;
}
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("returnNo", getReturnNo())
.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("assetStatus", getAssetStatus())
.append("originWarehouseId", getOriginWarehouseId())
.append("originWarehouseName", getOriginWarehouseName())
.append("originLocationId", getOriginLocationId())
.append("originLocationName", getOriginLocationName())
.append("beforeUserId", getBeforeUserId())
.append("beforeUserName", getBeforeUserName())
.append("beforeDeptId", getBeforeDeptId())
.append("beforeDeptName", getBeforeDeptName())
.append("afterWarehouseId", getAfterWarehouseId())
.append("afterWarehouseCode", getAfterWarehouseCode())
.append("afterWarehouseName", getAfterWarehouseName())
.append("afterLocationId", getAfterLocationId())
.append("afterLocationCode", getAfterLocationCode())
.append("afterLocationName", getAfterLocationName())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.append("delFlag", getDelFlag())
.toString();
}
}

@ -0,0 +1,78 @@
package com.ruoyi.asset.mapper;
import java.util.List;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsReturnAssetCandidate;
import com.ruoyi.asset.domain.AmsReturnOrder;
import com.ruoyi.asset.domain.AmsReturnOrderItem;
import org.apache.ibatis.annotations.Param;
/**
* 退Mapper
*
* @author Yangk
*/
public interface AmsReturnOrderMapper
{
public AmsReturnOrder selectAmsReturnOrderByOrderId(Long orderId);
/**
* 退FOR UPDATE
* <p>
*
* DRAFT -> PENDING_CONFIRM退PENDING_CONFIRM -> RETURNED
* 线线
* </p>
*/
public AmsReturnOrder selectAmsReturnOrderByOrderIdForUpdate(Long orderId);
public List<AmsReturnOrder> selectAmsReturnOrderList(AmsReturnOrder amsReturnOrder);
/**
* 退退
*
* @param amsAsset
* @param currentOrderId 退ID
* @param originWarehouseId ID
* @param missingOriginOnly
* @param inUseStatus
* @param draftStatus 退稿
* @param pendingStatus 退
*/
public List<AmsReturnAssetCandidate> selectAvailableReturnAssetList(@Param("asset") AmsAsset amsAsset,
@Param("currentOrderId") Long currentOrderId, @Param("originWarehouseId") Long originWarehouseId,
@Param("missingOriginOnly") Boolean missingOriginOnly, @Param("inUseStatus") String inUseStatus,
@Param("draftStatus") String draftStatus, @Param("pendingStatus") String pendingStatus);
/**
*
*
*/
public AmsReturnAssetCandidate selectReturnAssetCandidateByAssetId(Long assetId);
/**
* 退稿
*
*/
public int countOtherActiveReturnOrderByAssetId(@Param("assetId") Long assetId,
@Param("currentOrderId") Long currentOrderId, @Param("draftStatus") String draftStatus,
@Param("pendingStatus") String pendingStatus);
public int insertAmsReturnOrder(AmsReturnOrder amsReturnOrder);
public int updateAmsReturnOrder(AmsReturnOrder amsReturnOrder);
public int submitAmsReturnOrder(AmsReturnOrder amsReturnOrder);
public int confirmAmsReturnOrder(AmsReturnOrder amsReturnOrder);
public int deleteAmsReturnOrderByOrderId(Long orderId);
public int deleteAmsReturnOrderByOrderIds(String[] orderIds);
public int deleteAmsReturnOrderItemByOrderIds(String[] orderIds);
public int batchAmsReturnOrderItem(List<AmsReturnOrderItem> amsReturnOrderItemList);
public int deleteAmsReturnOrderItemByOrderId(Long orderId);
}

@ -0,0 +1,38 @@
package com.ruoyi.asset.service;
import java.util.List;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsReturnAssetCandidate;
import com.ruoyi.asset.domain.AmsReturnOrder;
/**
* 退Service
*
* @author Yangk
*/
public interface IAmsReturnOrderService
{
public AmsReturnOrder selectAmsReturnOrderByOrderId(Long orderId);
public List<AmsReturnOrder> selectAmsReturnOrderList(AmsReturnOrder amsReturnOrder);
/** 查询未被其他有效退库单占用的在用资产。 */
public List<AmsReturnAssetCandidate> selectAvailableReturnAssetList(AmsAsset amsAsset, Long currentOrderId,
Long originWarehouseId, Boolean missingOriginOnly);
public int insertAmsReturnOrder(AmsReturnOrder amsReturnOrder, Long applicantId,
String applicantName, Long applyDeptId);
public int updateAmsReturnOrder(AmsReturnOrder amsReturnOrder);
/** 提交退库单(草稿 → 待确认)。 */
public int submitReturn(Long orderId, String operateLoginName);
/** 确认退库(待确认 → 已退库)。 */
public int confirmReturn(Long orderId, Long operateUserId, String operateUserName,
String operateLoginName);
public int deleteAmsReturnOrderByOrderIds(String orderIds);
public int deleteAmsReturnOrderByOrderId(Long orderId);
}

@ -0,0 +1,689 @@
package com.ruoyi.asset.service.impl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import com.ruoyi.asset.constant.AssetStatus;
import com.ruoyi.asset.constant.ReturnOrderStatus;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsAssetLocation;
import com.ruoyi.asset.domain.AmsReturnAssetCandidate;
import com.ruoyi.asset.domain.AmsReturnOrder;
import com.ruoyi.asset.domain.AmsReturnOrderItem;
import com.ruoyi.asset.domain.AmsWarehouse;
import com.ruoyi.asset.domain.AssetTransitionContext;
import com.ruoyi.asset.mapper.AmsAssetMapper;
import com.ruoyi.asset.mapper.AmsReturnOrderMapper;
import com.ruoyi.asset.service.IAmsAssetLocationService;
import com.ruoyi.asset.service.IAmsReturnOrderService;
import com.ruoyi.asset.service.IAmsWarehouseService;
import com.ruoyi.asset.service.IAssetStatusTransitionService;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.domain.entity.SysUser;
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 com.ruoyi.system.service.ISysDeptService;
import com.ruoyi.system.service.ISysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 退Service
* <p>
*
* 1. 使退
* 2. 退 稿(DRAFT) -> (PENDING_CONFIRM) -> 退(RETURNED)
* 3. /
* 4. 退
* </p>
*
* @author Yangk
*/
@Service
public class AmsReturnOrderServiceImpl implements IAmsReturnOrderService
{
/** 退库单在 sys_code_rule 中配置的自动编码规则标识 */
private static final String RETURN_ORDER_RULE = "RETURN_ORDER";
private static final String ENABLED_YES = "Y";
private static final String DEL_FLAG_NORMAL = "0";
@Autowired
private AmsReturnOrderMapper amsReturnOrderMapper;
@Autowired
private AmsAssetMapper amsAssetMapper;
@Autowired
private ISysCodeRuleService sysCodeRuleService;
@Autowired
private IAmsWarehouseService amsWarehouseService;
@Autowired
private IAmsAssetLocationService amsAssetLocationService;
@Autowired
private ISysDeptService sysDeptService;
@Autowired
private ISysUserService sysUserService;
@Autowired
private IAssetStatusTransitionService assetStatusTransitionService;
@Override
public AmsReturnOrder selectAmsReturnOrderByOrderId(Long orderId)
{
AmsReturnOrder order = amsReturnOrderMapper.selectAmsReturnOrderByOrderId(orderId);
fillOriginReferences(order);
return order;
}
@Override
public List<AmsReturnOrder> selectAmsReturnOrderList(AmsReturnOrder order)
{
return amsReturnOrderMapper.selectAmsReturnOrderList(order);
}
@Override
public List<AmsReturnAssetCandidate> selectAvailableReturnAssetList(AmsAsset asset, Long currentOrderId,
Long originWarehouseId, Boolean missingOriginOnly)
{
return amsReturnOrderMapper.selectAvailableReturnAssetList(asset, currentOrderId,
originWarehouseId, missingOriginOnly,
AssetStatus.IN_USE, ReturnOrderStatus.DRAFT, ReturnOrderStatus.PENDING_CONFIRM);
}
/**
* 退稿
* <p>
*
* 1.
* 2. `sysCodeRuleService.nextCode` 退
* 3. 使
* </p>
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int insertAmsReturnOrder(AmsReturnOrder order, Long applicantId, String applicantName,
Long applyDeptId)
{
validateOrderRequest(order);
fillApplicantSnapshots(order, applicantId, applicantName, applyDeptId);
order.setReturnNo(sysCodeRuleService.nextCode(RETURN_ORDER_RULE));
order.setOrderStatus(ReturnOrderStatus.DRAFT);
order.setConfirmUserId(null);
order.setConfirmUserName(null);
order.setConfirmTime(null);
order.setDelFlag(DEL_FLAG_NORMAL);
order.setCreateTime(DateUtils.getNowDate());
fillOrderSnapshots(order, null);
if (amsReturnOrderMapper.insertAmsReturnOrder(order) != 1 || StringUtils.isNull(order.getOrderId()))
{
throw new ServiceException("退库单保存失败");
}
insertReturnOrderItems(order);
return 1;
}
/**
* 退稿
* <p>
*
* 1. 稿(DRAFT)退
* 2. 退 `returnNo`/
* 使
* 3.
* </p>
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int updateAmsReturnOrder(AmsReturnOrder order)
{
if (StringUtils.isNull(order) || StringUtils.isNull(order.getOrderId()))
{
throw new ServiceException("退库单ID不能为空");
}
AmsReturnOrder current = requireOrderForUpdate(order.getOrderId(), ReturnOrderStatus.DRAFT,
"仅草稿退库单允许修改");
validateOrderRequest(order);
// 受控字段必须使用数据库当前值,防止普通编辑绕过单据状态和申请人快照。
order.setReturnNo(current.getReturnNo());
order.setApplicantId(current.getApplicantId());
order.setApplicantName(current.getApplicantName());
order.setApplyDeptId(current.getApplyDeptId());
order.setApplyDeptName(current.getApplyDeptName());
order.setOrderStatus(ReturnOrderStatus.DRAFT);
order.setConfirmUserId(null);
order.setConfirmUserName(null);
order.setConfirmTime(null);
order.setUpdateTime(DateUtils.getNowDate());
fillOrderSnapshots(order, order.getOrderId());
amsReturnOrderMapper.deleteAmsReturnOrderItemByOrderId(order.getOrderId());
insertReturnOrderItems(order);
if (amsReturnOrderMapper.updateAmsReturnOrder(order) != 1)
{
throw new ServiceException("退库单状态已变化,保存失败");
}
return 1;
}
/**
* 退
* <p>
*
* 1. /使 `requireOrderForUpdate` `FOR UPDATE` 退
* 稿(DRAFT)
* 2. `validateOrderReadyForFlow` 退使稿
* 3.
* </p>
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int submitReturn(Long orderId, String operateLoginName)
{
AmsReturnOrder order = requireOrderForUpdate(orderId, ReturnOrderStatus.DRAFT,
"仅草稿退库单允许提交");
validateLoginName(operateLoginName);
validateOrderReadyForFlow(order);
order.setOrderStatus(ReturnOrderStatus.PENDING_CONFIRM);
order.setUpdateBy(StringUtils.trim(operateLoginName));
order.setUpdateTime(DateUtils.getNowDate());
if (amsReturnOrderMapper.submitAmsReturnOrder(order) != 1)
{
throw new ServiceException("退库单状态已变化,提交失败");
}
return 1;
}
/**
* 退
* <p>
*
* 1. 使 `requireOrderForUpdate` (PENDING_CONFIRM)
* 2. `validateOrderReadyForFlow`
* 3. 退使退/
* `assetStatusTransitionService.confirmReturn`
* SQL RFID
* </p>
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int confirmReturn(Long orderId, Long operateUserId, String operateUserName,
String operateLoginName)
{
AmsReturnOrder order = requireOrderForUpdate(orderId, ReturnOrderStatus.PENDING_CONFIRM,
"仅待确认退库单允许确认");
validateOperator(operateUserId, operateUserName, "确认人");
validateLoginName(operateLoginName);
validateOrderReadyForFlow(order);
for (AmsReturnOrderItem item : sortedItems(order))
{
AssetTransitionContext context = new AssetTransitionContext();
context.setSourceOrderId(order.getOrderId());
context.setSourceOrderNo(order.getReturnNo());
context.setSourceItemId(item.getItemId());
context.setOperateUserId(operateUserId);
context.setOperateUserName(StringUtils.trim(operateUserName));
context.setOperateLoginName(StringUtils.trim(operateLoginName));
context.setChangeSummary("确认资产退库");
context.setRemark(order.getRemark());
assetStatusTransitionService.confirmReturn(item.getAssetId(), item.getAfterWarehouseId(),
item.getAfterLocationId(), context);
}
Date now = DateUtils.getNowDate();
order.setOrderStatus(ReturnOrderStatus.RETURNED);
order.setConfirmUserId(operateUserId);
order.setConfirmUserName(StringUtils.trim(operateUserName));
order.setConfirmTime(now);
order.setUpdateBy(StringUtils.trim(operateLoginName));
order.setUpdateTime(now);
if (amsReturnOrderMapper.confirmAmsReturnOrder(order) != 1)
{
throw new ServiceException("退库单状态已变化,确认退库失败");
}
return 1;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteAmsReturnOrderByOrderIds(String orderIds)
{
Long[] sortedIds = Arrays.stream(Convert.toStrArray(orderIds))
.map(Long::valueOf).sorted().toArray(Long[]::new);
int rows = 0;
for (Long orderId : sortedIds)
{
AmsReturnOrder order = amsReturnOrderMapper.selectAmsReturnOrderByOrderIdForUpdate(orderId);
if (StringUtils.isNull(order))
{
continue;
}
requireDraft(order);
amsReturnOrderMapper.deleteAmsReturnOrderItemByOrderId(orderId);
rows += amsReturnOrderMapper.deleteAmsReturnOrderByOrderId(orderId);
}
return rows;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteAmsReturnOrderByOrderId(Long orderId)
{
AmsReturnOrder order = amsReturnOrderMapper.selectAmsReturnOrderByOrderIdForUpdate(orderId);
if (StringUtils.isNull(order))
{
return 0;
}
requireDraft(order);
amsReturnOrderMapper.deleteAmsReturnOrderItemByOrderId(orderId);
return amsReturnOrderMapper.deleteAmsReturnOrderByOrderId(orderId);
}
private void validateOrderRequest(AmsReturnOrder order)
{
if (StringUtils.isNull(order))
{
throw new ServiceException("退库单不能为空");
}
if (order.getAmsReturnOrderItemList() == null || order.getAmsReturnOrderItemList().isEmpty())
{
throw new ServiceException("退库单明细不能为空");
}
validateLength(order.getRemark(), 500, "备注");
}
private void fillApplicantSnapshots(AmsReturnOrder order, Long applicantId, String applicantName,
Long applyDeptId)
{
validateOperator(applicantId, applicantName, "申请人");
SysUser applicant = requireNormalUser(applicantId, "申请人");
SysDept applyDept = requireNormalDept(applyDeptId, "申请部门");
if (!Objects.equals(applicant.getDeptId(), applyDept.getDeptId()))
{
throw new ServiceException("申请人不属于当前申请部门");
}
order.setApplicantId(applicantId);
order.setApplicantName(StringUtils.trim(applicantName));
order.setApplyDeptId(applyDept.getDeptId());
order.setApplyDeptName(applyDept.getDeptName());
}
/**
* 稿ID退使退
*/
private void fillOrderSnapshots(AmsReturnOrder order, Long currentOrderId)
{
AmsWarehouse warehouse = requireEnabledWarehouse(order.getReceiveWarehouseId());
order.setReceiveWarehouseCode(warehouse.getWarehouseCode());
order.setReceiveWarehouseName(warehouse.getWarehouseName());
Set<Long> assetIds = new HashSet<>();
ReturnOriginGroup originGroup = new ReturnOriginGroup();
List<AmsReturnOrderItem> items = sortedItems(order);
order.setAmsReturnOrderItemList(items);
for (AmsReturnOrderItem item : items)
{
AmsAsset asset = lockReturnableAsset(item.getAssetId());
if (!assetIds.add(asset.getAssetId()))
{
throw new ServiceException("同一退库单不能重复选择资产");
}
validateNotOccupied(asset, currentOrderId);
originGroup.accept(asset, fillOriginReference(item));
fillAssetSnapshots(item, asset);
fillTargetSnapshots(item, asset, warehouse);
item.setReturnNo(order.getReturnNo());
item.setDelFlag(DEL_FLAG_NORMAL);
validateLength(item.getRemark(), 500, "明细备注");
}
}
/**
* 稿使退
*/
private void validateOrderReadyForFlow(AmsReturnOrder order)
{
AmsWarehouse warehouse = requireEnabledWarehouse(order.getReceiveWarehouseId());
List<AmsReturnOrderItem> items = sortedItems(order);
if (items.isEmpty())
{
throw new ServiceException("退库单明细不能为空");
}
Set<Long> assetIds = new HashSet<>();
ReturnOriginGroup originGroup = new ReturnOriginGroup();
for (AmsReturnOrderItem item : items)
{
AmsAsset asset = lockReturnableAsset(item.getAssetId());
if (!assetIds.add(asset.getAssetId()))
{
throw new ServiceException("同一退库单不能重复选择资产");
}
validateNotOccupied(asset, order.getOrderId());
originGroup.accept(asset, fillOriginReference(item));
validateBeforeSnapshot(item, asset);
fillTargetSnapshots(item, asset, warehouse);
}
}
private AmsAsset lockReturnableAsset(Long assetId)
{
if (StringUtils.isNull(assetId))
{
throw new ServiceException("退库资产不能为空");
}
AmsAsset asset = amsAssetMapper.selectAmsAssetByAssetIdForUpdate(assetId);
if (StringUtils.isNull(asset))
{
throw new ServiceException("退库资产不存在或已删除");
}
if (!StringUtils.equals(AssetStatus.IN_USE, asset.getAssetStatus()))
{
throw new ServiceException(StringUtils.format("资产【{}】当前状态不允许退库", asset.getAssetCode()));
}
if (StringUtils.isNull(asset.getUseDeptId()) || StringUtils.isNull(asset.getUseUserId())
|| StringUtils.isEmpty(asset.getUseDeptName()) || StringUtils.isEmpty(asset.getUseUserName()))
{
throw new ServiceException(StringUtils.format("在用资产【{}】缺少当前使用部门或使用人", asset.getAssetCode()));
}
return asset;
}
private void validateNotOccupied(AmsAsset asset, Long currentOrderId)
{
if (amsReturnOrderMapper.countOtherActiveReturnOrderByAssetId(asset.getAssetId(), currentOrderId,
ReturnOrderStatus.DRAFT, ReturnOrderStatus.PENDING_CONFIRM) > 0)
{
throw new ServiceException(StringUtils.format("资产【{}】已被其他有效退库单占用", asset.getAssetCode()));
}
}
private void fillAssetSnapshots(AmsReturnOrderItem item, AmsAsset asset)
{
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.setAssetStatus(asset.getAssetStatus());
item.setBeforeDeptId(asset.getUseDeptId());
item.setBeforeDeptName(asset.getUseDeptName());
item.setBeforeUserId(asset.getUseUserId());
item.setBeforeUserName(asset.getUseUserName());
}
/** 回填最近一次已完成领用的原仓位,供页面默认带出和后端单仓库规则校验。 */
private AmsReturnAssetCandidate fillOriginReference(AmsReturnOrderItem item)
{
AmsReturnAssetCandidate candidate = amsReturnOrderMapper.selectReturnAssetCandidateByAssetId(item.getAssetId());
item.setOriginWarehouseId(candidate == null ? null : candidate.getOriginWarehouseId());
item.setOriginWarehouseCode(candidate == null ? null : candidate.getOriginWarehouseCode());
item.setOriginWarehouseName(candidate == null ? null : candidate.getOriginWarehouseName());
item.setOriginWarehouseEnabled(candidate == null ? null : candidate.getOriginWarehouseEnabled());
item.setOriginLocationId(candidate == null ? null : candidate.getOriginLocationId());
item.setOriginLocationCode(candidate == null ? null : candidate.getOriginLocationCode());
item.setOriginLocationName(candidate == null ? null : candidate.getOriginLocationName());
item.setOriginLocationEnabled(candidate == null ? null : candidate.getOriginLocationEnabled());
return candidate;
}
private void fillOriginReferences(AmsReturnOrder order)
{
if (order == null || order.getAmsReturnOrderItemList() == null)
{
return;
}
for (AmsReturnOrderItem item : order.getAmsReturnOrderItemList())
{
fillOriginReference(item);
}
}
private void fillTargetSnapshots(AmsReturnOrderItem item, AmsAsset asset, AmsWarehouse warehouse)
{
AmsAssetLocation location = requireEnabledLocation(item.getAfterLocationId());
if (!Objects.equals(warehouse.getWarehouseId(), location.getWarehouseId()))
{
throw new ServiceException(StringUtils.format("资产【{}】的退库位置不属于接收仓库", asset.getAssetCode()));
}
item.setAfterWarehouseId(warehouse.getWarehouseId());
item.setAfterWarehouseCode(warehouse.getWarehouseCode());
item.setAfterWarehouseName(warehouse.getWarehouseName());
item.setAfterLocationCode(location.getLocationCode());
item.setAfterLocationName(location.getLocationName());
}
private void validateBeforeSnapshot(AmsReturnOrderItem item, AmsAsset asset)
{
if (!Objects.equals(item.getBeforeDeptId(), asset.getUseDeptId())
|| !Objects.equals(item.getBeforeUserId(), asset.getUseUserId()))
{
throw new ServiceException(StringUtils.format("资产【{}】当前使用归属已变化,请重新编辑并提交退库单",
asset.getAssetCode()));
}
}
private AmsWarehouse requireEnabledWarehouse(Long warehouseId)
{
if (StringUtils.isNull(warehouseId))
{
throw new ServiceException("接收仓库不能为空");
}
AmsWarehouse warehouse = amsWarehouseService.selectAmsWarehouseByWarehouseId(warehouseId);
if (StringUtils.isNull(warehouse) || !StringUtils.equals(ENABLED_YES, warehouse.getEnabled()))
{
throw new ServiceException("接收仓库不存在或已停用");
}
return warehouse;
}
private AmsAssetLocation requireEnabledLocation(Long locationId)
{
if (StringUtils.isNull(locationId))
{
throw new ServiceException("退库位置不能为空");
}
AmsAssetLocation location = amsAssetLocationService.selectAmsAssetLocationByLocationId(locationId);
if (StringUtils.isNull(location) || !StringUtils.equals(ENABLED_YES, location.getEnabled()))
{
throw new ServiceException("退库位置不存在或已停用");
}
return location;
}
private SysDept requireNormalDept(Long deptId, String fieldName)
{
if (StringUtils.isNull(deptId))
{
throw new ServiceException(fieldName + "不能为空");
}
SysDept dept = sysDeptService.selectDeptById(deptId);
if (StringUtils.isNull(dept)
|| !StringUtils.equals(UserConstants.DEPT_NORMAL, dept.getStatus())
|| !StringUtils.equals(DEL_FLAG_NORMAL, dept.getDelFlag()))
{
throw new ServiceException(fieldName + "不存在或已停用");
}
return dept;
}
private SysUser requireNormalUser(Long userId, String fieldName)
{
if (StringUtils.isNull(userId))
{
throw new ServiceException(fieldName + "不能为空");
}
SysUser user = sysUserService.selectUserById(userId);
if (StringUtils.isNull(user)
|| !StringUtils.equals(UserConstants.NORMAL, user.getStatus())
|| !StringUtils.equals(DEL_FLAG_NORMAL, user.getDelFlag()))
{
throw new ServiceException(fieldName + "不存在或已停用");
}
return user;
}
private AmsReturnOrder requireOrderForUpdate(Long orderId, String requiredStatus, String message)
{
if (StringUtils.isNull(orderId))
{
throw new ServiceException("退库单ID不能为空");
}
AmsReturnOrder order = amsReturnOrderMapper.selectAmsReturnOrderByOrderIdForUpdate(orderId);
if (StringUtils.isNull(order))
{
throw new ServiceException("退库单不存在或已删除");
}
if (!StringUtils.equals(requiredStatus, order.getOrderStatus()))
{
throw new ServiceException(message);
}
return order;
}
private void requireDraft(AmsReturnOrder order)
{
if (!StringUtils.equals(ReturnOrderStatus.DRAFT, order.getOrderStatus()))
{
throw new ServiceException("仅草稿退库单允许删除");
}
}
/**
* ID
* <p>
*
* Deadlock Prevention
* `FOR UPDATE`
* A12B21
* 使 ID 使
*
* </p>
*/
private List<AmsReturnOrderItem> sortedItems(AmsReturnOrder order)
{
if (order.getAmsReturnOrderItemList() == null)
{
return new ArrayList<>();
}
List<AmsReturnOrderItem> items = new ArrayList<>(order.getAmsReturnOrderItemList());
items.sort(Comparator.comparing(AmsReturnOrderItem::getAssetId,
Comparator.nullsFirst(Long::compareTo)));
return items;
}
private void insertReturnOrderItems(AmsReturnOrder order)
{
List<AmsReturnOrderItem> items = new ArrayList<>();
Date now = DateUtils.getNowDate();
for (AmsReturnOrderItem item : order.getAmsReturnOrderItemList())
{
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);
items.add(item);
}
if (amsReturnOrderMapper.batchAmsReturnOrderItem(items) != items.size())
{
throw new ServiceException("退库单明细保存失败");
}
}
private void validateOperator(Long userId, String userName, String fieldName)
{
if (StringUtils.isNull(userId) || StringUtils.isEmpty(StringUtils.trim(userName)))
{
throw new ServiceException(fieldName + "ID和名称不能为空");
}
validateLength(StringUtils.trim(userName), 100, fieldName + "名称");
}
private void validateLoginName(String loginName)
{
if (StringUtils.isEmpty(StringUtils.trim(loginName)))
{
throw new ServiceException("操作登录账号不能为空");
}
validateLength(StringUtils.trim(loginName), 64, "操作登录账号");
}
private void validateLength(String value, int maxLength, String fieldName)
{
if (StringUtils.isNotEmpty(value) && value.length() > maxLength)
{
throw new ServiceException(fieldName + "长度不能超过" + maxLength + "个字符");
}
}
/**
* 退
* <p>
*
* 1. 退 `originWarehouseId`退
*
* 2.
* 退
* </p>
*/
private static class ReturnOriginGroup
{
private boolean initialized;
private boolean traceable;
private Long originWarehouseId;
/**
*
*
* @param asset
* @param candidate
* @throws ServiceException ID
*/
void accept(AmsAsset asset, AmsReturnAssetCandidate candidate)
{
boolean currentTraceable = candidate != null && candidate.isOriginTraceable();
Long currentWarehouseId = currentTraceable ? candidate.getOriginWarehouseId() : null;
if (!initialized)
{
initialized = true;
traceable = currentTraceable;
originWarehouseId = currentWarehouseId;
return;
}
if (traceable != currentTraceable)
{
throw new ServiceException(StringUtils.format(
"资产【{}】与当前退库单的原领用仓位来源类型不一致", asset.getAssetCode()));
}
if (traceable && !Objects.equals(originWarehouseId, currentWarehouseId))
{
throw new ServiceException(StringUtils.format(
"资产【{}】的原领用仓库与当前退库单不一致", asset.getAssetCode()));
}
}
}
}

@ -0,0 +1,389 @@
<?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.AmsReturnOrderMapper">
<resultMap type="AmsReturnOrder" id="AmsReturnOrderResult">
<result property="orderId" column="order_id" />
<result property="returnNo" column="return_no" />
<result property="applicantId" column="applicant_id" />
<result property="applicantName" column="applicant_name" />
<result property="applyDeptId" column="apply_dept_id" />
<result property="applyDeptName" column="apply_dept_name" />
<result property="receiveWarehouseId" column="receive_warehouse_id" />
<result property="receiveWarehouseCode" column="receive_warehouse_code" />
<result property="receiveWarehouseName" column="receive_warehouse_name" />
<result property="confirmUserId" column="confirm_user_id" />
<result property="confirmUserName" column="confirm_user_name" />
<result property="confirmTime" column="confirm_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="AmsReturnOrderItemCollectionResult" type="AmsReturnOrder" extends="AmsReturnOrderResult">
<collection property="amsReturnOrderItemList" ofType="AmsReturnOrderItem"
column="order_id" select="selectAmsReturnOrderItemList" />
</resultMap>
<resultMap type="AmsReturnOrderItem" id="AmsReturnOrderItemResult">
<result property="itemId" column="item_id" />
<result property="orderId" column="order_id" />
<result property="returnNo" column="return_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="assetStatus" column="asset_status" />
<result property="beforeUserId" column="before_user_id" />
<result property="beforeUserName" column="before_user_name" />
<result property="beforeDeptId" column="before_dept_id" />
<result property="beforeDeptName" column="before_dept_name" />
<result property="afterWarehouseId" column="after_warehouse_id" />
<result property="afterWarehouseCode" column="after_warehouse_code" />
<result property="afterWarehouseName" column="after_warehouse_name" />
<result property="afterLocationId" column="after_location_id" />
<result property="afterLocationCode" column="after_location_code" />
<result property="afterLocationName" column="after_location_name" />
<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 type="AmsReturnAssetCandidate" id="AvailableReturnAssetResult">
<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="assetStatus" column="asset_status" />
<result property="warehouseId" column="warehouse_id" />
<result property="warehouseCode" column="warehouse_code" />
<result property="warehouseName" column="warehouse_name" />
<result property="locationId" column="location_id" />
<result property="locationCode" column="location_code" />
<result property="locationName" column="location_name" />
<result property="useDeptId" column="use_dept_id" />
<result property="useDeptName" column="use_dept_name" />
<result property="useUserId" column="use_user_id" />
<result property="useUserName" column="use_user_name" />
<result property="tagCode" column="tag_code" />
<result property="sourceReceiveOrderId" column="source_receive_order_id" />
<result property="sourceReceiveItemId" column="source_receive_item_id" />
<result property="originWarehouseId" column="origin_warehouse_id" />
<result property="originWarehouseCode" column="origin_warehouse_code" />
<result property="originWarehouseName" column="origin_warehouse_name" />
<result property="originWarehouseEnabled" column="origin_warehouse_enabled" />
<result property="originLocationId" column="origin_location_id" />
<result property="originLocationCode" column="origin_location_code" />
<result property="originLocationName" column="origin_location_name" />
<result property="originLocationEnabled" column="origin_location_enabled" />
</resultMap>
<sql id="selectAmsReturnOrderVo">
select order_id, return_no, applicant_id, applicant_name, apply_dept_id, apply_dept_name,
receive_warehouse_id, receive_warehouse_code, receive_warehouse_name,
confirm_user_id, confirm_user_name, confirm_time, order_status,
create_by, create_time, update_by, update_time, remark, del_flag
from ams_return_order
</sql>
<select id="selectAmsReturnOrderList" parameterType="AmsReturnOrder" resultMap="AmsReturnOrderResult">
<include refid="selectAmsReturnOrderVo"/>
<where>
del_flag = '0'
<if test="returnNo != null and returnNo != ''">
and return_no like concat(#{returnNo}, '%')
</if>
<if test="applicantName != null and applicantName != ''">
and applicant_name like concat('%', #{applicantName}, '%')
</if>
<if test="applyDeptId != null">
and apply_dept_id = #{applyDeptId}
</if>
<if test="receiveWarehouseId != null">
and receive_warehouse_id = #{receiveWarehouseId}
</if>
<if test="params.beginConfirmTime != null and params.beginConfirmTime != ''">
and confirm_time &gt;= #{params.beginConfirmTime}
</if>
<if test="params.endConfirmTime != null and params.endConfirmTime != ''">
and confirm_time &lt; date_add(#{params.endConfirmTime}, interval 1 day)
</if>
<if test="orderStatus != null and orderStatus != ''">
and order_status = #{orderStatus}
</if>
<if test="params.assetCode != null and params.assetCode != ''">
and exists (
select 1 from ams_return_order_item item
where item.order_id = ams_return_order.order_id
and item.del_flag = '0'
and item.asset_code like concat(#{params.assetCode}, '%')
)
</if>
</where>
order by create_time desc, order_id desc
</select>
<select id="selectAmsReturnOrderByOrderId" parameterType="Long"
resultMap="AmsReturnOrderItemCollectionResult">
<include refid="selectAmsReturnOrderVo"/>
where order_id = #{orderId} and del_flag = '0'
</select>
<select id="selectAmsReturnOrderByOrderIdForUpdate" parameterType="Long"
resultMap="AmsReturnOrderItemCollectionResult">
<include refid="selectAmsReturnOrderVo"/>
where order_id = #{orderId} and del_flag = '0'
for update
</select>
<select id="selectAvailableReturnAssetList" resultMap="AvailableReturnAssetResult">
select asset.asset_id, asset.asset_code, asset.asset_name, asset.category_id,
asset.category_code, asset.category_name, asset.spec_model, asset.brand,
asset.asset_status, asset.warehouse_id, asset.warehouse_code, asset.warehouse_name,
asset.location_id, asset.location_code, asset.location_name, asset.use_dept_id,
asset.use_dept_name, asset.use_user_id, asset.use_user_name, asset.tag_code,
origin_order.order_id as source_receive_order_id,
origin_item.item_id as source_receive_item_id,
origin_item.before_warehouse_id as origin_warehouse_id,
origin_item.before_warehouse_code as origin_warehouse_code,
origin_item.before_warehouse_name as origin_warehouse_name,
origin_warehouse.enabled as origin_warehouse_enabled,
origin_item.before_location_id as origin_location_id,
origin_item.before_location_code as origin_location_code,
origin_item.before_location_name as origin_location_name,
origin_location.enabled as origin_location_enabled
from ams_asset asset
left join ams_receive_order_item origin_item on origin_item.item_id = (
select candidate_item.item_id
from ams_receive_order_item candidate_item
inner join ams_receive_order candidate_order on candidate_order.order_id = candidate_item.order_id
and candidate_order.del_flag = '0'
and candidate_order.order_status = 'COMPLETED'
where candidate_item.asset_id = asset.asset_id
and candidate_item.del_flag = '0'
order by candidate_order.confirm_time desc, candidate_order.order_id desc, candidate_item.item_id desc
limit 1
)
left join ams_receive_order origin_order on origin_order.order_id = origin_item.order_id
left join ams_warehouse origin_warehouse on origin_warehouse.warehouse_id = origin_item.before_warehouse_id
and origin_warehouse.del_flag = '0'
left join ams_asset_location origin_location on origin_location.location_id = origin_item.before_location_id
and origin_location.del_flag = '0'
where asset.del_flag = '0'
and asset.asset_status = #{inUseStatus}
and not exists (
select 1
from ams_return_order_item item
inner join ams_return_order return_order on return_order.order_id = item.order_id
and return_order.del_flag = '0'
and return_order.order_status in (#{draftStatus}, #{pendingStatus})
where item.asset_id = asset.asset_id
and item.del_flag = '0'
<if test="currentOrderId != null">
and return_order.order_id != #{currentOrderId}
</if>
)
<if test="asset.assetCode != null and asset.assetCode != ''">
and asset.asset_code like concat('%', #{asset.assetCode}, '%')
</if>
<if test="asset.assetName != null and asset.assetName != ''">
and asset.asset_name like concat('%', #{asset.assetName}, '%')
</if>
<if test="asset.categoryName != null and asset.categoryName != ''">
and asset.category_name like concat('%', #{asset.categoryName}, '%')
</if>
<if test="asset.useDeptId != null">
and asset.use_dept_id = #{asset.useDeptId}
</if>
<if test="asset.useUserName != null and asset.useUserName != ''">
and asset.use_user_name like concat('%', #{asset.useUserName}, '%')
</if>
<if test="originWarehouseId != null">
and origin_item.before_warehouse_id = #{originWarehouseId}
and origin_item.before_location_id is not null
</if>
<if test="missingOriginOnly != null and missingOriginOnly">
and (origin_item.before_warehouse_id is null or origin_item.before_location_id is null)
</if>
order by asset.asset_id
</select>
<select id="selectReturnAssetCandidateByAssetId" parameterType="Long" resultMap="AvailableReturnAssetResult">
select asset.asset_id, asset.asset_code, asset.asset_name, asset.category_id,
asset.category_code, asset.category_name, asset.spec_model, asset.brand,
asset.asset_status, asset.warehouse_id, asset.warehouse_code, asset.warehouse_name,
asset.location_id, asset.location_code, asset.location_name, asset.use_dept_id,
asset.use_dept_name, asset.use_user_id, asset.use_user_name, asset.tag_code,
origin_order.order_id as source_receive_order_id,
origin_item.item_id as source_receive_item_id,
origin_item.before_warehouse_id as origin_warehouse_id,
origin_item.before_warehouse_code as origin_warehouse_code,
origin_item.before_warehouse_name as origin_warehouse_name,
origin_warehouse.enabled as origin_warehouse_enabled,
origin_item.before_location_id as origin_location_id,
origin_item.before_location_code as origin_location_code,
origin_item.before_location_name as origin_location_name,
origin_location.enabled as origin_location_enabled
from ams_asset asset
left join ams_receive_order_item origin_item on origin_item.item_id = (
select candidate_item.item_id
from ams_receive_order_item candidate_item
inner join ams_receive_order candidate_order on candidate_order.order_id = candidate_item.order_id
and candidate_order.del_flag = '0'
and candidate_order.order_status = 'COMPLETED'
where candidate_item.asset_id = asset.asset_id
and candidate_item.del_flag = '0'
order by candidate_order.confirm_time desc, candidate_order.order_id desc, candidate_item.item_id desc
limit 1
)
left join ams_receive_order origin_order on origin_order.order_id = origin_item.order_id
left join ams_warehouse origin_warehouse on origin_warehouse.warehouse_id = origin_item.before_warehouse_id
and origin_warehouse.del_flag = '0'
left join ams_asset_location origin_location on origin_location.location_id = origin_item.before_location_id
and origin_location.del_flag = '0'
where asset.asset_id = #{assetId}
and asset.del_flag = '0'
</select>
<select id="countOtherActiveReturnOrderByAssetId" resultType="int">
select count(1)
from ams_return_order_item item
inner join ams_return_order return_order on return_order.order_id = item.order_id
and return_order.del_flag = '0'
and return_order.order_status in (#{draftStatus}, #{pendingStatus})
where item.asset_id = #{assetId}
and item.del_flag = '0'
<if test="currentOrderId != null">
and return_order.order_id != #{currentOrderId}
</if>
</select>
<select id="selectAmsReturnOrderItemList" resultMap="AmsReturnOrderItemResult">
select item.item_id, item.order_id, item.return_no, item.asset_id, item.asset_code,
item.asset_name, item.category_id, item.category_code, item.category_name,
item.spec_model, item.brand, asset.asset_status, item.before_user_id,
item.before_user_name, item.before_dept_id, item.before_dept_name,
item.after_warehouse_id, item.after_warehouse_code, item.after_warehouse_name,
item.after_location_id, item.after_location_code, item.after_location_name,
item.create_by, item.create_time, item.update_by, item.update_time,
item.remark, item.del_flag
from ams_return_order_item item
left join ams_asset asset on asset.asset_id = item.asset_id and asset.del_flag = '0'
where item.order_id = #{order_id} and item.del_flag = '0'
order by item.item_id
</select>
<insert id="insertAmsReturnOrder" parameterType="AmsReturnOrder"
useGeneratedKeys="true" keyProperty="orderId">
insert into ams_return_order (
return_no, applicant_id, applicant_name, apply_dept_id, apply_dept_name,
receive_warehouse_id, receive_warehouse_code, receive_warehouse_name,
order_status, create_by, create_time, remark, del_flag
) values (
#{returnNo}, #{applicantId}, #{applicantName}, #{applyDeptId}, #{applyDeptName},
#{receiveWarehouseId}, #{receiveWarehouseCode}, #{receiveWarehouseName},
#{orderStatus}, #{createBy}, #{createTime}, #{remark}, #{delFlag}
)
</insert>
<update id="updateAmsReturnOrder" parameterType="AmsReturnOrder">
update ams_return_order
set receive_warehouse_id = #{receiveWarehouseId},
receive_warehouse_code = #{receiveWarehouseCode},
receive_warehouse_name = #{receiveWarehouseName},
update_by = #{updateBy},
update_time = #{updateTime},
remark = #{remark}
where order_id = #{orderId} and del_flag = '0' and order_status = 'DRAFT'
</update>
<update id="submitAmsReturnOrder" parameterType="AmsReturnOrder">
update ams_return_order
set order_status = #{orderStatus},
update_by = #{updateBy},
update_time = #{updateTime}
where order_id = #{orderId} and del_flag = '0' and order_status = 'DRAFT'
</update>
<update id="confirmAmsReturnOrder" parameterType="AmsReturnOrder">
update ams_return_order
set order_status = #{orderStatus},
confirm_user_id = #{confirmUserId},
confirm_user_name = #{confirmUserName},
confirm_time = #{confirmTime},
update_by = #{updateBy},
update_time = #{updateTime}
where order_id = #{orderId} and del_flag = '0' and order_status = 'PENDING_CONFIRM'
</update>
<update id="deleteAmsReturnOrderByOrderId" parameterType="Long">
update ams_return_order
set del_flag = '1'
where order_id = #{orderId} and del_flag = '0' and order_status = 'DRAFT'
</update>
<update id="deleteAmsReturnOrderByOrderIds" parameterType="String">
update ams_return_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="deleteAmsReturnOrderItemByOrderIds" parameterType="String">
update ams_return_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="deleteAmsReturnOrderItemByOrderId" parameterType="Long">
update ams_return_order_item
set del_flag = '1'
where order_id = #{orderId} and del_flag = '0'
</update>
<insert id="batchAmsReturnOrderItem">
insert into ams_return_order_item (
order_id, return_no, asset_id, asset_code, asset_name, category_id, category_code,
category_name, spec_model, brand, before_user_id, before_user_name, before_dept_id,
before_dept_name, after_warehouse_id, after_warehouse_code, after_warehouse_name,
after_location_id, after_location_code, after_location_name,
create_by, create_time, remark, del_flag
) values
<foreach item="item" collection="list" separator=",">
(
#{item.orderId}, #{item.returnNo}, #{item.assetId}, #{item.assetCode},
#{item.assetName}, #{item.categoryId}, #{item.categoryCode}, #{item.categoryName},
#{item.specModel}, #{item.brand}, #{item.beforeUserId}, #{item.beforeUserName},
#{item.beforeDeptId}, #{item.beforeDeptName}, #{item.afterWarehouseId},
#{item.afterWarehouseCode}, #{item.afterWarehouseName}, #{item.afterLocationId},
#{item.afterLocationCode}, #{item.afterLocationName}, #{item.createBy},
#{item.createTime}, #{item.remark}, #{item.delFlag}
)
</foreach>
</insert>
</mapper>

@ -0,0 +1,229 @@
<!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-return-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 name="receiveWarehouseId" class="form-control" required
onchange="changeReceiveWarehouse()">
<option value="">请选择接收仓库</option>
<option th:each="warehouse : ${warehouseList}" th:value="${warehouse.warehouseId}"
th:text="${warehouse.warehouseCode + ' - ' + warehouse.warehouseName}"></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"></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="selectAssets()"><i class="fa fa-plus"> 选择资产</i></button>
<button type="button" class="btn btn-white btn-sm" onclick="removeSelectedAssets()"><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/return";
var locationList = [[${locationList}]];
$("#form-return-add").validate({ focusCleanup: true });
function submitHandler() {
if ($("#bootstrap-table").bootstrapTable("getData").length === 0) {
$.modal.alertWarning("请至少添加一条退库明细");
return;
}
if ($.validate.form()) {
$.operate.save(prefix + "/add", $("#form-return-add").serialize());
}
}
$(function() { initDetailTable([]); });
function initDetailTable(data) {
$.table.init({
data: data,
pagination: false,
showSearch: false,
showRefresh: false,
showToggle: false,
showColumns: false,
sidePagination: "client",
columns: [
{ checkbox: true },
{ field: "assetId", title: "资产", formatter: function(value, row, index) { return buildAssetCell(row, index); } },
{ field: "categoryName", title: "资产类别" },
{ field: "originWarehouseName", title: "领用前仓位", formatter: function(value, row) { return buildOriginText(row); } },
{ field: "beforeDeptName", title: "退库前部门" },
{ field: "beforeUserName", title: "退库前使用人" },
{ field: "afterLocationId", title: "退库位置", formatter: function(value, row, index) { return buildLocationSelect(value, index); } },
{ field: "remark", title: "明细备注", formatter: function(value, row, index) { return buildInput("amsReturnOrderItemList[" + index + "].remark", value, 500); } },
{ title: "操作", align: "center", formatter: function(value, row) {
return '<a class="btn btn-danger btn-xs" href="javascript:void(0)" onclick="removeAsset(\'' + row.assetId + '\')"><i class="fa fa-remove"></i>删除</a>';
}}
]
});
}
function selectAssets() {
syncDetailRows();
$.modal.openOptions({ title: "选择可退库资产", url: buildSelectorUrl(), width: "1200", height: "680", callBack: addSelectedAssets });
}
function addSelectedAssets(index, layero) {
var selectedAssets = layero.find("iframe")[0].contentWindow.getSelectedAssets();
if (!selectedAssets || selectedAssets.length === 0) {
$.modal.alertWarning("请至少选择一条资产记录");
return;
}
syncDetailRows();
var currentRows = $("#bootstrap-table").bootstrapTable("getData");
if (!isCompatibleOriginGroup(currentRows.concat(selectedAssets))) {
$.modal.alertWarning("一张退库单只能选择同一原领用仓库的资产,缺少领用来源的资产不能与可追溯资产混选");
return;
}
if (currentRows.length === 0 && isOriginEnabled(selectedAssets[0])) {
$("[name='receiveWarehouseId']").val(selectedAssets[0].originWarehouseId);
}
var existing = {};
$.each(currentRows, function(i, row) { existing[String(row.assetId)] = true; });
$.each(selectedAssets, function(i, asset) {
if (!existing[String(asset.assetId)]) {
$("#bootstrap-table").bootstrapTable("insertRow", { index: $("#bootstrap-table").bootstrapTable("getData").length, row: buildReturnItem(asset) });
existing[String(asset.assetId)] = true;
}
});
$.modal.close(index);
}
function buildReturnItem(asset) {
return {
assetId: asset.assetId, assetCode: asset.assetCode, assetName: asset.assetName,
categoryName: asset.categoryName, beforeDeptId: asset.useDeptId,
beforeDeptName: asset.useDeptName, beforeUserId: asset.useUserId,
beforeUserName: asset.useUserName, originWarehouseId: asset.originWarehouseId,
originWarehouseCode: asset.originWarehouseCode, originWarehouseName: asset.originWarehouseName,
originWarehouseEnabled: asset.originWarehouseEnabled, originLocationId: asset.originLocationId,
originLocationCode: asset.originLocationCode, originLocationName: asset.originLocationName,
originLocationEnabled: asset.originLocationEnabled,
afterLocationId: isOriginEnabled(asset)
&& String(asset.originWarehouseId) === String($("[name='receiveWarehouseId']").val())
? asset.originLocationId : "", remark: ""
};
}
function buildSelectorUrl() {
var url = prefix + "/selectAsset";
var rows = $("#bootstrap-table").bootstrapTable("getData");
if (rows.length === 0) { return url; }
if (isTraceable(rows[0])) {
return url + "?originWarehouseId=" + rows[0].originWarehouseId;
}
return url + "?missingOriginOnly=true";
}
function isCompatibleOriginGroup(rows) {
if (!rows || rows.length === 0) { return true; }
var traceable = isTraceable(rows[0]);
var warehouseId = traceable ? String(rows[0].originWarehouseId) : "";
for (var i = 1; i < rows.length; i++) {
if (isTraceable(rows[i]) !== traceable) { return false; }
if (traceable && String(rows[i].originWarehouseId) !== warehouseId) { return false; }
}
return true;
}
function isTraceable(row) {
return row.originWarehouseId != null && row.originLocationId != null;
}
function isOriginEnabled(row) {
return isTraceable(row) && row.originWarehouseEnabled === "Y" && row.originLocationEnabled === "Y";
}
function buildOriginText(row) {
if (!isTraceable(row)) { return "无可追溯领用前仓位"; }
var text = (row.originWarehouseName || "-") + " / " + (row.originLocationName || "-");
return text + (isOriginEnabled(row) ? "" : "(原仓位已停用)");
}
function changeReceiveWarehouse() {
syncDetailRows();
var rows = $("#bootstrap-table").bootstrapTable("getData");
$.each(rows, function(index, row) { row.afterLocationId = ""; });
$("#bootstrap-table").bootstrapTable("load", rows);
}
function removeSelectedAssets() {
var rows = $("#bootstrap-table").bootstrapTable("getSelections");
if (rows.length === 0) {
$.modal.alertWarning("请至少选择一条记录");
return;
}
syncDetailRows();
$("#bootstrap-table").bootstrapTable("remove", { field: "assetId", values: $.map(rows, function(row) { return row.assetId; }) });
}
function removeAsset(assetId) {
syncDetailRows();
$("#bootstrap-table").bootstrapTable("remove", { field: "assetId", values: [assetId] });
}
function syncDetailRows() {
var rows = $("#bootstrap-table").bootstrapTable("getData");
$.each(rows, function(index, row) {
var tr = $("#bootstrap-table tbody tr[data-index='" + index + "']");
row.afterLocationId = tr.find("[name$='.afterLocationId']").val() || "";
row.remark = tr.find("[name$='.remark']").val() || "";
});
}
function buildAssetCell(row, index) {
var hidden = $("<input>").attr({ type: "hidden", name: "amsReturnOrderItemList[" + index + "].assetId", value: row.assetId }).prop("outerHTML");
return hidden + $("<span>").text(row.assetCode + " - " + row.assetName).prop("outerHTML");
}
function buildLocationSelect(value, index) {
var warehouseId = $("[name='receiveWarehouseId']").val();
var select = $("<select>").addClass("form-control").attr({ name: "amsReturnOrderItemList[" + index + "].afterLocationId", required: true });
select.append($("<option>").val("").text("请选择退库位置"));
$.each(locationList, function(i, location) {
if (String(location.warehouseId) === String(warehouseId)) {
var option = $("<option>").val(location.locationId).text(location.locationCode + " - " + location.locationName);
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,244 @@
<!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-return-edit" th:object="${amsReturnOrder}">
<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="*{returnNo}" 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">申请人/部门:</label>
<div class="col-sm-8"><input th:value="${amsReturnOrder.applicantName + ' / ' + amsReturnOrder.applyDeptName}" class="form-control" type="text" readonly></div>
</div>
</div>
</div>
<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 name="receiveWarehouseId" th:field="*{receiveWarehouseId}" class="form-control" required
onchange="changeReceiveWarehouse()">
<option value="">请选择接收仓库</option>
<option th:each="warehouse : ${warehouseList}" th:value="${warehouse.warehouseId}"
th:text="${warehouse.warehouseCode + ' - ' + warehouse.warehouseName}"></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="selectAssets()"><i class="fa fa-plus"> 选择资产</i></button>
<button type="button" class="btn btn-white btn-sm" onclick="removeSelectedAssets()"><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/return";
var orderId = [[${amsReturnOrder.orderId}]];
var detailList = [[${amsReturnOrder.amsReturnOrderItemList}]];
var locationList = [[${locationList}]];
$("#form-return-edit").validate({ focusCleanup: true });
function submitHandler() {
if ($("#bootstrap-table").bootstrapTable("getData").length === 0) {
$.modal.alertWarning("请至少保留一条退库明细");
return;
}
if ($.validate.form()) {
$.operate.save(prefix + "/edit", $("#form-return-edit").serialize());
}
}
$(function() { initDetailTable(detailList); });
function initDetailTable(data) {
$.table.init({
data: data,
pagination: false,
showSearch: false,
showRefresh: false,
showToggle: false,
showColumns: false,
sidePagination: "client",
columns: [
{ checkbox: true },
{ field: "assetId", title: "资产", formatter: function(value, row, index) { return buildAssetCell(row, index); } },
{ field: "categoryName", title: "资产类别" },
{ field: "originWarehouseName", title: "领用前仓位", formatter: function(value, row) { return buildOriginText(row); } },
{ field: "beforeDeptName", title: "退库前部门" },
{ field: "beforeUserName", title: "退库前使用人" },
{ field: "afterLocationId", title: "退库位置", formatter: function(value, row, index) { return buildLocationSelect(value, index); } },
{ field: "remark", title: "明细备注", formatter: function(value, row, index) { return buildInput("amsReturnOrderItemList[" + index + "].remark", value, 500); } },
{ title: "操作", align: "center", formatter: function(value, row) {
return '<a class="btn btn-danger btn-xs" href="javascript:void(0)" onclick="removeAsset(\'' + row.assetId + '\')"><i class="fa fa-remove"></i>删除</a>';
}}
]
});
}
function selectAssets() {
syncDetailRows();
$.modal.openOptions({ title: "选择可退库资产", url: buildSelectorUrl(), width: "1200", height: "680", callBack: addSelectedAssets });
}
function addSelectedAssets(index, layero) {
var selectedAssets = layero.find("iframe")[0].contentWindow.getSelectedAssets();
if (!selectedAssets || selectedAssets.length === 0) {
$.modal.alertWarning("请至少选择一条资产记录");
return;
}
syncDetailRows();
var currentRows = $("#bootstrap-table").bootstrapTable("getData");
if (!isCompatibleOriginGroup(currentRows.concat(selectedAssets))) {
$.modal.alertWarning("一张退库单只能选择同一原领用仓库的资产,缺少领用来源的资产不能与可追溯资产混选");
return;
}
if (currentRows.length === 0 && isOriginEnabled(selectedAssets[0])) {
$("[name='receiveWarehouseId']").val(selectedAssets[0].originWarehouseId);
}
var existing = {};
$.each(currentRows, function(i, row) { existing[String(row.assetId)] = true; });
$.each(selectedAssets, function(i, asset) {
if (!existing[String(asset.assetId)]) {
$("#bootstrap-table").bootstrapTable("insertRow", { index: $("#bootstrap-table").bootstrapTable("getData").length, row: buildReturnItem(asset) });
existing[String(asset.assetId)] = true;
}
});
$.modal.close(index);
}
function buildReturnItem(asset) {
return {
assetId: asset.assetId, assetCode: asset.assetCode, assetName: asset.assetName,
categoryName: asset.categoryName, beforeDeptId: asset.useDeptId,
beforeDeptName: asset.useDeptName, beforeUserId: asset.useUserId,
beforeUserName: asset.useUserName, originWarehouseId: asset.originWarehouseId,
originWarehouseCode: asset.originWarehouseCode, originWarehouseName: asset.originWarehouseName,
originWarehouseEnabled: asset.originWarehouseEnabled, originLocationId: asset.originLocationId,
originLocationCode: asset.originLocationCode, originLocationName: asset.originLocationName,
originLocationEnabled: asset.originLocationEnabled,
afterLocationId: isOriginEnabled(asset)
&& String(asset.originWarehouseId) === String($("[name='receiveWarehouseId']").val())
? asset.originLocationId : "", remark: ""
};
}
function buildSelectorUrl() {
var url = prefix + "/selectAsset?orderId=" + orderId;
var rows = $("#bootstrap-table").bootstrapTable("getData");
if (rows.length === 0) { return url; }
if (isTraceable(rows[0])) {
return url + "&originWarehouseId=" + rows[0].originWarehouseId;
}
return url + "&missingOriginOnly=true";
}
function isCompatibleOriginGroup(rows) {
if (!rows || rows.length === 0) { return true; }
var traceable = isTraceable(rows[0]);
var warehouseId = traceable ? String(rows[0].originWarehouseId) : "";
for (var i = 1; i < rows.length; i++) {
if (isTraceable(rows[i]) !== traceable) { return false; }
if (traceable && String(rows[i].originWarehouseId) !== warehouseId) { return false; }
}
return true;
}
function isTraceable(row) {
return row.originWarehouseId != null && row.originLocationId != null;
}
function isOriginEnabled(row) {
return isTraceable(row) && row.originWarehouseEnabled === "Y" && row.originLocationEnabled === "Y";
}
function buildOriginText(row) {
if (!isTraceable(row)) { return "无可追溯领用前仓位"; }
var text = (row.originWarehouseName || "-") + " / " + (row.originLocationName || "-");
return text + (isOriginEnabled(row) ? "" : "(原仓位已停用)");
}
function changeReceiveWarehouse() {
syncDetailRows();
var rows = $("#bootstrap-table").bootstrapTable("getData");
$.each(rows, function(index, row) { row.afterLocationId = ""; });
$("#bootstrap-table").bootstrapTable("load", rows);
}
function removeSelectedAssets() {
var rows = $("#bootstrap-table").bootstrapTable("getSelections");
if (rows.length === 0) {
$.modal.alertWarning("请至少选择一条记录");
return;
}
syncDetailRows();
$("#bootstrap-table").bootstrapTable("remove", { field: "assetId", values: $.map(rows, function(row) { return row.assetId; }) });
}
function removeAsset(assetId) {
syncDetailRows();
$("#bootstrap-table").bootstrapTable("remove", { field: "assetId", values: [assetId] });
}
function syncDetailRows() {
var rows = $("#bootstrap-table").bootstrapTable("getData");
$.each(rows, function(index, row) {
var tr = $("#bootstrap-table tbody tr[data-index='" + index + "']");
row.afterLocationId = tr.find("[name$='.afterLocationId']").val() || "";
row.remark = tr.find("[name$='.remark']").val() || "";
});
}
function buildAssetCell(row, index) {
var hidden = $("<input>").attr({ type: "hidden", name: "amsReturnOrderItemList[" + index + "].assetId", value: row.assetId }).prop("outerHTML");
return hidden + $("<span>").text(row.assetCode + " - " + row.assetName).prop("outerHTML");
}
function buildLocationSelect(value, index) {
var warehouseId = $("[name='receiveWarehouseId']").val();
var select = $("<select>").addClass("form-control").attr({ name: "amsReturnOrderItemList[" + index + "].afterLocationId", required: true });
select.append($("<option>").val("").text("请选择退库位置"));
$.each(locationList, function(i, location) {
if (String(location.warehouseId) === String(warehouseId)) {
var option = $("<option>").val(location.locationId).text(location.locationCode + " - " + location.locationName);
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,115 @@
<!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="returnNo"/></li>
<li><label>申请人:</label><input type="text" name="applicantName"/></li>
<li>
<label>申请部门:</label>
<select name="applyDeptId">
<option value="">所有</option>
<option th:each="dept : ${deptList}" th:value="${dept.deptId}" th:text="${dept.deptName}"></option>
</select>
</li>
<li>
<label>接收仓库:</label>
<select name="receiveWarehouseId">
<option value="">所有</option>
<option th:each="warehouse : ${warehouseList}" th:value="${warehouse.warehouseId}"
th:text="${warehouse.warehouseCode + ' - ' + warehouse.warehouseName}"></option>
</select>
</li>
<li class="select-time">
<label>确认时间:</label>
<input type="text" class="time-input" id="startTime" placeholder="开始时间" name="params[beginConfirmTime]"/>
<span>-</span>
<input type="text" class="time-input" id="endTime" placeholder="结束时间" name="params[endConfirmTime]"/>
</li>
<li>
<label>单据状态:</label>
<select name="orderStatus" th:with="type=${@dict.getType('ams_return_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="params[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:return:add"><i class="fa fa-plus"></i> 添加</a>
<a class="btn btn-warning" onclick="$.table.exportExcel()" shiro:hasPermission="asset:return: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:return:edit')}]];
var removeFlag = [[${@permission.hasPermi('asset:return:remove')}]];
var submitFlag = [[${@permission.hasPermi('asset:return:submit')}]];
var confirmFlag = [[${@permission.hasPermi('asset:return:confirm')}]];
var orderStatusDatas = [[${@dict.getType('ams_return_status')}]];
var prefix = ctx + "asset/return";
$(function() {
$.table.init({
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: "returnNo", title: "退库单号" },
{ field: "applicantName", title: "申请人" },
{ field: "applyDeptName", title: "申请部门" },
{ field: "receiveWarehouseName", title: "接收仓库" },
{ field: "confirmUserName", title: "确认人" },
{ field: "confirmTime", title: "确认时间" },
{ field: "orderStatus", title: "单据状态", formatter: function(value) { return $.table.selectDictLabel(orderStatusDatas, value); } },
{ 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 ' + submitFlag + '" href="javascript:void(0)" onclick="submitReturn(\'' + row.orderId + '\')"><i class="fa fa-upload"></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>');
} else if (row.orderStatus === "PENDING_CONFIRM") {
actions.push('<a class="btn btn-primary btn-xs ' + confirmFlag + '" href="javascript:void(0)" onclick="confirmReturn(\'' + row.orderId + '\')"><i class="fa fa-check"></i>确认退库</a>');
}
return actions.join("");
}}
]
});
});
function submitReturn(orderId) {
$.modal.confirm("提交后退库单不可再修改或删除,是否继续?", function() {
$.operate.post(prefix + "/submit/" + orderId, {});
});
}
function confirmReturn(orderId) {
$.modal.confirm("确认后资产将变为在库并清空使用归属,是否继续?", function() {
$.operate.post(prefix + "/confirm/" + orderId, {});
});
}
</script>
</body>
</html>

@ -0,0 +1,72 @@
<!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="useDeptId">
<option value="">所有</option>
<option th:each="dept : ${deptList}" th:value="${dept.deptId}" th:text="${dept.deptName}"></option>
</select>
</li>
<li><label>使用人:</label><input type="text" name="useUserName"></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="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/return";
var orderId = [[${orderId}]];
var originWarehouseId = [[${originWarehouseId}]];
var missingOriginOnly = [[${missingOriginOnly}]];
$(function() {
var url = prefix + "/availableAssetList";
var params = [];
if (orderId) { params.push("orderId=" + orderId); }
if (originWarehouseId) { params.push("originWarehouseId=" + originWarehouseId); }
if (missingOriginOnly) { params.push("missingOriginOnly=true"); }
if (params.length > 0) { url += "?" + params.join("&"); }
$.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: "originWarehouseName", title: "领用前仓库", formatter: function(value, row) { return originText(value, row.originWarehouseEnabled); } },
{ field: "originLocationName", title: "领用前位置", formatter: function(value, row) { return originText(value, row.originLocationEnabled); } },
{ field: "useDeptName", title: "当前使用部门" },
{ field: "useUserName", title: "当前使用人" }
]
});
});
function getSelectedAssets() { return $("#bootstrap-table").bootstrapTable("getSelections"); }
function originText(value, enabled) {
if (!value) { return "无可追溯来源"; }
return value + (enabled === "Y" ? "" : "(已停用)");
}
</script>
</body>
</html>

@ -0,0 +1,49 @@
<!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="${amsReturnOrder}">
<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="*{returnNo}"></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="*{applicantName}"></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="*{applyDeptName}"></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="*{receiveWarehouseName}"></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_return_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="*{confirmUserName}"></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="*{confirmTime == null ? '' : #dates.format(confirmTime, '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></tr></thead>
<tbody>
<tr th:each="item, stat : *{amsReturnOrderItemList}">
<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.beforeDeptName ?: '-') + ' / ' + (item.beforeUserName ?: '-')}"></td>
<td th:text="${(item.afterWarehouseName ?: '-') + ' / ' + (item.afterLocationName ?: '-')}"></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,376 @@
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.Arrays;
import java.util.List;
import com.ruoyi.asset.constant.AssetStatus;
import com.ruoyi.asset.constant.ReturnOrderStatus;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsAssetLocation;
import com.ruoyi.asset.domain.AmsReturnAssetCandidate;
import com.ruoyi.asset.domain.AmsReturnOrder;
import com.ruoyi.asset.domain.AmsReturnOrderItem;
import com.ruoyi.asset.domain.AmsWarehouse;
import com.ruoyi.asset.domain.AssetTransitionContext;
import com.ruoyi.asset.mapper.AmsAssetMapper;
import com.ruoyi.asset.mapper.AmsReturnOrderMapper;
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 AmsReturnOrderServiceImplTest
{
@Mock
private AmsReturnOrderMapper amsReturnOrderMapper;
@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 AmsReturnOrderServiceImpl service;
/** 新增退库草稿应生成单号,并使用台账及主数据回填快照。 */
@Test
void insertShouldGenerateCodeAndFillSnapshots()
{
AmsReturnOrder order = buildRequest();
stubApplicant();
stubInUseAsset();
stubTargetWarehouseLocation();
when(sysCodeRuleService.nextCode("RETURN_ORDER")).thenReturn("TK202606150001");
doAnswer(invocation -> {
AmsReturnOrder inserted = invocation.getArgument(0);
inserted.setOrderId(100L);
return 1;
}).when(amsReturnOrderMapper).insertAmsReturnOrder(any(AmsReturnOrder.class));
when(amsReturnOrderMapper.batchAmsReturnOrderItem(anyList()))
.thenAnswer(invocation -> ((List<?>) invocation.getArgument(0)).size());
assertEquals(1, service.insertAmsReturnOrder(order, 1L, "管理员", 103L));
AmsReturnOrderItem item = order.getAmsReturnOrderItemList().get(0);
assertEquals("TK202606150001", order.getReturnNo());
assertEquals(ReturnOrderStatus.DRAFT, order.getOrderStatus());
assertEquals("研发部门", order.getApplyDeptName());
assertEquals("二号仓", order.getReceiveWarehouseName());
assertEquals("测试部门", item.getBeforeDeptName());
assertEquals("测试用户", item.getBeforeUserName());
assertEquals("二号仓A区", item.getAfterLocationName());
assertEquals(100L, item.getOrderId());
assertNotNull(item.getCreateTime());
}
/** 退库位置必须属于主表所选接收仓库。 */
@Test
void insertShouldRejectLocationOutsideWarehouse()
{
AmsReturnOrder order = buildRequest();
stubApplicant();
stubInUseAsset();
stubTargetWarehouseLocation();
when(sysCodeRuleService.nextCode("RETURN_ORDER")).thenReturn("TK202606150001");
AmsAssetLocation location = new AmsAssetLocation();
location.setLocationId(20L);
location.setWarehouseId(99L);
location.setEnabled("Y");
when(amsAssetLocationService.selectAmsAssetLocationByLocationId(20L)).thenReturn(location);
ServiceException exception = assertThrows(ServiceException.class,
() -> service.insertAmsReturnOrder(order, 1L, "管理员", 103L));
assertTrue(exception.getMessage().contains("不属于接收仓库"));
verify(amsReturnOrderMapper, never()).insertAmsReturnOrder(any(AmsReturnOrder.class));
}
/** 被其他草稿或待确认退库单占用的资产不得再次加入退库单。 */
@Test
void insertShouldRejectAssetOccupiedByOtherActiveReturn()
{
AmsReturnOrder order = buildRequest();
stubApplicant();
stubInUseAsset();
stubTargetWarehouse();
when(sysCodeRuleService.nextCode("RETURN_ORDER")).thenReturn("TK202606150001");
when(amsReturnOrderMapper.countOtherActiveReturnOrderByAssetId(1L, null,
ReturnOrderStatus.DRAFT, ReturnOrderStatus.PENDING_CONFIRM)).thenReturn(1);
ServiceException exception = assertThrows(ServiceException.class,
() -> service.insertAmsReturnOrder(order, 1L, "管理员", 103L));
assertTrue(exception.getMessage().contains("其他有效退库单"));
verify(amsReturnOrderMapper, never()).insertAmsReturnOrder(any(AmsReturnOrder.class));
}
/** 同一退库单中的可追溯资产必须来自同一领用前仓库。 */
@Test
void insertShouldRejectAssetsFromDifferentOriginWarehouses()
{
AmsReturnOrder order = buildRequestWithAssets(1L, 2L);
stubApplicant();
stubInUseAsset(1L);
stubInUseAsset(2L);
stubTargetWarehouseLocation();
when(sysCodeRuleService.nextCode("RETURN_ORDER")).thenReturn("TK202606150001");
when(amsReturnOrderMapper.selectReturnAssetCandidateByAssetId(1L))
.thenReturn(buildCandidate(1L, 10L));
when(amsReturnOrderMapper.selectReturnAssetCandidateByAssetId(2L))
.thenReturn(buildCandidate(2L, 20L));
ServiceException exception = assertThrows(ServiceException.class,
() -> service.insertAmsReturnOrder(order, 1L, "管理员", 103L));
assertTrue(exception.getMessage().contains("原领用仓库"));
verify(amsReturnOrderMapper, never()).insertAmsReturnOrder(any(AmsReturnOrder.class));
}
/** 缺少领用来源的资产不能与可追溯资产混在同一退库单。 */
@Test
void insertShouldRejectMixedTraceableAndMissingOrigin()
{
AmsReturnOrder order = buildRequestWithAssets(1L, 2L);
stubApplicant();
stubInUseAsset(1L);
stubInUseAsset(2L);
stubTargetWarehouseLocation();
when(sysCodeRuleService.nextCode("RETURN_ORDER")).thenReturn("TK202606150001");
when(amsReturnOrderMapper.selectReturnAssetCandidateByAssetId(1L))
.thenReturn(buildCandidate(1L, 10L));
ServiceException exception = assertThrows(ServiceException.class,
() -> service.insertAmsReturnOrder(order, 1L, "管理员", 103L));
assertTrue(exception.getMessage().contains("来源类型不一致"));
verify(amsReturnOrderMapper, never()).insertAmsReturnOrder(any(AmsReturnOrder.class));
}
/** 草稿提交前校验资产仍在用且使用归属未变化。 */
@Test
void submitShouldMoveToPendingConfirm()
{
AmsReturnOrder order = buildPersistedOrder(ReturnOrderStatus.DRAFT);
when(amsReturnOrderMapper.selectAmsReturnOrderByOrderIdForUpdate(100L)).thenReturn(order);
stubInUseAsset();
stubTargetWarehouseLocation();
when(amsReturnOrderMapper.submitAmsReturnOrder(any(AmsReturnOrder.class))).thenReturn(1);
assertEquals(1, service.submitReturn(100L, "admin"));
assertEquals(ReturnOrderStatus.PENDING_CONFIRM, order.getOrderStatus());
verify(amsReturnOrderMapper).submitAmsReturnOrder(order);
}
/** 确认退库应委托公共流转服务,并记录确认信息。 */
@Test
void confirmShouldDelegateTransitionAndCompleteOrder()
{
AmsReturnOrder order = buildPersistedOrder(ReturnOrderStatus.PENDING_CONFIRM);
when(amsReturnOrderMapper.selectAmsReturnOrderByOrderIdForUpdate(100L)).thenReturn(order);
stubInUseAsset();
stubTargetWarehouseLocation();
when(amsReturnOrderMapper.confirmAmsReturnOrder(any(AmsReturnOrder.class))).thenReturn(1);
assertEquals(1, service.confirmReturn(100L, 1L, "管理员", "admin"));
ArgumentCaptor<AssetTransitionContext> contextCaptor = ArgumentCaptor.forClass(AssetTransitionContext.class);
verify(assetStatusTransitionService).confirmReturn(
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(ReturnOrderStatus.RETURNED, order.getOrderStatus());
assertEquals("管理员", order.getConfirmUserName());
assertNotNull(order.getConfirmTime());
}
/** 草稿保存后使用人发生变化时,提交必须阻断。 */
@Test
void submitShouldRejectStaleUseOwnership()
{
AmsReturnOrder order = buildPersistedOrder(ReturnOrderStatus.DRAFT);
order.getAmsReturnOrderItemList().get(0).setBeforeUserId(99L);
when(amsReturnOrderMapper.selectAmsReturnOrderByOrderIdForUpdate(100L)).thenReturn(order);
stubInUseAsset();
stubTargetWarehouse();
ServiceException exception = assertThrows(ServiceException.class,
() -> service.submitReturn(100L, "admin"));
assertTrue(exception.getMessage().contains("当前使用归属已变化"));
verify(amsReturnOrderMapper, never()).submitAmsReturnOrder(any(AmsReturnOrder.class));
}
/** 非草稿退库单不得删除。 */
@Test
void deleteShouldRejectNonDraftOrder()
{
AmsReturnOrder order = new AmsReturnOrder();
order.setOrderId(100L);
order.setOrderStatus(ReturnOrderStatus.PENDING_CONFIRM);
when(amsReturnOrderMapper.selectAmsReturnOrderByOrderIdForUpdate(100L)).thenReturn(order);
ServiceException exception = assertThrows(ServiceException.class,
() -> service.deleteAmsReturnOrderByOrderId(100L));
assertTrue(exception.getMessage().contains("仅草稿"));
verify(amsReturnOrderMapper, never()).deleteAmsReturnOrderByOrderId(100L);
}
private AmsReturnOrder buildRequest()
{
AmsReturnOrder order = new AmsReturnOrder();
order.setCreateBy("admin");
order.setReceiveWarehouseId(2L);
AmsReturnOrderItem item = new AmsReturnOrderItem();
item.setAssetId(1L);
item.setAfterLocationId(20L);
order.setAmsReturnOrderItemList(Collections.singletonList(item));
return order;
}
private AmsReturnOrder buildRequestWithAssets(Long... assetIds)
{
AmsReturnOrder order = new AmsReturnOrder();
order.setCreateBy("admin");
order.setReceiveWarehouseId(2L);
order.setAmsReturnOrderItemList(Arrays.stream(assetIds).map(assetId -> {
AmsReturnOrderItem item = new AmsReturnOrderItem();
item.setAssetId(assetId);
item.setAfterLocationId(20L);
return item;
}).toList());
return order;
}
private AmsReturnOrder buildPersistedOrder(String status)
{
AmsReturnOrder order = buildRequest();
order.setOrderId(100L);
order.setReturnNo("TK202606150001");
order.setOrderStatus(status);
AmsReturnOrderItem item = order.getAmsReturnOrderItemList().get(0);
item.setItemId(101L);
item.setBeforeDeptId(105L);
item.setBeforeUserId(2L);
item.setAfterWarehouseId(2L);
return order;
}
private void stubApplicant()
{
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);
}
private void stubInUseAsset()
{
stubInUseAsset(1L);
}
private void stubInUseAsset(Long assetId)
{
AmsAsset asset = new AmsAsset();
asset.setAssetId(assetId);
asset.setAssetCode("ASSET-" + assetId);
asset.setAssetName("测试资产");
asset.setCategoryId(3L);
asset.setCategoryCode("CAT-003");
asset.setCategoryName("测试类别");
asset.setAssetStatus(AssetStatus.IN_USE);
asset.setUseDeptId(105L);
asset.setUseDeptName("测试部门");
asset.setUseUserId(2L);
asset.setUseUserName("测试用户");
when(amsAssetMapper.selectAmsAssetByAssetIdForUpdate(assetId)).thenReturn(asset);
}
private AmsReturnAssetCandidate buildCandidate(Long warehouseId, Long locationId)
{
AmsReturnAssetCandidate candidate = new AmsReturnAssetCandidate();
candidate.setOriginWarehouseId(warehouseId);
candidate.setOriginWarehouseName("原仓库" + warehouseId);
candidate.setOriginWarehouseEnabled("Y");
candidate.setOriginLocationId(locationId);
candidate.setOriginLocationName("原位置" + locationId);
candidate.setOriginLocationEnabled("Y");
return candidate;
}
private void stubTargetWarehouseLocation()
{
stubTargetWarehouse();
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 stubTargetWarehouse()
{
AmsWarehouse warehouse = new AmsWarehouse();
warehouse.setWarehouseId(2L);
warehouse.setWarehouseCode("WH-002");
warehouse.setWarehouseName("二号仓");
warehouse.setEnabled("Y");
when(amsWarehouseService.selectAmsWarehouseByWarehouseId(2L)).thenReturn(warehouse);
}
}
Loading…
Cancel
Save