feat(asset): 新增借用归还明细报表功能

- 创建 AmsBorrowReturnDetailReport 实体类,定义报表所需字段
- 实现 AmsBorrowReturnDetailReportController 控制器,提供报表查询和导出接口
- 开发 AmsBorrowReturnDetailReportMapper 数据访问层,编写报表查询SQL
- 实现 AmsBorrowReturnDetailReportService 业务逻辑层,处理报表数据查询
- 添加报表页面模板 borrow.html,实现前端展示和筛选功能
- 编写单元测试验证报表查询逻辑的正确性
- 集成权限控制和Excel导出功能
main
yangk 3 days ago
parent 5238492666
commit 6eed283f72

@ -0,0 +1,86 @@
package com.ruoyi.asset.controller;
import java.util.List;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.ruoyi.asset.domain.AmsAssetCategory;
import com.ruoyi.asset.domain.AmsBorrowReturnDetailReport;
import com.ruoyi.asset.service.IAmsAssetCategoryService;
import com.ruoyi.asset.service.IAmsBorrowReturnDetailReportService;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
/**
* Controller
*
* @author Yangk
*/
@Controller
@RequestMapping("/asset/report/borrow")
public class AmsBorrowReturnDetailReportController extends BaseController
{
private static final String ENABLED_YES = "Y";
private String prefix = "asset/report";
@Autowired
private IAmsBorrowReturnDetailReportService amsBorrowReturnDetailReportService;
@Autowired
private IAmsAssetCategoryService amsAssetCategoryService;
@RequiresPermissions("asset:report:borrow:view")
@GetMapping()
public String borrow(ModelMap mmap)
{
mmap.put("categoryList", selectEnabledCategoryList());
return prefix + "/borrow";
}
/**
*
*/
@RequiresPermissions("asset:report:borrow:list")
@PostMapping("/list")
@ResponseBody
public TableDataInfo list(AmsBorrowReturnDetailReport amsBorrowReturnDetailReport)
{
startPage();
List<AmsBorrowReturnDetailReport> list = amsBorrowReturnDetailReportService
.selectBorrowReturnDetailReportList(amsBorrowReturnDetailReport);
return getDataTable(list);
}
/**
*
*/
@RequiresPermissions("asset:report:borrow:export")
@Log(title = "借用归还明细报表", businessType = BusinessType.EXPORT)
@PostMapping("/export")
@ResponseBody
public AjaxResult export(AmsBorrowReturnDetailReport amsBorrowReturnDetailReport)
{
List<AmsBorrowReturnDetailReport> list = amsBorrowReturnDetailReportService
.selectBorrowReturnDetailReportList(amsBorrowReturnDetailReport);
ExcelUtil<AmsBorrowReturnDetailReport> util = new ExcelUtil<AmsBorrowReturnDetailReport>(
AmsBorrowReturnDetailReport.class);
return util.exportExcel(list, "借用归还明细报表数据");
}
private List<AmsAssetCategory> selectEnabledCategoryList()
{
AmsAssetCategory category = new AmsAssetCategory();
category.setEnabled(ENABLED_YES);
return amsAssetCategoryService.selectAmsAssetCategoryList(category);
}
}

@ -0,0 +1,282 @@
package com.ruoyi.asset.domain;
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;
/**
*
*
* @author Yangk
*/
public class AmsBorrowReturnDetailReport extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 借用单号 */
@Excel(name = "借用单号")
private String borrowNo;
/** 借用确认时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Excel(name = "借用日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date confirmTime;
/** 借用人 */
@Excel(name = "借用人")
private String borrowUserName;
/** 借用部门 */
@Excel(name = "借用部门")
private String borrowDeptName;
/** 预计归还日期 */
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@Excel(name = "预计归还日期", width = 30, dateFormat = "yyyy-MM-dd")
private Date expectedReturnDate;
/** 实际归还日期 */
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@Excel(name = "实际归还日期", width = 30, dateFormat = "yyyy-MM-dd")
private Date actualReturnDate;
/** 资产编码 */
@Excel(name = "资产编码")
private String assetCode;
/** 资产名称 */
@Excel(name = "资产名称")
private String assetName;
/** 资产类别ID */
private Long categoryId;
/** 资产类别 */
@Excel(name = "资产类别")
private String categoryName;
/** 规格型号 */
@Excel(name = "规格型号")
private String specModel;
/** 品牌 */
@Excel(name = "品牌")
private String brand;
/** 借用前仓库 */
@Excel(name = "借用前仓库")
private String beforeWarehouseName;
/** 借用前位置 */
@Excel(name = "借用前位置")
private String beforeLocationName;
/** 归还仓库 */
@Excel(name = "归还仓库")
private String returnWarehouseName;
/** 归还位置 */
@Excel(name = "归还位置")
private String returnLocationName;
/** 归还状态 */
@Excel(name = "归还状态", dictType = "ams_borrow_status")
private String returnStatus;
public void setBorrowNo(String borrowNo)
{
this.borrowNo = borrowNo;
}
public String getBorrowNo()
{
return borrowNo;
}
public void setConfirmTime(Date confirmTime)
{
this.confirmTime = confirmTime;
}
public Date getConfirmTime()
{
return confirmTime;
}
public void setBorrowUserName(String borrowUserName)
{
this.borrowUserName = borrowUserName;
}
public String getBorrowUserName()
{
return borrowUserName;
}
public void setBorrowDeptName(String borrowDeptName)
{
this.borrowDeptName = borrowDeptName;
}
public String getBorrowDeptName()
{
return borrowDeptName;
}
public void setExpectedReturnDate(Date expectedReturnDate)
{
this.expectedReturnDate = expectedReturnDate;
}
public Date getExpectedReturnDate()
{
return expectedReturnDate;
}
public void setActualReturnDate(Date actualReturnDate)
{
this.actualReturnDate = actualReturnDate;
}
public Date getActualReturnDate()
{
return actualReturnDate;
}
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 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 setBeforeWarehouseName(String beforeWarehouseName)
{
this.beforeWarehouseName = beforeWarehouseName;
}
public String getBeforeWarehouseName()
{
return beforeWarehouseName;
}
public void setBeforeLocationName(String beforeLocationName)
{
this.beforeLocationName = beforeLocationName;
}
public String getBeforeLocationName()
{
return beforeLocationName;
}
public void setReturnWarehouseName(String returnWarehouseName)
{
this.returnWarehouseName = returnWarehouseName;
}
public String getReturnWarehouseName()
{
return returnWarehouseName;
}
public void setReturnLocationName(String returnLocationName)
{
this.returnLocationName = returnLocationName;
}
public String getReturnLocationName()
{
return returnLocationName;
}
public void setReturnStatus(String returnStatus)
{
this.returnStatus = returnStatus;
}
public String getReturnStatus()
{
return returnStatus;
}
@Override
public String toString()
{
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("borrowNo", getBorrowNo())
.append("confirmTime", getConfirmTime())
.append("borrowUserName", getBorrowUserName())
.append("borrowDeptName", getBorrowDeptName())
.append("expectedReturnDate", getExpectedReturnDate())
.append("actualReturnDate", getActualReturnDate())
.append("assetCode", getAssetCode())
.append("assetName", getAssetName())
.append("categoryId", getCategoryId())
.append("categoryName", getCategoryName())
.append("specModel", getSpecModel())
.append("brand", getBrand())
.append("beforeWarehouseName", getBeforeWarehouseName())
.append("beforeLocationName", getBeforeLocationName())
.append("returnWarehouseName", getReturnWarehouseName())
.append("returnLocationName", getReturnLocationName())
.append("returnStatus", getReturnStatus())
.toString();
}
}

@ -0,0 +1,24 @@
package com.ruoyi.asset.mapper;
import java.util.List;
import com.ruoyi.asset.domain.AmsBorrowReturnDetailReport;
import org.apache.ibatis.annotations.Param;
/**
* Mapper
*
* @author Yangk
*/
public interface AmsBorrowReturnDetailReportMapper
{
/**
*
*
* @param report
* @param effectiveStatuses
* @return
*/
public List<AmsBorrowReturnDetailReport> selectBorrowReturnDetailReportList(
@Param("report") AmsBorrowReturnDetailReport report,
@Param("effectiveStatuses") List<String> effectiveStatuses);
}

@ -0,0 +1,21 @@
package com.ruoyi.asset.service;
import java.util.List;
import com.ruoyi.asset.domain.AmsBorrowReturnDetailReport;
/**
* Service
*
* @author Yangk
*/
public interface IAmsBorrowReturnDetailReportService
{
/**
*
*
* @param amsBorrowReturnDetailReport
* @return
*/
public List<AmsBorrowReturnDetailReport> selectBorrowReturnDetailReportList(
AmsBorrowReturnDetailReport amsBorrowReturnDetailReport);
}

@ -0,0 +1,40 @@
package com.ruoyi.asset.service.impl;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.asset.constant.BorrowOrderStatus;
import com.ruoyi.asset.domain.AmsBorrowReturnDetailReport;
import com.ruoyi.asset.mapper.AmsBorrowReturnDetailReportMapper;
import com.ruoyi.asset.service.IAmsBorrowReturnDetailReportService;
/**
* Service
*
* @author Yangk
*/
@Service
public class AmsBorrowReturnDetailReportServiceImpl implements IAmsBorrowReturnDetailReportService
{
private static final List<String> EFFECTIVE_BORROW_STATUSES = Collections.unmodifiableList(Arrays.asList(
BorrowOrderStatus.BORROWING, BorrowOrderStatus.PENDING_RETURN_CONFIRM,
BorrowOrderStatus.BORROW_RETURNED));
@Autowired
private AmsBorrowReturnDetailReportMapper amsBorrowReturnDetailReportMapper;
/**
*
*/
@Override
public List<AmsBorrowReturnDetailReport> selectBorrowReturnDetailReportList(
AmsBorrowReturnDetailReport amsBorrowReturnDetailReport)
{
AmsBorrowReturnDetailReport query = amsBorrowReturnDetailReport == null ? new AmsBorrowReturnDetailReport()
: amsBorrowReturnDetailReport;
// 报表只统计已形成借用事实的单据,状态口径固定在服务端,避免前端把草稿或驳回单带入统计。
return amsBorrowReturnDetailReportMapper.selectBorrowReturnDetailReportList(query, EFFECTIVE_BORROW_STATUSES);
}
}

@ -0,0 +1,87 @@
<?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.AmsBorrowReturnDetailReportMapper">
<resultMap type="AmsBorrowReturnDetailReport" id="AmsBorrowReturnDetailReportResult">
<result property="borrowNo" column="borrow_no"/>
<result property="confirmTime" column="confirm_time"/>
<result property="borrowUserName" column="borrow_user_name"/>
<result property="borrowDeptName" column="borrow_dept_name"/>
<result property="expectedReturnDate" column="expected_return_date"/>
<result property="actualReturnDate" column="actual_return_date"/>
<result property="assetCode" column="asset_code"/>
<result property="assetName" column="asset_name"/>
<result property="categoryId" column="category_id"/>
<result property="categoryName" column="category_name"/>
<result property="specModel" column="spec_model"/>
<result property="brand" column="brand"/>
<result property="beforeWarehouseName" column="before_warehouse_name"/>
<result property="beforeLocationName" column="before_location_name"/>
<result property="returnWarehouseName" column="return_warehouse_name"/>
<result property="returnLocationName" column="return_location_name"/>
<result property="returnStatus" column="return_status"/>
</resultMap>
<sql id="borrowReturnDetailReportColumns">
o.borrow_no,
o.confirm_time,
o.borrow_user_name,
o.borrow_dept_name,
o.expected_return_date,
i.actual_return_date,
i.asset_code,
i.asset_name,
i.category_id,
i.category_name,
i.spec_model,
i.brand,
i.before_warehouse_name,
i.before_location_name,
i.return_warehouse_name,
i.return_location_name,
i.return_status
</sql>
<sql id="borrowReturnDetailReportWhere">
<where>
o.del_flag = '0'
and i.del_flag = '0'
and o.order_status in
<foreach collection="effectiveStatuses" item="status" open="(" separator="," close=")">
#{status}
</foreach>
<if test="report != null and report.borrowNo != null and report.borrowNo != ''">
and o.borrow_no like concat(#{report.borrowNo}, '%')
</if>
<if test="report != null and report.borrowUserName != null and report.borrowUserName != ''">
and o.borrow_user_name like concat('%', #{report.borrowUserName}, '%')
</if>
<if test="report != null and report.categoryId != null">
and i.category_id = #{report.categoryId}
</if>
<if test="report != null and report.assetCode != null and report.assetCode != ''">
and i.asset_code like concat(#{report.assetCode}, '%')
</if>
<if test="report != null and report.returnStatus != null and report.returnStatus != ''">
and i.return_status = #{report.returnStatus}
</if>
<if test="report != null and report.params.beginConfirmTime != null and report.params.beginConfirmTime != ''">
and o.confirm_time &gt;= #{report.params.beginConfirmTime}
</if>
<if test="report != null and report.params.endConfirmTime != null and report.params.endConfirmTime != ''">
and o.confirm_time &lt; date_add(#{report.params.endConfirmTime}, interval 1 day)
</if>
</where>
</sql>
<select id="selectBorrowReturnDetailReportList" resultMap="AmsBorrowReturnDetailReportResult">
select
<include refid="borrowReturnDetailReportColumns"/>
from ams_borrow_order o
inner join ams_borrow_order_item i on i.order_id = o.order_id
<include refid="borrowReturnDetailReportWhere"/>
order by o.confirm_time desc, o.order_id desc, i.item_id asc
</select>
</mapper>

@ -0,0 +1,147 @@
<!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" />
<th:block th:include="include :: select2-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="borrowNo"/>
</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="returnStatus" class="select2-control" style="width: 170px">
<option value="">所有</option>
<option th:each="dict : ${@dict.getType('ams_borrow_status')}"
th:if="${dict.dictValue == 'BORROWING' or dict.dictValue == 'PENDING_RETURN_CONFIRM' or dict.dictValue == 'BORROW_RETURNED'}"
th:value="${dict.dictValue}"
th:text="${dict.dictLabel}"></option>
</select>
</li>
<li>
<label>借用人:</label>
<input type="text" name="borrowUserName"/>
</li>
<li>
<label>资产编码:</label>
<input type="text" name="assetCode"/>
</li>
<li>
<label>资产类别:</label>
<select name="categoryId" class="select2-control" style="width: 170px">
<option value="">所有</option>
<option th:each="category : ${categoryList}"
th:value="${category.categoryId}"
th:text="${category.categoryName}"></option>
</select>
</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="resetReportSearch()">
<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-warning" onclick="$.table.exportExcel()" shiro:hasPermission="asset:report:borrow: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" />
<th:block th:include="include :: select2-js" />
<script th:inline="javascript">
var returnStatusDatas = [[${@dict.getType('ams_borrow_status')}]];
var prefix = ctx + "asset/report/borrow";
$(function() {
$(".select2-control").select2({
placeholder: "请选择",
allowClear: true
});
var options = {
url: prefix + "/list",
exportUrl: prefix + "/export",
modalName: "借用归还明细报表",
columns: [{
field: 'borrowNo',
title: '借用单号'
},
{
field: 'confirmTime',
title: '借用日期'
},
{
field: 'borrowUserName',
title: '借用人'
},
{
field: 'borrowDeptName',
title: '借用部门'
},
{
field: 'expectedReturnDate',
title: '预计归还日期'
},
{
field: 'actualReturnDate',
title: '实际归还日期'
},
{
field: 'assetCode',
title: '资产编码'
},
{
field: 'assetName',
title: '资产名称'
},
{
field: 'categoryName',
title: '资产类别'
},
{
field: 'returnStatus',
title: '归还状态',
formatter: function(value) {
return $.table.selectDictLabel(returnStatusDatas, value);
}
}]
};
$.table.init(options);
});
function resetReportSearch() {
$.form.reset();
$(".select2-control").val("").trigger("change");
$.table.search();
}
</script>
</body>
</html>

@ -0,0 +1,82 @@
package com.ruoyi.asset.service.impl;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.List;
import com.ruoyi.asset.constant.BorrowOrderStatus;
import com.ruoyi.asset.domain.AmsBorrowReturnDetailReport;
import com.ruoyi.asset.mapper.AmsBorrowReturnDetailReportMapper;
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 AmsBorrowReturnDetailReportServiceImplTest
{
@Mock
private AmsBorrowReturnDetailReportMapper amsBorrowReturnDetailReportMapper;
@InjectMocks
private AmsBorrowReturnDetailReportServiceImpl service;
/** 报表只展示已形成借用事实的状态,避免草稿、待确认、驳回单据污染统计口径。 */
@Test
void selectBorrowReturnDetailReportListShouldAlwaysUseEffectiveStatuses()
{
AmsBorrowReturnDetailReport query = new AmsBorrowReturnDetailReport();
query.setBorrowUserName("张三");
when(amsBorrowReturnDetailReportMapper.selectBorrowReturnDetailReportList(query, List.of(
BorrowOrderStatus.BORROWING, BorrowOrderStatus.PENDING_RETURN_CONFIRM,
BorrowOrderStatus.BORROW_RETURNED))).thenReturn(List.of());
service.selectBorrowReturnDetailReportList(query);
ArgumentCaptor<AmsBorrowReturnDetailReport> reportCaptor = ArgumentCaptor
.forClass(AmsBorrowReturnDetailReport.class);
@SuppressWarnings("unchecked")
ArgumentCaptor<List<String>> statusCaptor = ArgumentCaptor.forClass(List.class);
verify(amsBorrowReturnDetailReportMapper).selectBorrowReturnDetailReportList(reportCaptor.capture(),
statusCaptor.capture());
assertEquals("张三", reportCaptor.getValue().getBorrowUserName());
assertEquals(List.of(BorrowOrderStatus.BORROWING, BorrowOrderStatus.PENDING_RETURN_CONFIRM,
BorrowOrderStatus.BORROW_RETURNED), statusCaptor.getValue());
}
@Test
void selectBorrowReturnDetailReportListShouldHandleNullQuery()
{
when(amsBorrowReturnDetailReportMapper.selectBorrowReturnDetailReportList(
any(AmsBorrowReturnDetailReport.class), anyList())).thenReturn(List.of());
service.selectBorrowReturnDetailReportList(null);
verify(amsBorrowReturnDetailReportMapper).selectBorrowReturnDetailReportList(
any(AmsBorrowReturnDetailReport.class), anyList());
}
@Test
void selectBorrowReturnDetailReportListShouldReturnMapperRows()
{
AmsBorrowReturnDetailReport row = new AmsBorrowReturnDetailReport();
row.setBorrowNo("JY202606150001");
row.setAssetCode("ASSET-001");
row.setReturnStatus(BorrowOrderStatus.BORROWING);
when(amsBorrowReturnDetailReportMapper.selectBorrowReturnDetailReportList(
any(AmsBorrowReturnDetailReport.class), anyList())).thenReturn(List.of(row));
List<AmsBorrowReturnDetailReport> result = service
.selectBorrowReturnDetailReportList(new AmsBorrowReturnDetailReport());
assertEquals(1, result.size());
assertEquals("JY202606150001", result.get(0).getBorrowNo());
assertEquals("ASSET-001", result.get(0).getAssetCode());
assertEquals(BorrowOrderStatus.BORROWING, result.get(0).getReturnStatus());
}
}
Loading…
Cancel
Save