|
@@ -4,6 +4,7 @@ import easydo.technology.components.JdbcClient;
|
|
|
import easydo.technology.enums.MESEnum;
|
|
import easydo.technology.enums.MESEnum;
|
|
|
import easydo.technology.exception.BizException;
|
|
import easydo.technology.exception.BizException;
|
|
|
import easydo.technology.model.*;
|
|
import easydo.technology.model.*;
|
|
|
|
|
+import easydo.technology.model.vo.MaterialRequisitionOutboundVO;
|
|
|
import easydo.technology.model.vo.MaterialRequisitionVO;
|
|
import easydo.technology.model.vo.MaterialRequisitionVO;
|
|
|
import easydo.technology.service.FlowNoService;
|
|
import easydo.technology.service.FlowNoService;
|
|
|
import easydo.technology.service.MaterialRequisitionService;
|
|
import easydo.technology.service.MaterialRequisitionService;
|
|
@@ -33,6 +34,7 @@ public class MaterialRequisitionServiceImpl implements MaterialRequisitionServic
|
|
|
|
|
|
|
|
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
|
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
|
|
+ private static final BigDecimal ZERO = BigDecimal.ZERO;
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
|
@SuppressWarnings("unchecked")
|
|
@SuppressWarnings("unchecked")
|
|
@@ -471,4 +473,588 @@ public class MaterialRequisitionServiceImpl implements MaterialRequisitionServic
|
|
|
jdbcClient.jdbcInsert(item, connection);
|
|
jdbcClient.jdbcInsert(item, connection);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 领料单生成出库单并锁定库存
|
|
|
|
|
+ *
|
|
|
|
|
+ * 业务逻辑:
|
|
|
|
|
+ * 1. 支持灵活出库:可以指定部分物料、部分数量、指定仓库
|
|
|
|
|
+ * 2. 验证出库明细的合法性(物料是否在领料单中、数量是否超标)
|
|
|
|
|
+ * 3. 创建出库单主表,关联领料单、工单、派工单、BOM、销售订单
|
|
|
|
|
+ * 4. 为每个出库明细锁定库存(优先使用销售订单已锁定库存,不足则现场锁定)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param vo 出库请求,包含领料单ID和出库明细列表(物料编码、数量、仓库ID)
|
|
|
|
|
+ * @return 出库单ID
|
|
|
|
|
+ */
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String generateOutbound(MaterialRequisitionOutboundVO vo) throws Exception {
|
|
|
|
|
+ // 1. 获取当前用户
|
|
|
|
|
+ Long userId = SecurityUtils.getCurrentUserId();
|
|
|
|
|
+ if (userId == null) {
|
|
|
|
|
+ throw new BizException("用户未登录");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 验证入参
|
|
|
|
|
+ if (vo.getRequisitionId() == null || vo.getRequisitionId().trim().isEmpty()) {
|
|
|
|
|
+ throw new BizException("领料单ID不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (vo.getItems() == null || vo.getItems().isEmpty()) {
|
|
|
|
|
+ throw new BizException("出库明细不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Connection connection = dataSource.getConnection();
|
|
|
|
|
+ connection.setAutoCommit(false);
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 3. 查询领料单信息
|
|
|
|
|
+ MaterialRequisition requisition = getRequisitionForOutbound(vo.getRequisitionId(), connection);
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 查询领料单明细,用于验证出库明细的合法性
|
|
|
|
|
+ MaterialRequisitionItem itemQuery = new MaterialRequisitionItem();
|
|
|
|
|
+ itemQuery.setRequisitionId(vo.getRequisitionId());
|
|
|
|
|
+ List<MaterialRequisitionItem> requisitionItems = jdbcClient.getJdbcList(itemQuery, connection);
|
|
|
|
|
+ if (requisitionItems == null || requisitionItems.isEmpty()) {
|
|
|
|
|
+ throw new BizException("领料单明细为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 查询该领料单已生成的出库单数量(基于warehouse_record表统计)
|
|
|
|
|
+ // 统计领料单现场锁定的数量(ref_type=REQUISITION)
|
|
|
|
|
+ Map<String, BigDecimal> existingMaterialMap = new java.util.HashMap<>();
|
|
|
|
|
+ for (MaterialRequisitionItem reqItem : requisitionItems) {
|
|
|
|
|
+ WarehouseRecord recordQuery = new WarehouseRecord();
|
|
|
|
|
+ recordQuery.setRefType(MESEnum.WAREHOUSE_RECORD_OF_REF_TYPE_REQUISITION.getValue());
|
|
|
|
|
+ recordQuery.setRefId(vo.getRequisitionId());
|
|
|
|
|
+ recordQuery.setMaterialCode(reqItem.getMaterialCode());
|
|
|
|
|
+ recordQuery.setType(MESEnum.WAREHOUSE_RECORD_OF_TYPE_LOCK.getValue());
|
|
|
|
|
+ List<WarehouseRecord> records = jdbcClient.getJdbcList(recordQuery, connection);
|
|
|
|
|
+
|
|
|
|
|
+ BigDecimal quantity = ZERO;
|
|
|
|
|
+ if (records != null) {
|
|
|
|
|
+ for (WarehouseRecord record : records) {
|
|
|
|
|
+ if (record.getNumber() != null) {
|
|
|
|
|
+ quantity = quantity.add(record.getNumber());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (quantity.compareTo(ZERO) > 0) {
|
|
|
|
|
+ existingMaterialMap.put(reqItem.getMaterialCode(), quantity);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 验证每个出库明细
|
|
|
|
|
+ Map<String, BigDecimal> materialTotalMap = new java.util.HashMap<>();
|
|
|
|
|
+ for (MaterialRequisitionOutboundVO.OutboundItem outboundItem : vo.getItems()) {
|
|
|
|
|
+ // 6.1 验证物料编码
|
|
|
|
|
+ if (outboundItem.getMaterialCode() == null || outboundItem.getMaterialCode().trim().isEmpty()) {
|
|
|
|
|
+ throw new BizException("物料编码不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ // 6.2 验证出库数量
|
|
|
|
|
+ if (outboundItem.getQuantity() == null || outboundItem.getQuantity().compareTo(ZERO) <= 0) {
|
|
|
|
|
+ throw new BizException("出库数量必须大于0");
|
|
|
|
|
+ }
|
|
|
|
|
+ // 6.3 验证仓库ID
|
|
|
|
|
+ if (outboundItem.getWarehouseId() == null || outboundItem.getWarehouseId().trim().isEmpty()) {
|
|
|
|
|
+ throw new BizException("仓库ID不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 6.4 验证物料是否在领料单中
|
|
|
|
|
+ MaterialRequisitionItem requisitionItem = requisitionItems.stream()
|
|
|
|
|
+ .filter(item -> outboundItem.getMaterialCode().equals(item.getMaterialCode()))
|
|
|
|
|
+ .findFirst()
|
|
|
|
|
+ .orElseThrow(() -> new BizException("物料 " + outboundItem.getMaterialCode() + " 不在领料单中"));
|
|
|
|
|
+
|
|
|
|
|
+ // 6.5 累计同一物料的出库数量
|
|
|
|
|
+ String materialCode = outboundItem.getMaterialCode();
|
|
|
|
|
+ BigDecimal currentTotal = materialTotalMap.getOrDefault(materialCode, ZERO);
|
|
|
|
|
+ materialTotalMap.put(materialCode, currentTotal.add(outboundItem.getQuantity()));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 6.6 验证每种物料的累计出库数量(已有+本次)不超过领料单需求数量
|
|
|
|
|
+ for (Map.Entry<String, BigDecimal> entry : materialTotalMap.entrySet()) {
|
|
|
|
|
+ String materialCode = entry.getKey();
|
|
|
|
|
+ BigDecimal thisTimeQuantity = entry.getValue();
|
|
|
|
|
+ BigDecimal existingQuantity = existingMaterialMap.getOrDefault(materialCode, ZERO);
|
|
|
|
|
+ BigDecimal totalQuantity = existingQuantity.add(thisTimeQuantity);
|
|
|
|
|
+
|
|
|
|
|
+ MaterialRequisitionItem requisitionItem = requisitionItems.stream()
|
|
|
|
|
+ .filter(item -> materialCode.equals(item.getMaterialCode()))
|
|
|
|
|
+ .findFirst()
|
|
|
|
|
+ .orElseThrow(() -> new BizException("物料 " + materialCode + " 不在领料单中"));
|
|
|
|
|
+
|
|
|
|
|
+ BigDecimal requiredQuantity = requisitionItem.getRequiredQuantity() != null ? requisitionItem.getRequiredQuantity() : ZERO;
|
|
|
|
|
+ if (totalQuantity.compareTo(requiredQuantity) > 0) {
|
|
|
|
|
+ throw new BizException("物料 " + materialCode + " 累计出库数量(" + totalQuantity + ")不能超过需求数量(" + requiredQuantity + "),已有出库单数量(" + existingQuantity + ")");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 创建出库单主表
|
|
|
|
|
+ WarehouseOutbound outbound = new WarehouseOutbound();
|
|
|
|
|
+ outbound.setRequisitionId(requisition.getId());
|
|
|
|
|
+ outbound.setOrderId(requisition.getOrderId());
|
|
|
|
|
+ outbound.setDispatchId(requisition.getDispatchId());
|
|
|
|
|
+ outbound.setBomId(requisition.getBomId());
|
|
|
|
|
+ outbound.setSaleOrderId(requisition.getSaleOrderId());
|
|
|
|
|
+ outbound.setStatus(MESEnum.MATERIAL_REQUISITION_OUTBOUND_STATUS_PENDING.getValue());
|
|
|
|
|
+ outbound.setTenantId(requisition.getTenantId());
|
|
|
|
|
+ outbound.setCreateId(userId);
|
|
|
|
|
+ outbound.setCreateTime(FORMATTER.format(LocalDateTime.now()));
|
|
|
|
|
+ // 6.1 生成出库单编号
|
|
|
|
|
+ String outboundCode = flowNoService.generateWarehouseOutboundCode(outbound, connection);
|
|
|
|
|
+ outbound.setCode(outboundCode);
|
|
|
|
|
+ jdbcClient.jdbcInsert(outbound, connection);
|
|
|
|
|
+
|
|
|
|
|
+ // 7. 为每个出库明细创建出库单明细并锁定库存
|
|
|
|
|
+ for (MaterialRequisitionOutboundVO.OutboundItem outboundItem : vo.getItems()) {
|
|
|
|
|
+ createOutboundItemAndLock(outbound, requisition, outboundItem, userId, connection);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ connection.commit();
|
|
|
|
|
+ return outbound.getId();
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ connection.rollback();
|
|
|
|
|
+ throw new BizException(e.getMessage());
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ connection.close();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 统计领料单物料库存
|
|
|
|
|
+ *
|
|
|
|
|
+ * 业务逻辑:
|
|
|
|
|
+ * 1. 查询领料单的所有物料明细
|
|
|
|
|
+ * 2. 对每个物料,查询其在所有仓库的库存情况
|
|
|
|
|
+ * 3. 查询该物料已生成的出库单数量(支持分多次出库)
|
|
|
|
|
+ * 4. 按仓库维度组织数据,返回每个物料在每个仓库的库存明细
|
|
|
|
|
+ * 5. 包含总库存、可用库存、锁定库存、销售订单可用锁定量、已出库数量、剩余可出数量
|
|
|
|
|
+ *
|
|
|
|
|
+ * 返回结构:
|
|
|
|
|
+ * {
|
|
|
|
|
+ * "materialCode1": {
|
|
|
|
|
+ * "materialCode": "M001",
|
|
|
|
|
+ * "materialName": "物料名称",
|
|
|
|
|
+ * "requiredQuantity": 100,
|
|
|
|
|
+ * "outboundedQuantity": 60,
|
|
|
|
|
+ * "remainingQuantity": 40,
|
|
|
|
|
+ * "unit": "个",
|
|
|
|
|
+ * "warehouses": [
|
|
|
|
|
+ * {
|
|
|
|
|
+ * "warehouseId": "W1",
|
|
|
|
|
+ * "totalQuantity": 200,
|
|
|
|
|
+ * "normalQuantity": 150,
|
|
|
|
|
+ * "lockedQuantity": 50,
|
|
|
|
|
+ * "saleOrderAvailableLocked": 30
|
|
|
|
|
+ * }
|
|
|
|
|
+ * ]
|
|
|
|
|
+ * }
|
|
|
|
|
+ * }
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param requisitionId 领料单ID
|
|
|
|
|
+ * @return 物料库存统计,按物料和仓库维度组织
|
|
|
|
|
+ */
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public Map<String, Object> getRequisitionMaterialStock(String requisitionId) throws Exception {
|
|
|
|
|
+ Connection connection = dataSource.getConnection();
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 1. 查询领料单信息
|
|
|
|
|
+ MaterialRequisition requisition = getRequisitionForOutbound(requisitionId, connection);
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 查询领料单明细
|
|
|
|
|
+ MaterialRequisitionItem itemQuery = new MaterialRequisitionItem();
|
|
|
|
|
+ itemQuery.setRequisitionId(requisitionId);
|
|
|
|
|
+ List<MaterialRequisitionItem> items = jdbcClient.getJdbcList(itemQuery, connection);
|
|
|
|
|
+ if (items == null || items.isEmpty()) {
|
|
|
|
|
+ throw new BizException("领料单明细为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2.1 查询该领料单已生成的出库单数量(基于warehouse_record表统计)
|
|
|
|
|
+ // 统计领料单现场锁定的数量(ref_type=REQUISITION)
|
|
|
|
|
+ Map<String, BigDecimal> outboundedQuantityMap = new java.util.HashMap<>();
|
|
|
|
|
+ for (MaterialRequisitionItem item : items) {
|
|
|
|
|
+ WarehouseRecord recordQuery = new WarehouseRecord();
|
|
|
|
|
+ recordQuery.setRefType(MESEnum.WAREHOUSE_RECORD_OF_REF_TYPE_REQUISITION.getValue());
|
|
|
|
|
+ recordQuery.setRefId(requisitionId);
|
|
|
|
|
+ recordQuery.setMaterialCode(item.getMaterialCode());
|
|
|
|
|
+ recordQuery.setType(MESEnum.WAREHOUSE_RECORD_OF_TYPE_LOCK.getValue());
|
|
|
|
|
+ List<WarehouseRecord> records = jdbcClient.getJdbcList(recordQuery, connection);
|
|
|
|
|
+
|
|
|
|
|
+ BigDecimal quantity = ZERO;
|
|
|
|
|
+ if (records != null) {
|
|
|
|
|
+ for (WarehouseRecord record : records) {
|
|
|
|
|
+ if (record.getNumber() != null) {
|
|
|
|
|
+ quantity = quantity.add(record.getNumber());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ outboundedQuantityMap.put(item.getMaterialCode(), quantity);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> result = new java.util.HashMap<>();
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 遍历每个物料,统计其在各个仓库的库存
|
|
|
|
|
+ for (MaterialRequisitionItem item : items) {
|
|
|
|
|
+ String materialCode = item.getMaterialCode();
|
|
|
|
|
+
|
|
|
|
|
+ // 3.1 获取该物料已生成出库单数量(从前面统计的结果中获取)
|
|
|
|
|
+ BigDecimal outboundedQuantity = outboundedQuantityMap.getOrDefault(materialCode, ZERO);
|
|
|
|
|
+
|
|
|
|
|
+ // 3.2 计算剩余可出数量
|
|
|
|
|
+ BigDecimal requiredQuantity = item.getRequiredQuantity() != null ? item.getRequiredQuantity() : ZERO;
|
|
|
|
|
+ BigDecimal remainingQuantity = requiredQuantity.subtract(outboundedQuantity);
|
|
|
|
|
+ remainingQuantity = remainingQuantity.compareTo(ZERO) > 0 ? remainingQuantity : ZERO;
|
|
|
|
|
+
|
|
|
|
|
+ // 3.3 查询该物料在所有仓库的库存记录
|
|
|
|
|
+ WarehouseMaterial materialQuery = new WarehouseMaterial();
|
|
|
|
|
+ materialQuery.setMaterialCode(materialCode);
|
|
|
|
|
+ materialQuery.setTenantId(requisition.getTenantId());
|
|
|
|
|
+ List<WarehouseMaterial> materials = jdbcClient.getJdbcList(materialQuery, connection);
|
|
|
|
|
+
|
|
|
|
|
+ // 3.4 按仓库维度组织库存数据
|
|
|
|
|
+ List<Map<String, Object>> warehouseList = new ArrayList<>();
|
|
|
|
|
+ if (materials != null && !materials.isEmpty()) {
|
|
|
|
|
+ for (WarehouseMaterial wm : materials) {
|
|
|
|
|
+ Map<String, Object> warehouseStock = new java.util.HashMap<>();
|
|
|
|
|
+ warehouseStock.put("warehouseId", wm.getWarehouseId());
|
|
|
|
|
+ // 总库存数量
|
|
|
|
|
+ warehouseStock.put("totalQuantity", wm.getNumber() != null ? wm.getNumber() : ZERO);
|
|
|
|
|
+ // 可用库存数量(未锁定)
|
|
|
|
|
+ warehouseStock.put("normalQuantity", wm.getNormalNumber() != null ? wm.getNormalNumber() : ZERO);
|
|
|
|
|
+ // 已锁定库存数量
|
|
|
|
|
+ warehouseStock.put("lockedQuantity", wm.getLockedNumber() != null ? wm.getLockedNumber() : ZERO);
|
|
|
|
|
+
|
|
|
|
|
+ // 3.5 查询该仓库下销售订单可用锁定量
|
|
|
|
|
+ // 销售订单可用锁定量 = 销售订单已锁定量 - 已被其他出库单占用的量
|
|
|
|
|
+ // 这部分库存可以直接复用,无需现场锁定
|
|
|
|
|
+ BigDecimal availableLocked = getSaleOrderAvailableLockedQuantity(
|
|
|
|
|
+ requisition.getSaleOrderId(),
|
|
|
|
|
+ materialCode,
|
|
|
|
|
+ wm.getWarehouseId(),
|
|
|
|
|
+ connection
|
|
|
|
|
+ );
|
|
|
|
|
+ warehouseStock.put("saleOrderAvailableLocked", availableLocked);
|
|
|
|
|
+
|
|
|
|
|
+ warehouseList.add(warehouseStock);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3.6 组装物料维度的数据
|
|
|
|
|
+ Map<String, Object> materialStock = new java.util.HashMap<>();
|
|
|
|
|
+ materialStock.put("materialCode", materialCode);
|
|
|
|
|
+ materialStock.put("materialName", item.getMaterialName());
|
|
|
|
|
+ materialStock.put("requiredQuantity", requiredQuantity);
|
|
|
|
|
+ materialStock.put("outboundedQuantity", outboundedQuantity);
|
|
|
|
|
+ materialStock.put("remainingQuantity", remainingQuantity);
|
|
|
|
|
+ materialStock.put("unit", item.getUnit());
|
|
|
|
|
+ materialStock.put("warehouses", warehouseList);
|
|
|
|
|
+
|
|
|
|
|
+ result.put(materialCode, materialStock);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ throw new BizException(e.getMessage());
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ connection.close();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 查询领料单信息(用于出库单生成)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param requisitionId 领料单ID
|
|
|
|
|
+ * @param connection 数据库连接
|
|
|
|
|
+ * @return 领料单对象
|
|
|
|
|
+ * @throws BizException 领料单ID为空或领料单不存在时抛出异常
|
|
|
|
|
+ */
|
|
|
|
|
+ private MaterialRequisition getRequisitionForOutbound(String requisitionId, Connection connection) throws Exception {
|
|
|
|
|
+ if (requisitionId == null || requisitionId.trim().isEmpty()) {
|
|
|
|
|
+ throw new BizException("领料单ID不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ MaterialRequisition requisitionQuery = new MaterialRequisition();
|
|
|
|
|
+ requisitionQuery.setId(requisitionId);
|
|
|
|
|
+ MaterialRequisition requisition = jdbcClient.getJdbcModelById(requisitionQuery, connection);
|
|
|
|
|
+ if (requisition == null) {
|
|
|
|
|
+ throw new BizException("领料单不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return requisition;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 创建出库单明细并锁定库存
|
|
|
|
|
+ *
|
|
|
|
|
+ * 库存锁定策略(两级优先级):
|
|
|
|
|
+ * 1. 优先使用销售订单已锁定库存:如果该销售订单下该物料在该仓库已有锁定且未被其他出库单占用,优先复用
|
|
|
|
|
+ * 2. 现场锁定新库存:如果销售订单锁定不足,剩余部分从可用库存中现场锁定
|
|
|
|
|
+ *
|
|
|
|
|
+ * 为什么要优先使用销售订单锁定?
|
|
|
|
|
+ * - 避免重复锁定:销售订单下单时已经锁定了库存,领料时可以直接使用
|
|
|
|
|
+ * - 提高库存利用率:同一销售订单的多个领料单可以共享已锁定的库存
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param outbound 出库单主表
|
|
|
|
|
+ * @param requisition 领料单
|
|
|
|
|
+ * @param outboundItem 出库明细(物料编码、数量、仓库ID)
|
|
|
|
|
+ * @param userId 当前用户ID
|
|
|
|
|
+ * @param connection 数据库连接
|
|
|
|
|
+ */
|
|
|
|
|
+ private void createOutboundItemAndLock(WarehouseOutbound outbound,
|
|
|
|
|
+ MaterialRequisition requisition,
|
|
|
|
|
+ MaterialRequisitionOutboundVO.OutboundItem outboundItem,
|
|
|
|
|
+ Long userId,
|
|
|
|
|
+ Connection connection) throws Exception {
|
|
|
|
|
+ BigDecimal required = outboundItem.getQuantity();
|
|
|
|
|
+ if (required.compareTo(ZERO) <= 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String materialCode = outboundItem.getMaterialCode();
|
|
|
|
|
+ String warehouseId = outboundItem.getWarehouseId();
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 查询销售订单在该仓库下该物料的可用锁定量
|
|
|
|
|
+ BigDecimal availableLocked = getSaleOrderAvailableLockedQuantity(requisition.getSaleOrderId(), materialCode, warehouseId, connection);
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 计算使用销售订单锁定的数量(取可用锁定量和需求量的较小值)
|
|
|
|
|
+ BigDecimal useSaleLocked = availableLocked.min(required);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 计算需要现场锁定的数量
|
|
|
|
|
+ BigDecimal remaining = required.subtract(useSaleLocked);
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 如果有可用的销售订单锁定,记录锁定消耗并创建出库明细(标记为SALE来源)
|
|
|
|
|
+ if (useSaleLocked.compareTo(ZERO) > 0) {
|
|
|
|
|
+ // 4.1 插入 warehouse_record 记录,标记销售订单锁定已被该领料单占用
|
|
|
|
|
+ recordLockConsumption(requisition, materialCode, warehouseId, useSaleLocked, userId, connection);
|
|
|
|
|
+ // 4.2 创建出库明细
|
|
|
|
|
+ insertOutboundItem(outbound, materialCode, warehouseId, useSaleLocked, MESEnum.WAREHOUSE_OUTBOUND_LOCK_SOURCE_SALE.getValue(), userId, connection);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 如果需要现场锁定,先锁定库存,再创建出库明细(标记为REQUISITION来源)
|
|
|
|
|
+ if (remaining.compareTo(ZERO) > 0) {
|
|
|
|
|
+ lockWarehouseForRequisition(requisition, materialCode, warehouseId, remaining, userId, connection);
|
|
|
|
|
+ insertOutboundItem(outbound, materialCode, warehouseId, remaining, MESEnum.WAREHOUSE_OUTBOUND_LOCK_SOURCE_REQUISITION.getValue(), userId, connection);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 插入出库单明细
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param outbound 出库单主表
|
|
|
|
|
+ * @param materialCode 物料编码
|
|
|
|
|
+ * @param warehouseId 仓库ID
|
|
|
|
|
+ * @param required 需求数量
|
|
|
|
|
+ * @param lockSource 锁定来源(SALE=销售订单锁定,REQUISITION=领料单现场锁定)
|
|
|
|
|
+ * @param userId 当前用户ID
|
|
|
|
|
+ * @param connection 数据库连接
|
|
|
|
|
+ */
|
|
|
|
|
+ private void insertOutboundItem(WarehouseOutbound outbound,
|
|
|
|
|
+ String materialCode,
|
|
|
|
|
+ String warehouseId,
|
|
|
|
|
+ BigDecimal required,
|
|
|
|
|
+ String lockSource,
|
|
|
|
|
+ Long userId,
|
|
|
|
|
+ Connection connection) throws Exception {
|
|
|
|
|
+ WarehouseOutboundItem outboundItem = new WarehouseOutboundItem();
|
|
|
|
|
+ outboundItem.setOutboundId(outbound.getId());
|
|
|
|
|
+ outboundItem.setMaterialCode(materialCode);
|
|
|
|
|
+ outboundItem.setRequiredQuantity(required);
|
|
|
|
|
+ outboundItem.setActualQuantity(ZERO);
|
|
|
|
|
+ outboundItem.setLockSource(lockSource);
|
|
|
|
|
+ outboundItem.setWarehouseId(warehouseId);
|
|
|
|
|
+ outboundItem.setTenantId(outbound.getTenantId());
|
|
|
|
|
+ outboundItem.setCreateId(userId);
|
|
|
|
|
+ outboundItem.setCreateTime(FORMATTER.format(LocalDateTime.now()));
|
|
|
|
|
+ jdbcClient.jdbcInsert(outboundItem, connection);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 查询销售订单在指定仓库下指定物料的可用锁定量(基于warehouse_record表)
|
|
|
|
|
+ *
|
|
|
|
|
+ * 业务逻辑:
|
|
|
|
|
+ * 1. 查询该销售订单下该物料在该仓库的所有锁定记录(warehouse_record表)
|
|
|
|
|
+ * 2. 只统计"销售订单锁定来源"的记录(排除领料单现场锁定等其他来源)
|
|
|
|
|
+ * 3. 查询领料单现场锁定占用的量(warehouse_record表中ref_type=REQUISITION的记录)
|
|
|
|
|
+ * 4. 计算可用锁定量 = 销售订单锁定量 - 领料单占用量
|
|
|
|
|
+ *
|
|
|
|
|
+ * 什么是"销售订单锁定"?
|
|
|
|
|
+ * - 在销售订单下单时,系统会预先锁定库存,确保有货可发
|
|
|
|
|
+ * - 这部分锁定的库存可以被该销售订单下的多个领料单复用
|
|
|
|
|
+ * - 避免重复锁定,提高库存利用率
|
|
|
|
|
+ *
|
|
|
|
|
+ * 为什么要计算"可用锁定量"?
|
|
|
|
|
+ * - 销售订单锁定的库存可能已经被其他领料单占用
|
|
|
|
|
+ * - 只有未被占用的部分才能被当前出库单使用
|
|
|
|
|
+ * - 例如:销售订单锁定了100个,已有领料单占用了30个,则可用锁定量为70个
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param saleOrderId 销售订单ID
|
|
|
|
|
+ * @param materialCode 物料编码
|
|
|
|
|
+ * @param warehouseId 仓库ID
|
|
|
|
|
+ * @param connection 数据库连接
|
|
|
|
|
+ * @return 可用锁定量(大于等于0)
|
|
|
|
|
+ */
|
|
|
|
|
+ private BigDecimal getSaleOrderAvailableLockedQuantity(String saleOrderId, String materialCode, String warehouseId, Connection connection) throws Exception {
|
|
|
|
|
+ if (saleOrderId == null || saleOrderId.trim().isEmpty()) {
|
|
|
|
|
+ return ZERO;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 查询销售订单在该仓库下该物料的所有锁定记录
|
|
|
|
|
+ WarehouseRecord saleRecordQuery = new WarehouseRecord();
|
|
|
|
|
+ saleRecordQuery.setSaleOrderId(saleOrderId);
|
|
|
|
|
+ saleRecordQuery.setMaterialCode(materialCode);
|
|
|
|
|
+ saleRecordQuery.setFromWarehouseId(warehouseId);
|
|
|
|
|
+ saleRecordQuery.setType(MESEnum.WAREHOUSE_RECORD_OF_TYPE_LOCK.getValue());
|
|
|
|
|
+ List<WarehouseRecord> saleRecords = jdbcClient.getJdbcList(saleRecordQuery, connection);
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 统计销售订单锁定来源的总锁定量(排除领料单现场锁定等其他来源)
|
|
|
|
|
+ BigDecimal totalLocked = ZERO;
|
|
|
|
|
+ if (saleRecords != null) {
|
|
|
|
|
+ for (WarehouseRecord record : saleRecords) {
|
|
|
|
|
+ if (!isSaleOrderLockRecord(record)) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (record.getNumber() != null) {
|
|
|
|
|
+ totalLocked = totalLocked.add(record.getNumber());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (totalLocked.compareTo(ZERO) == 0) {
|
|
|
|
|
+ return ZERO;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 查询领料单现场锁定占用的量(只统计同一销售订单下的领料单锁定)
|
|
|
|
|
+ WarehouseRecord requisitionRecordQuery = new WarehouseRecord();
|
|
|
|
|
+ requisitionRecordQuery.setSaleOrderId(saleOrderId);
|
|
|
|
|
+ requisitionRecordQuery.setMaterialCode(materialCode);
|
|
|
|
|
+ requisitionRecordQuery.setFromWarehouseId(warehouseId);
|
|
|
|
|
+ requisitionRecordQuery.setType(MESEnum.WAREHOUSE_RECORD_OF_TYPE_LOCK.getValue());
|
|
|
|
|
+ requisitionRecordQuery.setRefType(MESEnum.WAREHOUSE_RECORD_OF_REF_TYPE_REQUISITION.getValue());
|
|
|
|
|
+ List<WarehouseRecord> requisitionRecords = jdbcClient.getJdbcList(requisitionRecordQuery, connection);
|
|
|
|
|
+
|
|
|
|
|
+ BigDecimal usedByRequisition = ZERO;
|
|
|
|
|
+ if (requisitionRecords != null) {
|
|
|
|
|
+ for (WarehouseRecord record : requisitionRecords) {
|
|
|
|
|
+ if (record.getNumber() != null) {
|
|
|
|
|
+ usedByRequisition = usedByRequisition.add(record.getNumber());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 计算可用锁定量 = 销售订单锁定量 - 领料单占用量
|
|
|
|
|
+ BigDecimal available = totalLocked.subtract(usedByRequisition);
|
|
|
|
|
+ return available.compareTo(ZERO) > 0 ? available : ZERO;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 仅允许消费”销售订单锁定来源”的库存记录,避免误用领料单现场锁定等其它来源
|
|
|
|
|
+ */
|
|
|
|
|
+ private boolean isSaleOrderLockRecord(WarehouseRecord record) {
|
|
|
|
|
+ String refType = record.getRefType();
|
|
|
|
|
+ return MESEnum.WAREHOUSE_RECORD_OF_REF_TYPE_PRODUCT.getValue().equals(refType)
|
|
|
|
|
+ || MESEnum.WAREHOUSE_RECORD_OF_REF_TYPE_PURCHASE.getValue().equals(refType)
|
|
|
|
|
+ || MESEnum.WAREHOUSE_RECORD_OF_REF_TYPE_OUTSOURCING.getValue().equals(refType);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 记录销售订单锁定的消耗
|
|
|
|
|
+ *
|
|
|
|
|
+ * 业务逻辑:
|
|
|
|
|
+ * 当领料单使用销售订单已锁定的库存时,需要插入 warehouse_record 记录来标记这部分锁定已被占用。
|
|
|
|
|
+ * 这样在统计"销售订单可用锁定量"时,可以准确计算出还有多少锁定量可用。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 注意:
|
|
|
|
|
+ * - 这里只插入记录,不修改 warehouse_material 表(因为预生产计划阶段已经锁定了)
|
|
|
|
|
+ * - ref_type 设置为 REQUISITION,表示这是领料单对销售订单锁定的消耗
|
|
|
|
|
+ * - sale_order_id 继承自领料单,用于按订单隔离统计
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param requisition 领料单
|
|
|
|
|
+ * @param materialCode 物料编码
|
|
|
|
|
+ * @param warehouseId 仓库ID
|
|
|
|
|
+ * @param quantity 消耗数量
|
|
|
|
|
+ * @param userId 当前用户ID
|
|
|
|
|
+ * @param connection 数据库连接
|
|
|
|
|
+ */
|
|
|
|
|
+ private void recordLockConsumption(MaterialRequisition requisition,
|
|
|
|
|
+ String materialCode,
|
|
|
|
|
+ String warehouseId,
|
|
|
|
|
+ BigDecimal quantity,
|
|
|
|
|
+ Long userId,
|
|
|
|
|
+ Connection connection) throws Exception {
|
|
|
|
|
+ // 插入 warehouse_record 记录,标记销售订单锁定已被该领料单占用
|
|
|
|
|
+ WarehouseRecord record = new WarehouseRecord();
|
|
|
|
|
+ record.setType(MESEnum.WAREHOUSE_RECORD_OF_TYPE_LOCK.getValue());
|
|
|
|
|
+ record.setRefType(MESEnum.WAREHOUSE_RECORD_OF_REF_TYPE_REQUISITION.getValue());
|
|
|
|
|
+ record.setRefId(requisition.getId());
|
|
|
|
|
+ record.setSaleOrderId(requisition.getSaleOrderId());
|
|
|
|
|
+ record.setMaterialCode(materialCode);
|
|
|
|
|
+ record.setFromWarehouseId(warehouseId);
|
|
|
|
|
+ record.setNumber(quantity);
|
|
|
|
|
+ record.setTenantId(requisition.getTenantId());
|
|
|
|
|
+ record.setCreateId(userId);
|
|
|
|
|
+ record.setCreateTime(FORMATTER.format(LocalDateTime.now()));
|
|
|
|
|
+ jdbcClient.jdbcInsert(record, connection);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 现场锁定库存(领料单锁定)
|
|
|
|
|
+ *
|
|
|
|
|
+ * 业务逻辑:
|
|
|
|
|
+ * 1. 创建库存锁定记录(warehouse_record表),记录锁定来源为领料单
|
|
|
|
|
+ * 2. 更新库存表(warehouse_material表):
|
|
|
|
|
+ * - normal_number(可用数量)减少
|
|
|
|
|
+ * - locked_number(锁定数量)增加
|
|
|
|
|
+ * 3. 如果可用库存不足,抛出异常
|
|
|
|
|
+ *
|
|
|
|
|
+ * 注意:这是"现场锁定",与"销售订单锁定"不同
|
|
|
|
|
+ * - 销售订单锁定:在销售订单下单时就已经锁定,可以被多个领料单复用
|
|
|
|
|
+ * - 领料单现场锁定:在生成出库单时临时锁定,仅供当前出库单使用
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param requisition 领料单
|
|
|
|
|
+ * @param materialCode 物料编码
|
|
|
|
|
+ * @param warehouseId 仓库ID
|
|
|
|
|
+ * @param lockQuantity 锁定数量
|
|
|
|
|
+ * @param userId 当前用户ID
|
|
|
|
|
+ * @param connection 数据库连接
|
|
|
|
|
+ */
|
|
|
|
|
+ private void lockWarehouseForRequisition(MaterialRequisition requisition,
|
|
|
|
|
+ String materialCode,
|
|
|
|
|
+ String warehouseId,
|
|
|
|
|
+ BigDecimal lockQuantity,
|
|
|
|
|
+ Long userId,
|
|
|
|
|
+ Connection connection) throws Exception {
|
|
|
|
|
+ // 1. 创建库存锁定记录
|
|
|
|
|
+ WarehouseRecord record = new WarehouseRecord();
|
|
|
|
|
+ record.setType(MESEnum.WAREHOUSE_RECORD_OF_TYPE_LOCK.getValue());
|
|
|
|
|
+ record.setMaterialCode(materialCode);
|
|
|
|
|
+ record.setNumber(lockQuantity);
|
|
|
|
|
+ record.setFromWarehouseId(warehouseId);
|
|
|
|
|
+ record.setTenantId(requisition.getTenantId());
|
|
|
|
|
+ record.setRefType(MESEnum.WAREHOUSE_RECORD_OF_REF_TYPE_REQUISITION.getValue());
|
|
|
|
|
+ record.setRefId(requisition.getId());
|
|
|
|
|
+ record.setSaleOrderId(requisition.getSaleOrderId());
|
|
|
|
|
+ record.setCreateId(userId);
|
|
|
|
|
+ record.setCreateTime(FORMATTER.format(LocalDateTime.now()));
|
|
|
|
|
+ jdbcClient.jdbcInsert(record, connection);
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 查询库存记录
|
|
|
|
|
+ WarehouseMaterial wm = new WarehouseMaterial();
|
|
|
|
|
+ wm.setWarehouseId(warehouseId);
|
|
|
|
|
+ wm.setMaterialCode(materialCode);
|
|
|
|
|
+ wm = jdbcClient.getJdbcModel(wm, connection);
|
|
|
|
|
+ if (wm == null) {
|
|
|
|
|
+ throw new BizException("库存记录不存在: " + materialCode);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 验证可用库存是否充足
|
|
|
|
|
+ BigDecimal currentNormal = wm.getNormalNumber() != null ? wm.getNormalNumber() : ZERO;
|
|
|
|
|
+ if (currentNormal.compareTo(lockQuantity) < 0) {
|
|
|
|
|
+ throw new BizException("库存不足: " + materialCode);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 更新库存:扣减可用数量,增加锁定数量
|
|
|
|
|
+ wm.setNormalNumber(currentNormal.subtract(lockQuantity));
|
|
|
|
|
+ wm.setLockedNumber((wm.getLockedNumber() != null ? wm.getLockedNumber() : ZERO).add(lockQuantity));
|
|
|
|
|
+ wm.setUpdateId(userId);
|
|
|
|
|
+ wm.setUpdateTime(FORMATTER.format(LocalDateTime.now()));
|
|
|
|
|
+ jdbcClient.jdbcUpdateById(wm, connection);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
}
|
|
}
|