chen.lin
17 小时以前 82065a03737fa1370eb9f4f01ab5332933baf08a
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
package com.vincent.rsf.server.manager.schedules;
 
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.server.manager.controller.params.OutStockToTaskParams;
import com.vincent.rsf.server.manager.controller.params.PakinItem;
import com.vincent.rsf.server.manager.controller.params.WaitPakinParam;
import com.vincent.rsf.server.manager.entity.*;
import com.vincent.rsf.server.manager.enums.AsnExceStatus;
import com.vincent.rsf.server.manager.enums.OrderType;
import com.vincent.rsf.server.manager.enums.OrderWorkType;
import com.vincent.rsf.server.manager.enums.TaskStsType;
import com.vincent.rsf.server.manager.service.*;
import com.vincent.rsf.server.manager.utils.LocManageUtil;
import com.vincent.rsf.server.system.constant.GlobalConfigCode;
import com.vincent.rsf.server.system.constant.SerialRuleCode;
import com.vincent.rsf.server.system.entity.Config;
import com.vincent.rsf.server.system.service.ConfigService;
import com.vincent.rsf.server.system.utils.SerialRuleUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
 
import java.util.*;
import java.util.stream.Collectors;
 
/**
 * 指定物料自动化定时任务:可配置物料编码后,
 * 1)有库存时自动生成全版出库单;
 * 2)该物料出库单自动下发任务;
 * 3)RCS 入库通知时(可选)自动组托,数量可配置。
 */
@Slf4j
@Component
public class MaterialAutoSchedules {
 
    private static final Long SYSTEM_USER_ID = 1L;
    private static final String DEFAULT_SITE_NO = "1001";
 
    @Autowired
    private ConfigService configService;
    @Autowired
    private OutStockService outStockService;
    @Autowired
    private AsnOrderItemService asnOrderItemService;
    @Autowired
    private LocItemService locItemService;
    @Autowired
    private LocService locService;
    @Autowired
    private MatnrService matnrService;
    @Autowired
    private TaskService taskService;
    @Autowired
    private TaskItemService taskItemService;
    @Autowired
    private com.vincent.rsf.server.api.service.MobileService mobileService;
    @Autowired
    private AsnOrderService asnOrderService;
    @Autowired
    private WaitPakinItemService waitPakinItemService;
 
    /**
     * 定时任务1:指定物料有库存时自动生成全版出库单(每 2 分钟)
     * 配置:AUTO_FULL_OUT_MATNR_CODE(物料编码)、AUTO_FULL_OUT_ENABLED(true 启用)
     */
    @Scheduled(cron = "0 0/2 * * * ?")
    @Transactional(rollbackFor = Exception.class)
    public void autoCreateFullOutOrder() {
        Config enabledConfig = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.AUTO_FULL_OUT_ENABLED));
        if (enabledConfig == null || !Boolean.parseBoolean(enabledConfig.getVal())) {
            return;
        }
        Config matnrConfig = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.AUTO_FULL_OUT_MATNR_CODE));
        if (matnrConfig == null || StringUtils.isBlank(matnrConfig.getVal())) {
            return;
        }
        String matnrCode = matnrConfig.getVal().trim();
        Matnr matnr = matnrService.getOne(new LambdaQueryWrapper<Matnr>().eq(Matnr::getCode, matnrCode));
        if (matnr == null) {
            log.warn("[自动全版出库单] 物料不存在: {}", matnrCode);
            return;
        }
        // 已有该物料未下发的出库单则本轮不再生成,等下发完后再生成(避免重复)
        List<Long> initOrderIds = outStockService.list(new LambdaQueryWrapper<WkOrder>()
                        .eq(WkOrder::getType, OrderType.ORDER_OUT.type)
                        .eq(WkOrder::getExceStatus, AsnExceStatus.OUT_STOCK_STATUS_TASK_INIT.val))
                .stream().map(WkOrder::getId).collect(Collectors.toList());
        if (!initOrderIds.isEmpty()) {
            long hasMatnr = asnOrderItemService.count(new LambdaQueryWrapper<WkOrderItem>()
                    .in(WkOrderItem::getOrderId, initOrderIds)
                    .eq(WkOrderItem::getMatnrCode, matnrCode));
            if (hasMatnr > 0) {
                return;
            }
        }
        // 按库位分组:该物料、库位状态为 F 的在库;每个库位各生成一张出库单
        List<LocItem> items = locItemService.list(new LambdaQueryWrapper<LocItem>()
                .eq(LocItem::getMatnrCode, matnrCode)
                .gt(LocItem::getAnfme, 0));
        if (items.isEmpty()) {
            return;
        }
        Map<Long, List<LocItem>> byLocId = items.stream().collect(Collectors.groupingBy(LocItem::getLocId));
        for (Long locId : byLocId.keySet()) {
            Loc loc = locService.getById(locId);
            if (loc == null || !"F".equals(loc.getUseStatus())) {
                continue;
            }
            List<LocItem> locItems = byLocId.get(locId);
            double sumQty = locItems.stream().mapToDouble(li -> li.getAnfme() != null ? li.getAnfme() : 0).sum();
            if (sumQty <= 0) continue;
            try {
                String ruleCode = SerialRuleUtils.generateRuleCode(SerialRuleCode.SYS_OUT_STOCK_CODE, null);
                if (StringUtils.isBlank(ruleCode)) {
                    log.warn("[自动全版出库单] 出库单编码规则未配置");
                    break;
                }
                WkOrder order = new WkOrder();
                order.setCode(ruleCode)
                        .setType(OrderType.ORDER_OUT.type)
                        .setWkType(OrderWorkType.ORDER_WORK_TYPE_STOCK_OUT.type)
                        .setExceStatus(AsnExceStatus.OUT_STOCK_STATUS_TASK_INIT.val)
                        .setAnfme(sumQty)
                        .setWorkQty(0.0)
                        .setQty(0.0)
                        .setCreateBy(SYSTEM_USER_ID)
                        .setUpdateBy(SYSTEM_USER_ID);
                if (!outStockService.save(order)) {
                    throw new CoolException("出库主单保存失败");
                }
                LocItem first = locItems.get(0);
                WkOrderItem orderItem = new WkOrderItem();
                orderItem.setOrderId(order.getId())
                        .setOrderCode(order.getCode())
                        .setMatnrId(matnr.getId())
                        .setMatnrCode(matnr.getCode())
                        .setMaktx(matnr.getName())
                        .setAnfme(sumQty)
                        .setWorkQty(0.0)
                        .setQty(0.0)
                        .setStockUnit(first.getUnit() != null ? first.getUnit() : "个")
                        .setPurUnit(first.getUnit() != null ? first.getUnit() : "个")
                        .setSplrBatch(first.getSplrBatch())
                        .setBatch(first.getBatch())
                        .setFieldsIndex(first.getFieldsIndex())
                        .setCreateBy(SYSTEM_USER_ID)
                        .setUpdateBy(SYSTEM_USER_ID);
                if (!asnOrderItemService.save(orderItem)) {
                    throw new CoolException("出库明细保存失败");
                }
                log.info("[自动全版出库单] 已生成出库单: {}, 库位: {}, 物料: {}, 数量: {}", order.getCode(), loc.getCode(), matnrCode, sumQty);
            } catch (Exception e) {
                log.error("[自动全版出库单] 生成失败, 库位: {}, 物料: {}", loc.getCode(), matnrCode, e);
            }
        }
    }
 
    /**
     * 定时任务2:该物料出库单自动下发任务(每 1 分钟)
     * 配置:AUTO_FULL_OUT_DISPATCH_ENABLED(true 启用)、AUTO_FULL_OUT_MATNR_CODE
     */
    @Scheduled(cron = "0 0/1 * * * ?")
    @Transactional(rollbackFor = Exception.class)
    public void autoDispatchOutTask() {
        Config enabledConfig = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.AUTO_FULL_OUT_DISPATCH_ENABLED));
        if (enabledConfig == null || !Boolean.parseBoolean(enabledConfig.getVal())) {
            return;
        }
        Config matnrConfig = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.AUTO_FULL_OUT_MATNR_CODE));
        if (matnrConfig == null || StringUtils.isBlank(matnrConfig.getVal())) {
            return;
        }
        String matnrCode = matnrConfig.getVal().trim();
        List<WkOrder> orders = outStockService.list(new LambdaQueryWrapper<WkOrder>()
                .eq(WkOrder::getType, OrderType.ORDER_OUT.type)
                .eq(WkOrder::getExceStatus, AsnExceStatus.OUT_STOCK_STATUS_TASK_INIT.val));
        for (WkOrder order : orders) {
            List<WkOrderItem> orderItems = asnOrderItemService.list(new LambdaQueryWrapper<WkOrderItem>()
                    .eq(WkOrderItem::getOrderId, order.getId())
                    .eq(WkOrderItem::getMatnrCode, matnrCode));
            if (orderItems.isEmpty()) continue;
            List<OutStockToTaskParams> paramsList = new ArrayList<>();
            for (WkOrderItem oi : orderItems) {
                double remaining = (oi.getAnfme() != null ? oi.getAnfme() : 0) - (oi.getWorkQty() != null ? oi.getWorkQty() : 0);
                if (remaining <= 0) continue;
                String batch = oi.getSplrBatch() != null ? oi.getSplrBatch() : oi.getBatch();
                List<LocItem> locItems = LocManageUtil.getFirstInFirstOutItemList(oi.getMatnrCode(), batch, remaining);
                for (LocItem locItem : locItems) {
                    if (remaining <= 0) break;
                    Loc loc = locService.getById(locItem.getLocId());
                    if (loc == null || !"F".equals(loc.getUseStatus())) continue;
                    double outQty = Math.min(remaining, locItem.getAnfme() != null ? locItem.getAnfme() : 0);
                    if (outQty <= 0) continue;
                    OutStockToTaskParams param = new OutStockToTaskParams();
                    param.setId(locItem.getId());
                    param.setLocCode(loc.getCode());
                    param.setOutQty(outQty);
                    param.setSiteNo(DEFAULT_SITE_NO);
                    param.setBatch(locItem.getBatch());
                    paramsList.add(param);
                    remaining -= outQty;
                }
            }
            if (paramsList.isEmpty()) continue;
            try {
                outStockService.genOutStockTask(paramsList, SYSTEM_USER_ID, order.getId());
                List<TaskItem> taskItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>().eq(TaskItem::getSourceId, order.getId()));
                if (!taskItems.isEmpty()) {
                    Set<Long> taskIds = taskItems.stream().map(TaskItem::getTaskId).collect(Collectors.toSet());
                    List<Task> tasks = taskService.listByIds(taskIds).stream()
                            .filter(t -> TaskStsType.GENERATE_OUT.id.equals(t.getTaskStatus()))
                            .collect(Collectors.toList());
                    if (!tasks.isEmpty()) {
                        taskService.pubTaskToWcs(tasks);
                        log.info("[自动下发任务] 出库单: {} 已下发任务", order.getCode());
                    }
                }
            } catch (Exception e) {
                log.error("[自动下发任务] 出库单: {} 下发失败", order.getCode(), e);
            }
        }
    }
 
    /**
     * 定时任务3:无订单组托 + 自动生成入库单(仅针对配置物料,每 2 分钟)
     * 先按配置物料与数量做无订单组托,再生成入库单并关联组托明细,便于 RCS 入库闭环。
     * 配置:AUTO_PAKIN_ON_ASN_ENABLED(true)、AUTO_FULL_OUT_MATNR_CODE、AUTO_PAKIN_QTY(数量)
     */
    @Scheduled(cron = "0 0/2 * * * ?")
    @Transactional(rollbackFor = Exception.class)
    public void autoPakinOnInbound() {
        Config enabledConfig = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.AUTO_PAKIN_ON_ASN_ENABLED));
        if (enabledConfig == null || !Boolean.parseBoolean(enabledConfig.getVal())) {
            return;
        }
        Config matnrConfig = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.AUTO_FULL_OUT_MATNR_CODE));
        if (matnrConfig == null || StringUtils.isBlank(matnrConfig.getVal())) {
            return;
        }
        Config qtyConfig = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.AUTO_PAKIN_QTY));
        double autoQty = 1.0;
        if (qtyConfig != null && StringUtils.isNotBlank(qtyConfig.getVal())) {
            try {
                autoQty = Double.parseDouble(qtyConfig.getVal().trim());
                if (autoQty <= 0) autoQty = 1.0;
            } catch (NumberFormatException e) {
                // ignore
            }
        }
        String matnrCode = matnrConfig.getVal().trim();
        Matnr matnr = matnrService.getOne(new LambdaQueryWrapper<Matnr>().eq(Matnr::getCode, matnrCode));
        if (matnr == null) {
            log.warn("[无订单自动组托] 物料不存在: {}", matnrCode);
            return;
        }
        // 已有该物料未执行入库单则本轮不继续生成,避免堆积
        List<Long> initAsnIds = asnOrderService.list(new LambdaQueryWrapper<WkOrder>()
                        .eq(WkOrder::getType, OrderType.ORDER_IN.type)
                        .eq(WkOrder::getExceStatus, AsnExceStatus.ASN_EXCE_STATUS_UN_EXCE.val))
                .stream().map(WkOrder::getId).collect(Collectors.toList());
        if (!initAsnIds.isEmpty()) {
            long hasMatnr = asnOrderItemService.count(new LambdaQueryWrapper<WkOrderItem>()
                    .in(WkOrderItem::getOrderId, initAsnIds)
                    .eq(WkOrderItem::getMatnrCode, matnrCode));
            if (hasMatnr > 0) {
                return;
            }
        }
        // 1)无订单组托:仅物料 + 数量,不传 asnCode/id
        List<PakinItem> pakinItems = new ArrayList<>();
        PakinItem pi = new PakinItem();
        pi.setMatnrId(matnr.getId());
        pi.setReceiptQty(autoQty);
        pi.setAsnCode(null);
        pi.setId(null);
        pakinItems.add(pi);
        String barcode = "AUTO-PAKIN-" + System.currentTimeMillis();
        WaitPakinParam param = new WaitPakinParam();
        param.setBarcode(barcode);
        param.setItems(pakinItems);
        WaitPakin waitPakin;
        try {
            waitPakin = mobileService.mergeItems(param, SYSTEM_USER_ID);
        } catch (Exception e) {
            log.warn("[无订单自动组托] 组托失败: {}", e.getMessage());
            return;
        }
        // 2)自动生成入库单(一条明细,配置物料 + 数量)
        String ruleCode = SerialRuleUtils.generateRuleCode(SerialRuleCode.SYS_ASN_ORDER, null);
        if (StringUtils.isBlank(ruleCode)) {
            log.warn("[无订单自动组托] 入库单编码规则未配置");
            return;
        }
        WkOrder order = new WkOrder();
        order.setCode(ruleCode)
                .setType(OrderType.ORDER_IN.type)
                .setExceStatus(AsnExceStatus.ASN_EXCE_STATUS_UN_EXCE.val)
                .setAnfme(autoQty)
                .setWorkQty(0.0)
                .setQty(0.0)
                .setCreateBy(SYSTEM_USER_ID)
                .setUpdateBy(SYSTEM_USER_ID);
        if (!asnOrderService.save(order)) {
            throw new CoolException("入库主单保存失败");
        }
        WkOrderItem orderItem = new WkOrderItem();
        orderItem.setOrderId(order.getId())
                .setOrderCode(order.getCode())
                .setMatnrId(matnr.getId())
                .setMatnrCode(matnr.getCode())
                .setMaktx(matnr.getName())
                .setAnfme(autoQty)
                .setWorkQty(0.0)
                .setQty(0.0)
                .setStockUnit(matnr.getStockUnit() != null ? matnr.getStockUnit() : "个")
                .setPurUnit(matnr.getPurUnit() != null ? matnr.getPurUnit() : "个")
                .setFieldsIndex(matnr.getFieldsIndex())
                .setCreateBy(SYSTEM_USER_ID)
                .setUpdateBy(SYSTEM_USER_ID);
        if (!asnOrderItemService.save(orderItem)) {
            throw new CoolException("入库明细保存失败");
        }
        // 3)关联组托明细到入库单(asnId / asnCode / asnItemId)
        boolean updated = waitPakinItemService.update(new LambdaUpdateWrapper<WaitPakinItem>()
                .eq(WaitPakinItem::getPakinId, waitPakin.getId())
                .set(WaitPakinItem::getAsnId, order.getId())
                .set(WaitPakinItem::getAsnCode, order.getCode())
                .set(WaitPakinItem::getAsnItemId, orderItem.getId()));
        if (!updated) {
            log.warn("[无订单自动组托] 组托明细关联入库单失败, pakinId={}", waitPakin.getId());
        }
        log.info("[无订单自动组托] 已组托并生成入库单: {}, 物料: {}, 数量: {}", order.getCode(), matnrCode, autoQty);
    }
}