6 小时以前 7bf754a7835d06f26240c4ca15bc5f83650de377
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
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业务层处理
 * <p>
 * 到货记录挂在「一次发货」(油品出库单)下:出库单与销售台账的对应关系由绑定表
 * stock_out_record_sales_ledger 给出,明细 id、车辆、车牌一律以出库单为准,不信前端传入。
 * 代储台账不产绑定行,出库单的单值列 sales_ledger_id 即为权威对应关系(见 requireBinding)。
 *
 * @author ruoyi
 * @date 2026-09-21
 */
@Service
@RequiredArgsConstructor
public class SalesLedgerArrivalServiceImpl extends ServiceImpl<SalesLedgerArrivalMapper, SalesLedgerArrival>
        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<SalesLedgerArrivalDto> 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<SalesLedgerArrivalDto> 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<Long> 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.<StockOutRecordSalesLedger>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());
        }
    }
}