2 天以前 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
package com.ruoyi.outsourcing.service.impl;
 
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.basic.mapper.ProductMapper;
import com.ruoyi.basic.mapper.ProductModelMapper;
import com.ruoyi.basic.pojo.Product;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.common.enums.ReviewStatusEnum;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.outsourcing.dto.OutsourcingOrderDto;
import com.ruoyi.outsourcing.enums.OutsourcingOrderStatusEnum;
import com.ruoyi.outsourcing.excel.OutsourcingOrderExcelDto;
import com.ruoyi.outsourcing.mapper.OutsourcingOrderMapper;
import com.ruoyi.outsourcing.mapper.OutsourcingOrderProductMapper;
import com.ruoyi.outsourcing.mapper.OutsourcingPartnerMapper;
import com.ruoyi.outsourcing.pojo.OutsourcingOrder;
import com.ruoyi.outsourcing.pojo.OutsourcingOrderProduct;
import com.ruoyi.outsourcing.pojo.OutsourcingPartner;
import com.ruoyi.outsourcing.service.IOutsourcingOrderService;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.pojo.StockOutRecord;
import com.ruoyi.stock.service.StockInventoryService;
import com.ruoyi.stock.service.StockOutRecordService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
 
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
 
/**
 * 委外订单 Service 实现
 */
@Service
@RequiredArgsConstructor
public class OutsourcingOrderServiceImpl extends ServiceImpl<OutsourcingOrderMapper, OutsourcingOrder>
        implements IOutsourcingOrderService {
 
    private static final String ORDER_NO_PREFIX = "WWDD";
 
    private final OutsourcingOrderMapper outsourcingOrderMapper;
    private final OutsourcingOrderProductMapper outsourcingOrderProductMapper;
    private final OutsourcingPartnerMapper outsourcingPartnerMapper;
    private final ProductMapper productMapper;
    private final ProductModelMapper productModelMapper;
    private final StockInventoryService stockInventoryService;
    private final StockOutRecordService stockOutRecordService;
 
    @Override
    public List<OutsourcingOrder> selectOrderList(OutsourcingOrderDto dto) {
        LambdaQueryWrapper<OutsourcingOrder> wrapper = new LambdaQueryWrapper<>();
        wrapper.like(StringUtils.isNotBlank(dto.getOrderNo()), OutsourcingOrder::getOrderNo, dto.getOrderNo())
                .eq(dto.getPartnerId() != null, OutsourcingOrder::getPartnerId, dto.getPartnerId())
                .like(StringUtils.isNotBlank(dto.getPartnerName()), OutsourcingOrder::getPartnerName, dto.getPartnerName())
                .eq(dto.getStatus() != null, OutsourcingOrder::getStatus, dto.getStatus())
                .like(StringUtils.isNotBlank(dto.getProjectName()), OutsourcingOrder::getProjectName, dto.getProjectName())
                .ge(StringUtils.isNotBlank(dto.getStartDate()), OutsourcingOrder::getApplyDate, dto.getStartDate())
                .le(StringUtils.isNotBlank(dto.getEndDate()), OutsourcingOrder::getApplyDate, dto.getEndDate())
                .orderByDesc(OutsourcingOrder::getId);
        return this.list(wrapper);
    }
 
    @Override
    public OutsourcingOrderDto getDetail(Long id) {
        OutsourcingOrder order = outsourcingOrderMapper.selectById(id);
        if (order == null) {
            throw new ServiceException("委外订单不存在");
        }
        OutsourcingOrderDto dto = new OutsourcingOrderDto();
        BeanUtils.copyProperties(order, dto);
        dto.setProductData(listProducts(id));
        return dto;
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long saveOrder(OutsourcingOrderDto dto) {
        OutsourcingOrder order = new OutsourcingOrder();
        BeanUtils.copyProperties(dto, order);
        order.setId(null);
        order.setStatus(OutsourcingOrderStatusEnum.DRAFT.getCode());
        order.setOrderNo(OrderUtils.countTodayByCreateTime(
                outsourcingOrderMapper, ORDER_NO_PREFIX, "order_no", LocalDateTime.now()));
        order.setConfirmTime(null);
        order.setCancelReason(null);
        fillPartnerName(order);
        if (order.getApplyDate() == null) {
            order.setApplyDate(LocalDate.now());
        }
        order.setContractAmount(sumProducts(dto.getProductData()));
        this.save(order);
 
        saveProducts(order.getId(), dto.getProductData());
        return order.getId();
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updateOrder(OutsourcingOrderDto dto) {
        if (dto.getId() == null) {
            throw new ServiceException("委外订单id不能为空");
        }
        OutsourcingOrder existing = outsourcingOrderMapper.selectById(dto.getId());
        if (existing == null) {
            throw new ServiceException("委外订单不存在");
        }
        if (!OutsourcingOrderStatusEnum.DRAFT.getCode().equals(existing.getStatus())) {
            throw new ServiceException("仅草稿状态的委外订单允许修改");
        }
 
        OutsourcingOrder order = new OutsourcingOrder();
        BeanUtils.copyProperties(dto, order);
        // 单号、状态、确认信息由系统维护,置空后 MyBatis-Plus 不会更新这些列
        order.setOrderNo(null);
        order.setStatus(null);
        order.setConfirmTime(null);
        order.setCancelReason(null);
        order.setTenantId(null);
        order.setDeptId(null);
        order.setCreateUser(null);
        order.setCreateTime(null);
        order.setUpdateTime(null);
        fillPartnerName(order);
        order.setContractAmount(sumProducts(dto.getProductData()));
        boolean updated = this.updateById(order);
 
        if (updated) {
            outsourcingOrderProductMapper.delete(Wrappers.<OutsourcingOrderProduct>lambdaQuery()
                    .eq(OutsourcingOrderProduct::getOutsourcingOrderId, dto.getId()));
            saveProducts(dto.getId(), dto.getProductData());
        }
        return updated;
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean deleteOrders(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            throw new ServiceException("请选择要删除的委外订单");
        }
        List<String> notDraftNos = this.listByIds(ids).stream()
                .filter(order -> !OutsourcingOrderStatusEnum.DRAFT.getCode().equals(order.getStatus()))
                .map(OutsourcingOrder::getOrderNo)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (!notDraftNos.isEmpty()) {
            throw new ServiceException("仅草稿状态的委外订单允许删除:" + String.join("、", notDraftNos));
        }
 
        outsourcingOrderProductMapper.delete(Wrappers.<OutsourcingOrderProduct>lambdaQuery()
                .in(OutsourcingOrderProduct::getOutsourcingOrderId, ids));
        return this.removeByIds(ids);
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean submit(Long id) {
        OutsourcingOrder order = requireOrder(id);
        if (!OutsourcingOrderStatusEnum.DRAFT.getCode().equals(order.getStatus())) {
            throw new ServiceException("仅草稿状态的委外订单允许提交");
        }
        if (order.getPartnerId() == null
                || outsourcingPartnerMapper.selectById(order.getPartnerId()) == null) {
            throw new ServiceException("请先选择有效合作商再提交");
        }
        List<OutsourcingOrderProduct> products = listProducts(id);
        if (products.isEmpty()) {
            throw new ServiceException("委外订单没有产品明细,无法提交");
        }
        if (order.getContractAmount() == null
                || order.getContractAmount().compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("委外订单金额必须大于 0 才能提交");
        }
 
        // 条件更新抢占草稿状态,避免并发下重复提交
        boolean submitted = this.update(Wrappers.<OutsourcingOrder>lambdaUpdate()
                .eq(OutsourcingOrder::getId, id)
                .eq(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.DRAFT.getCode())
                .set(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.CONFIRMED.getCode())
                .set(OutsourcingOrder::getConfirmTime, LocalDateTime.now()));
        if (!submitted) {
            throw new ServiceException("该委外订单已提交,请勿重复操作");
        }
        occupyScrapStock(products);
        return true;
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean complete(Long id) {
        OutsourcingOrder order = requireOrder(id);
        if (!OutsourcingOrderStatusEnum.CONFIRMED.getCode().equals(order.getStatus())) {
            throw new ServiceException("仅已确认状态的委外订单允许完成");
        }
        requireScrapStockOutApproved(order.getId());
        boolean completed = this.update(Wrappers.<OutsourcingOrder>lambdaUpdate()
                .eq(OutsourcingOrder::getId, id)
                .eq(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.CONFIRMED.getCode())
                .set(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.COMPLETED.getCode()));
        if (!completed) {
            throw new ServiceException("仅已确认状态的委外订单允许完成");
        }
        return true;
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean cancel(Long id, String cancelReason) {
        OutsourcingOrder order = requireOrder(id);
        Integer status = order.getStatus();
        if (!OutsourcingOrderStatusEnum.DRAFT.getCode().equals(status)
                && !OutsourcingOrderStatusEnum.CONFIRMED.getCode().equals(status)) {
            throw new ServiceException("该委外订单已完成或已作废,不允许重复作废");
        }
        boolean cancelled = this.update(Wrappers.<OutsourcingOrder>lambdaUpdate()
                .eq(OutsourcingOrder::getId, id)
                .in(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.DRAFT.getCode(),
                        OutsourcingOrderStatusEnum.CONFIRMED.getCode())
                .set(OutsourcingOrder::getStatus, OutsourcingOrderStatusEnum.CANCELLED.getCode())
                .set(OutsourcingOrder::getCancelReason, cancelReason));
        if (!cancelled) {
            throw new ServiceException("该委外订单已完成或已作废,不允许重复作废");
        }
        if (OutsourcingOrderStatusEnum.CONFIRMED.getCode().equals(status)) {
            // 提交时占用的报废品库存要释放:未审批的记录删掉即释放,已审批通过的按批次归还库存
            stockOutRecordService.releaseByRecordIds(scrapLineIds(id),
                    StockOutQualifiedRecordTypeEnum.OUTSOURCING_SCRAP_OUT.getCode());
        }
        return true;
    }
 
    @Override
    public void exportOrders(HttpServletResponse response, OutsourcingOrderDto dto) {
        List<OutsourcingOrder> orders = selectOrderList(dto);
        List<OutsourcingOrderExcelDto> excelList = orders.stream().map(order -> {
            OutsourcingOrderExcelDto excel = new OutsourcingOrderExcelDto();
            BeanUtils.copyProperties(order, excel);
            excel.setStatusLabel(OutsourcingOrderStatusEnum.getLabelByValue(order.getStatus()));
            return excel;
        }).collect(Collectors.toList());
        ExcelUtil<OutsourcingOrderExcelDto> util = new ExcelUtil<>(OutsourcingOrderExcelDto.class);
        util.exportExcel(response, excelList, "委外订单");
    }
 
    private OutsourcingOrder requireOrder(Long id) {
        if (id == null) {
            throw new ServiceException("委外订单id不能为空");
        }
        OutsourcingOrder order = outsourcingOrderMapper.selectById(id);
        if (order == null) {
            throw new ServiceException("委外订单不存在");
        }
        return order;
    }
 
    /**
     * 报废品选料行的 id,作为出库记录的 record_id。
     * 出库记录一条对应一行明细(一个规格+批号),与采购退货出库的记法一致
     */
    private List<Long> scrapLineIds(Long orderId) {
        return listProducts(orderId).stream()
                .filter(product -> Boolean.TRUE.equals(product.getIsScrap()))
                .map(OutsourcingOrderProduct::getId)
                .collect(Collectors.toList());
    }
 
    /**
     * 报废品选料占用库存:每条选料行写一条待审批出库记录。
     *
     * <p>报废品取的是<b>合格库存</b>(stock_inventory,出库记录 type=0)。
     * 记录一落库就通过 stock_out_record 的待审批量把可用量占住
     * (可用量 = qualitity − locked_quantity − 待审批出库量),因此同一批次不会被多张委外订单超选,
     * 但 qualitity 本身要等出库管理审批通过才真正扣减 —— 也就是「提交即占用、审批才实扣」。</p>
     */
    private void occupyScrapStock(List<OutsourcingOrderProduct> products) {
        for (OutsourcingOrderProduct product : products) {
            if (!Boolean.TRUE.equals(product.getIsScrap())) {
                continue;
            }
            if (product.getProductModelId() == null || StringUtils.isEmpty(product.getBatchNo())) {
                throw new ServiceException("报废品选料行缺少产品规格或批号,无法提交");
            }
            BigDecimal quantity = product.getQuantity();
            if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) {
                throw new ServiceException("报废品选料行数量必须大于 0");
            }
            StockInventoryDto stockInventoryDto = new StockInventoryDto();
            stockInventoryDto.setRecordId(product.getId());
            stockInventoryDto.setRecordType(StockOutQualifiedRecordTypeEnum.OUTSOURCING_SCRAP_OUT.getCode());
            stockInventoryDto.setProductModelId(product.getProductModelId());
            stockInventoryDto.setBatchNo(product.getBatchNo());
            stockInventoryDto.setQualitity(quantity);
            try {
                stockInventoryService.addStockOutRecordOnly(stockInventoryDto);
            } catch (RuntimeException e) {
                // 合格库存不足抛的是 ServiceException,不合格那种 BaseException 也一并兜住
                throw new ServiceException("报废品「" + product.getSpecificationModel() + "」批号「"
                        + product.getBatchNo() + "」" + e.getMessage());
            }
        }
    }
 
    /**
     * 报废品选料的出库记录必须全部审批通过才允许完成订单,
     * 否则会出现「委外已完成、库存没扣」的账实不符
     */
    private void requireScrapStockOutApproved(Long orderId) {
        List<Long> lineIds = scrapLineIds(orderId);
        if (lineIds.isEmpty()) {
            return;
        }
        List<StockOutRecord> records = stockOutRecordService.listByRecordIds(
                lineIds, StockOutQualifiedRecordTypeEnum.OUTSOURCING_SCRAP_OUT.getCode());
        if (records.size() < lineIds.size()) {
            throw new ServiceException("报废品选料缺少出库记录,请先作废后重新提交");
        }
        List<String> pendingBatches = records.stream()
                .filter(record -> !ReviewStatusEnum.APPROVED.getCode().equals(record.getApprovalStatus()))
                .map(StockOutRecord::getOutboundBatches)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (!pendingBatches.isEmpty()) {
            throw new ServiceException("报废品出库未审批通过,请先到出库管理审批出库批次:"
                    + String.join("、", pendingBatches));
        }
    }
 
    private List<OutsourcingOrderProduct> listProducts(Long orderId) {
        return outsourcingOrderProductMapper.selectList(Wrappers.<OutsourcingOrderProduct>lambdaQuery()
                .eq(OutsourcingOrderProduct::getOutsourcingOrderId, orderId)
                .orderByAsc(OutsourcingOrderProduct::getId));
    }
 
    private void saveProducts(Long orderId, List<OutsourcingOrderProduct> products) {
        if (products == null || products.isEmpty()) {
            return;
        }
        fillProductNames(products);
        for (OutsourcingOrderProduct product : products) {
            product.setId(null);
            product.setOutsourcingOrderId(orderId);
            product.setTenantId(null);
            product.setDeptId(null);
            product.setCreateUser(null);
            product.setCreateTime(null);
            product.setUpdateTime(null);
            outsourcingOrderProductMapper.insert(product);
        }
    }
 
    /**
     * 委外金额以明细含税总价合计为准,避免前端传入金额与明细对不上导致往来应付错乱
     */
    private BigDecimal sumProducts(List<OutsourcingOrderProduct> products) {
        if (products == null || products.isEmpty()) {
            return BigDecimal.ZERO;
        }
        return products.stream()
                .map(OutsourcingOrderProduct::getTaxInclusiveTotalPrice)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
 
    /**
     * 补全产品大类与规格型号,便于订单直接展示产品名称
     */
    private void fillProductNames(List<OutsourcingOrderProduct> products) {
        Set<Long> productIds = products.stream()
                .map(OutsourcingOrderProduct::getProductId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Set<Long> modelIds = products.stream()
                .map(OutsourcingOrderProduct::getProductModelId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
 
        Map<Long, String> productMap = new HashMap<>();
        if (!productIds.isEmpty()) {
            for (Product product : productMapper.selectBatchIds(productIds)) {
                productMap.put(product.getId(), product.getProductName());
            }
        }
        Map<Long, String> modelMap = new HashMap<>();
        if (!modelIds.isEmpty()) {
            for (ProductModel model : productModelMapper.selectBatchIds(modelIds)) {
                modelMap.put(model.getId(), model.getModel());
            }
        }
 
        for (OutsourcingOrderProduct product : products) {
            String productName = productMap.get(product.getProductId());
            if (productName != null) {
                product.setProductCategory(productName);
            }
            String model = modelMap.get(product.getProductModelId());
            if (model != null) {
                product.setSpecificationModel(model);
            }
        }
    }
 
    private void fillPartnerName(OutsourcingOrder order) {
        if (order.getPartnerId() == null) {
            return;
        }
        OutsourcingPartner partner = outsourcingPartnerMapper.selectById(order.getPartnerId());
        if (partner == null) {
            throw new ServiceException("合作商不存在,请重新选择");
        }
        order.setPartnerName(partner.getPartnerName());
    }
}