fix(self-operated): align turnover sales eligibility and known zero stock

This commit is contained in:
2026-10-14 14:01:39 +08:00
parent bc9c78122b
commit 6e7d922853
5 changed files with 89 additions and 18 deletions
@@ -14,6 +14,12 @@ public final class AnalyticsInventorySql {
+" UNION SELECT customer_code,sku_code FROM ll_selfop_sku_profile WHERE profile_run_id=#{p.inventoryRun} AND"+scope(p);}
private static String valid(String field){return field+" IS NOT NULL AND "+field+">=0 AND (quality_status='VALID' OR quality_reason IN ('FIELD_MISSING','NEGATIVE_QUANTITY'))";}
private static String quantity(String field){return "CASE WHEN MIN(CASE WHEN "+valid(field)+" THEN 1 ELSE 0 END)=1 THEN SUM("+field+") END";}
static String availableQuantity(String validAvailable){
// A published full snapshot can contain only a valid, empty defective row.
// Do not confuse that known zero with a missing or invalid normal-quality row.
return "CASE WHEN MIN(CASE WHEN product_quality=1 AND quality_status='VALID' AND physical_quantity=0 AND available_quantity=0 AND frozen_quantity=0 THEN 1 ELSE 0 END)=1 THEN 0 "
+"ELSE MAX(CASE WHEN product_quality=0 AND "+validAvailable+" THEN available_quantity END) END";
}
private static String defectiveQuantity(){return "CASE WHEN MIN(CASE WHEN product_quality NOT IN (0,1) THEN 0 WHEN product_quality=1 THEN CASE WHEN "+valid("physical_quantity")+" THEN 1 ELSE 0 END ELSE 1 END)=1 "
+"THEN SUM(CASE WHEN product_quality=1 THEN physical_quantity ELSE 0 END) END";}
static String cte(AnalyticsSqlRequest p){return cte(p,false);}
@@ -31,7 +37,7 @@ public final class AnalyticsInventorySql {
+"FROM candidates c LEFT JOIN ll_selfop_sku_profile s ON s.profile_run_id=#{p.salesRun} AND s.source_account=#{p.account} AND s.warehouse_code=#{p.warehouse} AND s.customer_code=c.customer_code AND s.sku_code=c.sku_code "
+"LEFT JOIN ll_selfop_sku_profile i ON i.profile_run_id=#{p.inventoryRun} AND i.source_account=#{p.account} AND i.warehouse_code=#{p.warehouse} AND i.customer_code=c.customer_code AND i.sku_code=c.sku_code),"
+" stock AS (SELECT customer_code,sku_code,"+quantity("physical_quantity")+" physical_quantity,"+quantity("frozen_quantity")+" frozen_quantity,"
+quantity("transit_quantity")+" transit_quantity,MAX(CASE WHEN product_quality=0 AND "+valid("available_quantity")+" THEN available_quantity END) available_quantity,"
+quantity("transit_quantity")+" transit_quantity,"+availableQuantity(valid("available_quantity"))+" available_quantity,"
+defectiveQuantity()+" defective_physical_quantity,MAX(observed_at_utc) observed_at_utc "
+"FROM ll_selfop_inventory_snapshot WHERE "+(dashboard&&!p.stockFresh()?"1=0 /* dashboard unused stock */ AND ":"")
+"snapshot_run_id=#{p.inventoryRun} AND"+scope(p)+" GROUP BY customer_code,sku_code),"
@@ -62,7 +62,7 @@ public final class InventoryTurnoverSql {
}
private static String cte(Input p) {
String stocks="SELECT snapshot_run_id,customer_code,sku_code,"+amount("physical_quantity")+" physical,"
+amount("frozen_quantity")+" frozen,MAX(CASE WHEN product_quality=0 AND "+valid("available_quantity")+" THEN available_quantity END) available,"
+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)+")) "
@@ -83,33 +83,35 @@ public final class InventoryTurnoverSql {
for(String field:List.of("physical","available","frozen","defective")) sql.append(",CASE WHEN #{p.stockFresh}=1 AND ip.quantity_unit='PCS' THEN st.").append(field).append(" END ").append(field);
sql.append(" FROM profiles c JOIN sku_windows w ON w.customer_code=c.customer_code AND w.sku_code=c.sku_code LEFT JOIN last_sales ls ON ls.customer_code=c.customer_code AND ls.sku_code=c.sku_code LEFT JOIN stocks st ON st.snapshot_run_id=#{p.inventoryRun} AND st.customer_code=c.customer_code AND st.sku_code=c.sku_code ")
.append("LEFT JOIN fixed_profiles ip ON ip.profile_run_id=#{p.inventoryRun} AND ip.customer_code=c.customer_code AND ip.sku_code=c.sku_code), sku_calculated AS (SELECT b.*,")
.append("CASE WHEN available=0 THEN 0 WHEN available>0 AND w").append(p.query.days()).append("_outbound>0 AND evidence<=#{p.baseFromUtc} THEN available*#{p.query.days}/w").append(p.query.days()).append("_outbound END coverage_days,")
.append("CASE WHEN available=0 THEN 0 WHEN available>0 AND w").append(p.query.days()).append("_outbound>0 THEN available*#{p.query.days}/w").append(p.query.days()).append("_outbound END coverage_days,")
.append("CASE WHEN sample_total>0 AND evidence<=#{p.baseFromUtc} THEN w").append(p.query.days()).append("_outbound*#{p.query.days}/sample_total END turnover,")
.append("CASE WHEN physical=0 THEN 0 WHEN physical>0 AND w"+p.query.days()+"_outbound IS NOT NULL AND evidence<=#{p.baseFromUtc} THEN CASE WHEN w"+p.query.days()+"_outbound=0 THEN 1 ELSE 0 END END no_outbound,")
.append("CASE WHEN physical IS NULL OR available IS NULL OR w").append(p.query.days()).append("_outbound IS NULL OR evidence IS NULL OR evidence>#{p.baseFromUtc} THEN 1 ELSE 0 END unassessable FROM sku_base b), sku_result AS (")
.append("CASE WHEN physical=0 THEN 0 WHEN physical>0 AND w"+p.query.days()+"_outbound IS NOT NULL THEN CASE WHEN w"+p.query.days()+"_outbound=0 THEN 1 ELSE 0 END END no_outbound,")
.append("CASE WHEN physical IS NULL OR available IS NULL OR w").append(p.query.days()).append("_outbound IS NULL THEN 1 ELSE 0 END unassessable FROM sku_base b), sku_result AS (")
.append("SELECT b.*,CASE WHEN #{p.query.coverageDaysThreshold,jdbcType=INTEGER} IS NULL THEN NULL WHEN physical=0 OR available=0 THEN 0 WHEN coverage_days IS NOT NULL AND available>0 THEN CASE WHEN coverage_days>#{p.query.coverageDaysThreshold,jdbcType=INTEGER} THEN 1 ELSE 0 END WHEN available>0 AND w")
.append(p.query.days()).append("_outbound=0 AND unassessable=0 THEN 0 END slow FROM sku_calculated b) ");
return sql.toString();
}
private static String skuWindows(Input p) {
// One shared suffix table qualifies absent sales dates. Millisecond evidence
// after a midnight selects the next midnight, never the truncated same date.
var sql=new StringBuilder(", evidence_boundaries AS (SELECT start_utc,LAG(start_utc) OVER (ORDER BY start_utc) previous_start");
// Published warehouse coverage qualifies absent sales dates independently of
// SKU observation age. Actual invalid rows below revoke their zero allowance.
var sql=new StringBuilder(", window_coverage AS (SELECT ");
boolean first=true;
for(int n:p.query().windows()) for(String kind:List.of("complete","known","partial")) {
String coverage=switch(kind){case "complete"->"coverage=1";case "known"->"coverage>0";default->"coverage=2";};
sql.append(",SUM(CASE WHEN business_date>=CAST(#{p.from").append(java.util.Set.of(7,15,30).contains(n)?Integer.toString(n):"Base").append("} AS DATE) AND ").append(coverage)
.append(" THEN 1 ELSE 0 END) OVER (ORDER BY start_utc ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) w").append(n).append('_').append(kind);
if(!first)sql.append(',');first=false;
sql.append("SUM(CASE WHEN business_date>=CAST(#{p.from").append(java.util.Set.of(7,15,30).contains(n)?Integer.toString(n):"Base").append("} AS DATE) AND ").append(coverage)
.append(" THEN 1 ELSE 0 END) w").append(n).append('_').append(kind);
}
sql.append(" FROM calendar), sku_zero_counts AS (SELECT c.customer_code,c.sku_code");
for(int n:p.query().windows()) for(String kind:List.of("complete","known","partial"))
sql.append(",CASE WHEN c.unit_valid=1").append("complete".equals(kind)?"":" AND #{p.salesKnown}=1")
.append(" THEN COALESCE(b.w").append(n).append('_').append(kind).append(",0) ELSE 0 END w").append(n).append('_').append(kind);
sql.append(" FROM profiles c LEFT JOIN evidence_boundaries b ON c.evidence<=b.start_utc AND (b.previous_start IS NULL OR c.evidence>b.previous_start)), actual_sku_days AS (")
sql.append(" FROM profiles c CROSS JOIN window_coverage b), actual_sku_days AS (")
.append("SELECT s.customer_code,s.sku_code,s.business_date,CASE WHEN #{p.salesKnown}=1 AND c.unit_valid=1 AND s.unit_valid=1 THEN s.quantity END observed,")
.append("CASE WHEN d.coverage=1 AND c.unit_valid=1 AND s.unit_valid=1 THEN 1 ELSE 0 END complete,")
.append("CASE WHEN d.coverage=1 AND c.unit_valid=1 AND c.evidence<=d.start_utc THEN 1 ELSE 0 END zero_complete,")
.append("CASE WHEN #{p.salesKnown}=1 AND c.unit_valid=1 AND d.coverage>0 AND c.evidence<=d.start_utc THEN 1 ELSE 0 END zero_known,")
.append("CASE WHEN #{p.salesKnown}=1 AND c.unit_valid=1 AND d.coverage=2 AND c.evidence<=d.start_utc THEN 1 ELSE 0 END zero_partial ")
.append("CASE WHEN d.coverage=1 AND c.unit_valid=1 THEN 1 ELSE 0 END zero_complete,")
.append("CASE WHEN #{p.salesKnown}=1 AND c.unit_valid=1 AND d.coverage>0 THEN 1 ELSE 0 END zero_known,")
.append("CASE WHEN #{p.salesKnown}=1 AND c.unit_valid=1 AND d.coverage=2 THEN 1 ELSE 0 END zero_partial ")
.append("FROM sales_days s JOIN profiles c ON c.customer_code=s.customer_code AND c.sku_code=s.sku_code JOIN calendar d ON d.business_date=s.business_date), sku_actual_counts AS (SELECT customer_code,sku_code");
for(int n:p.query().windows()) {
String range="business_date>=CAST("+windowStart(n)+" AS DATE)";