feat(asset): 新增入库明细报表功能

- 创建 AmsInboundDetailReport 实体类定义报表数据结构
- 实现 AmsInboundDetailReportController 提供报表查询和导出接口
- 开发 AmsInboundDetailReportMapper 和 XML 映射文件进行数据查询
- 实现 AmsInboundDetailReportService 业务逻辑层处理报表数据
- 添加单元测试验证报表服务的正确性
- 创建 inbound.html 模板实现报表页面展示和筛选功能
- 集成权限控制和 Excel 导出功能
main
yangk 4 days ago
parent 5fae393ac0
commit 5238492666

@ -0,0 +1,103 @@
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.AmsInboundDetailReport;
import com.ruoyi.asset.domain.AmsWarehouse;
import com.ruoyi.asset.service.IAmsAssetCategoryService;
import com.ruoyi.asset.service.IAmsInboundDetailReportService;
import com.ruoyi.asset.service.IAmsWarehouseService;
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/inbound")
public class AmsInboundDetailReportController extends BaseController
{
private static final String ENABLED_YES = "Y";
private String prefix = "asset/report";
@Autowired
private IAmsInboundDetailReportService amsInboundDetailReportService;
@Autowired
private IAmsAssetCategoryService amsAssetCategoryService;
@Autowired
private IAmsWarehouseService amsWarehouseService;
@RequiresPermissions("asset:report:inbound:view")
@GetMapping()
public String inbound(ModelMap mmap)
{
putReportOptions(mmap);
return prefix + "/inbound";
}
/**
*
*/
@RequiresPermissions("asset:report:inbound:list")
@PostMapping("/list")
@ResponseBody
public TableDataInfo list(AmsInboundDetailReport amsInboundDetailReport)
{
startPage();
List<AmsInboundDetailReport> list = amsInboundDetailReportService
.selectInboundDetailReportList(amsInboundDetailReport);
return getDataTable(list);
}
/**
*
*/
@RequiresPermissions("asset:report:inbound:export")
@Log(title = "入库明细报表", businessType = BusinessType.EXPORT)
@PostMapping("/export")
@ResponseBody
public AjaxResult export(AmsInboundDetailReport amsInboundDetailReport)
{
List<AmsInboundDetailReport> list = amsInboundDetailReportService
.selectInboundDetailReportList(amsInboundDetailReport);
ExcelUtil<AmsInboundDetailReport> util = new ExcelUtil<AmsInboundDetailReport>(AmsInboundDetailReport.class);
return util.exportExcel(list, "入库明细报表数据");
}
private void putReportOptions(ModelMap mmap)
{
mmap.put("warehouseList", selectEnabledWarehouseList());
mmap.put("categoryList", selectEnabledCategoryList());
}
private List<AmsWarehouse> selectEnabledWarehouseList()
{
AmsWarehouse warehouse = new AmsWarehouse();
warehouse.setEnabled(ENABLED_YES);
return amsWarehouseService.selectAmsWarehouseList(warehouse);
}
private List<AmsAssetCategory> selectEnabledCategoryList()
{
AmsAssetCategory category = new AmsAssetCategory();
category.setEnabled(ENABLED_YES);
return amsAssetCategoryService.selectAmsAssetCategoryList(category);
}
}

@ -0,0 +1,234 @@
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.annotation.Excel.ColumnType;
import com.ruoyi.common.core.domain.BaseEntity;
/**
*
*
* @author Yangk
*/
public class AmsInboundDetailReport extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 入库单号 */
@Excel(name = "入库单号")
private String inboundNo;
/** 入库时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Excel(name = "入库时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date inboundTime;
/** 入库仓库ID */
private Long warehouseId;
/** 入库仓库 */
@Excel(name = "入库仓库")
private String warehouseName;
/** 入库人 */
@Excel(name = "入库人")
private String inboundUserName;
/** 资产编码 */
@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;
/** 入库位置ID */
private Long locationId;
/** 入库位置 */
@Excel(name = "入库位置")
private String locationName;
/** 入库数量 */
@Excel(name = "入库数量", cellType = ColumnType.NUMERIC)
private Integer inboundQuantity;
public String getInboundNo()
{
return inboundNo;
}
public void setInboundNo(String inboundNo)
{
this.inboundNo = inboundNo;
}
public Date getInboundTime()
{
return inboundTime;
}
public void setInboundTime(Date inboundTime)
{
this.inboundTime = inboundTime;
}
public Long getWarehouseId()
{
return warehouseId;
}
public void setWarehouseId(Long warehouseId)
{
this.warehouseId = warehouseId;
}
public String getWarehouseName()
{
return warehouseName;
}
public void setWarehouseName(String warehouseName)
{
this.warehouseName = warehouseName;
}
public String getInboundUserName()
{
return inboundUserName;
}
public void setInboundUserName(String inboundUserName)
{
this.inboundUserName = inboundUserName;
}
public String getAssetCode()
{
return assetCode;
}
public void setAssetCode(String assetCode)
{
this.assetCode = assetCode;
}
public String getAssetName()
{
return assetName;
}
public void setAssetName(String assetName)
{
this.assetName = assetName;
}
public Long getCategoryId()
{
return categoryId;
}
public void setCategoryId(Long categoryId)
{
this.categoryId = categoryId;
}
public String getCategoryName()
{
return categoryName;
}
public void setCategoryName(String categoryName)
{
this.categoryName = categoryName;
}
public String getSpecModel()
{
return specModel;
}
public void setSpecModel(String specModel)
{
this.specModel = specModel;
}
public String getBrand()
{
return brand;
}
public void setBrand(String brand)
{
this.brand = brand;
}
public Long getLocationId()
{
return locationId;
}
public void setLocationId(Long locationId)
{
this.locationId = locationId;
}
public String getLocationName()
{
return locationName;
}
public void setLocationName(String locationName)
{
this.locationName = locationName;
}
public Integer getInboundQuantity()
{
return inboundQuantity;
}
public void setInboundQuantity(Integer inboundQuantity)
{
this.inboundQuantity = inboundQuantity;
}
@Override
public String toString()
{
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("inboundNo", getInboundNo())
.append("inboundTime", getInboundTime())
.append("warehouseId", getWarehouseId())
.append("warehouseName", getWarehouseName())
.append("inboundUserName", getInboundUserName())
.append("assetCode", getAssetCode())
.append("assetName", getAssetName())
.append("categoryId", getCategoryId())
.append("categoryName", getCategoryName())
.append("specModel", getSpecModel())
.append("brand", getBrand())
.append("locationId", getLocationId())
.append("locationName", getLocationName())
.append("inboundQuantity", getInboundQuantity())
.toString();
}
}

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

@ -0,0 +1,20 @@
package com.ruoyi.asset.service;
import java.util.List;
import com.ruoyi.asset.domain.AmsInboundDetailReport;
/**
* Service
*
* @author Yangk
*/
public interface IAmsInboundDetailReportService
{
/**
*
*
* @param amsInboundDetailReport
* @return
*/
public List<AmsInboundDetailReport> selectInboundDetailReportList(AmsInboundDetailReport amsInboundDetailReport);
}

@ -0,0 +1,33 @@
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.InboundOrderStatus;
import com.ruoyi.asset.domain.AmsInboundDetailReport;
import com.ruoyi.asset.mapper.AmsInboundDetailReportMapper;
import com.ruoyi.asset.service.IAmsInboundDetailReportService;
/**
* Service
*
* @author Yangk
*/
@Service
public class AmsInboundDetailReportServiceImpl implements IAmsInboundDetailReportService
{
@Autowired
private AmsInboundDetailReportMapper amsInboundDetailReportMapper;
/**
*
*/
@Override
public List<AmsInboundDetailReport> selectInboundDetailReportList(AmsInboundDetailReport amsInboundDetailReport)
{
AmsInboundDetailReport query = amsInboundDetailReport == null ? new AmsInboundDetailReport()
: amsInboundDetailReport;
// 报表只统计完成态数据,状态由服务端固定,避免前端参数把草稿单据混入报表。
return amsInboundDetailReportMapper.selectInboundDetailReportList(query, InboundOrderStatus.INBOUND_DONE);
}
}

@ -0,0 +1,74 @@
<?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.AmsInboundDetailReportMapper">
<resultMap type="AmsInboundDetailReport" id="AmsInboundDetailReportResult">
<result property="inboundNo" column="inbound_no" />
<result property="inboundTime" column="inbound_time" />
<result property="warehouseId" column="warehouse_id" />
<result property="warehouseName" column="warehouse_name" />
<result property="inboundUserName" column="inbound_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="locationId" column="location_id" />
<result property="locationName" column="location_name" />
<result property="inboundQuantity" column="inbound_quantity" />
</resultMap>
<sql id="inboundDetailReportWhere">
<where>
o.del_flag = '0'
and i.del_flag = '0'
and o.order_status = #{doneStatus}
<if test="report != null and report.inboundNo != null and report.inboundNo != ''">
and o.inbound_no like concat(#{report.inboundNo}, '%')
</if>
<if test="report != null and report.warehouseId != null">
and o.warehouse_id = #{report.warehouseId}
</if>
<if test="report != null and report.inboundUserName != null and report.inboundUserName != ''">
and o.inbound_user_name like concat('%', #{report.inboundUserName}, '%')
</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.params.beginInboundTime != null and report.params.beginInboundTime != ''">
and o.inbound_time &gt;= #{report.params.beginInboundTime}
</if>
<if test="report != null and report.params.endInboundTime != null and report.params.endInboundTime != ''">
and o.inbound_time &lt; date_add(#{report.params.endInboundTime}, interval 1 day)
</if>
</where>
</sql>
<select id="selectInboundDetailReportList" resultMap="AmsInboundDetailReportResult">
select o.inbound_no,
o.inbound_time,
o.warehouse_id,
o.warehouse_name,
o.inbound_user_name,
i.asset_code,
i.asset_name,
i.category_id,
i.category_name,
i.spec_model,
i.brand,
i.location_id,
i.location_name,
i.inbound_quantity
from ams_inbound_order o
inner join ams_inbound_order_item i
on i.order_id = o.order_id
<include refid="inboundDetailReportWhere"/>
order by o.inbound_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="inboundNo"/>
</li>
<li class="select-time">
<label>入库时间:</label>
<input type="text" class="time-input" id="startTime" placeholder="开始时间"
name="params[beginInboundTime]"/>
<span>-</span>
<input type="text" class="time-input" id="endTime" placeholder="结束时间"
name="params[endInboundTime]"/>
</li>
<li>
<label>入库仓库:</label>
<select name="warehouseId" class="select2-control" style="width: 170px">
<option value="">所有</option>
<option th:each="warehouse : ${warehouseList}"
th:value="${warehouse.warehouseId}"
th:text="${warehouse.warehouseCode + ' - ' + warehouse.warehouseName}"></option>
</select>
</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="assetCode"/>
</li>
<li>
<label>入库人:</label>
<input type="text" name="inboundUserName"/>
</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:inbound:export">
<i class="fa fa-download"></i> 导出
</a>
</div>
<div class="col-sm-12 select-table table-striped">
<table id="bootstrap-table"></table>
</div>
</div>
</div>
<th:block th:include="include :: footer" />
<th:block th:include="include :: datetimepicker-js" />
<th:block th:include="include :: select2-js" />
<script th:inline="javascript">
var prefix = ctx + "asset/report/inbound";
$(function() {
$(".select2-control").select2({
placeholder: "请选择",
allowClear: true
});
var options = {
url: prefix + "/list",
exportUrl: prefix + "/export",
modalName: "入库明细报表",
columns: [{
field: 'inboundNo',
title: '入库单号'
},
{
field: 'inboundTime',
title: '入库时间'
},
{
field: 'warehouseName',
title: '入库仓库'
},
{
field: 'inboundUserName',
title: '入库人'
},
{
field: 'assetCode',
title: '资产编码'
},
{
field: 'assetName',
title: '资产名称'
},
{
field: 'categoryName',
title: '资产类别'
},
{
field: 'specModel',
title: '规格型号'
},
{
field: 'brand',
title: '品牌'
},
{
field: 'locationName',
title: '入库位置'
},
{
field: 'inboundQuantity',
title: '入库数量',
align: 'right'
}]
};
$.table.init(options);
});
function resetReportSearch() {
$.form.reset();
$(".select2-control").val("").trigger("change");
$.table.search();
}
</script>
</body>
</html>

@ -0,0 +1,75 @@
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.InboundOrderStatus;
import com.ruoyi.asset.domain.AmsInboundDetailReport;
import com.ruoyi.asset.mapper.AmsInboundDetailReportMapper;
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 AmsInboundDetailReportServiceImplTest
{
@Mock
private AmsInboundDetailReportMapper amsInboundDetailReportMapper;
@InjectMocks
private AmsInboundDetailReportServiceImpl service;
/** 报表只能看已完成入库,防止草稿单据影响正式统计口径 */
@Test
void selectInboundDetailReportListShouldAlwaysUseDoneStatus()
{
AmsInboundDetailReport query = new AmsInboundDetailReport();
query.setWarehouseId(1L);
when(amsInboundDetailReportMapper.selectInboundDetailReportList(query, InboundOrderStatus.INBOUND_DONE))
.thenReturn(List.of());
service.selectInboundDetailReportList(query);
ArgumentCaptor<AmsInboundDetailReport> reportCaptor = ArgumentCaptor.forClass(AmsInboundDetailReport.class);
verify(amsInboundDetailReportMapper).selectInboundDetailReportList(reportCaptor.capture(),
eq(InboundOrderStatus.INBOUND_DONE));
assertEquals(1L, reportCaptor.getValue().getWarehouseId());
}
@Test
void selectInboundDetailReportListShouldHandleNullQuery()
{
when(amsInboundDetailReportMapper.selectInboundDetailReportList(any(AmsInboundDetailReport.class),
eq(InboundOrderStatus.INBOUND_DONE))).thenReturn(List.of());
service.selectInboundDetailReportList(null);
verify(amsInboundDetailReportMapper).selectInboundDetailReportList(any(AmsInboundDetailReport.class),
eq(InboundOrderStatus.INBOUND_DONE));
}
@Test
void selectInboundDetailReportListShouldReturnMapperRows()
{
AmsInboundDetailReport row = new AmsInboundDetailReport();
row.setInboundNo("RK202606150001");
row.setAssetCode("ASSET-001");
row.setInboundQuantity(1);
when(amsInboundDetailReportMapper.selectInboundDetailReportList(any(AmsInboundDetailReport.class),
eq(InboundOrderStatus.INBOUND_DONE))).thenReturn(List.of(row));
List<AmsInboundDetailReport> result = service.selectInboundDetailReportList(new AmsInboundDetailReport());
assertEquals(1, result.size());
assertEquals("RK202606150001", result.get(0).getInboundNo());
assertEquals("ASSET-001", result.get(0).getAssetCode());
assertEquals(1, result.get(0).getInboundQuantity());
}
}
Loading…
Cancel
Save