| | |
| | | |
| | | 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.enums.StockInUnQualifiedRecordTypeEnum; |
| | | 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.mapper.StockInventoryMapper; |
| | | import com.ruoyi.stock.mapper.StockOutRecordMapper; |
| | | import com.ruoyi.stock.mapper.StockUninventoryMapper; |
| | | import com.ruoyi.stock.pojo.StockInRecord; |
| | | 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 lombok.AllArgsConstructor; |
| | | 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 javax.servlet.http.HttpServletResponse; |
| | | import java.math.BigDecimal; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | import java.util.Objects; |
| | | |
| | | /** |
| | | * <p> |
| | |
| | | * @since 2026-01-21 05:27:04 |
| | | */ |
| | | @Service |
| | | @AllArgsConstructor |
| | | @RequiredArgsConstructor |
| | | public class StockOutRecordServiceImpl extends ServiceImpl<StockOutRecordMapper, StockOutRecord> implements StockOutRecordService { |
| | | private StockOutRecordMapper stockOutRecordMapper; |
| | | private StockInventoryMapper stockInventoryMapper; |
| | | private StockUninventoryMapper stockUninventoryMapper; |
| | | /** |
| | | * 出库类别:销售。销售类油品出库在审批通过时按批次扣减库存 |
| | | */ |
| | | 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) { |
| | |
| | | |
| | | @Override |
| | | public int add(StockOutRecordDto stockOutRecordDto) { |
| | | String no = OrderUtils.countTodayByCreateTime(stockOutRecordMapper, "CK","outboundBatches"); |
| | | 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); |
| | | StockInRecord stockInRecord = new StockInRecord(); |
| | | BeanUtils.copyProperties(stockOutRecordDto, stockInRecord); |
| | | stockOutRecordDto.setCreateTime(createTime); |
| | | if (StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode().equals(stockOutRecordDto.getRecordType())){ |
| | | stockOutRecordDto.setApprovalStatus(3); |
| | | } |
| | | return stockOutRecordMapper.insert(stockOutRecordDto); |
| | | } |
| | | |
| | |
| | | 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")) { |
| | | StockInventory stockInventory = stockInventoryMapper.selectOne(new LambdaQueryWrapper<StockInventory>().eq(StockInventory::getProductModelId, stockOutRecord.getProductModelId())); |
| | | 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")) { |
| | | StockUninventory stockUninventory = stockUninventoryMapper.selectOne(new LambdaQueryWrapper<StockUninventory>().eq(StockUninventory::getProductModelId, stockOutRecord.getProductModelId())); |
| | | } |
| | | 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 |
| | |
| | | if (stockInRecordExportData.getType().equals("0")) { |
| | | stockInRecordExportData.setRecordType(EnumUtil.fromCode(StockOutQualifiedRecordTypeEnum.class, Integer.parseInt(stockInRecordExportData.getRecordType())).getValue()); |
| | | }else { |
| | | stockInRecordExportData.setRecordType(EnumUtil.fromCode(StockInUnQualifiedRecordTypeEnum.class, Integer.parseInt(stockInRecordExportData.getRecordType())).getValue()); |
| | | 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()); |
| | | } |
| | | } |