feat(asset): 添加资产报废明细报表功能

- 新增 AmsDisposalDetailReport 实体类定义报表数据结构
- 创建 AmsDisposalDetailReportController 提供报表查询和导出接口
- 实现 AmsDisposalDetailReportService 业务逻辑处理
- 配置 AmsDisposalDetailReportMapper 数据访问层
- 设计 disposal.html 前端页面支持报表筛选和展示
- 添加单元测试确保报表查询逻辑正确性
- 集成权限控制和 Excel 导出功能
main
yangk 2 days ago
parent 6eed283f72
commit 677c591024

@ -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.AmsDisposalDetailReport;
import com.ruoyi.asset.service.IAmsAssetCategoryService;
import com.ruoyi.asset.service.IAmsDisposalDetailReportService;
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/disposal")
public class AmsDisposalDetailReportController extends BaseController
{
private static final String ENABLED_YES = "Y";
private String prefix = "asset/report";
@Autowired
private IAmsDisposalDetailReportService amsDisposalDetailReportService;
@Autowired
private IAmsAssetCategoryService amsAssetCategoryService;
@RequiresPermissions("asset:report:disposal:view")
@GetMapping()
public String disposal(ModelMap mmap)
{
mmap.put("categoryList", selectEnabledCategoryList());
return prefix + "/disposal";
}
/**
*
*/
@RequiresPermissions("asset:report:disposal:list")
@PostMapping("/list")
@ResponseBody
public TableDataInfo list(AmsDisposalDetailReport amsDisposalDetailReport)
{
startPage();
List<AmsDisposalDetailReport> list = amsDisposalDetailReportService
.selectDisposalDetailReportList(amsDisposalDetailReport);
return getDataTable(list);
}
/**
*
*/
@RequiresPermissions("asset:report:disposal:export")
@Log(title = "报废明细报表", businessType = BusinessType.EXPORT)
@PostMapping("/export")
@ResponseBody
public AjaxResult export(AmsDisposalDetailReport amsDisposalDetailReport)
{
List<AmsDisposalDetailReport> list = amsDisposalDetailReportService
.selectDisposalDetailReportList(amsDisposalDetailReport);
ExcelUtil<AmsDisposalDetailReport> util = new ExcelUtil<AmsDisposalDetailReport>(
AmsDisposalDetailReport.class);
return util.exportExcel(list, "报废明细报表数据");
}
private List<AmsAssetCategory> selectEnabledCategoryList()
{
AmsAssetCategory category = new AmsAssetCategory();
category.setEnabled(ENABLED_YES);
return amsAssetCategoryService.selectAmsAssetCategoryList(category);
}
}

@ -0,0 +1,235 @@
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 AmsDisposalDetailReport extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 报废单号 */
@Excel(name = "报废单号")
private String disposalNo;
/** 报废日期 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Excel(name = "报废日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date disposalTime;
/** 申请人 */
@Excel(name = "申请人")
private String applicantName;
/** 申请部门 */
@Excel(name = "申请部门")
private String applyDeptName;
/** 处置方式 */
@Excel(name = "处置方式")
private String disposalMethod;
/** 确认人 */
@Excel(name = "确认人")
private String confirmUserName;
/** 资产编码 */
@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 disposalReason;
/** 处置备注 */
@Excel(name = "处置备注")
private String disposalRemark;
public void setDisposalNo(String disposalNo)
{
this.disposalNo = disposalNo;
}
public String getDisposalNo()
{
return disposalNo;
}
public void setDisposalTime(Date disposalTime)
{
this.disposalTime = disposalTime;
}
public Date getDisposalTime()
{
return disposalTime;
}
public void setApplicantName(String applicantName)
{
this.applicantName = applicantName;
}
public String getApplicantName()
{
return applicantName;
}
public void setApplyDeptName(String applyDeptName)
{
this.applyDeptName = applyDeptName;
}
public String getApplyDeptName()
{
return applyDeptName;
}
public void setDisposalMethod(String disposalMethod)
{
this.disposalMethod = disposalMethod;
}
public String getDisposalMethod()
{
return disposalMethod;
}
public void setConfirmUserName(String confirmUserName)
{
this.confirmUserName = confirmUserName;
}
public String getConfirmUserName()
{
return confirmUserName;
}
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 setDisposalReason(String disposalReason)
{
this.disposalReason = disposalReason;
}
public String getDisposalReason()
{
return disposalReason;
}
public void setDisposalRemark(String disposalRemark)
{
this.disposalRemark = disposalRemark;
}
public String getDisposalRemark()
{
return disposalRemark;
}
@Override
public String toString()
{
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("disposalNo", getDisposalNo())
.append("disposalTime", getDisposalTime())
.append("applicantName", getApplicantName())
.append("applyDeptName", getApplyDeptName())
.append("disposalMethod", getDisposalMethod())
.append("confirmUserName", getConfirmUserName())
.append("assetCode", getAssetCode())
.append("assetName", getAssetName())
.append("categoryId", getCategoryId())
.append("categoryName", getCategoryName())
.append("specModel", getSpecModel())
.append("brand", getBrand())
.append("disposalReason", getDisposalReason())
.append("disposalRemark", getDisposalRemark())
.toString();
}
}

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

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

@ -0,0 +1,35 @@
package com.ruoyi.asset.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.asset.constant.DisposalOrderStatus;
import com.ruoyi.asset.domain.AmsDisposalDetailReport;
import com.ruoyi.asset.mapper.AmsDisposalDetailReportMapper;
import com.ruoyi.asset.service.IAmsDisposalDetailReportService;
/**
* Service
*
* @author Yangk
*/
@Service
public class AmsDisposalDetailReportServiceImpl implements IAmsDisposalDetailReportService
{
@Autowired
private AmsDisposalDetailReportMapper amsDisposalDetailReportMapper;
/**
*
*/
@Override
public List<AmsDisposalDetailReport> selectDisposalDetailReportList(
AmsDisposalDetailReport amsDisposalDetailReport)
{
AmsDisposalDetailReport query = amsDisposalDetailReport == null ? new AmsDisposalDetailReport()
: amsDisposalDetailReport;
// 报表只统计已确认报废事实,状态口径固定在服务端,避免草稿、待确认或驳回单据进入历史报表。
return amsDisposalDetailReportMapper.selectDisposalDetailReportList(query,
DisposalOrderStatus.DISPOSED_DONE);
}
}

@ -0,0 +1,78 @@
<?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.AmsDisposalDetailReportMapper">
<resultMap type="AmsDisposalDetailReport" id="AmsDisposalDetailReportResult">
<result property="disposalNo" column="disposal_no"/>
<result property="disposalTime" column="disposal_time"/>
<result property="applicantName" column="applicant_name"/>
<result property="applyDeptName" column="apply_dept_name"/>
<result property="disposalMethod" column="disposal_method"/>
<result property="confirmUserName" column="confirm_user_name"/>
<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="disposalReason" column="disposal_reason"/>
<result property="disposalRemark" column="disposal_remark"/>
</resultMap>
<sql id="disposalDetailReportColumns">
o.disposal_no,
o.disposal_time,
o.applicant_name,
o.apply_dept_name,
o.disposal_method,
o.confirm_user_name,
i.asset_code,
i.asset_name,
i.category_id,
i.category_name,
i.spec_model,
i.brand,
i.disposal_reason,
i.disposal_remark
</sql>
<sql id="disposalDetailReportWhere">
<where>
o.del_flag = '0'
and i.del_flag = '0'
and o.order_status = #{doneStatus}
<if test="report != null and report.disposalNo != null and report.disposalNo != ''">
and o.disposal_no like concat(#{report.disposalNo}, '%')
</if>
<if test="report != null and report.disposalMethod != null and report.disposalMethod != ''">
and o.disposal_method like concat('%', #{report.disposalMethod}, '%')
</if>
<if test="report != null and report.categoryId != null">
and i.category_id = #{report.categoryId}
</if>
<if test="report != null and report.applyDeptName != null and report.applyDeptName != ''">
and o.apply_dept_name like concat('%', #{report.applyDeptName}, '%')
</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.params.beginDisposalTime != null and report.params.beginDisposalTime != ''">
and o.disposal_time &gt;= #{report.params.beginDisposalTime}
</if>
<if test="report != null and report.params.endDisposalTime != null and report.params.endDisposalTime != ''">
and o.disposal_time &lt; date_add(#{report.params.endDisposalTime}, interval 1 day)
</if>
</where>
</sql>
<select id="selectDisposalDetailReportList" resultMap="AmsDisposalDetailReportResult">
select
<include refid="disposalDetailReportColumns"/>
from ams_disposal_order o
inner join ams_disposal_order_item i on i.order_id = o.order_id
<include refid="disposalDetailReportWhere"/>
order by o.disposal_time desc, o.order_id desc, i.item_id asc
</select>
</mapper>

@ -0,0 +1,133 @@
<!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="disposalNo"/>
</li>
<li class="select-time">
<label>报废日期:</label>
<input type="text" class="time-input" id="startTime" placeholder="开始时间"
name="params[beginDisposalTime]"/>
<span>-</span>
<input type="text" class="time-input" id="endTime" placeholder="结束时间"
name="params[endDisposalTime]"/>
</li>
<li>
<label>处置方式:</label>
<input type="text" name="disposalMethod"/>
</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>
<label>申请部门:</label>
<input type="text" name="applyDeptName"/>
</li>
<li>
<label>资产编码:</label>
<input type="text" name="assetCode"/>
</li>
<li>
<a class="btn btn-primary btn-rounded btn-sm" onclick="$.table.search()">
<i class="fa fa-search"></i>&nbsp;搜索
</a>
<a class="btn btn-warning btn-rounded btn-sm" onclick="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:disposal: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 prefix = ctx + "asset/report/disposal";
$(function() {
$(".select2-control").select2({
placeholder: "请选择",
allowClear: true
});
var options = {
url: prefix + "/list",
exportUrl: prefix + "/export",
modalName: "报废明细报表",
columns: [{
field: 'disposalNo',
title: '报废单号'
},
{
field: 'disposalTime',
title: '报废日期'
},
{
field: 'applicantName',
title: '申请人'
},
{
field: 'applyDeptName',
title: '申请部门'
},
{
field: 'disposalMethod',
title: '处置方式'
},
{
field: 'assetCode',
title: '资产编码'
},
{
field: 'assetName',
title: '资产名称'
},
{
field: 'categoryName',
title: '资产类别'
},
{
field: 'disposalReason',
title: '报废原因'
}]
};
$.table.init(options);
});
function resetReportSearch() {
$.form.reset();
$(".select2-control").val("").trigger("change");
$.table.search();
}
</script>
</body>
</html>

@ -0,0 +1,79 @@
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.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.List;
import com.ruoyi.asset.constant.DisposalOrderStatus;
import com.ruoyi.asset.domain.AmsDisposalDetailReport;
import com.ruoyi.asset.mapper.AmsDisposalDetailReportMapper;
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 AmsDisposalDetailReportServiceImplTest
{
@Mock
private AmsDisposalDetailReportMapper amsDisposalDetailReportMapper;
@InjectMocks
private AmsDisposalDetailReportServiceImpl service;
/** 报表只展示已确认报废事实,避免草稿、待确认、驳回明细污染历史统计口径。 */
@Test
void selectDisposalDetailReportListShouldAlwaysUseDisposedDoneStatus()
{
AmsDisposalDetailReport query = new AmsDisposalDetailReport();
query.setApplyDeptName("设备部");
query.setCategoryId(5L);
when(amsDisposalDetailReportMapper.selectDisposalDetailReportList(query,
DisposalOrderStatus.DISPOSED_DONE)).thenReturn(List.of());
service.selectDisposalDetailReportList(query);
ArgumentCaptor<AmsDisposalDetailReport> reportCaptor = ArgumentCaptor
.forClass(AmsDisposalDetailReport.class);
verify(amsDisposalDetailReportMapper).selectDisposalDetailReportList(reportCaptor.capture(),
eq(DisposalOrderStatus.DISPOSED_DONE));
assertEquals("设备部", reportCaptor.getValue().getApplyDeptName());
assertEquals(5L, reportCaptor.getValue().getCategoryId());
}
@Test
void selectDisposalDetailReportListShouldHandleNullQuery()
{
when(amsDisposalDetailReportMapper.selectDisposalDetailReportList(any(AmsDisposalDetailReport.class),
eq(DisposalOrderStatus.DISPOSED_DONE))).thenReturn(List.of());
service.selectDisposalDetailReportList(null);
verify(amsDisposalDetailReportMapper).selectDisposalDetailReportList(any(AmsDisposalDetailReport.class),
eq(DisposalOrderStatus.DISPOSED_DONE));
}
@Test
void selectDisposalDetailReportListShouldReturnMapperRows()
{
AmsDisposalDetailReport row = new AmsDisposalDetailReport();
row.setDisposalNo("BF202606180002");
row.setAssetCode("TEST-ASSET-009");
row.setDisposalReason("无法使用");
when(amsDisposalDetailReportMapper.selectDisposalDetailReportList(any(AmsDisposalDetailReport.class),
eq(DisposalOrderStatus.DISPOSED_DONE))).thenReturn(List.of(row));
List<AmsDisposalDetailReport> result = service
.selectDisposalDetailReportList(new AmsDisposalDetailReport());
assertEquals(1, result.size());
assertEquals("BF202606180002", result.get(0).getDisposalNo());
assertEquals("TEST-ASSET-009", result.get(0).getAssetCode());
assertEquals("无法使用", result.get(0).getDisposalReason());
}
}
Loading…
Cancel
Save