昨天 3e5e22f5358156ff7430ee117546123ffe042611
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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
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 &gt;= 数量 的守卫,影响行数为 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());
    }
}