feat: support mixed-SKU dropship cartons
This commit is contained in:
@@ -65,18 +65,18 @@ public interface OrderCargoClient {
|
||||
record CargoLabelBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
|
||||
String skuNo, Integer packageCount, Integer unitsPerCarton,
|
||||
BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm,
|
||||
String shipmentId) {
|
||||
String shipmentId, String packingGroupNo) {
|
||||
public CargoLabelBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
|
||||
Integer packageCount, BigDecimal lengthCm, BigDecimal widthCm,
|
||||
BigDecimal heightCm) {
|
||||
this(cargoId, orderId, itemNo, cargoName, null,
|
||||
packageCount, null, lengthCm, widthCm, heightCm, null);
|
||||
packageCount, null, lengthCm, widthCm, heightCm, null, null);
|
||||
}
|
||||
public CargoLabelBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
|
||||
String skuNo, Integer packageCount, BigDecimal lengthCm,
|
||||
BigDecimal widthCm, BigDecimal heightCm, String shipmentId) {
|
||||
this(cargoId, orderId, itemNo, cargoName, skuNo,
|
||||
packageCount, null, lengthCm, widthCm, heightCm, shipmentId);
|
||||
packageCount, null, lengthCm, widthCm, heightCm, shipmentId, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,12 @@ public interface PrintPdfClient {
|
||||
@RequestHeader(value = "X-Request-Id", required = false) String requestId,
|
||||
@RequestBody FreightA4PdfRequest request);
|
||||
|
||||
@PostMapping(value = "/dropship-pallet-a4-documents", consumes = "application/json")
|
||||
ResponseEntity<byte[]> generateDropshipPalletA4Documents(
|
||||
@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestHeader(value = "X-Request-Id", required = false) String requestId,
|
||||
@RequestBody DropshipPalletA4PdfRequest request);
|
||||
|
||||
record InboundMarkPdfRequest(String templateCode, PageSpec pageSpec,
|
||||
String downloadFileName, List<Label> labels) {
|
||||
}
|
||||
@@ -44,4 +50,14 @@ public interface PrintPdfClient {
|
||||
|
||||
record CargoRow(String marksAndNos, String quantity, String packaging,
|
||||
String descriptionOfGoods, String grossWeight, String measurement) {}
|
||||
|
||||
record DropshipPalletA4PdfRequest(String templateCode, PageSpec pageSpec,
|
||||
String downloadFileName, List<DropshipPalletDocument> documents) {}
|
||||
|
||||
record DropshipPalletDocument(String palletNo, String inboundCodes, int cartonCount,
|
||||
int skuCount, BigDecimal weightKg, BigDecimal volumeCbm,
|
||||
String barcodeContent, List<DropshipPalletSkuRow> skuRows) {}
|
||||
|
||||
record DropshipPalletSkuRow(String inboundCode, String skuCode, String cargoName,
|
||||
int cartonCount, int skuQuantity) {}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.PositiveOrZero;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
@@ -19,12 +22,16 @@ public final class DropshipPalletizationDtos {
|
||||
|
||||
public record CountCartonRequest(@Positive long warehouseId,
|
||||
@NotBlank @Size(max = 80) String boxCode,
|
||||
@Positive int actualSkuQuantity,
|
||||
@NotNull DiscrepancyAction discrepancyAction,
|
||||
@Size(max = 500) String discrepancyRemark,
|
||||
@NotEmpty @Valid List<CountSkuLine> skuLines,
|
||||
@Pattern(regexp = "DP-[0-9]{6}-[0-9]{4}") String palletNo,
|
||||
@NotBlank @Size(max = 128) String operationId) {}
|
||||
|
||||
public record CountSkuLine(@Positive long inboundDetailId,
|
||||
@NotBlank @Size(max = 128) String skuCode,
|
||||
@PositiveOrZero int actualQuantity,
|
||||
@NotNull DiscrepancyAction discrepancyAction,
|
||||
@Size(max = 500) String discrepancyRemark) {}
|
||||
|
||||
public record BindPendingRequest(@Positive long warehouseId,
|
||||
@NotBlank @Pattern(regexp = "DP-[0-9]{6}-[0-9]{4}") String palletNo,
|
||||
@NotBlank @Size(max = 128) String operationId) {}
|
||||
@@ -63,12 +70,14 @@ public final class DropshipPalletizationDtos {
|
||||
List<String> cartonMarks) {}
|
||||
|
||||
public record Carton(Long cartonId, String boxCode, Long inboundId, String inboundCode,
|
||||
Long inboundDetailId, String skuCode, String cargoName,
|
||||
Integer expectedSkuQuantity, Integer actualSkuQuantity,
|
||||
String discrepancyStatus, String discrepancyRemark,
|
||||
List<CartonSkuLine> skuLines,
|
||||
String countStatus, Long palletId, String palletNo,
|
||||
Boolean canCount, String blockReason) {}
|
||||
|
||||
public record CartonSkuLine(Long inboundDetailId, String skuCode, String cargoName,
|
||||
Integer expectedQuantity, Integer actualQuantity,
|
||||
String discrepancyStatus, String discrepancyRemark) {}
|
||||
|
||||
public record CountResult(Carton carton, Integer countedCartons, Integer boundCartons,
|
||||
Integer unboundCartons, Integer remainingCartons, Pallet pallet) {}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,12 @@ public interface DropshipPalletizationMapper {
|
||||
FROM fl_inbound_carton_label l JOIN fl_dropship_pallet_task t ON t.inbound_id=l.inbound_id AND t.tenant_id=l.tenant_id
|
||||
WHERE t.id=#{taskId} AND l.box_code=#{boxCode} AND l.batch_status='ACTIVE'""")
|
||||
LabelRow label(@Param("taskId") long taskId,@Param("boxCode") String boxCode);
|
||||
@Select("""
|
||||
SELECT s.inbound_detail_id,s.sku_code,s.cargo_name,s.expected_quantity,s.line_no
|
||||
FROM fl_inbound_carton_label_sku s JOIN fl_inbound_carton_label l ON l.id=s.carton_label_id
|
||||
JOIN fl_dropship_pallet_task t ON t.inbound_id=l.inbound_id AND t.tenant_id=l.tenant_id
|
||||
WHERE t.id=#{taskId} AND l.id=#{labelId} ORDER BY s.line_no
|
||||
""") List<LabelSkuRow> labelSkus(@Param("taskId") long taskId,@Param("labelId") long labelId);
|
||||
@Select("SELECT c.*,p.pallet_no FROM fl_dropship_pallet_carton c LEFT JOIN fl_dropship_pallet p ON p.id=c.pallet_id WHERE c.count_operation_id=#{operationId}") CartonRow cartonByCountOperation(@Param("operationId") String operationId);
|
||||
@Select("SELECT c.*,p.pallet_no FROM fl_dropship_pallet_carton c LEFT JOIN fl_dropship_pallet p ON p.id=c.pallet_id WHERE c.task_id=#{taskId} AND c.id=#{cartonId}") CartonRow carton(@Param("taskId") long taskId,@Param("cartonId") long cartonId);
|
||||
@Select("SELECT c.*,p.pallet_no FROM fl_dropship_pallet_carton c LEFT JOIN fl_dropship_pallet p ON p.id=c.pallet_id WHERE c.task_id=#{taskId} AND c.carton_label_id=#{labelId}")
|
||||
@@ -103,6 +109,24 @@ public interface DropshipPalletizationMapper {
|
||||
@Param("discrepancyStatus") String discrepancyStatus,@Param("discrepancyRemark") String discrepancyRemark,
|
||||
@Param("operationId") String operationId,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO fl_dropship_pallet_carton_sku
|
||||
(id,carton_id,inbound_detail_id,sku_code,cargo_name,expected_quantity,actual_quantity,
|
||||
discrepancy_status,discrepancy_remark,line_no)
|
||||
VALUES(#{id},#{cartonId},#{detailId},#{skuCode},#{cargoName},#{expectedQuantity},#{actualQuantity},
|
||||
#{discrepancyStatus},#{discrepancyRemark},#{lineNo})
|
||||
""")
|
||||
int insertCartonSku(@Param("id") long id,@Param("cartonId") long cartonId,@Param("detailId") long detailId,
|
||||
@Param("skuCode") String skuCode,@Param("cargoName") String cargoName,
|
||||
@Param("expectedQuantity") int expectedQuantity,@Param("actualQuantity") int actualQuantity,
|
||||
@Param("discrepancyStatus") String discrepancyStatus,@Param("discrepancyRemark") String discrepancyRemark,
|
||||
@Param("lineNo") int lineNo);
|
||||
|
||||
@Select("""
|
||||
SELECT inbound_detail_id,sku_code,cargo_name,expected_quantity,actual_quantity,discrepancy_status,discrepancy_remark,line_no
|
||||
FROM fl_dropship_pallet_carton_sku WHERE carton_id=#{cartonId} ORDER BY line_no
|
||||
""") List<CartonSkuRow> cartonSkus(@Param("cartonId") long cartonId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_dropship_pallet_task SET counted_cartons=counted_cartons+1,allocated_cartons=allocated_cartons+#{boundCount},
|
||||
discrepancy_cartons=discrepancy_cartons+#{discrepancyCount},version=version+1,updated_at=CURRENT_TIMESTAMP(3)
|
||||
@@ -170,8 +194,9 @@ public interface DropshipPalletizationMapper {
|
||||
WHERE c.pallet_id=#{palletId}""")
|
||||
PalletMetrics metrics(@Param("palletId") long palletId);
|
||||
@Select("""
|
||||
SELECT p.id pallet_id,p.pallet_no,'SEALED_PALLET' unit_type,TRUE indivisible,COUNT(c.id) carton_count,
|
||||
COUNT(DISTINCT c.sku_code) sku_count,
|
||||
SELECT p.id pallet_id,p.pallet_no,'SEALED_PALLET' unit_type,TRUE indivisible,COUNT(DISTINCT c.id) carton_count,
|
||||
(SELECT COUNT(DISTINCT sku.sku_code) FROM fl_dropship_pallet_carton pc
|
||||
JOIN fl_dropship_pallet_carton_sku sku ON sku.carton_id=pc.id WHERE pc.pallet_id=p.id) sku_count,
|
||||
COALESCE(SUM(o.actual_gross_weight_kg/NULLIF(o.total_package_count,0)),0) weight_kg,
|
||||
COALESCE(SUM(o.actual_volume_cbm/NULLIF(o.total_package_count,0)),0) volume_cbm
|
||||
FROM fl_dropship_pallet p JOIN fl_dropship_pallet_carton c ON c.pallet_id=p.id
|
||||
@@ -186,23 +211,26 @@ public interface DropshipPalletizationMapper {
|
||||
SELECT p.id pallet_id,p.pallet_no,p.pallet_status,c.tenant_id account_id,c.inbound_id,o.order_id,o.order_no,
|
||||
o.inbound_code,o.warehouse_id,o.channel_code,o.channel_name,o.destination_warehouse_id,
|
||||
o.destination_code destination_warehouse_code,o.destination_name destination_warehouse_name,o.customer_name,
|
||||
c.inbound_detail_id,c.sku_code,MAX(c.cargo_name) cargo_name,COUNT(c.id) carton_count,
|
||||
SUM(c.actual_sku_quantity) actual_sku_quantity,
|
||||
s.inbound_detail_id,s.sku_code,MAX(s.cargo_name) cargo_name,COUNT(DISTINCT c.id) carton_count,
|
||||
SUM(s.actual_quantity) actual_sku_quantity,
|
||||
COALESCE(o.actual_gross_weight_kg/NULLIF(o.total_package_count,0),0) unit_gross_weight_kg,
|
||||
COALESCE(o.actual_net_weight_kg/NULLIF(o.total_package_count,0),0) unit_net_weight_kg,
|
||||
COALESCE(o.actual_volume_cbm/NULLIF(o.total_package_count,0),0) unit_volume_cbm
|
||||
FROM fl_dropship_pallet p JOIN fl_dropship_pallet_carton c ON c.pallet_id=p.id
|
||||
JOIN fl_dropship_pallet_carton_sku s ON s.carton_id=c.id
|
||||
JOIN fl_inbound_order o ON o.id=c.inbound_id AND o.tenant_id=c.tenant_id WHERE p.id=#{palletId}
|
||||
GROUP BY p.id,p.pallet_no,p.pallet_status,c.tenant_id,c.inbound_id,o.order_id,o.order_no,o.inbound_code,
|
||||
o.warehouse_id,o.channel_code,o.channel_name,o.destination_warehouse_id,o.destination_code,o.destination_name,
|
||||
o.customer_name,c.inbound_detail_id,c.sku_code,o.actual_gross_weight_kg,o.actual_net_weight_kg,o.actual_volume_cbm,o.total_package_count
|
||||
ORDER BY c.inbound_detail_id
|
||||
o.customer_name,s.inbound_detail_id,s.sku_code,o.actual_gross_weight_kg,o.actual_net_weight_kg,o.actual_volume_cbm,o.total_package_count
|
||||
ORDER BY s.inbound_detail_id
|
||||
""") List<PalletLine> sealedPalletLines(@Param("palletId") long palletId);
|
||||
|
||||
@Getter @Setter class TaskRow { private Long id,tenantId,inboundId,warehouseId,assigneeId; private String inboundCode,customerName,warehouseName,taskStatus,assigneeName,processStatus,fulfillmentType; private Integer totalCartons,countedCartons,allocatedCartons,discrepancyCartons,expectedPalletCount,version; }
|
||||
@Getter @Setter class PalletRow { private Long id,taskId,tenantId,inboundId,warehouseId; private String palletNo,palletStatus,sealOperationId; private Integer version; private LocalDateTime completedAt; }
|
||||
@Getter @Setter class LabelRow { private Long cartonLabelId,inboundDetailId; private String skuCode,cargoName,inboundCode,boxCode; private Integer unitsPerCarton; }
|
||||
@Getter @Setter class LabelSkuRow { private Long inboundDetailId; private String skuCode,cargoName; private Integer expectedQuantity,lineNo; }
|
||||
@Getter @Setter class CartonRow { private Long id,taskId,palletId,inboundId,inboundDetailId; private String palletNo,inboundCode,skuCode,cargoName,boxCode,discrepancyStatus,discrepancyRemark; private Integer expectedSkuQuantity,actualSkuQuantity; }
|
||||
@Getter @Setter class CartonSkuRow { private Long inboundDetailId; private String skuCode,cargoName,discrepancyStatus,discrepancyRemark; private Integer expectedQuantity,actualQuantity,lineNo; }
|
||||
record PalletMetrics(BigDecimal weightKg,BigDecimal volumeCbm) {}
|
||||
@Getter @Setter class PalletLine { private Long palletId,tenantId,accountId,inboundId,orderId,warehouseId,destinationWarehouseId,inboundDetailId; private String palletNo,palletStatus,orderNo,inboundCode,channelCode,channelName,destinationWarehouseCode,destinationWarehouseName,customerName,skuCode,cargoName; private Integer cartonCount,actualSkuQuantity; private BigDecimal unitGrossWeightKg,unitNetWeightKg,unitVolumeCbm; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** A physical carton label may freeze more than one SKU line. */
|
||||
@Mapper
|
||||
public interface InboundCartonLabelSkuMapper {
|
||||
@Insert("""
|
||||
INSERT INTO fl_inbound_carton_label_sku
|
||||
(id,tenant_id,inbound_id,carton_label_id,inbound_detail_id,sku_code,cargo_name,expected_quantity,line_no,created_at)
|
||||
VALUES(#{id},#{tenantId},#{inboundId},#{labelId},#{detailId},#{skuCode},#{cargoName},#{expectedQuantity},#{lineNo},CURRENT_TIMESTAMP(3))
|
||||
""")
|
||||
int insert(@Param("id") long id, @Param("tenantId") long tenantId,
|
||||
@Param("inboundId") long inboundId, @Param("labelId") long labelId,
|
||||
@Param("detailId") long detailId, @Param("skuCode") String skuCode,
|
||||
@Param("cargoName") String cargoName, @Param("expectedQuantity") int expectedQuantity,
|
||||
@Param("lineNo") int lineNo);
|
||||
|
||||
@Select("""
|
||||
SELECT inbound_detail_id,sku_code,cargo_name,expected_quantity,line_no
|
||||
FROM fl_inbound_carton_label_sku WHERE carton_label_id=#{labelId} ORDER BY line_no
|
||||
""")
|
||||
List<SkuRow> findByLabel(@Param("labelId") long labelId);
|
||||
|
||||
@Select("""
|
||||
SELECT carton_label_id,inbound_detail_id,sku_code,cargo_name,expected_quantity,line_no
|
||||
FROM fl_inbound_carton_label_sku WHERE tenant_id=#{tenantId} AND inbound_id=#{inboundId}
|
||||
ORDER BY carton_label_id,line_no
|
||||
""")
|
||||
List<SkuRow> findByInbound(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
|
||||
|
||||
@Getter @Setter
|
||||
class SkuRow {
|
||||
private Long inboundDetailId;
|
||||
private Long cartonLabelId;
|
||||
private String skuCode;
|
||||
private String cargoName;
|
||||
private Integer expectedQuantity;
|
||||
private Integer lineNo;
|
||||
}
|
||||
}
|
||||
+49
-20
@@ -62,8 +62,10 @@ public class DropshipPalletizationService {
|
||||
else if(!Objects.equals(task.getAssigneeId(),operatorId)) block="任务不属于当前操作员";
|
||||
else if(!"WAIT_PALLETIZE".equals(task.getProcessStatus())) block="入仓单已不在待清点组托状态";
|
||||
else if(!palletSetupReady(task)) block=palletSetupMessage(task);
|
||||
return new Carton(null,label.getBoxCode(),task.getInboundId(),label.getInboundCode(),label.getInboundDetailId(),
|
||||
label.getSkuCode(),label.getCargoName(),label.getUnitsPerCarton(),null,null,null,"UNCOUNTED",null,null,
|
||||
List<CartonSkuLine> skuLines=labelSkuLines(taskId,label).stream().map(line ->
|
||||
new CartonSkuLine(line.getInboundDetailId(),line.getSkuCode(),line.getCargoName(),line.getExpectedQuantity(),
|
||||
null,null,null)).toList();
|
||||
return new Carton(null,label.getBoxCode(),task.getInboundId(),label.getInboundCode(),skuLines,"UNCOUNTED",null,null,
|
||||
block==null,block);
|
||||
}
|
||||
|
||||
@@ -74,22 +76,22 @@ public class DropshipPalletizationService {
|
||||
CartonRow replay=mapper.cartonByCountOperation(operation);
|
||||
if(replay!=null){
|
||||
if(!Objects.equals(replay.getTaskId(),taskId) || !Objects.equals(replay.getBoxCode(),request.boxCode().trim())
|
||||
|| !Objects.equals(replay.getActualSkuQuantity(),request.actualSkuQuantity())) throw conflict("清点幂等号已用于不同操作");
|
||||
|| !sameCountLines(mapper.cartonSkus(replay.getId()),request.skuLines())) throw conflict("清点幂等号已用于不同操作");
|
||||
return countResult(task,replay);
|
||||
}
|
||||
LabelRow label=requireLabel(taskId,request.boxCode());
|
||||
if(mapper.countedByLabel(taskId,label.getCartonLabelId())!=null) throw conflict("箱码已清点,不能重复清点");
|
||||
int expected=requireExpectedQuantity(label);
|
||||
boolean mismatch=expected!=request.actualSkuQuantity();
|
||||
String remark=normalize(request.discrepancyRemark());
|
||||
if(!mismatch && request.discrepancyAction()!=DiscrepancyAction.MATCH) throw invalid("实收数量一致时差异动作必须为 MATCH");
|
||||
if(mismatch && request.discrepancyAction()!=DiscrepancyAction.ACCEPT_VARIANCE) throw conflict("实收数量存在差异,请确认差异处理结果");
|
||||
if(mismatch && !StringUtils.hasText(remark)) throw invalid("接受数量差异时必须填写处理说明");
|
||||
List<CountedLine> lines=validateCountLines(labelSkuLines(taskId,label),request.skuLines());
|
||||
boolean mismatch=lines.stream().anyMatch(CountedLine::mismatch);
|
||||
PalletRow pallet=StringUtils.hasText(request.palletNo())?requireAssignablePallet(task,request.palletNo().trim()):null;
|
||||
long cartonId=IdWorker.getId();
|
||||
CountedLine primary=lines.get(0);
|
||||
mapper.insertCarton(cartonId,taskId,pallet==null?null:pallet.getId(),task.getTenantId(),task.getInboundId(),
|
||||
label.getInboundDetailId(),label.getCartonLabelId(),label.getSkuCode(),label.getCargoName(),label.getBoxCode(),
|
||||
expected,request.actualSkuQuantity(),mismatch?"ACCEPTED_VARIANCE":"MATCH",remark,operation,operatorId,operatorName);
|
||||
primary.detailId(),label.getCartonLabelId(),primary.skuCode(),primary.cargoName(),label.getBoxCode(),
|
||||
primary.expected(),primary.actual(),mismatch?"ACCEPTED_VARIANCE":"MATCH",null,operation,operatorId,operatorName);
|
||||
int lineNo=0;
|
||||
for(CountedLine line:lines) mapper.insertCartonSku(IdWorker.getId(),cartonId,line.detailId(),line.skuCode(),line.cargoName(),
|
||||
line.expected(),line.actual(),line.mismatch()?"ACCEPTED_VARIANCE":"MATCH",line.remark(),++lineNo);
|
||||
if(mapper.addCounted(taskId,operatorId,pallet==null?0:1,mismatch?1:0)!=1) throw conflict("任务清点数量已变化,请刷新后重试");
|
||||
TaskRow refreshed=requireTask(taskId,request.warehouseId());
|
||||
return countResult(refreshed,mapper.carton(taskId,cartonId));
|
||||
@@ -181,11 +183,14 @@ public class DropshipPalletizationService {
|
||||
}
|
||||
|
||||
private Pallet palletView(PalletRow row,String inboundCode){
|
||||
List<CartonRow> cartons=mapper.cartons(row.getId()); Map<String,List<CartonRow>> groups=new LinkedHashMap<>();
|
||||
for(CartonRow c:cartons) groups.computeIfAbsent(c.getInboundId()+"|"+c.getInboundDetailId()+"|"+c.getSkuCode(),ignored->new ArrayList<>()).add(c);
|
||||
List<SkuItem> items=groups.values().stream().map(values->{CartonRow first=values.get(0);return new SkuItem(first.getInboundId(),first.getInboundCode(),
|
||||
first.getInboundDetailId(),first.getSkuCode(),first.getCargoName(),values.size(),values.stream().mapToInt(v->v.getActualSkuQuantity()==null?0:v.getActualSkuQuantity()).sum(),
|
||||
values.stream().map(CartonRow::getBoxCode).toList());}).toList();
|
||||
List<CartonRow> cartons=mapper.cartons(row.getId()); Map<String,List<CartonSkuWithBox>> groups=new LinkedHashMap<>();
|
||||
for(CartonRow c:cartons) for(CartonSkuRow line:mapper.cartonSkus(c.getId()))
|
||||
groups.computeIfAbsent(c.getInboundId()+"|"+line.getInboundDetailId()+"|"+line.getSkuCode(),ignored->new ArrayList<>())
|
||||
.add(new CartonSkuWithBox(c,line));
|
||||
List<SkuItem> items=groups.values().stream().map(values->{CartonSkuWithBox first=values.get(0);return new SkuItem(first.carton().getInboundId(),first.carton().getInboundCode(),
|
||||
first.line().getInboundDetailId(),first.line().getSkuCode(),first.line().getCargoName(),values.size(),
|
||||
values.stream().mapToInt(v->v.line().getActualQuantity()==null?0:v.line().getActualQuantity()).sum(),
|
||||
values.stream().map(v->v.carton().getBoxCode()).toList());}).toList();
|
||||
int discrepancies=(int)cartons.stream().filter(c->"ACCEPTED_VARIANCE".equals(c.getDiscrepancyStatus())).count();
|
||||
PalletMetrics metrics=mapper.metrics(row.getId());
|
||||
return new Pallet(row.getId(),row.getPalletNo(),row.getPalletStatus(),cartons.size(),items.size(),discrepancies,
|
||||
@@ -194,12 +199,34 @@ public class DropshipPalletizationService {
|
||||
}
|
||||
|
||||
private Carton cartonView(CartonRow row,String inboundCode,boolean canCount,String blockReason){
|
||||
return new Carton(row.getId(),row.getBoxCode(),row.getInboundId(),inboundCode,row.getInboundDetailId(),row.getSkuCode(),row.getCargoName(),
|
||||
row.getExpectedSkuQuantity(),row.getActualSkuQuantity(),row.getDiscrepancyStatus(),row.getDiscrepancyRemark(),
|
||||
List<CartonSkuLine> lines=mapper.cartonSkus(row.getId()).stream().map(line -> new CartonSkuLine(line.getInboundDetailId(),
|
||||
line.getSkuCode(),line.getCargoName(),line.getExpectedQuantity(),line.getActualQuantity(),line.getDiscrepancyStatus(),line.getDiscrepancyRemark())).toList();
|
||||
return new Carton(row.getId(),row.getBoxCode(),row.getInboundId(),inboundCode,lines,
|
||||
row.getPalletId()==null?"COUNTED_UNBOUND":"BOUND",row.getPalletId(),row.getPalletNo(),canCount,blockReason);
|
||||
}
|
||||
private LabelRow requireLabel(long taskId,String boxCode){ LabelRow label=mapper.label(taskId,boxCode.trim()); if(label==null) throw new BizException(ResultCode.NOT_FOUND,"箱码不属于当前一件代发入仓单"); if(!StringUtils.hasText(label.getSkuCode())) throw conflict("箱码缺少SKU快照,不能清点"); requireExpectedQuantity(label); return label; }
|
||||
private int requireExpectedQuantity(LabelRow label){ if(label.getUnitsPerCarton()==null||label.getUnitsPerCarton()<1) throw conflict("箱码缺少每箱SKU数量快照,不能清点"); return label.getUnitsPerCarton(); }
|
||||
private LabelRow requireLabel(long taskId,String boxCode){ LabelRow label=mapper.label(taskId,boxCode.trim()); if(label==null) throw new BizException(ResultCode.NOT_FOUND,"箱码不属于当前一件代发入仓单"); if(labelSkuLines(taskId,label).isEmpty()) throw conflict("箱码缺少SKU快照,不能清点"); return label; }
|
||||
private List<LabelSkuRow> labelSkuLines(long taskId,LabelRow label){ return mapper.labelSkus(taskId,label.getCartonLabelId()); }
|
||||
private List<CountedLine> validateCountLines(List<LabelSkuRow> expected,List<CountSkuLine> actual){
|
||||
Map<Long,CountSkuLine> submitted=new LinkedHashMap<>();
|
||||
for(CountSkuLine line:actual) if(submitted.put(line.inboundDetailId(),line)!=null) throw invalid("同一SKU明细不能重复清点");
|
||||
if(submitted.size()!=expected.size()) throw invalid("必须清点当前箱内全部SKU");
|
||||
List<CountedLine> result=new ArrayList<>();
|
||||
for(LabelSkuRow line:expected){
|
||||
CountSkuLine input=submitted.get(line.getInboundDetailId());
|
||||
if(input==null || !Objects.equals(line.getSkuCode(),input.skuCode().trim())) throw invalid("清点SKU与箱唛不一致");
|
||||
boolean mismatch=!Objects.equals(line.getExpectedQuantity(),input.actualQuantity()); String remark=normalize(input.discrepancyRemark());
|
||||
if(!mismatch && input.discrepancyAction()!=DiscrepancyAction.MATCH) throw invalid("实收数量一致时差异动作必须为 MATCH");
|
||||
if(mismatch && input.discrepancyAction()!=DiscrepancyAction.ACCEPT_VARIANCE) throw conflict("实收数量存在差异,请确认差异处理结果");
|
||||
if(mismatch && !StringUtils.hasText(remark)) throw invalid("接受数量差异时必须填写处理说明");
|
||||
result.add(new CountedLine(line.getInboundDetailId(),line.getSkuCode(),line.getCargoName(),line.getExpectedQuantity(),input.actualQuantity(),remark,mismatch));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
private boolean sameCountLines(List<CartonSkuRow> saved,List<CountSkuLine> submitted){
|
||||
if(saved.size()!=submitted.size()) return false;
|
||||
Map<Long,Integer> quantities=new HashMap<>(); for(CountSkuLine line:submitted) quantities.put(line.inboundDetailId(),line.actualQuantity());
|
||||
return saved.stream().allMatch(line->Objects.equals(line.getActualQuantity(),quantities.get(line.getInboundDetailId())));
|
||||
}
|
||||
private TaskRow requireWorkingTask(long taskId,long warehouseId,long operatorId){ TaskRow task=requireTask(taskId,warehouseId); if(!"CLAIMED".equals(task.getTaskStatus())) throw conflict("请先领取待清点组托任务"); if(!Objects.equals(task.getAssigneeId(),operatorId)) throw conflict("任务不属于当前操作员"); if(!"WAIT_PALLETIZE".equals(task.getProcessStatus())) throw conflict("入仓单已不在待清点组托状态"); requirePalletSetup(task); return task; }
|
||||
private void requirePalletSetup(TaskRow task){ if(!palletSetupReady(task)) throw conflict(palletSetupMessage(task)); }
|
||||
private boolean palletSetupReady(TaskRow task){ return task.getExpectedPalletCount()!=null && task.getExpectedPalletCount()>0 && mapper.palletCount(task.getId())==task.getExpectedPalletCount(); }
|
||||
@@ -211,4 +238,6 @@ public class DropshipPalletizationService {
|
||||
private static String normalize(String v){return StringUtils.hasText(v)?v.trim():null;}
|
||||
private static BizException conflict(String m){return new BizException(ResultCode.CONFLICT,m);}
|
||||
private static BizException invalid(String m){return new BizException(ResultCode.VALIDATION_FAILED,m);}
|
||||
private record CountedLine(long detailId,String skuCode,String cargoName,int expected,int actual,String remark,boolean mismatch){}
|
||||
private record CartonSkuWithBox(CartonRow carton,CartonSkuRow line){}
|
||||
}
|
||||
|
||||
+18
-11
@@ -25,6 +25,8 @@ import com.baiyue.firstleg.dto.DropshipPalletizationDtos.Pallet;
|
||||
@Service
|
||||
public class InboundCartonLabelPrintService {
|
||||
private static final BigDecimal PAGE_MM = new BigDecimal("100");
|
||||
private static final BigDecimal A4_WIDTH_MM = new BigDecimal("210");
|
||||
private static final BigDecimal A4_HEIGHT_MM = new BigDecimal("297");
|
||||
private static final List<String> SAFE_RESPONSE_HEADERS = List.of(
|
||||
HttpHeaders.CONTENT_DISPOSITION, HttpHeaders.CACHE_CONTROL, HttpHeaders.PRAGMA,
|
||||
"X-Print-Page-Count", "X-Request-Id", "X-Trace-Id");
|
||||
@@ -113,7 +115,7 @@ public class InboundCartonLabelPrintService {
|
||||
return ResponseEntity.status(response.getStatusCode()).headers(headers).body(response.getBody());
|
||||
}
|
||||
|
||||
/** 一件代发托盘标签:托盘号、入仓码、SKU箱数和托盘二维码。 */
|
||||
/** 一件代发 A4 托盘信息单:托盘号、Code 128 一维码和 SKU 归托明细。 */
|
||||
public ResponseEntity<byte[]> generateDropshipPallet(Pallet pallet, String inboundCode, String requestId) {
|
||||
if (pallet == null || !StringUtils.hasText(pallet.palletNo())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "托盘码尚未生成,不能打印");
|
||||
@@ -121,22 +123,27 @@ public class InboundCartonLabelPrintService {
|
||||
String inboundCodes = pallet.items().stream().map(item -> item.inboundCode())
|
||||
.filter(StringUtils::hasText).distinct().collect(java.util.stream.Collectors.joining("/"));
|
||||
if (!StringUtils.hasText(inboundCodes)) inboundCodes = inboundCode;
|
||||
String skuSummary = pallet.items().stream()
|
||||
.map(item -> item.skuCode() + "×" + item.cartonCount())
|
||||
.collect(java.util.stream.Collectors.joining(" / "));
|
||||
if (!StringUtils.hasText(skuSummary)) skuSummary = "待归托";
|
||||
PrintPdfClient.Label label = new PrintPdfClient.Label(pallet.palletNo(), inboundCodes,
|
||||
pallet.cartonCount() == 0 ? "CTNS:—" : "CTNS:" + pallet.cartonCount(), skuSummary, pallet.palletNo(), null);
|
||||
PrintPdfClient.InboundMarkPdfRequest request = new PrintPdfClient.InboundMarkPdfRequest(
|
||||
"INBOUND_MARK_V1", new PrintPdfClient.PageSpec(PAGE_MM, PAGE_MM),
|
||||
pallet.palletNo() + "-托盘唛.pdf", List.of(label));
|
||||
List<PrintPdfClient.DropshipPalletSkuRow> skuRows = pallet.items().stream()
|
||||
.map(item -> new PrintPdfClient.DropshipPalletSkuRow(item.inboundCode(), item.skuCode(),
|
||||
item.cargoName(), number(item.cartonCount()), number(item.skuQuantity())))
|
||||
.toList();
|
||||
PrintPdfClient.DropshipPalletDocument document = new PrintPdfClient.DropshipPalletDocument(
|
||||
pallet.palletNo(), inboundCodes, number(pallet.cartonCount()), number(pallet.skuCount()),
|
||||
pallet.weightKg(), pallet.volumeCbm(), pallet.palletNo(), skuRows);
|
||||
PrintPdfClient.DropshipPalletA4PdfRequest request = new PrintPdfClient.DropshipPalletA4PdfRequest(
|
||||
"DROPSHIP_PALLET_A4_V1", new PrintPdfClient.PageSpec(A4_WIDTH_MM, A4_HEIGHT_MM),
|
||||
pallet.palletNo() + "-A4托盘信息单.pdf", List.of(document));
|
||||
try {
|
||||
return requirePdf(printClient.generateInboundMarks(internalToken, requestId, request));
|
||||
return requirePdf(printClient.generateDropshipPalletA4Documents(internalToken, requestId, request));
|
||||
} catch (FeignException | NoFallbackAvailableException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "打印服务暂不可用,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
private int number(Integer value) {
|
||||
return value == null ? 0 : value;
|
||||
}
|
||||
|
||||
private ResponseEntity<byte[]> generatePdf(String fileName, List<InboundCartonLabels.Label> labels,
|
||||
String requestId) {
|
||||
PrintPdfClient.InboundMarkPdfRequest request = new PrintPdfClient.InboundMarkPdfRequest(
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.baiyue.firstleg.dto.InboundCartonLabels;
|
||||
import com.baiyue.firstleg.entity.InboundCartonLabelEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.repository.InboundCartonLabelRepository;
|
||||
import com.baiyue.firstleg.mapper.InboundCartonLabelSkuMapper;
|
||||
import feign.FeignException;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
|
||||
@@ -24,6 +25,8 @@ import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@@ -36,6 +39,7 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
|
||||
private static final Set<String> ALLOCATABLE_STATUSES = Set.of("PREPARING", "WAIT_MEASURE", "MEASURING");
|
||||
|
||||
@Resource private InboundCartonLabelRepository repository;
|
||||
@Resource private InboundCartonLabelSkuMapper labelSkus;
|
||||
@Resource private OrderCargoClient orderCargoClient;
|
||||
@Resource private PrintableOrderInboundClient printableOrderInboundClient;
|
||||
@Resource private InboundOrderCreationService inboundOrderCreationService;
|
||||
@@ -76,7 +80,8 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
|
||||
if (existingFingerprint.equals(fingerprint)) return batch(inbound, consistent);
|
||||
return batch(inbound, consistent);
|
||||
}
|
||||
int total = cargoes.stream().mapToInt(this::requirePackageCount).sum();
|
||||
List<CargoGroup> groups = cartonGroups(cargoes, inbound.getFulfillmentType());
|
||||
int total = groups.stream().mapToInt(CargoGroup::packageCount).sum();
|
||||
if (total < 1 || total > MAX_LABELS) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "唛头箱数必须为1至2000箱");
|
||||
}
|
||||
@@ -86,12 +91,20 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
|
||||
|
||||
int cartonIndex = 0;
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (OrderCargoClient.CargoLabelBinding cargo : cargoes) {
|
||||
int detailCount = requirePackageCount(cargo);
|
||||
for (CargoGroup group : groups) {
|
||||
OrderCargoClient.CargoLabelBinding cargo = group.cargoes().get(0);
|
||||
int detailCount = group.packageCount();
|
||||
for (int offset = 0; offset < detailCount; offset++) {
|
||||
cartonIndex++;
|
||||
InboundCartonLabelEntity label = label(inbound, cargo, total, cartonIndex, fingerprint, now);
|
||||
repository.insert(label);
|
||||
if ("ONE_CLICK_DROPSHIP".equals(inbound.getFulfillmentType())) {
|
||||
int lineNo = 0;
|
||||
for (OrderCargoClient.CargoLabelBinding line : group.cargoes()) {
|
||||
labelSkus.insert(IdWorker.getId(), inbound.getTenantId(), inbound.getId(), label.getId(),
|
||||
line.cargoId(), line.skuNo().trim(), line.cargoName(), line.unitsPerCarton(), ++lineNo);
|
||||
}
|
||||
}
|
||||
existing.add(label);
|
||||
}
|
||||
}
|
||||
@@ -195,6 +208,27 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
|
||||
return cargo.packageCount();
|
||||
}
|
||||
|
||||
private List<CargoGroup> cartonGroups(List<OrderCargoClient.CargoLabelBinding> cargoes, String fulfillmentType) {
|
||||
if (!"ONE_CLICK_DROPSHIP".equals(fulfillmentType)) {
|
||||
return cargoes.stream().map(cargo -> new CargoGroup(null, requirePackageCount(cargo), List.of(cargo))).toList();
|
||||
}
|
||||
Map<String, List<OrderCargoClient.CargoLabelBinding>> grouped = new LinkedHashMap<>();
|
||||
int single = 0;
|
||||
for (OrderCargoClient.CargoLabelBinding cargo : cargoes) {
|
||||
requirePackageCount(cargo);
|
||||
String group = StringUtils.hasText(cargo.packingGroupNo()) ? cargo.packingGroupNo().trim() : "__single_" + (++single);
|
||||
grouped.computeIfAbsent(group, ignored -> new ArrayList<>()).add(cargo);
|
||||
}
|
||||
return grouped.entrySet().stream().map(entry -> {
|
||||
List<OrderCargoClient.CargoLabelBinding> lines = entry.getValue();
|
||||
int count = lines.get(0).packageCount();
|
||||
if (lines.stream().anyMatch(line -> line.packageCount() != count)) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "同一混箱组合的箱数必须一致:" + entry.getKey());
|
||||
}
|
||||
return new CargoGroup(entry.getKey(), count, List.copyOf(lines));
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private void requirePrintableInboundCode(String inboundCode, int total) {
|
||||
if (!StringUtils.hasText(inboundCode) || !inboundCode.matches("[A-Za-z0-9._:-]+")) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "入仓码不能生成可打印二维码");
|
||||
@@ -276,6 +310,7 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
|
||||
cargo.cargoName() == null ? "" : cargo.cargoName().trim(),
|
||||
cargo.skuNo() == null ? "" : cargo.skuNo().trim(), String.valueOf(cargo.packageCount()),
|
||||
String.valueOf(cargo.unitsPerCarton()),
|
||||
cargo.packingGroupNo() == null ? "" : cargo.packingGroupNo().trim(),
|
||||
decimalOrEmpty(cargo.lengthCm()), decimalOrEmpty(cargo.widthCm()), decimalOrEmpty(cargo.heightCm())))
|
||||
.collect(java.util.stream.Collectors.joining("\n"));
|
||||
try {
|
||||
@@ -296,11 +331,16 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
|
||||
|
||||
private InboundCartonLabels.Batch batch(InboundOrderEntity inbound,
|
||||
List<InboundCartonLabelEntity> entities) {
|
||||
List<InboundCartonLabelSkuMapper.SkuRow> skuRows = labelSkus.findByInbound(inbound.getTenantId(), inbound.getId());
|
||||
Map<Long, List<InboundCartonLabelSkuMapper.SkuRow>> skuRowsByLabel = skuRows == null ? Map.of()
|
||||
: skuRows.stream().collect(java.util.stream.Collectors.groupingBy(
|
||||
InboundCartonLabelSkuMapper.SkuRow::getCartonLabelId, LinkedHashMap::new,
|
||||
java.util.stream.Collectors.toList()));
|
||||
List<InboundCartonLabels.Label> labels = entities.stream().map(entity ->
|
||||
new InboundCartonLabels.Label(entity.getId(), entity.getInboundDetailId(), entity.getCargoItemNo(),
|
||||
entity.getCargoName(), entity.getDetailPackageCount(), entity.getBoxCode(),
|
||||
entity.getTotalCartonCount(), entity.getCartonIndex(), entity.getBoxSpecification(),
|
||||
entity.getLengthCm(), entity.getWidthCm(), entity.getHeightCm(), entity.getSkuCode(), inbound.getInboundCode(),
|
||||
entity.getLengthCm(), entity.getWidthCm(), entity.getHeightCm(), displaySkuCodes(entity, skuRowsByLabel), inbound.getInboundCode(),
|
||||
fallback(inbound.getWarehouseName(), "WAREHOUSE"),
|
||||
"CTNS:" + entity.getTotalCartonCount() + "-" + entity.getCartonIndex(),
|
||||
fallback(inbound.getChannelName(), fallback(inbound.getChannelCode(), "ROUTE"))))
|
||||
@@ -314,9 +354,20 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
|
||||
return StringUtils.hasText(value) ? value.trim() : fallback;
|
||||
}
|
||||
|
||||
private String displaySkuCodes(InboundCartonLabelEntity label,
|
||||
Map<Long, List<InboundCartonLabelSkuMapper.SkuRow>> skuRowsByLabel) {
|
||||
List<InboundCartonLabelSkuMapper.SkuRow> lines = skuRowsByLabel.get(label.getId());
|
||||
if (lines == null || lines.isEmpty()) return label.getSkuCode();
|
||||
return lines.stream().map(InboundCartonLabelSkuMapper.SkuRow::getSkuCode)
|
||||
.filter(StringUtils::hasText).distinct().collect(java.util.stream.Collectors.joining(" / "));
|
||||
}
|
||||
|
||||
private void requireIds(long tenantId, long inboundId) {
|
||||
if (tenantId <= 0 || inboundId <= 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "客户ID和入库单ID必须大于0");
|
||||
}
|
||||
}
|
||||
|
||||
private record CargoGroup(String groupNo, int packageCount,
|
||||
List<OrderCargoClient.CargoLabelBinding> cargoes) {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user