package com.ruoyi.sales.service.impl; 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.common.enums.ReviewStatusEnum; import com.ruoyi.common.exception.base.BaseException; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.sales.dto.SalesLedgerArrivalDto; import com.ruoyi.sales.mapper.SalesLedgerArrivalMapper; import com.ruoyi.sales.mapper.SalesLedgerMapper; import com.ruoyi.sales.mapper.StockOutRecordSalesLedgerMapper; import com.ruoyi.sales.pojo.SalesLedger; import com.ruoyi.sales.pojo.SalesLedgerArrival; import com.ruoyi.sales.pojo.StockOutRecordSalesLedger; import com.ruoyi.sales.service.ISalesLedgerArrivalService; import com.ruoyi.stock.mapper.StockOutRecordMapper; import com.ruoyi.stock.pojo.StockOutRecord; 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.List; /** * 销售台账到货记录Service业务层处理 *

* 到货记录挂在「一次发货」(油品出库单)下:出库单与销售台账的对应关系由绑定表 * stock_out_record_sales_ledger 给出,明细 id、车辆、车牌一律以出库单为准,不信前端传入。 * 代储台账不产绑定行,出库单的单值列 sales_ledger_id 即为权威对应关系(见 requireBinding)。 * * @author ruoyi * @date 2026-09-21 */ @Service @RequiredArgsConstructor public class SalesLedgerArrivalServiceImpl extends ServiceImpl implements ISalesLedgerArrivalService { private static final String LEDGER_TYPE_DEPOSIT = "代储"; private final SalesLedgerArrivalMapper salesLedgerArrivalMapper; private final StockOutRecordSalesLedgerMapper stockOutRecordSalesLedgerMapper; private final StockOutRecordMapper stockOutRecordMapper; private final SalesLedgerMapper salesLedgerMapper; @Override public IPage listPage(Page page, SalesLedgerArrivalDto req) { // 该查询用 /*data_scope*/ 跳过数据权限,而 MP 默认会把 count 语句优化成「SELECT COUNT(*) FROM 主表 WHERE ...」, // 优化后标记丢失、count 反被追加权限条件,total 会比实际少。关掉 count 优化后退化成「SELECT COUNT(*) FROM (原句) TOTAL」 page.setOptimizeCountSql(false); return salesLedgerArrivalMapper.listPage(page, req); } @Override public List bindableVehicles(Long salesLedgerId) { if (salesLedgerId == null) { throw new BaseException("请传入销售台账id"); } return salesLedgerArrivalMapper.bindableVehicles(salesLedgerId); } @Override public boolean addArrival(SalesLedgerArrivalDto req) { if (req == null) { throw new BaseException("到货记录不能为空"); } StockOutRecord outRecord = requireApprovedOilOut(req.getStockOutRecordId()); StockOutRecordSalesLedger binding = requireBinding(outRecord, req.getSalesLedgerId()); applyOutRecordVehicle(req, outRecord); req.setSalesLedgerProductId(binding.getSalesLedgerProductId()); checkArrivalQuantity(req, binding, null); req.setId(null); return save(req); } @Override public boolean updateArrival(SalesLedgerArrivalDto req) { if (req == null || req.getId() == null) { throw new BaseException("请传来到货记录id"); } SalesLedgerArrivalDto existing = salesLedgerArrivalMapper.selectArrivalById(req.getId()); if (existing == null) { throw new BaseException("该到货记录不存在,无法修改,id:" + req.getId()); } // 出库单/台账是记录的身份:前端不回传就沿用原值,改了则按新身份重新校验 Long stockOutRecordId = req.getStockOutRecordId() != null ? req.getStockOutRecordId() : existing.getStockOutRecordId(); Long salesLedgerId = req.getSalesLedgerId() != null ? req.getSalesLedgerId() : existing.getSalesLedgerId(); StockOutRecord outRecord = requireApprovedOilOut(stockOutRecordId); StockOutRecordSalesLedger binding = requireBinding(outRecord, salesLedgerId); req.setStockOutRecordId(stockOutRecordId); req.setSalesLedgerId(salesLedgerId); req.setSalesLedgerProductId(binding.getSalesLedgerProductId()); applyOutRecordVehicle(req, outRecord); if (req.getArrivalQuantity() == null) { req.setArrivalQuantity(existing.getArrivalQuantity()); } checkArrivalQuantity(req, binding, req.getId()); return updateById(req); } @Override @Transactional(rollbackFor = Exception.class) public boolean deleteArrivals(List ids) { if (CollectionUtils.isEmpty(ids)) { throw new BaseException("请传入要删除的到货记录id"); } return removeByIds(ids); } /** * 出库单必须是「审批通过的油品出库」(type 为空),否则它不是发货台账的父行,挂到货记录会变成孤儿 */ private StockOutRecord requireApprovedOilOut(Long stockOutRecordId) { if (stockOutRecordId == null) { throw new BaseException("请选择出库单"); } StockOutRecord outRecord = stockOutRecordMapper.selectById(stockOutRecordId); if (outRecord == null) { throw new BaseException("所选出库单不存在,出库单id:" + stockOutRecordId); } if (!ReviewStatusEnum.APPROVED.getCode().equals(outRecord.getApprovalStatus())) { throw new BaseException("出库单未审批通过,不能登记到货,出库单号:" + outRecord.getOutboundBatches()); } if (StringUtils.isNotEmpty(outRecord.getType())) { throw new BaseException("该出库单不是油品出库,不能登记到货,出库单号:" + outRecord.getOutboundBatches()); } return outRecord; } /** * 台账必须与该出库单有绑定关系 —— 绑定行同时给出明细 id 和本台账分摊到的出库数量(到货上限)。 * 不能看 stock_out_record.sales_ledger_id,那一列只存了首项台账。 * 代储例外:代储不产绑定行(绑定行同时是开票/收款候选的数据源,代储不能进),但它「一次出库只能绑一个台账」, * 单值列反而权威,故按整单出库量造一行临时的绑定(不落库)用于后续校验 */ private StockOutRecordSalesLedger requireBinding(StockOutRecord outRecord, Long salesLedgerId) { if (salesLedgerId == null) { throw new BaseException("请选择销售台账"); } SalesLedger salesLedger = salesLedgerMapper.selectById(salesLedgerId); if (salesLedger == null) { throw new BaseException("所选销售台账不存在,销售台账id:" + salesLedgerId); } StockOutRecordSalesLedger binding = stockOutRecordSalesLedgerMapper.selectOne( Wrappers.lambdaQuery() .eq(StockOutRecordSalesLedger::getStockOutRecordId, outRecord.getId()) .eq(StockOutRecordSalesLedger::getSalesLedgerId, salesLedgerId)); if (binding != null) { return binding; } if (LEDGER_TYPE_DEPOSIT.equals(salesLedger.getLedgerType()) && salesLedgerId.equals(outRecord.getSalesLedgerId())) { StockOutRecordSalesLedger deposit = new StockOutRecordSalesLedger(); deposit.setStockOutRecordId(outRecord.getId()); deposit.setSalesLedgerId(salesLedgerId); deposit.setQuantity(outRecord.getStockOutNum()); return deposit; } throw new BaseException("该出库单未绑定此销售台账,不能登记到货"); } /** * 一次出库只对应一辆车,车辆与车牌一律取出库单上登记的,前端传的只用于比对。 * 出库单没登记车辆时(代储/客存出库不强制选车)允许留空,不阻塞登记 */ private void applyOutRecordVehicle(SalesLedgerArrivalDto req, StockOutRecord outRecord) { if (outRecord.getVehicleId() == null) { req.setVehicleId(null); req.setTruckPlateNo(null); return; } if (req.getVehicleId() != null && !req.getVehicleId().equals(outRecord.getVehicleId())) { throw new BaseException("所选车辆与该出库单登记的车辆不一致,出库单号:" + outRecord.getOutboundBatches()); } req.setVehicleId(outRecord.getVehicleId()); req.setTruckPlateNo(outRecord.getTruckPlateNo()); } /** * 到货数量上限 = 该台账在本出库单上分摊到的数量(不是台账明细的合同数量,否则挡不住「到货超过实发」) */ private void checkArrivalQuantity(SalesLedgerArrivalDto req, StockOutRecordSalesLedger binding, Long excludeId) { BigDecimal quantity = req.getArrivalQuantity(); if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) { throw new BaseException("到货数量必须大于0"); } BigDecimal limit = binding.getQuantity() == null ? BigDecimal.ZERO : binding.getQuantity(); BigDecimal arrived = salesLedgerArrivalMapper.sumArrivedQuantity( binding.getStockOutRecordId(), binding.getSalesLedgerId(), excludeId); if (arrived.add(quantity).compareTo(limit) > 0) { throw new BaseException("到货数量超出该台账本次出库数量,出库数量:" + limit.toPlainString() + ",已到货:" + arrived.toPlainString()); } } }