support split fulfillment by base unit

This commit is contained in:
Timi
2026-09-09 01:49:40 +08:00
parent ea44dd5c0e
commit 00bb300755
13 changed files with 320 additions and 189 deletions
@@ -29,7 +29,7 @@ public class GaoOrderFulfillmentItem implements IDEntity<String>, Creatable {
/// SKU ID
private String skuId;
/// SKU 单位 ID
/// 本次实际履约 SKU 单位 ID
private String skuUnitId;
/// 产品图片附件
@@ -45,17 +45,17 @@ public class GaoOrderFulfillmentItem implements IDEntity<String>, Creatable {
/// 基础单位数量
private BigDecimal baseQuantity;
/// 操作前待履约数量
private BigDecimal reservedBefore;
/// 操作前待履约基础单位数量
private BigDecimal reservedBaseBefore;
/// 操作后待履约数量
private BigDecimal reservedAfter;
/// 操作后待履约基础单位数量
private BigDecimal reservedBaseAfter;
/// 操作前已履约数量
private BigDecimal fulfilledBefore;
/// 操作前已履约基础单位数量
private BigDecimal fulfilledBaseBefore;
/// 操作后已履约数量
private BigDecimal fulfilledAfter;
/// 操作后已履约基础单位数量
private BigDecimal fulfilledBaseAfter;
/// 产品名称快照
private String productNameSnapshot;
@@ -71,11 +71,14 @@ public class GaoSalesOrderItem extends UUIDEntity {
/// 购买数量
private BigDecimal quantity;
/// 待履约数量
private BigDecimal reservedQuantity;
/// 购买基础单位数量
private BigDecimal baseQuantity;
/// 已履约数量
private BigDecimal fulfilledQuantity;
/// 待履约基础单位数量
private BigDecimal reservedBaseQuantity;
/// 已履约基础单位数量
private BigDecimal fulfilledBaseQuantity;
/// 实际单价,单位分
private Long unitPrice;
@@ -89,15 +92,15 @@ public class GaoSalesOrderItem extends UUIDEntity {
/// 备注
private String remark;
/// 待核销数量
/// 待核销基础单位数量
@Transient
private BigDecimal pendingQuantity;
private BigDecimal pendingBaseQuantity;
/// 当前可履约数量
/// 当前可履约基础单位数量
@Transient
private BigDecimal availableQuantity;
private BigDecimal availableBaseQuantity;
/// 权益剩余数量
/// 权益剩余基础单位数量
@Transient
private BigDecimal remainingQuantity;
private BigDecimal remainingBaseQuantity;
}
@@ -22,9 +22,9 @@ public interface GaoSalesOrderItemMapper extends BaseMapper<GaoSalesOrderItem, S
/// 锁定订单明细
GaoSalesOrderItem selectForUpdate(@Param("id") String id);
/// 原子调整待履约数量
int updateReservedQuantity(@Param("id") String id, @Param("delta") BigDecimal delta);
/// 原子调整待履约基础单位数量
int updateReservedBaseQuantity(@Param("id") String id, @Param("delta") BigDecimal delta);
/// 原子调整已履约数量
int updateFulfilledQuantity(@Param("id") String id, @Param("delta") BigDecimal delta);
/// 原子调整已履约基础单位数量
int updateFulfilledBaseQuantity(@Param("id") String id, @Param("delta") BigDecimal delta);
}
@@ -58,11 +58,11 @@ public interface GaoSalesOrderService extends BaseService<GaoSalesOrder, String>
/// 检查履约权限
void checkFulfillmentPermission(GaoSalesOrder order);
/// 原子调整订单项待履约数量
int updateItemReservedQuantity(String itemId, BigDecimal delta);
/// 原子调整订单项待履约基础单位数量
int updateItemReservedBaseQuantity(String itemId, BigDecimal delta);
/// 原子调整订单项已履约数量
int updateItemFulfilledQuantity(String itemId, BigDecimal delta);
/// 原子调整订单项已履约基础单位数量
int updateItemFulfilledBaseQuantity(String itemId, BigDecimal delta);
/// 锁定订单明细
GaoSalesOrderItem getItemForUpdate(String itemId);
@@ -22,6 +22,7 @@ import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePage;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseRequest;
import com.imyeyu.api.modules.gao.vo.GaoCustomerPackagePurchaseItem;
import com.imyeyu.api.modules.gao.vo.GaoPackageOrderCreateData;
import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSelection;
import com.imyeyu.api.modules.gao.vo.GaoSalesOrderCancelRequest;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
@@ -38,6 +39,10 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -53,6 +58,8 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
public class GaoCustomerPackageServiceImplement extends AbstractEntityService<GaoCustomerPackage, String> implements GaoCustomerPackageService {
private static final BigDecimal ZERO = BigDecimal.ZERO;
private final GaoCustomerPackageMapper mapper;
private final GaoPackageConfigService packageConfigService;
private final GaoProductSkuService skuService;
@@ -143,9 +150,13 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
if (request.getItemList() == null) {
request.setItemList(defaultPurchaseItems(configItemList));
}
List<GaoCustomerPackagePurchaseItem> itemList = new java.util.ArrayList<>(request.getItemList());
verifyPurchaseItems(itemList);
itemList.sort(java.util.Comparator.comparing(GaoCustomerPackagePurchaseItem::getSort));
List<GaoCustomerPackagePurchaseItem> requestItemList = new ArrayList<>(request.getItemList());
verifyPurchaseItems(requestItemList);
boolean canOverridePriceAndQuantity = permissionChecker.hasAny(ModuleCode.GAO,
com.imyeyu.api.modules.gao.bean.GaoPermissionCode.CUSTOMER_PACKAGE_PRICE_OVERRIDE.getValue());
List<GaoCustomerPackagePurchaseItem> itemList = resolvePurchaseItems(config.getStoreId(), requestItemList, configItemList, canOverridePriceAndQuantity);
itemList.sort(Comparator.comparing(GaoCustomerPackagePurchaseItem::getSort).thenComparing(GaoCustomerPackagePurchaseItem::getSkuId)
.thenComparing(GaoCustomerPackagePurchaseItem::getSkuUnitId));
request.setItemList(itemList);
GaoSalesOrder existingOrder = salesOrderService.getByStoreIdAndRequestNo(config.getStoreId(), request.getRequestNo());
@@ -154,19 +165,14 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
verifyPurchaseIdempotency(existingOrder, existingPackage, request);
return existingPackage;
}
verifyConfigSellableItems(config.getStoreId(), configItemList);
Long purchasedAt = TimiJava.defaultIfNull(request.getPurchasedAt(), Time.now());
Long now = Time.now();
TimiException.requiredTrue(config.getStatus() == GaoPackageConfig.Status.ACTIVE, "套餐配置不可购买");
TimiException.requiredTrue(config.getBeginAt() == null || config.getBeginAt() <= now, "套餐配置尚未生效");
TimiException.requiredTrue(config.getEndAt() == null || now <= config.getEndAt(), "套餐配置已过期");
GaoCustomer customer = validateCustomer(request.getCustomerId(), config.getStoreId());
verifySameStructureAsConfig(itemList, configItemList);
boolean canOverridePriceAndQuantity = permissionChecker.hasAny(ModuleCode.GAO, com.imyeyu.api.modules.gao.bean.GaoPermissionCode.CUSTOMER_PACKAGE_PRICE_OVERRIDE.getValue());
if (!canOverridePriceAndQuantity) {
TimiException.requiredTrue(request.getSaleAmount().equals(config.getStandardAmount()), "普通员工不能修改套餐成交价");
verifySameQuantityAsConfig(itemList, configItemList);
}
List<GaoSalesOrderItem> orderItemList = itemList.stream().map(this::toOrderItem).toList();
@@ -267,8 +273,9 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
item.setUnitSnapshot(source.getUnitSnapshot());
item.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot());
item.setQuantity(source.getTotalQuantity());
item.setReservedQuantity(BigDecimal.ZERO);
item.setFulfilledQuantity(BigDecimal.ZERO);
item.setBaseQuantity(toBaseQuantity(source.getTotalQuantity(), source.getBaseQuantityPerUnitSnapshot(), "套餐权益基础数量最多保留三位小数"));
item.setReservedBaseQuantity(ZERO);
item.setFulfilledBaseQuantity(ZERO);
item.setUnitPrice(null);
item.setLineAmount(null);
item.setSort(source.getSort());
@@ -277,7 +284,6 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
}
private void verifyPurchaseItems(List<GaoCustomerPackagePurchaseItem> itemList) {
TimiException.requiredTrue(!itemList.isEmpty(), "购买套餐至少需要一个产品");
Set<String> keySet = new HashSet<>();
for (int i = 0; i < itemList.size(); i++) {
GaoCustomerPackagePurchaseItem item = itemList.get(i);
@@ -285,7 +291,7 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
TimiException.required(item.getSkuId(), "购买 SKU 不能为空");
TimiException.required(item.getSkuUnitId(), "购买 SKU 单位不能为空");
TimiException.required(item.getTotalQuantity(), "购买产品数量不能为空");
TimiException.requiredTrue(0 < item.getTotalQuantity().signum(), "购买产品数量必须大于 0");
TimiException.requiredTrue(0 <= item.getTotalQuantity().signum(), "购买产品数量不能为负数");
TimiException.requiredTrue(3 >= item.getTotalQuantity().stripTrailingZeros().scale(), "购买产品数量最多保留三位小数");
String key = item.getSkuId() + "|" + item.getSkuUnitId();
TimiException.requiredTrue(keySet.add(key), "购买套餐内 SKU 和单位不能重复");
@@ -293,38 +299,69 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
}
}
private void verifyConfigSellableItems(String storeId, List<GaoPackageConfigItem> itemList) {
for (GaoPackageConfigItem item : itemList) {
TimiException.required(item.getSkuId(), "套餐配置缺少 SKU");
TimiException.required(item.getSkuUnitId(), "套餐配置缺少 SKU 单位");
// 通过 SKU 服务唯一选品入口重新校验 SKU 和单位仍处于可销售状态
skuService.getSellableSkuUnit(storeId, item.getSkuId(), item.getSkuUnitId());
private List<GaoCustomerPackagePurchaseItem> resolvePurchaseItems(String storeId, List<GaoCustomerPackagePurchaseItem> requestItemList,
List<GaoPackageConfigItem> configItemList, boolean canOverridePriceAndQuantity) {
Map<String, List<GaoPackageConfigItem>> configItemMap = configItemList.stream()
.collect(Collectors.groupingBy(GaoPackageConfigItem::getSkuId));
Map<String, BigDecimal> configBaseQuantityMap = new HashMap<>();
for (GaoPackageConfigItem configItem : configItemList) {
configBaseQuantityMap.merge(configItem.getSkuId(),
toBaseQuantity(configItem.getDefaultQuantity(), configItem.getBaseQuantityPerUnitSnapshot(), "套餐配置基础数量最多保留三位小数"), BigDecimal::add);
}
Map<String, BigDecimal> requestBaseQuantityMap = new HashMap<>();
List<GaoCustomerPackagePurchaseItem> result = new ArrayList<>();
for (GaoCustomerPackagePurchaseItem requestItem : requestItemList) {
List<GaoPackageConfigItem> skuConfigItemList = configItemMap.get(requestItem.getSkuId());
TimiException.requiredTrue(skuConfigItemList != null && !skuConfigItemList.isEmpty(), "购买 SKU 不属于套餐配置");
GaoProductSkuUnitSelection selection = skuService.getSellableSkuUnit(storeId, requestItem.getSkuId(), requestItem.getSkuUnitId());
TimiException.requiredTrue(skuConfigItemList.stream().anyMatch(configItem -> 0 <= configItem.getBaseQuantityPerUnitSnapshot()
.compareTo(selection.getUnit().getBaseQuantityPerUnit())), "套餐权益单位不能高于配置单位");
BigDecimal requestBaseQuantity = toBaseQuantity(requestItem.getTotalQuantity(), selection.getUnit().getBaseQuantityPerUnit(), "套餐权益基础数量最多保留三位小数");
requestBaseQuantityMap.merge(requestItem.getSkuId(), requestBaseQuantity, BigDecimal::add);
if (requestItem.getTotalQuantity().signum() == 0) {
continue;
}
if (!Boolean.TRUE.equals(selection.getSku().getAllowDecimalQuantity())) {
TimiException.requiredTrue(requestItem.getTotalQuantity().stripTrailingZeros().scale() <= 0, "该 SKU 不允许小数数量");
}
GaoPackageConfigItem template = resolveItemTemplate(skuConfigItemList, selection.getUnit().getId());
GaoCustomerPackagePurchaseItem item = new GaoCustomerPackagePurchaseItem();
item.setSkuId(selection.getSku().getId());
item.setSkuUnitId(selection.getUnit().getId());
item.setProductNameSnapshot(selection.getProduct().getName());
item.setProductIdSnapshot(selection.getProduct().getId());
item.setSkuCodeSnapshot(selection.getSku().getCode());
item.setSkuNameSnapshot(selection.getSku().getName());
item.setSpecSummarySnapshot(selection.getSku().getSpecSummary());
item.setUnitSnapshot(selection.getUnit().getName());
item.setBaseQuantityPerUnitSnapshot(selection.getUnit().getBaseQuantityPerUnit());
item.setTotalQuantity(requestItem.getTotalQuantity());
item.setSort(template.getSort());
item.setRemark(template.getRemark());
result.add(item);
}
TimiException.requiredTrue(!result.isEmpty(), "购买套餐至少需要一个产品");
if (!canOverridePriceAndQuantity) {
for (Map.Entry<String, BigDecimal> entry : configBaseQuantityMap.entrySet()) {
TimiException.requiredTrue(entry.getValue().compareTo(requestBaseQuantityMap.getOrDefault(entry.getKey(), ZERO)) == 0,
"普通员工不能修改套餐权益配额");
}
}
return result;
}
private void verifySameStructureAsConfig(List<GaoCustomerPackagePurchaseItem> itemList, List<GaoPackageConfigItem> configItemList) {
TimiException.requiredTrue(itemList.size() == configItemList.size(), "套餐权益项目必须与套餐配置一致");
Map<String, GaoCustomerPackagePurchaseItem> itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), Function.identity()));
for (GaoPackageConfigItem configItem : configItemList) {
GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getSkuId() + "|" + configItem.getSkuUnitId());
TimiException.required(item, "套餐权益项目必须与套餐配置一致");
item.setProductNameSnapshot(configItem.getProductNameSnapshot());
item.setProductIdSnapshot(configItem.getProductIdSnapshot());
item.setSkuCodeSnapshot(configItem.getSkuCodeSnapshot());
item.setSkuNameSnapshot(configItem.getSkuNameSnapshot());
item.setSpecSummarySnapshot(configItem.getSpecSummarySnapshot());
item.setUnitSnapshot(configItem.getUnitSnapshot());
item.setBaseQuantityPerUnitSnapshot(configItem.getBaseQuantityPerUnitSnapshot());
TimiException.requiredTrue(item.getSort().equals(configItem.getSort())
&& sameText(item.getRemark(), configItem.getRemark()), "套餐权益项目的名称、单位、排序和备注不可修改");
}
private GaoPackageConfigItem resolveItemTemplate(List<GaoPackageConfigItem> configItemList, String skuUnitId) {
return configItemList.stream().filter(item -> skuUnitId.equals(item.getSkuUnitId())).findFirst()
.orElseGet(() -> configItemList.stream().min(Comparator.comparing(GaoPackageConfigItem::getSort)).orElseThrow());
}
private void verifySameQuantityAsConfig(List<GaoCustomerPackagePurchaseItem> itemList, List<GaoPackageConfigItem> configItemList) {
Map<String, GaoCustomerPackagePurchaseItem> itemMap = itemList.stream().collect(Collectors.toMap(item -> item.getSkuId() + "|" + item.getSkuUnitId(), Function.identity()));
for (GaoPackageConfigItem configItem : configItemList) {
GaoCustomerPackagePurchaseItem item = itemMap.get(configItem.getSkuId() + "|" + configItem.getSkuUnitId());
TimiException.requiredTrue(item.getTotalQuantity().compareTo(configItem.getDefaultQuantity()) == 0, "普通员工不能修改套餐权益配额");
private BigDecimal toBaseQuantity(BigDecimal quantity, BigDecimal baseQuantityPerUnit, String message) {
BigDecimal result = quantity.multiply(baseQuantityPerUnit);
TimiException.requiredTrue(3 >= result.stripTrailingZeros().scale(), message);
try {
return result.setScale(3, RoundingMode.UNNECESSARY);
} catch (ArithmeticException e) {
throw new TimiException(com.imyeyu.java.bean.timi.TimiCode.ERROR, message, e);
}
}
@@ -29,6 +29,7 @@ import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentItemRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPage;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentPrepareStockRequest;
import com.imyeyu.api.modules.gao.vo.GaoOrderFulfillmentRevokeRequest;
import com.imyeyu.api.modules.gao.vo.GaoProductSkuUnitSelection;
import com.imyeyu.api.modules.gao.vo.GaoStockChangeItem;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
@@ -46,6 +47,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
@@ -194,12 +196,11 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
GaoOrderFulfillment snapshot = mapper.selectById(request.getFulfillmentId());
TimiException.required(snapshot, "not found fulfillment");
GaoSalesOrder order = salesOrderService.getForUpdate(snapshot.getSalesOrderId());
List<SourceEntry> sourceList = loadSources(order, itemRequests(snapshot.getId()), false);
List<SourceEntry> sourceList = loadPersistedSources(order, itemMapper.selectByFulfillmentId(snapshot.getId()));
GaoOrderFulfillment fulfillment = lockFulfillment(snapshot.getId(), order);
storeScopeService.checkStoreId(fulfillment.getStoreId());
TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.WAITING_STOCK, "履约单不是等待库存状态");
TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能准备库存");
salesOrderService.checkFulfillmentPermission(order);
List<GaoOrderFulfillmentItem> fulfillmentItemList = itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId());
TimiException.requiredTrue(!fulfillmentItemList.isEmpty(), "履约单没有明细");
List<GaoStockChangeItem> stockItems = buildStockItems(order.getStoreId(), sourceList);
@@ -228,12 +229,11 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
GaoOrderFulfillment snapshot = mapper.selectById(request.getFulfillmentId());
TimiException.required(snapshot, "not found fulfillment");
GaoSalesOrder order = salesOrderService.getForUpdate(snapshot.getSalesOrderId());
List<SourceEntry> sourceList = loadSources(order, itemRequests(snapshot.getId()), false);
List<SourceEntry> sourceList = loadPersistedSources(order, itemMapper.selectByFulfillmentId(snapshot.getId()));
GaoOrderFulfillment fulfillment = lockFulfillment(snapshot.getId(), order);
storeScopeService.checkStoreId(fulfillment.getStoreId());
TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.PENDING, "履约单不是待核销状态");
TimiException.requiredTrue(order.getStatus() != GaoSalesOrder.Status.CANCELLED, "已取消订单不能核销履约");
salesOrderService.checkFulfillmentPermission(order);
List<GaoOrderFulfillmentItem> fulfillmentItemList = itemMapper.selectByFulfillmentIdForUpdate(fulfillment.getId());
TimiException.requiredTrue(!fulfillmentItemList.isEmpty(), "履约单没有明细");
assertReserved(sourceList);
@@ -283,7 +283,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
GaoOrderFulfillment snapshot = mapper.selectById(request.getFulfillmentId());
TimiException.required(snapshot, "not found fulfillment");
GaoSalesOrder order = salesOrderService.getForUpdate(snapshot.getSalesOrderId());
List<SourceEntry> sourceList = loadSources(order, itemRequests(snapshot.getId()), false);
List<SourceEntry> sourceList = loadPersistedSources(order, itemMapper.selectByFulfillmentId(snapshot.getId()));
GaoOrderFulfillment fulfillment = lockFulfillment(snapshot.getId(), order);
storeScopeService.checkStoreId(fulfillment.getStoreId());
TimiException.requiredTrue(fulfillment.getStatus() == GaoOrderFulfillment.Status.WAITING_STOCK
@@ -326,7 +326,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
}
GaoSalesOrder order = salesOrderService.getForUpdate(sourceSnapshot.getSalesOrderId());
List<GaoOrderFulfillmentItem> sourceItemList = itemMapper.selectByFulfillmentId(sourceSnapshot.getId());
List<SourceEntry> sourceList = loadSources(order, copyRequestItems(sourceItemList), false);
List<SourceEntry> sourceList = loadPersistedSources(order, sourceItemList);
GaoOrderFulfillment source = lockFulfillment(sourceSnapshot.getId(), order);
storeScopeService.checkStoreId(source.getStoreId());
TimiException.requiredTrue(source.getStatus() == GaoOrderFulfillment.Status.VERIFIED && source.getSourceFulfillmentId() == null, "只有原始已核销履约可以冲正");
@@ -356,10 +356,10 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
TimiException.required(sourceItem, "履约明细来源订单项不存在");
GaoOrderFulfillmentItem reverseItem = copyFulfillmentItem(sourceItem);
reverseItem.setFulfillmentId(reverse.getId());
reverseItem.setReservedBefore(sourceEntry.reservedBefore());
reverseItem.setReservedAfter(sourceEntry.reservedBefore());
reverseItem.setFulfilledBefore(sourceEntry.fulfilledBefore());
reverseItem.setFulfilledAfter(sourceEntry.fulfilledBefore().subtract(sourceEntry.quantity()));
reverseItem.setReservedBaseBefore(sourceEntry.reservedBaseBefore());
reverseItem.setReservedBaseAfter(sourceEntry.reservedBaseBefore());
reverseItem.setFulfilledBaseBefore(sourceEntry.fulfilledBaseBefore());
reverseItem.setFulfilledBaseAfter(sourceEntry.fulfilledBaseBefore().subtract(sourceEntry.baseQuantity()));
itemMapper.insert(reverseItem);
}
if (!stockItems.isEmpty()) {
@@ -397,28 +397,28 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
private void insertSourceReservations(GaoOrderFulfillment fulfillment, List<SourceEntry> sourceList) {
for (SourceEntry source : sourceList) {
TimiException.requiredTrue(1 == salesOrderService.updateItemReservedQuantity(source.sourceId(), source.quantity()), "订单明细占用失败");
TimiException.requiredTrue(1 == salesOrderService.updateItemReservedBaseQuantity(source.sourceId(), source.baseQuantity()), "订单明细占用失败");
GaoOrderFulfillmentItem item = source.toFulfillmentItem(fulfillment.getId());
itemMapper.insert(item);
}
}
private void decreaseReserved(SourceEntry source) {
TimiException.requiredTrue(1 == salesOrderService.updateItemReservedQuantity(source.sourceId(), source.quantity().negate()), "订单明细占用释放失败");
TimiException.requiredTrue(1 == salesOrderService.updateItemReservedBaseQuantity(source.sourceId(), source.baseQuantity().negate()), "订单明细占用释放失败");
}
private void increaseFulfilled(SourceEntry source) {
TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledQuantity(source.sourceId(), source.quantity()), "订单明细履约失败");
TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledBaseQuantity(source.sourceId(), source.baseQuantity()), "订单明细履约失败");
}
private void decreaseFulfilled(SourceEntry source) {
TimiException.requiredTrue(0 <= source.fulfilledBefore().compareTo(source.quantity()), "已履约数量不足以冲正");
TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledQuantity(source.sourceId(), source.quantity().negate()), "订单明细冲正失败");
TimiException.requiredTrue(0 <= source.fulfilledBaseBefore().compareTo(source.baseQuantity()), "已履约数量不足以冲正");
TimiException.requiredTrue(1 == salesOrderService.updateItemFulfilledBaseQuantity(source.sourceId(), source.baseQuantity().negate()), "订单明细冲正失败");
}
private void assertReserved(List<SourceEntry> sourceList) {
for (SourceEntry source : sourceList) {
TimiException.requiredTrue(0 <= source.reservedBefore().compareTo(source.quantity()), "履约待占用数量不足");
TimiException.requiredTrue(0 <= source.reservedBaseBefore().compareTo(source.baseQuantity()), "履约待占用数量不足");
}
}
@@ -430,6 +430,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
TimiException.required(requestItem, "履约明细不能为空");
String sourceId = requiredText(requestItem.getSourceOrderItemId(), "履约明细来源订单项不能为空");
requestItem.setSourceOrderItemId(sourceId);
requestItem.setSkuUnitId(requiredText(requestItem.getSkuUnitId(), "履约 SKU 单位不能为空"));
TimiException.requiredTrue(sourceIdSet.add(sourceId), "同一履约单不能重复来源明细");
TimiException.required(requestItem.getQuantity(), "履约数量不能为空");
TimiException.requiredTrue(0 < requestItem.getQuantity().signum(), "履约数量必须大于 0");
@@ -444,40 +445,73 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
GaoSalesOrderItem.ItemType expectedItemType = order.getOrderType() == GaoSalesOrder.OrderType.PACKAGE
? GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT : GaoSalesOrderItem.ItemType.DIRECT_PRODUCT;
TimiException.requiredTrue(source.getItemType() == expectedItemType, "订单项类型与订单类型不匹配");
result.add(toSource(order, source, requestItem.getQuantity(), checkAvailable));
GaoProductSkuUnitSelection selection = skuService.getSellableSkuUnit(order.getStoreId(), source.getSkuId(), requestItem.getSkuUnitId());
result.add(toSource(order, source, selection.getProduct(), selection.getSku(), selection.getUnit(), requestItem.getQuantity(), checkAvailable));
}
return result;
}
private SourceEntry toSource(GaoSalesOrder order, GaoSalesOrderItem source, BigDecimal quantity, boolean checkAvailable) {
GaoProductSku sku = skuService.get(source.getSkuId());
TimiException.required(sku, "履约来源 SKU 不存在");
GaoProduct product = productService.get(sku.getProductId());
TimiException.required(product, "履约来源产品不存在");
TimiException.requiredTrue(order.getStoreId().equals(product.getStoreId()), "履约来源产品不属于当前门店");
BigDecimal baseQuantityPerUnit = requiredBaseQuantityPerUnit(source.getBaseQuantityPerUnitSnapshot());
GaoProductSkuUnit unit = snapshotUnit(source.getSkuUnitId(), source.getUnitSnapshot(), baseQuantityPerUnit);
JsonNode skuSpecSnapshot = source.getSkuSpecSnapshot();
return buildSource(order, source.getId(), source.getSkuId(), source.getSkuUnitId(), source.getProductIdSnapshot(), source.getProductNameSnapshot(), source.getSkuCodeSnapshot(), source.getSkuNameSnapshot(), source.getSpecSummarySnapshot(), skuSpecSnapshot,
source.getUnitSnapshot(), baseQuantityPerUnit, quantity, source.getQuantity(), source.getReservedQuantity(), source.getFulfilledQuantity(),
product, sku, unit, checkAvailable);
private List<SourceEntry> loadPersistedSources(GaoSalesOrder order, List<GaoOrderFulfillmentItem> itemList) {
TimiException.requiredTrue(itemList != null && !itemList.isEmpty(), "履约单没有明细");
List<GaoOrderFulfillmentItem> sortedItemList = new ArrayList<>(itemList);
sortedItemList.sort(Comparator.comparing(GaoOrderFulfillmentItem::getSourceOrderItemId));
Set<String> sourceIdSet = new HashSet<>();
List<SourceEntry> result = new ArrayList<>();
for (GaoOrderFulfillmentItem item : sortedItemList) {
TimiException.required(item, "履约明细不能为空");
String sourceId = requiredText(item.getSourceOrderItemId(), "履约明细来源订单项不能为空");
TimiException.requiredTrue(sourceIdSet.add(sourceId), "同一履约单不能重复来源明细");
GaoSalesOrderItem source = salesOrderService.getItemForUpdate(sourceId);
validateSourceOrderItem(order, source);
GaoProductSku sku = skuService.get(source.getSkuId());
TimiException.required(sku, "履约来源 SKU 不存在");
GaoProduct product = productService.get(sku.getProductId());
TimiException.required(product, "履约来源产品不存在");
TimiException.requiredTrue(order.getStoreId().equals(product.getStoreId()), "履约来源产品不属于当前门店");
BigDecimal baseQuantityPerUnit = requiredBaseQuantityPerUnit(item.getBaseQuantityPerUnitSnapshot());
BigDecimal quantity = requiredPositiveQuantity(item.getQuantity(), "履约数量");
BigDecimal baseQuantity = requiredBaseQuantity(item.getBaseQuantity());
TimiException.requiredTrue(baseQuantity.compareTo(toBaseQuantity(quantity, baseQuantityPerUnit, "履约基础数量最多保留三位小数")) == 0,
"履约基础数量与单位换算快照不一致");
result.add(buildSource(order, source, item.getSkuUnitId(), item.getUnitSnapshot(), baseQuantityPerUnit, quantity, baseQuantity, product, sku, false));
}
return result;
}
private SourceEntry buildSource(GaoSalesOrder order, String orderItemId, String skuId, String unitId, String productId,
String productName, String skuCode, String skuName, String specSummary, JsonNode skuSpecSnapshot, String unitName, BigDecimal baseQuantityPerUnit, BigDecimal quantity, BigDecimal totalQuantity,
BigDecimal reservedQuantity, BigDecimal fulfilledQuantity, GaoProduct product, GaoProductSku sku, GaoProductSkuUnit unit,
boolean checkAvailable) {
private SourceEntry toSource(GaoSalesOrder order, GaoSalesOrderItem source, GaoProduct product, GaoProductSku sku,
GaoProductSkuUnit unit, BigDecimal quantity, boolean checkAvailable) {
TimiException.requiredTrue(source.getSkuId().equals(sku.getId()), "履约单位不属于来源 SKU");
TimiException.requiredTrue(order.getStoreId().equals(product.getStoreId()), "履约来源产品不属于当前门店");
quantity = requiredPositiveQuantity(quantity, "履约数量");
if (!Boolean.TRUE.equals(sku.getAllowDecimalQuantity())) {
TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量");
}
BigDecimal reservedBefore = decimal(reservedQuantity);
BigDecimal fulfilledBefore = decimal(fulfilledQuantity);
BigDecimal baseQuantityPerUnit = requiredBaseQuantityPerUnit(unit.getBaseQuantityPerUnit());
BigDecimal baseQuantity = toBaseQuantity(quantity, baseQuantityPerUnit, "履约基础数量最多保留三位小数");
return buildSource(order, source, unit.getId(), unit.getName(), baseQuantityPerUnit, quantity, baseQuantity, product, sku, checkAvailable);
}
private SourceEntry buildSource(GaoSalesOrder order, GaoSalesOrderItem source, String unitId, String unitName,
BigDecimal baseQuantityPerUnit, BigDecimal quantity, BigDecimal baseQuantity, GaoProduct product, GaoProductSku sku,
boolean checkAvailable) {
BigDecimal reservedBaseBefore = decimal(source.getReservedBaseQuantity());
BigDecimal fulfilledBaseBefore = decimal(source.getFulfilledBaseQuantity());
BigDecimal totalBaseQuantity = requiredBaseQuantity(source.getBaseQuantity());
if (checkAvailable) {
BigDecimal available = decimal(totalQuantity).subtract(reservedBefore).subtract(fulfilledBefore);
TimiException.requiredTrue(0 <= available.compareTo(quantity), "来源产品可履约数量不足");
BigDecimal availableBaseQuantity = totalBaseQuantity.subtract(reservedBaseBefore).subtract(fulfilledBaseBefore);
TimiException.requiredTrue(0 <= availableBaseQuantity.compareTo(baseQuantity), "来源产品可履约数量不足");
}
return new SourceEntry(orderItemId, skuId, unitId, productId, productName, skuCode, skuName, specSummary, skuSpecSnapshot, unitName, baseQuantityPerUnit,
quantity, decimal(totalQuantity), reservedBefore, fulfilledBefore, product, sku, unit);
return new SourceEntry(source.getId(), source.getSkuId(), unitId, source.getProductIdSnapshot(), source.getProductNameSnapshot(),
source.getSkuCodeSnapshot(), source.getSkuNameSnapshot(), source.getSpecSummarySnapshot(), source.getSkuSpecSnapshot(), unitName,
baseQuantityPerUnit, quantity, baseQuantity, totalBaseQuantity, reservedBaseBefore, fulfilledBaseBefore, product, sku);
}
private void validateSourceOrderItem(GaoSalesOrder order, GaoSalesOrderItem source) {
TimiException.required(source, "not found sales order item");
TimiException.requiredTrue(order.getId().equals(source.getSalesOrderId()), "订单明细不属于当前订单");
GaoSalesOrderItem.ItemType expectedItemType = order.getOrderType() == GaoSalesOrder.OrderType.PACKAGE
? GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT : GaoSalesOrderItem.ItemType.DIRECT_PRODUCT;
TimiException.requiredTrue(source.getItemType() == expectedItemType, "订单项类型与订单类型不匹配");
}
private List<GaoStockChangeItem> buildStockItems(String storeId, List<SourceEntry> sourceList) {
@@ -489,7 +523,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
GaoProductSkuUnit baseUnit = skuService.listUnitBySkuId(source.sku().getId()).stream()
.filter(unit -> Boolean.TRUE.equals(unit.getIsBase())).findFirst().orElse(null);
TimiException.required(baseUnit, "实物产品缺少基础库存单位");
BigDecimal baseQuantity = source.quantity().multiply(source.unit().getBaseQuantityPerUnit());
BigDecimal baseQuantity = source.baseQuantity();
GaoStockChangeItem item = itemMap.get(source.sku().getId());
if (item == null) {
item = GaoStockChangeItem.of(source.product(), source.sku(), baseUnit, baseQuantity, baseQuantity);
@@ -528,21 +562,6 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
}
}
private List<GaoOrderFulfillmentItemRequest> itemRequests(String fulfillmentId) {
return copyRequestItems(itemMapper.selectByFulfillmentId(fulfillmentId));
}
private List<GaoOrderFulfillmentItemRequest> copyRequestItems(List<GaoOrderFulfillmentItem> itemList) {
List<GaoOrderFulfillmentItemRequest> result = new ArrayList<>();
for (GaoOrderFulfillmentItem item : TimiJava.safeIterable(itemList)) {
GaoOrderFulfillmentItemRequest request = new GaoOrderFulfillmentItemRequest();
request.setSourceOrderItemId(item.getSourceOrderItemId());
request.setQuantity(item.getQuantity());
result.add(request);
}
return result;
}
private GaoOrderFulfillmentItem copyFulfillmentItem(GaoOrderFulfillmentItem source) {
GaoOrderFulfillmentItem result = new GaoOrderFulfillmentItem();
result.setSkuId(source.getSkuId());
@@ -561,20 +580,36 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
return result;
}
private GaoProductSkuUnit snapshotUnit(String id, String name, BigDecimal baseQuantityPerUnit) {
GaoProductSkuUnit unit = new GaoProductSkuUnit();
unit.setId(id);
unit.setName(name);
unit.setBaseQuantityPerUnit(baseQuantityPerUnit);
return unit;
}
private BigDecimal requiredBaseQuantityPerUnit(BigDecimal value) {
TimiException.required(value, "履约来源缺少基础单位数量系数快照");
TimiException.requiredTrue(0 < value.signum(), "履约来源基础单位数量系数无效");
return value;
}
private BigDecimal requiredPositiveQuantity(BigDecimal value, String fieldName) {
TimiException.required(value, fieldName + "不能为空");
TimiException.requiredTrue(0 < value.signum(), fieldName + "必须大于 0");
TimiException.requiredTrue(3 >= value.stripTrailingZeros().scale(), fieldName + "最多保留三位小数");
return value;
}
private BigDecimal requiredBaseQuantity(BigDecimal value) {
TimiException.required(value, "基础单位数量不能为空");
TimiException.requiredTrue(0 < value.signum(), "基础单位数量必须大于 0");
TimiException.requiredTrue(3 >= value.stripTrailingZeros().scale(), "基础单位数量最多保留三位小数");
return value;
}
private BigDecimal toBaseQuantity(BigDecimal quantity, BigDecimal baseQuantityPerUnit, String message) {
BigDecimal result = quantity.multiply(baseQuantityPerUnit);
TimiException.requiredTrue(3 >= result.stripTrailingZeros().scale(), message);
try {
return result.setScale(3, RoundingMode.UNNECESSARY);
} catch (ArithmeticException e) {
throw new TimiException(TimiCode.ERROR, message, e);
}
}
private void verifyIdempotency(GaoOrderFulfillment existing, GaoOrderFulfillmentCreateRequest request) {
TimiException.requiredTrue(existing.getSalesOrderId().equals(request.getSalesOrderId())
&& (request.getDeliveredAt() == null || request.getDeliveredAt().equals(existing.getDeliveredAt()))
@@ -582,12 +617,14 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
List<GaoOrderFulfillmentItem> existingItems = itemMapper.selectByFulfillmentId(existing.getId());
List<GaoOrderFulfillmentItemRequest> requestItems = request.getItemList();
TimiException.requiredTrue(existingItems.size() == (requestItems == null ? 0 : requestItems.size()), "requestNo 对应履约请求内容不一致");
Map<String, BigDecimal> requestMap = new HashMap<>();
Map<String, GaoOrderFulfillmentItemRequest> requestMap = new HashMap<>();
for (GaoOrderFulfillmentItemRequest item : TimiJava.safeIterable(requestItems)) {
requestMap.put(item.getSourceOrderItemId(), item.getQuantity());
requestMap.put(item.getSourceOrderItemId(), item);
}
for (GaoOrderFulfillmentItem item : existingItems) {
TimiException.requiredTrue(item.getQuantity().compareTo(requestMap.getOrDefault(item.getSourceOrderItemId(), ZERO)) == 0, "requestNo 对应履约请求内容不一致");
GaoOrderFulfillmentItemRequest requestItem = requestMap.get(item.getSourceOrderItemId());
TimiException.requiredTrue(requestItem != null && item.getSkuUnitId().equals(requestItem.getSkuUnitId())
&& item.getQuantity().compareTo(requestItem.getQuantity()) == 0, "requestNo 对应履约请求内容不一致");
}
}
@@ -671,8 +708,8 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
private record SourceEntry(String sourceOrderItemId, String skuId, String skuUnitId, String productIdSnapshot,
String productNameSnapshot, String skuCodeSnapshot, String skuNameSnapshot, String specSummarySnapshot, JsonNode skuSpecSnapshot,
String unitSnapshot, BigDecimal baseQuantityPerUnitSnapshot, BigDecimal quantity,
BigDecimal totalQuantity, BigDecimal reservedBefore, BigDecimal fulfilledBefore, GaoProduct product, GaoProductSku sku,
GaoProductSkuUnit unit) {
BigDecimal baseQuantity, BigDecimal totalBaseQuantity, BigDecimal reservedBaseBefore, BigDecimal fulfilledBaseBefore,
GaoProduct product, GaoProductSku sku) {
private String sourceId() {
return sourceOrderItemId;
@@ -686,11 +723,11 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
result.setProductIdSnapshot(productIdSnapshot);
result.setSourceOrderItemId(sourceOrderItemId);
result.setQuantity(quantity);
result.setBaseQuantity(quantity.multiply(baseQuantityPerUnitSnapshot).setScale(3));
result.setReservedBefore(reservedBefore);
result.setReservedAfter(reservedBefore.add(quantity));
result.setFulfilledBefore(fulfilledBefore);
result.setFulfilledAfter(fulfilledBefore);
result.setBaseQuantity(baseQuantity);
result.setReservedBaseBefore(reservedBaseBefore);
result.setReservedBaseAfter(reservedBaseBefore.add(baseQuantity));
result.setFulfilledBaseBefore(fulfilledBaseBefore);
result.setFulfilledBaseAfter(fulfilledBaseBefore);
result.setProductNameSnapshot(productNameSnapshot);
result.setSkuCodeSnapshot(skuCodeSnapshot);
result.setSkuNameSnapshot(skuNameSnapshot);
@@ -73,6 +73,9 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
private static final BigDecimal ZERO = BigDecimal.ZERO;
// 过渡兼容:旧事件登记系统已收款,但新订单可能仍为零有效收款,暂时允许店长发起履约;过渡结束改为 false 并删除此分支
private static final boolean ALLOW_STORE_MANAGER_ZERO_PAYMENT_FULFILLMENT = true;
private final GaoSalesOrderMapper mapper;
private final GaoSalesOrderItemMapper itemMapper;
private final GaoFundRecordService fundRecordService;
@@ -280,18 +283,21 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
if (safe(order.getSaleAmount()) == 0 || safe(order.getPaidAmount()) == safe(order.getSaleAmount())) {
return;
}
TimiException.requiredTrue(0 < safe(order.getPaidAmount()), "订单未有已核销收款,不能履约");
roleChecker.checkAny(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name());
if (ALLOW_STORE_MANAGER_ZERO_PAYMENT_FULFILLMENT) {
roleChecker.checkAny(ModuleCode.GAO, GaoRoleCode.STORE_MANAGER.name());
return;
}
TimiException.requiredTrue(false, "订单未全额收款,不能发起履约");
}
@Override
public int updateItemReservedQuantity(String itemId, BigDecimal delta) {
return itemMapper.updateReservedQuantity(itemId, delta);
public int updateItemReservedBaseQuantity(String itemId, BigDecimal delta) {
return itemMapper.updateReservedBaseQuantity(itemId, delta);
}
@Override
public int updateItemFulfilledQuantity(String itemId, BigDecimal delta) {
return itemMapper.updateFulfilledQuantity(itemId, delta);
public int updateItemFulfilledBaseQuantity(String itemId, BigDecimal delta) {
return itemMapper.updateFulfilledBaseQuantity(itemId, delta);
}
@Override
@@ -309,7 +315,7 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
List<GaoSalesOrderItem> itemList = itemMapper.selectBySalesOrderId(order.getId());
boolean fulfilled = !itemList.isEmpty();
for (GaoSalesOrderItem item : itemList) {
if (decimal(item.getFulfilledQuantity()).compareTo(decimal(item.getQuantity())) < 0) {
if (decimal(item.getFulfilledBaseQuantity()).compareTo(decimal(item.getBaseQuantity())) < 0) {
fulfilled = false;
break;
}
@@ -383,8 +389,9 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
item.setUnitSnapshot(unit.getName());
item.setBaseQuantityPerUnitSnapshot(unit.getBaseQuantityPerUnit());
item.setQuantity(quantity);
item.setReservedQuantity(ZERO);
item.setFulfilledQuantity(ZERO);
item.setBaseQuantity(toBaseQuantity(quantity, unit.getBaseQuantityPerUnit(), "订单项基础单位数量最多保留三位小数"));
item.setReservedBaseQuantity(ZERO);
item.setFulfilledBaseQuantity(ZERO);
item.setUnitPrice(unitPrice);
item.setLineAmount(lineAmount);
item.setSort(TimiJava.defaultIfNull(source.getSort(), i));
@@ -454,11 +461,14 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
TimiException.requiredTrue(3 >= item.getQuantity().stripTrailingZeros().scale(), "订单项数量最多保留三位小数");
TimiException.required(item.getBaseQuantityPerUnitSnapshot(), "订单项单位换算快照不能为空");
TimiException.requiredTrue(0 < item.getBaseQuantityPerUnitSnapshot().signum(), "订单项单位换算快照必须大于 0");
TimiException.requiredTrue(item.getReservedQuantity() != null && item.getFulfilledQuantity() != null,
TimiException.required(item.getBaseQuantity(), "订单项基础单位数量不能为空");
TimiException.requiredTrue(item.getBaseQuantity().compareTo(toBaseQuantity(item.getQuantity(), item.getBaseQuantityPerUnitSnapshot(), "订单项基础单位数量最多保留三位小数")) == 0,
"订单项基础单位数量与单位换算快照不一致");
TimiException.requiredTrue(item.getReservedBaseQuantity() != null && item.getFulfilledBaseQuantity() != null,
"订单项履约数量不能为空");
TimiException.requiredTrue(0 <= item.getReservedQuantity().signum() && 0 <= item.getFulfilledQuantity().signum(),
TimiException.requiredTrue(0 <= item.getReservedBaseQuantity().signum() && 0 <= item.getFulfilledBaseQuantity().signum(),
"订单项履约数量不能为负数");
TimiException.requiredTrue(item.getReservedQuantity().add(item.getFulfilledQuantity()).compareTo(item.getQuantity()) <= 0,
TimiException.requiredTrue(item.getReservedBaseQuantity().add(item.getFulfilledBaseQuantity()).compareTo(item.getBaseQuantity()) <= 0,
"订单项履约数量不能超过购买数量");
if (expectedItemType == GaoSalesOrderItem.ItemType.PACKAGE_ENTITLEMENT) {
TimiException.requiredNull(item.getUnitPrice(), "套餐订单项不能填写单价");
@@ -544,12 +554,12 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
///
/// @param item 订单项
private void fillItemBalance(GaoSalesOrderItem item) {
BigDecimal quantity = decimal(item.getQuantity());
BigDecimal reserved = decimal(item.getReservedQuantity());
BigDecimal fulfilled = decimal(item.getFulfilledQuantity());
item.setPendingQuantity(reserved);
item.setAvailableQuantity(quantity.subtract(reserved).subtract(fulfilled));
item.setRemainingQuantity(quantity.subtract(fulfilled));
BigDecimal baseQuantity = decimal(item.getBaseQuantity());
BigDecimal reservedBaseQuantity = decimal(item.getReservedBaseQuantity());
BigDecimal fulfilledBaseQuantity = decimal(item.getFulfilledBaseQuantity());
item.setPendingBaseQuantity(reservedBaseQuantity);
item.setAvailableBaseQuantity(baseQuantity.subtract(reservedBaseQuantity).subtract(fulfilledBaseQuantity));
item.setRemainingBaseQuantity(baseQuantity.subtract(fulfilledBaseQuantity));
}
private void fillItemAttachments(List<GaoSalesOrderItem> itemList) {
@@ -606,6 +616,24 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
return quantity;
}
/// 转换基础单位数量
///
/// @param quantity 当前单位数量
/// @param baseQuantityPerUnit 当前单位折算基础单位系数
/// @param message 精度错误信息
/// @return 三位小数的基础单位数量
private BigDecimal toBaseQuantity(BigDecimal quantity, BigDecimal baseQuantityPerUnit, String message) {
TimiException.required(quantity, "数量不能为空");
TimiException.required(baseQuantityPerUnit, "基础单位数量系数不能为空");
BigDecimal result = quantity.multiply(baseQuantityPerUnit);
TimiException.requiredTrue(3 >= result.stripTrailingZeros().scale(), message);
try {
return result.setScale(3, RoundingMode.UNNECESSARY);
} catch (ArithmeticException e) {
throw new TimiException(TimiCode.ERROR, message, e);
}
}
private long toCentAmount(BigDecimal quantity, long unitPrice) {
try {
return BigDecimal.valueOf(unitPrice).multiply(quantity).setScale(0, RoundingMode.UNNECESSARY).longValueExact();
@@ -14,6 +14,9 @@ public class GaoOrderFulfillmentItemRequest {
/// 来源销售订单明细 ID
private String sourceOrderItemId;
/// 本次实际履约 SKU 单位 ID
private String skuUnitId;
/// 本次履约数量
private BigDecimal quantity;
}