feat(selfop): calculate inventory and outbound volume by owner and SKU

This commit is contained in:
2026-10-14 17:48:52 +08:00
parent 814eb11d2f
commit 8bc8518496
6 changed files with 205 additions and 26 deletions
@@ -36,6 +36,19 @@ public final class InventoryTurnoverPolicy {
var value=decimal(row,field);int unknown=owner?count(row,field+"_unknown"):value==null?1:0;
return new Total(value,owner?decimal(row,field+"_known"):value,unknown,unknown==0?List.of():List.of("INVENTORY_UNKNOWN_OR_STALE"));
}
static Volume volume(Map<String,Object> row,String field) {
var reasons=new ArrayList<String>();
int missing=count(row,field+"_missing_dimension_skus"),unknown=count(row,field+"_unknown_quantity_skus");
var declared=decimal(row,field+"_estimated_quantity");
if(missing>0)reasons.add("DIMENSIONS_MISSING_OR_INVALID");
if(unknown>0||decimal(row,field+"_volume")==null&&decimal(row,field+"_volume_known")==null)reasons.add("QUANTITY_UNAVAILABLE");
if(declared!=null&&declared.signum()>0)reasons.add("DECLARED_DIMENSIONS_ESTIMATE");
return new Volume(decimal(row,field+"_volume"),decimal(row,field+"_volume_known"),decimal(row,field+"_missing_dimension_quantity"),
missing,unknown,declared,List.copyOf(reasons));
}
static List<VolumeWindow> volumeWindows(Map<String,Object> row,LocalDate until,List<Integer> sizes) {
return sizes.stream().map(n->new VolumeWindow(n,until.minusDays(n),until,volume(row,"w"+n+"_outbound"))).toList();
}
private static Risk risk(Map<String,Object> row,String name,Total physical,boolean owner,boolean enabled) {
if(!enabled)return new Risk(null,null,0,null);
int unknown=owner?count(row,name+"_unknown"):row.get(name)==null?1:0;
@@ -69,17 +82,19 @@ public final class InventoryTurnoverPolicy {
total(r,"frozen",owner),total(r,"defective",owner),total(r,"transit",owner),owner?decimal(r,"physical_skus"):physical.value()==null?null:physical.value().signum()>0?BigDecimal.ONE:BigDecimal.ZERO,
windows,base.averageDailyOutboundQuantity(),coverage,coverageStatus,average,turnover,sampleStatus,sampleCount,q.days(),List.copyOf(missing),
"VALID".equals(sampleStatus)?List.of():List.of(sampleStatus),last==null?null:LocalDate.parse(last.substring(0,10)),List.copyOf(attention),
risk(r,"no_outbound",physical,owner,true),risk(r,"slow",physical,owner,q.coverageDaysThreshold()!=null),count(r,"unassessable"));
risk(r,"no_outbound",physical,owner,true),risk(r,"slow",physical,owner,q.coverageDaysThreshold()!=null),count(r,"unassessable"),
owner?null:decimal(r,"unit_volume"),owner?null:text(r,"dimension_source"),volume(r,"physical"),volumeWindows(r,context.endDate().plusDays(1),q.windows()));
}
static Overview overview(Map<String,Object> raw,Context context) {
var r=normalized(raw);int noUnknown=count(r,"no_outbound_unknown");var noKnown=decimal(r,"no_outbound_count");
if(count(r,"sku_count")==0&&!"VALID".equals(context.analyticsContext().inventoryStatus())) {
var unknown=new Total(null,null,0,List.of("INVENTORY_UNKNOWN_OR_STALE"));
return new Overview(unknown,unknown,new Total(null,null,0,List.of("SALES_OR_STOCK_UNASSESSABLE")),
new Total(null,null,0,List.of("SALES_OR_STOCK_UNASSESSABLE")));
new Total(null,null,0,List.of("SALES_OR_STOCK_UNASSESSABLE")),volume(Map.of(),"physical"),volumeWindows(r,context.endDate().plusDays(1),volumeSizes(context)));
}
return new Overview(total(r,"physical",true),total(r,"available",true),
new Total(noUnknown==0?noKnown:null,noKnown,noUnknown,noUnknown==0?List.of():List.of("SALES_OR_STOCK_UNASSESSABLE")),
new Total(decimal(r,"unassessable"),decimal(r,"unassessable"),0,List.of()));
new Total(decimal(r,"unassessable"),decimal(r,"unassessable"),0,List.of()),volume(r,"physical"),volumeWindows(r,context.endDate().plusDays(1),volumeSizes(context)));
}
private static List<Integer> volumeSizes(Context context){return java.util.stream.Stream.of(7,15,30,context.baseWindowDays()).distinct().sorted().toList();}
}
@@ -11,6 +11,9 @@ public final class InventoryTurnoverResponses {
BigDecimal observedQuantity, BigDecimal averageDailyOutboundQuantity, int completeDayCount,
int partialDayCount, int unknownDayCount, String dataStatus, List<String> reasonCodes) {}
public record Total(BigDecimal value, BigDecimal knownSubtotal, int unknownSkuCount, List<String> reasonCodes) {}
public record Volume(BigDecimal value,BigDecimal knownSubtotal,BigDecimal missingDimensionQuantity,
int missingDimensionSkuCount,int unknownQuantitySkuCount,BigDecimal declaredDimensionQuantity,List<String> reasonCodes) {}
public record VolumeWindow(int days,LocalDate startDate,LocalDate endDateExclusive,Volume volume) {}
public record Risk(BigDecimal matchedSkuCount, BigDecimal knownPhysicalQuantity, int unknownSkuCount, BigDecimal physicalQuantityRatio) {}
public record Row(String customerCode,String customerName,String sku,String productName,
Total physicalQuantity,Total availableQuantity,Total frozenQuantity,Total defectiveQuantity,Total transitQuantity,
@@ -18,13 +21,15 @@ public final class InventoryTurnoverResponses {
BigDecimal inventoryCoverageDays,String coverageStatus,BigDecimal averagePhysicalQuantity,
BigDecimal quantityTurnoverCount,String turnoverStatus,int qualifiedSampleDays,int requiredSampleDays,
List<LocalDate> missingSampleDates,List<String> sampleReasonCodes,LocalDate lastObservedOutboundDate,
List<String> attentionReasons,Risk noOutboundInWindow,Risk slowCoverage,int unassessableSkuCount) {}
List<String> attentionReasons,Risk noOutboundInWindow,Risk slowCoverage,int unassessableSkuCount,
BigDecimal unitVolumeM3,String dimensionSource,Volume inventoryVolumeM3,List<VolumeWindow> outboundVolumesM3) {}
public record Sample(LocalDate date,Long runId) {}
public record Context(String snapshotContext,String sourceAccount,String warehouseCode,LocalDate endDate,
int baseWindowDays,Instant queryAsOf,Instant queryExpiresAt,AnalyticsResponses.Context analyticsContext,
Instant inventoryObservedFrom,Instant inventoryObservedUntil,String sampleRuleVersion,
List<Sample> dailySamples,List<LocalDate> missingSampleDates,LocalDate accumulationStartedOn) {}
public record Overview(Total physicalQuantity,Total availableQuantity,Total noOutboundSkuCount,Total unassessableSkuCount) {}
public record Overview(Total physicalQuantity,Total availableQuantity,Total noOutboundSkuCount,Total unassessableSkuCount,
Volume inventoryVolumeM3,List<VolumeWindow> outboundVolumesM3) {}
public record OwnerPhysical(String customerCode,String customerName,BigDecimal physicalQuantity) {}
public record Charts(List<OwnerPhysical> ownerPhysicalTop,int unknownOwnerCount,List<Window> averageDaily) {}
public record Response(Context context,Overview summary,Charts charts,List<Row> rows,long total) {}
@@ -8,7 +8,7 @@ public final class InventoryTurnoverSql {
private InventoryTurnoverSql() {}
public record Day(LocalDate date,LocalDateTime startUtc,int coverage,Long sampleRun) {}
public record Input(InventoryTurnoverRequest query,long salesRun,long inventoryRun,boolean stockFresh,
boolean salesKnown,List<Day> days) {
boolean salesKnown,List<Day> days,LocalDateTime queryAsOf) {
public LocalDate until(){return days.get(days.size()-1).date().plusDays(1);}
public LocalDate from(){return days.get(0).date();}
public LocalDate fromBase(){return baseFrom();}
@@ -33,7 +33,7 @@ public final class InventoryTurnoverSql {
var days=frozen.response().dailySamples().stream().map(s->new Day(s.date(),s.date().atStartOfDay().minusHours(8),
!trusted?0:SalesQueryCoverage.complete(coverage,s.date(),s.date().plusDays(1))?1:SalesQueryCoverage.at(coverage,s.date())==null?0:2,s.runId())).toList();
return new Input(q,frozen.response().analyticsContext().salesRunId(),frozen.response().analyticsContext().inventoryRunId(),
"VALID".equals(frozen.response().analyticsContext().inventoryStatus()),frozen.snapshot().sourceKnown(),days);
"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) {
@@ -75,19 +75,19 @@ public final class InventoryTurnoverSql {
.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(skuWindows(p)).append(", last_sales AS (SELECT customer_code,sku_code,MAX(business_date) last_outbound FROM ll_selfop_sku_daily_sales WHERE ")
.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)");
sql.append(", sku_base AS (SELECT c.customer_code,c.sku_code,c.product_name,c.evidence,c.unit_valid");
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");
for(String field:List.of("physical","available","frozen","defective","transit")) 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("LEFT JOIN product_volumes pv ON pv.customer_code=c.customer_code AND pv.sku_code=c.sku_code 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 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 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("SELECT b.*").append(InventoryVolumeSql.sku(p)).append(",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();
}
@@ -184,17 +184,18 @@ public final class InventoryTurnoverSql {
+"CASE WHEN COUNT(CASE WHEN business_date>=CAST(#{p.baseFrom} AS DATE) THEN sample_physical END)=#{p.query.days} THEN SUM(CASE WHEN business_date>=CAST(#{p.baseFrom} AS DATE) THEN sample_physical END) END sample_total,"
+"CASE WHEN COUNT(CASE WHEN business_date>=CAST(#{p.baseFrom} AS DATE) THEN sample_physical END)=#{p.query.days} THEN SUM(CASE WHEN business_date>=CAST(#{p.baseFrom} AS DATE) THEN sample_physical END)/#{p.query.days} END average_physical,"
+sampleMask()+" sample_mask FROM owner_days GROUP BY customer_code), owner_windows AS (SELECT w.*"+formalWindows(p)+" FROM owner_window_counts w), owner_totals AS ("
+totals("customer_code",true)+" FROM sku_result GROUP BY customer_code), owner_result AS (SELECT t.*,w.sample_count,w.average_physical,w.sample_mask,"
+totals(p,"customer_code",true)+" FROM sku_result GROUP BY customer_code), owner_result AS (SELECT t.*,w.sample_count,w.average_physical,w.sample_mask,"
+"CASE WHEN w.sample_total>0 THEN w.w"+p.query.days()+"_outbound*#{p.query.days}/w.sample_total END turnover,"
+String.join(",",p.query().windows().stream().flatMap(n->List.of("outbound","observed","complete","partial").stream().map(s->"w.w"+n+"_"+s)).toList())
+" FROM owner_totals t JOIN owner_windows w ON w.customer_code=t.customer_code) ";
}
private static String totals(String key,boolean withFocus) {
private static String totals(Input p,String key,boolean withFocus) {
var sql=new StringBuilder("SELECT ").append(key==null?"COUNT(*) sku_count":key);
for(String field:List.of("physical","available","frozen","defective","transit")) sql.append(",CASE WHEN COUNT(").append(field).append(")=COUNT(*) THEN COALESCE(SUM(").append(field).append("),0) END ").append(field)
.append(",CASE WHEN COUNT(*)=0 THEN 0 ELSE SUM(").append(field).append(") END ").append(field).append("_known,COUNT(*)-COUNT(").append(field).append(") ").append(field).append("_unknown");
sql.append(",MAX(last_outbound) last_outbound,CASE WHEN COUNT(physical)=COUNT(*) THEN SUM(CASE WHEN physical>0 THEN 1 ELSE 0 END) END physical_skus,COALESCE(SUM(unassessable),0) unassessable");
for(String risk:List.of("no_outbound","slow")) sql.append(",COALESCE(SUM(CASE WHEN ").append(risk).append("=1 THEN 1 ELSE 0 END),0) ").append(risk).append("_count,COALESCE(SUM(CASE WHEN ").append(risk).append("=1 THEN physical ELSE 0 END),0) ").append(risk).append("_physical,COUNT(*)-COUNT(").append(risk).append(") ").append(risk).append("_unknown");
sql.append(InventoryVolumeSql.totals(p));
if(withFocus)sql.append(",MAX(CASE WHEN available>0 AND coverage_days IS NOT NULL AND coverage_days<=#{p.query.coverageDaysThreshold,jdbcType=INTEGER} THEN 1 ELSE 0 END) fast_count,MAX(CASE WHEN frozen>0 THEN 1 ELSE 0 END) frozen_count,MAX(CASE WHEN defective>0 THEN 1 ELSE 0 END) defective_count");
return sql.toString();
}
@@ -241,7 +242,7 @@ public final class InventoryTurnoverSql {
return cte(p)+(owner?ownerCte(p):"")+"SELECT COUNT(*) FROM "+(owner?"owner_result":"sku_result")+filter(p,owner);
}
public static String overview(Map<String,Object> args) {
Input p=(Input)args.get("p");return cte(p)+totals(null,false)+" FROM sku_result";
Input p=(Input)args.get("p");return cte(p)+totals(p,null,false)+" FROM sku_result";
}
public static String owners(Map<String,Object> args) {
Input p=(Input)args.get("p");return cte(p)+ownerCte(p)
@@ -263,22 +264,24 @@ public final class InventoryTurnoverSql {
String source=owner?"owner_result":"sku_result";
List<String> fields=new ArrayList<>(List.of("customer_code","physical","available","frozen","defective","transit","sample_count","average_physical","missing_dates","last_outbound","turnover","unassessable"));
for(int n:p.query().windows())for(String suffix:List.of("outbound","observed","complete","partial"))fields.add("w"+n+"_"+suffix);
fields.addAll(InventoryVolumeSql.fields(p));
if(owner) {
fields.add("physical_skus");
for(String field:List.of("physical","available","frozen","defective","transit"))for(String suffix:List.of("known","unknown"))fields.add(field+"_"+suffix);
for(String risk:List.of("no_outbound","slow"))for(String suffix:List.of("count","physical","unknown"))fields.add(risk+"_"+suffix);
} else fields.addAll(List.of("sku_code","product_name","coverage_days","no_outbound","slow"));
} else fields.addAll(List.of("sku_code","product_name","coverage_days","no_outbound","slow","unit_volume","dimension_source"));
// The physical-stock chart needs owner totals, not owner sales/sample windows.
String sql=cte(p)+(owner?ownerCte(p):overview?", owner_totals AS ("+totals("customer_code",true)+" FROM sku_result GROUP BY customer_code) ":"")
String sql=cte(p)+(owner?ownerCte(p):overview?", owner_totals AS ("+totals(p,"customer_code",true)+" FROM sku_result GROUP BY customer_code) ":"")
+", page_result AS (SELECT r.*,ROW_NUMBER() OVER ("+order(p,owner)+") ordinal FROM "+source+" r"+filter(p,owner)+order(p,owner)+" LIMIT #{p.query.pageSize} OFFSET #{p.query.offset})";
sql+=pageMissingDates();
if(overview)sql+=", summary_result AS ("+totals(null,false)+" FROM sku_result), top_owners AS (SELECT customer_code,physical,ROW_NUMBER() OVER (ORDER BY physical DESC,customer_code) ordinal FROM owner_totals WHERE physical IS NOT NULL ORDER BY physical DESC,customer_code LIMIT 10)";
if(overview)sql+=", summary_result AS ("+totals(p,null,false)+" FROM sku_result), top_owners AS (SELECT customer_code,physical,ROW_NUMBER() OVER (ORDER BY physical DESC,customer_code) ordinal FROM owner_totals WHERE physical IS NOT NULL ORDER BY physical DESC,customer_code LIMIT 10)";
// Statement-local only: share intermediate relations across report branches;
// do not change connection/global settings or any result/filter semantics.
sql+=" SELECT /*+ SET_VAR(group_concat_max_len=16384) SET_VAR(optimizer_switch='derived_merge=off') SET_VAR(optimizer_search_depth=4) */ 'ROW' section,ordinal,"+jsonFields(fields)+" payload FROM page_missing"
+" UNION ALL SELECT 'TOTAL' section,0 ordinal,JSON_OBJECT('total',COUNT(*)) payload FROM "+source+filter(p,owner);
if(overview) {
List<String> summary=new ArrayList<>(List.of("sku_count","unassessable","no_outbound_count","no_outbound_unknown"));
summary.addAll(InventoryVolumeSql.fields(p));
for(String field:List.of("physical","available")){summary.add(field);summary.add(field+"_known");summary.add(field+"_unknown");}
sql+=" UNION ALL SELECT 'OVERVIEW' section,0 ordinal,"+jsonFields(summary)+" payload FROM summary_result"
+" UNION ALL SELECT 'OWNER' section,ordinal,JSON_OBJECT('customer_code',customer_code,'physical',physical) payload FROM top_owners"
@@ -0,0 +1,64 @@
package com.baiyue.lastleg.selfoperated.analytics.query;
import java.util.ArrayList;
import java.util.List;
/** Volumes use a complete dimension triplet from the master-data version visible at queryAsOf. */
final class InventoryVolumeSql {
private InventoryVolumeSql() {}
private static String cube(String prefix) {
return "CASE WHEN " + prefix + "_length>0 AND " + prefix + "_width>0 AND " + prefix + "_height>0 THEN "
+ prefix + "_length*" + prefix + "_width*" + prefix + "_height*CASE LOWER(TRIM(" + prefix + "_size_unit))"
+ " WHEN 'mm' THEN 0.000000001 WHEN 'cm' THEN 0.000001 WHEN 'm' THEN 1"
+ " WHEN 'in' THEN 0.000016387064 WHEN 'ft' THEN 0.028316846592 END END";
}
static String products() {
return ", volume_master AS (SELECT r.id FROM ll_selfop_sync_run r JOIN ll_selfop_sync_task t ON t.id=r.task_id"
+ " WHERE r.source_account=#{p.query.sourceAccount} AND t.source_account=#{p.query.sourceAccount}"
+ " AND t.task_type='MASTER_DATA' AND t.task_role='ACCOUNT_DISCOVERY'"
+ " AND r.execution_purpose='BUSINESS' AND t.execution_purpose='BUSINESS'"
+ " AND r.status='PUBLISHED' AND r.coverage_status='COMPLETE' AND r.published_at_utc<=#{p.queryAsOf}"
+ " ORDER BY r.published_at_utc DESC,r.id DESC LIMIT 1), product_cubes AS ("
+ "SELECT customer_code,sku_code," + cube("measured") + " measured_cube," + cube("declared") + " declared_cube"
+ " FROM ll_selfop_product WHERE source_account=#{p.query.sourceAccount} AND run_id=(SELECT id FROM volume_master)),"
+ " product_volumes AS (SELECT customer_code,sku_code,COALESCE(measured_cube,declared_cube) unit_volume,"
+ "CASE WHEN measured_cube IS NOT NULL THEN 'WAREHOUSE_MEASURED' WHEN declared_cube IS NOT NULL THEN 'PRODUCT_DECLARED' ELSE 'MISSING' END dimension_source FROM product_cubes)";
}
static List<String> quantities(InventoryTurnoverSql.Input p) {
var fields=new ArrayList<String>();fields.add("physical");
p.query().windows().forEach(n->fields.add("w"+n+"_outbound"));return fields;
}
static final List<String> SUFFIXES=List.of("volume","volume_known","missing_dimension_quantity","missing_dimension_skus","unknown_quantity_skus","estimated_quantity");
static List<String> fields(InventoryTurnoverSql.Input p) {
return quantities(p).stream().flatMap(q->SUFFIXES.stream().map(s->q+"_"+s)).toList();
}
static String sku(InventoryTurnoverSql.Input p) {
var sql=new StringBuilder();
for(var q:quantities(p)) {
String volume="CASE WHEN "+q+"=0 THEN 0 WHEN "+q+">0 THEN "+q+"*unit_volume END";
sql.append(',').append(volume).append(' ').append(q).append("_volume")
.append(',').append(volume).append(' ').append(q).append("_volume_known")
.append(",CASE WHEN ").append(q).append(">0 AND unit_volume IS NULL THEN ").append(q).append(" ELSE 0 END ").append(q).append("_missing_dimension_quantity")
.append(",CASE WHEN ").append(q).append(">0 AND unit_volume IS NULL THEN 1 ELSE 0 END ").append(q).append("_missing_dimension_skus")
.append(",CASE WHEN ").append(q).append(" IS NULL THEN 1 ELSE 0 END ").append(q).append("_unknown_quantity_skus")
.append(",CASE WHEN ").append(q).append(">0 AND dimension_source='PRODUCT_DECLARED' THEN ").append(q).append(" ELSE 0 END ").append(q).append("_estimated_quantity");
}
return sql.toString();
}
static String totals(InventoryTurnoverSql.Input p) {
var sql=new StringBuilder();
for(var q:quantities(p)) {
String emptyKnown;
if(q.equals("physical"))emptyKnown="#{p.stockFresh}=1";
else {
int n=Integer.parseInt(q.substring(1,q.indexOf('_')));
String from="#{p.from"+(java.util.Set.of(7,15,30).contains(n)?Integer.toString(n):"Base")+"}";
emptyKnown="#{p.salesKnown}=1 AND (SELECT COUNT(*) FROM calendar WHERE business_date>=CAST("+from+" AS DATE) AND coverage=1)="+n;
}
sql.append(",CASE WHEN COUNT(*)=0 THEN CASE WHEN ").append(emptyKnown).append(" THEN 0 END WHEN COUNT(").append(q).append("_volume)=COUNT(*) THEN SUM(").append(q).append("_volume) END ").append(q).append("_volume")
.append(",CASE WHEN COUNT(*)=0 THEN CASE WHEN ").append(emptyKnown).append(" THEN 0 END ELSE SUM(").append(q).append("_volume) END ").append(q).append("_volume_known");
for(var suffix:SUFFIXES.subList(2,SUFFIXES.size()))sql.append(",COALESCE(SUM(").append(q).append('_').append(suffix).append("),0) ").append(q).append('_').append(suffix);
}
return sql.toString();
}
}