Merge remote-tracking branch 'origin/test' into test
This commit is contained in:
+10
@@ -27,6 +27,16 @@ import java.util.List;
|
||||
@RequestMapping("/api/merchant/v1/last-leg/warehouse-transfers")
|
||||
public class MerchantWarehouseTransferController {
|
||||
@Resource private WarehouseTransferService service;
|
||||
@Resource private com.baiyue.lastleg.transfer.service.WarehouseTransferForecastService forecastService;
|
||||
|
||||
/** 测量完成前追加预报箱唛,保留原编号和已测量内容。 */
|
||||
@PostMapping("/{transferNo}/increase-forecast-cartons")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_TRANSFER_CREATE + "')")
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> increaseForecastCartons(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String transferNo,
|
||||
@Valid @RequestBody WarehouseTransferRequests.IncreaseForecastCartons request) {
|
||||
return ApiResponse.success(forecastService.increase(customer(user), transferNo, operator(user), request));
|
||||
}
|
||||
|
||||
@GetMapping("/inventory-source-cartons")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_TRANSFER_CREATE + "')")
|
||||
|
||||
+7
@@ -84,6 +84,13 @@ public class PdaWarehouseTransferController {
|
||||
return ApiResponse.success(service.pdaCartonLabel(identity.overseasWarehouseId(), transferNo));
|
||||
}
|
||||
|
||||
@PostMapping("/{transferNo}/cartons/reopen")
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> reopenMeasurement(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String transferNo) {
|
||||
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
|
||||
return ApiResponse.success(service.reopenMeasurement(identity.overseasWarehouseId(), transferNo));
|
||||
}
|
||||
|
||||
@PostMapping("/{transferNo}/cartons/count")
|
||||
public ApiResponse<WarehouseTransferResponses.Detail> countCartons(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String transferNo,
|
||||
|
||||
+18
-3
@@ -105,10 +105,19 @@ public final class WarehouseTransferRequests {
|
||||
public record Outbound(@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
/** 仅增加既有SKU的预报箱数,携带原箱数用于并发核对及重试去重。 */
|
||||
public record IncreaseForecastCartons(
|
||||
@Positive @Max(5000) int expectedPackageCount,
|
||||
@NotNull Long transferItemId,
|
||||
@Min(0) @Max(5000) int expectedItemPackageCount,
|
||||
@Positive @Max(5000) int packageCount
|
||||
) {}
|
||||
|
||||
public record CountCartons(
|
||||
@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@NotEmpty @Size(max = 5000) List<@Valid @NotNull CountCarton> boxes,
|
||||
@Size(max = 1000) String discrepancyReason
|
||||
@Size(max = 1000) String discrepancyReason,
|
||||
@NotNull @Positive Integer forecastPackageCount
|
||||
) {}
|
||||
|
||||
public record CountCarton(
|
||||
@@ -127,8 +136,14 @@ public final class WarehouseTransferRequests {
|
||||
|
||||
public record Palletize(
|
||||
@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@NotEmpty @Size(max = 20) List<@NotNull Long> photoFileIds
|
||||
) {}
|
||||
@NotEmpty @Size(max = 20) List<@NotNull Long> photoFileIds,
|
||||
@Size(max = 1000) String discrepancyReason
|
||||
) {
|
||||
/** 无差异的既有调用不需要填写原因。 */
|
||||
public Palletize(String idempotencyKey, List<Long> photoFileIds) {
|
||||
this(idempotencyKey, photoFileIds, null);
|
||||
}
|
||||
}
|
||||
|
||||
public record PalletDraft(
|
||||
Long palletId,
|
||||
|
||||
+9
@@ -10,6 +10,15 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface WarehouseTransferCartonMapper extends BaseMapper<WarehouseTransferCarton> {
|
||||
/** 按客户及本单范围保存完成原因,允许清空重测前的差异说明。 */
|
||||
@org.apache.ibatis.annotations.Update("UPDATE ll_warehouse_transfer_carton SET discrepancy_reason=#{reason}, updated_at=CURRENT_TIMESTAMP WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId}")
|
||||
int saveDiscrepancyReason(@Param("customerId") String customerId, @Param("transferOrderId") Long transferOrderId, @Param("reason") String reason);
|
||||
|
||||
@org.apache.ibatis.annotations.Update("UPDATE ll_warehouse_transfer_carton SET counted_at=NULL, counted_by=NULL, count_idempotency_key=NULL, count_request_hash=NULL, updated_at=CURRENT_TIMESTAMP WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId}")
|
||||
int reopenMeasurement(@Param("customerId") String customerId, @Param("transferOrderId") Long transferOrderId);
|
||||
|
||||
@org.apache.ibatis.annotations.Update("UPDATE ll_warehouse_transfer_carton SET transfer_pallet_id=NULL, updated_at=CURRENT_TIMESTAMP WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId} AND transfer_pallet_id=#{palletId}")
|
||||
int unassignPallet(@Param("customerId") String customerId, @Param("transferOrderId") Long transferOrderId, @Param("palletId") Long palletId);
|
||||
@Select("SELECT * FROM ll_warehouse_transfer_carton WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId} ORDER BY carton_index")
|
||||
List<WarehouseTransferCarton> listOrder(@Param("customerId") String customerId,
|
||||
@Param("transferOrderId") Long transferOrderId);
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package com.baiyue.lastleg.transfer.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baiyue.lastleg.transfer.entity.*;
|
||||
import com.baiyue.lastleg.transfer.mapper.*;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegInboundOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderDetail;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
||||
import com.baiyue.lastleg.mapper.*;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanLineMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/** 转运预报补箱的数据访问,统一保留原箱号及客户隔离。 */
|
||||
@Repository
|
||||
public class TransferForecastRepository {
|
||||
@Resource private WarehouseTransferOrderMapper transfers;
|
||||
@Resource private WarehouseTransferItemMapper items;
|
||||
@Resource private WarehouseTransferCartonMapper cartons;
|
||||
@Resource private LastlegOrderMapper orders;
|
||||
@Resource private LastlegInboundOrderMapper inboundOrders;
|
||||
@Resource private LastlegOrderDetailMapper orderDetails;
|
||||
@Resource private LastlegOrderStatusHistoryMapper histories;
|
||||
@Resource private InboundPlanLineMapper planLines;
|
||||
|
||||
/** 与PDA清点及完工锁定同一转运单,避免补箱与完工并发。 */
|
||||
public WarehouseTransferOrder lock(String customerId, String transferNo) {
|
||||
return transfers.lockNo(customerId, transferNo);
|
||||
}
|
||||
|
||||
/** 按创建顺序读取预报明细,保持关联尾程明细的序号稳定。 */
|
||||
public List<WarehouseTransferItem> items(String customerId, Long transferId) {
|
||||
return items.selectList(new LambdaQueryWrapper<WarehouseTransferItem>()
|
||||
.eq(WarehouseTransferItem::getCustomerId, customerId)
|
||||
.eq(WarehouseTransferItem::getTransferOrderId, transferId).orderByAsc(WarehouseTransferItem::getId));
|
||||
}
|
||||
|
||||
/** 读取原箱号用于确定新增编号,不删除或重建原箱。 */
|
||||
public List<WarehouseTransferCarton> cartons(String customerId, Long transferId) {
|
||||
return cartons.listOrder(customerId, transferId);
|
||||
}
|
||||
|
||||
/** 追加预报箱并同步各预报口径;清点数据保留,整单清点确认失效。 */
|
||||
public void append(WarehouseTransferOrder transfer, WarehouseTransferItem item, int itemIndex,
|
||||
List<WarehouseTransferCarton> added, LocalDateTime now, String operatorId) {
|
||||
String customerId = transfer.getCustomerId();
|
||||
var order = orders.selectOne(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getCustomerId, customerId).eq(LastlegOrder::getId, transfer.getLastlegOrderId()));
|
||||
if (order == null) throw new IllegalStateException("转运关联尾程订单不存在");
|
||||
items.updateById(item);
|
||||
transfers.updateById(transfer);
|
||||
for (var carton : added) cartons.insert(carton);
|
||||
cartons.update(null, new LambdaUpdateWrapper<WarehouseTransferCarton>()
|
||||
.eq(WarehouseTransferCarton::getCustomerId, customerId).eq(WarehouseTransferCarton::getTransferOrderId, transfer.getId())
|
||||
.set(WarehouseTransferCarton::getTotalCartons, transfer.getPackageCount()).set(WarehouseTransferCarton::getUpdatedAt, now));
|
||||
cartons.reopenMeasurement(customerId, transfer.getId());
|
||||
orders.update(null, new LambdaUpdateWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getCustomerId, customerId).eq(LastlegOrder::getId, transfer.getLastlegOrderId())
|
||||
.set(LastlegOrder::getPackageCount, transfer.getPackageCount()).set(LastlegOrder::getUpdatedAt, now));
|
||||
inboundOrders.update(null, new LambdaUpdateWrapper<LastlegInboundOrder>()
|
||||
.eq(LastlegInboundOrder::getCustomerId, customerId).eq(LastlegInboundOrder::getId, transfer.getInboundOrderId())
|
||||
.set(LastlegInboundOrder::getPackageCount, transfer.getPackageCount()).set(LastlegInboundOrder::getUpdatedAt, now));
|
||||
int updated = orderDetails.update(null, new LambdaUpdateWrapper<LastlegOrderDetail>()
|
||||
.eq(LastlegOrderDetail::getCustomerId, customerId).eq(LastlegOrderDetail::getOrderId, transfer.getLastlegOrderId())
|
||||
.eq(LastlegOrderDetail::getSourceInboundDetailId, transfer.getTransferNo() + ":" + itemIndex)
|
||||
.set(LastlegOrderDetail::getPackageCount, item.getPackageCount()));
|
||||
if (updated != 1) throw new IllegalStateException("转运关联尾程货物明细不完整");
|
||||
planLines.update(null, new LambdaUpdateWrapper<InboundPlanLine>()
|
||||
.eq(InboundPlanLine::getCustomerId, customerId).eq(InboundPlanLine::getOrderId, transfer.getLastlegOrderId())
|
||||
.set(InboundPlanLine::getRequiredPackageCount, transfer.getPackageCount()));
|
||||
var history = new LastlegOrderStatusHistory(); history.setCustomerId(customerId); history.setOrderId(transfer.getLastlegOrderId());
|
||||
history.setFromStatus(order.getCurrentStatus()); history.setToStatus(order.getCurrentStatus());
|
||||
history.setEventId(transfer.getTransferNo()); history.setCreatedAt(now);
|
||||
history.setRemark("客户增加预报箱数,补充箱唛" + added.size() + "个;操作人:" + operatorId);
|
||||
histories.insert(history);
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.baiyue.lastleg.transfer.repository;
|
||||
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferCartonMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/** 封装转运测量完成时的箱级差异说明持久化。 */
|
||||
@Repository
|
||||
public class TransferMeasurementRepository {
|
||||
@Resource private WarehouseTransferCartonMapper cartons;
|
||||
|
||||
/** 整单统一保存原因,无差异时显式清空重测前的旧原因。 */
|
||||
public void saveDiscrepancyReason(String customerId, Long transferOrderId, String reason) {
|
||||
cartons.saveDiscrepancyReason(customerId, transferOrderId, reason);
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.baiyue.lastleg.transfer.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.lastleg.transfer.dto.WarehouseTransferRequests;
|
||||
import com.baiyue.lastleg.transfer.dto.WarehouseTransferResponses;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferCarton;
|
||||
import com.baiyue.lastleg.transfer.repository.TransferForecastRepository;
|
||||
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Set;
|
||||
|
||||
/** 测量完成前补充预报箱,只追加编号,不重建既有箱号。 */
|
||||
@Service
|
||||
public class WarehouseTransferForecastService {
|
||||
@Resource private TransferForecastRepository repository;
|
||||
@Resource private WarehouseTransferService transfers;
|
||||
|
||||
/** 锁单校验原预报快照,追加箱号并同步预报箱数,重复提交不重复生成。 */
|
||||
@Transactional
|
||||
public WarehouseTransferResponses.Detail increase(String customerId, String transferNo, String operatorId,
|
||||
WarehouseTransferRequests.IncreaseForecastCartons request) {
|
||||
var transfer = repository.lock(customerId, transferNo);
|
||||
if (transfer == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "转运单不存在");
|
||||
if (!transferNo.equals(transfer.getInboundCode()) || !Set.of("PENDING_REVIEW", "WAIT_PDA_RECEIVING").contains(transfer.getStatus()))
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "仅测量完成前的外来转运单可增加预报箱数");
|
||||
var items = repository.items(customerId, transfer.getId());
|
||||
var item = items.stream().filter(row -> row.getId().equals(request.transferItemId())).findFirst()
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "SKU不属于当前转运单"));
|
||||
int addedCount = request.packageCount() - request.expectedItemPackageCount();
|
||||
long total = (long) request.expectedPackageCount() + addedCount;
|
||||
if (addedCount <= 0 || total > 5000 || request.expectedPackageCount() <= 0)
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "只能增加预报箱数,整单最多5000箱");
|
||||
// 相同目标值视为已完成的重试;不同原快照则要求刷新,避免多端覆盖。
|
||||
if (item.getPackageCount() == request.packageCount() && transfer.getPackageCount() == total)
|
||||
return transfers.detail(customerId, transferNo);
|
||||
if (transfer.getPackageCount() != request.expectedPackageCount() || item.getPackageCount() != request.expectedItemPackageCount())
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "预报箱数已变化,请刷新后重试");
|
||||
var existing = repository.cartons(customerId, transfer.getId());
|
||||
int index = existing.stream().mapToInt(WarehouseTransferCarton::getCartonIndex).max().orElse(0);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
var added = new ArrayList<WarehouseTransferCarton>();
|
||||
for (int i = 0; i < addedCount; i++) {
|
||||
var carton = new WarehouseTransferCarton(); carton.setId(IdWorker.getId()); carton.setCustomerId(customerId);
|
||||
carton.setTransferOrderId(transfer.getId()); carton.setTransferItemId(item.getId());
|
||||
carton.setCartonMark(LastlegOrderNoGenerator.compactCartonMark(carton.getId()));
|
||||
carton.setCartonIndex(++index); carton.setTotalCartons((int) total); carton.setStatus("EXPECTED");
|
||||
carton.setCreatedAt(now); carton.setUpdatedAt(now); added.add(carton);
|
||||
}
|
||||
item.setPackageCount(request.packageCount()); transfer.setPackageCount((int) total); transfer.setUpdatedAt(now);
|
||||
repository.append(transfer, item, items.indexOf(item), added, now, operatorId);
|
||||
return transfers.detail(customerId, transferNo);
|
||||
}
|
||||
}
|
||||
+59
-18
@@ -120,6 +120,7 @@ public class WarehouseTransferService {
|
||||
@Resource private WarehouseTransferOrderMapper transfers;
|
||||
@Resource private WarehouseTransferItemMapper transferItems;
|
||||
@Resource private WarehouseTransferCartonMapper transferCartons;
|
||||
@Resource private com.baiyue.lastleg.transfer.repository.TransferMeasurementRepository transferMeasurementRepository;
|
||||
@Resource private WarehouseTransferSourceOrderMapper sourceOrders;
|
||||
@Resource private WarehouseTransferPalletMapper transferPallets;
|
||||
@Resource private WarehouseTransferPalletItemMapper transferPalletItems;
|
||||
@@ -697,13 +698,13 @@ public class WarehouseTransferService {
|
||||
return cartonLabel(transfer.getCustomerId(), transferNo);
|
||||
}
|
||||
|
||||
/** 提交预报后打印整单固定箱唛,测量及未收状态不改变箱号集合。 */
|
||||
@Transactional(readOnly = true)
|
||||
public WarehouseTransferResponses.Label cartonLabel(String customerId, String transferNo) {
|
||||
WarehouseTransferOrder transfer = transfers.findNo(customerId, transferNo);
|
||||
if (transfer == null) throw notFound("Warehouse transfer order not found");
|
||||
if (DRAFT.equals(transfer.getStatus())) throw conflict("Submit the warehouse transfer order before printing carton labels");
|
||||
List<WarehouseTransferResponses.Carton> cartons = transferCartons.listOrder(customerId, transfer.getId()).stream()
|
||||
.filter(value -> !"NOT_RECEIVED".equals(value.getStatus()))
|
||||
.map(value -> new WarehouseTransferResponses.Carton(value.getCartonMark(), value.getCustomerCartonMark(), value.getCartonIndex(), value.getTotalCartons(), value.getStatus(), value.getTransferItemId(), value.getTransferPalletId(), value.getLengthCm(), value.getWidthCm(), value.getHeightCm(), value.getGrossWeightKg(), value.getCountedQuantity(), value.getDiscrepancyReason(), value.getCountedBy(), value.getCountedAt(), cartonContents(value))).toList();
|
||||
StringBuilder labels = new StringBuilder();
|
||||
for (WarehouseTransferResponses.Carton carton : cartons) {
|
||||
@@ -816,11 +817,24 @@ public class WarehouseTransferService {
|
||||
WarehouseTransferOrder transfer = requirePdaMeasuringTransfer(warehouseId, transferNo);
|
||||
WarehouseTransferPallet pallet = transferPallets.lockOne(transfer.getCustomerId(), transfer.getId(), palletId);
|
||||
if (pallet == null) return detail(transfer.getCustomerId(), transferNo);
|
||||
if (!"DRAFT".equals(pallet.getStatus())) throw conflict("Generated pallet cannot be removed");
|
||||
if (!Set.of("DRAFT", "GENERATED", "BOUND").contains(pallet.getStatus())) throw conflict("当前托盘不能删除");
|
||||
if (pallet.getWarehouseResourceLockId() != null) {
|
||||
warehouseResources.release(transfer.getCustomerId(), pallet.getWarehouseResourceLockId());
|
||||
}
|
||||
transferCartons.unassignPallet(transfer.getCustomerId(), transfer.getId(), palletId);
|
||||
transferCartons.reopenMeasurement(transfer.getCustomerId(), transfer.getId());
|
||||
transferPallets.deleteById(palletId);
|
||||
return detail(transfer.getCustomerId(), transferNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public WarehouseTransferResponses.Detail reopenMeasurement(long warehouseId, String transferNo) {
|
||||
WarehouseTransferOrder transfer = requirePdaMeasuringTransfer(warehouseId, transferNo);
|
||||
// Preserve dimensions, SKU contents and assignments; invalidate the previous count request.
|
||||
transferCartons.reopenMeasurement(transfer.getCustomerId(), transfer.getId());
|
||||
return detail(transfer.getCustomerId(), transferNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public WarehouseTransferResponses.Detail generatePallet(long warehouseId, String transferNo, long palletId,
|
||||
WarehouseTransferRequests.PalletAction request) {
|
||||
@@ -874,6 +888,7 @@ public class WarehouseTransferService {
|
||||
return detail(transfer.getCustomerId(), transferNo);
|
||||
}
|
||||
|
||||
/** 保存逐箱实测及托盘归属,按整托汇总体积并保留三位小数。 */
|
||||
@Transactional
|
||||
public WarehouseTransferResponses.Detail countCartons(String operatorId, long warehouseId, String transferNo,
|
||||
WarehouseTransferRequests.CountCartons request) {
|
||||
@@ -881,6 +896,12 @@ public class WarehouseTransferService {
|
||||
if (transfer == null) throw notFound("当前仓没有该转运单");
|
||||
String customerId = transfer.getCustomerId();
|
||||
List<WarehouseTransferCarton> cartons = transferCartons.listOrder(customerId, transfer.getId());
|
||||
// 客户补箱后旧设备快照不可确认短收,必须刷新取得新增的预报箱唛。
|
||||
if (!java.util.Objects.equals(request.forecastPackageCount(), transfer.getPackageCount()))
|
||||
throw conflict("预报箱数已变化,请刷新后继续清点");
|
||||
// PDA只清点客户预报的固定箱唛;不足时由客户补充预报,禁止现场另造编号。
|
||||
if (request.boxes().stream().anyMatch(box -> !StringUtils.hasText(box.cartonMark())))
|
||||
throw badRequest("请先由客户增加预报箱数,刷新取得箱唛后再清点");
|
||||
String requestHash = hash(request);
|
||||
boolean sameKey = cartons.stream().anyMatch(box -> request.idempotencyKey().equals(box.getCountIdempotencyKey()));
|
||||
if (sameKey) {
|
||||
@@ -891,8 +912,7 @@ public class WarehouseTransferService {
|
||||
if (!WAIT_PDA_RECEIVING.equals(transfer.getStatus())) throw conflict("转运单已结束入库测量");
|
||||
if (cartons.stream().anyMatch(box -> box.getCountedAt() != null)) throw conflict("本单已完成清点,请刷新查看结果");
|
||||
if (cartons.isEmpty()) throw conflict("转运单缺少箱号预报");
|
||||
if (request.boxes().size() != transfer.getPackageCount() && !StringUtils.hasText(request.discrepancyReason()))
|
||||
throw badRequest("实收箱数与预报不一致时必须填写差异原因");
|
||||
// 清点阶段只记录实收事实,差异原因在最终完成测量时强制填写。
|
||||
Map<String, WarehouseTransferCarton> byMark = cartons.stream().collect(Collectors.toMap(WarehouseTransferCarton::getCartonMark, Function.identity()));
|
||||
Map<Long, WarehouseTransferItem> byItem = transferItems.selectList(new LambdaQueryWrapper<WarehouseTransferItem>()
|
||||
.eq(WarehouseTransferItem::getCustomerId, customerId).eq(WarehouseTransferItem::getTransferOrderId, transfer.getId()))
|
||||
@@ -908,10 +928,9 @@ public class WarehouseTransferService {
|
||||
|| !Set.of("GENERATED", "BOUND").contains(targetPallet.getStatus()))
|
||||
throw badRequest("请先创建本单托盘号,再选择箱子所属托盘");
|
||||
var carton = byMark.get(input.cartonMark());
|
||||
if (StringUtils.hasText(input.cartonMark()) && (carton == null || !seen.add(input.cartonMark())))
|
||||
if (carton == null || !seen.add(input.cartonMark()))
|
||||
throw badRequest("箱号不属于本单或重复填写");
|
||||
if (carton == null && input.transferItemId() == null) throw badRequest("新增实收箱必须选择本单申报明细");
|
||||
var item = byItem.get(carton == null ? input.transferItemId() : carton.getTransferItemId());
|
||||
var item = byItem.get(carton.getTransferItemId());
|
||||
if (item == null) throw badRequest("箱子申报明细不属于本单");
|
||||
if (input.items() == null || input.items().isEmpty() || input.items().size() > 200)
|
||||
throw badRequest("每箱必须填写 1 至 200 行 SKU");
|
||||
@@ -934,16 +953,8 @@ public class WarehouseTransferService {
|
||||
carton.setCountIdempotencyKey(request.idempotencyKey()); carton.setCountRequestHash(requestHash);
|
||||
carton.setDiscrepancyReason(trim(request.discrepancyReason())); carton.setUpdatedAt(now); transferCartons.updateById(carton);
|
||||
}
|
||||
int nextIndex = cartons.stream().mapToInt(WarehouseTransferCarton::getCartonIndex).max().orElse(0);
|
||||
for (var input : request.boxes()) {
|
||||
var carton = byMark.get(input.cartonMark());
|
||||
boolean added = carton == null;
|
||||
if (added) {
|
||||
carton = new WarehouseTransferCarton(); carton.setId(IdWorker.getId());
|
||||
carton.setTransferOrderId(transfer.getId()); carton.setTransferItemId(input.transferItemId()); carton.setCustomerId(customerId);
|
||||
carton.setCartonIndex(++nextIndex); carton.setTotalCartons(transfer.getPackageCount());
|
||||
carton.setCartonMark(LastlegOrderNoGenerator.compactCartonMark(carton.getId())); carton.setCreatedAt(now);
|
||||
}
|
||||
carton.setTransferPalletId(input.transferPalletId());
|
||||
carton.setLengthCm(input.length()); carton.setWidthCm(input.width()); carton.setHeightCm(input.height());
|
||||
carton.setGrossWeightKg(input.weight());
|
||||
@@ -957,14 +968,14 @@ public class WarehouseTransferService {
|
||||
carton.setCountedBy(operatorId); carton.setCountedAt(now); carton.setStatus("COUNTED");
|
||||
carton.setCountIdempotencyKey(request.idempotencyKey()); carton.setCountRequestHash(requestHash); carton.setUpdatedAt(now);
|
||||
carton.setDiscrepancyReason(trim(request.discrepancyReason()));
|
||||
if (added) transferCartons.insert(carton); else transferCartons.updateById(carton);
|
||||
transferCartons.updateById(carton);
|
||||
}
|
||||
// Aggregate only the submitted, physically assigned boxes; never infer SKU placement from declaration order.
|
||||
// 仅汇总实际分配到本托的实收箱,体积在整托汇总后四舍五入保存三位,避免逐箱舍入累积误差。
|
||||
for (var pallet : countedPallets) {
|
||||
var assigned = request.boxes().stream().filter(box -> pallet.getId().equals(box.transferPalletId())).toList();
|
||||
pallet.setCartonCount(assigned.size());
|
||||
pallet.setVolumeCbm(assigned.stream().map(box -> box.length().multiply(box.width()).multiply(box.height())
|
||||
.movePointLeft(6)).reduce(BigDecimal.ZERO, BigDecimal::add).setScale(6, java.math.RoundingMode.HALF_UP));
|
||||
.movePointLeft(6)).reduce(BigDecimal.ZERO, BigDecimal::add).setScale(3, java.math.RoundingMode.HALF_UP));
|
||||
pallet.setGrossWeightKg(assigned.stream().map(WarehouseTransferRequests.CountCarton::weight).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
pallet.setUpdatedAt(now); transferPallets.updateById(pallet);
|
||||
}
|
||||
@@ -981,6 +992,26 @@ public class WarehouseTransferService {
|
||||
}
|
||||
}
|
||||
|
||||
/** 按 SKU 汇总预报及实收件数;混箱逐项累计,兼容原单 SKU 箱记录。 */
|
||||
boolean hasSkuReceiptDifference(List<WarehouseTransferCarton> cartons, List<WarehouseTransferItem> items) {
|
||||
Map<String, Long> expected = new java.util.HashMap<>();
|
||||
Map<String, Long> received = new java.util.HashMap<>();
|
||||
Map<Long, WarehouseTransferItem> byId = items.stream().collect(Collectors.toMap(WarehouseTransferItem::getId, Function.identity()));
|
||||
for (var item : items) expected.merge(item.getSkuCode(), item.getDeclaredQuantity() == null ? 0L : item.getDeclaredQuantity(), Long::sum);
|
||||
for (var carton : cartons) {
|
||||
var contents = cartonContents(carton);
|
||||
if (contents.isEmpty()) {
|
||||
var item = byId.get(carton.getTransferItemId());
|
||||
if (item == null) throw conflict("实收箱缺少有效SKU归属,请重新清点");
|
||||
received.merge(item.getSkuCode(), carton.getCountedQuantity() == null ? 0L : carton.getCountedQuantity(), Long::sum);
|
||||
} else {
|
||||
for (var line : contents) received.merge(line.skuCode(), line.quantity(), Long::sum);
|
||||
}
|
||||
}
|
||||
return expected.entrySet().stream().anyMatch(entry -> !entry.getValue().equals(received.getOrDefault(entry.getKey(), 0L)))
|
||||
|| received.entrySet().stream().anyMatch(entry -> !entry.getValue().equals(expected.getOrDefault(entry.getKey(), 0L)));
|
||||
}
|
||||
|
||||
private List<WarehouseTransferCarton> requireCountedCartons(WarehouseTransferOrder transfer) {
|
||||
var cartons = transferCartons.listOrder(transfer.getCustomerId(), transfer.getId());
|
||||
if (cartons.isEmpty() || cartons.stream().anyMatch(box -> box.getCountedAt() == null)
|
||||
@@ -1004,6 +1035,7 @@ public class WarehouseTransferService {
|
||||
}
|
||||
}
|
||||
|
||||
/** 核对清点、照片、托盘及差异说明,在同一事务内完成测量并入库。 */
|
||||
@Transactional
|
||||
public WarehouseTransferResponses.Detail palletize(String operatorId, long overseasWarehouseId,
|
||||
String transferNo,
|
||||
@@ -1032,6 +1064,13 @@ public class WarehouseTransferService {
|
||||
if (items.isEmpty() || items.stream().mapToInt(WarehouseTransferItem::getPackageCount).sum() != transfer.getPackageCount()) {
|
||||
throw conflict("Warehouse transfer declared cargo is incomplete");
|
||||
}
|
||||
// 按每种 SKU 的件数核对实收,箱数变化不触发,不同 SKU 的差异不能抵消。
|
||||
boolean skuDifference = hasSkuReceiptDifference(countedCartons, items);
|
||||
String discrepancyReason = skuDifference ? trim(request.discrepancyReason()) : null;
|
||||
if (skuDifference && !StringUtils.hasText(discrepancyReason))
|
||||
throw badRequest("SKU实收件数与预报不一致时必须填写差异原因");
|
||||
if (discrepancyReason != null && discrepancyReason.length() > 1000)
|
||||
throw badRequest("差异原因不能超过1000字");
|
||||
if (request.photoFileIds() == null || request.photoFileIds().isEmpty() || request.photoFileIds().size() > 20) {
|
||||
throw conflict("Measurement and palletization require 1 to 20 on-site photos");
|
||||
}
|
||||
@@ -1052,6 +1091,8 @@ public class WarehouseTransferService {
|
||||
List<DerivedPalletAllocation> derivedAllocations = deriveCountedPalletAllocations(countedCartons, persistedPallets);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
List<WarehouseTransferPalletItem> completedPalletItems = new ArrayList<>();
|
||||
// 与测量完成处于同一事务;短收箱与实收箱保持同一说明,供详情回显。
|
||||
transferMeasurementRepository.saveDiscrepancyReason(customerId, transfer.getId(), discrepancyReason);
|
||||
for (int index = 0; index < persistedPallets.size(); index++) {
|
||||
WarehouseTransferPallet pallet = persistedPallets.get(index);
|
||||
for (DerivedPalletAllocation allocation : derivedAllocations) {
|
||||
|
||||
Reference in New Issue
Block a user