feat(selfop): filter turnover by multiple owners across all aggregates
This commit is contained in:
+6
-3
@@ -8,11 +8,11 @@ import org.springframework.format.annotation.DateTimeFormat;
|
||||
/** Separate from DETAIL: its dates, windows and filters must never be silently mixed. */
|
||||
public record InventoryTurnoverRequest(String sourceAccount, String warehouseCode,
|
||||
@DateTimeFormat(iso=DateTimeFormat.ISO.DATE) LocalDate endDate, Integer baseWindowDays,
|
||||
String groupBy, String customerCode, String sku, String focus, Integer coverageDaysThreshold,
|
||||
String groupBy, java.util.List<String> customerCodes, String sku, String focus, Integer coverageDaysThreshold,
|
||||
String turnoverStatus, String sortBy, Integer current, Integer size, Boolean includeOverview,
|
||||
String snapshotContext, Boolean hideZeroInventory) {
|
||||
private static final Set<String> PARAMETERS=Set.of("sourceAccount","warehouseCode","endDate","baseWindowDays",
|
||||
"groupBy","customerCode","sku","focus","coverageDaysThreshold","turnoverStatus","sortBy","current","size","includeOverview","snapshotContext","hideZeroInventory");
|
||||
"groupBy","customerCodes","sku","focus","coverageDaysThreshold","turnoverStatus","sortBy","current","size","includeOverview","snapshotContext","hideZeroInventory");
|
||||
public static void validateParameters(Set<String> keys) {
|
||||
if(!PARAMETERS.containsAll(keys)) throw new BizException("库存周转不接受其他视图的查询参数");
|
||||
}
|
||||
@@ -29,7 +29,10 @@ public record InventoryTurnoverRequest(String sourceAccount, String warehouseCod
|
||||
public LocalDate end(LocalDate today){return endDate==null?today.minusDays(1):endDate;}
|
||||
public void validate(LocalDate today) {
|
||||
AnalyticsQuery.text(sourceAccount,64,"来源账号"); AnalyticsQuery.text(warehouseCode,64,"仓库");
|
||||
if(customerCode!=null) AnalyticsQuery.text(customerCode,64,"货主");
|
||||
if(customerCodes!=null) {
|
||||
if(customerCodes.size()>100) throw new BizException("最多选择100个货主");
|
||||
for(String code:customerCodes) AnalyticsQuery.text(code,64,"货主");
|
||||
}
|
||||
if(sku!=null) AnalyticsQuery.text(sku,128,"SKU");
|
||||
if(days()<1||days()>366) throw new BizException("统计区间须为1至366个完整自然日");
|
||||
if(!end(today).isBefore(today)) throw new BizException("截止日必须是北京时间已结束的日期");
|
||||
|
||||
+14
-10
@@ -36,9 +36,13 @@ public final class InventoryTurnoverSql {
|
||||
"VALID".equals(frozen.response().analyticsContext().inventoryStatus()),frozen.snapshot().sourceKnown(),days,LocalDateTime.ofInstant(frozen.response().queryAsOf(),ZoneOffset.UTC));
|
||||
}
|
||||
private static String windowStart(int n){return "#{p.from"+(java.util.Set.of(7,15,30).contains(n)?Integer.toString(n):"Base")+"}";}
|
||||
private static String scope(String alias) {
|
||||
return alias+"source_account=#{p.query.sourceAccount} AND "+alias+"warehouse_code=#{p.query.warehouseCode}"
|
||||
+" AND (#{p.query.customerCode,jdbcType=VARCHAR} IS NULL OR "+alias+"customer_code=#{p.query.customerCode,jdbcType=VARCHAR})";
|
||||
private static String scope(Input p,String alias) {
|
||||
String base=alias+"source_account=#{p.query.sourceAccount} AND "+alias+"warehouse_code=#{p.query.warehouseCode}";
|
||||
var owners=p.query.customerCodes();
|
||||
if(owners==null||owners.isEmpty())return base;
|
||||
var values=new ArrayList<String>();
|
||||
for(int i=0;i<owners.size();i++)values.add("#{p.query.customerCodes["+i+"],jdbcType=VARCHAR}");
|
||||
return base+" AND "+alias+"customer_code IN ("+String.join(",",values)+")";
|
||||
}
|
||||
private static String valid(String field){return "product_quality IN (0,1) AND "+field+" IS NOT NULL AND "+field+">=0 AND (quality_status='VALID' OR quality_reason IN ('FIELD_MISSING','NEGATIVE_QUANTITY'))";}
|
||||
private static String amount(String field){return "CASE WHEN MIN(CASE WHEN "+valid(field)+" THEN 1 ELSE 0 END)=1 THEN SUM("+field+") END";}
|
||||
@@ -56,27 +60,27 @@ public final class InventoryTurnoverSql {
|
||||
runs.add("#{p.days["+i+"].sampleRun,jdbcType=BIGINT}");
|
||||
return runs.isEmpty()?"NULL":String.join(",",runs);
|
||||
}
|
||||
private static String salesDays() {
|
||||
private static String salesDays(Input p) {
|
||||
return "SELECT customer_code,sku_code,business_date,SUM(outbound_quantity) quantity,MIN(CASE WHEN quantity_unit='PCS' AND outbound_quantity>=0 THEN 1 ELSE 0 END) unit_valid FROM ll_selfop_sku_daily_sales WHERE "
|
||||
+scope("")+" AND sales_run_id=#{p.salesRun} AND business_date>=CAST(#{p.from} AS DATE) AND business_date<CAST(#{p.until} AS DATE) GROUP BY customer_code,sku_code,business_date";
|
||||
+scope(p,"")+" AND sales_run_id=#{p.salesRun} AND business_date>=CAST(#{p.from} AS DATE) AND business_date<CAST(#{p.until} AS DATE) GROUP BY customer_code,sku_code,business_date";
|
||||
}
|
||||
private static String cte(Input p) {
|
||||
String stocks="SELECT snapshot_run_id,customer_code,sku_code,"+amount("physical_quantity")+" physical,"
|
||||
+amount("transit_quantity")+" transit,"+amount("frozen_quantity")+" frozen,"+AnalyticsInventorySql.availableQuantity(valid("available_quantity"))+" available,"
|
||||
+"CASE WHEN MIN(CASE WHEN product_quality NOT IN (0,1) OR product_quality IS NULL THEN 0 WHEN product_quality=1 AND NOT ("+valid("physical_quantity")+") THEN 0 ELSE 1 END)=1 "
|
||||
+"THEN SUM(CASE WHEN product_quality=1 THEN physical_quantity ELSE 0 END) END defective "
|
||||
+"FROM ll_selfop_inventory_snapshot WHERE "+scope("")+" AND (snapshot_run_id=#{p.inventoryRun} OR snapshot_run_id IN ("+sampleRuns(p)+")) "
|
||||
+"FROM ll_selfop_inventory_snapshot WHERE "+scope(p,"")+" AND (snapshot_run_id=#{p.inventoryRun} OR snapshot_run_id IN ("+sampleRuns(p)+")) "
|
||||
+"GROUP BY snapshot_run_id,customer_code,sku_code";
|
||||
String profiles="SELECT * FROM ll_selfop_sku_profile WHERE "+scope("")+" AND (profile_run_id IN (#{p.salesRun},#{p.inventoryRun}) OR profile_run_id IN ("+sampleRuns(p)+"))";
|
||||
String profiles="SELECT * FROM ll_selfop_sku_profile WHERE "+scope(p,"")+" AND (profile_run_id IN (#{p.salesRun},#{p.inventoryRun}) OR profile_run_id IN ("+sampleRuns(p)+"))";
|
||||
StringBuilder sql=new StringBuilder("WITH calendar AS (").append(calendar(p)).append("), fixed_profiles AS (").append(profiles)
|
||||
.append("), stocks AS (").append(stocks).append("), sales_days AS (").append(salesDays()).append("), candidates AS (")
|
||||
.append("), stocks AS (").append(stocks).append("), sales_days AS (").append(salesDays(p)).append("), candidates AS (")
|
||||
.append("SELECT customer_code,sku_code FROM fixed_profiles UNION SELECT customer_code,sku_code FROM stocks UNION SELECT customer_code,sku_code FROM sales_days), profiles AS (")
|
||||
.append("SELECT c.customer_code,c.sku_code,COALESCE(MAX(CASE WHEN f.profile_run_id=#{p.inventoryRun} THEN f.product_name END),MAX(f.product_name)) product_name,")
|
||||
.append("COALESCE(MAX(CASE WHEN f.profile_run_id=#{p.salesRun} THEN CASE WHEN f.quantity_unit='PCS' THEN 1 ELSE 0 END END),MAX(CASE WHEN f.profile_run_id=#{p.inventoryRun} THEN CASE WHEN f.quantity_unit='PCS' THEN 1 ELSE 0 END END),MIN(CASE WHEN f.quantity_unit='PCS' THEN 1 ELSE 0 END)) unit_valid,")
|
||||
.append("MIN(CASE WHEN f.evidence_run_id IS NOT NULL AND f.evidence_type IN ('VALID_OUTBOUND','INVENTORY_OBSERVATION') THEN f.earliest_evidence_at_utc END) evidence ")
|
||||
.append("FROM candidates c LEFT JOIN fixed_profiles f ON f.customer_code=c.customer_code AND f.sku_code=c.sku_code GROUP BY c.customer_code,c.sku_code)")
|
||||
.append(InventoryVolumeSql.products()).append(skuWindows(p)).append(", last_sales AS (SELECT customer_code,sku_code,MAX(business_date) last_outbound FROM ll_selfop_sku_daily_sales WHERE ")
|
||||
.append(scope("")).append(" AND sales_run_id=#{p.salesRun} AND business_date<CAST(#{p.until} AS DATE) AND outbound_quantity>0 AND quantity_unit='PCS' GROUP BY customer_code,sku_code)");
|
||||
.append(scope(p,"")).append(" AND sales_run_id=#{p.salesRun} AND business_date<CAST(#{p.until} AS DATE) AND outbound_quantity>0 AND quantity_unit='PCS' GROUP BY customer_code,sku_code)");
|
||||
sql.append(", sku_base AS (SELECT c.customer_code,c.sku_code,c.product_name,c.evidence,c.unit_valid,pv.unit_volume,COALESCE(pv.dimension_source,'MISSING') dimension_source");
|
||||
for(int n:p.query().windows()) for(String suffix:List.of("outbound","observed","complete","partial")) sql.append(",w.w").append(n).append('_').append(suffix);
|
||||
sql.append(",w.sample_count,w.sample_total,w.average_physical,w.sample_mask,ls.last_outbound");
|
||||
@@ -254,7 +258,7 @@ public final class InventoryTurnoverSql {
|
||||
+"SELECT 'OWNER' kind,customer_code,physical,0 unknown_count FROM ranked_owners UNION ALL SELECT 'META' kind,NULL customer_code,NULL physical,COUNT(*) unknown_count FROM owner_result WHERE physical IS NULL ORDER BY kind DESC,physical DESC,customer_code";
|
||||
}
|
||||
public static String chartWindows(Map<String,Object> args) {
|
||||
Input p=(Input)args.get("p");return "WITH calendar AS ("+calendar(p)+"), sales_days AS ("+salesDays()+"), warehouse_days AS (SELECT d.business_date,"
|
||||
Input p=(Input)args.get("p");return "WITH calendar AS ("+calendar(p)+"), sales_days AS ("+salesDays(p)+"), warehouse_days AS (SELECT d.business_date,"
|
||||
+"CASE WHEN #{p.salesKnown}=1 AND d.coverage=1 AND MIN(COALESCE(s.unit_valid,1))=1 THEN 1 ELSE 0 END complete,"
|
||||
+"CASE WHEN #{p.salesKnown}=1 THEN CASE WHEN COUNT(s.business_date)=0 AND d.coverage>0 THEN 0 ELSE SUM(CASE WHEN s.unit_valid=1 THEN s.quantity END) END END observed "
|
||||
+"FROM calendar d LEFT JOIN sales_days s ON s.business_date=d.business_date GROUP BY d.business_date,d.coverage), warehouse_window_counts AS (SELECT COUNT(*) day_count"+windowAggregates(p)+" FROM warehouse_days) SELECT w.*"+formalWindows(p)+" FROM warehouse_window_counts w";
|
||||
|
||||
Reference in New Issue
Block a user