feat: add KPI dense lane relocation
This commit is contained in:
@@ -10,6 +10,8 @@ import com.ruoyi.kpi.service.KpiService;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
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.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@@ -63,6 +65,27 @@ public class KpiController extends BaseController
|
|||||||
return AjaxResult.success(kpiService.getDenseCargoSpacePlanData(warehouseId));
|
return AjaxResult.success(kpiService.getDenseCargoSpacePlanData(warehouseId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/denseRelocation/options")
|
||||||
|
@PreAuthorize("@ss.hasPermi('kpi:cargo:space:plan')")
|
||||||
|
public AjaxResult denseRelocationOptions(
|
||||||
|
com.ruoyi.out.relocation.domain.RelocationGenerateRequest request) {
|
||||||
|
return AjaxResult.success(kpiService.getDenseRelocationOptions(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/denseRelocation/preview")
|
||||||
|
@PreAuthorize("@ss.hasPermi('kpi:cargo:space:plan')")
|
||||||
|
public AjaxResult previewDenseRelocation(
|
||||||
|
@RequestBody com.ruoyi.out.relocation.domain.RelocationGenerateRequest request) {
|
||||||
|
return AjaxResult.success(kpiService.previewDenseRelocation(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/denseRelocation/generate")
|
||||||
|
@PreAuthorize("@ss.hasPermi('kpi:cargo:space:plan')")
|
||||||
|
public AjaxResult generateDenseRelocation(
|
||||||
|
@RequestBody com.ruoyi.out.relocation.domain.RelocationGenerateRequest request) {
|
||||||
|
return AjaxResult.success(kpiService.generateDenseRelocation(request));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 货位平面图-货位详情
|
* 货位平面图-货位详情
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -63,4 +63,13 @@ public interface KpiService
|
|||||||
* @return 包含 msg 的Map
|
* @return 包含 msg 的Map
|
||||||
*/
|
*/
|
||||||
Map<String, Object> getCargoSpaceMag(String warehouseId, String thePlatoon, String theColumn, String theFloor);
|
Map<String, Object> getCargoSpaceMag(String warehouseId, String thePlatoon, String theColumn, String theFloor);
|
||||||
|
|
||||||
|
Map<String, Object> getDenseRelocationOptions(
|
||||||
|
com.ruoyi.out.relocation.domain.RelocationGenerateRequest request);
|
||||||
|
|
||||||
|
com.ruoyi.out.relocation.domain.RelocationPreview previewDenseRelocation(
|
||||||
|
com.ruoyi.out.relocation.domain.RelocationGenerateRequest request);
|
||||||
|
|
||||||
|
List<String> generateDenseRelocation(
|
||||||
|
com.ruoyi.out.relocation.domain.RelocationGenerateRequest request);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ import com.ruoyi.in.job.mapper.MydoJobMapper;
|
|||||||
import com.ruoyi.kpi.domain.*;
|
import com.ruoyi.kpi.domain.*;
|
||||||
import com.ruoyi.kpi.mapper.KpiMapper;
|
import com.ruoyi.kpi.mapper.KpiMapper;
|
||||||
import com.ruoyi.kpi.service.KpiService;
|
import com.ruoyi.kpi.service.KpiService;
|
||||||
|
import com.ruoyi.out.dense.domain.DenseLaneOption;
|
||||||
|
import com.ruoyi.out.dense.mapper.DenseLaneMapper;
|
||||||
|
import com.ruoyi.out.relocation.domain.RelocationGenerateRequest;
|
||||||
|
import com.ruoyi.out.relocation.domain.RelocationPreview;
|
||||||
|
import com.ruoyi.out.relocation.service.IRelocationJobService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -43,6 +48,12 @@ public class KpiServiceImpl implements KpiService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private MydoJobMapper mydoJobMapper;
|
private MydoJobMapper mydoJobMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DenseLaneMapper denseLaneMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IRelocationJobService relocationJobService;
|
||||||
|
|
||||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
private static final String UNASSIGNED_ROADWAY = "未分配";
|
private static final String UNASSIGNED_ROADWAY = "未分配";
|
||||||
@@ -420,4 +431,54 @@ public class KpiServiceImpl implements KpiService {
|
|||||||
result.put("msg", msg.toString());
|
result.put("msg", msg.toString());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getDenseRelocationOptions(RelocationGenerateRequest request)
|
||||||
|
{
|
||||||
|
validateDenseRelocationBase(request);
|
||||||
|
List<DenseLaneOption> options = denseLaneMapper.selectLaneOptions(
|
||||||
|
request.getWarehouseId(), request.getTheFloor());
|
||||||
|
if (options == null) {
|
||||||
|
options = Collections.emptyList();
|
||||||
|
}
|
||||||
|
int requiredCapacity = request.getTrayCount() == null || request.getTrayCount() <= 0
|
||||||
|
? 1 : request.getTrayCount();
|
||||||
|
List<DenseLaneOption> sourceLanes = options.stream()
|
||||||
|
.filter(item -> item.getFullTrayCount() != null && item.getFullTrayCount() > 0)
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
List<DenseLaneOption> targetLanes = options.stream()
|
||||||
|
.filter(item -> !StringUtils.hasText(request.getSourceGroupCoding())
|
||||||
|
|| !request.getSourceGroupCoding().equals(item.getGroupCoding()))
|
||||||
|
.filter(item -> item.getEmptyLocationCount() != null
|
||||||
|
&& item.getEmptyLocationCount() >= requiredCapacity)
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("sourceLanes", sourceLanes);
|
||||||
|
result.put("targetLanes", targetLanes);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RelocationPreview previewDenseRelocation(RelocationGenerateRequest request)
|
||||||
|
{
|
||||||
|
validateDenseRelocationBase(request);
|
||||||
|
return relocationJobService.preview(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> generateDenseRelocation(RelocationGenerateRequest request)
|
||||||
|
{
|
||||||
|
validateDenseRelocationBase(request);
|
||||||
|
return relocationJobService.generate(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateDenseRelocationBase(RelocationGenerateRequest request)
|
||||||
|
{
|
||||||
|
if (request == null || !"03".equals(request.getWarehouseId())) {
|
||||||
|
throw new RuntimeException("指定移库仅支持03仓密集库");
|
||||||
|
}
|
||||||
|
if (request.getTheFloor() == null) {
|
||||||
|
throw new RuntimeException("指定移库必须选择当前层");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.ruoyi.out.dense.domain;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密集库通道移库选项。
|
||||||
|
*/
|
||||||
|
public class DenseLaneOption
|
||||||
|
{
|
||||||
|
private String groupCoding;
|
||||||
|
private String cargoArea;
|
||||||
|
private Long thePlatoon;
|
||||||
|
private Long theFloor;
|
||||||
|
private String directionType;
|
||||||
|
private Integer fullTrayCount;
|
||||||
|
private Integer emptyLocationCount;
|
||||||
|
private BigDecimal totalQuantity;
|
||||||
|
|
||||||
|
public String getGroupCoding() { return groupCoding; }
|
||||||
|
public void setGroupCoding(String groupCoding) { this.groupCoding = groupCoding; }
|
||||||
|
public String getCargoArea() { return cargoArea; }
|
||||||
|
public void setCargoArea(String cargoArea) { this.cargoArea = cargoArea; }
|
||||||
|
public Long getThePlatoon() { return thePlatoon; }
|
||||||
|
public void setThePlatoon(Long thePlatoon) { this.thePlatoon = thePlatoon; }
|
||||||
|
public Long getTheFloor() { return theFloor; }
|
||||||
|
public void setTheFloor(Long theFloor) { this.theFloor = theFloor; }
|
||||||
|
public String getDirectionType() { return directionType; }
|
||||||
|
public void setDirectionType(String directionType) { this.directionType = directionType; }
|
||||||
|
public Integer getFullTrayCount() { return fullTrayCount; }
|
||||||
|
public void setFullTrayCount(Integer fullTrayCount) { this.fullTrayCount = fullTrayCount; }
|
||||||
|
public Integer getEmptyLocationCount() { return emptyLocationCount; }
|
||||||
|
public void setEmptyLocationCount(Integer emptyLocationCount) { this.emptyLocationCount = emptyLocationCount; }
|
||||||
|
public BigDecimal getTotalQuantity() { return totalQuantity; }
|
||||||
|
public void setTotalQuantity(BigDecimal totalQuantity) { this.totalQuantity = totalQuantity; }
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.ruoyi.out.dense.domain;
|
package com.ruoyi.out.dense.domain;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 密集库通道货位及其几何信息。
|
* 密集库通道货位及其几何信息。
|
||||||
*/
|
*/
|
||||||
@@ -19,6 +21,7 @@ public class DenseLanePosition
|
|||||||
private String directionType;
|
private String directionType;
|
||||||
private Long minColumn;
|
private Long minColumn;
|
||||||
private Long maxColumn;
|
private Long maxColumn;
|
||||||
|
private BigDecimal stockQuantity;
|
||||||
|
|
||||||
public String getCargoSpaceId() { return cargoSpaceId; }
|
public String getCargoSpaceId() { return cargoSpaceId; }
|
||||||
public void setCargoSpaceId(String cargoSpaceId) { this.cargoSpaceId = cargoSpaceId; }
|
public void setCargoSpaceId(String cargoSpaceId) { this.cargoSpaceId = cargoSpaceId; }
|
||||||
@@ -48,4 +51,6 @@ public class DenseLanePosition
|
|||||||
public void setMinColumn(Long minColumn) { this.minColumn = minColumn; }
|
public void setMinColumn(Long minColumn) { this.minColumn = minColumn; }
|
||||||
public Long getMaxColumn() { return maxColumn; }
|
public Long getMaxColumn() { return maxColumn; }
|
||||||
public void setMaxColumn(Long maxColumn) { this.maxColumn = maxColumn; }
|
public void setMaxColumn(Long maxColumn) { this.maxColumn = maxColumn; }
|
||||||
|
public BigDecimal getStockQuantity() { return stockQuantity; }
|
||||||
|
public void setStockQuantity(BigDecimal stockQuantity) { this.stockQuantity = stockQuantity; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.ruoyi.out.dense.mapper;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import com.ruoyi.bas.cargospace.domain.BasCargoSpace;
|
import com.ruoyi.bas.cargospace.domain.BasCargoSpace;
|
||||||
import com.ruoyi.out.dense.domain.DenseLanePosition;
|
import com.ruoyi.out.dense.domain.DenseLanePosition;
|
||||||
|
import com.ruoyi.out.dense.domain.DenseLaneOption;
|
||||||
import com.ruoyi.out.dense.domain.RelocationTargetQuery;
|
import com.ruoyi.out.dense.domain.RelocationTargetQuery;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
@@ -20,4 +21,7 @@ public interface DenseLaneMapper
|
|||||||
List<DenseLanePosition> selectBlockersForUpdate(@Param("cargoSpaceId") String cargoSpaceId);
|
List<DenseLanePosition> selectBlockersForUpdate(@Param("cargoSpaceId") String cargoSpaceId);
|
||||||
|
|
||||||
BasCargoSpace selectRelocationTargetForUpdate(RelocationTargetQuery query);
|
BasCargoSpace selectRelocationTargetForUpdate(RelocationTargetQuery query);
|
||||||
|
|
||||||
|
List<DenseLaneOption> selectLaneOptions(@Param("warehouseId") String warehouseId,
|
||||||
|
@Param("theFloor") Long theFloor);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,6 +219,53 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
target_space.cargo_space_id asc
|
target_space.cargo_space_id asc
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectLaneOptions" resultType="com.ruoyi.out.dense.domain.DenseLaneOption">
|
||||||
|
;with reserved_targets as
|
||||||
|
(
|
||||||
|
select distinct target_cargo_space_id as cargo_space_id
|
||||||
|
from mydo_job
|
||||||
|
where status in ('1', '2', '3')
|
||||||
|
and target_cargo_space_id is not null
|
||||||
|
), stock_totals as
|
||||||
|
(
|
||||||
|
select stock_space.group_coding,
|
||||||
|
stock_space.the_floor,
|
||||||
|
sum(isnull(detail.quantity, 0)) as total_quantity
|
||||||
|
from bas_cargo_space stock_space
|
||||||
|
inner join bas_stock stock on stock.cargo_space_id = stock_space.cargo_space_id
|
||||||
|
inner join bas_stock_detail detail on detail.stock_id = stock.stock_id
|
||||||
|
where stock_space.warehouse_id = #{warehouseId}
|
||||||
|
and stock_space.the_floor = #{theFloor}
|
||||||
|
group by stock_space.group_coding, stock_space.the_floor
|
||||||
|
)
|
||||||
|
select lane.group_coding,
|
||||||
|
min(lane.this_cargo_area) as cargo_area,
|
||||||
|
min(lane.the_platoon) as the_platoon,
|
||||||
|
lane.the_floor,
|
||||||
|
isnull(min(config.direction_type), '1') as direction_type,
|
||||||
|
sum(case when lane.status = '2' and lane.tray_code is not null then 1 else 0 end) as full_tray_count,
|
||||||
|
sum(case when lane.status = '1'
|
||||||
|
and isnull(lane.locked, 'N') = 'N'
|
||||||
|
and lane.tray_code is null
|
||||||
|
and reserved.cargo_space_id is null then 1 else 0 end) as empty_location_count,
|
||||||
|
isnull(max(stock_totals.total_quantity), 0) as total_quantity
|
||||||
|
from bas_cargo_space lane
|
||||||
|
left join bas_cargo_area_config config
|
||||||
|
on config.warehouse_id = lane.warehouse_id
|
||||||
|
and config.cargo_area_code = lane.this_cargo_area
|
||||||
|
and config.is_active = '1'
|
||||||
|
left join reserved_targets reserved on reserved.cargo_space_id = lane.cargo_space_id
|
||||||
|
left join stock_totals
|
||||||
|
on stock_totals.group_coding = lane.group_coding
|
||||||
|
and stock_totals.the_floor = lane.the_floor
|
||||||
|
where lane.warehouse_id = #{warehouseId}
|
||||||
|
and lane.the_floor = #{theFloor}
|
||||||
|
and lane.group_coding is not null
|
||||||
|
and ltrim(rtrim(lane.group_coding)) != ''
|
||||||
|
group by lane.group_coding, lane.the_floor
|
||||||
|
order by lane.group_coding
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="isCurrentOutermost" resultType="java.lang.Integer">
|
<select id="isCurrentOutermost" resultType="java.lang.Integer">
|
||||||
select case
|
select case
|
||||||
when source_space.warehouse_id != '03'
|
when source_space.warehouse_id != '03'
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
cargo.tray_code,
|
cargo.tray_code,
|
||||||
isnull(config.direction_type, '1') as direction_type,
|
isnull(config.direction_type, '1') as direction_type,
|
||||||
boundary.min_column,
|
boundary.min_column,
|
||||||
boundary.max_column
|
boundary.max_column,
|
||||||
|
isnull(stock_total.stock_quantity, 0) as stock_quantity
|
||||||
from bas_cargo_space cargo with (UPDLOCK,HOLDLOCK)
|
from bas_cargo_space cargo with (UPDLOCK,HOLDLOCK)
|
||||||
left join bas_cargo_area_config config
|
left join bas_cargo_area_config config
|
||||||
on config.warehouse_id = cargo.warehouse_id
|
on config.warehouse_id = cargo.warehouse_id
|
||||||
@@ -63,6 +64,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
and lane.group_coding = cargo.group_coding
|
and lane.group_coding = cargo.group_coding
|
||||||
and lane.the_floor = cargo.the_floor
|
and lane.the_floor = cargo.the_floor
|
||||||
) boundary
|
) boundary
|
||||||
|
outer apply
|
||||||
|
(
|
||||||
|
select sum(isnull(detail.quantity, 0)) as stock_quantity
|
||||||
|
from bas_stock stock
|
||||||
|
inner join bas_stock_detail detail on detail.stock_id = stock.stock_id
|
||||||
|
where stock.cargo_space_id = cargo.cargo_space_id
|
||||||
|
and stock.tray_code = cargo.tray_code
|
||||||
|
) stock_total
|
||||||
where cargo.warehouse_id = #{warehouseId}
|
where cargo.warehouse_id = #{warehouseId}
|
||||||
and cargo.the_floor = #{theFloor}
|
and cargo.the_floor = #{theFloor}
|
||||||
and cargo.group_coding = #{sourceGroupCoding}
|
and cargo.group_coding = #{sourceGroupCoding}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package com.ruoyi.kpi.service.impl;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import com.ruoyi.bas.cargospace.mapper.BasCargoSpaceMapper;
|
||||||
|
import com.ruoyi.bas.stock.mapper.BasStockDetailMapper;
|
||||||
|
import com.ruoyi.in.job.mapper.MydoJobMapper;
|
||||||
|
import com.ruoyi.kpi.mapper.KpiMapper;
|
||||||
|
import com.ruoyi.out.dense.domain.DenseLaneOption;
|
||||||
|
import com.ruoyi.out.dense.mapper.DenseLaneMapper;
|
||||||
|
import com.ruoyi.out.relocation.domain.RelocationGenerateRequest;
|
||||||
|
import com.ruoyi.out.relocation.domain.RelocationPreview;
|
||||||
|
import com.ruoyi.out.relocation.service.IRelocationJobService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class KpiDenseRelocationServiceTest
|
||||||
|
{
|
||||||
|
@Mock private KpiMapper kpiMapper;
|
||||||
|
@Mock private BasCargoSpaceMapper basCargoSpaceMapper;
|
||||||
|
@Mock private BasStockDetailMapper basStockDetailMapper;
|
||||||
|
@Mock private MydoJobMapper mydoJobMapper;
|
||||||
|
@Mock private DenseLaneMapper denseLaneMapper;
|
||||||
|
@Mock private IRelocationJobService relocationJobService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private KpiServiceImpl service;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void denseRelocationOptionsOnlyAcceptWarehouse03()
|
||||||
|
{
|
||||||
|
RelocationGenerateRequest request = request("02", 1L, "A", null, 1);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.getDenseRelocationOptions(request))
|
||||||
|
.hasMessageContaining("03仓");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void optionsReturnFullSourceLanesAndSameFloorTargetsWithEnoughCapacity()
|
||||||
|
{
|
||||||
|
DenseLaneOption source = lane("A", 2L, 3, 0);
|
||||||
|
DenseLaneOption enough = lane("B", 2L, 0, 2);
|
||||||
|
DenseLaneOption insufficient = lane("C", 2L, 0, 1);
|
||||||
|
when(denseLaneMapper.selectLaneOptions("03", 2L))
|
||||||
|
.thenReturn(List.of(source, enough, insufficient));
|
||||||
|
RelocationGenerateRequest request = request("03", 2L, "A", null, 2);
|
||||||
|
|
||||||
|
Map<String, Object> result = service.getDenseRelocationOptions(request);
|
||||||
|
|
||||||
|
assertThat(result.get("sourceLanes")).asList().containsExactly(source);
|
||||||
|
assertThat(result.get("targetLanes")).asList().containsExactly(enough);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void previewReturnsExactOuterTraysAndInventoryTotal()
|
||||||
|
{
|
||||||
|
RelocationGenerateRequest request = request("03", 2L, "A", "B", 2);
|
||||||
|
RelocationPreview preview = new RelocationPreview();
|
||||||
|
preview.setTotalQuantity(new BigDecimal("25"));
|
||||||
|
when(relocationJobService.preview(request)).thenReturn(preview);
|
||||||
|
|
||||||
|
assertThat(service.previewDenseRelocation(request).getTotalQuantity())
|
||||||
|
.isEqualByComparingTo("25");
|
||||||
|
verify(relocationJobService).preview(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void generateRevalidatesThroughTransactionalRelocationService()
|
||||||
|
{
|
||||||
|
RelocationGenerateRequest request = request("03", 2L, "A", "B", 2);
|
||||||
|
when(relocationJobService.generate(request)).thenReturn(List.of("MOVE-1", "MOVE-2"));
|
||||||
|
|
||||||
|
assertThat(service.generateDenseRelocation(request))
|
||||||
|
.containsExactly("MOVE-1", "MOVE-2");
|
||||||
|
verify(relocationJobService).generate(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RelocationGenerateRequest request(String warehouseId, Long floor,
|
||||||
|
String source, String target, int count)
|
||||||
|
{
|
||||||
|
RelocationGenerateRequest request = new RelocationGenerateRequest();
|
||||||
|
request.setWarehouseId(warehouseId);
|
||||||
|
request.setTheFloor(floor);
|
||||||
|
request.setSourceGroupCoding(source);
|
||||||
|
request.setTargetGroupCoding(target);
|
||||||
|
request.setTrayCount(count);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DenseLaneOption lane(String groupCoding, Long floor, int full, int empty)
|
||||||
|
{
|
||||||
|
DenseLaneOption option = new DenseLaneOption();
|
||||||
|
option.setGroupCoding(groupCoding);
|
||||||
|
option.setTheFloor(floor);
|
||||||
|
option.setFullTrayCount(full);
|
||||||
|
option.setEmptyLocationCount(empty);
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,6 +70,39 @@ export function getDenseChartData(query) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密集库指定移库通道选项
|
||||||
|
*/
|
||||||
|
export function getDenseRelocationOptions(query) {
|
||||||
|
return request({
|
||||||
|
url: '/kpi/denseRelocation/options',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预览密集库指定移库托盘
|
||||||
|
*/
|
||||||
|
export function previewDenseRelocation(data) {
|
||||||
|
return request({
|
||||||
|
url: '/kpi/denseRelocation/preview',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成密集库指定移库作业
|
||||||
|
*/
|
||||||
|
export function generateDenseRelocation(data) {
|
||||||
|
return request({
|
||||||
|
url: '/kpi/denseRelocation/generate',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 货位平面图-货位详情
|
* 货位平面图-货位详情
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -59,6 +59,16 @@
|
|||||||
@click="loadChartData"
|
@click="loadChartData"
|
||||||
>刷新</el-button>
|
>刷新</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
icon="el-icon-rank"
|
||||||
|
size="small"
|
||||||
|
data-testid="dense-relocation-open"
|
||||||
|
@click="openRelocationDialog"
|
||||||
|
>指定移库</el-button>
|
||||||
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -102,6 +112,123 @@
|
|||||||
:loading="trayLoading"
|
:loading="trayLoading"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
title="指定同层移库"
|
||||||
|
:visible.sync="relocationDialogVisible"
|
||||||
|
width="720px"
|
||||||
|
append-to-body
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
custom-class="dense-relocation-dialog"
|
||||||
|
@closed="resetRelocationDialog"
|
||||||
|
>
|
||||||
|
<div class="relocation-context">
|
||||||
|
<span>仓库 <strong>03 密集库</strong></span>
|
||||||
|
<span>当前层 <strong>{{ selectedDenseFloor }} 层</strong></span>
|
||||||
|
<span>规则 <strong>外侧优先 · 同层异通道</strong></span>
|
||||||
|
</div>
|
||||||
|
<el-form
|
||||||
|
ref="relocationForm"
|
||||||
|
:model="relocationForm"
|
||||||
|
:rules="relocationRules"
|
||||||
|
label-width="92px"
|
||||||
|
class="relocation-form"
|
||||||
|
>
|
||||||
|
<el-row :gutter="14">
|
||||||
|
<el-col :span="10">
|
||||||
|
<el-form-item label="源通道" prop="sourceGroupCoding">
|
||||||
|
<el-select
|
||||||
|
v-model="relocationForm.sourceGroupCoding"
|
||||||
|
filterable
|
||||||
|
placeholder="选择有满托盘的通道"
|
||||||
|
style="width: 100%"
|
||||||
|
@change="handleRelocationConditionChange"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in relocationSourceLanes"
|
||||||
|
:key="item.groupCoding"
|
||||||
|
:label="laneLabel(item, 'source')"
|
||||||
|
:value="item.groupCoding"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item label="托盘数量" prop="trayCount">
|
||||||
|
<el-input-number
|
||||||
|
v-model="relocationForm.trayCount"
|
||||||
|
:min="1"
|
||||||
|
:max="selectedSourceCapacity"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
@change="handleRelocationConditionChange"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="目标通道" prop="targetGroupCoding">
|
||||||
|
<el-select
|
||||||
|
v-model="relocationForm.targetGroupCoding"
|
||||||
|
filterable
|
||||||
|
placeholder="选择容量足够的通道"
|
||||||
|
style="width: 100%"
|
||||||
|
@change="invalidateRelocationPreview"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in relocationTargetLanes"
|
||||||
|
:key="item.groupCoding"
|
||||||
|
:label="laneLabel(item, 'target')"
|
||||||
|
:value="item.groupCoding"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div class="relocation-preview-head">
|
||||||
|
<div>
|
||||||
|
<strong>外侧托盘预览</strong>
|
||||||
|
<span v-if="relocationPreview.trays.length">
|
||||||
|
{{ relocationPreview.trays.length }} 托 / 库存总量 {{ relocationPreview.totalQuantity || 0 }}
|
||||||
|
</span>
|
||||||
|
<span v-else>选择通道后先预览,提交时后端会再次校验</span>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
icon="el-icon-view"
|
||||||
|
:loading="relocationPreviewLoading"
|
||||||
|
@click="handlePreviewRelocation"
|
||||||
|
>刷新预览</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
:data="relocationPreview.trays"
|
||||||
|
height="248"
|
||||||
|
size="mini"
|
||||||
|
border
|
||||||
|
empty-text="暂无预览数据"
|
||||||
|
data-testid="dense-relocation-preview"
|
||||||
|
>
|
||||||
|
<el-table-column type="index" label="顺序" width="58" align="center" />
|
||||||
|
<el-table-column prop="trayCode" label="托盘码" min-width="145" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="cargoSpaceName" label="源货位" min-width="170" show-overflow-tooltip>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ scope.row.cargoSpaceName || scope.row.cargoSpaceId }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="groupCoding" label="源通道" min-width="130" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="stockQuantity" label="库存数量" width="100" align="right" />
|
||||||
|
</el-table>
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="relocationDialogVisible = false">取消</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:disabled="!relocationPreviewValid"
|
||||||
|
:loading="relocationGenerating"
|
||||||
|
@click="handleGenerateRelocation"
|
||||||
|
>生成移库作业</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -109,7 +236,13 @@
|
|||||||
import { listWarehouse } from '@/api/bas/warehouse'
|
import { listWarehouse } from '@/api/bas/warehouse'
|
||||||
import { listRoadway } from '@/api/bas/roadway'
|
import { listRoadway } from '@/api/bas/roadway'
|
||||||
import { getPlatoons } from '@/api/bas/cargospace'
|
import { getPlatoons } from '@/api/bas/cargospace'
|
||||||
import { getChartData, getDenseChartData } from '@/api/kpi/kpi'
|
import {
|
||||||
|
getChartData,
|
||||||
|
getDenseChartData,
|
||||||
|
getDenseRelocationOptions,
|
||||||
|
previewDenseRelocation,
|
||||||
|
generateDenseRelocation
|
||||||
|
} from '@/api/kpi/kpi'
|
||||||
import DenseCargoSpaceMap from './components/DenseCargoSpaceMap.vue'
|
import DenseCargoSpaceMap from './components/DenseCargoSpaceMap.vue'
|
||||||
import TrayCargoSpaceMap from './components/TrayCargoSpaceMap.vue'
|
import TrayCargoSpaceMap from './components/TrayCargoSpaceMap.vue'
|
||||||
|
|
||||||
@@ -135,6 +268,26 @@ export default {
|
|||||||
denseFloors: [],
|
denseFloors: [],
|
||||||
denseLocations: [],
|
denseLocations: [],
|
||||||
selectedDenseFloor: 1,
|
selectedDenseFloor: 1,
|
||||||
|
relocationDialogVisible: false,
|
||||||
|
relocationOptionsLoading: false,
|
||||||
|
relocationPreviewLoading: false,
|
||||||
|
relocationGenerating: false,
|
||||||
|
relocationSourceLanes: [],
|
||||||
|
relocationTargetLanes: [],
|
||||||
|
relocationPreview: { trays: [], totalQuantity: 0 },
|
||||||
|
relocationPreviewSignature: '',
|
||||||
|
relocationForm: {
|
||||||
|
warehouseId: '03',
|
||||||
|
theFloor: 1,
|
||||||
|
sourceGroupCoding: '',
|
||||||
|
targetGroupCoding: '',
|
||||||
|
trayCount: 1
|
||||||
|
},
|
||||||
|
relocationRules: {
|
||||||
|
sourceGroupCoding: [{ required: true, message: '请选择源通道', trigger: 'change' }],
|
||||||
|
targetGroupCoding: [{ required: true, message: '请选择目标通道', trigger: 'change' }],
|
||||||
|
trayCount: [{ required: true, message: '请输入托盘数量', trigger: 'change' }]
|
||||||
|
},
|
||||||
trayLoading: false,
|
trayLoading: false,
|
||||||
trayLocations: [],
|
trayLocations: [],
|
||||||
trayColumns: [],
|
trayColumns: [],
|
||||||
@@ -151,6 +304,16 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
isDenseWarehouse() {
|
isDenseWarehouse() {
|
||||||
return String(this.queryParams.warehouseId) === '03'
|
return String(this.queryParams.warehouseId) === '03'
|
||||||
|
},
|
||||||
|
selectedSourceCapacity() {
|
||||||
|
const selected = this.relocationSourceLanes.find(
|
||||||
|
item => item.groupCoding === this.relocationForm.sourceGroupCoding
|
||||||
|
)
|
||||||
|
return selected && selected.fullTrayCount ? Number(selected.fullTrayCount) : 1
|
||||||
|
},
|
||||||
|
relocationPreviewValid() {
|
||||||
|
return this.relocationPreview.trays.length === Number(this.relocationForm.trayCount) &&
|
||||||
|
this.relocationPreviewSignature === this.buildRelocationSignature()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -291,6 +454,90 @@ export default {
|
|||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.trayLoading = false
|
this.trayLoading = false
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
openRelocationDialog() {
|
||||||
|
this.relocationForm = {
|
||||||
|
warehouseId: '03',
|
||||||
|
theFloor: Number(this.selectedDenseFloor),
|
||||||
|
sourceGroupCoding: '',
|
||||||
|
targetGroupCoding: '',
|
||||||
|
trayCount: 1
|
||||||
|
}
|
||||||
|
this.invalidateRelocationPreview()
|
||||||
|
this.relocationDialogVisible = true
|
||||||
|
this.loadRelocationOptions()
|
||||||
|
},
|
||||||
|
resetRelocationDialog() {
|
||||||
|
this.relocationSourceLanes = []
|
||||||
|
this.relocationTargetLanes = []
|
||||||
|
this.invalidateRelocationPreview()
|
||||||
|
if (this.$refs.relocationForm) this.$refs.relocationForm.clearValidate()
|
||||||
|
},
|
||||||
|
loadRelocationOptions() {
|
||||||
|
this.relocationOptionsLoading = true
|
||||||
|
getDenseRelocationOptions(this.relocationForm).then(response => {
|
||||||
|
const data = response.data || {}
|
||||||
|
this.relocationSourceLanes = data.sourceLanes || []
|
||||||
|
this.relocationTargetLanes = data.targetLanes || []
|
||||||
|
if (this.relocationForm.targetGroupCoding &&
|
||||||
|
!this.relocationTargetLanes.some(item => item.groupCoding === this.relocationForm.targetGroupCoding)) {
|
||||||
|
this.relocationForm.targetGroupCoding = ''
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.relocationOptionsLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleRelocationConditionChange() {
|
||||||
|
if (this.relocationForm.trayCount > this.selectedSourceCapacity) {
|
||||||
|
this.relocationForm.trayCount = this.selectedSourceCapacity
|
||||||
|
}
|
||||||
|
this.relocationForm.targetGroupCoding = ''
|
||||||
|
this.invalidateRelocationPreview()
|
||||||
|
this.loadRelocationOptions()
|
||||||
|
},
|
||||||
|
invalidateRelocationPreview() {
|
||||||
|
this.relocationPreview = { trays: [], totalQuantity: 0 }
|
||||||
|
this.relocationPreviewSignature = ''
|
||||||
|
},
|
||||||
|
buildRelocationSignature() {
|
||||||
|
const form = this.relocationForm
|
||||||
|
return [form.warehouseId, form.theFloor, form.sourceGroupCoding,
|
||||||
|
form.targetGroupCoding, form.trayCount].join('|')
|
||||||
|
},
|
||||||
|
laneLabel(item, type) {
|
||||||
|
const count = type === 'source' ? item.fullTrayCount : item.emptyLocationCount
|
||||||
|
return `${item.groupCoding}(${type === 'source' ? '满托' : '空位'} ${count || 0})`
|
||||||
|
},
|
||||||
|
handlePreviewRelocation() {
|
||||||
|
this.$refs.relocationForm.validate(valid => {
|
||||||
|
if (!valid) return
|
||||||
|
this.relocationPreviewLoading = true
|
||||||
|
previewDenseRelocation(this.relocationForm).then(response => {
|
||||||
|
this.relocationPreview = response.data || { trays: [], totalQuantity: 0 }
|
||||||
|
this.relocationPreviewSignature = this.buildRelocationSignature()
|
||||||
|
}).finally(() => {
|
||||||
|
this.relocationPreviewLoading = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleGenerateRelocation() {
|
||||||
|
if (!this.relocationPreviewValid) {
|
||||||
|
this.$modal.msgWarning('预览已失效,请重新预览')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$modal.confirm(
|
||||||
|
`确认将外侧 ${this.relocationForm.trayCount} 个托盘移至通道 ${this.relocationForm.targetGroupCoding}?`
|
||||||
|
).then(() => {
|
||||||
|
this.relocationGenerating = true
|
||||||
|
return generateDenseRelocation(this.relocationForm)
|
||||||
|
}).then(response => {
|
||||||
|
const ids = response.data || []
|
||||||
|
this.$modal.msgSuccess(`已生成 ${ids.length} 个移库作业`)
|
||||||
|
this.relocationDialogVisible = false
|
||||||
|
this.loadDenseChartData()
|
||||||
|
}).finally(() => {
|
||||||
|
this.relocationGenerating = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -320,4 +567,53 @@ export default {
|
|||||||
.chart-container--tray {
|
.chart-container--tray {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.relocation-context {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 18px;
|
||||||
|
margin: -6px 0 18px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
border-left: 3px solid #2c7a8f;
|
||||||
|
color: #627789;
|
||||||
|
background: #f2f7f9;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relocation-context strong {
|
||||||
|
margin-left: 4px;
|
||||||
|
color: #183f52;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relocation-form ::v-deep .el-form-item {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relocation-preview-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 0 2px;
|
||||||
|
border-top: 1px solid #e3e9ee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relocation-preview-head strong {
|
||||||
|
margin-right: 10px;
|
||||||
|
color: #213f52;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relocation-preview-head span {
|
||||||
|
color: #7a8d9c;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.relocation-context {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user