From 2b3b13913b8d3ed567d2a20fcab4111f9511a1aa Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 14 Oct 2026 14:08:52 +0800 Subject: [PATCH] feat(self-operated): expose and sort transit stock in turnover views --- .../query/InventoryTurnoverPolicy.java | 2 +- .../query/InventoryTurnoverRequest.java | 2 +- .../query/InventoryTurnoverResponses.java | 2 +- .../analytics/query/InventoryTurnoverSql.java | 11 ++++---- .../self-operated-inventory-turnover-v1.yaml | 7 ++++-- .../query/InventoryTurnoverSqlTest.java | 25 ++++++++++++++++--- 6 files changed, 35 insertions(+), 14 deletions(-) diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverPolicy.java b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverPolicy.java index e213d7485..a3075b78e 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverPolicy.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverPolicy.java @@ -66,7 +66,7 @@ public final class InventoryTurnoverPolicy { if(count(r,"unassessable")>0)attention.add("UNASSESSABLE"); String last=text(r,"last_outbound"); return new Row(text(r,"customer_code"),null,owner?null:text(r,"sku_code"),owner?null:text(r,"product_name"),physical,available, - total(r,"frozen",owner),total(r,"defective",owner),owner?decimal(r,"physical_skus"):physical.value()==null?null:physical.value().signum()>0?BigDecimal.ONE:BigDecimal.ZERO, + 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")); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverRequest.java b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverRequest.java index 885658381..2115afd66 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverRequest.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverRequest.java @@ -42,7 +42,7 @@ public record InventoryTurnoverRequest(String sourceAccount, String warehouseCod if(!Set.of("PHYSICAL_ASC","PHYSICAL_DESC","AVAILABLE_ASC","AVAILABLE_DESC", "OUTBOUND_7_ASC","OUTBOUND_7_DESC","OUTBOUND_15_ASC","OUTBOUND_15_DESC","OUTBOUND_30_ASC","OUTBOUND_30_DESC", "AVERAGE_DAILY_ASC","AVERAGE_DAILY_DESC","COVERAGE_ASC","COVERAGE_DESC", - "FROZEN_ASC","FROZEN_DESC","DEFECTIVE_ASC","DEFECTIVE_DESC","AVERAGE_PHYSICAL_ASC","AVERAGE_PHYSICAL_DESC", + "TRANSIT_ASC","TRANSIT_DESC","FROZEN_ASC","FROZEN_DESC","DEFECTIVE_ASC","DEFECTIVE_DESC","AVERAGE_PHYSICAL_ASC","AVERAGE_PHYSICAL_DESC", "TURNOVER_ASC","TURNOVER_DESC","NO_OUTBOUND_PHYSICAL_DESC").contains(sort()) || "OWNER".equals(group())&&sort().startsWith("COVERAGE_") || "SKU".equals(group())&&"NO_OUTBOUND_PHYSICAL_DESC".equals(sort())) throw new BizException("排序不适用于当前分组"); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverResponses.java b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverResponses.java index b850b5989..6e2b1d324 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverResponses.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverResponses.java @@ -13,7 +13,7 @@ public final class InventoryTurnoverResponses { public record Total(BigDecimal value, BigDecimal knownSubtotal, int unknownSkuCount, List reasonCodes) {} 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 physicalQuantity,Total availableQuantity,Total frozenQuantity,Total defectiveQuantity,Total transitQuantity, BigDecimal physicalSkuCount,List windows,BigDecimal averageDailyOutboundQuantity, BigDecimal inventoryCoverageDays,String coverageStatus,BigDecimal averagePhysicalQuantity, BigDecimal quantityTurnoverCount,String turnoverStatus,int qualifiedSampleDays,int requiredSampleDays, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverSql.java b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverSql.java index f13f745ca..18e530d73 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverSql.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverSql.java @@ -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,"+AnalyticsInventorySql.availableQuantity(valid("available_quantity"))+" available," + +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)+")) " @@ -80,7 +80,7 @@ public final class InventoryTurnoverSql { sql.append(", sku_base AS (SELECT c.customer_code,c.sku_code,c.product_name,c.evidence,c.unit_valid"); 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")) sql.append(",CASE WHEN #{p.stockFresh}=1 AND ip.quantity_unit='PCS' THEN st.").append(field).append(" END ").append(field); + 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("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,") @@ -191,7 +191,7 @@ public final class InventoryTurnoverSql { } private static String totals(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")) sql.append(",CASE WHEN COUNT(").append(field).append(")=COUNT(*) THEN COALESCE(SUM(").append(field).append("),0) END ").append(field) + 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"); @@ -222,6 +222,7 @@ public final class InventoryTurnoverSql { // The denominator is constant for the whole query; use the unrounded numerator. case "AVERAGE_DAILY_ASC","AVERAGE_DAILY_DESC" -> "w"+p.query.days()+"_outbound"; case "COVERAGE_ASC","COVERAGE_DESC" -> "coverage_days"; + case "TRANSIT_ASC","TRANSIT_DESC" -> "transit"; case "FROZEN_ASC","FROZEN_DESC" -> "frozen"; case "DEFECTIVE_ASC","DEFECTIVE_DESC" -> "defective"; case "AVERAGE_PHYSICAL_ASC","AVERAGE_PHYSICAL_DESC" -> "average_physical"; @@ -260,11 +261,11 @@ public final class InventoryTurnoverSql { public static String report(Map args) { Input p=(Input)args.get("p");boolean owner="OWNER".equals(p.query.group()),overview=p.query.overview(); String source=owner?"owner_result":"sku_result"; - List fields=new ArrayList<>(List.of("customer_code","physical","available","frozen","defective","sample_count","average_physical","missing_dates","last_outbound","turnover","unassessable")); + List 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); if(owner) { fields.add("physical_skus"); - for(String field:List.of("physical","available","frozen","defective"))for(String suffix:List.of("known","unknown"))fields.add(field+"_"+suffix); + 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")); // The physical-stock chart needs owner totals, not owner sales/sample windows. diff --git a/lastleg-service/src/main/resources/openapi/self-operated-inventory-turnover-v1.yaml b/lastleg-service/src/main/resources/openapi/self-operated-inventory-turnover-v1.yaml index 5529a8792..d071633da 100644 --- a/lastleg-service/src/main/resources/openapi/self-operated-inventory-turnover-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/self-operated-inventory-turnover-v1.yaml @@ -42,7 +42,7 @@ paths: - name: sortBy in: query description: 'OWNER禁止COVERAGE_ASC/DESC,SKU禁止NO_OUTBOUND_PHYSICAL_DESC。全量筛选后按正式未舍入值排序再分页,未知值始终末尾,同值按SKU及货主升序。AVERAGE_DAILY使用baseWindowDays指定的日均窗口。' - schema: {type: string, default: PHYSICAL_DESC, enum: [PHYSICAL_ASC, PHYSICAL_DESC, AVAILABLE_ASC, AVAILABLE_DESC, OUTBOUND_7_ASC, OUTBOUND_7_DESC, OUTBOUND_15_ASC, OUTBOUND_15_DESC, OUTBOUND_30_ASC, OUTBOUND_30_DESC, AVERAGE_DAILY_ASC, AVERAGE_DAILY_DESC, COVERAGE_ASC, COVERAGE_DESC, FROZEN_ASC, FROZEN_DESC, DEFECTIVE_ASC, DEFECTIVE_DESC, AVERAGE_PHYSICAL_ASC, AVERAGE_PHYSICAL_DESC, TURNOVER_ASC, TURNOVER_DESC, NO_OUTBOUND_PHYSICAL_DESC]} + schema: {type: string, default: PHYSICAL_DESC, enum: [PHYSICAL_ASC, PHYSICAL_DESC, AVAILABLE_ASC, AVAILABLE_DESC, OUTBOUND_7_ASC, OUTBOUND_7_DESC, OUTBOUND_15_ASC, OUTBOUND_15_DESC, OUTBOUND_30_ASC, OUTBOUND_30_DESC, AVERAGE_DAILY_ASC, AVERAGE_DAILY_DESC, COVERAGE_ASC, COVERAGE_DESC, TRANSIT_ASC, TRANSIT_DESC, FROZEN_ASC, FROZEN_DESC, DEFECTIVE_ASC, DEFECTIVE_DESC, AVERAGE_PHYSICAL_ASC, AVERAGE_PHYSICAL_DESC, TURNOVER_ASC, TURNOVER_DESC, NO_OUTBOUND_PHYSICAL_DESC]} - {name: current, in: query, schema: {type: integer, minimum: 1, default: 1}} - {name: size, in: query, schema: {type: integer, minimum: 1, maximum: 100, default: 20}} - {name: includeOverview, in: query, schema: {type: boolean, default: true}} @@ -133,7 +133,7 @@ components: accumulationStartedOn: {type: string, format: date, nullable: true} Row: type: object - required: [customerCode, customerName, sku, productName, physicalQuantity, availableQuantity, frozenQuantity, defectiveQuantity, physicalSkuCount, windows, averageDailyOutboundQuantity, inventoryCoverageDays, coverageStatus, averagePhysicalQuantity, quantityTurnoverCount, turnoverStatus, qualifiedSampleDays, requiredSampleDays, missingSampleDates, sampleReasonCodes, lastObservedOutboundDate, attentionReasons, noOutboundInWindow, slowCoverage, unassessableSkuCount] + required: [customerCode, customerName, sku, productName, physicalQuantity, availableQuantity, frozenQuantity, defectiveQuantity, transitQuantity, physicalSkuCount, windows, averageDailyOutboundQuantity, inventoryCoverageDays, coverageStatus, averagePhysicalQuantity, quantityTurnoverCount, turnoverStatus, qualifiedSampleDays, requiredSampleDays, missingSampleDates, sampleReasonCodes, lastObservedOutboundDate, attentionReasons, noOutboundInWindow, slowCoverage, unassessableSkuCount] properties: customerCode: {type: string} customerName: {type: string, nullable: true} @@ -143,6 +143,9 @@ components: availableQuantity: {$ref: '#/components/schemas/Total'} frozenQuantity: {$ref: '#/components/schemas/Total'} defectiveQuantity: {$ref: '#/components/schemas/Total'} + transitQuantity: + allOf: [{$ref: '#/components/schemas/Total'}] + description: 当前固定库存版本的全品质在途件数;独立展示,不计入实物、可售或周转分母;缺失、异常或过期保留未知。 physicalSkuCount: {$ref: '#/components/schemas/Quantity'} windows: {type: array, minItems: 3, maxItems: 4, items: {$ref: '#/components/schemas/Window'}} averageDailyOutboundQuantity: {$ref: '#/components/schemas/Quantity'} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverSqlTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverSqlTest.java index 682f7d4d1..2c33f87be 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverSqlTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverSqlTest.java @@ -20,7 +20,7 @@ class InventoryTurnoverSqlTest { execute("CREATE ALIAS CONVERT_TZ FOR 'com.baiyue.lastleg.selfoperated.analytics.query.AnalyticsSqlSemanticTest.convertZone'"); execute("CREATE TABLE ll_selfop_sku_profile(profile_run_id BIGINT,source_account VARCHAR,warehouse_code VARCHAR,customer_code VARCHAR,sku_code VARCHAR,product_name VARCHAR,quantity_unit VARCHAR,evidence_type VARCHAR,evidence_run_id BIGINT,earliest_evidence_at_utc TIMESTAMP)"); execute("CREATE TABLE ll_selfop_sku_daily_sales(sales_run_id BIGINT,source_account VARCHAR,warehouse_code VARCHAR,customer_code VARCHAR,sku_code VARCHAR,business_date DATE,outbound_quantity DECIMAL(20,6),quantity_unit VARCHAR)"); - execute("CREATE TABLE ll_selfop_inventory_snapshot(snapshot_run_id BIGINT,source_account VARCHAR,warehouse_code VARCHAR,customer_code VARCHAR,sku_code VARCHAR,product_quality INT,physical_quantity DECIMAL(20,6),available_quantity DECIMAL(20,6),frozen_quantity DECIMAL(20,6),quality_status VARCHAR,quality_reason VARCHAR)"); + execute("CREATE TABLE ll_selfop_inventory_snapshot(snapshot_run_id BIGINT,source_account VARCHAR,warehouse_code VARCHAR,customer_code VARCHAR,sku_code VARCHAR,product_quality INT,physical_quantity DECIMAL(20,6),available_quantity DECIMAL(20,6),frozen_quantity DECIMAL(20,6),transit_quantity DECIMAL(20,6),quality_status VARCHAR,quality_reason VARCHAR)"); } @AfterEach void close() throws Exception {db.close();} static InventoryTurnoverRequest request(String group,String focus,String sort,String sku,Integer threshold,int page,int size) { @@ -61,6 +61,23 @@ class InventoryTurnoverSqlTest { execute("UPDATE ll_selfop_inventory_snapshot SET available_quantity=NULL,quality_status='UNKNOWN',quality_reason='FIELD_MISSING' WHERE product_quality=0"); assertNull(query("page",input(q,30,0,true)).get(0).get("available")); } + @Test void transitUsesBothQualitiesAndPropagatesUnknownWithoutChangingStockMetrics() throws Exception { + seed("A","KNOWN",0);seed("A","UNKNOWN",0); + execute("UPDATE ll_selfop_inventory_snapshot SET transit_quantity=CASE WHEN product_quality=0 THEN 5 ELSE 2 END WHERE snapshot_run_id=22"); + execute("UPDATE ll_selfop_inventory_snapshot SET transit_quantity=NULL,quality_status='UNKNOWN',quality_reason='FIELD_MISSING' WHERE snapshot_run_id=22 AND sku_code='UNKNOWN' AND product_quality=0"); + var q=request("SKU","ALL","TRANSIT_DESC",null,null,1,20); + var rows=reportRows(input(q,30,0,true)); + assertEquals("KNOWN",rows.get(0).get("sku_code")); + var known=InventoryTurnoverPolicy.row(rows.get(0),q,InventoryTurnoverPolicyTest.context()); + decimal("7",known.transitQuantity().value());decimal("610",known.physicalQuantity().value()); + decimal("600",known.availableQuantity().value());decimal("60",known.inventoryCoverageDays()); + assertNull(rows.get(1).get("transit"));decimal("0",rows.get(1).get("unassessable")); + var ownerQuery=request("OWNER","ALL","TRANSIT_DESC",null,null,1,20); + var owner=InventoryTurnoverPolicy.row(reportRows(input(ownerQuery,30,0,true)).get(0),ownerQuery,InventoryTurnoverPolicyTest.context()); + assertNull(owner.transitQuantity().value());decimal("7",owner.transitQuantity().knownSubtotal()); + assertEquals(1,owner.transitQuantity().unknownSkuCount());decimal("1220",owner.physicalQuantity().value()); + assertTrue(query("page",input(q,30,0,false)).stream().allMatch(r->r.get("transit")==null)); + } @Test void threeWindowsAndDailyQualitiesAreNotMultiplied() throws Exception { seed("A","SKU",30); var p=input(request("SKU","ALL","PHYSICAL_DESC",null,null,1,20),30,30,true); @@ -504,10 +521,10 @@ class InventoryTurnoverSqlTest { return raw instanceof byte[] bytes?json.readTree(bytes):json.readTree(raw.toString()); } @org.junit.jupiter.params.ParameterizedTest - @org.junit.jupiter.params.provider.ValueSource(strings={"PHYSICAL","AVAILABLE","FROZEN","DEFECTIVE","AVERAGE_PHYSICAL","TURNOVER","COVERAGE","OUTBOUND_7","OUTBOUND_15","OUTBOUND_30","AVERAGE_DAILY"}) + @org.junit.jupiter.params.provider.ValueSource(strings={"PHYSICAL","AVAILABLE","TRANSIT","FROZEN","DEFECTIVE","AVERAGE_PHYSICAL","TURNOVER","COVERAGE","OUTBOUND_7","OUTBOUND_15","OUTBOUND_30","AVERAGE_DAILY"}) void numericSortsUseFormalValuesInBothDirectionsAndKeepUnknownLast(String metric) throws Exception { seed("L","SKU",30);seed("H","SKU",30);seed("U","SKU",30); - execute("UPDATE ll_selfop_inventory_snapshot SET physical_quantity=CASE customer_code WHEN 'L' THEN 100 WHEN 'H' THEN 1000 ELSE NULL END,available_quantity=CASE customer_code WHEN 'L' THEN 100 WHEN 'H' THEN 1000 ELSE NULL END,frozen_quantity=CASE customer_code WHEN 'L' THEN 1 WHEN 'H' THEN 9 ELSE NULL END"); + execute("UPDATE ll_selfop_inventory_snapshot SET physical_quantity=CASE customer_code WHEN 'L' THEN 100 WHEN 'H' THEN 1000 ELSE NULL END,available_quantity=CASE customer_code WHEN 'L' THEN 100 WHEN 'H' THEN 1000 ELSE NULL END,transit_quantity=CASE customer_code WHEN 'L' THEN 2 WHEN 'H' THEN 8 ELSE NULL END,frozen_quantity=CASE customer_code WHEN 'L' THEN 1 WHEN 'H' THEN 9 ELSE NULL END"); execute("UPDATE ll_selfop_sku_daily_sales SET outbound_quantity=CASE customer_code WHEN 'L' THEN 1 WHEN 'H' THEN 5 ELSE 9999 END,quantity_unit=CASE customer_code WHEN 'U' THEN 'UNKNOWN' ELSE 'PCS' END"); for(String group:List.of("SKU","OWNER")) { if(group.equals("OWNER")&&metric.equals("COVERAGE")) continue; @@ -563,7 +580,7 @@ class InventoryTurnoverSqlTest { execute("INSERT INTO ll_selfop_sku_profile VALUES("+run+",'demo','W','"+owner+"','"+sku+"','Fixture','PCS','INVENTORY_OBSERVATION',1,TIMESTAMP '2026-01-01 00:00:00')"); } private void stock(long run,String owner,String sku,int quality,int physical,int available) throws Exception { - execute("INSERT INTO ll_selfop_inventory_snapshot VALUES("+run+",'demo','W','"+owner+"','"+sku+"',"+quality+","+physical+","+available+",0,'VALID',NULL)"); + execute("INSERT INTO ll_selfop_inventory_snapshot VALUES("+run+",'demo','W','"+owner+"','"+sku+"',"+quality+","+physical+","+available+",0,0,'VALID',NULL)"); } private void execute(String sql) throws Exception {try(var s=db.createStatement()){s.execute(sql);}} private List> query(String method,InventoryTurnoverSql.Input p) throws Exception {