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
package cn.iocoder.yudao.module.mes.service.wm.productissue;
 
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productissue.vo.line.MesWmProductIssueLinePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productissue.vo.line.MesWmProductIssueLineRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productissue.vo.line.MesWmProductIssueLineReturnableRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productissue.vo.line.MesWmProductIssueLineSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productissue.MesWmProductIssueDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productissue.MesWmProductIssueLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnissue.MesWmReturnIssueLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productissue.MesWmProductIssueLineMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmProductIssueStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderBomService;
import cn.iocoder.yudao.module.mes.service.wm.productissue.MesWmProductIssueDetailService;
import cn.iocoder.yudao.module.mes.service.wm.productissue.MesWmProductIssueLineService;
import cn.iocoder.yudao.module.mes.service.wm.productissue.MesWmProductIssueService;
import cn.iocoder.yudao.module.mes.service.wm.returnissue.MesWmReturnIssueLineService;
import cn.iocoder.yudao.module.mes.service.wm.unitconvert.MesWmItemUnitConvertService;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
 
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
 
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
 
/**
 * MES 领料出库单行 Service 实现类
 */
@Service
@Validated
public class MesWmProductIssueLineServiceImpl implements MesWmProductIssueLineService {
 
    @Resource
    private MesWmProductIssueLineMapper issueLineMapper;
 
    @Resource
    @Lazy
    private MesWmProductIssueService issueService;
    @Resource
    private MesMdItemService mdmItemService;
    @Resource
    @Lazy
    private MesWmReturnIssueLineService returnIssueLineService;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    @Resource
    @Lazy
    private MesWmProductIssueDetailService issueDetailService;
    @Resource
    private MesProWorkOrderBomService workOrderBomService;
    @Resource
    private MesWmItemUnitConvertService itemUnitConvertService;
 
    @Override
    public Long createProductIssueLine(MesWmProductIssueLineSaveReqVO createReqVO) {
        validateProductIssueLineSaveData(createReqVO);
        // 双单位换算:按录入单位换算为物料主单位数量
        fillPrimaryQuantity(createReqVO);
 
        // 插入
        MesWmProductIssueLineDO line = BeanUtils.toBean(createReqVO, MesWmProductIssueLineDO.class);
        issueLineMapper.insert(line);
        return line.getId();
    }
 
    @Override
    public void updateProductIssueLine(MesWmProductIssueLineSaveReqVO updateReqVO) {
        // 校验存在
        validateProductIssueLineExists(updateReqVO.getId());
        validateProductIssueLineSaveData(updateReqVO);
        // 双单位换算:按录入单位换算为物料主单位数量
        fillPrimaryQuantity(updateReqVO);
 
        // 更新
        MesWmProductIssueLineDO updateObj = BeanUtils.toBean(updateReqVO, MesWmProductIssueLineDO.class);
        issueLineMapper.updateById(updateObj);
    }
 
    /**
     * 双单位换算:选择了录入单位时,按「录入数量 × 换算比率」得到物料主单位口径的领料数量
     *
     * 未按辅单位录入时必须直接填写领料数量
     */
    private void fillPrimaryQuantity(MesWmProductIssueLineSaveReqVO reqVO) {
        if (reqVO.getEnterUnitMeasureId() != null) {
            BigDecimal primaryQuantity = itemUnitConvertService.convertToPrimaryUnit(
                    reqVO.getItemId(), reqVO.getItemModelId(),
                    reqVO.getEnterUnitMeasureId(), reqVO.getEnterQuantity());
            if (primaryQuantity != null) {
                reqVO.setQuantity(primaryQuantity);
            }
        }
        if (reqVO.getQuantity() == null) {
            throw exception(WM_PRODUCT_ISSUE_LINE_QUANTITY_REQUIRED);
        }
    }
 
    @Override
    public void deleteProductIssueLine(Long id) {
        // 校验存在
        MesWmProductIssueLineDO line = validateProductIssueLineExists(id);
        // 校验主单为草稿状态才允许删除行
        issueService.validateProductIssueExistsAndPrepare(line.getIssueId());
        // 级联删除该行的明细
        issueDetailService.deleteProductIssueDetailByLineId(id);
        // 删除行
        issueLineMapper.deleteById(id);
    }
 
    @Override
    public MesWmProductIssueLineDO getProductIssueLine(Long id) {
        return issueLineMapper.selectById(id);
    }
 
    @Override
    public PageResult<MesWmProductIssueLineDO> getProductIssueLinePage(MesWmProductIssueLinePageReqVO pageReqVO) {
        return issueLineMapper.selectPage(pageReqVO);
    }
 
    @Override
    public List<MesWmProductIssueLineDO> getProductIssueLineListByIssueId(Long issueId) {
        return issueLineMapper.selectListByIssueId(issueId);
    }
 
    @Override
    public void deleteProductIssueLineByIssueId(Long issueId) {
        issueLineMapper.deleteByIssueId(issueId);
    }
 
    @Override
    public MesWmProductIssueLineDO validateProductIssueLineExists(Long id) {
        MesWmProductIssueLineDO line = issueLineMapper.selectById(id);
        if (line == null) {
            throw exception(WM_PRODUCT_ISSUE_LINE_NOT_EXISTS);
        }
        return line;
    }
 
    /**
     * 校验保存时的关联数据
     */
    private void validateProductIssueLineSaveData(MesWmProductIssueLineSaveReqVO reqVO) {
        // 校验父数据存在 + 草稿状态才允许新增/修改行
        issueService.validateProductIssueExistsAndPrepare(reqVO.getIssueId());
        // 校验物料在工单 BOM 中
        validateItemInWorkOrderBom(reqVO.getIssueId(), reqVO.getItemId());
        // 校验物料存在
        mdmItemService.validateItemExists(reqVO.getItemId());
    }
 
    private void validateItemInWorkOrderBom(Long issueId, Long itemId) {
        // 校验领料单存在,并获取工单编号
        MesWmProductIssueDO issue = issueService.validateProductIssueExists(issueId);
        // 校验物料是否在工单 BOM 中(防错料)
        List<MesProWorkOrderBomDO> workOrderBoms = workOrderBomService.getWorkOrderBomListByWorkOrderId(issue.getWorkOrderId());
        if (CollUtil.isEmpty(workOrderBoms)) {
            return;
        }
        MesProWorkOrderBomDO workOrderBom = CollUtil.findOne(workOrderBoms,
                bom -> bom.getItemId().equals(itemId));
        if (workOrderBom == null) {
            throw exception(WM_PRODUCT_ISSUE_LINE_ITEM_NOT_IN_BOM);
        }
    }
 
    @Override
    public List<MesWmProductIssueLineReturnableRespVO> getReturnableList(Long workOrderId) {
        // 1. 查询该工单下已完成的领料单
        List<MesWmProductIssueDO> finishedIssues = issueService.getFinishedIssuesByWorkOrderId(workOrderId);
        if (CollUtil.isEmpty(finishedIssues)) {
            return Collections.emptyList();
        }
 
        // 2. 获取这些领料单的所有行
        List<Long> issueIds = CollectionUtils.convertList(finishedIssues, MesWmProductIssueDO::getId);
        List<MesWmProductIssueLineDO> lines = issueLineMapper.selectListByIssueIds(issueIds);
        if (CollUtil.isEmpty(lines)) {
            return Collections.emptyList();
        }
 
        // 3. 批量查询已退数量(按 issueLineId 汇总)
        List<Long> lineIds = CollectionUtils.convertList(lines, MesWmProductIssueLineDO::getId);
        List<MesWmReturnIssueLineDO> returnLines = returnIssueLineService.getReturnIssueLineListByIssueLineIds(lineIds);
        // 按 issueLineId 汇总退料数量
        Map<Long, BigDecimal> returnedQtyMap = new java.util.HashMap<>();
        for (MesWmReturnIssueLineDO returnLine : returnLines) {
            if (returnLine.getIssueLineId() != null && returnLine.getQuantity() != null) {
                returnedQtyMap.merge(returnLine.getIssueLineId(), returnLine.getQuantity(), BigDecimal::add);
            }
        }
 
        // 4. 批量查询物料和单位信息
        Map<Long, MesMdItemDO> itemMap = mdmItemService.getItemMap(
                CollectionUtils.convertSet(lines, MesWmProductIssueLineDO::getItemId));
        Map<Long, MesMdUnitMeasureDO> unitMap = unitMeasureService.getUnitMeasureMap(
                CollectionUtils.convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
 
        // 5. 组装结果,过滤可退数量 > 0 的行
        List<MesWmProductIssueLineReturnableRespVO> result = new ArrayList<>();
        for (MesWmProductIssueLineDO line : lines) {
            BigDecimal issuedQty = line.getQuantity() != null ? line.getQuantity() : BigDecimal.ZERO;
            BigDecimal returnedQty = returnedQtyMap.getOrDefault(line.getId(), BigDecimal.ZERO);
            BigDecimal returnableQty = issuedQty.subtract(returnedQty);
 
            if (returnableQty.compareTo(BigDecimal.ZERO) > 0) {
                MesWmProductIssueLineReturnableRespVO vo = new MesWmProductIssueLineReturnableRespVO();
                vo.setId(line.getId());
                vo.setItemId(line.getItemId());
                vo.setBatchId(line.getBatchId());
                vo.setQuantity(returnableQty);
 
                // 填充物料信息
                MesMdItemDO item = itemMap.get(line.getItemId());
                if (item != null) {
                    vo.setItemCode(item.getCode());
                    vo.setItemName(item.getName());
                    vo.setSpecification(item.getSpecification());
                    // 填充单位信息
                    MesMdUnitMeasureDO unit = unitMap.get(item.getUnitMeasureId());
                    vo.setUnitMeasureName(unit != null ? unit.getName() : null);
                }
 
                result.add(vo);
            }
        }
 
        return result;
    }
 
}