feat: add dense lane outbound workflow
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
package com.ruoyi.out.dense.controller;
|
||||
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.out.dense.domain.DenseLaneOutboundQuery;
|
||||
import com.ruoyi.out.dense.domain.DenseLaneOutboundRequest;
|
||||
import com.ruoyi.out.dense.service.IDenseLaneOutboundService;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
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.RestController;
|
||||
|
||||
/**
|
||||
* 指定密集库通道出库接口。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/out/denseLaneOutbound")
|
||||
public class DenseLaneOutboundController extends BaseController
|
||||
{
|
||||
private final IDenseLaneOutboundService service;
|
||||
|
||||
public DenseLaneOutboundController(IDenseLaneOutboundService service)
|
||||
{
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('out:denseLaneOutbound:list')")
|
||||
@GetMapping("/list")
|
||||
public AjaxResult list(DenseLaneOutboundQuery query)
|
||||
{
|
||||
return success(service.list(query));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('out:denseLaneOutbound:preview')")
|
||||
@GetMapping("/preview")
|
||||
public AjaxResult preview(DenseLaneOutboundRequest request)
|
||||
{
|
||||
return success(service.preview(request));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('out:denseLaneOutbound:generate')")
|
||||
@PostMapping("/generate")
|
||||
public AjaxResult generate(@RequestBody DenseLaneOutboundRequest request)
|
||||
{
|
||||
return success(service.generate(request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.ruoyi.out.dense.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 指定通道出库托盘预览。
|
||||
*/
|
||||
public class DenseLaneOutboundPreview
|
||||
{
|
||||
private List<DenseLanePosition> trays = new ArrayList<>();
|
||||
private BigDecimal totalQuantity = BigDecimal.ZERO;
|
||||
private String previewToken;
|
||||
|
||||
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; }
|
||||
public String getPreviewToken() { return previewToken; }
|
||||
public void setPreviewToken(String previewToken) { this.previewToken = previewToken; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.ruoyi.out.dense.domain;
|
||||
|
||||
/**
|
||||
* 指定通道出库列表查询。
|
||||
*/
|
||||
public class DenseLaneOutboundQuery
|
||||
{
|
||||
private String warehouseId = "03";
|
||||
private Long theFloor;
|
||||
private String groupCoding;
|
||||
|
||||
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 getGroupCoding() { return groupCoding; }
|
||||
public void setGroupCoding(String groupCoding) { this.groupCoding = groupCoding; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.ruoyi.out.dense.domain;
|
||||
|
||||
/**
|
||||
* 指定通道整托出库预览/生成请求。
|
||||
*/
|
||||
public class DenseLaneOutboundRequest
|
||||
{
|
||||
private String warehouseId = "03";
|
||||
private Long theFloor;
|
||||
private String groupCoding;
|
||||
private Integer trayCount;
|
||||
private String exitId;
|
||||
private String previewToken;
|
||||
|
||||
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 getGroupCoding() { return groupCoding; }
|
||||
public void setGroupCoding(String groupCoding) { this.groupCoding = groupCoding; }
|
||||
public Integer getTrayCount() { return trayCount; }
|
||||
public void setTrayCount(Integer trayCount) { this.trayCount = trayCount; }
|
||||
public String getExitId() { return exitId; }
|
||||
public void setExitId(String exitId) { this.exitId = exitId; }
|
||||
public String getPreviewToken() { return previewToken; }
|
||||
public void setPreviewToken(String previewToken) { this.previewToken = previewToken; }
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.ruoyi.out.dense.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.ruoyi.in.jobdetail.domain.MydoJobDetail;
|
||||
|
||||
/**
|
||||
* 密集库通道货位及其几何信息。
|
||||
@@ -22,6 +25,7 @@ public class DenseLanePosition
|
||||
private Long minColumn;
|
||||
private Long maxColumn;
|
||||
private BigDecimal stockQuantity;
|
||||
private List<MydoJobDetail> stockDetails = new ArrayList<>();
|
||||
|
||||
public String getCargoSpaceId() { return cargoSpaceId; }
|
||||
public void setCargoSpaceId(String cargoSpaceId) { this.cargoSpaceId = cargoSpaceId; }
|
||||
@@ -53,4 +57,6 @@ public class DenseLanePosition
|
||||
public void setMaxColumn(Long maxColumn) { this.maxColumn = maxColumn; }
|
||||
public BigDecimal getStockQuantity() { return stockQuantity; }
|
||||
public void setStockQuantity(BigDecimal stockQuantity) { this.stockQuantity = stockQuantity; }
|
||||
public List<MydoJobDetail> getStockDetails() { return stockDetails; }
|
||||
public void setStockDetails(List<MydoJobDetail> stockDetails) { this.stockDetails = stockDetails; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.ruoyi.out.dense.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 03仓密集库通道整托库存汇总。
|
||||
*/
|
||||
public class DenseLaneSummary
|
||||
{
|
||||
private String groupCoding;
|
||||
private String cargoArea;
|
||||
private Long thePlatoon;
|
||||
private Long theFloor;
|
||||
private String directionType;
|
||||
private Integer fullTrayCount;
|
||||
private Integer availableTrayCount;
|
||||
private BigDecimal totalQuantity;
|
||||
private String outermostTrayCode;
|
||||
private String outermostCargoSpaceId;
|
||||
|
||||
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 getAvailableTrayCount() { return availableTrayCount; }
|
||||
public void setAvailableTrayCount(Integer availableTrayCount) { this.availableTrayCount = availableTrayCount; }
|
||||
public BigDecimal getTotalQuantity() { return totalQuantity; }
|
||||
public void setTotalQuantity(BigDecimal totalQuantity) { this.totalQuantity = totalQuantity; }
|
||||
public String getOutermostTrayCode() { return outermostTrayCode; }
|
||||
public void setOutermostTrayCode(String outermostTrayCode) { this.outermostTrayCode = outermostTrayCode; }
|
||||
public String getOutermostCargoSpaceId() { return outermostCargoSpaceId; }
|
||||
public void setOutermostCargoSpaceId(String outermostCargoSpaceId) { this.outermostCargoSpaceId = outermostCargoSpaceId; }
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import java.util.List;
|
||||
import com.ruoyi.bas.cargospace.domain.BasCargoSpace;
|
||||
import com.ruoyi.out.dense.domain.DenseLanePosition;
|
||||
import com.ruoyi.out.dense.domain.DenseLaneOption;
|
||||
import com.ruoyi.out.dense.domain.DenseLaneOutboundQuery;
|
||||
import com.ruoyi.out.dense.domain.DenseLaneSummary;
|
||||
import com.ruoyi.out.dense.domain.RelocationTargetQuery;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
@@ -24,4 +26,6 @@ public interface DenseLaneMapper
|
||||
|
||||
List<DenseLaneOption> selectLaneOptions(@Param("warehouseId") String warehouseId,
|
||||
@Param("theFloor") Long theFloor);
|
||||
|
||||
List<DenseLaneSummary> selectLaneSummaries(DenseLaneOutboundQuery query);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ruoyi.out.dense.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.out.dense.domain.DenseLaneOutboundPreview;
|
||||
import com.ruoyi.out.dense.domain.DenseLaneOutboundQuery;
|
||||
import com.ruoyi.out.dense.domain.DenseLaneOutboundRequest;
|
||||
import com.ruoyi.out.dense.domain.DenseLaneSummary;
|
||||
|
||||
/**
|
||||
* 指定密集库通道整托出库服务。
|
||||
*/
|
||||
public interface IDenseLaneOutboundService
|
||||
{
|
||||
List<DenseLaneSummary> list(DenseLaneOutboundQuery query);
|
||||
|
||||
DenseLaneOutboundPreview preview(DenseLaneOutboundRequest request);
|
||||
|
||||
List<String> generate(DenseLaneOutboundRequest request);
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
package com.ruoyi.out.dense.service.impl;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import com.ruoyi.bas.cargospace.mapper.BasCargoSpaceMapper;
|
||||
import com.ruoyi.bas.id.job.service.JobIdService;
|
||||
import com.ruoyi.bas.id.taskno.service.TaskNoService;
|
||||
import com.ruoyi.bas.stationpoint.domain.BasStationPoint;
|
||||
import com.ruoyi.bas.stationpoint.service.IBasStationPointService;
|
||||
import com.ruoyi.bas.stock.mapper.BasStockMapper;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.in.job.domain.MydoJob;
|
||||
import com.ruoyi.in.job.service.IMydoJobService;
|
||||
import com.ruoyi.in.jobdetail.domain.MydoJobDetail;
|
||||
import com.ruoyi.in.jobdetail.service.IMydoJobDetailService;
|
||||
import com.ruoyi.out.dense.domain.DenseLaneOutboundPreview;
|
||||
import com.ruoyi.out.dense.domain.DenseLaneOutboundQuery;
|
||||
import com.ruoyi.out.dense.domain.DenseLaneOutboundRequest;
|
||||
import com.ruoyi.out.dense.domain.DenseLanePosition;
|
||||
import com.ruoyi.out.dense.domain.DenseLaneSummary;
|
||||
import com.ruoyi.out.dense.mapper.DenseLaneMapper;
|
||||
import com.ruoyi.out.dense.service.IDenseLaneOutboundService;
|
||||
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.location.mapper.LocationOutboundMapper;
|
||||
import com.ruoyi.out.relocation.domain.RelocationGenerateRequest;
|
||||
import com.ruoyi.out.relocation.mapper.RelocationJobMapper;
|
||||
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 DenseLaneOutboundServiceImpl implements IDenseLaneOutboundService
|
||||
{
|
||||
private final DenseLaneMapper denseLaneMapper;
|
||||
private final IDenseLaneService denseLaneService;
|
||||
private final RelocationJobMapper relocationJobMapper;
|
||||
private final IBasStationPointService stationPointService;
|
||||
private final IMydoJobService mydoJobService;
|
||||
private final IMydoJobDetailService mydoJobDetailService;
|
||||
private final LocationOutboundMapper locationOutboundMapper;
|
||||
private final BasStockMapper basStockMapper;
|
||||
private final BasCargoSpaceMapper cargoSpaceMapper;
|
||||
private final JobIdService jobIdService;
|
||||
private final TaskNoService taskNoService;
|
||||
private final IOutboundDispatchService dispatchService;
|
||||
|
||||
public DenseLaneOutboundServiceImpl(DenseLaneMapper denseLaneMapper,
|
||||
IDenseLaneService denseLaneService,
|
||||
RelocationJobMapper relocationJobMapper,
|
||||
IBasStationPointService stationPointService,
|
||||
IMydoJobService mydoJobService,
|
||||
IMydoJobDetailService mydoJobDetailService,
|
||||
LocationOutboundMapper locationOutboundMapper,
|
||||
BasStockMapper basStockMapper,
|
||||
BasCargoSpaceMapper cargoSpaceMapper,
|
||||
JobIdService jobIdService,
|
||||
TaskNoService taskNoService,
|
||||
IOutboundDispatchService dispatchService)
|
||||
{
|
||||
this.denseLaneMapper = denseLaneMapper;
|
||||
this.denseLaneService = denseLaneService;
|
||||
this.relocationJobMapper = relocationJobMapper;
|
||||
this.stationPointService = stationPointService;
|
||||
this.mydoJobService = mydoJobService;
|
||||
this.mydoJobDetailService = mydoJobDetailService;
|
||||
this.locationOutboundMapper = locationOutboundMapper;
|
||||
this.basStockMapper = basStockMapper;
|
||||
this.cargoSpaceMapper = cargoSpaceMapper;
|
||||
this.jobIdService = jobIdService;
|
||||
this.taskNoService = taskNoService;
|
||||
this.dispatchService = dispatchService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DenseLaneSummary> list(DenseLaneOutboundQuery query)
|
||||
{
|
||||
if (query == null) {
|
||||
query = new DenseLaneOutboundQuery();
|
||||
}
|
||||
if (!"03".equals(query.getWarehouseId())) {
|
||||
throw new RuntimeException("指定通道出库仅支持03仓密集库");
|
||||
}
|
||||
List<DenseLaneSummary> summaries = denseLaneMapper.selectLaneSummaries(query);
|
||||
return summaries == null ? new ArrayList<>() : summaries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DenseLaneOutboundPreview preview(DenseLaneOutboundRequest request)
|
||||
{
|
||||
validateRequest(request, false);
|
||||
return buildPreview(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<String> generate(DenseLaneOutboundRequest request)
|
||||
{
|
||||
validateRequest(request, true);
|
||||
DenseLaneOutboundPreview current = buildPreview(request);
|
||||
if (!request.getPreviewToken().equals(current.getPreviewToken())) {
|
||||
throw new RuntimeException("预览已失效,请刷新后重新确认");
|
||||
}
|
||||
BasStationPoint exit = validateExit(request.getExitId(), current.getTrays());
|
||||
List<String> jobIds = new ArrayList<>();
|
||||
for (DenseLanePosition position : current.getTrays()) {
|
||||
List<MydoJobDetail> stockDetails = relocationJobMapper.selectStockDetailsForUpdate(
|
||||
position.getCargoSpaceId(), position.getTrayCode());
|
||||
if (stockDetails == null || stockDetails.isEmpty()) {
|
||||
throw new RuntimeException("托盘" + position.getTrayCode() + "没有可出库库存明细");
|
||||
}
|
||||
String jobId = jobIdService.getId("CK");
|
||||
if (!StringUtils.hasText(jobId)) {
|
||||
throw new RuntimeException("指定通道出库作业ID生成失败");
|
||||
}
|
||||
MydoJob job = buildJob(jobId, taskNoService.getTaskNo("20"), position, exit);
|
||||
if (mydoJobService.insertMydoJob(job) != 1) {
|
||||
throw new RuntimeException("指定通道出库作业生成失败");
|
||||
}
|
||||
int index = 1;
|
||||
for (MydoJobDetail template : stockDetails) {
|
||||
MydoJobDetail detail = buildDetail(template, jobId, index++);
|
||||
if (mydoJobDetailService.insertMydoJobDetail(detail) != 1) {
|
||||
throw new RuntimeException("指定通道出库作业明细生成失败");
|
||||
}
|
||||
HashMap<String, Object> reserve = new HashMap<>();
|
||||
reserve.put("stockDetailId", template.getStockinDetailId());
|
||||
reserve.put("stockId", template.getStockId());
|
||||
reserve.put("jobDetailId", detail.getJobDetailId());
|
||||
reserve.put("quantity", detail.getOutQuantity());
|
||||
if (locationOutboundMapper.reserveStockDetailForOutbound(reserve) != 1) {
|
||||
throw new RuntimeException("托盘库存状态已变化,请刷新后重试");
|
||||
}
|
||||
}
|
||||
stockDetails.stream().map(MydoJobDetail::getStockId).filter(StringUtils::hasText)
|
||||
.distinct().forEach(basStockMapper::refreshBasStockSummary);
|
||||
if (cargoSpaceMapper.markOutboundAssignedByCargoSpaceId(position.getCargoSpaceId()) != 1) {
|
||||
throw new RuntimeException("端头货位状态已变化,请刷新后重试");
|
||||
}
|
||||
jobIds.add(jobId);
|
||||
}
|
||||
afterCommit(() -> jobIds.forEach(id -> dispatchService.dispatch(id, DispatchTrigger.INITIAL)));
|
||||
return jobIds;
|
||||
}
|
||||
|
||||
private DenseLaneOutboundPreview buildPreview(DenseLaneOutboundRequest request)
|
||||
{
|
||||
RelocationGenerateRequest sourceQuery = new RelocationGenerateRequest();
|
||||
sourceQuery.setWarehouseId(request.getWarehouseId());
|
||||
sourceQuery.setTheFloor(request.getTheFloor());
|
||||
sourceQuery.setSourceGroupCoding(request.getGroupCoding());
|
||||
sourceQuery.setTrayCount(request.getTrayCount());
|
||||
List<DenseLanePosition> available = relocationJobMapper.selectRelocationSourcesForUpdate(sourceQuery);
|
||||
if (available == null || available.size() < request.getTrayCount()) {
|
||||
throw new RuntimeException("当前通道可出库托盘数量不足");
|
||||
}
|
||||
List<DenseLanePosition> selected = denseLaneService.orderOutermostFirst(available)
|
||||
.stream().limit(request.getTrayCount()).collect(Collectors.toList());
|
||||
selected.forEach(position -> {
|
||||
List<MydoJobDetail> details = relocationJobMapper.selectStockDetailsForUpdate(
|
||||
position.getCargoSpaceId(), position.getTrayCode());
|
||||
position.setStockDetails(details == null ? new ArrayList<>() : details);
|
||||
});
|
||||
String[] trayCodes = selected.stream().map(DenseLanePosition::getTrayCode).toArray(String[]::new);
|
||||
BigDecimal total = relocationJobMapper.sumTrayStockQuantity(trayCodes);
|
||||
DenseLaneOutboundPreview preview = new DenseLaneOutboundPreview();
|
||||
preview.setTrays(selected);
|
||||
preview.setTotalQuantity(total == null ? BigDecimal.ZERO : total);
|
||||
preview.setPreviewToken(previewToken(request, selected));
|
||||
return preview;
|
||||
}
|
||||
|
||||
private void validateRequest(DenseLaneOutboundRequest request, boolean requireToken)
|
||||
{
|
||||
if (request == null || !"03".equals(request.getWarehouseId())) {
|
||||
throw new RuntimeException("指定通道出库仅支持03仓密集库");
|
||||
}
|
||||
if (request.getTheFloor() == null || !StringUtils.hasText(request.getGroupCoding())) {
|
||||
throw new RuntimeException("请选择密集库层和通道");
|
||||
}
|
||||
if (request.getTrayCount() == null || request.getTrayCount() <= 0) {
|
||||
throw new RuntimeException("出库托盘数量必须大于0");
|
||||
}
|
||||
if (requireToken && !StringUtils.hasText(request.getPreviewToken())) {
|
||||
throw new RuntimeException("请先预览要出库的端头托盘");
|
||||
}
|
||||
}
|
||||
|
||||
private BasStationPoint validateExit(String exitId, List<DenseLanePosition> positions)
|
||||
{
|
||||
if (!StringUtils.hasText(exitId)) {
|
||||
throw new RuntimeException("请选择出库口");
|
||||
}
|
||||
BasStationPoint point = stationPointService.selectBasStationPointByPointId(exitId);
|
||||
if (point == null || !"03".equals(point.getWarehouseId())) {
|
||||
throw new RuntimeException("出库口不存在或不属于03仓");
|
||||
}
|
||||
if ((!"2".equals(point.getPointType()) && !"3".equals(point.getPointType()))
|
||||
|| !"0".equals(point.getStatus())
|
||||
|| (!StringUtils.isEmpty(point.getPointStatus()) && !"0".equals(point.getPointStatus()))) {
|
||||
throw new RuntimeException("当前出库口不可用");
|
||||
}
|
||||
for (DenseLanePosition position : positions) {
|
||||
String trayType = trayType(position.getTrayCode());
|
||||
if (StringUtils.hasText(trayType) && StringUtils.hasText(point.getTrayType())
|
||||
&& !supportsTrayType(point.getTrayType(), trayType)) {
|
||||
throw new RuntimeException("出库口不支持托盘类型" + trayType);
|
||||
}
|
||||
}
|
||||
return point;
|
||||
}
|
||||
|
||||
private MydoJob buildJob(String jobId, String taskNo, DenseLanePosition position,
|
||||
BasStationPoint exit)
|
||||
{
|
||||
MydoJob job = new MydoJob();
|
||||
job.setJobId(jobId);
|
||||
job.setTaskNo(StringUtils.hasText(taskNo) ? taskNo : jobId);
|
||||
job.setInOrOut(20L);
|
||||
job.setStatus("1");
|
||||
job.setExecutionPhase("0");
|
||||
job.setTrayCode(position.getTrayCode());
|
||||
job.setWarehouseId("03");
|
||||
job.setCargoSpaceId(position.getCargoSpaceId());
|
||||
job.setCargoSpaceName(position.getCargoSpaceName());
|
||||
job.setExitId(exit.getPointId());
|
||||
job.setLineType("1");
|
||||
job.setPriority(10L);
|
||||
job.setCreateTime(DateUtils.getNowDate());
|
||||
return job;
|
||||
}
|
||||
|
||||
private MydoJobDetail buildDetail(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.setProductId(source.getProductId());
|
||||
detail.setProductName(source.getProductName());
|
||||
detail.setStockQuantity(source.getStockQuantity());
|
||||
detail.setOutQuantity(source.getStockQuantity());
|
||||
detail.setStockinId(source.getStockinId());
|
||||
detail.setStockinDetailId(source.getStockinDetailId());
|
||||
detail.setSupplierId(source.getSupplierId());
|
||||
detail.setSupplierName(source.getSupplierName());
|
||||
detail.setProjectNo(source.getProjectNo());
|
||||
return detail;
|
||||
}
|
||||
|
||||
private String previewToken(DenseLaneOutboundRequest request, List<DenseLanePosition> selected)
|
||||
{
|
||||
String payload = request.getWarehouseId() + "|" + request.getTheFloor() + "|"
|
||||
+ request.getGroupCoding() + "|" + request.getTrayCount() + "|"
|
||||
+ selected.stream().map(item -> item.getCargoSpaceId() + ":" + item.getTrayCode()
|
||||
+ "[" + item.getStockDetails().stream()
|
||||
.map(detail -> String.valueOf(detail.getStockinDetailId()) + ":"
|
||||
+ String.valueOf(detail.getStockId()) + ":"
|
||||
+ String.valueOf(detail.getStockQuantity()))
|
||||
.sorted()
|
||||
.collect(Collectors.joining(";")) + "]")
|
||||
.collect(Collectors.joining(","));
|
||||
try {
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest(payload.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("生成通道出库预览签名失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String trayType(String trayCode)
|
||||
{
|
||||
return StringUtils.hasText(trayCode) ? trayCode.trim().substring(0, 1).toUpperCase() : null;
|
||||
}
|
||||
|
||||
private boolean supportsTrayType(String supported, String trayType)
|
||||
{
|
||||
for (String item : supported.split(",")) {
|
||||
if (trayType.equalsIgnoreCase(item.trim())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void afterCommit(Runnable action)
|
||||
{
|
||||
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
action.run();
|
||||
return;
|
||||
}
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization()
|
||||
{
|
||||
@Override
|
||||
public void afterCommit()
|
||||
{
|
||||
action.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -185,6 +185,22 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
on area_config.warehouse_id = target_space.warehouse_id
|
||||
and area_config.cargo_area_code = target_space.this_cargo_area
|
||||
and area_config.is_active = '1'
|
||||
cross apply
|
||||
(
|
||||
select min(lane_boundary.the_column) as min_column,
|
||||
max(lane_boundary.the_column) as max_column
|
||||
from bas_cargo_space lane_boundary with (UPDLOCK,HOLDLOCK)
|
||||
where lane_boundary.warehouse_id = target_space.warehouse_id
|
||||
and lane_boundary.group_coding = target_space.group_coding
|
||||
and lane_boundary.the_floor = target_space.the_floor
|
||||
) boundary
|
||||
cross apply
|
||||
(
|
||||
select case when target_space.the_column - boundary.min_column
|
||||
<= boundary.max_column - target_space.the_column
|
||||
then target_space.the_column - boundary.min_column
|
||||
else boundary.max_column - target_space.the_column end as distance_to_edge
|
||||
) geometry
|
||||
where target_space.warehouse_id = #{warehouseId}
|
||||
and target_space.the_floor = #{theFloor}
|
||||
and target_space.group_coding != #{sourceGroupCoding}
|
||||
@@ -214,7 +230,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
case when area_config.direction_type = '2'
|
||||
then target_space.the_column end desc,
|
||||
case when area_config.direction_type = '3'
|
||||
then abs(target_space.the_column - 1) end asc,
|
||||
then geometry.distance_to_edge end asc,
|
||||
target_space.group_coding asc,
|
||||
target_space.cargo_space_id asc
|
||||
</select>
|
||||
@@ -266,6 +282,108 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
order by lane.group_coding
|
||||
</select>
|
||||
|
||||
<select id="selectLaneSummaries" resultType="com.ruoyi.out.dense.domain.DenseLaneSummary">
|
||||
;with lane_positions as
|
||||
(
|
||||
select cargo.cargo_space_id,
|
||||
cargo.warehouse_id,
|
||||
cargo.group_coding,
|
||||
cargo.this_cargo_area,
|
||||
cargo.the_platoon,
|
||||
cargo.the_column,
|
||||
cargo.the_floor,
|
||||
cargo.status,
|
||||
cargo.tray_code,
|
||||
isnull(config.direction_type, '1') as direction_type,
|
||||
min(cargo.the_column) over
|
||||
(partition by cargo.group_coding, cargo.the_floor) as min_column,
|
||||
max(cargo.the_column) over
|
||||
(partition by cargo.group_coding, cargo.the_floor) as max_column,
|
||||
case when exists
|
||||
(
|
||||
select 1 from mydo_job active_job
|
||||
where active_job.status in ('1', '2', '3')
|
||||
and active_job.in_or_out >= 20
|
||||
and (active_job.cargo_space_id = cargo.cargo_space_id
|
||||
or active_job.tray_code = cargo.tray_code)
|
||||
) then 1 else 0 end as has_active_job
|
||||
from bas_cargo_space cargo
|
||||
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'
|
||||
where cargo.warehouse_id = #{warehouseId}
|
||||
and cargo.group_coding is not null
|
||||
and ltrim(rtrim(cargo.group_coding)) != ''
|
||||
<if test="theFloor != null">
|
||||
and cargo.the_floor = #{theFloor}
|
||||
</if>
|
||||
<if test="groupCoding != null and groupCoding != ''">
|
||||
and cargo.group_coding like concat('%', #{groupCoding}, '%')
|
||||
</if>
|
||||
), lane_totals as
|
||||
(
|
||||
select group_coding,
|
||||
the_floor,
|
||||
min(this_cargo_area) as cargo_area,
|
||||
min(the_platoon) as the_platoon,
|
||||
min(direction_type) as direction_type,
|
||||
sum(case when status = '2' and tray_code is not null then 1 else 0 end) as full_tray_count,
|
||||
sum(case when status = '2' and tray_code is not null
|
||||
and has_active_job = 0 then 1 else 0 end) as available_tray_count
|
||||
from lane_positions
|
||||
group by group_coding, the_floor
|
||||
), stock_totals as
|
||||
(
|
||||
select position.group_coding,
|
||||
position.the_floor,
|
||||
sum(isnull(detail.quantity, 0)) as total_quantity
|
||||
from lane_positions position
|
||||
inner join bas_stock stock on stock.cargo_space_id = position.cargo_space_id
|
||||
inner join bas_stock_detail detail on detail.stock_id = stock.stock_id
|
||||
group by position.group_coding, position.the_floor
|
||||
)
|
||||
select totals.group_coding,
|
||||
totals.cargo_area,
|
||||
totals.the_platoon,
|
||||
totals.the_floor,
|
||||
totals.direction_type,
|
||||
totals.full_tray_count,
|
||||
totals.available_tray_count,
|
||||
isnull(stock_totals.total_quantity, 0) as total_quantity,
|
||||
outer_position.tray_code as outermost_tray_code,
|
||||
outer_position.cargo_space_id as outermost_cargo_space_id
|
||||
from lane_totals totals
|
||||
left join stock_totals
|
||||
on stock_totals.group_coding = totals.group_coding
|
||||
and stock_totals.the_floor = totals.the_floor
|
||||
outer apply
|
||||
(
|
||||
select top 1 position.tray_code,
|
||||
position.cargo_space_id,
|
||||
case when position.the_column - position.min_column
|
||||
<= position.max_column - position.the_column
|
||||
then position.the_column - position.min_column
|
||||
else position.max_column - position.the_column end as distance_to_edge
|
||||
from lane_positions position
|
||||
where position.group_coding = totals.group_coding
|
||||
and position.the_floor = totals.the_floor
|
||||
and position.status = '2'
|
||||
and position.tray_code is not null
|
||||
and position.has_active_job = 0
|
||||
order by case when totals.direction_type = '1' then position.the_column end desc,
|
||||
case when totals.direction_type = '2' then position.the_column end asc,
|
||||
case when totals.direction_type = '3' then
|
||||
case when position.the_column - position.min_column
|
||||
<= position.max_column - position.the_column
|
||||
then position.the_column - position.min_column
|
||||
else position.max_column - position.the_column end end asc,
|
||||
position.the_column asc
|
||||
) outer_position
|
||||
where totals.full_tray_count > 0
|
||||
order by totals.the_floor, totals.group_coding
|
||||
</select>
|
||||
|
||||
<select id="isCurrentOutermost" resultType="java.lang.Integer">
|
||||
select case
|
||||
when source_space.warehouse_id != '03'
|
||||
|
||||
@@ -31,6 +31,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where stock.cargo_space_id = #{cargoSpaceId}
|
||||
and stock.tray_code = #{trayCode}
|
||||
and isnull(detail.quantity, 0) > 0
|
||||
order by detail.detail_id
|
||||
</select>
|
||||
|
||||
<select id="selectRelocationSourcesForUpdate"
|
||||
|
||||
@@ -63,6 +63,25 @@ class DenseLaneMapperSqlTest
|
||||
.contains("not exists");
|
||||
}
|
||||
|
||||
@Test
|
||||
void directionThreeOrderingUsesActualLaneBoundaries() throws Exception
|
||||
{
|
||||
String xml = Files.readString(resolveMapperXml());
|
||||
String summarySql = section(xml, "<select id=\"selectLaneSummaries\"", "</select>");
|
||||
String targetSql = section(xml, "<select id=\"selectRelocationTargetForUpdate\"", "</select>");
|
||||
|
||||
assertThat(summarySql)
|
||||
.contains("min(cargo.the_column) over")
|
||||
.contains("max(cargo.the_column) over")
|
||||
.contains("distance_to_edge")
|
||||
.doesNotContain("abs(position.the_column - 1)");
|
||||
assertThat(targetSql)
|
||||
.contains("min(lane_boundary.the_column)")
|
||||
.contains("max(lane_boundary.the_column)")
|
||||
.contains("distance_to_edge")
|
||||
.doesNotContain("abs(target_space.the_column - 1)");
|
||||
}
|
||||
|
||||
private Path resolveMapperXml()
|
||||
{
|
||||
return Stream.of(
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
package com.ruoyi.out.dense.service.impl;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import com.ruoyi.bas.cargospace.mapper.BasCargoSpaceMapper;
|
||||
import com.ruoyi.bas.id.job.service.JobIdService;
|
||||
import com.ruoyi.bas.id.taskno.service.TaskNoService;
|
||||
import com.ruoyi.bas.stationpoint.domain.BasStationPoint;
|
||||
import com.ruoyi.bas.stationpoint.service.IBasStationPointService;
|
||||
import com.ruoyi.bas.stock.mapper.BasStockMapper;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.in.job.domain.MydoJob;
|
||||
import com.ruoyi.in.job.service.IMydoJobService;
|
||||
import com.ruoyi.in.jobdetail.domain.MydoJobDetail;
|
||||
import com.ruoyi.in.jobdetail.service.IMydoJobDetailService;
|
||||
import com.ruoyi.out.dense.domain.DenseLaneOutboundPreview;
|
||||
import com.ruoyi.out.dense.domain.DenseLaneOutboundQuery;
|
||||
import com.ruoyi.out.dense.domain.DenseLaneOutboundRequest;
|
||||
import com.ruoyi.out.dense.domain.DenseLanePosition;
|
||||
import com.ruoyi.out.dense.domain.DenseLaneSummary;
|
||||
import com.ruoyi.out.dense.mapper.DenseLaneMapper;
|
||||
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.location.mapper.LocationOutboundMapper;
|
||||
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.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class DenseLaneOutboundServiceImplTest
|
||||
{
|
||||
private DenseLaneMapper denseLaneMapper;
|
||||
private IDenseLaneService denseLaneService;
|
||||
private RelocationJobMapper relocationJobMapper;
|
||||
private IBasStationPointService stationPointService;
|
||||
private IMydoJobService mydoJobService;
|
||||
private IMydoJobDetailService mydoJobDetailService;
|
||||
private LocationOutboundMapper locationOutboundMapper;
|
||||
private BasStockMapper basStockMapper;
|
||||
private BasCargoSpaceMapper cargoSpaceMapper;
|
||||
private JobIdService jobIdService;
|
||||
private TaskNoService taskNoService;
|
||||
private IOutboundDispatchService dispatchService;
|
||||
private DenseLaneOutboundServiceImpl service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp()
|
||||
{
|
||||
denseLaneMapper = mock(DenseLaneMapper.class);
|
||||
denseLaneService = mock(IDenseLaneService.class);
|
||||
relocationJobMapper = mock(RelocationJobMapper.class);
|
||||
stationPointService = mock(IBasStationPointService.class);
|
||||
mydoJobService = mock(IMydoJobService.class);
|
||||
mydoJobDetailService = mock(IMydoJobDetailService.class);
|
||||
locationOutboundMapper = mock(LocationOutboundMapper.class);
|
||||
basStockMapper = mock(BasStockMapper.class);
|
||||
cargoSpaceMapper = mock(BasCargoSpaceMapper.class);
|
||||
jobIdService = mock(JobIdService.class);
|
||||
taskNoService = mock(TaskNoService.class);
|
||||
dispatchService = mock(IOutboundDispatchService.class);
|
||||
service = new DenseLaneOutboundServiceImpl(denseLaneMapper, denseLaneService,
|
||||
relocationJobMapper, stationPointService, mydoJobService, mydoJobDetailService,
|
||||
locationOutboundMapper, basStockMapper, cargoSpaceMapper, jobIdService,
|
||||
taskNoService, dispatchService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listOnlyAcceptsWarehouse03DenseLanes()
|
||||
{
|
||||
DenseLaneOutboundQuery query = new DenseLaneOutboundQuery();
|
||||
query.setWarehouseId("02");
|
||||
|
||||
assertThatThrownBy(() -> service.list(query)).hasMessageContaining("03仓");
|
||||
}
|
||||
|
||||
@Test
|
||||
void listReturnsFullLaneTotalsAndCurrentOuterTray()
|
||||
{
|
||||
DenseLaneOutboundQuery query = new DenseLaneOutboundQuery();
|
||||
query.setWarehouseId("03");
|
||||
DenseLaneSummary summary = new DenseLaneSummary();
|
||||
summary.setGroupCoding("G-01");
|
||||
summary.setFullTrayCount(3);
|
||||
summary.setAvailableTrayCount(2);
|
||||
summary.setTotalQuantity(new BigDecimal("40"));
|
||||
summary.setOutermostTrayCode("A001");
|
||||
when(denseLaneMapper.selectLaneSummaries(query)).thenReturn(List.of(summary));
|
||||
|
||||
assertThat(service.list(query)).containsExactly(summary);
|
||||
}
|
||||
|
||||
@Test
|
||||
void previewSelectsExactlyOutermostNUnreservedTrays()
|
||||
{
|
||||
DenseLaneOutboundRequest request = request(2);
|
||||
List<DenseLanePosition> positions = List.of(position("CS-2", 2L, "A002"),
|
||||
position("CS-3", 3L, "A003"), position("CS-1", 1L, "A001"));
|
||||
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(positions);
|
||||
when(denseLaneService.orderOutermostFirst(positions))
|
||||
.thenReturn(List.of(positions.get(1), positions.get(0), positions.get(2)));
|
||||
when(relocationJobMapper.sumTrayStockQuantity(any(String[].class)))
|
||||
.thenReturn(new BigDecimal("25"));
|
||||
|
||||
DenseLaneOutboundPreview preview = service.preview(request);
|
||||
|
||||
assertThat(preview.getTrays()).extracting(DenseLanePosition::getTrayCode)
|
||||
.containsExactly("A003", "A002");
|
||||
assertThat(preview.getTotalQuantity()).isEqualByComparingTo("25");
|
||||
assertThat(preview.getPreviewToken()).isNotBlank();
|
||||
}
|
||||
|
||||
@Test
|
||||
void previewIncludesCompleteInventoryDetailsForEverySelectedTray()
|
||||
{
|
||||
DenseLaneOutboundRequest request = request(1);
|
||||
DenseLanePosition position = position("CS-1", 1L, "A001");
|
||||
List<MydoJobDetail> details = List.of(
|
||||
detail("STOCK-1", "D-1", "P-1", "7"),
|
||||
detail("STOCK-1", "D-2", "P-2", "5"));
|
||||
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(position));
|
||||
when(denseLaneService.orderOutermostFirst(any())).thenReturn(List.of(position));
|
||||
when(relocationJobMapper.selectStockDetailsForUpdate("CS-1", "A001")).thenReturn(details);
|
||||
when(relocationJobMapper.sumTrayStockQuantity(any(String[].class)))
|
||||
.thenReturn(new BigDecimal("12"));
|
||||
|
||||
DenseLaneOutboundPreview preview = service.preview(request);
|
||||
|
||||
assertThat(preview.getTrays()).singleElement().satisfies(tray ->
|
||||
assertThat(tray.getStockDetails()).containsExactlyElementsOf(details));
|
||||
}
|
||||
|
||||
@Test
|
||||
void previewRejectsQuantityOverAvailableCapacity()
|
||||
{
|
||||
DenseLaneOutboundRequest request = request(2);
|
||||
when(relocationJobMapper.selectRelocationSourcesForUpdate(any()))
|
||||
.thenReturn(List.of(position("CS-1", 1L, "A001")));
|
||||
|
||||
assertThatThrownBy(() -> service.preview(request)).hasMessageContaining("可出库托盘数量不足");
|
||||
}
|
||||
|
||||
@Test
|
||||
void previewTokenChangesWhenSelectedTrayInventoryChanges()
|
||||
{
|
||||
DenseLaneOutboundRequest request = request(1);
|
||||
DenseLanePosition position = position("CS-1", 1L, "A001");
|
||||
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(position));
|
||||
when(denseLaneService.orderOutermostFirst(any())).thenReturn(List.of(position));
|
||||
when(relocationJobMapper.sumTrayStockQuantity(any(String[].class))).thenReturn(BigDecimal.TEN);
|
||||
when(relocationJobMapper.selectStockDetailsForUpdate("CS-1", "A001"))
|
||||
.thenReturn(List.of(detail("STOCK-1", "D-1", "P-1", "10")))
|
||||
.thenReturn(List.of(detail("STOCK-1", "D-1", "P-1", "9")));
|
||||
|
||||
String firstToken = service.preview(request).getPreviewToken();
|
||||
String changedToken = service.preview(request).getPreviewToken();
|
||||
|
||||
assertThat(changedToken).isNotEqualTo(firstToken);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generationRejectsStalePreview()
|
||||
{
|
||||
DenseLaneOutboundRequest request = request(1);
|
||||
DenseLanePosition first = position("CS-1", 1L, "A001");
|
||||
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(first));
|
||||
when(denseLaneService.orderOutermostFirst(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(relocationJobMapper.sumTrayStockQuantity(any(String[].class))).thenReturn(BigDecimal.TEN);
|
||||
request.setPreviewToken(service.preview(request).getPreviewToken());
|
||||
DenseLanePosition changed = position("CS-2", 2L, "A002");
|
||||
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(changed));
|
||||
|
||||
assertThatThrownBy(() -> service.generate(request)).hasMessageContaining("预览已失效");
|
||||
}
|
||||
|
||||
@Test
|
||||
void generationValidatesExitAndCreatesCompleteType20Jobs()
|
||||
{
|
||||
DenseLaneOutboundRequest request = request(1);
|
||||
DenseLanePosition position = position("CS-1", 1L, "A001");
|
||||
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(position));
|
||||
when(denseLaneService.orderOutermostFirst(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(relocationJobMapper.sumTrayStockQuantity(any(String[].class))).thenReturn(new BigDecimal("12"));
|
||||
List<MydoJobDetail> stockDetails = List.of(detail("STOCK-1", "D-1", "P-1", "7"),
|
||||
detail("STOCK-1", "D-2", "P-2", "5"));
|
||||
when(relocationJobMapper.selectStockDetailsForUpdate("CS-1", "A001"))
|
||||
.thenReturn(stockDetails);
|
||||
request.setPreviewToken(service.preview(request).getPreviewToken());
|
||||
BasStationPoint exit = new BasStationPoint();
|
||||
exit.setPointId("OUT-1");
|
||||
exit.setWarehouseId("03");
|
||||
exit.setPointType("2");
|
||||
exit.setStatus("0");
|
||||
exit.setPointStatus("0");
|
||||
exit.setTrayType("A,B");
|
||||
when(stationPointService.selectBasStationPointByPointId("OUT-1")).thenReturn(exit);
|
||||
when(jobIdService.getId("CK")).thenReturn("OUT-JOB-1");
|
||||
when(taskNoService.getTaskNo("20")).thenReturn("TASK-20-1");
|
||||
when(mydoJobService.insertMydoJob(any(MydoJob.class))).thenReturn(1);
|
||||
when(mydoJobDetailService.insertMydoJobDetail(any(MydoJobDetail.class))).thenReturn(1);
|
||||
when(locationOutboundMapper.reserveStockDetailForOutbound(any())).thenReturn(1);
|
||||
when(cargoSpaceMapper.markOutboundAssignedByCargoSpaceId("CS-1")).thenReturn(1);
|
||||
when(dispatchService.dispatch("OUT-JOB-1", DispatchTrigger.INITIAL))
|
||||
.thenReturn(AjaxResult.success());
|
||||
|
||||
List<String> jobIds = service.generate(request);
|
||||
|
||||
assertThat(jobIds).containsExactly("OUT-JOB-1");
|
||||
ArgumentCaptor<MydoJob> jobCaptor = ArgumentCaptor.forClass(MydoJob.class);
|
||||
verify(mydoJobService).insertMydoJob(jobCaptor.capture());
|
||||
assertThat(jobCaptor.getValue().getInOrOut()).isEqualTo(20L);
|
||||
assertThat(jobCaptor.getValue().getTrayCode()).isEqualTo("A001");
|
||||
verify(mydoJobDetailService, org.mockito.Mockito.times(2))
|
||||
.insertMydoJobDetail(any(MydoJobDetail.class));
|
||||
verify(dispatchService).dispatch("OUT-JOB-1", DispatchTrigger.INITIAL);
|
||||
}
|
||||
|
||||
private DenseLaneOutboundRequest request(int count)
|
||||
{
|
||||
DenseLaneOutboundRequest request = new DenseLaneOutboundRequest();
|
||||
request.setWarehouseId("03");
|
||||
request.setTheFloor(1L);
|
||||
request.setGroupCoding("G-01");
|
||||
request.setTrayCount(count);
|
||||
request.setExitId("OUT-1");
|
||||
return request;
|
||||
}
|
||||
|
||||
private DenseLanePosition position(String cargoSpaceId, Long column, String trayCode)
|
||||
{
|
||||
DenseLanePosition position = new DenseLanePosition();
|
||||
position.setCargoSpaceId(cargoSpaceId);
|
||||
position.setCargoSpaceName(cargoSpaceId);
|
||||
position.setWarehouseId("03");
|
||||
position.setGroupCoding("G-01");
|
||||
position.setTheFloor(1L);
|
||||
position.setTheColumn(column);
|
||||
position.setTrayCode(trayCode);
|
||||
position.setStockQuantity(BigDecimal.TEN);
|
||||
return position;
|
||||
}
|
||||
|
||||
private MydoJobDetail detail(String stockId, String detailId, String productId, String quantity)
|
||||
{
|
||||
MydoJobDetail detail = new MydoJobDetail();
|
||||
detail.setStockId(stockId);
|
||||
detail.setStockinDetailId(detailId);
|
||||
detail.setProductId(productId);
|
||||
detail.setProductName(productId);
|
||||
detail.setStockQuantity(new BigDecimal(quantity));
|
||||
return detail;
|
||||
}
|
||||
}
|
||||
28
ruoyi-ui/src/api/out/denseLaneOutbound.js
Normal file
28
ruoyi-ui/src/api/out/denseLaneOutbound.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询03仓密集库通道整托库存汇总
|
||||
export function listDenseLaneOutbound(query) {
|
||||
return request({
|
||||
url: '/out/denseLaneOutbound/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 预览指定通道当前最外侧托盘
|
||||
export function previewDenseLaneOutbound(query) {
|
||||
return request({
|
||||
url: '/out/denseLaneOutbound/preview',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 按预览结果生成整托出库作业
|
||||
export function generateDenseLaneOutbound(data) {
|
||||
return request({
|
||||
url: '/out/denseLaneOutbound/generate',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
553
ruoyi-ui/src/views/out/denseLaneOutbound/index.vue
Normal file
553
ruoyi-ui/src/views/out/denseLaneOutbound/index.vue
Normal file
@@ -0,0 +1,553 @@
|
||||
<template>
|
||||
<div class="app-container dense-lane-outbound">
|
||||
<el-form
|
||||
ref="queryForm"
|
||||
:model="queryParams"
|
||||
size="small"
|
||||
:inline="true"
|
||||
v-show="showSearch"
|
||||
label-width="76px"
|
||||
>
|
||||
<el-form-item label="仓库">
|
||||
<el-input value="03仓密集库" disabled class="warehouse-input" />
|
||||
</el-form-item>
|
||||
<el-form-item label="通道编码" prop="groupCoding">
|
||||
<el-input
|
||||
v-model="queryParams.groupCoding"
|
||||
placeholder="请输入通道组编码"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="层" prop="theFloor">
|
||||
<el-input-number
|
||||
v-model="queryParams.theFloor"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
placeholder="全部"
|
||||
class="floor-input"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-alert
|
||||
title="仅显示03仓有托盘的密集通道;生成时严格按当前端头顺序选择整托。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="lane-hint"
|
||||
/>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
|
||||
</el-row>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="laneList"
|
||||
row-key="laneKey"
|
||||
border
|
||||
stripe
|
||||
@expand-change="handleExpandChange"
|
||||
>
|
||||
<el-table-column type="expand" width="44">
|
||||
<template slot-scope="scope">
|
||||
<div class="inventory-expand" v-loading="scope.row.inventoryLoading">
|
||||
<div class="inventory-heading">
|
||||
<span>当前最外侧托盘库存</span>
|
||||
<span class="inventory-subtitle">
|
||||
{{ scope.row.outermostTrayCode || '-' }} / {{ scope.row.outermostCargoSpaceId || '-' }}
|
||||
</span>
|
||||
</div>
|
||||
<el-table :data="scope.row.inventoryDetails || []" size="mini" border max-height="260">
|
||||
<el-table-column label="项目号" prop="projectNo" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="物料编码" prop="productId" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column label="物料名称" prop="productName" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="批次" prop="lotNo" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column label="库存数量" prop="stockQuantity" width="110" align="right" />
|
||||
<el-table-column label="入库日期" prop="inDate" width="160" />
|
||||
<template slot="empty">
|
||||
<div class="empty-detail">{{ scope.row.inventoryLoaded ? '当前端头托盘无库存明细' : '正在读取库存明细' }}</div>
|
||||
</template>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="通道组编码" prop="groupCoding" min-width="140" fixed="left" show-overflow-tooltip />
|
||||
<el-table-column label="货区" prop="cargoArea" width="90" align="center" />
|
||||
<el-table-column label="排" prop="thePlatoon" width="72" align="center" />
|
||||
<el-table-column label="层" prop="theFloor" width="72" align="center" />
|
||||
<el-table-column label="出库方向" prop="directionType" width="110" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" effect="plain">{{ directionLabel(scope.row.directionType) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="满托盘数" prop="fullTrayCount" width="105" align="right" />
|
||||
<el-table-column label="可出库托盘" prop="availableTrayCount" width="120" align="right">
|
||||
<template slot-scope="scope">
|
||||
<span :class="{ 'available-count': Number(scope.row.availableTrayCount) > 0 }">
|
||||
{{ scope.row.availableTrayCount || 0 }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="总库存" prop="totalQuantity" min-width="110" align="right" />
|
||||
<el-table-column label="当前最外侧托盘" prop="outermostTrayCode" min-width="145" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.outermostTrayCode || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="端头货位" prop="outermostCargoSpaceId" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-s-promotion"
|
||||
:disabled="Number(scope.row.availableTrayCount || 0) < 1"
|
||||
v-hasPermi="['out:denseLaneOutbound:generate']"
|
||||
@click="openGenerateDialog(scope.row)"
|
||||
>整托出库</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog
|
||||
title="指定通道整托出库"
|
||||
:visible.sync="dialogVisible"
|
||||
width="920px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
@closed="resetDialog"
|
||||
>
|
||||
<el-descriptions v-if="currentLane" :column="4" size="small" border class="lane-summary">
|
||||
<el-descriptions-item label="通道">{{ currentLane.groupCoding }}</el-descriptions-item>
|
||||
<el-descriptions-item label="货区 / 排">{{ currentLane.cargoArea || '-' }} / {{ currentLane.thePlatoon || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="层">{{ currentLane.theFloor }}</el-descriptions-item>
|
||||
<el-descriptions-item label="可出库">{{ currentLane.availableTrayCount || 0 }} 托</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-form :model="generateForm" size="small" :inline="true" label-width="86px" class="generate-form">
|
||||
<el-form-item label="整托数量" required>
|
||||
<el-input-number
|
||||
v-model="generateForm.trayCount"
|
||||
:min="1"
|
||||
:max="currentLane ? Number(currentLane.availableTrayCount || 1) : 1"
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
@change="handleTrayCountChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-view"
|
||||
:loading="previewLoading"
|
||||
v-hasPermi="['out:denseLaneOutbound:preview']"
|
||||
@click="loadPreview"
|
||||
>刷新预览</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="出库口" required class="exit-form-item">
|
||||
<el-select
|
||||
v-model="generateForm.exitId"
|
||||
placeholder="请选择与托盘类型匹配的出库口"
|
||||
filterable
|
||||
clearable
|
||||
:loading="exitLoading"
|
||||
class="exit-select"
|
||||
>
|
||||
<el-option
|
||||
v-for="point in compatibleExitOptions"
|
||||
:key="point.pointId"
|
||||
:label="formatExitLabel(point)"
|
||||
:value="point.pointId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="preview-header">
|
||||
<div>
|
||||
<span class="preview-title">本次准确出库托盘</span>
|
||||
<span class="preview-note">提交前会再次校验端头顺序和库存</span>
|
||||
</div>
|
||||
<div class="preview-total">合计库存:<strong>{{ previewData.totalQuantity || 0 }}</strong></div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="previewLoading"
|
||||
:data="previewData.trays || []"
|
||||
row-key="cargoSpaceId"
|
||||
border
|
||||
max-height="390"
|
||||
>
|
||||
<el-table-column type="expand" width="42">
|
||||
<template slot-scope="scope">
|
||||
<el-table :data="scope.row.stockDetails || []" size="mini" border class="preview-detail-table">
|
||||
<el-table-column label="项目号" prop="projectNo" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column label="物料编码" prop="productId" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="物料名称" prop="productName" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="批次" prop="lotNo" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="库存数量" prop="stockQuantity" width="105" align="right" />
|
||||
</el-table>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="顺序" type="index" width="62" align="center" />
|
||||
<el-table-column label="托盘号" prop="trayCode" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="货位编码" prop="cargoSpaceId" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="货位名称" prop="cargoSpaceName" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="列" prop="theColumn" width="72" align="center" />
|
||||
<el-table-column label="库存数量" prop="stockQuantity" width="115" align="right" />
|
||||
</el-table>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="generating"
|
||||
:disabled="!canGenerate"
|
||||
@click="confirmGenerate"
|
||||
>确认生成 {{ generateForm.trayCount || 0 }} 托出库作业</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
listDenseLaneOutbound,
|
||||
previewDenseLaneOutbound,
|
||||
generateDenseLaneOutbound
|
||||
} from '@/api/out/denseLaneOutbound'
|
||||
import { listStationPoint } from '@/api/bas/stationpoint'
|
||||
|
||||
const emptyPreview = () => ({ trays: [], totalQuantity: 0, previewToken: null })
|
||||
|
||||
export default {
|
||||
name: 'DenseLaneOutbound',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
showSearch: true,
|
||||
laneList: [],
|
||||
queryParams: {
|
||||
warehouseId: '03',
|
||||
groupCoding: null,
|
||||
theFloor: null
|
||||
},
|
||||
dialogVisible: false,
|
||||
currentLane: null,
|
||||
previewLoading: false,
|
||||
generating: false,
|
||||
exitLoading: false,
|
||||
exitOptions: [],
|
||||
previewData: emptyPreview(),
|
||||
generateForm: {
|
||||
warehouseId: '03',
|
||||
groupCoding: null,
|
||||
theFloor: null,
|
||||
trayCount: 1,
|
||||
exitId: null,
|
||||
previewToken: null
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
compatibleExitOptions() {
|
||||
const trayTypes = Array.from(new Set((this.previewData.trays || [])
|
||||
.map(tray => this.trayType(tray.trayCode))
|
||||
.filter(Boolean)))
|
||||
return this.exitOptions.filter(point => trayTypes.every(type => this.pointSupportsTrayType(point, type)))
|
||||
},
|
||||
canGenerate() {
|
||||
return Boolean(
|
||||
this.previewData.previewToken &&
|
||||
this.generateForm.exitId &&
|
||||
(this.previewData.trays || []).length === Number(this.generateForm.trayCount)
|
||||
)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList()
|
||||
},
|
||||
methods: {
|
||||
laneKey(row) {
|
||||
return `${row.groupCoding}-${row.theFloor}`
|
||||
},
|
||||
getList() {
|
||||
this.loading = true
|
||||
listDenseLaneOutbound(this.queryParams).then(response => {
|
||||
this.laneList = (response.data || []).map(row => ({
|
||||
...row,
|
||||
inventoryLoading: false,
|
||||
inventoryLoaded: false,
|
||||
inventoryDetails: []
|
||||
}))
|
||||
this.loading = false
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handleQuery() {
|
||||
this.getList()
|
||||
},
|
||||
resetQuery() {
|
||||
this.resetForm('queryForm')
|
||||
this.queryParams.warehouseId = '03'
|
||||
this.getList()
|
||||
},
|
||||
handleExpandChange(row, expandedRows) {
|
||||
const expanded = expandedRows.some(item => this.laneKey(item) === this.laneKey(row))
|
||||
if (!expanded || row.inventoryLoaded || row.inventoryLoading || !row.outermostTrayCode) {
|
||||
return
|
||||
}
|
||||
this.$set(row, 'inventoryLoading', true)
|
||||
previewDenseLaneOutbound(this.previewParams(row, 1)).then(response => {
|
||||
const tray = ((response.data || {}).trays || [])[0]
|
||||
this.$set(row, 'inventoryDetails', tray ? (tray.stockDetails || []) : [])
|
||||
this.$set(row, 'inventoryLoaded', true)
|
||||
this.$set(row, 'inventoryLoading', false)
|
||||
}).catch(() => {
|
||||
this.$set(row, 'inventoryLoading', false)
|
||||
})
|
||||
},
|
||||
openGenerateDialog(row) {
|
||||
if (Number(row.availableTrayCount || 0) < 1) {
|
||||
this.$modal.msgWarning('当前通道没有可出库的端头托盘')
|
||||
return
|
||||
}
|
||||
this.currentLane = row
|
||||
this.generateForm = {
|
||||
warehouseId: '03',
|
||||
groupCoding: row.groupCoding,
|
||||
theFloor: row.theFloor,
|
||||
trayCount: 1,
|
||||
exitId: null,
|
||||
previewToken: null
|
||||
}
|
||||
this.previewData = emptyPreview()
|
||||
this.dialogVisible = true
|
||||
this.loadExitOptions()
|
||||
this.loadPreview()
|
||||
},
|
||||
resetDialog() {
|
||||
this.currentLane = null
|
||||
this.previewData = emptyPreview()
|
||||
this.exitOptions = []
|
||||
this.generateForm.previewToken = null
|
||||
},
|
||||
handleTrayCountChange() {
|
||||
this.previewData = emptyPreview()
|
||||
this.generateForm.previewToken = null
|
||||
this.generateForm.exitId = null
|
||||
},
|
||||
previewParams(row, trayCount) {
|
||||
return {
|
||||
warehouseId: '03',
|
||||
groupCoding: row.groupCoding,
|
||||
theFloor: row.theFloor,
|
||||
trayCount: trayCount
|
||||
}
|
||||
},
|
||||
loadPreview() {
|
||||
if (!this.currentLane || !this.generateForm.trayCount) {
|
||||
return
|
||||
}
|
||||
this.previewLoading = true
|
||||
this.generateForm.exitId = null
|
||||
previewDenseLaneOutbound(this.previewParams(this.currentLane, this.generateForm.trayCount)).then(response => {
|
||||
this.previewData = response.data || emptyPreview()
|
||||
this.generateForm.previewToken = this.previewData.previewToken
|
||||
this.selectOnlyCompatibleExit()
|
||||
this.previewLoading = false
|
||||
}).catch(() => {
|
||||
this.previewData = emptyPreview()
|
||||
this.generateForm.previewToken = null
|
||||
this.previewLoading = false
|
||||
})
|
||||
},
|
||||
loadExitOptions() {
|
||||
this.exitLoading = true
|
||||
listStationPoint({ warehouseId: '03', status: '0' }).then(response => {
|
||||
const rows = response.rows || response.data || []
|
||||
this.exitOptions = rows.filter(point => ['2', '3'].includes(String(point.pointType)) &&
|
||||
String(point.pointStatus || '0') === '0')
|
||||
this.selectOnlyCompatibleExit()
|
||||
this.exitLoading = false
|
||||
}).catch(() => {
|
||||
this.exitOptions = []
|
||||
this.exitLoading = false
|
||||
})
|
||||
},
|
||||
selectOnlyCompatibleExit() {
|
||||
this.$nextTick(() => {
|
||||
if (this.compatibleExitOptions.length === 1) {
|
||||
this.generateForm.exitId = this.compatibleExitOptions[0].pointId
|
||||
}
|
||||
})
|
||||
},
|
||||
trayType(trayCode) {
|
||||
return trayCode ? String(trayCode).trim().slice(0, 1).toUpperCase() : null
|
||||
},
|
||||
pointSupportsTrayType(point, trayType) {
|
||||
if (!trayType || !point.trayType) {
|
||||
return true
|
||||
}
|
||||
return String(point.trayType).split(',').map(item => item.trim().toUpperCase()).includes(trayType)
|
||||
},
|
||||
formatExitLabel(point) {
|
||||
const typeName = String(point.pointType) === '3' ? '入出库' : '出库'
|
||||
const trayType = point.trayType ? `,${point.trayType}类托盘` : ''
|
||||
return `${point.pointName || point.pointId}(${point.pointId},${typeName}${trayType})`
|
||||
},
|
||||
directionLabel(directionType) {
|
||||
if (String(directionType) === '1') return '大列号端'
|
||||
if (String(directionType) === '2') return '小列号端'
|
||||
if (String(directionType) === '3') return '最近端头'
|
||||
return '-'
|
||||
},
|
||||
confirmGenerate() {
|
||||
if (!this.canGenerate) {
|
||||
this.$modal.msgWarning('请先刷新预览并选择匹配的出库口')
|
||||
return
|
||||
}
|
||||
const trays = (this.previewData.trays || []).map(item => item.trayCode).join('、')
|
||||
this.$modal.confirm(`确认按当前端头顺序生成 ${this.generateForm.trayCount} 托出库作业?托盘:${trays}`).then(() => {
|
||||
this.generating = true
|
||||
return generateDenseLaneOutbound({
|
||||
...this.generateForm,
|
||||
previewToken: this.previewData.previewToken
|
||||
})
|
||||
}).then(response => {
|
||||
const ids = response.data || []
|
||||
this.$modal.msgSuccess(`已生成 ${ids.length} 条出库作业`)
|
||||
this.generating = false
|
||||
this.dialogVisible = false
|
||||
this.getList()
|
||||
}).catch(error => {
|
||||
this.generating = false
|
||||
if (error && error.message && error.message.includes('预览已失效')) {
|
||||
this.$modal.msgWarning('端头托盘或库存已变化,已为你刷新预览,请重新确认')
|
||||
this.loadPreview()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dense-lane-outbound .warehouse-input {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.dense-lane-outbound .floor-input {
|
||||
width: 128px;
|
||||
}
|
||||
|
||||
.lane-hint {
|
||||
width: auto;
|
||||
padding: 5px 12px;
|
||||
}
|
||||
|
||||
.available-count {
|
||||
color: #1890ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.inventory-expand {
|
||||
padding: 8px 18px 14px 58px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.inventory-heading,
|
||||
.preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.inventory-subtitle,
|
||||
.preview-note {
|
||||
margin-left: 10px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.empty-detail {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.lane-summary {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.generate-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
padding: 12px 12px 0;
|
||||
margin-bottom: 14px;
|
||||
border: 1px solid #ebeef5;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.exit-form-item {
|
||||
flex: 1;
|
||||
min-width: 360px;
|
||||
}
|
||||
|
||||
.exit-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.preview-total {
|
||||
color: #606266;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.preview-total strong {
|
||||
color: #1890ff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.preview-detail-table {
|
||||
width: calc(100% - 48px);
|
||||
margin: 4px 18px 10px 48px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.inventory-expand {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.generate-form {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.exit-form-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user