feat: add dense warehouse relocation jobs
This commit is contained in:
@@ -14,6 +14,7 @@ import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.in.job.domain.MydoJob;
|
||||
import com.ruoyi.in.job.service.IMydoJobService;
|
||||
import com.ruoyi.out.relocation.service.IRelocationJobService;
|
||||
import com.ruoyi.pda.service.PdaService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
@@ -37,6 +38,7 @@ public class ExternalApiController extends BaseController {
|
||||
@Autowired private MesClient mesClient;
|
||||
@Autowired private WcsClient wcsClient;
|
||||
@Autowired private IMydoJobService mydoJobService;
|
||||
@Autowired private IRelocationJobService relocationJobService;
|
||||
@Autowired private PdaService pdaService;
|
||||
|
||||
// ═══════════════ ERP ═══════════════
|
||||
@@ -353,11 +355,20 @@ public class ExternalApiController extends BaseController {
|
||||
job = mydoJobService.selectLatestOutMydoJobByTrayCodeAndWarehouseId(request.getTrayCode().trim(), request.getWarehouseId().trim());
|
||||
}
|
||||
if (job == null) {
|
||||
if (request != null && request.getJobId() != null && !request.getJobId().trim().isEmpty()) {
|
||||
AjaxResult relocationResult = relocationJobService.completeFromWcs(request.getJobId().trim());
|
||||
if (relocationResult != null && relocationResult.isSuccess()) {
|
||||
return wcsResult(relocationResult);
|
||||
}
|
||||
}
|
||||
return wcsError("未找到对应出库作业");
|
||||
}
|
||||
if (!isExpectedJobType(job, false)) {
|
||||
return wcsError("非法出库作业");
|
||||
}
|
||||
if (Long.valueOf(23L).equals(job.getInOrOut())) {
|
||||
return wcsResult(relocationJobService.completeFromWcs(job.getJobId()));
|
||||
}
|
||||
return wcsResult(mydoJobService.completeOutJobFromWcs(job.getJobId()));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.ruoyi.out.relocation.controller;
|
||||
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.out.relocation.domain.RelocationGenerateRequest;
|
||||
import com.ruoyi.out.relocation.service.IRelocationJobService;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 密集库移库作业接口。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/out/relocation")
|
||||
public class RelocationJobController extends BaseController
|
||||
{
|
||||
private final IRelocationJobService relocationJobService;
|
||||
|
||||
public RelocationJobController(IRelocationJobService relocationJobService)
|
||||
{
|
||||
this.relocationJobService = relocationJobService;
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('out:relocation:preview')")
|
||||
@PostMapping("/preview")
|
||||
public AjaxResult preview(@RequestBody RelocationGenerateRequest request)
|
||||
{
|
||||
return success(relocationJobService.preview(request));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('out:relocation:generate')")
|
||||
@PostMapping("/generate")
|
||||
public AjaxResult generate(@RequestBody RelocationGenerateRequest request)
|
||||
{
|
||||
return success(relocationJobService.generate(request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ruoyi.out.relocation.domain;
|
||||
|
||||
/**
|
||||
* 密集库指定移库请求。
|
||||
*/
|
||||
public class RelocationGenerateRequest
|
||||
{
|
||||
private String warehouseId;
|
||||
private Long theFloor;
|
||||
private String sourceGroupCoding;
|
||||
private String targetGroupCoding;
|
||||
private Integer trayCount;
|
||||
|
||||
public String getWarehouseId() { return warehouseId; }
|
||||
public void setWarehouseId(String warehouseId) { this.warehouseId = warehouseId; }
|
||||
public Long getTheFloor() { return theFloor; }
|
||||
public void setTheFloor(Long theFloor) { this.theFloor = theFloor; }
|
||||
public String getSourceGroupCoding() { return sourceGroupCoding; }
|
||||
public void setSourceGroupCoding(String sourceGroupCoding) { this.sourceGroupCoding = sourceGroupCoding; }
|
||||
public String getTargetGroupCoding() { return targetGroupCoding; }
|
||||
public void setTargetGroupCoding(String targetGroupCoding) { this.targetGroupCoding = targetGroupCoding; }
|
||||
public Integer getTrayCount() { return trayCount; }
|
||||
public void setTrayCount(Integer trayCount) { this.trayCount = trayCount; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ruoyi.out.relocation.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.ruoyi.out.dense.domain.DenseLanePosition;
|
||||
|
||||
/**
|
||||
* 指定移库外侧托盘预览。
|
||||
*/
|
||||
public class RelocationPreview
|
||||
{
|
||||
private List<DenseLanePosition> trays = new ArrayList<>();
|
||||
private BigDecimal totalQuantity = BigDecimal.ZERO;
|
||||
|
||||
public List<DenseLanePosition> getTrays() { return trays; }
|
||||
public void setTrays(List<DenseLanePosition> trays) { this.trays = trays; }
|
||||
public BigDecimal getTotalQuantity() { return totalQuantity; }
|
||||
public void setTotalQuantity(BigDecimal totalQuantity) { this.totalQuantity = totalQuantity; }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.ruoyi.out.relocation.mapper;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.in.job.domain.MydoJob;
|
||||
import com.ruoyi.in.jobdetail.domain.MydoJobDetail;
|
||||
import com.ruoyi.out.dense.domain.DenseLanePosition;
|
||||
import com.ruoyi.out.relocation.domain.RelocationGenerateRequest;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 密集库移库作业 Mapper。
|
||||
*/
|
||||
public interface RelocationJobMapper
|
||||
{
|
||||
MydoJob selectJobForUpdate(@Param("jobId") String jobId);
|
||||
|
||||
List<MydoJobDetail> selectStockDetailsForUpdate(@Param("cargoSpaceId") String cargoSpaceId,
|
||||
@Param("trayCode") String trayCode);
|
||||
|
||||
List<DenseLanePosition> selectRelocationSourcesForUpdate(RelocationGenerateRequest request);
|
||||
|
||||
BigDecimal sumTrayStockQuantity(@Param("trayCodes") String[] trayCodes);
|
||||
|
||||
int reserveTarget(Map<String, Object> params);
|
||||
|
||||
int reserveSource(Map<String, Object> params);
|
||||
|
||||
int insertRelocationJob(MydoJob job);
|
||||
|
||||
int insertRelocationDetail(MydoJobDetail detail);
|
||||
|
||||
int lockRelocationState(MydoJob job);
|
||||
|
||||
int moveTrayStock(MydoJob job);
|
||||
|
||||
int completeCargoSpaces(MydoJob job);
|
||||
|
||||
int archiveRelocationDetails(@Param("jobId") String jobId);
|
||||
|
||||
int archiveRelocationJob(@Param("jobId") String jobId);
|
||||
|
||||
int deleteRelocationDetails(@Param("jobId") String jobId);
|
||||
|
||||
int deleteRelocationJob(@Param("jobId") String jobId);
|
||||
|
||||
Integer isFinishedRelocation(@Param("jobId") String jobId);
|
||||
|
||||
String selectNextSourceLaneJob(@Param("cargoSpaceId") String cargoSpaceId);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ruoyi.out.relocation.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.out.relocation.domain.RelocationGenerateRequest;
|
||||
import com.ruoyi.out.relocation.domain.RelocationPreview;
|
||||
|
||||
/**
|
||||
* 密集库移库作业服务。
|
||||
*/
|
||||
public interface IRelocationJobService
|
||||
{
|
||||
List<String> createBlockerRelocations(String blockedJobId);
|
||||
|
||||
RelocationPreview preview(RelocationGenerateRequest request);
|
||||
|
||||
List<String> generate(RelocationGenerateRequest request);
|
||||
|
||||
AjaxResult completeFromWcs(String jobId);
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
package com.ruoyi.out.relocation.service.impl;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import com.ruoyi.bas.cargospace.domain.BasCargoSpace;
|
||||
import com.ruoyi.bas.id.job.service.JobIdService;
|
||||
import com.ruoyi.bas.id.taskno.service.TaskNoService;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.in.job.domain.MydoJob;
|
||||
import com.ruoyi.in.jobdetail.domain.MydoJobDetail;
|
||||
import com.ruoyi.out.dense.domain.DenseLanePosition;
|
||||
import com.ruoyi.out.dense.domain.RelocationTargetQuery;
|
||||
import com.ruoyi.out.dense.service.IDenseLaneService;
|
||||
import com.ruoyi.out.dispatch.domain.DispatchTrigger;
|
||||
import com.ruoyi.out.dispatch.service.IOutboundDispatchService;
|
||||
import com.ruoyi.out.relocation.domain.RelocationGenerateRequest;
|
||||
import com.ruoyi.out.relocation.domain.RelocationPreview;
|
||||
import com.ruoyi.out.relocation.mapper.RelocationJobMapper;
|
||||
import com.ruoyi.out.relocation.service.IRelocationJobService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
/**
|
||||
* 密集库移库作业服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class RelocationJobServiceImpl implements IRelocationJobService
|
||||
{
|
||||
private final RelocationJobMapper mapper;
|
||||
private final IDenseLaneService denseLaneService;
|
||||
private final JobIdService jobIdService;
|
||||
private final TaskNoService taskNoService;
|
||||
private final IOutboundDispatchService dispatchService;
|
||||
|
||||
public RelocationJobServiceImpl(RelocationJobMapper mapper,
|
||||
IDenseLaneService denseLaneService,
|
||||
JobIdService jobIdService,
|
||||
TaskNoService taskNoService,
|
||||
IOutboundDispatchService dispatchService)
|
||||
{
|
||||
this.mapper = mapper;
|
||||
this.denseLaneService = denseLaneService;
|
||||
this.jobIdService = jobIdService;
|
||||
this.taskNoService = taskNoService;
|
||||
this.dispatchService = dispatchService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<String> createBlockerRelocations(String blockedJobId)
|
||||
{
|
||||
if (!StringUtils.hasText(blockedJobId)) {
|
||||
throw new RuntimeException("被遮挡作业ID不能为空");
|
||||
}
|
||||
MydoJob blockedJob = mapper.selectJobForUpdate(blockedJobId.trim());
|
||||
if (blockedJob == null) {
|
||||
throw new RuntimeException("被遮挡出库作业不存在");
|
||||
}
|
||||
List<DenseLanePosition> blockers = denseLaneService.selectBlockersForUpdate(
|
||||
blockedJob.getCargoSpaceId());
|
||||
return createRelocations(blockers, blockedJob.getWarehouseName(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public RelocationPreview preview(RelocationGenerateRequest request)
|
||||
{
|
||||
validateRequest(request);
|
||||
List<DenseLanePosition> candidates = mapper.selectRelocationSourcesForUpdate(request);
|
||||
if (candidates == null || candidates.size() < request.getTrayCount()) {
|
||||
throw new RuntimeException("源通道可移库托盘数量不足");
|
||||
}
|
||||
List<DenseLanePosition> selected = denseLaneService.orderOutermostFirst(candidates)
|
||||
.stream().limit(request.getTrayCount()).collect(Collectors.toList());
|
||||
RelocationPreview preview = new RelocationPreview();
|
||||
preview.setTrays(selected);
|
||||
String[] trayCodes = selected.stream().map(DenseLanePosition::getTrayCode).toArray(String[]::new);
|
||||
BigDecimal total = mapper.sumTrayStockQuantity(trayCodes);
|
||||
preview.setTotalQuantity(total == null ? BigDecimal.ZERO : total);
|
||||
return preview;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<String> generate(RelocationGenerateRequest request)
|
||||
{
|
||||
validateRequest(request);
|
||||
List<DenseLanePosition> candidates = mapper.selectRelocationSourcesForUpdate(request);
|
||||
if (candidates == null || candidates.size() < request.getTrayCount()) {
|
||||
throw new RuntimeException("源通道可移库托盘数量不足");
|
||||
}
|
||||
List<DenseLanePosition> selected = denseLaneService.orderOutermostFirst(candidates)
|
||||
.stream().limit(request.getTrayCount()).collect(Collectors.toList());
|
||||
return createRelocations(selected, null, request.getTargetGroupCoding());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public AjaxResult completeFromWcs(String jobId)
|
||||
{
|
||||
if (!StringUtils.hasText(jobId)) {
|
||||
return AjaxResult.error("移库作业ID不能为空");
|
||||
}
|
||||
MydoJob job = mapper.selectJobForUpdate(jobId.trim());
|
||||
if (job == null) {
|
||||
if (Integer.valueOf(1).equals(mapper.isFinishedRelocation(jobId.trim()))) {
|
||||
return AjaxResult.success("移库作业已完成");
|
||||
}
|
||||
return AjaxResult.error("移库作业不存在");
|
||||
}
|
||||
if (!Long.valueOf(23L).equals(job.getInOrOut())) {
|
||||
return AjaxResult.error("当前作业不是移库作业");
|
||||
}
|
||||
if (!"3".equals(job.getStatus()) || !"1".equals(job.getExecutionPhase())) {
|
||||
return AjaxResult.error("移库作业尚未成功下发WCS");
|
||||
}
|
||||
if (mapper.lockRelocationState(job) != 1) {
|
||||
throw new RuntimeException("移库源货位、目标货位或托盘状态已变化");
|
||||
}
|
||||
if (mapper.moveTrayStock(job) <= 0) {
|
||||
throw new RuntimeException("移库托盘库存不存在");
|
||||
}
|
||||
if (mapper.completeCargoSpaces(job) != 2) {
|
||||
throw new RuntimeException("移库货位状态更新失败");
|
||||
}
|
||||
mapper.archiveRelocationDetails(job.getJobId());
|
||||
if (mapper.archiveRelocationJob(job.getJobId()) != 1) {
|
||||
throw new RuntimeException("移库作业归档失败");
|
||||
}
|
||||
mapper.deleteRelocationDetails(job.getJobId());
|
||||
if (mapper.deleteRelocationJob(job.getJobId()) != 1) {
|
||||
throw new RuntimeException("移库活动作业删除失败");
|
||||
}
|
||||
afterCommit(() -> dispatchNextSourceLaneJob(job.getCargoSpaceId()));
|
||||
return AjaxResult.success("移库作业完成");
|
||||
}
|
||||
|
||||
private List<String> createRelocations(List<DenseLanePosition> sources,
|
||||
String warehouseName,
|
||||
String targetGroupCoding)
|
||||
{
|
||||
if (sources == null || sources.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<BasCargoSpace> targets = new ArrayList<>();
|
||||
for (DenseLanePosition source : sources) {
|
||||
RelocationTargetQuery query = new RelocationTargetQuery();
|
||||
query.setWarehouseId(source.getWarehouseId());
|
||||
query.setTheFloor(source.getTheFloor());
|
||||
query.setSourceGroupCoding(source.getGroupCoding());
|
||||
query.setTargetGroupCoding(targetGroupCoding);
|
||||
query.setTrayCode(source.getTrayCode());
|
||||
BasCargoSpace target = denseLaneService.selectRelocationTargetForUpdate(query);
|
||||
if (target == null) {
|
||||
throw new RuntimeException("同层可用移库货位不足");
|
||||
}
|
||||
Map<String, Object> reserve = new HashMap<>();
|
||||
reserve.put("cargoSpaceId", target.getCargoSpaceId());
|
||||
reserve.put("trayCode", source.getTrayCode());
|
||||
if (mapper.reserveTarget(reserve) != 1) {
|
||||
throw new RuntimeException("移库目标货位已被占用");
|
||||
}
|
||||
targets.add(target);
|
||||
}
|
||||
|
||||
List<String> jobIds = new ArrayList<>();
|
||||
for (int index = 0; index < sources.size(); index++) {
|
||||
DenseLanePosition source = sources.get(index);
|
||||
BasCargoSpace target = targets.get(index);
|
||||
Map<String, Object> sourceReserve = new HashMap<>();
|
||||
sourceReserve.put("cargoSpaceId", source.getCargoSpaceId());
|
||||
sourceReserve.put("trayCode", source.getTrayCode());
|
||||
if (mapper.reserveSource(sourceReserve) != 1) {
|
||||
throw new RuntimeException("移库源货位状态已变化");
|
||||
}
|
||||
List<MydoJobDetail> stockDetails = mapper.selectStockDetailsForUpdate(
|
||||
source.getCargoSpaceId(), source.getTrayCode());
|
||||
if (stockDetails == null || stockDetails.isEmpty()) {
|
||||
throw new RuntimeException("移库托盘没有库存明细");
|
||||
}
|
||||
String jobId = jobIdService.getId("CK");
|
||||
if (!StringUtils.hasText(jobId)) {
|
||||
throw new RuntimeException("移库作业ID生成失败");
|
||||
}
|
||||
MydoJob job = buildJob(jobId, taskNoService.getTaskNo("23"), source,
|
||||
target, warehouseName);
|
||||
if (mapper.insertRelocationJob(job) != 1) {
|
||||
throw new RuntimeException("移库作业生成失败");
|
||||
}
|
||||
int detailIndex = 1;
|
||||
for (MydoJobDetail template : stockDetails) {
|
||||
MydoJobDetail detail = copyDetail(template, jobId, detailIndex++);
|
||||
if (mapper.insertRelocationDetail(detail) != 1) {
|
||||
throw new RuntimeException("移库作业明细生成失败");
|
||||
}
|
||||
}
|
||||
jobIds.add(jobId);
|
||||
}
|
||||
afterCommit(() -> jobIds.forEach(id -> dispatchService.dispatch(id, DispatchTrigger.INITIAL)));
|
||||
return jobIds;
|
||||
}
|
||||
|
||||
private MydoJob buildJob(String jobId, String taskNo, DenseLanePosition source,
|
||||
BasCargoSpace target, String warehouseName)
|
||||
{
|
||||
MydoJob job = new MydoJob();
|
||||
job.setJobId(jobId);
|
||||
job.setTaskNo(StringUtils.hasText(taskNo) ? taskNo : jobId);
|
||||
job.setInOrOut(23L);
|
||||
job.setStatus("1");
|
||||
job.setExecutionPhase("0");
|
||||
job.setTrayCode(source.getTrayCode());
|
||||
job.setWarehouseId(source.getWarehouseId());
|
||||
job.setWarehouseName(warehouseName);
|
||||
job.setCargoSpaceId(source.getCargoSpaceId());
|
||||
job.setCargoSpaceName(source.getCargoSpaceName());
|
||||
job.setTargetCargoSpaceId(target.getCargoSpaceId());
|
||||
job.setTargetCargoSpaceName(target.getCargoSpaceName());
|
||||
job.setLineType("1");
|
||||
job.setPriority(10L);
|
||||
job.setCreateTime(DateUtils.getNowDate());
|
||||
return job;
|
||||
}
|
||||
|
||||
private MydoJobDetail copyDetail(MydoJobDetail source, String jobId, int index)
|
||||
{
|
||||
MydoJobDetail detail = new MydoJobDetail();
|
||||
detail.setJobDetailId(jobId + "-" + index);
|
||||
detail.setJobId(jobId);
|
||||
detail.setStockId(source.getStockId());
|
||||
detail.setLotNo(source.getLotNo());
|
||||
detail.setInDate(source.getInDate());
|
||||
detail.setInOrOutId(source.getInOrOutId());
|
||||
detail.setInOrOutDId(source.getInOrOutDId());
|
||||
detail.setProductId(source.getProductId());
|
||||
detail.setProductName(source.getProductName());
|
||||
detail.setStockQuantity(source.getStockQuantity());
|
||||
detail.setStockinId(source.getStockinId());
|
||||
detail.setStockinDetailId(source.getStockinDetailId());
|
||||
detail.setCustomerId(source.getCustomerId());
|
||||
detail.setCustomerName(source.getCustomerName());
|
||||
detail.setSupplierId(source.getSupplierId());
|
||||
detail.setSupplierName(source.getSupplierName());
|
||||
detail.setProjectNo(source.getProjectNo());
|
||||
return detail;
|
||||
}
|
||||
|
||||
private void validateRequest(RelocationGenerateRequest request)
|
||||
{
|
||||
if (request == null || !"03".equals(request.getWarehouseId())) {
|
||||
throw new RuntimeException("指定移库仅支持03仓密集库");
|
||||
}
|
||||
if (request.getTheFloor() == null) {
|
||||
throw new RuntimeException("请选择移库层");
|
||||
}
|
||||
if (!StringUtils.hasText(request.getSourceGroupCoding())
|
||||
|| !StringUtils.hasText(request.getTargetGroupCoding())) {
|
||||
throw new RuntimeException("请选择源通道和目标通道");
|
||||
}
|
||||
if (request.getSourceGroupCoding().equals(request.getTargetGroupCoding())) {
|
||||
throw new RuntimeException("移库目标必须是同层不同通道");
|
||||
}
|
||||
if (request.getTrayCount() == null || request.getTrayCount() <= 0) {
|
||||
throw new RuntimeException("移库托盘数量必须大于0");
|
||||
}
|
||||
}
|
||||
|
||||
private void dispatchNextSourceLaneJob(String sourceCargoSpaceId)
|
||||
{
|
||||
String nextJobId = mapper.selectNextSourceLaneJob(sourceCargoSpaceId);
|
||||
if (StringUtils.hasText(nextJobId)) {
|
||||
dispatchService.dispatch(nextJobId, DispatchTrigger.INITIAL);
|
||||
}
|
||||
}
|
||||
|
||||
private void afterCommit(Runnable action)
|
||||
{
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization()
|
||||
{
|
||||
@Override
|
||||
public void afterCommit()
|
||||
{
|
||||
action.run();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
action.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
<?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.out.relocation.mapper.RelocationJobMapper">
|
||||
|
||||
<select id="selectJobForUpdate" resultType="com.ruoyi.in.job.domain.MydoJob">
|
||||
select *
|
||||
from mydo_job with (UPDLOCK,HOLDLOCK)
|
||||
where job_id = #{jobId}
|
||||
</select>
|
||||
|
||||
<select id="selectStockDetailsForUpdate"
|
||||
resultType="com.ruoyi.in.jobdetail.domain.MydoJobDetail">
|
||||
select detail.stock_id,
|
||||
detail.lot_no,
|
||||
convert(varchar(19), stock.in_date, 120) as in_date,
|
||||
stock.stockin_id,
|
||||
detail.detail_id as stockin_detail_id,
|
||||
detail.product_id,
|
||||
detail.product_name,
|
||||
detail.quantity as stock_quantity,
|
||||
cast(null as nvarchar(255)) as customer_id,
|
||||
cast(null as nvarchar(255)) as customer_name,
|
||||
stock.suplier_id as supplier_id,
|
||||
stock.suplier_name as supplier_name,
|
||||
detail.project_no
|
||||
from bas_stock stock with (UPDLOCK,HOLDLOCK)
|
||||
inner join bas_stock_detail detail with (UPDLOCK,HOLDLOCK)
|
||||
on detail.stock_id = stock.stock_id
|
||||
where stock.cargo_space_id = #{cargoSpaceId}
|
||||
and stock.tray_code = #{trayCode}
|
||||
and isnull(detail.quantity, 0) > 0
|
||||
</select>
|
||||
|
||||
<select id="selectRelocationSourcesForUpdate"
|
||||
resultType="com.ruoyi.out.dense.domain.DenseLanePosition">
|
||||
select cargo.cargo_space_id,
|
||||
cargo.cargo_space_name,
|
||||
cargo.warehouse_id,
|
||||
cargo.group_coding,
|
||||
cargo.this_cargo_area as cargo_area,
|
||||
cargo.the_platoon,
|
||||
cargo.the_column,
|
||||
cargo.the_floor,
|
||||
cargo.status,
|
||||
cargo.locked,
|
||||
cargo.tray_code,
|
||||
isnull(config.direction_type, '1') as direction_type,
|
||||
boundary.min_column,
|
||||
boundary.max_column
|
||||
from bas_cargo_space cargo with (UPDLOCK,HOLDLOCK)
|
||||
left join bas_cargo_area_config config
|
||||
on config.warehouse_id = cargo.warehouse_id
|
||||
and config.cargo_area_code = cargo.this_cargo_area
|
||||
and config.is_active = '1'
|
||||
cross apply
|
||||
(
|
||||
select min(lane.the_column) as min_column,
|
||||
max(lane.the_column) as max_column
|
||||
from bas_cargo_space lane with (UPDLOCK,HOLDLOCK)
|
||||
where lane.warehouse_id = cargo.warehouse_id
|
||||
and lane.group_coding = cargo.group_coding
|
||||
and lane.the_floor = cargo.the_floor
|
||||
) boundary
|
||||
where cargo.warehouse_id = #{warehouseId}
|
||||
and cargo.the_floor = #{theFloor}
|
||||
and cargo.group_coding = #{sourceGroupCoding}
|
||||
and cargo.status = '2'
|
||||
and cargo.tray_code is not null
|
||||
and not exists
|
||||
(
|
||||
select 1 from mydo_job active_job with (UPDLOCK,HOLDLOCK)
|
||||
where active_job.status in ('1', '2', '3')
|
||||
and (active_job.cargo_space_id = cargo.cargo_space_id
|
||||
or active_job.tray_code = cargo.tray_code)
|
||||
)
|
||||
</select>
|
||||
|
||||
<select id="sumTrayStockQuantity" resultType="java.math.BigDecimal">
|
||||
select isnull(sum(isnull(detail.quantity, 0)), 0)
|
||||
from bas_stock stock
|
||||
inner join bas_stock_detail detail on detail.stock_id = stock.stock_id
|
||||
where stock.tray_code in
|
||||
<foreach collection="trayCodes" item="trayCode" open="(" separator="," close=")">
|
||||
#{trayCode}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
<update id="reserveTarget" parameterType="java.util.Map">
|
||||
update bas_cargo_space
|
||||
set status = '3', tray_code = #{trayCode}
|
||||
where cargo_space_id = #{cargoSpaceId}
|
||||
and status = '1'
|
||||
and isnull(locked, 'N') = 'N'
|
||||
and tray_code is null
|
||||
</update>
|
||||
|
||||
<update id="reserveSource" parameterType="java.util.Map">
|
||||
update bas_cargo_space
|
||||
set status = '4'
|
||||
where cargo_space_id = #{cargoSpaceId}
|
||||
and status in ('2', '3')
|
||||
and tray_code = #{trayCode}
|
||||
</update>
|
||||
|
||||
<insert id="insertRelocationJob" parameterType="com.ruoyi.in.job.domain.MydoJob">
|
||||
insert into mydo_job
|
||||
(job_id, status, task_no, in_or_out, execution_phase, tray_code,
|
||||
warehouse_id, warehouse_name, cargo_space_id, cargo_space_name,
|
||||
target_cargo_space_id, target_cargo_space_name, line_type, priority, create_time)
|
||||
values
|
||||
(#{jobId}, #{status}, #{taskNo}, #{inOrOut}, #{executionPhase}, #{trayCode},
|
||||
#{warehouseId}, #{warehouseName}, #{cargoSpaceId}, #{cargoSpaceName},
|
||||
#{targetCargoSpaceId}, #{targetCargoSpaceName}, #{lineType}, #{priority}, #{createTime})
|
||||
</insert>
|
||||
|
||||
<insert id="insertRelocationDetail" parameterType="com.ruoyi.in.jobdetail.domain.MydoJobDetail">
|
||||
insert into mydo_job_detail
|
||||
(job_detail_id, job_id, stock_id, lot_no, in_date, in_or_out_id,
|
||||
in_or_out_d_id, product_id, product_name, stock_quantity, stockin_id,
|
||||
stockin_detail_id, customer_id, customer_name, supplier_id, supplier_name, project_no)
|
||||
values
|
||||
(#{jobDetailId}, #{jobId}, #{stockId}, #{lotNo}, #{inDate}, #{inOrOutId},
|
||||
#{inOrOutDId}, #{productId}, #{productName}, #{stockQuantity}, #{stockinId},
|
||||
#{stockinDetailId}, #{customerId}, #{customerName}, #{supplierId}, #{supplierName}, #{projectNo})
|
||||
</insert>
|
||||
|
||||
<select id="lockRelocationState" resultType="java.lang.Integer">
|
||||
select case when count(1) = 2 then 1 else 0 end
|
||||
from bas_cargo_space cargo with (UPDLOCK,HOLDLOCK)
|
||||
where (cargo.cargo_space_id = #{cargoSpaceId}
|
||||
and cargo.status = '4' and cargo.tray_code = #{trayCode})
|
||||
or (cargo.cargo_space_id = #{targetCargoSpaceId}
|
||||
and cargo.status = '3' and cargo.tray_code = #{trayCode})
|
||||
</select>
|
||||
|
||||
<update id="moveTrayStock" parameterType="com.ruoyi.in.job.domain.MydoJob">
|
||||
update bas_stock
|
||||
set cargo_space_id = #{targetCargoSpaceId}
|
||||
where cargo_space_id = #{cargoSpaceId}
|
||||
and tray_code = #{trayCode}
|
||||
</update>
|
||||
|
||||
<update id="completeCargoSpaces" parameterType="com.ruoyi.in.job.domain.MydoJob">
|
||||
update bas_cargo_space
|
||||
set status = case when cargo_space_id = #{cargoSpaceId} then '1' else '2' end,
|
||||
tray_code = case when cargo_space_id = #{cargoSpaceId} then null else #{trayCode} end
|
||||
where (cargo_space_id = #{cargoSpaceId} and status = '4' and tray_code = #{trayCode})
|
||||
or (cargo_space_id = #{targetCargoSpaceId} and status = '3' and tray_code = #{trayCode})
|
||||
</update>
|
||||
|
||||
<insert id="archiveRelocationDetails">
|
||||
insert into mydo_job_detail_finish
|
||||
(job_detail_id, job_id, stock_id, lot_no, in_date, in_or_out_id,
|
||||
in_or_out_d_id, product_id, product_name, in_quantity, stock_quantity,
|
||||
out_quantity, stockin_id, stockin_detail_id, customer_id, customer_name,
|
||||
supplier_id, supplier_name, project_no)
|
||||
select job_detail_id, job_id, stock_id, lot_no, in_date, in_or_out_id,
|
||||
in_or_out_d_id, product_id, product_name, in_quantity, stock_quantity,
|
||||
out_quantity, stockin_id, stockin_detail_id, customer_id, customer_name,
|
||||
supplier_id, supplier_name, project_no
|
||||
from mydo_job_detail with (UPDLOCK,HOLDLOCK)
|
||||
where job_id = #{jobId}
|
||||
</insert>
|
||||
|
||||
<insert id="archiveRelocationJob">
|
||||
insert into mydo_job_finish
|
||||
(job_id, status, task_no, in_or_out, execution_phase, tray_code,
|
||||
warehouse_id, warehouse_name, roadway_id, cargo_space_id, cargo_space_name,
|
||||
target_roadway_id, target_cargo_space_id, target_cargo_space_name, line_type,
|
||||
priority, height, weight, create_time, start_time, finish_time, entrance_id,
|
||||
exit_id, dept_id, dept_name, user_id, user_name, supplier_id, supplier_name,
|
||||
customer_id, customer_name)
|
||||
select job_id, '4', task_no, in_or_out, '5', tray_code,
|
||||
warehouse_id, warehouse_name, roadway_id, cargo_space_id, cargo_space_name,
|
||||
target_roadway_id, target_cargo_space_id, target_cargo_space_name, line_type,
|
||||
priority, height, weight, create_time, start_time, getdate(), entrance_id,
|
||||
exit_id, dept_id, dept_name, user_id, user_name, supplier_id, supplier_name,
|
||||
customer_id, customer_name
|
||||
from mydo_job with (UPDLOCK,HOLDLOCK)
|
||||
where job_id = #{jobId} and in_or_out = 23
|
||||
</insert>
|
||||
|
||||
<delete id="deleteRelocationDetails">
|
||||
delete from mydo_job_detail where job_id = #{jobId}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteRelocationJob">
|
||||
delete from mydo_job where job_id = #{jobId} and in_or_out = 23
|
||||
</delete>
|
||||
|
||||
<select id="isFinishedRelocation" resultType="java.lang.Integer">
|
||||
select case when exists
|
||||
(select 1 from mydo_job_finish where job_id = #{jobId} and in_or_out = 23)
|
||||
then 1 else 0 end
|
||||
</select>
|
||||
|
||||
<select id="selectNextSourceLaneJob" resultType="java.lang.String">
|
||||
select top 1 job.job_id
|
||||
from mydo_job job
|
||||
inner join bas_cargo_space job_space on job_space.cargo_space_id = job.cargo_space_id
|
||||
inner join bas_cargo_space source_space on source_space.cargo_space_id = #{cargoSpaceId}
|
||||
left join wcs_job_dispatch dispatch on dispatch.job_id = job.job_id
|
||||
where job.in_or_out >= 20
|
||||
and job.status = '1'
|
||||
and job_space.warehouse_id = source_space.warehouse_id
|
||||
and job_space.group_coding = source_space.group_coding
|
||||
and job_space.the_floor = source_space.the_floor
|
||||
and (dispatch.dispatch_status is null or dispatch.dispatch_status != 'SUCCESS')
|
||||
order by job.priority desc, job.create_time asc, job.job_id asc
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -0,0 +1,212 @@
|
||||
package com.ruoyi.out.relocation.service.impl;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import com.ruoyi.bas.cargospace.domain.BasCargoSpace;
|
||||
import com.ruoyi.bas.id.job.service.JobIdService;
|
||||
import com.ruoyi.bas.id.taskno.service.TaskNoService;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.in.job.domain.MydoJob;
|
||||
import com.ruoyi.in.jobdetail.domain.MydoJobDetail;
|
||||
import com.ruoyi.out.dense.domain.DenseLanePosition;
|
||||
import com.ruoyi.out.dense.domain.RelocationTargetQuery;
|
||||
import com.ruoyi.out.dense.service.IDenseLaneService;
|
||||
import com.ruoyi.out.dispatch.domain.DispatchTrigger;
|
||||
import com.ruoyi.out.dispatch.service.IOutboundDispatchService;
|
||||
import com.ruoyi.out.relocation.domain.RelocationGenerateRequest;
|
||||
import com.ruoyi.out.relocation.mapper.RelocationJobMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class RelocationJobServiceImplTest
|
||||
{
|
||||
private RelocationJobMapper mapper;
|
||||
private IDenseLaneService denseLaneService;
|
||||
private JobIdService jobIdService;
|
||||
private TaskNoService taskNoService;
|
||||
private IOutboundDispatchService dispatchService;
|
||||
private RelocationJobServiceImpl service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp()
|
||||
{
|
||||
mapper = mock(RelocationJobMapper.class);
|
||||
denseLaneService = mock(IDenseLaneService.class);
|
||||
jobIdService = mock(JobIdService.class);
|
||||
taskNoService = mock(TaskNoService.class);
|
||||
dispatchService = mock(IOutboundDispatchService.class);
|
||||
service = new RelocationJobServiceImpl(
|
||||
mapper, denseLaneService, jobIdService, taskNoService, dispatchService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsOneType23JobPerBlockerAndCopiesEveryStockDetail()
|
||||
{
|
||||
MydoJob blocked = blockedJob("OUT-1", "CS-3");
|
||||
DenseLanePosition outer = source("CS-5", "T-5");
|
||||
DenseLanePosition inner = source("CS-4", "T-4");
|
||||
when(mapper.selectJobForUpdate("OUT-1")).thenReturn(blocked);
|
||||
when(denseLaneService.selectBlockersForUpdate("CS-3")).thenReturn(List.of(outer, inner));
|
||||
when(denseLaneService.selectRelocationTargetForUpdate(any(RelocationTargetQuery.class)))
|
||||
.thenReturn(target("TG-1", "TARGET-1"), target("TG-2", "TARGET-2"));
|
||||
when(mapper.reserveTarget(any())).thenReturn(1);
|
||||
when(mapper.reserveSource(any())).thenReturn(1);
|
||||
when(jobIdService.getId("CK")).thenReturn("MOVE-1", "MOVE-2");
|
||||
when(taskNoService.getTaskNo("23")).thenReturn("MOVE-TASK-1", "MOVE-TASK-2");
|
||||
when(mapper.selectStockDetailsForUpdate(anyString(), anyString()))
|
||||
.thenReturn(List.of(detail("S-1", "P-1"), detail("S-2", "P-2")));
|
||||
when(mapper.insertRelocationJob(any(MydoJob.class))).thenReturn(1);
|
||||
when(mapper.insertRelocationDetail(any(MydoJobDetail.class))).thenReturn(1);
|
||||
when(dispatchService.dispatch(anyString(), any(DispatchTrigger.class)))
|
||||
.thenReturn(AjaxResult.success());
|
||||
|
||||
List<String> ids = service.createBlockerRelocations("OUT-1");
|
||||
|
||||
assertThat(ids).containsExactly("MOVE-1", "MOVE-2");
|
||||
ArgumentCaptor<MydoJob> jobs = ArgumentCaptor.forClass(MydoJob.class);
|
||||
verify(mapper, org.mockito.Mockito.times(2)).insertRelocationJob(jobs.capture());
|
||||
assertThat(jobs.getAllValues()).allSatisfy(job -> {
|
||||
assertThat(job.getInOrOut()).isEqualTo(23L);
|
||||
assertThat(job.getStatus()).isEqualTo("1");
|
||||
assertThat(job.getExecutionPhase()).isEqualTo("0");
|
||||
assertThat(job.getTargetCargoSpaceId()).isNotBlank();
|
||||
});
|
||||
verify(mapper, org.mockito.Mockito.times(4)).insertRelocationDetail(any(MydoJobDetail.class));
|
||||
verify(taskNoService, org.mockito.Mockito.times(2)).getTaskNo("23");
|
||||
verify(dispatchService).dispatch("MOVE-1", DispatchTrigger.INITIAL);
|
||||
verify(dispatchService).dispatch("MOVE-2", DispatchTrigger.INITIAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void reservesAllTargetsBeforeInsertingAnyRelocationJob()
|
||||
{
|
||||
MydoJob blocked = blockedJob("OUT-2", "CS-3");
|
||||
when(mapper.selectJobForUpdate("OUT-2")).thenReturn(blocked);
|
||||
when(denseLaneService.selectBlockersForUpdate("CS-3"))
|
||||
.thenReturn(List.of(source("CS-5", "T-5"), source("CS-4", "T-4")));
|
||||
when(denseLaneService.selectRelocationTargetForUpdate(any(RelocationTargetQuery.class)))
|
||||
.thenReturn(target("TG-1", "TARGET-1"), (BasCargoSpace) null);
|
||||
when(mapper.reserveTarget(any())).thenReturn(1);
|
||||
|
||||
assertThatThrownBy(() -> service.createBlockerRelocations("OUT-2"))
|
||||
.hasMessageContaining("同层可用移库货位不足");
|
||||
verify(mapper, never()).insertRelocationJob(any(MydoJob.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsManualRelocationToSameLaneOrDifferentFloor()
|
||||
{
|
||||
RelocationGenerateRequest sameLane = new RelocationGenerateRequest();
|
||||
sameLane.setWarehouseId("03");
|
||||
sameLane.setTheFloor(2L);
|
||||
sameLane.setSourceGroupCoding("G-02");
|
||||
sameLane.setTargetGroupCoding("G-02");
|
||||
sameLane.setTrayCount(1);
|
||||
|
||||
assertThatThrownBy(() -> service.generate(sameLane))
|
||||
.hasMessageContaining("不同通道");
|
||||
}
|
||||
|
||||
@Test
|
||||
void completionMovesInventorySwapsCargoSpacesArchivesAndWakesNextJob()
|
||||
{
|
||||
MydoJob move = blockedJob("MOVE-1", "SOURCE");
|
||||
move.setInOrOut(23L);
|
||||
move.setStatus("3");
|
||||
move.setExecutionPhase("1");
|
||||
move.setTargetCargoSpaceId("TARGET");
|
||||
move.setTrayCode("T-1");
|
||||
when(mapper.selectJobForUpdate("MOVE-1")).thenReturn(move);
|
||||
when(mapper.lockRelocationState(move)).thenReturn(1);
|
||||
when(mapper.moveTrayStock(move)).thenReturn(2);
|
||||
when(mapper.completeCargoSpaces(move)).thenReturn(2);
|
||||
when(mapper.archiveRelocationDetails("MOVE-1")).thenReturn(2);
|
||||
when(mapper.archiveRelocationJob("MOVE-1")).thenReturn(1);
|
||||
when(mapper.deleteRelocationDetails("MOVE-1")).thenReturn(2);
|
||||
when(mapper.deleteRelocationJob("MOVE-1")).thenReturn(1);
|
||||
when(mapper.selectNextSourceLaneJob("SOURCE")).thenReturn("NEXT-1");
|
||||
when(dispatchService.dispatch("NEXT-1", DispatchTrigger.INITIAL))
|
||||
.thenReturn(AjaxResult.success());
|
||||
|
||||
AjaxResult result = service.completeFromWcs("MOVE-1");
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var ordered = inOrder(mapper);
|
||||
ordered.verify(mapper).moveTrayStock(move);
|
||||
ordered.verify(mapper).completeCargoSpaces(move);
|
||||
ordered.verify(mapper).archiveRelocationDetails("MOVE-1");
|
||||
ordered.verify(mapper).archiveRelocationJob("MOVE-1");
|
||||
ordered.verify(mapper).deleteRelocationDetails("MOVE-1");
|
||||
ordered.verify(mapper).deleteRelocationJob("MOVE-1");
|
||||
verify(dispatchService).dispatch("NEXT-1", DispatchTrigger.INITIAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateCompletionOfArchivedRelocationIsIdempotent()
|
||||
{
|
||||
when(mapper.selectJobForUpdate("MOVE-DONE")).thenReturn(null);
|
||||
when(mapper.isFinishedRelocation("MOVE-DONE")).thenReturn(1);
|
||||
|
||||
assertThat(service.completeFromWcs("MOVE-DONE").isSuccess()).isTrue();
|
||||
verify(mapper, never()).moveTrayStock(any(MydoJob.class));
|
||||
}
|
||||
|
||||
private MydoJob blockedJob(String jobId, String cargoSpaceId)
|
||||
{
|
||||
MydoJob job = new MydoJob();
|
||||
job.setJobId(jobId);
|
||||
job.setWarehouseId("03");
|
||||
job.setWarehouseName("三号仓");
|
||||
job.setCargoSpaceId(cargoSpaceId);
|
||||
job.setStatus("1");
|
||||
job.setInOrOut(20L);
|
||||
return job;
|
||||
}
|
||||
|
||||
private DenseLanePosition source(String cargoSpaceId, String trayCode)
|
||||
{
|
||||
DenseLanePosition position = new DenseLanePosition();
|
||||
position.setCargoSpaceId(cargoSpaceId);
|
||||
position.setCargoSpaceName(cargoSpaceId);
|
||||
position.setWarehouseId("03");
|
||||
position.setGroupCoding("SOURCE-GROUP");
|
||||
position.setTheFloor(1L);
|
||||
position.setTrayCode(trayCode);
|
||||
return position;
|
||||
}
|
||||
|
||||
private BasCargoSpace target(String group, String cargoSpaceId)
|
||||
{
|
||||
BasCargoSpace target = new BasCargoSpace();
|
||||
target.setCargoSpaceId(cargoSpaceId);
|
||||
target.setCargoSpaceName(cargoSpaceId);
|
||||
target.setWarehouseId("03");
|
||||
target.setGroupCoding(group);
|
||||
target.setTheFloor(1L);
|
||||
return target;
|
||||
}
|
||||
|
||||
private MydoJobDetail detail(String stockId, String productId)
|
||||
{
|
||||
MydoJobDetail detail = new MydoJobDetail();
|
||||
detail.setStockId(stockId);
|
||||
detail.setStockinDetailId("DETAIL-" + stockId);
|
||||
detail.setStockinId("IN-" + stockId);
|
||||
detail.setProductId(productId);
|
||||
detail.setProductName(productId);
|
||||
detail.setProjectNo("PROJECT-1");
|
||||
detail.setStockQuantity(BigDecimal.TEN);
|
||||
return detail;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user