Merge remote-tracking branch 'origin/test' into test
This commit is contained in:
+4
-2
@@ -22,7 +22,7 @@ public class InventoryTurnoverQueryService {
|
|||||||
var frozen=contexts.resolve(request);var input=InventoryTurnoverSql.input(request,frozen);var context=frozen.response();
|
var frozen=contexts.resolve(request);var input=InventoryTurnoverSql.input(request,frozen);var context=frozen.response();
|
||||||
var names=ownerDirectory.names(request.sourceAccount(),context.queryAsOf());
|
var names=ownerDirectory.names(request.sourceAccount(),context.queryAsOf());
|
||||||
long total=0;boolean hasTotal=false;Overview summary=null;Charts charts=null;
|
long total=0;boolean hasTotal=false;Overview summary=null;Charts charts=null;
|
||||||
List<Row> rows=new ArrayList<>();List<OwnerPhysical> owners=new ArrayList<>();int unknown=0;
|
List<Row> rows=new ArrayList<>();List<OwnerPhysical> owners=new ArrayList<>();List<OwnerVolume> volumeOwners=new ArrayList<>();int unknown=0,unknownVolume=0;
|
||||||
for(var part:mapper.report(input)) {
|
for(var part:mapper.report(input)) {
|
||||||
var envelope=normalized(part);Map<String,Object> value;
|
var envelope=normalized(part);Map<String,Object> value;
|
||||||
try {
|
try {
|
||||||
@@ -37,12 +37,14 @@ public class InventoryTurnoverQueryService {
|
|||||||
case "OVERVIEW" -> summary=overview(value,context);
|
case "OVERVIEW" -> summary=overview(value,context);
|
||||||
case "OWNER" -> owners.add(new OwnerPhysical(text(value,"customer_code"),text(value,"customer_name"),decimal(value,"physical")));
|
case "OWNER" -> owners.add(new OwnerPhysical(text(value,"customer_code"),text(value,"customer_name"),decimal(value,"physical")));
|
||||||
case "OWNER_META" -> unknown=count(value,"unknown_count");
|
case "OWNER_META" -> unknown=count(value,"unknown_count");
|
||||||
|
case "OWNER_VOLUME" -> volumeOwners.add(new OwnerVolume(text(value,"customer_code"),text(value,"customer_name"),volume(value,"physical")));
|
||||||
|
case "OWNER_VOLUME_META" -> unknownVolume=count(value,"unknown_count");
|
||||||
default -> throw new IllegalStateException("Unknown turnover query section");
|
default -> throw new IllegalStateException("Unknown turnover query section");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!hasTotal||request.overview()&&summary==null)throw new IllegalStateException("Missing turnover query section");
|
if(!hasTotal||request.overview()&&summary==null)throw new IllegalStateException("Missing turnover query section");
|
||||||
if(request.overview()) {
|
if(request.overview()) {
|
||||||
charts=new Charts(List.copyOf(owners),unknown,windows(normalized(mapper.chartWindows(InventoryTurnoverSql.chartInput(request,frozen))),context.endDate().plusDays(1),request.windows()));
|
charts=new Charts(List.copyOf(owners),unknown,List.copyOf(volumeOwners),unknownVolume,windows(normalized(mapper.chartWindows(InventoryTurnoverSql.chartInput(request,frozen))),context.endDate().plusDays(1),request.windows()));
|
||||||
}
|
}
|
||||||
return new Response(context,summary,charts,List.copyOf(rows),total);
|
return new Response(context,summary,charts,List.copyOf(rows),total);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -43,8 +43,9 @@ public record InventoryTurnoverRequest(String sourceAccount, String warehouseCod
|
|||||||
"OUTBOUND_7_ASC","OUTBOUND_7_DESC","OUTBOUND_15_ASC","OUTBOUND_15_DESC","OUTBOUND_30_ASC","OUTBOUND_30_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",
|
"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",
|
"TRANSIT_ASC","TRANSIT_DESC","FROZEN_ASC","FROZEN_DESC","DEFECTIVE_ASC","DEFECTIVE_DESC","AVERAGE_PHYSICAL_ASC","AVERAGE_PHYSICAL_DESC",
|
||||||
|
"INVENTORY_VOLUME_ASC","INVENTORY_VOLUME_DESC","OUTBOUND_VOLUME_ASC","OUTBOUND_VOLUME_DESC","UNIT_VOLUME_ASC","UNIT_VOLUME_DESC",
|
||||||
"TURNOVER_ASC","TURNOVER_DESC","NO_OUTBOUND_PHYSICAL_DESC").contains(sort())
|
"TURNOVER_ASC","TURNOVER_DESC","NO_OUTBOUND_PHYSICAL_DESC").contains(sort())
|
||||||
|| "OWNER".equals(group())&&sort().startsWith("COVERAGE_")
|
|| "OWNER".equals(group())&&(sort().startsWith("COVERAGE_")||sort().startsWith("UNIT_VOLUME_"))
|
||||||
|| "SKU".equals(group())&&"NO_OUTBOUND_PHYSICAL_DESC".equals(sort())) throw new BizException("排序不适用于当前分组");
|
|| "SKU".equals(group())&&"NO_OUTBOUND_PHYSICAL_DESC".equals(sort())) throw new BizException("排序不适用于当前分组");
|
||||||
if(page()<1||pageSize()<1||pageSize()>100) throw new BizException("分页范围为current≥1,size为1至100");
|
if(page()<1||pageSize()<1||pageSize()>100) throw new BizException("分页范围为current≥1,size为1至100");
|
||||||
if(snapshotContext==null&&page()!=1) throw new BizException("翻页必须携带首屏上下文");
|
if(snapshotContext==null&&page()!=1) throw new BizException("翻页必须携带首屏上下文");
|
||||||
|
|||||||
+3
-1
@@ -31,6 +31,8 @@ public final class InventoryTurnoverResponses {
|
|||||||
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) {}
|
Volume inventoryVolumeM3,List<VolumeWindow> outboundVolumesM3) {}
|
||||||
public record OwnerPhysical(String customerCode,String customerName,BigDecimal physicalQuantity) {}
|
public record OwnerPhysical(String customerCode,String customerName,BigDecimal physicalQuantity) {}
|
||||||
public record Charts(List<OwnerPhysical> ownerPhysicalTop,int unknownOwnerCount,List<Window> averageDaily) {}
|
public record OwnerVolume(String customerCode,String customerName,Volume inventoryVolumeM3) {}
|
||||||
|
public record Charts(List<OwnerPhysical> ownerPhysicalTop,int unknownOwnerCount,List<OwnerVolume> ownerVolumeTop,
|
||||||
|
int unknownVolumeOwnerCount,List<Window> averageDaily) {}
|
||||||
public record Response(Context context,Overview summary,Charts charts,List<Row> rows,long total) {}
|
public record Response(Context context,Overview summary,Charts charts,List<Row> rows,long total) {}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-3
@@ -215,6 +215,9 @@ public final class InventoryTurnoverSql {
|
|||||||
}
|
}
|
||||||
private static String order(Input p,boolean owner) {
|
private static String order(Input p,boolean owner) {
|
||||||
String field=switch(p.query.sort()) {
|
String field=switch(p.query.sort()) {
|
||||||
|
case "INVENTORY_VOLUME_ASC","INVENTORY_VOLUME_DESC" -> "physical_volume";
|
||||||
|
case "OUTBOUND_VOLUME_ASC","OUTBOUND_VOLUME_DESC" -> "w"+p.query.days()+"_outbound_volume";
|
||||||
|
case "UNIT_VOLUME_ASC","UNIT_VOLUME_DESC" -> "unit_volume";
|
||||||
case "PHYSICAL_ASC","PHYSICAL_DESC" -> "physical";
|
case "PHYSICAL_ASC","PHYSICAL_DESC" -> "physical";
|
||||||
case "AVAILABLE_ASC","AVAILABLE_DESC" -> "available";
|
case "AVAILABLE_ASC","AVAILABLE_DESC" -> "available";
|
||||||
case "OUTBOUND_7_ASC","OUTBOUND_7_DESC" -> "w7_outbound";
|
case "OUTBOUND_7_ASC","OUTBOUND_7_DESC" -> "w7_outbound";
|
||||||
@@ -270,22 +273,27 @@ public final class InventoryTurnoverSql {
|
|||||||
for(String field:List.of("physical","available","frozen","defective","transit"))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);
|
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","unit_volume","dimension_source"));
|
} 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.
|
// Stock charts share owner totals, independently of list filters and pagination.
|
||||||
String sql=cte(p)+(owner?ownerCte(p):overview?", owner_totals AS ("+totals(p,"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})";
|
+", 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();
|
sql+=pageMissingDates();
|
||||||
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)";
|
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)"
|
||||||
|
+", top_volume_owners AS (SELECT t.*,ROW_NUMBER() OVER (ORDER BY physical_volume DESC,customer_code) ordinal FROM owner_totals t WHERE physical_volume IS NOT NULL ORDER BY physical_volume DESC,customer_code LIMIT 10)";
|
||||||
// Statement-local only: share intermediate relations across report branches;
|
// Statement-local only: share intermediate relations across report branches;
|
||||||
// do not change connection/global settings or any result/filter semantics.
|
// 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"
|
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);
|
+" UNION ALL SELECT 'TOTAL' section,0 ordinal,JSON_OBJECT('total',COUNT(*)) payload FROM "+source+filter(p,owner);
|
||||||
if(overview) {
|
if(overview) {
|
||||||
|
List<String> ownerVolumeFields=new ArrayList<>(List.of("customer_code"));
|
||||||
|
InventoryVolumeSql.SUFFIXES.forEach(suffix->ownerVolumeFields.add("physical_"+suffix));
|
||||||
List<String> summary=new ArrayList<>(List.of("sku_count","unassessable","no_outbound_count","no_outbound_unknown"));
|
List<String> summary=new ArrayList<>(List.of("sku_count","unassessable","no_outbound_count","no_outbound_unknown"));
|
||||||
summary.addAll(InventoryVolumeSql.fields(p));
|
summary.addAll(InventoryVolumeSql.fields(p));
|
||||||
for(String field:List.of("physical","available")){summary.add(field);summary.add(field+"_known");summary.add(field+"_unknown");}
|
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"
|
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"
|
+" UNION ALL SELECT 'OWNER' section,ordinal,JSON_OBJECT('customer_code',customer_code,'physical',physical) payload FROM top_owners"
|
||||||
+" UNION ALL SELECT 'OWNER_META' section,0 ordinal,JSON_OBJECT('unknown_count',COUNT(*)) payload FROM owner_totals WHERE physical IS NULL";
|
+" UNION ALL SELECT 'OWNER_META' section,0 ordinal,JSON_OBJECT('unknown_count',COUNT(*)) payload FROM owner_totals WHERE physical IS NULL"
|
||||||
|
+" UNION ALL SELECT 'OWNER_VOLUME' section,ordinal,"+jsonFields(ownerVolumeFields)+" payload FROM top_volume_owners"
|
||||||
|
+" UNION ALL SELECT 'OWNER_VOLUME_META' section,0 ordinal,JSON_OBJECT('unknown_count',COUNT(*)) payload FROM owner_totals WHERE physical_volume IS NULL";
|
||||||
}
|
}
|
||||||
return sql+" ORDER BY section,ordinal";
|
return sql+" ORDER BY section,ordinal";
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-3
@@ -41,8 +41,8 @@ paths:
|
|||||||
- {name: turnoverStatus, in: query, schema: {type: string, enum: [INSUFFICIENT]}}
|
- {name: turnoverStatus, in: query, schema: {type: string, enum: [INSUFFICIENT]}}
|
||||||
- name: sortBy
|
- name: sortBy
|
||||||
in: query
|
in: query
|
||||||
description: 'OWNER禁止COVERAGE_ASC/DESC,SKU禁止NO_OUTBOUND_PHYSICAL_DESC。全量筛选后按正式未舍入值排序再分页,未知值始终末尾,同值按SKU及货主升序。AVERAGE_DAILY使用baseWindowDays指定的日均窗口。'
|
description: 'OWNER禁止COVERAGE_ASC/DESC及UNIT_VOLUME_ASC/DESC,SKU禁止NO_OUTBOUND_PHYSICAL_DESC。全量筛选后按正式未舍入值排序再分页,未知值始终末尾,同值按SKU及货主升序。AVERAGE_DAILY及OUTBOUND_VOLUME使用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, 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]}
|
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, INVENTORY_VOLUME_ASC, INVENTORY_VOLUME_DESC, OUTBOUND_VOLUME_ASC, OUTBOUND_VOLUME_DESC, UNIT_VOLUME_ASC, UNIT_VOLUME_DESC, NO_OUTBOUND_PHYSICAL_DESC]}
|
||||||
- {name: current, in: query, schema: {type: integer, minimum: 1, default: 1}}
|
- {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: size, in: query, schema: {type: integer, minimum: 1, maximum: 100, default: 20}}
|
||||||
- {name: includeOverview, in: query, schema: {type: boolean, default: true}}
|
- {name: includeOverview, in: query, schema: {type: boolean, default: true}}
|
||||||
@@ -205,7 +205,7 @@ components:
|
|||||||
Charts:
|
Charts:
|
||||||
type: object
|
type: object
|
||||||
nullable: true
|
nullable: true
|
||||||
required: [ownerPhysicalTop, unknownOwnerCount, averageDaily]
|
required: [ownerPhysicalTop, unknownOwnerCount, ownerVolumeTop, unknownVolumeOwnerCount, averageDaily]
|
||||||
properties:
|
properties:
|
||||||
ownerPhysicalTop:
|
ownerPhysicalTop:
|
||||||
type: array
|
type: array
|
||||||
@@ -218,6 +218,18 @@ components:
|
|||||||
customerName: {type: string, nullable: true}
|
customerName: {type: string, nullable: true}
|
||||||
physicalQuantity: {$ref: '#/components/schemas/Quantity'}
|
physicalQuantity: {$ref: '#/components/schemas/Quantity'}
|
||||||
unknownOwnerCount: {type: integer, minimum: 0}
|
unknownOwnerCount: {type: integer, minimum: 0}
|
||||||
|
ownerVolumeTop:
|
||||||
|
type: array
|
||||||
|
maxItems: 10
|
||||||
|
description: 当前库存正式体积降序,同体积按货主编码稳定排序;排除体积未知货主,不受列表筛选及分页影响。
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
required: [customerCode, customerName, inventoryVolumeM3]
|
||||||
|
properties:
|
||||||
|
customerCode: {type: string}
|
||||||
|
customerName: {type: string, nullable: true}
|
||||||
|
inventoryVolumeM3: {$ref: '#/components/schemas/Volume'}
|
||||||
|
unknownVolumeOwnerCount: {type: integer, minimum: 0, description: 因缺尺寸或库存数量未知未参与体积排名的货主数}
|
||||||
averageDaily: {type: array, minItems: 3, maxItems: 4, items: {$ref: '#/components/schemas/Window'}}
|
averageDaily: {type: array, minItems: 3, maxItems: 4, items: {$ref: '#/components/schemas/Window'}}
|
||||||
Response:
|
Response:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
+14
@@ -115,6 +115,20 @@ class InventoryTurnoverContextTest {
|
|||||||
&&p.days().subList(0,12).stream().allMatch(d->d.coverage()==0)
|
&&p.days().subList(0,12).stream().allMatch(d->d.coverage()==0)
|
||||||
&&p.days().subList(15,30).stream().allMatch(d->d.coverage()==1)));
|
&&p.days().subList(15,30).stream().allMatch(d->d.coverage()==1)));
|
||||||
}
|
}
|
||||||
|
@Test void volumeChartUsesDirectoryNamesAndKeepsEstimateEvidence() {
|
||||||
|
var directory=mock(SelfopOwnerDirectory.class);
|
||||||
|
when(directory.names(any(),any())).thenReturn(Map.of("C","货主甲"));
|
||||||
|
when(mapper.report(any())).thenReturn(List.of(
|
||||||
|
Map.of("section","TOTAL","payload","{\"total\":0}"),
|
||||||
|
Map.of("section","OVERVIEW","payload","{\"sku_count\":0}"),
|
||||||
|
Map.of("section","OWNER_VOLUME","payload","{\"customer_code\":\"C\",\"physical_volume\":12.345678,\"physical_estimated_quantity\":9}"),
|
||||||
|
Map.of("section","OWNER_VOLUME_META","payload","{\"unknown_count\":2}")));
|
||||||
|
var charts=new InventoryTurnoverQueryService(contexts,mapper,json,directory).query(request(null)).charts();
|
||||||
|
assertEquals(2,charts.unknownVolumeOwnerCount());assertEquals(1,charts.ownerVolumeTop().size());
|
||||||
|
var owner=charts.ownerVolumeTop().get(0);assertEquals("货主甲",owner.customerName());
|
||||||
|
assertEquals(new java.math.BigDecimal("12.345678"),owner.inventoryVolumeM3().value());
|
||||||
|
assertTrue(owner.inventoryVolumeM3().reasonCodes().contains("DECLARED_DIMENSIONS_ESTIMATE"));
|
||||||
|
}
|
||||||
@Test void firstPageSharesAggregationAndPaginationDoesNotRepeatOverviewQueries() {
|
@Test void firstPageSharesAggregationAndPaginationDoesNotRepeatOverviewQueries() {
|
||||||
var service=new InventoryTurnoverQueryService(contexts,mapper,json,org.mockito.Mockito.mock(SelfopOwnerDirectory.class));
|
var service=new InventoryTurnoverQueryService(contexts,mapper,json,org.mockito.Mockito.mock(SelfopOwnerDirectory.class));
|
||||||
when(mapper.report(any())).thenReturn(List.of(Map.of("section","TOTAL","payload","{\"total\":0}"),
|
when(mapper.report(any())).thenReturn(List.of(Map.of("section","TOTAL","payload","{\"total\":0}"),
|
||||||
|
|||||||
+57
@@ -33,6 +33,63 @@ class InventoryTurnoverSqlTest {
|
|||||||
private void product(String owner,String sku,String declared,String measured) throws Exception {
|
private void product(String owner,String sku,String declared,String measured) throws Exception {
|
||||||
execute("INSERT INTO ll_selfop_product VALUES(901,'demo','"+owner+"','"+sku+"',"+declared+","+measured+")");
|
execute("INSERT INTO ll_selfop_product VALUES(901,'demo','"+owner+"','"+sku+"',"+declared+","+measured+")");
|
||||||
}
|
}
|
||||||
|
@Test void ownerVolumeChartRanksFullTotalsExcludesUnknownsAndIgnoresListFilters() throws Exception {
|
||||||
|
master();
|
||||||
|
for(int i=0;i<12;i++) {
|
||||||
|
String owner=String.format("C%02d",i);seed(owner,"SAME",0);
|
||||||
|
product(owner,"SAME",(i<2?"1":Integer.toString(i))+",1,1,'m'","NULL,NULL,NULL,NULL");
|
||||||
|
}
|
||||||
|
seed("MISSING","SAME",0);seed("UNKNOWN","SAME",0);
|
||||||
|
product("UNKNOWN","SAME","100,1,1,'m'","NULL,NULL,NULL,NULL");
|
||||||
|
execute("UPDATE ll_selfop_inventory_snapshot SET quality_status='INVALID' WHERE customer_code='UNKNOWN'");
|
||||||
|
for(int days:List.of(7,30)) {
|
||||||
|
var q=new InventoryTurnoverRequest("demo","W",END,days,"SKU",null,"not-present","FROZEN",null,null,"PHYSICAL_DESC",2,1,true,"fixed");
|
||||||
|
var report=query("report",input(q,30,0,true));
|
||||||
|
var ranking=new ArrayList<com.fasterxml.jackson.databind.JsonNode>();
|
||||||
|
for(var part:report)if("OWNER_VOLUME".equals(part.get("section")))ranking.add(payload(part));
|
||||||
|
assertEquals(10,ranking.size());
|
||||||
|
assertEquals("C11",ranking.get(0).path("customer_code").asText());
|
||||||
|
assertEquals("C02",ranking.get(9).path("customer_code").asText());
|
||||||
|
decimal("6710",ranking.get(0).path("physical_volume").decimalValue());
|
||||||
|
decimal("610",ranking.get(0).path("physical_estimated_quantity").decimalValue());
|
||||||
|
assertEquals(2,payload(report.stream().filter(r->"OWNER_VOLUME_META".equals(r.get("section"))).findFirst().orElseThrow()).path("unknown_count").asInt());
|
||||||
|
assertTrue(report.stream().noneMatch(r->"ROW".equals(r.get("section"))));
|
||||||
|
}
|
||||||
|
execute("UPDATE ll_selfop_product SET declared_length=1");
|
||||||
|
var tied=query("report",input(request("OWNER","ALL","PHYSICAL_DESC",null,null,1,1),30,0,true));
|
||||||
|
var ranking=new ArrayList<com.fasterxml.jackson.databind.JsonNode>();
|
||||||
|
for(var part:tied)if("OWNER_VOLUME".equals(part.get("section")))ranking.add(payload(part));
|
||||||
|
assertEquals("C00",ranking.get(0).path("customer_code").asText());
|
||||||
|
assertEquals("C09",ranking.get(9).path("customer_code").asText());
|
||||||
|
execute("UPDATE ll_selfop_inventory_snapshot SET quality_status='INVALID'");
|
||||||
|
var unknown=query("report",input(request("SKU","ALL","PHYSICAL_DESC",null,null,1,1),30,0,true));
|
||||||
|
assertTrue(unknown.stream().noneMatch(r->"OWNER_VOLUME".equals(r.get("section"))));
|
||||||
|
}
|
||||||
|
@Test void volumeSortsUseSelectedWindowFormalValuesBeforePagingWithStableTiesAndUnknownLast() throws Exception {
|
||||||
|
master();
|
||||||
|
for(String owner:List.of("A","B","C","D"))seed(owner,"SAME",0);
|
||||||
|
for(String owner:List.of("A","B","D"))product(owner,"SAME",("B".equals(owner)?"2":"1")+",1,1,'m'","NULL,NULL,NULL,NULL");
|
||||||
|
execute("UPDATE ll_selfop_sku_daily_sales SET outbound_quantity=CASE WHEN customer_code='A' THEN CASE WHEN business_date>=DATE '"+END.minusDays(2)+"' THEN 100 ELSE 0 END WHEN customer_code='B' THEN CASE WHEN business_date>=DATE '"+END.minusDays(2)+"' THEN 1 ELSE 100 END ELSE 10 END");
|
||||||
|
for(String group:List.of("SKU","OWNER"))for(String metric:List.of("INVENTORY_VOLUME","OUTBOUND_VOLUME","UNIT_VOLUME")) {
|
||||||
|
if(group.equals("OWNER")&&metric.equals("UNIT_VOLUME"))continue;
|
||||||
|
for(int days:List.of(3,30))for(String direction:List.of("ASC","DESC")) {
|
||||||
|
var expected=metric.equals("OUTBOUND_VOLUME")&&days==3
|
||||||
|
? direction.equals("ASC")?List.of("B","D","A","C"):List.of("A","D","B","C")
|
||||||
|
: direction.equals("ASC")?List.of("A","D","B","C"):List.of("B","A","D","C");
|
||||||
|
var actual=new ArrayList<Object>();
|
||||||
|
for(int page=1;page<=2;page++) {
|
||||||
|
var q=new InventoryTurnoverRequest("demo","W",END,days,group,null,null,"ALL",null,null,metric+"_"+direction,page,2,true,"fixed");
|
||||||
|
q.validate(END.plusDays(1));
|
||||||
|
actual.addAll(reportRows(input(q,30,0,true)).stream().map(r->r.get("customer_code")).toList());
|
||||||
|
}
|
||||||
|
assertEquals(expected,actual,group+" "+metric+" "+days+" "+direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var incomplete=reportRows(input(request("SKU","ALL","OUTBOUND_VOLUME_DESC",null,null,1,20),7,0,true));
|
||||||
|
assertEquals(List.of("A","B","C","D"),incomplete.stream().map(r->r.get("customer_code")).toList());
|
||||||
|
assertTrue(incomplete.stream().allMatch(r->r.get("w30_outbound_volume")==null));
|
||||||
|
for(String direction:List.of("ASC","DESC"))assertThrows(RuntimeException.class,()->request("OWNER","ALL","UNIT_VOLUME_"+direction,null,null,1,20).validate(END.plusDays(1)));
|
||||||
|
}
|
||||||
@Test void emptyUncoveredOutboundVolumeIsUnknownAndCompleteEmptyVolumesAreZero() throws Exception {
|
@Test void emptyUncoveredOutboundVolumeIsUnknownAndCompleteEmptyVolumesAreZero() throws Exception {
|
||||||
var q=request("SKU","ALL","PHYSICAL_DESC",null,null,1,20);
|
var q=request("SKU","ALL","PHYSICAL_DESC",null,null,1,20);
|
||||||
var unknown=query("overview",input(q,0,0,true)).get(0);
|
var unknown=query("overview",input(q,0,0,true)).get(0);
|
||||||
|
|||||||
Reference in New Issue
Block a user