费用提交

This commit is contained in:
2026-10-26 13:41:43 +08:00
parent 0fe1d10566
commit 761fe3686e
171 changed files with 8750 additions and 386 deletions
@@ -91,6 +91,8 @@ import java.util.stream.Collectors;
@Service
public class DeliveryTransferService {
@jakarta.annotation.Resource private com.baiyue.lastleg.storagerent.service.StorageRentService storageRent;
@Resource private DispatchAggregateRepository dispatches;
@Resource private DispatchItemMapper dispatchItems;
@Resource private OutboundOrderMapper outboundOrders;
@@ -526,6 +528,7 @@ public class DeliveryTransferService {
return pickResolved(warehouseId, operatorId, taskNo, request, false);
}
/** PDA 真实出库在扣库存前校验同一扫码的仓租冻结,原幂等结果优先恢复。 */
private DeliveryResponses.PdaTask pickResolved(String warehouseId, String operatorId, String taskNo,
DeliveryRequests.PdaPick request, boolean palletScan) {
OutboundPdaTask task = pdaTasks.lockByNoAndWarehouse(required(taskNo, "PDA task number is required"),
@@ -560,6 +563,9 @@ public class DeliveryTransferService {
if (item == null || !customerId.equals(item.getCustomerId())
|| !task.getOutboundOrderId().equals(item.getOutboundOrderId())) throw notFound("Outbound pallet item not found");
if (request.packages() > item.getPackageCount() - value(item.getPickedPackageCount())) throw conflict("Picked quantity exceeds outbound pallet quantity");
storageRent.consume(warehouseId, new com.baiyue.lastleg.storagerent.dto.StorageRentDtos.PrepareRequest(
"OUTBOUND", taskNo, request.outboundPalletItemId(), request.inventoryPositionId(), request.packages(), null, null,
request.idempotencyKey(), request.cartonMarks(), request.skuCode(), request.productQuantity()));
if ("RETURN_OUTBOUND".equals(outbound.getOutboundType())
|| ("RELABEL_OUTBOUND".equals(outbound.getOutboundType()) && item.getWarehousePalletId() == null)) {
if ("RETURN_OUTBOUND".equals(outbound.getOutboundType()) && StringUtils.hasText(item.getPalletNo()) && !palletScan)
@@ -729,9 +735,22 @@ public class DeliveryTransferService {
return taskResult(outbound, task);
}
/** 扫描整托前核平全部指定来源,任一不足时不执行部分拣货。 */
/** 整托扫描使用与资金预校验完全一致的实际子来源,任一失败则整托回滚。 */
@Transactional
public DeliveryResponses.PdaTask pdaPickPallet(String warehouseId, String operatorId, String taskNo,
String palletNo, DeliveryRequests.PdaPalletPick request) {
var children = storageRentPalletRequests(warehouseId, operatorId, taskNo, palletNo, request);
DeliveryResponses.PdaTask result = null;
for (var child : children) result = pickResolved(warehouseId, operatorId, taskNo, child, true);
if (result != null) return result;
var task = pdaTasks.lockByNoAndWarehouse(taskNo, warehouseId);
if (task == null) throw notFound("当前仓没有该出库工单");
return taskResult(requireOutbound(task), task);
}
/** 资金预校验和实际整托扫码共享同一来源展开及子扫描幂等键算法。 */
@Transactional
public java.util.List<DeliveryRequests.PdaPick> storageRentPalletRequests(String warehouseId, String operatorId, String taskNo,
String palletNo, DeliveryRequests.PdaPalletPick request) {
String boundWarehouseId = required(warehouseId, "PDA warehouse binding is required");
String code = required(taskNo, "PDA task number is required");
@@ -752,7 +771,7 @@ public class DeliveryTransferService {
.stream().filter(item -> palletCode.equals(item.getPalletNo())).toList();
if (palletItems.isEmpty()) throw notFound("当前出库任务没有该托盘");
if (palletItems.stream().allMatch(item -> value(item.getPickedPackageCount()) >= value(item.getPackageCount()))) {
return taskResult(outbound, task);
return java.util.List.of();
}
if ("RETURN_OUTBOUND".equals(outbound.getOutboundType())) {
@@ -763,14 +782,9 @@ public class DeliveryTransferService {
!task.getCustomerId().equals(carton.getCustomerId()) || !expectedMarks.contains(carton.getCartonMark())
|| !"OUTBOUND_PENDING".equals(carton.getStatus())))
throw conflict("托盘内容或出库预留已变化,请刷新核对");
DeliveryResponses.PdaTask result = taskResult(outbound, task);
for (OutboundPalletItem item : palletItems.stream().sorted(Comparator.comparing(OutboundPalletItem::getId)).toList()) {
if (value(item.getPickedPackageCount()) >= value(item.getPackageCount())) continue;
result = pickResolved(boundWarehouseId, operatorId, code,
new DeliveryRequests.PdaPick(item.getId(), null, 1L,
childKey(request.idempotencyKey(), "RETURN:" + item.getId()), List.of(item.getCartonMarks())), true);
}
return result;
return palletItems.stream().filter(item -> value(item.getPickedPackageCount()) < value(item.getPackageCount()))
.sorted(Comparator.comparing(OutboundPalletItem::getId)).map(item -> new DeliveryRequests.PdaPick(item.getId(), null, 1L,
childKey(request.idempotencyKey(), "RETURN:" + item.getId()), List.of(item.getCartonMarks()))) .toList();
}
List<PalletPickPlan> plans = new ArrayList<>();
@@ -797,13 +811,8 @@ public class DeliveryTransferService {
if (remaining > 0) throw conflict("托盘可用库存不足,整托拣货未执行:" + palletCode);
}
DeliveryResponses.PdaTask result = taskResult(outbound, task);
for (PalletPickPlan plan : plans) {
String operationId = palletPickOperationId(request.idempotencyKey(), code, palletCode, plan);
result = pdaPick(boundWarehouseId, operatorId, code, new DeliveryRequests.PdaPick(
plan.itemId(), plan.positionId(), plan.packages(), operationId, plan.cartonMarks()));
}
return result;
return plans.stream().map(plan -> new DeliveryRequests.PdaPick(plan.itemId(), plan.positionId(), plan.packages(),
palletPickOperationId(request.idempotencyKey(), code, palletCode, plan), plan.cartonMarks())).toList();
}
@Transactional
@@ -1011,7 +1020,7 @@ public class DeliveryTransferService {
delivery.setUpdatedAt(now); cancellations.save(delivery);
}
OutboundPdaTask task = cancellations.task(outbound);
if (task != null) { task.setStatus("CANCELLED"); task.setUpdatedAt(now); cancellations.save(task); }
if (task != null) { storageRent.cancelExecution("OUTBOUND",task.getTaskNo()); task.setStatus("CANCELLED"); task.setUpdatedAt(now); cancellations.save(task); }
outbound.setStatus("CANCELLED"); outbound.setUpdatedAt(now); cancellations.save(outbound);
for (OutboundPalletItem item : outboundItems) { item.setStatus("CANCELLED"); cancellations.save(item); }
if ("WAREHOUSE_TRANSFER_OUTBOUND".equals(outbound.getOutboundType())) {
@@ -116,17 +116,13 @@ public class DispatchDeliveryFees {
if (fee==null || blank(fee.feeType()) || !SERVICES.contains(fee.feeType()) || blank(fee.direction()) || !Set.of("RECEIVABLE","PAYABLE").contains(fee.direction())
|| blank(fee.currency()) || !Set.of("CNY","USD").contains(fee.currency()) || blank(fee.feeName()) || fee.amount()==null || fee.amount().signum()<=0)
throw conflict("派送费用类型、方向、币种或金额不合法");
if ("RECEIVABLE".equals(fee.direction())) throw conflict("请到海外费用登记并审核应收费用");
if (!seen.add(fee.direction()+":"+fee.feeType())) throw conflict("同组同方向同费用不得重复登记");
if ("RECEIVABLE".equals(fee.direction()) && group.cargo().stream().allMatch(c->covered(c,fee.feeType(),evidence,freight)))
throw conflict("本组该服务已全部覆盖,不得重复登记应收");
}
var required=new HashSet<String>(); required.add("DELIVERY_FEE");
if (Set.of("FLOOR","MIXED").contains(input.loadingType())) required.add("LOADING_FEE");
if ("BUY_APPOINTMENT".equals(input.appointmentAccount())) {
if (blank(input.sellerName())) throw conflict("买约须填写卖家名称");required.add("APPOINTMENT_FEE");
}
for (var code:required) if (group.cargo().stream().anyMatch(c->!covered(c,code,evidence,freight)) && !seen.contains("RECEIVABLE:"+code))
throw conflict("本组尚有未覆盖的服务费用,请核对" );
if ("BUY_APPOINTMENT".equals(input.appointmentAccount()) && blank(input.sellerName()))
throw conflict("买约须填写卖家名称");
}
/** 子单及来源锁已经收齐后才核对原费用撤销状态,不能重复收费。 */
@@ -134,7 +130,7 @@ public class DispatchDeliveryFees {
fees.requireNoActiveDeliveryReceivables(customerId,freight.priorInboundCodes());
}
/** 费用与子单同事务保存,只向真实客户目的地发布一次正数来源。 */
/** 外接报价保留记录;只发布供应商应付,客户应收走人工审核。 */
public void publish(Context context,Group group,ArrangeGroup input,DeliveryOrder delivery,
DeliveryFeeCoverageModels.Evidence evidence,FleetBookingFreight.Preview freight,String operatorId,String arrangementKey) {
var now=LocalDateTime.now();
@@ -144,12 +140,10 @@ public class DispatchDeliveryFees {
operation.setOperatorId(operatorId);operation.setOperationNode("CONFIRM_EXTERNAL_FREIGHT");
operation.setResult("外接预订 "+quote.bookingNo()+";实收确认版本 "+quote.recordVersion()+";约定运费 "+quote.currency()+" "+quote.amount().toPlainString());
operation.setCreatedAt(now);repository.operation(operation);
if (quote.amount().signum()==0) continue;
fees.publishApproved(operatorId,groups.hash(List.of(arrangementKey,group.key(),quote.bookingNo(),"FREIGHT")),
save(group,delivery,null,"DELIVERY_FEE","外接约定运费", "RECEIVABLE",quote.currency(),quote.amount(),
quote.quantity(),quote.quantityUnit(),quote.unitPrice(),null,quote.inboundCode()));
// 约定运费只保留商业记录,应收由海外费用人工登记审核。
}
for (var fee:input.fees()==null?List.<FeeItem>of():input.fees()) {
if (!"PAYABLE".equals(fee.direction())) throw conflict("请到海外费用登记并审核应收费用");
var charged=group.cargo().stream().filter(c->!"RECEIVABLE".equals(fee.direction())||!covered(c,fee.feeType(),evidence,freight)).toList();
var orders=charged.stream().map(CargoLine::orderId).filter(Objects::nonNull).distinct().toList();
var inbound=charged.stream().map(CargoLine::inboundCode).filter(Objects::nonNull).distinct().toList();
@@ -164,7 +158,7 @@ public class DispatchDeliveryFees {
String direction,String currency,BigDecimal amount,BigDecimal quantity,String unit,BigDecimal unitPrice,FeeItem input,String inboundCode) {
return new OverseasFeeSourceDtos.SaveRequest(group.customerId(),group.customerName(),"OVERSEAS_DELIVERY_FEE","DELIVERY_ORDER",
delivery.getId(),delivery.getDeliveryNo(),orderId,null,inboundCode,null,null,null,Long.valueOf(delivery.getOverseasWarehouseId()),
code,name,direction,input==null?null:input.supplierId(),input==null?null:input.supplierName(),input==null?null:input.supplierCategory(),
code,name, null,direction,input==null?null:input.supplierId(),input==null?null:input.supplierName(),input==null?null:input.supplierCategory(),
input==null?null:input.settlementMode(),input==null||input.paidAt()==null?null:LocalDateTime.ofInstant(input.paidAt(),ZoneId.systemDefault()),
input==null?null:input.paymentMethod(),input==null?null:input.paymentReferenceNo(),input==null?null:input.paymentVoucherFileIds(),
LocalDate.now(),currency,amount,quantity,unit,unitPrice,input==null?null:input.periodStart(),input==null?null:input.periodEnd());