package com.ruoyi.stock.service.impl;
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.ruoyi.account.mapper.sales.AccountInvoiceApplicationMapper;
|
import com.ruoyi.account.mapper.sales.AccountSalesCollectionMapper;
|
import com.ruoyi.basic.pojo.Vehicle;
|
import com.ruoyi.basic.service.IVehicleService;
|
import com.ruoyi.common.enums.ReviewStatusEnum;
|
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
|
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
|
import com.ruoyi.common.exception.base.BaseException;
|
import com.ruoyi.common.utils.EnumUtil;
|
import com.ruoyi.common.utils.OrderUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.bean.BeanUtils;
|
import com.ruoyi.common.utils.poi.ExcelUtil;
|
import com.ruoyi.outsourcing.enums.OutsourcingOrderStatusEnum;
|
import com.ruoyi.outsourcing.mapper.OutsourcingOrderMapper;
|
import com.ruoyi.outsourcing.mapper.OutsourcingOrderProductMapper;
|
import com.ruoyi.outsourcing.pojo.OutsourcingOrder;
|
import com.ruoyi.outsourcing.pojo.OutsourcingOrderProduct;
|
import com.ruoyi.sales.dto.SalesLedgerDto;
|
import com.ruoyi.sales.mapper.SalesLedgerMapper;
|
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
|
import com.ruoyi.sales.pojo.SalesLedger;
|
import com.ruoyi.sales.pojo.SalesLedgerProduct;
|
|
import java.time.LocalDateTime;
|
import com.ruoyi.stock.dto.OilOutAvailableDto;
|
import com.ruoyi.stock.dto.StockInventoryDto;
|
import com.ruoyi.stock.dto.StockOutRecordDto;
|
import com.ruoyi.stock.dto.StockUninventoryDto;
|
import com.ruoyi.stock.execl.StockOutRecordExportData;
|
import com.ruoyi.stock.mapper.StockInventoryMapper;
|
import com.ruoyi.stock.mapper.StockOutRecordMapper;
|
import com.ruoyi.stock.mapper.StockUninventoryMapper;
|
import com.ruoyi.stock.pojo.StockInventory;
|
import com.ruoyi.stock.pojo.StockOutRecord;
|
import com.ruoyi.stock.pojo.StockUninventory;
|
import com.ruoyi.stock.service.StockOutRecordService;
|
import jakarta.servlet.http.HttpServletResponse;
|
import lombok.RequiredArgsConstructor;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.util.CollectionUtils;
|
|
import java.math.BigDecimal;
|
import java.util.Collections;
|
import java.util.List;
|
import java.util.Objects;
|
|
/**
|
* <p>
|
* 出库记录表 服务实现类
|
* </p>
|
*
|
* @author 芯导软件(江苏)有限公司
|
* @since 2026-01-21 05:27:04
|
*/
|
@Service
|
@RequiredArgsConstructor
|
public class StockOutRecordServiceImpl extends ServiceImpl<StockOutRecordMapper, StockOutRecord> implements StockOutRecordService {
|
/**
|
* 出库类别:销售。销售类油品出库在审批通过时按批次扣减库存
|
*/
|
private static final String OUT_CATEGORY_SALE = "销售";
|
|
/**
|
* 出库类别:代储。客户把自己的油寄存在我们的罐里,提走只是记录一笔,不扣库存、不进发货台账
|
*/
|
private static final String OUT_CATEGORY_DEPOSIT = "代储";
|
|
/**
|
* 销售台账的销售类型:代储(卖存储位置)。这类台账没有产品明细,只有油库+储罐,见 SalesLedger.ledgerType
|
*/
|
private static final String LEDGER_TYPE_DEPOSIT = "代储";
|
|
/**
|
* 销售台账产品明细的行类型:销售。见 sales_ledger_product.type
|
*/
|
private static final Integer LEDGER_PRODUCT_TYPE_SALE = 1;
|
|
private final StockOutRecordMapper stockOutRecordMapper;
|
private final IVehicleService vehicleService;
|
private final StockInventoryMapper stockInventoryMapper;
|
private final StockUninventoryMapper stockUninventoryMapper;
|
private final AccountSalesCollectionMapper accountSalesCollectionMapper;
|
private final AccountInvoiceApplicationMapper accountInvoiceApplicationMapper;
|
private final OutsourcingOrderProductMapper outsourcingOrderProductMapper;
|
private final OutsourcingOrderMapper outsourcingOrderMapper;
|
private final SalesLedgerMapper salesLedgerMapper;
|
private final SalesLedgerProductMapper salesLedgerProductMapper;
|
|
@Override
|
public IPage<StockOutRecordDto> listPage(Page page, StockOutRecordDto stockOutRecordDto) {
|
return stockOutRecordMapper.listPage(page, stockOutRecordDto);
|
}
|
|
@Override
|
public int add(StockOutRecordDto stockOutRecordDto) {
|
resolveSalesLedgerBinding(stockOutRecordDto);
|
if (OUT_CATEGORY_SALE.equals(stockOutRecordDto.getOutCategory())) {
|
// 销售类油品出库审批通过时要按批次扣减库存,批次不能为空
|
if (StringUtils.isEmpty(stockOutRecordDto.getBatchNo())) {
|
throw new BaseException("销售类出库必须选择出库批次");
|
}
|
resolveVehicle(stockOutRecordDto);
|
checkSaleOutAvailable(stockOutRecordDto);
|
}
|
LocalDateTime createTime = stockOutRecordDto.getCreateTime();
|
if (createTime == null) {
|
createTime = LocalDateTime.now();
|
}
|
String no = OrderUtils.countTodayByCreateTime(stockOutRecordMapper, "CK","outbound_batches", createTime);
|
stockOutRecordDto.setOutboundBatches(no);
|
stockOutRecordDto.setCreateTime(createTime);
|
if (StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode().equals(stockOutRecordDto.getRecordType())){
|
stockOutRecordDto.setApprovalStatus(3);
|
}
|
return stockOutRecordMapper.insert(stockOutRecordDto);
|
}
|
|
@Override
|
public int update(Long id, StockOutRecordDto stockOutRecordDto) {
|
// 判断对象是否存在
|
StockOutRecord stockOutRecord = stockOutRecordMapper.selectById(id);
|
if (stockOutRecord == null){
|
throw new BaseException("该出库记录不存在,无法更新!!!");
|
}
|
|
resolveSalesLedgerBinding(stockOutRecordDto);
|
String[] ignoreProperties = {"id", "outbound_batches"};//排除id属性
|
BeanUtils.copyProperties(stockOutRecordDto, stockOutRecord, ignoreProperties);
|
return stockOutRecordMapper.updateById(stockOutRecord);
|
}
|
|
/**
|
* 绑定销售台账:客户一律以台账为准覆盖,避免出库记录与台账的客户对不上;出库类别按台账的销售类型自动定,
|
* 免得出现「绑了卖油台账却按代储不扣库存」这类矛盾数据。
|
* <ul>
|
* <li>卖油(含 ledgerType 为空):出库类别置「销售」,并校验所选规格确实在该台账的销售明细里</li>
|
* <li>代储:出库类别置「代储」,带入台账的油库/储罐,跳过产品校验 ——
|
* 代储台账本身就是卖存储位置,没有产品明细</li>
|
* </ul>
|
* salesLedgerId 为空时整体跳过,不绑台账的出库行为与改动前完全一致
|
*/
|
private void resolveSalesLedgerBinding(StockOutRecordDto stockOutRecordDto) {
|
if (stockOutRecordDto.getSalesLedgerId() == null) {
|
return;
|
}
|
SalesLedger salesLedger = salesLedgerMapper.selectById(stockOutRecordDto.getSalesLedgerId());
|
if (salesLedger == null) {
|
throw new BaseException("所选销售台账不存在,销售台账id:" + stockOutRecordDto.getSalesLedgerId());
|
}
|
stockOutRecordDto.setCustomerId(salesLedger.getCustomerId());
|
stockOutRecordDto.setCustomerName(salesLedger.getCustomerName());
|
if (LEDGER_TYPE_DEPOSIT.equals(salesLedger.getLedgerType())) {
|
stockOutRecordDto.setOutCategory(OUT_CATEGORY_DEPOSIT);
|
stockOutRecordDto.setOilDepotId(salesLedger.getOilDepotId());
|
stockOutRecordDto.setOilDepotName(salesLedger.getOilDepotName());
|
stockOutRecordDto.setTankId(salesLedger.getTankId());
|
stockOutRecordDto.setTankNo(salesLedger.getTankNo());
|
return;
|
}
|
stockOutRecordDto.setOutCategory(OUT_CATEGORY_SALE);
|
if (stockOutRecordDto.getProductModelId() == null) {
|
throw new BaseException("绑定销售台账时必须选择产品规格,销售合同号:" + salesLedger.getSalesContractNo());
|
}
|
boolean belongsToLedger = salesLedgerProductMapper.selectList(
|
Wrappers.<SalesLedgerProduct>lambdaQuery()
|
.eq(SalesLedgerProduct::getSalesLedgerId, stockOutRecordDto.getSalesLedgerId())
|
.eq(SalesLedgerProduct::getType, LEDGER_PRODUCT_TYPE_SALE))
|
.stream()
|
.anyMatch(item -> stockOutRecordDto.getProductModelId().equals(item.getProductModelId()));
|
if (!belongsToLedger) {
|
throw new BaseException("所选产品不在该销售台账明细中,销售合同号:" + salesLedger.getSalesContractNo());
|
}
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public int batchDelete(List<Long> ids) {
|
//todo 如果出库与开票收款有关联则无法删除
|
if (accountSalesCollectionMapper.existsByStockOutRecordId(ids) ||
|
accountInvoiceApplicationMapper.existsByStockOutRecordId(ids)) {
|
throw new BaseException("出库记录存在开票收款关联数据,无法删除!!!");
|
}
|
for (Long id : ids) {
|
StockOutRecord stockOutRecord = stockOutRecordMapper.selectById(id);
|
if (stockOutRecord.getType().equals("0")) {
|
LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<StockInventory>()
|
.eq(StockInventory::getProductModelId, stockOutRecord.getProductModelId());
|
if (StringUtils.isEmpty(stockOutRecord.getBatchNo())) {
|
wrapper.isNull(StockInventory::getBatchNo);
|
} else {
|
wrapper.eq(StockInventory::getBatchNo, stockOutRecord.getBatchNo());
|
}
|
StockInventory stockInventory = stockInventoryMapper.selectOne(wrapper);
|
if (stockInventory == null) {
|
throw new BaseException("库存记录中没有对应的产品,无法删除!!!");
|
}else {
|
StockInventoryDto stockInRecordDto = new StockInventoryDto();
|
stockInRecordDto.setProductModelId(stockInventory.getProductModelId());
|
stockInRecordDto.setQualitity(stockOutRecord.getStockOutNum());
|
stockInRecordDto.setBatchNo(stockInventory.getBatchNo());
|
stockInventoryMapper.updateAddStockInventory(stockInRecordDto);
|
}
|
}
|
else if (stockOutRecord.getType().equals("1")) {
|
LambdaQueryWrapper<StockUninventory> wrapper = new LambdaQueryWrapper<StockUninventory>()
|
.eq(StockUninventory::getProductModelId, stockOutRecord.getProductModelId());
|
if (StringUtils.isEmpty(stockOutRecord.getBatchNo())) {
|
wrapper.isNull(StockUninventory::getBatchNo);
|
} else {
|
wrapper.eq(StockUninventory::getBatchNo, stockOutRecord.getBatchNo());
|
}
|
StockUninventory stockUninventory = stockUninventoryMapper.selectOne(wrapper);
|
if (stockUninventory == null) {
|
throw new BaseException("库存记录中没有对应的产品,无法删除!!!");
|
}else {
|
StockUninventoryDto stockUninventoryDto = new StockUninventoryDto();
|
stockUninventoryDto.setProductModelId(stockUninventory.getProductModelId());
|
stockUninventoryDto.setQualitity(stockOutRecord.getStockOutNum());
|
stockUninventoryDto.setBatchNo(stockUninventory.getBatchNo());
|
stockUninventoryMapper.updateAddStockUnInventory(stockUninventoryDto);
|
}
|
}
|
else if (OUT_CATEGORY_SALE.equals(stockOutRecord.getOutCategory())) {
|
// 销售类油品出库只有审批通过时才扣过库存,删除已通过的记录需按同批次归还
|
returnSaleOilOutStock(stockOutRecord);
|
}
|
}
|
return stockOutRecordMapper.deleteBatchIds(ids);
|
}
|
|
/**
|
* 销售类油品出库归还库存,仅对审批通过(即扣过库存)的记录生效。
|
* 归还量取 deduct_quantity(实际扣减量);改造前审批通过的记录没有该值,按全额归还
|
*/
|
private void returnSaleOilOutStock(StockOutRecord stockOutRecord) {
|
if (!ReviewStatusEnum.APPROVED.getCode().equals(stockOutRecord.getApprovalStatus())
|
|| StringUtils.isEmpty(stockOutRecord.getBatchNo())
|
|| stockOutRecord.getStockOutNum() == null) {
|
return;
|
}
|
BigDecimal returnQuantity = stockOutRecord.getDeductQuantity() != null
|
? stockOutRecord.getDeductQuantity()
|
: stockOutRecord.getStockOutNum();
|
if (returnQuantity.compareTo(BigDecimal.ZERO) <= 0) {
|
return;
|
}
|
StockInventoryDto stockInventoryDto = new StockInventoryDto();
|
stockInventoryDto.setProductModelId(stockOutRecord.getProductModelId());
|
stockInventoryDto.setBatchNo(stockOutRecord.getBatchNo());
|
stockInventoryDto.setQualitity(returnQuantity);
|
stockInventoryMapper.updateAddStockInventory(stockInventoryDto);
|
}
|
|
@Override
|
public void exportStockOutRecord(HttpServletResponse response, StockOutRecordDto stockOutRecordDto) {
|
List<StockOutRecordExportData> list = stockOutRecordMapper.listStockOutRecordExportData(stockOutRecordDto);
|
for (StockOutRecordExportData stockInRecordExportData : list) {
|
if (stockInRecordExportData.getType().equals("0")) {
|
stockInRecordExportData.setRecordType(EnumUtil.fromCode(StockOutQualifiedRecordTypeEnum.class, Integer.parseInt(stockInRecordExportData.getRecordType())).getValue());
|
}else {
|
stockInRecordExportData.setRecordType(EnumUtil.fromCode(StockInQualifiedRecordTypeEnum.class, Integer.parseInt(stockInRecordExportData.getRecordType())).getValue());
|
}
|
}
|
ExcelUtil<StockOutRecordExportData> util = new ExcelUtil<>(StockOutRecordExportData.class);
|
util.exportExcel(response,list, "出库记录信息");
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public int batchDeletePending(List<Long> ids) {
|
for (Long id : ids) {
|
StockOutRecord stockOutRecord = stockOutRecordMapper.selectById(id);
|
if (stockOutRecord == null) {
|
throw new BaseException("出库记录不存在,无法删除!!!");
|
}
|
if (stockOutRecord.getApprovalStatus() != null && !ReviewStatusEnum.PENDING_REVIEW.getCode().equals(stockOutRecord.getApprovalStatus())) {
|
throw new BaseException("只有待审批状态的记录才能删除,出库批次:" + stockOutRecord.getOutboundBatches());
|
}
|
}
|
return stockOutRecordMapper.deleteBatchIds(ids);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public int batchApprove(List<Long> ids, Integer approvalStatus) {
|
if (CollectionUtils.isEmpty(ids)) {
|
throw new BaseException("请选择至少一条数据");
|
}
|
if (approvalStatus == null || (!ReviewStatusEnum.APPROVED.getCode().equals(approvalStatus) && !ReviewStatusEnum.REJECTED.getCode().equals(approvalStatus))) {
|
throw new BaseException("审批状态值无效");
|
}
|
for (Long id : ids) {
|
StockOutRecord stockOutRecord = stockOutRecordMapper.selectById(id);
|
if (stockOutRecord == null) {
|
throw new BaseException("出库记录不存在,无法审批!!!");
|
}
|
if (stockOutRecord.getApprovalStatus() != null && !ReviewStatusEnum.PENDING_REVIEW.getCode().equals(stockOutRecord.getApprovalStatus())) {
|
throw new BaseException("只有待审批状态的记录才能审批,出库批次:" + stockOutRecord.getOutboundBatches());
|
}
|
// 审批通过时先扣减库存,扣减量(deduct_quantity)要随审批状态一起落库,所以放在更新之前
|
if (ReviewStatusEnum.APPROVED.getCode().equals(approvalStatus)) {
|
if ("0".equals(stockOutRecord.getType())) {
|
// 合格出库 -> 先查库存是否存在,存在才扣减
|
StockInventory stockInventory = getStockInventory(stockOutRecord.getProductModelId(), stockOutRecord.getBatchNo());
|
if (stockInventory == null) {
|
throw new BaseException("合格库存记录不存在,出库批次:" + stockOutRecord.getOutboundBatches());
|
}
|
StockInventoryDto stockInventoryDto = new StockInventoryDto();
|
stockInventoryDto.setProductModelId(stockOutRecord.getProductModelId());
|
stockInventoryDto.setBatchNo(stockOutRecord.getBatchNo());
|
stockInventoryDto.setQualitity(stockOutRecord.getStockOutNum());
|
stockInventoryMapper.updateSubtractStockInventory(stockInventoryDto);
|
} else if ("1".equals(stockOutRecord.getType())) {
|
// 不合格出库 -> 先查库存是否存在,存在才扣减
|
StockUninventory stockUninventory = getStockUninventory(stockOutRecord.getProductModelId(), stockOutRecord.getBatchNo());
|
if (stockUninventory == null) {
|
throw new BaseException("不合格库存记录不存在,出库批次:" + stockOutRecord.getOutboundBatches());
|
}
|
StockUninventoryDto stockUninventoryDto = new StockUninventoryDto();
|
stockUninventoryDto.setProductModelId(stockOutRecord.getProductModelId());
|
stockUninventoryDto.setBatchNo(stockOutRecord.getBatchNo());
|
stockUninventoryDto.setQualitity(stockOutRecord.getStockOutNum());
|
stockUninventoryMapper.updateSubtractStockUnInventory(stockUninventoryDto);
|
} else if (OUT_CATEGORY_SALE.equals(stockOutRecord.getOutCategory())) {
|
// 销售类油品出库(type 为空)审批通过时按批次扣减库存
|
deductSaleOilOutStock(stockOutRecord);
|
}
|
}
|
stockOutRecord.setApprovalStatus(approvalStatus);
|
stockOutRecordMapper.updateById(stockOutRecord);
|
if (ReviewStatusEnum.APPROVED.getCode().equals(approvalStatus)
|
&& StockOutQualifiedRecordTypeEnum.OUTSOURCING_SCRAP_OUT.getCode()
|
.equals(stockOutRecord.getRecordType())) {
|
tryCompleteOutsourcingOrder(stockOutRecord);
|
}
|
}
|
return ids.size();
|
}
|
|
/**
|
* 委外报废品出库审批通过后,若该委外订单的报废品选料行已<b>全部</b>出库审批通过,
|
* 自动把订单从「已确认」推到「已完成」,省掉人工再点一次完成。
|
*
|
* <p>直接用 mapper 而不是 {@code IOutsourcingOrderService#complete}:后者依赖本服务,
|
* 两边都走构造器注入会形成循环依赖。驳回/反审不回退订单状态(处理错误审批走人工重新审批)。</p>
|
*/
|
private void tryCompleteOutsourcingOrder(StockOutRecord record) {
|
if (record.getRecordId() == null) {
|
return;
|
}
|
OutsourcingOrderProduct line = outsourcingOrderProductMapper.selectById(record.getRecordId());
|
if (line == null || line.getOutsourcingOrderId() == null) {
|
return;
|
}
|
Long orderId = line.getOutsourcingOrderId();
|
// isScrap 在 Java 侧过滤,与委外模块自己的 scrapLineIds 一致,不依赖 Boolean 字段的列名推断
|
List<Long> lineIds = outsourcingOrderProductMapper.selectList(
|
Wrappers.<OutsourcingOrderProduct>lambdaQuery()
|
.eq(OutsourcingOrderProduct::getOutsourcingOrderId, orderId))
|
.stream()
|
.filter(item -> Boolean.TRUE.equals(item.getIsScrap()))
|
.map(OutsourcingOrderProduct::getId)
|
.toList();
|
if (lineIds.isEmpty()) {
|
return;
|
}
|
// 出库记录一条对应一行明细;缺记录(被反审删除等)就不完成,与人工完成的校验口径一致
|
long approvedCount = listByRecordIds(lineIds, record.getRecordType()).stream()
|
.filter(item -> ReviewStatusEnum.APPROVED.getCode().equals(item.getApprovalStatus()))
|
.count();
|
if (approvedCount < lineIds.size()) {
|
return;
|
}
|
// 条件更新,只有「已确认」能推进;已作废/已完成的订单不动
|
outsourcingOrderMapper.update(null, Wrappers.<OutsourcingOrder>lambdaUpdate()
|
.eq(OutsourcingOrder::getId, orderId)
|
.eq(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.CONFIRMED.getCode())
|
.set(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.COMPLETED.getCode()));
|
}
|
|
/**
|
* 销售类油品出库扣减库存。出库数量是本次装车总量,车上上次剩的油本次继续用,
|
* 所以只从库存补足「出库数量 − 上次剩余油量」的差额。
|
* updateSubtractStockInventory 自带 qualitity >= 数量 的守卫,影响行数为 0 即库存不足,
|
* 抛错由外层事务回滚
|
*/
|
private void deductSaleOilOutStock(StockOutRecord stockOutRecord) {
|
String suffix = ",出库批次:" + stockOutRecord.getOutboundBatches();
|
if (StringUtils.isEmpty(stockOutRecord.getBatchNo())) {
|
throw new BaseException("销售类出库必须选择出库批次" + suffix);
|
}
|
BigDecimal quantity = stockOutRecord.getStockOutNum();
|
if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) {
|
throw new BaseException("销售类出库数量必须大于0" + suffix);
|
}
|
BigDecimal lastResidual = getLastResidualOil(stockOutRecord.getVehicleId(), stockOutRecord.getProductModelId(), stockOutRecord.getId());
|
if (lastResidual.compareTo(quantity) > 0) {
|
throw new BaseException("出库数量不能小于该车上次剩余油量" + toPlain(lastResidual) + suffix);
|
}
|
BigDecimal deduct = quantity.subtract(lastResidual);
|
if (deduct.compareTo(BigDecimal.ZERO) > 0) {
|
StockInventoryDto stockInventoryDto = new StockInventoryDto();
|
stockInventoryDto.setProductModelId(stockOutRecord.getProductModelId());
|
stockInventoryDto.setBatchNo(stockOutRecord.getBatchNo());
|
stockInventoryDto.setQualitity(deduct);
|
if (stockInventoryMapper.updateSubtractStockInventory(stockInventoryDto) == 0) {
|
throw new BaseException("库存不足无法出库" + suffix);
|
}
|
}
|
// 车上剩油刚好够(deduct=0)时不涉及库存更新,同样落库便于删除时按实际扣减量归还
|
stockOutRecord.setDeductQuantity(deduct);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public int batchReAudit(List<Long> ids) {
|
if (CollectionUtils.isEmpty(ids)) {
|
throw new BaseException("请选择至少一条数据");
|
}
|
for (Long id : ids) {
|
StockOutRecord stockOutRecord = stockOutRecordMapper.selectById(id);
|
if (stockOutRecord == null) {
|
throw new BaseException("出库记录不存在,无法重新审核!!!");
|
}
|
if (!ReviewStatusEnum.REJECTED.getCode().equals(stockOutRecord.getApprovalStatus())) {
|
throw new BaseException("只有驳回状态的记录才能重新审核,出库批次:" + stockOutRecord.getOutboundBatches());
|
}
|
stockOutRecord.setApprovalStatus(ReviewStatusEnum.PENDING_REVIEW.getCode());
|
stockOutRecordMapper.updateById(stockOutRecord);
|
}
|
return ids.size();
|
}
|
|
@Override
|
public int updateResidualOil(Long id, BigDecimal residualOil) {
|
StockOutRecord stockOutRecord = stockOutRecordMapper.selectById(id);
|
if (stockOutRecord == null) {
|
throw new BaseException("该出库记录不存在,无法更新!!!");
|
}
|
if (residualOil == null || residualOil.compareTo(BigDecimal.ZERO) < 0) {
|
throw new BaseException("剩余油量不能为空且不能小于0");
|
}
|
if (stockOutRecord.getStockOutNum() != null && residualOil.compareTo(stockOutRecord.getStockOutNum()) > 0) {
|
throw new BaseException("剩余油量不能大于出库数量" + toPlain(stockOutRecord.getStockOutNum()));
|
}
|
StockOutRecord update = new StockOutRecord();
|
update.setId(id);
|
update.setResidualOil(residualOil);
|
return stockOutRecordMapper.updateById(update);
|
}
|
|
private StockInventory getStockInventory(Long productModelId, String batchNo) {
|
LambdaQueryWrapper<StockInventory> eq = new LambdaQueryWrapper<>();
|
eq.eq(StockInventory::getProductModelId, productModelId);
|
if (StringUtils.isEmpty(batchNo)) {
|
eq.isNull(StockInventory::getBatchNo);
|
} else {
|
eq.eq(StockInventory::getBatchNo, batchNo);
|
}
|
return stockInventoryMapper.selectOne(eq);
|
}
|
|
private StockUninventory getStockUninventory(Long productModelId, String batchNo) {
|
LambdaQueryWrapper<StockUninventory> eq = new LambdaQueryWrapper<>();
|
eq.eq(StockUninventory::getProductModelId, productModelId);
|
if (StringUtils.isEmpty(batchNo)) {
|
eq.isNull(StockUninventory::getBatchNo);
|
} else {
|
eq.eq(StockUninventory::getBatchNo, batchNo);
|
}
|
return stockUninventoryMapper.selectOne(eq);
|
}
|
|
/**
|
* 车辆回填:前端可以直接传 vehicleId,也可以只传车牌号,
|
* 后者按车辆主数据反查回填。两者都拿不到就无法定位「上次剩余油量」,直接报错
|
*/
|
private void resolveVehicle(StockOutRecordDto stockOutRecordDto) {
|
if (stockOutRecordDto.getVehicleId() != null) {
|
return;
|
}
|
if (StringUtils.isEmpty(stockOutRecordDto.getTruckPlateNo())) {
|
throw new BaseException("销售类出库必须选择车辆");
|
}
|
LambdaQueryWrapper<Vehicle> wrapper = new LambdaQueryWrapper<Vehicle>()
|
.eq(Vehicle::getPlateNumber, stockOutRecordDto.getTruckPlateNo());
|
Vehicle vehicle = vehicleService.getOne(wrapper, false);
|
if (vehicle == null) {
|
throw new BaseException("车牌号 " + stockOutRecordDto.getTruckPlateNo() + " 不在车辆管理中,请先到车辆管理添加");
|
}
|
stockOutRecordDto.setVehicleId(vehicle.getId());
|
}
|
|
/**
|
* 出库数量是本次装车总量,必须落在「上次剩余油量 ~ 上次剩余油量 + 库存」区间内
|
*/
|
private void checkSaleOutAvailable(StockOutRecordDto stockOutRecordDto) {
|
String suffix = ",出库批次:" + stockOutRecordDto.getOutboundBatches();
|
BigDecimal quantity = stockOutRecordDto.getStockOutNum();
|
if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) {
|
throw new BaseException("销售类出库数量必须大于0" + suffix);
|
}
|
BigDecimal lastResidual = getLastResidualOil(stockOutRecordDto.getVehicleId(), stockOutRecordDto.getProductModelId(), null);
|
if (lastResidual.compareTo(quantity) > 0) {
|
throw new BaseException("出库数量不能小于该车上次剩余油量" + toPlain(lastResidual) + suffix);
|
}
|
BigDecimal stockQuantity = getStockQuantity(stockOutRecordDto.getProductModelId(), stockOutRecordDto.getBatchNo());
|
BigDecimal availableQty = lastResidual.add(stockQuantity);
|
if (quantity.compareTo(availableQty) > 0) {
|
throw new BaseException("出库数量超出可用量(上次剩余" + toPlain(lastResidual)
|
+ ",库存" + toPlain(stockQuantity) + ",可用" + toPlain(availableQty) + ")" + suffix);
|
}
|
}
|
|
/**
|
* 该车该规格上次送完货后剩余的油量,取最近一条填过残油且已审批的记录。
|
* excludeId 用于排除「本次要审批的这条自己」——它自己的 residual_oil 是送完之后的值,不能用来算本次扣减
|
*/
|
private BigDecimal getLastResidualOil(Long vehicleId, Long productModelId, Long excludeId) {
|
if (vehicleId == null || productModelId == null) {
|
return BigDecimal.ZERO;
|
}
|
LambdaQueryWrapper<StockOutRecord> wrapper = new LambdaQueryWrapper<StockOutRecord>()
|
.eq(StockOutRecord::getVehicleId, vehicleId)
|
.eq(StockOutRecord::getProductModelId, productModelId)
|
.isNotNull(StockOutRecord::getResidualOil)
|
.eq(StockOutRecord::getApprovalStatus, ReviewStatusEnum.APPROVED.getCode());
|
if (excludeId != null) {
|
wrapper.lt(StockOutRecord::getId, excludeId);
|
}
|
wrapper.orderByDesc(StockOutRecord::getId).last("limit 1");
|
StockOutRecord last = stockOutRecordMapper.selectOne(wrapper);
|
return last == null || last.getResidualOil() == null ? BigDecimal.ZERO : last.getResidualOil();
|
}
|
|
/**
|
* 当前库存量,按规格合计;传了批次号再叠加批次条件
|
*/
|
private BigDecimal getStockQuantity(Long productModelId, String batchNo) {
|
if (productModelId == null) {
|
return BigDecimal.ZERO;
|
}
|
LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<StockInventory>()
|
.eq(StockInventory::getProductModelId, productModelId);
|
if (!StringUtils.isEmpty(batchNo)) {
|
wrapper.eq(StockInventory::getBatchNo, batchNo);
|
}
|
return stockInventoryMapper.selectList(wrapper).stream()
|
.map(StockInventory::getQualitity)
|
.filter(Objects::nonNull)
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
}
|
|
@Override
|
public OilOutAvailableDto getOilOutAvailableQty(StockOutRecordDto query) {
|
if (query == null || query.getVehicleId() == null || query.getProductModelId() == null) {
|
throw new BaseException("请先选择车辆和产品规格");
|
}
|
BigDecimal lastResidualOil = getLastResidualOil(query.getVehicleId(), query.getProductModelId(), query.getId());
|
BigDecimal stockQuantity = getStockQuantity(query.getProductModelId(), query.getBatchNo());
|
OilOutAvailableDto result = new OilOutAvailableDto();
|
result.setLastResidualOil(lastResidualOil);
|
result.setStockQuantity(stockQuantity);
|
result.setAvailableQty(lastResidualOil.add(stockQuantity));
|
return result;
|
}
|
|
private String toPlain(BigDecimal value) {
|
return value == null ? "0" : value.stripTrailingZeros().toPlainString();
|
}
|
|
@Override
|
public List<StockOutRecord> listByRecordIds(List<Long> recordIds, String recordType) {
|
if (CollectionUtils.isEmpty(recordIds) || StringUtils.isEmpty(recordType)) {
|
return Collections.emptyList();
|
}
|
return stockOutRecordMapper.selectByRecordIds(recordIds, recordType);
|
}
|
|
@Override
|
public IPage<SalesLedger> listBindableSalesLedger(Page page, SalesLedgerDto salesLedgerDto) {
|
// 该查询用 /*data_scope*/ 跳过数据权限,而 MP 默认会把 count 语句优化成「SELECT COUNT(*) FROM sales_ledger
|
// WHERE ...」,优化后标记丢失,count 反而被追加权限条件,total 会比实际记录少。关掉 count 优化后
|
// count 退化成「SELECT COUNT(*) FROM (原句) TOTAL」,标记原样保留
|
page.setOptimizeCountSql(false);
|
return stockOutRecordMapper.bindableSalesLedgerPage(page, salesLedgerDto);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public int releaseByRecordIds(List<Long> recordIds, String recordType) {
|
List<StockOutRecord> records = listByRecordIds(recordIds, recordType);
|
if (records.isEmpty()) {
|
return 0;
|
}
|
for (StockOutRecord record : records) {
|
if (!ReviewStatusEnum.APPROVED.getCode().equals(record.getApprovalStatus())) {
|
continue;
|
}
|
// type=0 归还合格库存(stock_inventory),type=1 归还不合格库存(stock_uninventory)
|
if ("0".equals(record.getType())) {
|
StockInventoryDto stockInventoryDto = new StockInventoryDto();
|
stockInventoryDto.setProductModelId(record.getProductModelId());
|
stockInventoryDto.setBatchNo(record.getBatchNo());
|
stockInventoryDto.setQualitity(record.getStockOutNum());
|
if (stockInventoryMapper.updateAddStockInventory(stockInventoryDto) == 0) {
|
throw new BaseException("库存记录中没有对应的产品,无法归还,出库批次:" + record.getOutboundBatches());
|
}
|
} else if ("1".equals(record.getType())) {
|
StockUninventoryDto stockUninventoryDto = new StockUninventoryDto();
|
stockUninventoryDto.setProductModelId(record.getProductModelId());
|
stockUninventoryDto.setBatchNo(record.getBatchNo());
|
stockUninventoryDto.setQualitity(record.getStockOutNum());
|
if (stockUninventoryMapper.updateAddStockUnInventory(stockUninventoryDto) == 0) {
|
throw new BaseException("库存记录中没有对应的产品,无法归还,出库批次:" + record.getOutboundBatches());
|
}
|
} else {
|
throw new BaseException("出库记录类型异常,无法归还库存,出库批次:" + record.getOutboundBatches());
|
}
|
}
|
return stockOutRecordMapper.deleteBatchIds(records.stream().map(StockOutRecord::getId).toList());
|
}
|
}
|