update product unit

This commit is contained in:
Timi
2026-09-03 15:16:16 +08:00
parent 731c777b8b
commit 913dc3d278
19 changed files with 102 additions and 66 deletions
@@ -30,8 +30,8 @@ public class GaoCustomerPackageItem extends UUIDEntity {
/// 产品单位快照
private String unitSnapshot;
/// 折算率快照
private BigDecimal conversionToBaseSnapshot;
/// 基础单位数量系数快照
private BigDecimal baseQuantityPerUnitSnapshot;
/// 总权益数量
private BigDecimal totalQuantity;
@@ -53,6 +53,6 @@ public class GaoOrderFulfillmentItem extends UUIDEntity {
/// 单位快照
private String unitSnapshot;
/// 折算率快照
private BigDecimal conversionToBaseSnapshot;
/// 基础单位数量系数快照
private BigDecimal baseQuantityPerUnitSnapshot;
}
@@ -29,8 +29,8 @@ public class GaoPackageConfigItem extends UUIDEntity {
/// 单位快照
private String unitSnapshot;
/// 折算率快照
private BigDecimal conversionToBaseSnapshot;
/// 基础单位数量系数快照
private BigDecimal baseQuantityPerUnitSnapshot;
/// 默认数量
private BigDecimal defaultQuantity;
@@ -77,9 +77,6 @@ public class GaoProduct extends UUIDEntity {
/// 状态
private Status status;
/// 建议售价,单位分
private Long salePrice;
/// 参考成本,单位分
private Long referenceCost;
@@ -20,8 +20,11 @@ public class GaoProductUnit extends UUIDEntity {
/// 单位名称
private String name;
/// 折算为基础单位的比例
private BigDecimal conversionToBase;
/// 每个业务单位对应的基础单位数量
private BigDecimal baseQuantityPerUnit;
/// 该业务单位建议售价,单位分
private Long salePrice;
/// true 为基础库存单位
private Boolean isBase;
@@ -29,8 +29,8 @@ public class GaoSalesOrderItem extends UUIDEntity {
/// 单位快照
private String unitSnapshot;
/// 折算率快照
private BigDecimal conversionToBaseSnapshot;
/// 基础单位数量系数快照
private BigDecimal baseQuantityPerUnitSnapshot;
/// 购买数量
private BigDecimal quantity;
@@ -79,8 +79,8 @@ public class GaoStockRecord implements IDEntity<String>, Creatable {
/// 单位名称快照
private String unitSnapshot;
/// 折算率快照
private BigDecimal conversionToBaseSnapshot;
/// 基础单位数量系数快照
private BigDecimal baseQuantityPerUnitSnapshot;
/// 业务数量
private BigDecimal quantity;
@@ -19,10 +19,10 @@ public interface GaoProductUnitService {
/// @return 规范化后的单位列表
List<GaoProductUnit> validateAndNormalize(GaoProduct product, List<GaoProductUnit> unitList);
/// 按基础单位换算数量
/// 按基础单位数量系数换算业务数量
///
/// @param unit 单位
/// @param quantity 业务数量
/// @return 基础单位数量
BigDecimal convertToBase(GaoProductUnit unit, BigDecimal quantity);
BigDecimal convertQuantityToBase(GaoProductUnit unit, BigDecimal quantity);
}
@@ -250,7 +250,7 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
item.setProductUnitId(source.getProductUnitId());
item.setProductNameSnapshot(source.getProductNameSnapshot());
item.setUnitSnapshot(source.getUnitSnapshot());
item.setConversionToBaseSnapshot(source.getConversionToBaseSnapshot());
item.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot());
item.setTotalQuantity(source.getTotalQuantity());
item.setReservedQuantity(ZERO);
item.setFulfilledQuantity(ZERO);
@@ -315,7 +315,7 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
item.setProductUnitId(configItem.getProductUnitId());
item.setProductNameSnapshot(configItem.getProductNameSnapshot());
item.setUnitSnapshot(configItem.getUnitSnapshot());
item.setConversionToBaseSnapshot(configItem.getConversionToBaseSnapshot());
item.setBaseQuantityPerUnitSnapshot(configItem.getBaseQuantityPerUnitSnapshot());
item.setTotalQuantity(configItem.getDefaultQuantity());
item.setSort(configItem.getSort());
item.setRemark(configItem.getRemark());
@@ -357,7 +357,7 @@ public class GaoCustomerPackageServiceImplement extends AbstractEntityService<Ga
TimiException.required(item, "套餐权益项目必须与事件绑定套餐一致");
item.setProductNameSnapshot(configItem.getProductNameSnapshot());
item.setUnitSnapshot(configItem.getUnitSnapshot());
item.setConversionToBaseSnapshot(configItem.getConversionToBaseSnapshot());
item.setBaseQuantityPerUnitSnapshot(configItem.getBaseQuantityPerUnitSnapshot());
TimiException.requiredTrue(item.getSort().equals(configItem.getSort())
&& sameText(item.getRemark(), configItem.getRemark()), "套餐权益项目的名称、单位、排序和备注不可修改");
}
@@ -462,10 +462,10 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
GaoProduct product = productService.get(source.getProductId());
TimiException.required(product, "履约来源产品不存在");
TimiException.requiredTrue(order.getStoreId().equals(product.getStoreId()), "履约来源产品不属于当前门店");
BigDecimal conversion = requiredConversion(source.getConversionToBaseSnapshot());
GaoProductUnit unit = snapshotUnit(source.getProductUnitId(), source.getUnitSnapshot(), conversion);
BigDecimal baseQuantityPerUnit = requiredBaseQuantityPerUnit(source.getBaseQuantityPerUnitSnapshot());
GaoProductUnit unit = snapshotUnit(source.getProductUnitId(), source.getUnitSnapshot(), baseQuantityPerUnit);
return buildSource(order, source.getId(), null, source.getProductId(), source.getProductUnitId(), source.getProductNameSnapshot(),
source.getUnitSnapshot(), conversion, quantity, source.getQuantity(), source.getReservedQuantity(), source.getFulfilledQuantity(),
source.getUnitSnapshot(), baseQuantityPerUnit, quantity, source.getQuantity(), source.getReservedQuantity(), source.getFulfilledQuantity(),
product, unit, packageSource, checkAvailable);
}
@@ -475,15 +475,15 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
GaoProduct product = productService.get(source.getProductId());
TimiException.required(product, "履约来源产品不存在");
TimiException.requiredTrue(order.getStoreId().equals(product.getStoreId()), "履约来源产品不属于当前门店");
BigDecimal conversion = requiredConversion(source.getConversionToBaseSnapshot());
GaoProductUnit unit = snapshotUnit(source.getProductUnitId(), source.getUnitSnapshot(), conversion);
BigDecimal baseQuantityPerUnit = requiredBaseQuantityPerUnit(source.getBaseQuantityPerUnitSnapshot());
GaoProductUnit unit = snapshotUnit(source.getProductUnitId(), source.getUnitSnapshot(), baseQuantityPerUnit);
return buildSource(order, null, source.getId(), source.getProductId(), source.getProductUnitId(), source.getProductNameSnapshot(),
source.getUnitSnapshot(), conversion, quantity, source.getTotalQuantity(), source.getReservedQuantity(), fulfilledQuantity(source),
source.getUnitSnapshot(), baseQuantityPerUnit, quantity, source.getTotalQuantity(), source.getReservedQuantity(), fulfilledQuantity(source),
product, unit, packageSource, checkAvailable);
}
private SourceEntry buildSource(GaoSalesOrder order, String orderItemId, String packageItemId, String productId, String unitId,
String productName, String unitName, BigDecimal conversion, BigDecimal quantity, BigDecimal totalQuantity,
String productName, String unitName, BigDecimal baseQuantityPerUnit, BigDecimal quantity, BigDecimal totalQuantity,
BigDecimal reservedQuantity, BigDecimal fulfilledQuantity, GaoProduct product, GaoProductUnit unit,
boolean packageSource, boolean checkAvailable) {
if (!Boolean.TRUE.equals(product.getAllowDecimalQuantity())) {
@@ -495,7 +495,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
BigDecimal available = decimal(totalQuantity).subtract(reservedBefore).subtract(fulfilledBefore);
TimiException.requiredTrue(0 <= available.compareTo(quantity), "来源产品可履约数量不足");
}
return new SourceEntry(orderItemId, packageItemId, productId, unitId, productName, unitName, conversion,
return new SourceEntry(orderItemId, packageItemId, productId, unitId, productName, unitName, baseQuantityPerUnit,
quantity, decimal(totalQuantity), reservedBefore, fulfilledBefore, product, unit, packageSource);
}
@@ -508,7 +508,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
GaoProductUnit baseUnit = productService.listUnitByProductId(source.product().getId()).stream()
.filter(unit -> Boolean.TRUE.equals(unit.getIsBase())).findFirst().orElse(null);
TimiException.required(baseUnit, "实物产品缺少基础库存单位");
BigDecimal baseQuantity = unitService.convertToBase(source.unit(), source.quantity());
BigDecimal baseQuantity = unitService.convertQuantityToBase(source.unit(), source.quantity());
GaoStockChangeItem item = itemMap.get(source.product().getId());
if (item == null) {
item = GaoStockChangeItem.of(source.product(), baseUnit, baseQuantity, baseQuantity);
@@ -590,21 +590,21 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
result.setBaseQuantity(source.getBaseQuantity());
result.setProductNameSnapshot(source.getProductNameSnapshot());
result.setUnitSnapshot(source.getUnitSnapshot());
result.setConversionToBaseSnapshot(source.getConversionToBaseSnapshot());
result.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot());
return result;
}
private GaoProductUnit snapshotUnit(String id, String name, BigDecimal conversion) {
private GaoProductUnit snapshotUnit(String id, String name, BigDecimal baseQuantityPerUnit) {
GaoProductUnit unit = new GaoProductUnit();
unit.setId(id);
unit.setName(name);
unit.setConversionToBase(conversion);
unit.setBaseQuantityPerUnit(baseQuantityPerUnit);
return unit;
}
private BigDecimal requiredConversion(BigDecimal value) {
TimiException.required(value, "履约来源缺少折算率快照");
TimiException.requiredTrue(0 < value.signum(), "履约来源折算率无效");
private BigDecimal requiredBaseQuantityPerUnit(BigDecimal value) {
TimiException.required(value, "履约来源缺少基础单位数量系数快照");
TimiException.requiredTrue(0 < value.signum(), "履约来源基础单位数量系数无效");
return value;
}
@@ -665,7 +665,7 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
}
private record SourceEntry(String sourceOrderItemId, String sourceCustomerPackageItemId, String productId, String productUnitId,
String productNameSnapshot, String unitSnapshot, BigDecimal conversionToBaseSnapshot, BigDecimal quantity,
String productNameSnapshot, String unitSnapshot, BigDecimal baseQuantityPerUnitSnapshot, BigDecimal quantity,
BigDecimal totalQuantity, BigDecimal reservedBefore, BigDecimal fulfilledBefore, GaoProduct product,
GaoProductUnit unit, boolean packageSource) {
@@ -681,14 +681,14 @@ public class GaoOrderFulfillmentServiceImplement extends AbstractEntityService<G
result.setSourceOrderItemId(sourceOrderItemId);
result.setSourceCustomerPackageItemId(sourceCustomerPackageItemId);
result.setQuantity(quantity);
result.setBaseQuantity(quantity.multiply(conversionToBaseSnapshot).setScale(3));
result.setBaseQuantity(quantity.multiply(baseQuantityPerUnitSnapshot).setScale(3));
result.setReservedBefore(reservedBefore);
result.setReservedAfter(reservedBefore.add(quantity));
result.setFulfilledBefore(fulfilledBefore);
result.setFulfilledAfter(fulfilledBefore);
result.setProductNameSnapshot(productNameSnapshot);
result.setUnitSnapshot(unitSnapshot);
result.setConversionToBaseSnapshot(conversionToBaseSnapshot);
result.setBaseQuantityPerUnitSnapshot(baseQuantityPerUnitSnapshot);
return result;
}
}
@@ -196,12 +196,12 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService<GaoP
if (!Boolean.TRUE.equals(product.getAllowDecimalQuantity())) {
TimiException.requiredTrue(quantity.stripTrailingZeros().scale() <= 0, "该产品不允许小数数量");
}
unitService.convertToBase(unitEntity, quantity);
unitService.convertQuantityToBase(unitEntity, quantity);
String key = item.getProductId() + "|" + item.getProductUnitId();
TimiException.requiredTrue(keySet.add(key), "同一套餐内产品和单位不能重复");
item.setProductNameSnapshot(product.getName());
item.setUnitSnapshot(unitEntity.getName());
item.setConversionToBaseSnapshot(unitEntity.getConversionToBase());
item.setBaseQuantityPerUnitSnapshot(unitEntity.getBaseQuantityPerUnit());
item.setSort(TimiJava.defaultIfNull(item.getSort(), i));
}
}
@@ -214,7 +214,7 @@ public class GaoPackageConfigServiceImplement extends AbstractEntityService<GaoP
item.setProductUnitId(source.getProductUnitId());
item.setProductNameSnapshot(source.getProductNameSnapshot());
item.setUnitSnapshot(source.getUnitSnapshot());
item.setConversionToBaseSnapshot(source.getConversionToBaseSnapshot());
item.setBaseQuantityPerUnitSnapshot(source.getBaseQuantityPerUnitSnapshot());
item.setDefaultQuantity(source.getDefaultQuantity());
item.setSort(source.getSort());
item.setRemark(source.getRemark());
@@ -143,7 +143,6 @@ public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct
product.setStockEnabled(TimiJava.defaultIfNull(request.getStockEnabled(), product.getStockEnabled()));
product.setAllowDecimalQuantity(TimiJava.defaultIfNull(request.getAllowDecimalQuantity(), product.getAllowDecimalQuantity()));
product.setStatus(TimiJava.defaultIfNull(request.getStatus(), product.getStatus()));
product.setSalePrice(request.getSalePrice());
product.setReferenceCost(request.getReferenceCost());
product.setMinStockQuantity(request.getMinStockQuantity());
product.setSort(TimiJava.defaultIfNull(request.getSort(), product.getSort()));
@@ -197,7 +196,6 @@ public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct
TimiException.requiredTrue(product.getProductType() != GaoProduct.ProductType.SERVICE || !product.getStockEnabled(), "SERVICE 产品不能启用库存");
product.setAllowDecimalQuantity(TimiJava.defaultIfNull(product.getAllowDecimalQuantity(), false));
product.setStatus(TimiJava.defaultIfNull(product.getStatus(), GaoProduct.Status.ACTIVE));
product.setSalePrice(nonNegative(product.getSalePrice(), "建议售价不能为负数"));
product.setReferenceCost(nonNegative(product.getReferenceCost(), "参考成本不能为负数"));
if (!Boolean.TRUE.equals(product.getStockEnabled())) {
TimiException.requiredNull(product.getMinStockQuantity(), "未启用库存的产品不能填写最低库存");
@@ -232,7 +230,8 @@ public class GaoProductServiceImplement extends AbstractEntityService<GaoProduct
unit.setId(existing == null ? null : existing.getId());
unit.setProductId(productId);
unit.setName(source.getName());
unit.setConversionToBase(source.getConversionToBase());
unit.setBaseQuantityPerUnit(source.getBaseQuantityPerUnit());
unit.setSalePrice(source.getSalePrice());
unit.setIsBase(source.getIsBase());
unit.setAllowPurchase(source.getAllowPurchase());
unit.setAllowSale(source.getAllowSale());
@@ -175,7 +175,7 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
GaoProductUnit unit = selection.getUnit();
TimiException.requiredTrue(Boolean.TRUE.equals(product.getStockEnabled()), "该产品未启用库存管理");
BigDecimal quantity = validateQuantity(product, request.getQuantity());
BigDecimal baseQuantity = unitService.convertToBase(unit, quantity);
BigDecimal baseQuantity = unitService.convertQuantityToBase(unit, quantity);
List<GaoStockRecord> existingList = recordService.listByRequestNo(storeId, requestNo);
if (!existingList.isEmpty()) {
GaoStockRecord existing = existingList.getFirst();
@@ -237,7 +237,7 @@ public class GaoProductStockServiceImplement implements GaoProductStockService {
record.setProductUnitId(item.getProductUnitId());
record.setProductNameSnapshot(item.getProductNameSnapshot());
record.setUnitSnapshot(item.getUnitSnapshot());
record.setConversionToBaseSnapshot(item.getConversionToBaseSnapshot());
record.setBaseQuantityPerUnitSnapshot(item.getBaseQuantityPerUnitSnapshot());
record.setQuantity(item.getQuantity());
record.setBaseQuantity(item.getBaseQuantity());
record.setOperationType(operationType);
@@ -44,16 +44,17 @@ public class GaoProductUnitServiceImplement implements GaoProductUnitService {
}
unit.setName(requiredText(source.getName(), "产品单位名称不能为空"));
TimiException.requiredTrue(nameSet.add(unit.getName()), "同一产品的单位名称不能重复");
unit.setConversionToBase(TimiJava.defaultIfNull(source.getConversionToBase(), ONE));
TimiException.requiredTrue(0 < unit.getConversionToBase().signum(), "单位折算率必须大于 0");
TimiException.requiredTrue(6 >= unit.getConversionToBase().scale(), "单位折算率最多保留六位小数");
unit.setBaseQuantityPerUnit(TimiJava.defaultIfNull(source.getBaseQuantityPerUnit(), ONE));
TimiException.requiredTrue(0 < unit.getBaseQuantityPerUnit().signum(), "基础单位数量系数必须大于 0");
TimiException.requiredTrue(6 >= unit.getBaseQuantityPerUnit().scale(), "基础单位数量系数最多保留六位小数");
unit.setSalePrice(nonNegative(source.getSalePrice(), "单位建议售价不能为负数"));
unit.setIsBase(Boolean.TRUE.equals(source.getIsBase()));
unit.setAllowPurchase(TimiJava.defaultIfNull(source.getAllowPurchase(), true));
unit.setAllowSale(TimiJava.defaultIfNull(source.getAllowSale(), true));
unit.setSort(TimiJava.defaultIfNull(source.getSort(), i));
if (unit.getIsBase()) {
TimiException.requiredNull(base, "一个产品只能有一个基础单位");
TimiException.requiredTrue(unit.getConversionToBase().compareTo(ONE) == 0, "基础单位折算率必须为 1");
TimiException.requiredTrue(unit.getBaseQuantityPerUnit().compareTo(ONE) == 0, "基础单位数量系数必须为 1");
TimiException.requiredTrue(baseUnit.equals(unit.getName()), "基础单位名称必须与产品基础单位一致");
base = unit;
}
@@ -65,22 +66,22 @@ public class GaoProductUnitServiceImplement implements GaoProductUnitService {
}
@Override
public BigDecimal convertToBase(GaoProductUnit unit, BigDecimal quantity) {
public BigDecimal convertQuantityToBase(GaoProductUnit unit, BigDecimal quantity) {
TimiException.required(unit, "not found product unit");
TimiException.required(quantity, "数量不能为空");
TimiException.requiredTrue(0 < quantity.signum(), "数量必须大于 0");
BigDecimal result = quantity.multiply(unit.getConversionToBase());
BigDecimal result = quantity.multiply(unit.getBaseQuantityPerUnit());
try {
return result.setScale(3, RoundingMode.UNNECESSARY);
} catch (ArithmeticException e) {
throw new TimiException(com.imyeyu.java.bean.timi.TimiCode.ERROR, "业务数量折算后不能精确保存为三位小数", e);
throw new TimiException(com.imyeyu.java.bean.timi.TimiCode.ERROR, "业务数量换算后不能精确保存为三位小数", e);
}
}
private GaoProductUnit createBaseUnit(GaoProduct product) {
GaoProductUnit unit = new GaoProductUnit();
unit.setName(product.getBaseUnit());
unit.setConversionToBase(ONE);
unit.setBaseQuantityPerUnit(ONE);
unit.setIsBase(true);
unit.setAllowPurchase(Boolean.TRUE.equals(product.getStockEnabled()));
unit.setAllowSale(true);
@@ -94,4 +95,11 @@ public class GaoProductUnitServiceImplement implements GaoProductUnitService {
TimiException.requiredTrue(!result.isEmpty(), message);
return result;
}
private Long nonNegative(Long value, String message) {
if (value != null) {
TimiException.requiredTrue(0 <= value, message);
}
return value;
}
}
@@ -357,13 +357,13 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
GaoProduct product = selection.getProduct();
GaoProductUnit unit = selection.getUnit();
BigDecimal quantity = validateQuantity(product, source.getQuantity());
unitService.convertToBase(unit, quantity);
Long unitPrice = TimiJava.defaultIfNull(source.getUnitPrice(), product.getSalePrice());
unitService.convertQuantityToBase(unit, quantity);
Long unitPrice = TimiJava.defaultIfNull(source.getUnitPrice(), unit.getSalePrice());
TimiException.required(unitPrice, "产品单价不能为空");
TimiException.requiredTrue(0 <= unitPrice, "产品单价不能为负数");
if (!canOverridePrice) {
TimiException.required(product.getSalePrice(), "普通员工只能使用建议售价");
TimiException.requiredTrue(product.getSalePrice().equals(unitPrice), "普通员工只能使用建议售价");
TimiException.required(unit.getSalePrice(), "普通员工只能使用单位建议售价");
TimiException.requiredTrue(unit.getSalePrice().equals(unitPrice), "普通员工只能使用单位建议售价");
}
long lineAmount = toCentAmount(quantity, unitPrice);
GaoSalesOrderItem item = new GaoSalesOrderItem();
@@ -371,7 +371,7 @@ public class GaoSalesOrderServiceImplement extends AbstractEntityService<GaoSale
item.setProductUnitId(unit.getId());
item.setProductNameSnapshot(product.getName());
item.setUnitSnapshot(unit.getName());
item.setConversionToBaseSnapshot(unit.getConversionToBase());
item.setBaseQuantityPerUnitSnapshot(unit.getBaseQuantityPerUnit());
item.setQuantity(quantity);
item.setReservedQuantity(ZERO);
item.setFulfilledQuantity(ZERO);
@@ -23,8 +23,8 @@ public class GaoCustomerPackagePurchaseItem {
/// 单位快照,由后端生成
private String unitSnapshot;
/// 折算率快照,由后端生成
private BigDecimal conversionToBaseSnapshot;
/// 基础单位数量系数快照,由后端生成
private BigDecimal baseQuantityPerUnitSnapshot;
/// 总权益数量
private BigDecimal totalQuantity;
@@ -25,8 +25,8 @@ public class GaoStockChangeItem {
/// 单位名称快照
private String unitSnapshot;
/// 折算率快照
private BigDecimal conversionToBaseSnapshot;
/// 基础单位数量系数快照
private BigDecimal baseQuantityPerUnitSnapshot;
/// 业务数量
private BigDecimal quantity;
@@ -50,7 +50,7 @@ public class GaoStockChangeItem {
result.productUnitId = unit.getId();
result.productNameSnapshot = product.getName();
result.unitSnapshot = unit.getName();
result.conversionToBaseSnapshot = unit.getConversionToBase();
result.baseQuantityPerUnitSnapshot = unit.getBaseQuantityPerUnit();
result.quantity = quantity;
result.baseQuantity = baseQuantity;
return result;