Commit d04916a9 by wangchunyang
parents 13acd4f6 887e590d
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
>删除</el-button> >删除</el-button>
<el-button <el-button
type="warning" type="success"
plain plain
icon="el-icon-upload" icon="el-icon-upload"
size="medium" size="medium"
......
...@@ -84,7 +84,7 @@ ...@@ -84,7 +84,7 @@
</el-col> </el-col>
<!-- <el-col :span="1.5" v-if="isEditable"> <!-- <el-col :span="1.5" v-if="isEditable">
<el-button <el-button
type="warning" type="success"
plain plain
icon="el-icon-download" icon="el-icon-download"
size="mini" size="mini"
......
...@@ -29,6 +29,14 @@ ...@@ -29,6 +29,14 @@
v-hasPermi="['inventory:locations:remove']" v-hasPermi="['inventory:locations:remove']"
>删除</el-button> >删除</el-button>
<el-button <el-button
type="success"
plain
icon="el-icon-upload"
size="medium"
@click="handleImport"
v-hasPermi="['inventory:locations:add']"
>导入</el-button>
<el-button
type="warning" type="warning"
plain plain
icon="el-icon-download" icon="el-icon-download"
...@@ -36,14 +44,6 @@ ...@@ -36,14 +44,6 @@
@click="handleExport" @click="handleExport"
v-hasPermi="['inventory:locations:export']" v-hasPermi="['inventory:locations:export']"
>导出</el-button> >导出</el-button>
<el-button
type="info"
plain
icon="el-icon-upload2"
size="medium"
@click="handleImport"
v-hasPermi="['inventory:locations:add']"
>导入</el-button>
</template> </template>
</PageTitle> </PageTitle>
......
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
<el-button <el-button
type="success" type="success"
plain plain
icon="el-icon-upload2" icon="el-icon-upload"
size="medium" size="medium"
@click="handleImport" @click="handleImport"
v-hasPermi="['inventory:materials:import']" v-hasPermi="['inventory:materials:import']"
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
>删除</el-button> >删除</el-button>
<el-button <el-button
type="warning" type="success"
plain plain
icon="el-icon-upload" icon="el-icon-upload"
size="medium" size="medium"
......
...@@ -34,6 +34,15 @@ ...@@ -34,6 +34,15 @@
>删除</el-button> >删除</el-button>
<el-button <el-button
type="success"
plain
icon="el-icon-upload"
size="medium"
@click="handleImport"
v-hasPermi="['inventory:owners:add']"
>导入</el-button>
<el-button
type="warning" type="warning"
plain plain
icon="el-icon-download" icon="el-icon-download"
...@@ -42,14 +51,6 @@ ...@@ -42,14 +51,6 @@
v-hasPermi="['inventory:owners:export']" v-hasPermi="['inventory:owners:export']"
>导出</el-button> >导出</el-button>
<el-button
type="info"
plain
icon="el-icon-upload2"
size="medium"
@click="handleImport"
v-hasPermi="['inventory:owners:add']"
>导入</el-button>
</template> </template>
......
...@@ -36,6 +36,14 @@ ...@@ -36,6 +36,14 @@
v-hasPermi="['system:role:remove']" v-hasPermi="['system:role:remove']"
>删除</el-button> >删除</el-button>
<el-button
type="success"
plain
icon="el-icon-upload"
size="medium"
@click="handleImport"
v-hasPermi="['system:role:import']"
>导入</el-button>
<el-button <el-button
type="warning" type="warning"
...@@ -46,15 +54,6 @@ ...@@ -46,15 +54,6 @@
v-hasPermi="['system:role:export']" v-hasPermi="['system:role:export']"
>导出</el-button> >导出</el-button>
<el-button
type="info"
plain
icon="el-icon-upload2"
size="medium"
@click="handleImport"
v-hasPermi="['system:role:import']"
>导入</el-button>
</template> </template>
</PageTitle> </PageTitle>
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
<el-button type="danger" plain icon="el-icon-delete" size="medium" :disabled="multiple" @click="handleDelete" v-hasPermi="['system:user:remove']">删除</el-button> <el-button type="danger" plain icon="el-icon-delete" size="medium" :disabled="multiple" @click="handleDelete" v-hasPermi="['system:user:remove']">删除</el-button>
<el-button type="info" plain icon="el-icon-upload2" size="medium" @click="handleImport" v-hasPermi="['system:user:import']">导入</el-button> <el-button type="success" plain icon="el-icon-upload" size="medium" @click="handleImport" v-hasPermi="['system:user:import']">导入</el-button>
<el-button type="warning" plain icon="el-icon-download" size="medium" @click="handleExport" v-hasPermi="['system:user:export']">导出</el-button> <el-button type="warning" plain icon="el-icon-download" size="medium" @click="handleExport" v-hasPermi="['system:user:export']">导出</el-button>
</template> </template>
......
...@@ -109,5 +109,6 @@ public class OutboundOrders extends BaseEntity ...@@ -109,5 +109,6 @@ public class OutboundOrders extends BaseEntity
/** 出库单明细信息 */ /** 出库单明细信息 */
private List<OutboundOrderItems> outboundOrderItemsList; private List<OutboundOrderItems> outboundOrderItemsList;
@Excel(name = "导入标识")
private Long isImport; // 对应数据库字段is_import,驼峰命名转换
} }
\ No newline at end of file
package com.ruoyi.inventory.service.impl; package com.ruoyi.inventory.service.impl;
import java.util.*;
import java.util.stream.Collectors;
import com.ruoyi.common.annotation.SerialExecution; import com.ruoyi.common.annotation.SerialExecution;
import com.ruoyi.common.config.WarehouseConfig; import com.ruoyi.common.config.WarehouseConfig;
import com.ruoyi.common.core.domain.entity.Materials; import com.ruoyi.common.core.domain.entity.Materials;
import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils; import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.inventory.utils.InventoryCache;
import com.ruoyi.inventory.domain.*; import com.ruoyi.inventory.domain.*;
import com.ruoyi.inventory.domain.vo.OutboundTemplateVO; import com.ruoyi.inventory.domain.vo.OutboundTemplateVO;
import com.ruoyi.inventory.mapper.InventoryMapper; import com.ruoyi.inventory.mapper.InventoryMapper;
import com.ruoyi.inventory.mapper.OutboundOrderItemsMapper; import com.ruoyi.inventory.mapper.OutboundOrderItemsMapper;
import com.ruoyi.inventory.mapper.OutboundOrderLogMapper; import com.ruoyi.inventory.mapper.OutboundOrderLogMapper;
import com.ruoyi.inventory.mapper.OutboundOrdersMapper;
import com.ruoyi.inventory.service.IOutboundOrdersService;
import org.apache.commons.lang3.SystemUtils; import org.apache.commons.lang3.SystemUtils;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.ruoyi.common.utils.StringUtils;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.inventory.mapper.OutboundOrdersMapper;
import com.ruoyi.inventory.service.IOutboundOrdersService;
import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
/** /**
* 出库单主Service业务层处理 * 出库单主Service业务层处理
* 核心修正: * 最终修复版:
* 1. 库存匹配Key统一为 物料ID_库位ID_库存类型(移除仓库维度) * 1. 合并维度调整为「物料ID+库存类型+库位ID」,确保库位信息准确
* 2. 确保inventoryType完整参与匹配 * 2. 扣减时按该维度分组,统一收集记录后合并
* 3. 库存扣减后≤0时强制设置inventory_status=0 * 3. 修复跨场景(无库位→有库位)扣减同一物料+库存类型的合并逻辑
* 4. 无库位库存不足时,自动扣减同物料同库存类型的有库位库存 * 4. 新增相同库存维度的明细合并逻辑,插入前合并为一条记录
* 5. 导入场景下仅保留新增逻辑,移除更新相关处理
* 6. 移除所有仓库相关逻辑,仅保留物料+库位维度匹配
* *
* @author ruoyi * @author ruoyi
* @date 2025-12-03 * @date 2025-12-03
*/ */
@Service @Service
public class OutboundOrdersServiceImpl implements IOutboundOrdersService public class OutboundOrdersServiceImpl implements IOutboundOrdersService {
{
@Autowired @Autowired
private OutboundOrdersMapper outboundOrdersMapper; private OutboundOrdersMapper outboundOrdersMapper;
...@@ -64,6 +63,9 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -64,6 +63,9 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
@Autowired @Autowired
private InventoryMapper inventoryMapper; private InventoryMapper inventoryMapper;
@Autowired
private SqlSessionTemplate sqlSessionTemplate;
/** /**
* 查询出库单主 * 查询出库单主
* *
...@@ -71,8 +73,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -71,8 +73,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
* @return 出库单主 * @return 出库单主
*/ */
@Override @Override
public OutboundOrders selectOutboundOrdersById(String id) public OutboundOrders selectOutboundOrdersById(String id) {
{
return outboundOrdersMapper.selectOutboundOrdersById(id); return outboundOrdersMapper.selectOutboundOrdersById(id);
} }
...@@ -83,8 +84,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -83,8 +84,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
* @return 出库单主 * @return 出库单主
*/ */
@Override @Override
public List<OutboundOrders> selectOutboundOrdersList(OutboundOrders outboundOrders) public List<OutboundOrders> selectOutboundOrdersList(OutboundOrders outboundOrders) {
{
List<OutboundOrders> outboundOrders1 = outboundOrdersMapper.selectOutboundOrdersList(outboundOrders); List<OutboundOrders> outboundOrders1 = outboundOrdersMapper.selectOutboundOrdersList(outboundOrders);
return outboundOrders1; return outboundOrders1;
} }
...@@ -97,11 +97,11 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -97,11 +97,11 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
*/ */
@Transactional @Transactional
@Override @Override
public int insertOutboundOrders(OutboundOrders outboundOrders) public int insertOutboundOrders(OutboundOrders outboundOrders) {
{
outboundOrders.setCreateTime(DateUtils.getNowDate()); outboundOrders.setCreateTime(DateUtils.getNowDate());
outboundOrders.setCreateUserCode(SystemUtils.getUserName()); outboundOrders.setCreateUserCode(SystemUtils.getUserName());
outboundOrders.setId(UUID.randomUUID().toString()); outboundOrders.setId(UUID.randomUUID().toString());
outboundOrders.setIsImport(0l);
int rows = outboundOrdersMapper.insertOutboundOrders(outboundOrders); int rows = outboundOrdersMapper.insertOutboundOrders(outboundOrders);
insertOutboundOrderItems(outboundOrders); insertOutboundOrderItems(outboundOrders);
...@@ -116,8 +116,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -116,8 +116,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
*/ */
@Transactional @Transactional
@Override @Override
public int updateOutboundOrders(OutboundOrders outboundOrders) public int updateOutboundOrders(OutboundOrders outboundOrders) {
{
throw new ServiceException("当前系统仅支持新增出库单,不支持修改操作"); throw new ServiceException("当前系统仅支持新增出库单,不支持修改操作");
} }
...@@ -129,8 +128,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -129,8 +128,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
*/ */
@Transactional @Transactional
@Override @Override
public int deleteOutboundOrdersByIds(String[] ids) public int deleteOutboundOrdersByIds(String[] ids) {
{
outboundOrdersMapper.deleteOutboundOrderItemsByOrderIds(ids); outboundOrdersMapper.deleteOutboundOrderItemsByOrderIds(ids);
outboundOrderLogMapper.deleteOutboundOrderLogByOrdersIds(ids); outboundOrderLogMapper.deleteOutboundOrderLogByOrdersIds(ids);
return outboundOrdersMapper.deleteOutboundOrdersByIds(ids); return outboundOrdersMapper.deleteOutboundOrdersByIds(ids);
...@@ -144,8 +142,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -144,8 +142,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
*/ */
@Transactional @Transactional
@Override @Override
public int deleteOutboundOrdersById(String id) public int deleteOutboundOrdersById(String id) {
{
outboundOrdersMapper.deleteOutboundOrderItemsByOrderId(id); outboundOrdersMapper.deleteOutboundOrderItemsByOrderId(id);
return outboundOrdersMapper.deleteOutboundOrdersById(id); return outboundOrdersMapper.deleteOutboundOrdersById(id);
} }
...@@ -186,16 +183,10 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -186,16 +183,10 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
} }
/** /**
* 核心库存扣减逻辑 * 核心库存扣减逻辑(最终修复版)
* 1. 有库位:直接扣减指定库位库存 * 1. 扣减维度:物料ID+库存类型+库位ID(统一维度,跨场景合并)
* 2. 无库位:先扣无库位库存,不足则扣同物料同库存类型的有库位库存 * 2. 扣减时先按该维度分组,再累计扣减数量
* 3. 扣减后数量≤0时设置inventory_status=0 * 3. 收集记录时按该维度统一收集,合并器仅做最终校验
* 4. 无库位扣减有库位时,记录扣减的库位和数量(用于生成明细)
* 5. 移除仓库维度,仅按物料+库位+库存类型匹配
* @param outboundOrderItems 出库明细
* @param updateUser 操作人
* @param updateTime 操作时间
* @return 无库位明细扣减的有库位库存记录 Map<无库位明细ID, List<扣减的库位库存信息Map>>
*/ */
private Map<String, List<Map<String, Object>>> deductInventory(List<OutboundOrderItems> outboundOrderItems, String updateUser, Date updateTime) { private Map<String, List<Map<String, Object>>> deductInventory(List<OutboundOrderItems> outboundOrderItems, String updateUser, Date updateTime) {
if (CollectionUtils.isEmpty(outboundOrderItems)) { if (CollectionUtils.isEmpty(outboundOrderItems)) {
...@@ -203,10 +194,12 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -203,10 +194,12 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
} }
Map<String, List<Map<String, Object>>> deductRecordMap = new HashMap<>(); Map<String, List<Map<String, Object>>> deductRecordMap = new HashMap<>();
Map<String, List<Inventory>> inventoryGroupMap = this.loadInventoryGroupMap(); // 预加载库存:按「物料ID+库存类型+库位ID」分组(核心调整)
Map<String, Inventory> inventoryFullMap = this.loadInventoryFullMap();
Map<String, Long> deductQtyMap = this.buildDeductQtyMap(outboundOrderItems); Map<String, Long> deductQtyMap = this.buildDeductQtyMap(outboundOrderItems);
List<Inventory> needUpdateList = new ArrayList<>(); // 库存更新Map(最终去重,保留最终状态)
Map<String, Inventory> toUpdateInventoryMap = new LinkedHashMap<>();
for (Map.Entry<String, Long> entry : deductQtyMap.entrySet()) { for (Map.Entry<String, Long> entry : deductQtyMap.entrySet()) {
String key = entry.getKey(); String key = entry.getKey();
...@@ -217,121 +210,83 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -217,121 +210,83 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
String materialId = keyParts.length > 0 ? keyParts[0] : ""; String materialId = keyParts.length > 0 ? keyParts[0] : "";
String locationId = keyParts.length > 1 ? keyParts[1] : ""; String locationId = keyParts.length > 1 ? keyParts[1] : "";
String inventoryType = keyParts.length > 2 ? keyParts[2] : ""; String inventoryType = keyParts.length > 2 ? keyParts[2] : "";
String groupKey = String.join("_", materialId, inventoryType);
List<Inventory> inventoryList = inventoryGroupMap.get(groupKey);
if (CollectionUtils.isEmpty(inventoryList)) {
throw new ServiceException(String.format(
"物料[%s]库存类型[%s]无可用库存,无法扣减",
materialId, inventoryType));
}
Long remainDeductQty = totalDeductQty;
List<Map<String, Object>> deductRecords = new ArrayList<>();
String itemId = outboundOrderItems.stream() String itemId = outboundOrderItems.stream()
.filter(item -> key.equals(buildDeductKey(item))) .filter(item -> key.equals(buildDeductKey(item)))
.map(OutboundOrderItems::getId) .map(OutboundOrderItems::getId)
.findFirst() .findFirst()
.orElse(null); .orElse(null);
// 区分无库位/有库位逻辑 Long remainDeductQty = totalDeductQty;
if (StringUtils.isBlank(locationId)) { // 核心:按「物料ID+库存类型+库位ID」分组扣减(先处理无库位,再处理有库位)
// 无库位:先扣无库位库存,再扣有库位库存 List<Map<String, Object>> tempDeductRecords = new ArrayList<>();
// 第一步:扣无库位库存
List<Inventory> noLocationInvList = inventoryList.stream()
.filter(inv -> StringUtils.isBlank(inv.getLocationId()))
.collect(Collectors.toList());
for (Inventory inv : noLocationInvList) {
if (remainDeductQty <= 0) break;
Long currentQty = Optional.ofNullable(inv.getQuantity()).orElse(0L);
Long deductQty = Math.min(remainDeductQty, currentQty); // 不扣负数,只扣现有库存
inv.setQuantity(currentQty - deductQty);
if (inv.getQuantity() <= 0) {
inv.setInventoryStatus(0L);
}
inv.setUpdateBy(updateUser);
inv.setUpdateTime(updateTime);
needUpdateList.add(inv);
// 记录无库位扣减 // 步骤1:先扣指定维度的库存(无库位/有库位)
deductRecords.add(buildDeductRecord(inv, inventoryType, deductQty)); if (StringUtils.isBlank(locationId)) {
remainDeductQty -= deductQty; // 无库位:先扣「物料+库存类型+空库位」的库存
} String noLocKey = buildInventoryKey(materialId, "", inventoryType);
remainDeductQty = deductByInventoryKey(noLocKey, remainDeductQty, updateUser, updateTime, inventoryFullMap, toUpdateInventoryMap, tempDeductRecords);
// 第二步:无库位不足,扣有库位库存 // 无库位不足,扣「物料+库存类型+任意有库位」的库存
if (remainDeductQty > 0) { if (remainDeductQty > 0) {
List<Inventory> hasLocationInvList = inventoryList.stream() // 筛选该物料+库存类型的所有有库位库存
.filter(inv -> StringUtils.isNotBlank(inv.getLocationId())) List<String> hasLocKeys = inventoryFullMap.keySet().stream()
.filter(k -> {
String[] parts = k.split("_");
return parts.length >= 3
&& parts[0].equals(materialId)
&& parts[2].equals(inventoryType)
&& StringUtils.isNotBlank(parts[1]);
})
.collect(Collectors.toList()); .collect(Collectors.toList());
for (Inventory inv : hasLocationInvList) {
if (remainDeductQty <= 0) break;
Long currentQty = Optional.ofNullable(inv.getQuantity()).orElse(0L);
Long deductQty = Math.min(remainDeductQty, currentQty); // 不扣负数
inv.setQuantity(currentQty - deductQty); for (String hasLocKey : hasLocKeys) {
if (inv.getQuantity() <= 0) { if (remainDeductQty <= 0) break;
inv.setInventoryStatus(0L); remainDeductQty = deductByInventoryKey(hasLocKey, remainDeductQty, updateUser, updateTime, inventoryFullMap, toUpdateInventoryMap, tempDeductRecords);
} }
inv.setUpdateBy(updateUser);
inv.setUpdateTime(updateTime);
needUpdateList.add(inv);
// 记录有库位扣减
deductRecords.add(buildDeductRecord(inv, inventoryType, deductQty));
remainDeductQty -= deductQty;
} }
} else {
// 有库位:扣指定「物料+库存类型+库位ID」的库存
String targetKey = buildInventoryKey(materialId, locationId, inventoryType);
remainDeductQty = deductByInventoryKey(targetKey, remainDeductQty, updateUser, updateTime, inventoryFullMap, toUpdateInventoryMap, tempDeductRecords);
} }
// 最后仍有剩余(所有库存耗尽),允许扣最后一个库存为负数 // 步骤2:剩余部分扣负数(最后一个库存)
if (remainDeductQty > 0 && !inventoryList.isEmpty()) { if (remainDeductQty > 0 && !tempDeductRecords.isEmpty()) {
Inventory lastInv = inventoryList.get(inventoryList.size() - 1); Map<String, Object> lastRecord = tempDeductRecords.get(tempDeductRecords.size() - 1);
Long deductQty = remainDeductQty; String lastInvId = (String) lastRecord.get("inventoryId");
lastInv.setQuantity(Optional.ofNullable(lastInv.getQuantity()).orElse(0L) - deductQty); Inventory lastInv = toUpdateInventoryMap.get(lastInvId);
if (lastInv == null) {
throw new ServiceException(String.format("物料[%s]库存类型[%s]扣减负数时未找到目标库存", materialId, inventoryType));
}
// 累计扣减负数数量
Long finalDeduct = remainDeductQty;
lastInv.setQuantity(lastInv.getQuantity() - finalDeduct);
lastInv.setInventoryStatus(0L); lastInv.setInventoryStatus(0L);
lastInv.setUpdateBy(updateUser); lastInv.setUpdateBy(updateUser);
lastInv.setUpdateTime(updateTime); lastInv.setUpdateTime(updateTime);
needUpdateList.add(lastInv); toUpdateInventoryMap.put(lastInvId, lastInv);
deductRecords.add(buildDeductRecord(lastInv, inventoryType, deductQty));
remainDeductQty = 0L; // 合并到最后一条记录
} lastRecord.put("deductQty", (Long) lastRecord.get("deductQty") + finalDeduct);
} else {
// 有库位:直接扣指定库位库存
Inventory targetInv = inventoryList.stream()
.filter(inv -> locationId.equals(inv.getLocationId()))
.findFirst()
.orElseThrow(() -> new ServiceException(String.format(
"物料[%s]库位[%s]无库存", materialId, locationId)));
Long currentQty = Optional.ofNullable(targetInv.getQuantity()).orElse(0L);
Long deductQty = Math.min(remainDeductQty, currentQty);
targetInv.setQuantity(currentQty - deductQty);
if (targetInv.getQuantity() <= 0) {
targetInv.setInventoryStatus(0L);
}
targetInv.setUpdateBy(updateUser);
targetInv.setUpdateTime(updateTime);
needUpdateList.add(targetInv);
deductRecords.add(buildDeductRecord(targetInv, inventoryType, deductQty));
remainDeductQty -= deductQty;
// 剩余部分扣为负数
if (remainDeductQty > 0) {
Long finalDeduct = remainDeductQty;
targetInv.setQuantity(targetInv.getQuantity() - finalDeduct);
targetInv.setInventoryStatus(0L);
deductRecords.add(buildDeductRecord(targetInv, inventoryType, finalDeduct));
remainDeductQty = 0L; remainDeductQty = 0L;
} }
// 步骤3:统一合并(按物料+库存类型+库位ID)
if (itemId != null && !tempDeductRecords.isEmpty()) {
List<Map<String, Object>> mergedRecords = mergeDeductRecords(tempDeductRecords);
deductRecordMap.put(itemId, mergedRecords);
} }
// 关联明细与扣减记录 // 校验是否扣减完成
if (itemId != null && !deductRecords.isEmpty()) { if (remainDeductQty > 0) {
deductRecordMap.put(itemId, deductRecords); throw new ServiceException(String.format("物料[%s]库存类型[%s]扣减失败,剩余%d数量未扣减", materialId, inventoryType, remainDeductQty));
} }
} }
// 批量更新库存 // 批量更新库存(最终去重)
if (!needUpdateList.isEmpty()) { if (!toUpdateInventoryMap.isEmpty()) {
List<Inventory> needUpdateList = new ArrayList<>(toUpdateInventoryMap.values());
inventoryMapper.batchUpdateInventory(needUpdateList); inventoryMapper.batchUpdateInventory(needUpdateList);
inventoryService.RefreshInventory(needUpdateList.stream().map(Inventory::getId).distinct().collect(Collectors.toList())); inventoryService.RefreshInventory(needUpdateList.stream().map(Inventory::getId).distinct().collect(Collectors.toList()));
} }
...@@ -339,82 +294,126 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -339,82 +294,126 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
return deductRecordMap; return deductRecordMap;
} }
private Map<String, Object> buildDeductRecord(Inventory inv, String inventoryType, Long deductQty) {
Map<String, Object> record = new HashMap<>();
record.put("inventoryId", inv.getId());
record.put("locationId", inv.getLocationId());
record.put("materialId", inv.getMaterialId());
record.put("inventoryType", inventoryType);
record.put("deductQty", deductQty);
return record;
}
/** /**
* 扣减单个库存的数量 * 按「物料ID+库位ID+库存类型」扣减指定库存
* @param inventory 库存对象 * @param inventoryKey 库存Key(物料ID_库位ID_库存类型)
* @param deductQty 待扣减数量 * @param deductQty 待扣减数量
* @param updateUser 操作人
* @param updateTime 操作时间
* @return 剩余未扣减数量 * @return 剩余未扣减数量
*/ */
private Long deductSingleInventory(Inventory inventory, Long deductQty, String updateUser, Date updateTime) { private Long deductByInventoryKey(String inventoryKey, Long deductQty, String updateUser, Date updateTime,
Long currentQty = Optional.ofNullable(inventory.getQuantity()).orElse(0L); Map<String, Inventory> inventoryFullMap, Map<String, Inventory> toUpdateInventoryMap,
// 关键修改:不再限制扣减数量,直接扣减(允许库存变为负数) List<Map<String, Object>> tempDeductRecords) {
Long canDeductQty = deductQty; // 原逻辑:Math.min(deductQty, currentQty) Inventory inv = inventoryFullMap.get(inventoryKey);
if (inv == null) {
return deductQty;
}
// 扣减数量(允许负数) Long currentQty = Optional.ofNullable(inv.getQuantity()).orElse(0L);
Long newQty = currentQty - canDeductQty; Long canDeduct = Math.min(deductQty, currentQty);
inventory.setQuantity(newQty);
// 扣减后≤0,设置状态为0(即使是负数也设置) // 更新库存状态
if (newQty <= 0) { inv.setQuantity(currentQty - canDeduct);
inventory.setInventoryStatus(0L); inv.setInventoryStatus(inv.getQuantity() <= 0 ? 0L : 1L);
inv.setUpdateBy(updateUser);
inv.setUpdateTime(updateTime);
toUpdateInventoryMap.put(inv.getId(), inv);
// 收集扣减记录(按统一维度)
Map<String, Object> record = buildDeductRecord(inv, inv.getInventoryType().toString(), canDeduct);
tempDeductRecords.add(record);
// 返回剩余未扣减数量
return deductQty - canDeduct;
}
/**
* 合并同一「物料ID+库存类型+库位ID」的扣减记录(最终版)
* 核心:按「物料ID_库位ID_库存类型」合并,保留库位信息准确性
*/
private List<Map<String, Object>> mergeDeductRecords(List<Map<String, Object>> deductRecords) {
if (CollectionUtils.isEmpty(deductRecords)) {
return Collections.emptyList();
} }
// 补充审计字段 // 合并Key:物料ID_库位ID_库存类型
inventory.setUpdateBy(updateUser); Map<String, Map<String, Object>> mergeMap = new LinkedHashMap<>();
inventory.setUpdateTime(updateTime); for (Map<String, Object> record : deductRecords) {
String materialId = (String) record.get("materialId");
String locationId = (String) record.get("locationId");
String inventoryType = (String) record.get("inventoryType");
Long deductQty = (Long) record.get("deductQty");
// 返回剩余未扣减数量(扣减全部,剩余为0) if (StringUtils.isBlank(materialId) || StringUtils.isBlank(inventoryType) || deductQty <= 0) {
return 0L; // 原逻辑:deductQty - canDeductQty continue;
}
String mergeKey = buildInventoryKey(materialId, locationId, inventoryType);
if (mergeMap.containsKey(mergeKey)) {
Map<String, Object> existRecord = mergeMap.get(mergeKey);
existRecord.put("deductQty", (Long) existRecord.get("deductQty") + deductQty);
} else {
Map<String, Object> newRecord = new HashMap<>(record);
mergeMap.put(mergeKey, newRecord);
}
}
return new ArrayList<>(mergeMap.values());
} }
/** /**
* 预加载库存分组Map * 构建库存Key:物料ID_库位ID_库存类型
* Key=物料ID_库存类型 Value=该维度下所有库位的库存列表(含无库位)
* 移除仓库维度
*/ */
private Map<String, List<Inventory>> loadInventoryGroupMap() { private String buildInventoryKey(String materialId, String locationId, String inventoryType) {
Inventory query = new Inventory(); return String.join("_",
query.setInventoryStatus(1L); Optional.ofNullable(materialId).orElse(""),
query.setIsUsed(1L); Optional.ofNullable(locationId).orElse(""),
List<Inventory> inventoryList = inventoryService.selectInventoryList(query); Optional.ofNullable(inventoryType).orElse("")
);
}
if (CollectionUtils.isEmpty(inventoryList)) { /**
* 构建扣减记录
*/
private Map<String, Object> buildDeductRecord(Inventory inv, String inventoryType, Long deductQty) {
Map<String, Object> record = new HashMap<>();
record.put("inventoryId", inv.getId());
record.put("locationId", inv.getLocationId());
record.put("materialId", inv.getMaterialId());
record.put("inventoryType", inventoryType);
record.put("deductQty", deductQty);
return record;
}
/**
* 预加载库存全量Map(按「物料ID_库位ID_库存类型」为Key)
*/
private Map<String, Inventory> loadInventoryFullMap() {
Collection<Inventory> allInventory = InventoryCache.getAll().values();
if (CollectionUtils.isEmpty(allInventory)) {
return Collections.emptyMap(); return Collections.emptyMap();
} }
// 按「物料ID_库存类型」分组(移除仓库维度) return allInventory.stream()
return inventoryList.stream() .collect(Collectors.toMap(
.collect(Collectors.groupingBy( inv -> buildInventoryKey(
inv -> String.join("_", inv.getMaterialId(),
Optional.ofNullable(inv.getMaterialId()).orElse(""), inv.getLocationId(),
Optional.ofNullable(inv.getInventoryType()).map(String::valueOf).orElse("") Optional.ofNullable(inv.getInventoryType()).map(String::valueOf).orElse("")
), ),
HashMap::new, inv -> inv,
Collectors.toList() (k1, k2) -> k1, // 重复Key保留第一个
LinkedHashMap::new
)); ));
} }
/** /**
* 构建扣减数量Map * 构建扣减数量Map
* Key规则:物料ID_库位ID_库存类型(移除仓库维度) * Key规则:物料ID_库位ID_库存类型
*/ */
private Map<String, Long> buildDeductQtyMap(List<OutboundOrderItems> items) { private Map<String, Long> buildDeductQtyMap(List<OutboundOrderItems> items) {
Map<String, Long> deductQtyMap = new HashMap<>(); Map<String, Long> deductQtyMap = new HashMap<>();
for (OutboundOrderItems item : items) { for (OutboundOrderItems item : items) {
String key = buildDeductKey(item); String key = buildDeductKey(item);
// 累加扣减数量
Long qty = Optional.ofNullable(item.getActualQuantity()).orElse(0L); Long qty = Optional.ofNullable(item.getActualQuantity()).orElse(0L);
deductQtyMap.put(key, deductQtyMap.getOrDefault(key, 0L) + qty); deductQtyMap.put(key, deductQtyMap.getOrDefault(key, 0L) + qty);
} }
...@@ -423,18 +422,17 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -423,18 +422,17 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
/** /**
* 构建明细的扣减Key * 构建明细的扣减Key
* 移除仓库维度,Key规则:物料ID_库位ID_库存类型
*/ */
private String buildDeductKey(OutboundOrderItems item) { private String buildDeductKey(OutboundOrderItems item) {
return String.join("_", return buildInventoryKey(
Optional.ofNullable(item.getMaterialId()).orElse(""), item.getMaterialId(),
Optional.ofNullable(item.getLocationId()).orElse(""), item.getLocationId(),
Optional.ofNullable(item.getInventoryType()).map(String::valueOf).orElse("") Optional.ofNullable(item.getInventoryType()).map(String::valueOf).orElse("")
); );
} }
@Override @Override
public List<Map<String,String>> outboundOrdersTopTenByQuantity() { public List<Map<String, String>> outboundOrdersTopTenByQuantity() {
return outboundOrdersMapper.SelectOutboundOrdersMaterialsTopTenByQuantity(); return outboundOrdersMapper.SelectOutboundOrdersMaterialsTopTenByQuantity();
} }
...@@ -450,8 +448,6 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -450,8 +448,6 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
/** /**
* 新增出库单明细信息 * 新增出库单明细信息
*
* @param outboundOrders 出库单主对象
*/ */
public void insertOutboundOrderItems(OutboundOrders outboundOrders) { public void insertOutboundOrderItems(OutboundOrders outboundOrders) {
List<OutboundOrderItems> outboundOrderItemsList = outboundOrders.getOutboundOrderItemsList(); List<OutboundOrderItems> outboundOrderItemsList = outboundOrders.getOutboundOrderItemsList();
...@@ -466,20 +462,23 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -466,20 +462,23 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
throw new RuntimeException("库存被修改请重新确认"); throw new RuntimeException("库存被修改请重新确认");
} }
// 合并相同库存维度的明细记录(简化版)
List<OutboundOrderItems> mergedItemsList = mergeSameInventoryItems(outboundOrderItemsList);
// 为明细设置订单ID和主键ID // 为明细设置订单ID和主键ID
for (OutboundOrderItems items : outboundOrderItemsList) { for (OutboundOrderItems items : mergedItemsList) {
items.setOutboundOrderId(id); items.setOutboundOrderId(id);
items.setOrderId(outboundOrders.getOrderId()); items.setOrderId(outboundOrders.getOrderId());
items.setId(UUID.randomUUID().toString().replace("-", "")); items.setId(UUID.randomUUID().toString().replace("-", ""));
} }
// 批量插入出库单明细 // 批量插入出库单明细
outboundOrdersMapper.batchOutboundOrderItems(outboundOrderItemsList); outboundOrdersMapper.batchOutboundOrderItems(mergedItemsList);
// 拷贝明细到日志列表 // 拷贝明细到日志列表
List<String> inventoryIds = new ArrayList<>(); List<String> inventoryIds = new ArrayList<>();
List<OutboundOrderLog> outboundOrderLogs = new ArrayList<>(); List<OutboundOrderLog> outboundOrderLogs = new ArrayList<>();
for (OutboundOrderItems items : outboundOrderItemsList) { for (OutboundOrderItems items : mergedItemsList) {
OutboundOrderLog log = new OutboundOrderLog(); OutboundOrderLog log = new OutboundOrderLog();
BeanUtils.copyProperties(items, log); BeanUtils.copyProperties(items, log);
log.setOrderId(items.getOutboundOrderId()); log.setOrderId(items.getOutboundOrderId());
...@@ -496,6 +495,40 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -496,6 +495,40 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
} }
} }
/**
* 简化版:合并相同库存维度的明细记录
* 按「物料ID+库存类型+库位ID」维度合并,仅累加实际数量
*/
private List<OutboundOrderItems> mergeSameInventoryItems(List<OutboundOrderItems> itemsList) {
if (CollectionUtils.isEmpty(itemsList)) {
return Collections.emptyList();
}
// 按「物料ID+库存类型+库位ID」分组合并
Map<String, OutboundOrderItems> mergeMap = new LinkedHashMap<>();
for (OutboundOrderItems item : itemsList) {
String mergeKey = buildInventoryKey(
item.getMaterialId(),
item.getLocationId(),
Optional.ofNullable(item.getInventoryType()).map(String::valueOf).orElse("")
);
if (mergeMap.containsKey(mergeKey)) {
// 只合并实际数量
OutboundOrderItems existItem = mergeMap.get(mergeKey);
Long newActualQty = Optional.ofNullable(existItem.getActualQuantity()).orElse(0L)
+ Optional.ofNullable(item.getActualQuantity()).orElse(0L);
existItem.setActualQuantity(newActualQty);
} else {
// 新记录直接放入
mergeMap.put(mergeKey, item);
}
}
return new ArrayList<>(mergeMap.values());
}
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@Override @Override
public String importOutboundOrders(List<OutboundTemplateVO> inboundOrdersList, Boolean isUpdateSupport, String operName, Integer orderType) { public String importOutboundOrders(List<OutboundTemplateVO> inboundOrdersList, Boolean isUpdateSupport, String operName, Integer orderType) {
...@@ -519,7 +552,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -519,7 +552,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
Map<String, List<OutboundOrderItems>> validItemMap = new HashMap<>(); Map<String, List<OutboundOrderItems>> validItemMap = new HashMap<>();
boolean hasValidateError = false; boolean hasValidateError = false;
// 3. 预加载映射缓存(移除仓库相关映射) // 3. 预加载映射缓存
Map<String, String> sapToMaterialIdMap = loadSapToMaterialIdMap(); Map<String, String> sapToMaterialIdMap = loadSapToMaterialIdMap();
Map<String, String> locationNameToIdMap = loadLocationNameToIdMap(); Map<String, String> locationNameToIdMap = loadLocationNameToIdMap();
Map<String, String> ownerNameToIdMap = loadOwnerNameToIdMap(); Map<String, String> ownerNameToIdMap = loadOwnerNameToIdMap();
...@@ -530,7 +563,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -530,7 +563,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
.filter(vo -> StringUtils.isNotBlank(vo.getOrderId())) .filter(vo -> StringUtils.isNotBlank(vo.getOrderId()))
.collect(Collectors.groupingBy(OutboundTemplateVO::getOrderId)); .collect(Collectors.groupingBy(OutboundTemplateVO::getOrderId));
// 5. 数据验证(仅保留新增逻辑,检测到已存在则抛异常 // 5. 数据验证(仅新增逻辑
for (Map.Entry<String, List<OutboundTemplateVO>> entry : orderGroupMap.entrySet()) { for (Map.Entry<String, List<OutboundTemplateVO>> entry : orderGroupMap.entrySet()) {
String orderId = entry.getKey(); String orderId = entry.getKey();
List<OutboundTemplateVO> voList = entry.getValue(); List<OutboundTemplateVO> voList = entry.getValue();
...@@ -539,7 +572,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -539,7 +572,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
try { try {
OutboundTemplateVO firstVO = voList.get(0); OutboundTemplateVO firstVO = voList.get(0);
// 检查出库单是否已存在(仅新增,存在则抛异常) // 检查出库单是否已存在
OutboundOrders outboundOrdersQuery = new OutboundOrders(); OutboundOrders outboundOrdersQuery = new OutboundOrders();
outboundOrdersQuery.setOrderId(orderId); outboundOrdersQuery.setOrderId(orderId);
List<OutboundOrders> existMains = outboundOrdersMapper.selectOutboundOrdersList(outboundOrdersQuery); List<OutboundOrders> existMains = outboundOrdersMapper.selectOutboundOrdersList(outboundOrdersQuery);
...@@ -548,7 +581,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -548,7 +581,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
throw new ServiceException(String.format("入库单号【%s】已存在,当前系统仅支持新增,不支持更新", orderId)); throw new ServiceException(String.format("入库单号【%s】已存在,当前系统仅支持新增,不支持更新", orderId));
} }
// 仅新增逻辑:构建新出库单主数据 // 构建新出库单主数据
mainDO = new OutboundOrders(); mainDO = new OutboundOrders();
BeanUtils.copyProperties(firstVO, mainDO, BeanUtils.copyProperties(firstVO, mainDO,
"sapNo", "materialName", "plannedQuantity", "actualQuantity", "sapNo", "materialName", "plannedQuantity", "actualQuantity",
...@@ -591,6 +624,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -591,6 +624,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
mainDO.setUpdateTime(now); mainDO.setUpdateTime(now);
mainDO.setUpdateUserCode(operId); mainDO.setUpdateUserCode(operId);
mainDO.setSortNo(Optional.ofNullable(mainDO.getSortNo()).orElse(0L)); mainDO.setSortNo(Optional.ofNullable(mainDO.getSortNo()).orElse(0L));
mainDO.setIsImport(0L);
// 明细校验 // 明细校验
for (int i = 0; i < voList.size(); i++) { for (int i = 0; i < voList.size(); i++) {
...@@ -645,22 +679,45 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -645,22 +679,45 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
// 库存校验(包含inventoryType)- 有库位才校验,无库位直接跳过 // 库存校验(包含inventoryType)- 有库位才校验,无库位直接跳过
if (StringUtils.isNotBlank(locationName)) { if (StringUtils.isNotBlank(locationName)) {
String inventoryTypeStr = Optional.ofNullable(orderType).map(String::valueOf).orElse(""); String inventoryTypeStr = Optional.ofNullable(orderType).map(String::valueOf).orElse("");
String inventoryMatchKey = String.join("_", materialId, locationId, inventoryTypeStr); String inventoryMatchKey = buildInventoryKey(materialId, locationId, inventoryTypeStr);
AbstractMap.SimpleEntry<String, Long> inventoryEntry = inventoryTOIdMap.get(inventoryMatchKey); AbstractMap.SimpleEntry<String, Long> inventoryEntry = inventoryTOIdMap.get(inventoryMatchKey);
String inventoryId = "";
if (inventoryEntry == null) { if (inventoryEntry == null) {
throw new ServiceException(String.format( Inventory inventory = new Inventory();
"入库单号【%s】第%d条明细:物料【%s】+库位【%s】+库存类型【%s】组合的库存记录不存在", BeanUtils.copyProperties(itemDO, inventory);
orderId, lineNo, vo.getMaterialName(), vo.getLocationName(), inventoryTypeStr)); inventoryId = UUID.randomUUID().toString();
inventory.setInventoryType(Long.valueOf(orderType));
inventory.setBatchId(itemDO.getBatchCode());
inventory.setWarehousesId("local");
inventory.setQuantity(0L);
inventory.setInventoryStatus(1L);
inventory.setIsUsed(1L);
inventory.setId(inventoryId);
int insertCount = inventoryMapper.insertInventory(inventory);
if (insertCount != 1) {
throw new ServiceException(String.format("入库单号【%s】第%d条明细新增库存失败,插入行数为0", orderId, lineNo));
}
// 插入后直接添加到全局缓存
InventoryCache.addInventory(inventoryMatchKey, inventory);
sqlSessionTemplate.clearCache();
sqlSessionTemplate.flushStatements();
} else {
System.out.println("库存已存在,使用已有库存ID:" + inventoryEntry.getKey());
inventoryId = inventoryEntry.getKey();
} }
itemDO.setInventoryId(inventoryEntry.getKey()); itemDO.setInventoryId(inventoryId);
} }
// 无库位时不校验库存,也不设置inventoryId // 无库位时不校验库存,也不设置inventoryId
itemDOList.add(itemDO); itemDOList.add(itemDO);
} }
// 合并相同库存维度的明细
List<OutboundOrderItems> mergedItemList = mergeSameInventoryItems(itemDOList);
validMainMap.put(orderId, mainDO); validMainMap.put(orderId, mainDO);
validItemMap.put(orderId, itemDOList); validItemMap.put(orderId, mergedItemList);
} catch (Exception e) { } catch (Exception e) {
hasValidateError = true; hasValidateError = true;
...@@ -675,27 +732,26 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -675,27 +732,26 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
throw new ServiceException(String.format("验证失败,导入终止!失败详情:%s", failureMsg.toString())); throw new ServiceException(String.format("验证失败,导入终止!失败详情:%s", failureMsg.toString()));
} }
// 7. 执行新增操作(仅新增,无更新) // 7. 执行新增操作
Map<String, List<OutboundOrderItems>> allItemListMap = new HashMap<>(); Map<String, List<OutboundOrderItems>> allItemListMap = new HashMap<>();
for (Map.Entry<String, OutboundOrders> entry : validMainMap.entrySet()) { for (Map.Entry<String, OutboundOrders> entry : validMainMap.entrySet()) {
String orderId = entry.getKey(); String orderId = entry.getKey();
OutboundOrders mainDO = entry.getValue(); OutboundOrders mainDO = entry.getValue();
List<OutboundOrderItems> itemDOList = validItemMap.get(orderId); List<OutboundOrderItems> itemDOList = validItemMap.get(orderId);
// 新增主单 // 新增主单
outboundOrdersMapper.insertOutboundOrders(mainDO); outboundOrdersMapper.insertOutboundOrders(mainDO);
totalMainSuccess++; totalMainSuccess++;
successMsg.append(String.format("入库单号【%s】已新增;\n", orderId)); successMsg.append(String.format("入库单号【%s】已新增;\n", orderId));
// 插入明细(仅新增,无删除操作) // 插入明细
if (!CollectionUtils.isEmpty(itemDOList)) { if (!CollectionUtils.isEmpty(itemDOList)) {
// 批量插入新明细
int itemSuccess = outboundOrderItemsMapper.batchInsertOutboundOrderItems(itemDOList); int itemSuccess = outboundOrderItemsMapper.batchInsertOutboundOrderItems(itemDOList);
totalItemSuccess += itemSuccess; totalItemSuccess += itemSuccess;
int itemFail = itemDOList.size() - itemSuccess; int itemFail = itemDOList.size() - itemSuccess;
totalItemFailure += itemFail; totalItemFailure += itemFail;
successMsg.append(String.format("入库单号【%s】成功导入%d条物料明细;\n", orderId, itemSuccess)); successMsg.append(String.format("入库单号【%s】成功导入%d条物料明细(已合并相同库存维度);\n", orderId, itemSuccess));
if (itemFail > 0) { if (itemFail > 0) {
failureMsg.append(String.format("入库单号【%s】有%d条物料明细导入失败;\n", orderId, itemFail)); failureMsg.append(String.format("入库单号【%s】有%d条物料明细导入失败;\n", orderId, itemFail));
} }
...@@ -704,28 +760,36 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -704,28 +760,36 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
} }
} }
// 8. 执行库存扣减并处理无库位明细拆分 // 8. 异步执行库存扣减和无库位明细拆分
CompletableFuture.runAsync(() -> {
try {
for (Map.Entry<String, List<OutboundOrderItems>> entry : allItemListMap.entrySet()) { for (Map.Entry<String, List<OutboundOrderItems>> entry : allItemListMap.entrySet()) {
List<OutboundOrderItems> itemList = entry.getValue(); List<OutboundOrderItems> itemList = entry.getValue();
// 执行库存扣减,获取无库位明细的扣减记录 // 执行库存扣减
Map<String, List<Map<String, Object>>> deductRecordMap = deductInventory(itemList, operId, now); Map<String, List<Map<String, Object>>> deductRecordMap = deductInventory(itemList, operId, now);
// 处理无库位明细拆分:整理有效明细后统一插入 // 处理无库位明细拆分
if (!deductRecordMap.isEmpty()) { boolean hasNoLocationItem = itemList.stream()
.anyMatch(item -> StringUtils.isBlank(item.getLocationId()));
if (hasNoLocationItem && !deductRecordMap.isEmpty()) {
handleNoLocationItemSplit(itemList, deductRecordMap, operId, now); handleNoLocationItemSplit(itemList, deductRecordMap, operId, now);
} }
} }
} catch (Exception e) {
e.printStackTrace();
}
});
// 9. 结果汇总 // 9. 结果汇总
if (totalMainFailure > 0 || totalItemFailure > 0) { if (totalMainFailure > 0 || totalItemFailure > 0) {
String finalFailureMsg = String.format( String finalFailureMsg = String.format(
"导入结果:成功新增%d个入库单,失败%d个;成功导入%d条明细,失败%d条。失败详情:%s", "导入结果:成功新增%d个入库单,失败%d个;成功导入%d条明细(已合并相同库存维度),失败%d条。失败详情:%s",
totalMainSuccess, totalMainFailure, totalItemSuccess, totalItemFailure, failureMsg.toString() totalMainSuccess, totalMainFailure, totalItemSuccess, totalItemFailure, failureMsg.toString()
); );
throw new ServiceException(finalFailureMsg); throw new ServiceException(finalFailureMsg);
} else { } else {
String finalSuccessMsg = String.format( String finalSuccessMsg = String.format(
"恭喜您,数据已全部导入成功!共新增%d个入库单,成功导入%d条物料明细。详情:%s", "恭喜您,数据已全部导入成功!共新增%d个入库单,成功导入%d条物料明细(已合并相同库存维度)。详情:%s",
totalMainSuccess, totalItemSuccess, successMsg.toString() totalMainSuccess, totalItemSuccess, successMsg.toString()
); );
return finalSuccessMsg; return finalSuccessMsg;
...@@ -733,10 +797,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -733,10 +797,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
} }
/** /**
* 处理无库位明细拆分: * 处理无库位明细拆分(适配新的合并维度)
* 1. 过滤无效扣减(≤0),按库位合并有效扣减数量
* 2. 仅新增场景:删除临时明细,插入整理后的有效明细
* 3. 严格保留原逻辑,仅适配新增场景
*/ */
private void handleNoLocationItemSplit(List<OutboundOrderItems> itemList, private void handleNoLocationItemSplit(List<OutboundOrderItems> itemList,
Map<String, List<Map<String, Object>>> deductRecordMap, Map<String, List<Map<String, Object>>> deductRecordMap,
...@@ -744,39 +805,27 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -744,39 +805,27 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
List<OutboundOrderItems> newValidItemList = new ArrayList<>(); List<OutboundOrderItems> newValidItemList = new ArrayList<>();
Set<String> orderIdSet = new HashSet<>(); Set<String> orderIdSet = new HashSet<>();
// 第一步:遍历扣减记录,整理有效明细(过滤≤0,合并同库位)
for (OutboundOrderItems item : itemList) { for (OutboundOrderItems item : itemList) {
String itemId = item.getId(); String itemId = item.getId();
List<Map<String, Object>> deductRecords = deductRecordMap.get(itemId); List<Map<String, Object>> deductRecords = deductRecordMap.get(itemId);
if (CollectionUtils.isEmpty(deductRecords)) continue; if (CollectionUtils.isEmpty(deductRecords)) continue;
// 收集订单ID(用于删除临时明细)
orderIdSet.add(item.getOutboundOrderId()); orderIdSet.add(item.getOutboundOrderId());
// 按库位合并扣减数量,过滤≤0的无效记录 // 直接遍历合并后的记录(已按物料+库存类型+库位ID合并)
Map<String, Long> locationQtyMap = new HashMap<>();
for (Map<String, Object> rec : deductRecords) { for (Map<String, Object> rec : deductRecords) {
String inventoryId = (String) rec.get("inventoryId");
String locId = (String) rec.get("locationId"); String locId = (String) rec.get("locationId");
Long deductQty = (Long) rec.get("deductQty"); Long validQty = (Long) rec.get("deductQty");
if (deductQty <= 0) continue; // 过滤无效扣减
locationQtyMap.put(locId, locationQtyMap.getOrDefault(locId, 0L) + deductQty);
}
// 生成有效明细 if (validQty <= 0 || StringUtils.isBlank(inventoryId)) continue;
for (Map.Entry<String, Long> entry : locationQtyMap.entrySet()) {
String locId = entry.getKey();
Long validQty = entry.getValue();
OutboundOrderItems newItem = new OutboundOrderItems(); OutboundOrderItems newItem = new OutboundOrderItems();
BeanUtils.copyProperties(item, newItem); BeanUtils.copyProperties(item, newItem);
newItem.setId(UUID.randomUUID().toString().replace("-", "")); newItem.setId(UUID.randomUUID().toString().replace("-", ""));
newItem.setLocationId(locId); newItem.setLocationId(locId);
newItem.setActualQuantity(validQty); newItem.setActualQuantity(validQty);
newItem.setInventoryId(deductRecords.stream() newItem.setInventoryId(inventoryId);
.filter(r -> locId.equals(r.get("locationId")))
.findFirst()
.map(r -> (String) r.get("inventoryId"))
.orElse(""));
newItem.setCreateBy(operId); newItem.setCreateBy(operId);
newItem.setCreateTime(now); newItem.setCreateTime(now);
newItem.setUpdateBy(operId); newItem.setUpdateBy(operId);
...@@ -785,18 +834,19 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -785,18 +834,19 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
} }
} }
// 第二步:删除临时明细(导入时插入的原始明细) // 删除临时明细
for (String orderId : orderIdSet) { for (String orderId : orderIdSet) {
outboundOrderItemsMapper.deleteOutboundOrderItemsByOrderId(orderId); outboundOrderItemsMapper.deleteOutboundOrderItemsByOrderId(orderId);
} }
// 第三步:批量插入整理后的有效明细 // 批量插入有效明细(插入前再次合并)
if (!newValidItemList.isEmpty()) { List<OutboundOrderItems> mergedNewItems = mergeSameInventoryItems(newValidItemList);
outboundOrderItemsMapper.batchInsertOutboundOrderItems(newValidItemList); if (!mergedNewItems.isEmpty()) {
outboundOrderItemsMapper.batchInsertOutboundOrderItems(mergedNewItems);
// 同步生成日志(保留原日志逻辑) // 生成日志
List<OutboundOrderLog> logList = new ArrayList<>(); List<OutboundOrderLog> logList = new ArrayList<>();
for (OutboundOrderItems newItem : newValidItemList) { for (OutboundOrderItems newItem : mergedNewItems) {
OutboundOrderLog log = new OutboundOrderLog(); OutboundOrderLog log = new OutboundOrderLog();
BeanUtils.copyProperties(newItem, log); BeanUtils.copyProperties(newItem, log);
log.setOrderId(newItem.getOutboundOrderId()); log.setOrderId(newItem.getOutboundOrderId());
...@@ -807,7 +857,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -807,7 +857,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
} }
} }
// ========== 预加载映射辅助方法(移除仓库相关) ========== // ========== 预加载映射辅助方法 ==========
private Map<String, String> loadSapToMaterialIdMap() { private Map<String, String> loadSapToMaterialIdMap() {
List<Materials> materialsList = materialsService.selectMaterialsList(new Materials()); List<Materials> materialsList = materialsService.selectMaterialsList(new Materials());
if (CollectionUtils.isEmpty(materialsList)) { if (CollectionUtils.isEmpty(materialsList)) {
...@@ -853,8 +903,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -853,8 +903,7 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
} }
/** /**
* 加载库存映射Map(移除仓库维度) * 加载库存映射Map(按新维度)+ 同步填充全局缓存
* Key=物料ID_库位ID_库存类型
*/ */
private Map<String, AbstractMap.SimpleEntry<String, Long>> loadInventoryTOIdMap() { private Map<String, AbstractMap.SimpleEntry<String, Long>> loadInventoryTOIdMap() {
Inventory inventory = new Inventory(); Inventory inventory = new Inventory();
...@@ -862,40 +911,35 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService ...@@ -862,40 +911,35 @@ public class OutboundOrdersServiceImpl implements IOutboundOrdersService
inventory.setIsUsed(1L); inventory.setIsUsed(1L);
List<Inventory> inventoryList = inventoryService.selectInventoryList(inventory); List<Inventory> inventoryList = inventoryService.selectInventoryList(inventory);
// 清空全局缓存
InventoryCache.clear();
if (CollectionUtils.isEmpty(inventoryList)) { if (CollectionUtils.isEmpty(inventoryList)) {
return Collections.emptyMap(); return Collections.emptyMap();
} }
Map<String, AbstractMap.SimpleEntry<String, Long>> emptyLocationMap = inventoryList.stream() return inventoryList.stream()
.filter(inv -> StringUtils.isNotBlank(inv.getMaterialId()) .filter(inv -> StringUtils.isNotBlank(inv.getMaterialId())
&& inv.getInventoryType() != null && inv.getInventoryType() != null
&& StringUtils.isNotBlank(inv.getId()) && StringUtils.isNotBlank(inv.getId()))
&& StringUtils.isBlank(inv.getLocationId())) .peek(inv -> {
.collect(Collectors.toMap( // 同步到全局缓存(按新维度)
inv -> String.join("_", String key = buildInventoryKey(
inv.getMaterialId().trim(), inv.getMaterialId(),
"", inv.getInventoryType().toString()), inv.getLocationId(),
inv -> new AbstractMap.SimpleEntry<>(inv.getId().trim(), Optional.ofNullable(inv.getQuantity()).orElse(0L)), inv.getInventoryType().toString()
(k1, k2) -> k1, );
HashMap::new InventoryCache.addInventory(key, inv);
)); })
Map<String, AbstractMap.SimpleEntry<String, Long>> nonEmptyLocationMap = inventoryList.stream()
.filter(inv -> StringUtils.isNotBlank(inv.getMaterialId())
&& StringUtils.isNotBlank(inv.getLocationId())
&& StringUtils.isNotBlank(inv.getId())
&& inv.getInventoryType() != null)
.collect(Collectors.toMap( .collect(Collectors.toMap(
inv -> String.join("_", inv -> buildInventoryKey(
inv.getMaterialId().trim(), inv.getMaterialId(),
inv.getLocationId().trim(), inv.getLocationId(),
inv.getInventoryType().toString()), inv.getInventoryType().toString()
),
inv -> new AbstractMap.SimpleEntry<>(inv.getId().trim(), Optional.ofNullable(inv.getQuantity()).orElse(0L)), inv -> new AbstractMap.SimpleEntry<>(inv.getId().trim(), Optional.ofNullable(inv.getQuantity()).orElse(0L)),
(k1, k2) -> k1, (k1, k2) -> k1,
HashMap::new HashMap::new
)); ));
emptyLocationMap.putAll(nonEmptyLocationMap);
return emptyLocationMap;
} }
} }
\ No newline at end of file
package com.ruoyi.inventory.utils;
import com.ruoyi.inventory.domain.Inventory;
import java.util.AbstractMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 库存映射全局缓存(解决导入新增库存即时可见问题)
*/
public class InventoryCache {
// 并发安全Map,Key=物料ID_库位ID_库存类型,Value=库存对象
private static final Map<String, Inventory> INVENTORY_MAP = new ConcurrentHashMap<>();
// 添加库存(直接存对象,避免参数不匹配)
public static void addInventory(String key, Inventory inventory) {
INVENTORY_MAP.put(key, inventory);
}
// 获取库存
public static Inventory getInventory(String key) {
return INVENTORY_MAP.get(key);
}
// 清空缓存
public static void clear() {
INVENTORY_MAP.clear();
}
// 获取全部缓存(核心:供loadInventoryGroupMap直接读取)
public static Map<String, Inventory> getAll() {
return INVENTORY_MAP;
}
}
\ No newline at end of file
...@@ -435,7 +435,7 @@ ...@@ -435,7 +435,7 @@
and inventory_status = '1' and inventory_status = '1'
]]> ]]>
</select> </select>
<insert id="insertInventory" parameterType="Inventory"> <insert id="insertInventory" parameterType="Inventory" flushCache="true">
insert into inventory insert into inventory
<trim prefix="(" suffix=")" suffixOverrides=","> <trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if> <if test="id != null">id,</if>
...@@ -563,6 +563,7 @@ and inventory_status = '1' ...@@ -563,6 +563,7 @@ and inventory_status = '1'
left join inventory i on i.material_id = m.id left join inventory i on i.material_id = m.id
and i.is_used = 1 and i.is_used = 1
and i.unit_price > 0 and i.unit_price > 0
and i.inventory_status=1
and i.last_inbound_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01') and i.last_inbound_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01')
and i.last_inbound_time &lt; DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01') and i.last_inbound_time &lt; DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01')
where where
...@@ -580,6 +581,7 @@ and inventory_status = '1' ...@@ -580,6 +581,7 @@ and inventory_status = '1'
from materials m from materials m
left join inventory i on i.material_id = m.id left join inventory i on i.material_id = m.id
and i.is_used = 1 and i.is_used = 1
and i.inventory_status=1
and i.last_inbound_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01') and i.last_inbound_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01')
and i.last_inbound_time &lt; DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01') and i.last_inbound_time &lt; DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01')
where where
......
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
<result property="ownerId" column="owner_id" /> <result property="ownerId" column="owner_id" />
<result property="ownerName" column="owner_name" /> <result property="ownerName" column="owner_name" />
<result property="orderStatus" column="order_status" /> <result property="orderStatus" column="order_status" />
<!-- 保留原字段映射,新增startDate/endDate用于查询 -->
<result property="inboundDate" column="inbound_date" /> <result property="inboundDate" column="inbound_date" />
<result property="destination" column="destination" /> <result property="destination" column="destination" />
<result property="totalPlannedQuantity" column="total_planned_quantity" /> <result property="totalPlannedQuantity" column="total_planned_quantity" />
...@@ -29,6 +28,8 @@ ...@@ -29,6 +28,8 @@
<result property="createUserCode" column="create_user_code" /> <result property="createUserCode" column="create_user_code" />
<result property="updateTime" column="update_time" /> <result property="updateTime" column="update_time" />
<result property="updateUserCode" column="update_user_code" /> <result property="updateUserCode" column="update_user_code" />
<!-- 新增 isImport 字段映射 -->
<result property="isImport" column="is_import" />
</resultMap> </resultMap>
<resultMap id="OutboundOrdersOutboundOrderItemsResult" type="com.ruoyi.inventory.domain.OutboundOrders" extends="OutboundOrdersResult"> <resultMap id="OutboundOrdersOutboundOrderItemsResult" type="com.ruoyi.inventory.domain.OutboundOrders" extends="OutboundOrdersResult">
...@@ -94,7 +95,8 @@ ...@@ -94,7 +95,8 @@
oo.create_time, oo.create_time,
oo.create_user_code, oo.create_user_code,
oo.update_time, oo.update_time,
oo.update_user_code oo.update_user_code,
oo.is_import
from outbound_orders oo from outbound_orders oo
left join owners o on oo.owner_id = o.id and o.is_used = 1 left join owners o on oo.owner_id = o.id and o.is_used = 1
left join warehouses w on oo.warehouse_id = w.id and w.is_used = 1 and w.is_enabled = 1 left join warehouses w on oo.warehouse_id = w.id and w.is_used = 1 and w.is_enabled = 1
...@@ -111,7 +113,8 @@ ...@@ -111,7 +113,8 @@
<if test="warehouseId != null and warehouseId != ''"> and oo.warehouse_id = #{warehouseId}</if> <if test="warehouseId != null and warehouseId != ''"> and oo.warehouse_id = #{warehouseId}</if>
<if test="ownerId != null and ownerId != ''"> and oo.owner_id = #{ownerId}</if> <if test="ownerId != null and ownerId != ''"> and oo.owner_id = #{ownerId}</if>
<if test="orderStatus != null "> and oo.order_status = #{orderStatus}</if> <if test="orderStatus != null "> and oo.order_status = #{orderStatus}</if>
<!-- 替换为时间段查询:startDate和endDate --> <!-- 新增 isImport 查询条件 -->
<if test="isImport != null "> and oo.is_import = #{isImport}</if>
<if test="startDate != null"> and oo.inbound_date &gt;= #{startDate}</if> <if test="startDate != null"> and oo.inbound_date &gt;= #{startDate}</if>
<if test="endDate != null"> and oo.inbound_date &lt;= #{endDate}</if> <if test="endDate != null"> and oo.inbound_date &lt;= #{endDate}</if>
<if test="destination != null and destination != ''"> and oo.destination = #{destination}</if> <if test="destination != null and destination != ''"> and oo.destination = #{destination}</if>
...@@ -147,7 +150,8 @@ ...@@ -147,7 +150,8 @@
oo.create_time, oo.create_time,
oo.create_user_code, oo.create_user_code,
oo.update_time, oo.update_time,
oo.update_user_code oo.update_user_code,
oo.is_import
from outbound_orders oo from outbound_orders oo
left join owners o on oo.owner_id = o.id left join owners o on oo.owner_id = o.id
left join warehouses w on oo.warehouse_id = w.id left join warehouses w on oo.warehouse_id = w.id
...@@ -202,7 +206,6 @@ ...@@ -202,7 +206,6 @@
<if test="warehouseId != null">warehouse_id,</if> <if test="warehouseId != null">warehouse_id,</if>
<if test="ownerId != null">owner_id,</if> <if test="ownerId != null">owner_id,</if>
<if test="orderStatus != null">order_status,</if> <if test="orderStatus != null">order_status,</if>
<!-- 保留inboundDate字段的插入(业务字段仍需存储) -->
<if test="inboundDate != null">inbound_date,</if> <if test="inboundDate != null">inbound_date,</if>
<if test="destination != null">destination,</if> <if test="destination != null">destination,</if>
<if test="totalPlannedQuantity != null">total_planned_quantity,</if> <if test="totalPlannedQuantity != null">total_planned_quantity,</if>
...@@ -215,6 +218,8 @@ ...@@ -215,6 +218,8 @@
<if test="createUserCode != null">create_user_code,</if> <if test="createUserCode != null">create_user_code,</if>
<if test="updateTime != null">update_time,</if> <if test="updateTime != null">update_time,</if>
<if test="updateUserCode != null">update_user_code,</if> <if test="updateUserCode != null">update_user_code,</if>
<!-- 新增 is_import 字段插入 -->
<if test="isImport != null">is_import,</if>
</trim> </trim>
<trim prefix="values (" suffix=")" suffixOverrides=","> <trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if> <if test="id != null">#{id},</if>
...@@ -238,6 +243,8 @@ ...@@ -238,6 +243,8 @@
<if test="createUserCode != null">#{createUserCode},</if> <if test="createUserCode != null">#{createUserCode},</if>
<if test="updateTime != null">#{updateTime},</if> <if test="updateTime != null">#{updateTime},</if>
<if test="updateUserCode != null">#{updateUserCode},</if> <if test="updateUserCode != null">#{updateUserCode},</if>
<!-- 新增 is_import 字段值 -->
<if test="isImport != null">#{isImport},</if>
</trim> </trim>
</insert> </insert>
...@@ -264,6 +271,8 @@ ...@@ -264,6 +271,8 @@
<if test="createUserCode != null">create_user_code = #{createUserCode},</if> <if test="createUserCode != null">create_user_code = #{createUserCode},</if>
<if test="updateTime != null">update_time = #{updateTime},</if> <if test="updateTime != null">update_time = #{updateTime},</if>
<if test="updateUserCode != null">update_user_code = #{updateUserCode},</if> <if test="updateUserCode != null">update_user_code = #{updateUserCode},</if>
<!-- 新增 is_import 字段更新 -->
<if test="isImport != null">is_import = #{isImport},</if>
</trim> </trim>
where id = #{id} where id = #{id}
</update> </update>
...@@ -316,6 +325,7 @@ ...@@ -316,6 +325,7 @@
left join outbound_order_items ooi left join outbound_order_items ooi
on ooi.material_id = m.id on ooi.material_id = m.id
and ooi.is_used = 1 and ooi.is_used = 1
and ooi.item_status=3
and ooi.shipped_at >= DATE_FORMAT(CURDATE(), '%Y-%m-01 00:00:00') and ooi.shipped_at >= DATE_FORMAT(CURDATE(), '%Y-%m-01 00:00:00')
and ooi.shipped_at &lt; DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01 00:00:00') and ooi.shipped_at &lt; DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01 00:00:00')
where where
...@@ -334,6 +344,7 @@ ...@@ -334,6 +344,7 @@
left join outbound_order_items ooi left join outbound_order_items ooi
on ooi.material_id = m.id on ooi.material_id = m.id
and ooi.is_used = 1 and ooi.is_used = 1
and ooi.item_status=3
and ooi.shipped_at >= DATE_FORMAT(CURDATE(), '%Y-%m-01 00:00:00') and ooi.shipped_at >= DATE_FORMAT(CURDATE(), '%Y-%m-01 00:00:00')
and ooi.shipped_at &lt; DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01 00:00:00') and ooi.shipped_at &lt; DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01 00:00:00')
where where
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论