feat(self-operated): expose and sort transit stock in turnover views

This commit is contained in:
2026-10-14 14:08:52 +08:00
parent 5ace2fb584
commit 2b3b13913b
6 changed files with 35 additions and 14 deletions
@@ -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<Map<String,Object>> query(String method,InventoryTurnoverSql.Input p) throws Exception {