fix(firstleg): project completed loading lists from actual picked cargo
This commit is contained in:
@@ -19,6 +19,35 @@ import java.util.List;
|
||||
/** 基于共享装柜计划表提供清单查询投影和状态推进持久化操作。 */
|
||||
@Mapper
|
||||
public interface LoadingListQueryMapper {
|
||||
// Completed projections use the same settled physical scope as LoadingCompletedEventMapper.
|
||||
// Keep skipped plan rows for history and for the unfinished preload projection.
|
||||
String ITEM_SCOPE = """
|
||||
AND (p.plan_status != 'COMPLETED' OR EXISTS (
|
||||
SELECT 1 FROM fl_loading_pick_item actual_pick
|
||||
JOIN fl_loading_pick_task actual_task ON actual_task.id=actual_pick.pick_task_id
|
||||
WHERE actual_task.loading_plan_id=p.id AND actual_pick.loading_plan_item_id=i.id
|
||||
AND actual_pick.item_status IN ('PICKED','PARTIAL')
|
||||
AND actual_pick.picked_package_count > 0
|
||||
AND actual_pick.picked_package_count=i.package_count))
|
||||
""";
|
||||
String PALLET_ITEM_SCOPE = """
|
||||
AND (p.plan_status != 'COMPLETED' OR EXISTS (
|
||||
SELECT 1 FROM fl_loading_pick_item actual_pick
|
||||
JOIN fl_loading_pick_task actual_task ON actual_task.id=actual_pick.pick_task_id
|
||||
WHERE actual_task.loading_plan_id=p.id AND actual_pick.loading_plan_item_id=pallet_item.id
|
||||
AND actual_pick.item_status IN ('PICKED','PARTIAL')
|
||||
AND actual_pick.picked_package_count > 0
|
||||
AND actual_pick.picked_package_count=pallet_item.package_count))
|
||||
""";
|
||||
String CUSTOMER_ACTUAL_SCOPE = """
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM fl_loading_pick_item actual_pick
|
||||
JOIN fl_loading_pick_task actual_task ON actual_task.id=actual_pick.pick_task_id
|
||||
WHERE actual_task.loading_plan_id=p.id AND actual_pick.loading_plan_item_id=customer_item.id
|
||||
AND actual_pick.item_status IN ('PICKED','PARTIAL')
|
||||
AND actual_pick.picked_package_count > 0
|
||||
AND actual_pick.picked_package_count=customer_item.package_count)
|
||||
""";
|
||||
String AGGREGATE_COLUMNS = """
|
||||
p.id AS plan_id, p.plan_no, p.container_booking_id, p.plan_status, p.remark,
|
||||
p.created_by, p.created_by_name, p.created_at, p.version,
|
||||
@@ -50,28 +79,36 @@ public interface LoadingListQueryMapper {
|
||||
FROM fl_dropship_pallet_carton carton
|
||||
WHERE EXISTS (SELECT 1 FROM fl_loading_plan_item pallet_item
|
||||
WHERE pallet_item.loading_plan_id=p.id AND pallet_item.deleted=0
|
||||
AND pallet_item.pallet_id=carton.pallet_id)), 0) AS total_package_count,
|
||||
AND pallet_item.pallet_id=carton.pallet_id
|
||||
""" + PALLET_ITEM_SCOPE + """
|
||||
)), 0) AS total_package_count,
|
||||
COALESCE(SUM(CASE WHEN i.pallet_id IS NULL THEN i.gross_weight_kg ELSE 0 END), 0)
|
||||
+ COALESCE((SELECT SUM(inbound.actual_gross_weight_kg/NULLIF(inbound.total_package_count,0))
|
||||
FROM fl_dropship_pallet_carton carton
|
||||
JOIN fl_inbound_order inbound ON inbound.id=carton.inbound_id AND inbound.tenant_id=carton.tenant_id
|
||||
WHERE EXISTS (SELECT 1 FROM fl_loading_plan_item pallet_item
|
||||
WHERE pallet_item.loading_plan_id=p.id AND pallet_item.deleted=0
|
||||
AND pallet_item.pallet_id=carton.pallet_id)), 0) AS total_gross_weight_kg,
|
||||
AND pallet_item.pallet_id=carton.pallet_id
|
||||
""" + PALLET_ITEM_SCOPE + """
|
||||
)), 0) AS total_gross_weight_kg,
|
||||
COALESCE(SUM(CASE WHEN i.pallet_id IS NULL THEN i.net_weight_kg ELSE 0 END), 0)
|
||||
+ COALESCE((SELECT SUM(inbound.actual_net_weight_kg/NULLIF(inbound.total_package_count,0))
|
||||
FROM fl_dropship_pallet_carton carton
|
||||
JOIN fl_inbound_order inbound ON inbound.id=carton.inbound_id AND inbound.tenant_id=carton.tenant_id
|
||||
WHERE EXISTS (SELECT 1 FROM fl_loading_plan_item pallet_item
|
||||
WHERE pallet_item.loading_plan_id=p.id AND pallet_item.deleted=0
|
||||
AND pallet_item.pallet_id=carton.pallet_id)), 0) AS total_net_weight_kg,
|
||||
AND pallet_item.pallet_id=carton.pallet_id
|
||||
""" + PALLET_ITEM_SCOPE + """
|
||||
)), 0) AS total_net_weight_kg,
|
||||
COALESCE(SUM(CASE WHEN i.pallet_id IS NULL THEN i.volume_cbm ELSE 0 END), 0)
|
||||
+ COALESCE((SELECT SUM(inbound.actual_volume_cbm/NULLIF(inbound.total_package_count,0))
|
||||
FROM fl_dropship_pallet_carton carton
|
||||
JOIN fl_inbound_order inbound ON inbound.id=carton.inbound_id AND inbound.tenant_id=carton.tenant_id
|
||||
WHERE EXISTS (SELECT 1 FROM fl_loading_plan_item pallet_item
|
||||
WHERE pallet_item.loading_plan_id=p.id AND pallet_item.deleted=0
|
||||
AND pallet_item.pallet_id=carton.pallet_id)), 0) AS total_volume_cbm,
|
||||
AND pallet_item.pallet_id=carton.pallet_id
|
||||
""" + PALLET_ITEM_SCOPE + """
|
||||
)), 0) AS total_volume_cbm,
|
||||
COALESCE(SUM(i.product_quantity), 0) AS total_quantity
|
||||
""";
|
||||
|
||||
@@ -115,8 +152,11 @@ public interface LoadingListQueryMapper {
|
||||
FROM fl_loading_plan p
|
||||
JOIN fl_container_booking b ON b.id = p.container_booking_id
|
||||
LEFT JOIN fl_loading_plan_item i ON i.loading_plan_id = p.id AND i.deleted = 0
|
||||
""" + ITEM_SCOPE + """
|
||||
WHERE p.plan_status = 'COMPLETED'
|
||||
<if test="query.customerId != null">AND EXISTS (SELECT 1 FROM fl_loading_plan_item customer_item WHERE customer_item.loading_plan_id = p.id AND customer_item.deleted = 0 AND customer_item.account_id = #{query.customerId})</if>
|
||||
<if test="query.customerId != null">AND EXISTS (SELECT 1 FROM fl_loading_plan_item customer_item WHERE customer_item.loading_plan_id = p.id AND customer_item.deleted = 0 AND customer_item.account_id = #{query.customerId}
|
||||
""" + CUSTOMER_ACTUAL_SCOPE + """
|
||||
)</if>
|
||||
<if test="query.keyword != null and query.keyword != ''">
|
||||
AND (p.plan_no LIKE CONCAT('%', #{query.keyword}, '%')
|
||||
OR b.container_no LIKE CONCAT('%', #{query.keyword}, '%')
|
||||
@@ -138,6 +178,7 @@ public interface LoadingListQueryMapper {
|
||||
FROM fl_loading_plan p
|
||||
JOIN fl_container_booking b ON b.id = p.container_booking_id
|
||||
LEFT JOIN fl_loading_plan_item i ON i.loading_plan_id = p.id AND i.deleted = 0
|
||||
""" + ITEM_SCOPE + """
|
||||
WHERE p.id = #{planId}
|
||||
GROUP BY
|
||||
""" + AGGREGATE_GROUP)
|
||||
@@ -149,6 +190,7 @@ public interface LoadingListQueryMapper {
|
||||
FROM fl_loading_plan p
|
||||
JOIN fl_container_booking b ON b.id = p.container_booking_id
|
||||
LEFT JOIN fl_loading_plan_item i ON i.loading_plan_id = p.id AND i.deleted = 0
|
||||
""" + ITEM_SCOPE + """
|
||||
WHERE p.container_booking_id = #{bookingId}
|
||||
GROUP BY
|
||||
""" + AGGREGATE_GROUP)
|
||||
@@ -224,6 +266,7 @@ public interface LoadingListQueryMapper {
|
||||
JOIN fl_container_booking b ON b.id=p.container_booking_id AND b.record_status='ACTIVE'
|
||||
LEFT JOIN fl_inbound_order inbound ON inbound.id=i.inbound_id AND inbound.tenant_id=i.account_id
|
||||
WHERE i.loading_plan_id = #{planId} AND i.deleted = 0
|
||||
""" + ITEM_SCOPE + """
|
||||
ORDER BY i.load_sequence, i.id
|
||||
""")
|
||||
List<ItemRow> selectItems(@Param("planId") long planId);
|
||||
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedQuery;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadQuery;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
||||
import com.baiyue.firstleg.mapper.LoadingListQueryMapper;
|
||||
import org.h2.jdbcx.JdbcDataSource;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/** Executes the real SQL, including pagination COUNT and correlated physical-pallet totals. */
|
||||
class LoadingListActualCargoTest {
|
||||
private JdbcTemplate jdbc;
|
||||
private LoadingListQueryMapper mapper;
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
var ds = new JdbcDataSource();
|
||||
ds.setURL("jdbc:h2:mem:loaded-list-" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1");
|
||||
jdbc = new JdbcTemplate(ds);
|
||||
table("fl_loading_plan", LoadingPlanEntity.class);
|
||||
table("fl_loading_plan_item", LoadingPlanItemEntity.class);
|
||||
table("fl_container_booking", ContainerBookingEntity.class);
|
||||
table("fl_inbound_order", InboundOrderEntity.class);
|
||||
jdbc.execute("CREATE TABLE fl_outbound_dispatch(container_booking_id BIGINT, confirmed_at TIMESTAMP, reversed_at TIMESTAMP, status VARCHAR)");
|
||||
jdbc.execute("CREATE TABLE fl_preload_loading_plan_link(loading_plan_id BIGINT, preload_plan_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE fl_preload_plan(id BIGINT, status VARCHAR)");
|
||||
jdbc.execute("CREATE TABLE fl_ocean_booking_container_allocation(container_booking_id BIGINT, ocean_booking_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE fl_ocean_booking(id BIGINT, status VARCHAR)");
|
||||
jdbc.execute("CREATE TABLE fl_loading_pick_task(id BIGINT, loading_plan_id BIGINT, task_status VARCHAR)");
|
||||
jdbc.execute("CREATE TABLE fl_loading_pick_item(loading_plan_item_id BIGINT, pick_task_id BIGINT, item_status VARCHAR, picked_package_count INT)");
|
||||
jdbc.execute("CREATE TABLE fl_dropship_pallet_carton(id BIGINT, pallet_id BIGINT, inbound_id BIGINT, tenant_id BIGINT)");
|
||||
jdbc.execute("CREATE TABLE fl_inbound_weight_result(tenant_id BIGINT, inbound_id BIGINT, deleted INT, recognition_status VARCHAR)");
|
||||
jdbc.update("INSERT INTO fl_container_booking(id,warehouse_id,record_status) VALUES(10,1,'ACTIVE')");
|
||||
jdbc.update("INSERT INTO fl_loading_plan(id,container_booking_id,plan_status,created_at) VALUES(1,10,'COMPLETED',CURRENT_TIMESTAMP)");
|
||||
jdbc.update("INSERT INTO fl_loading_pick_task VALUES(100,1,'COMPLETED'),(200,2,'COMPLETED')");
|
||||
var configuration = new MybatisConfiguration();
|
||||
configuration.setMapUnderscoreToCamelCase(true);
|
||||
configuration.addMapper(LoadingListQueryMapper.class);
|
||||
var pagination = new MybatisPlusInterceptor();
|
||||
pagination.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
|
||||
var factory = new MybatisSqlSessionFactoryBean();
|
||||
factory.setDataSource(ds);
|
||||
factory.setConfiguration(configuration);
|
||||
factory.setPlugins(pagination);
|
||||
mapper = new SqlSessionTemplate(factory.getObject()).getMapper(LoadingListQueryMapper.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void completedDetailAndListExcludeSkippedAndUnsettledRowsFromAllMeasures() {
|
||||
ordinaryCargo();
|
||||
assertActualTotals(mapper.selectAggregate(1));
|
||||
assertActualTotals(mapper.selectAggregateByBooking(10));
|
||||
assertThat(mapper.selectItems(1)).extracting(LoadingListQueryMapper.ItemRow::getItemId)
|
||||
.containsExactly(1L, 2L);
|
||||
var page = mapper.selectConfirmedPage(new Page<>(1, 1), query(null));
|
||||
assertThat(page.getTotal()).isEqualTo(1);
|
||||
assertThat(page.getRecords()).singleElement().satisfies(this::assertActualTotals);
|
||||
}
|
||||
|
||||
@Test
|
||||
void customerWithOnlySkippedCargoCannotMatchCompletedListCountOrRows() {
|
||||
ordinaryCargo();
|
||||
var skipped = mapper.selectConfirmedPage(new Page<>(1, 1), query(22L));
|
||||
assertThat(skipped.getTotal()).isZero();
|
||||
assertThat(skipped.getRecords()).isEmpty();
|
||||
var loaded = mapper.selectConfirmedPage(new Page<>(1, 1), query(11L));
|
||||
assertThat(loaded.getTotal()).isEqualTo(1);
|
||||
assertThat(loaded.getRecords()).singleElement().satisfies(this::assertActualTotals);
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyActualScopeDoesNotHideThePlanIdentityOrFallBackToPlannedTotals() {
|
||||
item(1, 22, 1, null, "SKIPPED", 0, 100);
|
||||
var row = mapper.selectAggregate(1);
|
||||
assertThat(row.getPlanId()).isEqualTo(1);
|
||||
assertThat(row.getPlanStatus()).isEqualTo("COMPLETED");
|
||||
assertThat(row.getTotalPackageCount()).isZero();
|
||||
assertThat(row.getTotalGrossWeightKg()).isEqualByComparingTo("0");
|
||||
assertThat(mapper.selectItems(1)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void unfinishedPreloadKeepsPlannedCargoIncludingPendingAndSkippedItems() {
|
||||
ordinaryCargo();
|
||||
jdbc.update("UPDATE fl_loading_plan SET plan_status='LOADING' WHERE id=1");
|
||||
assertThat(mapper.selectItems(1)).hasSize(6);
|
||||
assertThat(mapper.selectAggregate(1).getTotalPackageCount()).isEqualTo(8);
|
||||
var page = mapper.selectPreloadPage(new Page<>(1, 1),
|
||||
new PreloadQuery(null, null, 22L, null, null, null, 1, 1));
|
||||
assertThat(page.getTotal()).isEqualTo(1);
|
||||
assertThat(page.getRecords()).singleElement().satisfies(row ->
|
||||
assertThat(row.getTotalPackageCount()).isEqualTo(8));
|
||||
}
|
||||
|
||||
@Test
|
||||
void physicalPalletTotalsIgnoreUnloadedPalletsAndDoNotCountSkuFragmentsTwice() {
|
||||
item(1, 11, 2, 501L, "PICKED", 2, 100);
|
||||
item(2, 11, 2, 501L, "PICKED", 2, 100);
|
||||
item(3, 22, 1, 502L, "SKIPPED", 0, 100);
|
||||
jdbc.update("INSERT INTO fl_inbound_order(id,tenant_id,total_package_count,actual_gross_weight_kg,actual_net_weight_kg,actual_volume_cbm) VALUES(700,11,2,20,16,4),(701,22,1,999,999,999)");
|
||||
jdbc.update("INSERT INTO fl_dropship_pallet_carton VALUES(1,501,700,11),(2,501,700,11),(3,502,701,22)");
|
||||
var actual = mapper.selectAggregate(1);
|
||||
assertThat(actual.getTotalPackageCount()).isEqualTo(2);
|
||||
assertThat(actual.getTotalGrossWeightKg()).isEqualByComparingTo("20");
|
||||
assertThat(actual.getTotalNetWeightKg()).isEqualByComparingTo("16");
|
||||
assertThat(actual.getTotalVolumeCbm()).isEqualByComparingTo("4");
|
||||
assertThat(actual.getTotalQuantity()).isEqualTo(40);
|
||||
assertThat(mapper.selectItems(1)).hasSize(2);
|
||||
assertThat(mapper.selectConfirmedPage(new Page<>(1, 1), query(22L)).getTotal()).isZero();
|
||||
jdbc.update("UPDATE fl_loading_plan SET plan_status='EDITING' WHERE id=1");
|
||||
assertThat(mapper.selectAggregate(1).getTotalPackageCount()).isEqualTo(3);
|
||||
assertThat(mapper.selectAggregate(1).getTotalGrossWeightKg()).isEqualByComparingTo("1019");
|
||||
assertThat(mapper.selectItems(1)).hasSize(3);
|
||||
}
|
||||
|
||||
private void ordinaryCargo() {
|
||||
item(1, 11, 2, null, "PICKED", 2, 100);
|
||||
item(2, 11, 1, null, "PARTIAL", 1, 100);
|
||||
item(3, 22, 1, null, "SKIPPED", 0, 100);
|
||||
item(4, 22, 1, null, "PENDING", 0, 100);
|
||||
item(5, 22, 1, null, "PICKED", 1, 200); // wrong task belongs to a different plan
|
||||
item(6, 22, 2, null, "PARTIAL", 1, 100); // not reconciled to the final fragment
|
||||
item(7, 11, 9, null, "PICKED", 9, 100);
|
||||
jdbc.update("UPDATE fl_loading_plan_item SET deleted=1 WHERE id=7");
|
||||
}
|
||||
|
||||
private void item(long id, long customer, int packages, Long pallet, String status, int picked, long task) {
|
||||
jdbc.update("INSERT INTO fl_loading_plan_item(id,loading_plan_id,account_id,sku_code,package_count,product_quantity,gross_weight_kg,net_weight_kg,volume_cbm,pallet_id,pallet_count,deleted,load_sequence) VALUES(?,1,?,?,?,?,?,?,?,?,1,0,?)",
|
||||
id, customer, "SKU-" + id, packages, packages * 10, packages * 5, packages * 4, packages, pallet, id);
|
||||
jdbc.update("INSERT INTO fl_loading_pick_item VALUES(?,?,?,?)", id, task, status, picked);
|
||||
}
|
||||
|
||||
private ConfirmedQuery query(Long customer) {
|
||||
return new ConfirmedQuery(null, customer, null, null, null, 1, 1);
|
||||
}
|
||||
|
||||
private void assertActualTotals(LoadingListQueryMapper.AggregateRow row) {
|
||||
assertThat(row.getTotalPackageCount()).isEqualTo(3);
|
||||
assertThat(row.getTotalQuantity()).isEqualTo(30);
|
||||
assertThat(row.getSkuCount()).isEqualTo(2);
|
||||
assertThat(row.getTotalGrossWeightKg()).isEqualByComparingTo("15");
|
||||
assertThat(row.getTotalNetWeightKg()).isEqualByComparingTo("12");
|
||||
assertThat(row.getTotalVolumeCbm()).isEqualByComparingTo("3");
|
||||
}
|
||||
|
||||
private void table(String name, Class<?> entity) {
|
||||
var columns = new ArrayList<String>();
|
||||
for (var type = entity; type != Object.class; type = type.getSuperclass()) {
|
||||
for (var field : type.getDeclaredFields()) {
|
||||
var annotation = field.getAnnotation(TableField.class);
|
||||
if (Modifier.isStatic(field.getModifiers()) || annotation != null && !annotation.exist()) continue;
|
||||
String column = field.getName().replaceAll("([a-z0-9])([A-Z])", "$1_$2").toLowerCase();
|
||||
Class<?> value = field.getType();
|
||||
String sqlType = value == Long.class || value == long.class ? "BIGINT"
|
||||
: value == Integer.class || value == int.class || value == Boolean.class || value == boolean.class ? "INT"
|
||||
: value == BigDecimal.class ? "DECIMAL(20,6)"
|
||||
: value == java.time.LocalDateTime.class ? "TIMESTAMP" : "VARCHAR(2000)";
|
||||
columns.add(column + " " + sqlType);
|
||||
}
|
||||
}
|
||||
jdbc.execute("CREATE TABLE " + name + " (" + String.join(",", columns) + ")");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user