修改优化
1. 波次生成修改优化
2. 出库库位查找 优化
| | |
| | | fieldsIndex: "fieldsIndex", |
| | | anfme: "anfme", |
| | | workQty: "workQty", |
| | | qty: "Qty", |
| | | stockLocs: "Stock Locs" |
| | | }, |
| | | task: { |
| | | taskCode: "TaskCode", |
| | |
| | | fieldsIndex: "动态扩展", |
| | | anfme: "数量", |
| | | workQty: "执行数", |
| | | qty: "完成数量", |
| | | stockLocs: "库存位置" |
| | | }, |
| | | task: { |
| | | taskCode: "任务号", |
New file |
| | |
| | | import React, { useState, useRef, useEffect, useMemo, useCallback } from "react"; |
| | | import { |
| | | List, |
| | | DatagridConfigurable, |
| | | SearchInput, |
| | | TopToolbar, |
| | | SelectColumnsButton, |
| | | EditButton, |
| | | FilterButton, |
| | | CreateButton, |
| | | ExportButton, |
| | | BulkDeleteButton, |
| | | WrapperField, |
| | | useRecordContext, |
| | | useTranslate, |
| | | useNotify, |
| | | useListContext, |
| | | FunctionField, |
| | | TextField, |
| | | NumberField, |
| | | DateField, |
| | | BooleanField, |
| | | ReferenceField, |
| | | TextInput, |
| | | DateTimeInput, |
| | | DateInput, |
| | | SelectInput, |
| | | NumberInput, |
| | | ReferenceInput, |
| | | ReferenceArrayInput, |
| | | AutocompleteInput, |
| | | DeleteButton, |
| | | useRefresh, |
| | | useRedirect, |
| | | Button, |
| | | useGetList, |
| | | ListContextProvider, |
| | | useList, |
| | | Toolbar, |
| | | SingleFieldList, |
| | | } from 'react-admin'; |
| | | |
| | | import { PAGE_DRAWER_WIDTH, OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting'; |
| | | import { Box, Typography, Card, Stack, DialogContent, DialogTitle, DialogActions, Dialog } from '@mui/material'; |
| | | import { styled } from '@mui/material/styles'; |
| | | import DialogCloseButton from "../../components/DialogCloseButton"; |
| | | import request from '@/utils/request'; |
| | | import { filter } from "lodash"; |
| | | import { height } from "@mui/system"; |
| | | |
| | | const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({ |
| | | '& .css-1vooibu-MuiSvgIcon-root': { |
| | | height: '.9em' |
| | | }, |
| | | '& .RaDatagrid-row': { |
| | | cursor: 'auto' |
| | | }, |
| | | '& .column-name': { |
| | | }, |
| | | '& .opt': { |
| | | width: 200 |
| | | }, |
| | | })); |
| | | |
| | | const ItemToTaskModal = (props) => { |
| | | const { open, setOpen, record } = props; |
| | | const translate = useTranslate(); |
| | | const [createDialog, setCreateDialog] = useState(false); |
| | | const [drawerVal, setDrawerVal] = useState(false); |
| | | const handleClose = (event, reason) => { |
| | | if (reason !== "backdropClick") { |
| | | setOpen(false); |
| | | } |
| | | }; |
| | | |
| | | const { data, total, isPending, error, refetch, meta } = useGetList('/wave/locs/preview', { filter: { waveId: record?.id } }); |
| | | const listContext = useList({ data, isPending }); |
| | | |
| | | return ( |
| | | <Box display="flex"> |
| | | <Dialog |
| | | open={open} |
| | | onClose={handleClose} |
| | | aria-labelledby="form-dialog-title" |
| | | aria-hidden |
| | | fullWidth |
| | | disableRestoreFocus |
| | | maxWidth="lg" |
| | | > |
| | | <DialogTitle id="form-dialog-title" sx={{ |
| | | position: 'sticky', |
| | | top: 0, |
| | | backgroundColor: 'background.paper', |
| | | zIndex: 1000 |
| | | }}> |
| | | {translate('toolbar.createTask')} |
| | | <Box sx={{ position: 'absolute', top: 8, right: 8, zIndex: 1001 }}> |
| | | <DialogCloseButton onClose={handleClose} /> |
| | | </Box> |
| | | </DialogTitle> |
| | | <DialogContent sx={{ height: 400 }}> |
| | | <ListContextProvider |
| | | value={listContext} |
| | | sx={{ |
| | | flexGrow: 1, |
| | | transition: (theme) => |
| | | theme.transitions.create(['all'], { |
| | | duration: theme.transitions.duration.enteringScreen, |
| | | }), |
| | | marginRight: !!drawerVal ? `${PAGE_DRAWER_WIDTH}px` : 0, |
| | | }} |
| | | title={"menu.waveItem"} |
| | | empty={false} |
| | | sort={{ field: "create_time", order: "desc" }} |
| | | actions={( |
| | | <TopToolbar> |
| | | <SelectColumnsButton preferenceKey='waveItem' /> |
| | | </TopToolbar> |
| | | )} |
| | | perPage={DEFAULT_PAGE_SIZE} |
| | | > |
| | | <StyledDatagrid |
| | | preferenceKey='waveItem' |
| | | bulkActionButtons={false} |
| | | rowClick={(id, resource, record) => false} |
| | | expand={false} |
| | | expandSingle={false} |
| | | omit={['id', 'createTime', 'matnrId', 'waveId', 'batch', 'orderItemId', 'unit', 'batch', 'trackCode', 'fieldsIndex', 'createBy', 'memo']} |
| | | > |
| | | <NumberField source="id" /> |
| | | <NumberField source="waveId" label="table.field.waveItem.waveId" /> |
| | | <TextField source="waveCode" label="table.field.waveItem.waveCode" /> |
| | | <TextField source="orderCode" label="table.field.waveItem.orderCode" /> |
| | | {/* <TextField source="maktx" label="table.field.waveItem.matnrName" /> */} |
| | | <NumberField source="matnrId" label="table.field.waveItem.matnrId" /> |
| | | <TextField source="matnrCode" label="table.field.waveItem.matnrCode" /> |
| | | <TextField source="batch" label="table.field.waveItem.batch" /> |
| | | <TextField source="splrBatch" label="table.field.waveItem.splrBatch" /> |
| | | <NumberField source="orderItemId" label="table.field.waveItem.orderItemId" /> |
| | | <TextField source="unit" label="table.field.waveItem.unit" /> |
| | | <TextField source="trackCode" label="table.field.waveItem.trackCode" /> |
| | | <TextField source="fieldsIndex" label="table.field.waveItem.fieldsIndex" /> |
| | | <NumberField source="anfme" label="table.field.waveItem.anfme" /> |
| | | <NumberField source="workQty" label="table.field.waveItem.workQty" /> |
| | | <NumberField source="qty" label="table.field.waveItem.qty" /> |
| | | <WrapperField cellClassName="opt" label="table.field.waveItem.stockLocs"> |
| | | <ArrayField source="stockLocs" stockLocs="table.field.waveItem.stockLocs"> |
| | | <SingleFieldList linkType={false}> |
| | | <ChilpField source="" size="small"></ChilpField> |
| | | </SingleFieldList> |
| | | </ArrayField> |
| | | {/* <NumberField source="anfme" label="table.field.waveItem.stockLocs" /> */} |
| | | {/* <EditButton sx={{ padding: '1px', fontSize: '.75rem' }} /> |
| | | <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode={OPERATE_MODE} /> */} |
| | | </WrapperField> |
| | | </StyledDatagrid> |
| | | </ListContextProvider> |
| | | </DialogContent> |
| | | <DialogActions> |
| | | <Toolbar sx={{ width: '100%', justifyContent: 'end' }} > |
| | | <GenerateTaskButton record={[record?.id]} /> |
| | | </Toolbar> |
| | | </DialogActions> |
| | | </Dialog> |
| | | </Box> |
| | | ) |
| | | } |
| | | |
| | | export default ItemToTaskModal; |
| | | |
| | | const GenerateTaskButton = (record) => { |
| | | const refresh = useRefresh(); |
| | | const notify = useNotify(); |
| | | const redirect = useRedirect(); |
| | | const generateTask = async () => { |
| | | const res = await request.post(`/wave/public/task`, { ids: record?.record }); |
| | | if (res?.data?.code === 200) { |
| | | notify(res.data.msg); |
| | | redirect("/task") |
| | | } else { |
| | | notify(res.data.msg); |
| | | } |
| | | refresh(); |
| | | } |
| | | return (<Button variant="contained" label={"ra.action.save"} onClick={generateTask}></Button>) |
| | | } |
| | |
| | | import { PAGE_DRAWER_WIDTH, OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting'; |
| | | import * as Common from '@/utils/common'; |
| | | import PublicIcon from '@mui/icons-material/Public'; |
| | | import ItemToTaskModal from "./ItemToTaskModal"; |
| | | import ConfirmButton from "../../components/ConfirmButton"; |
| | | |
| | | |
| | | const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({ |
| | |
| | | '& .column-name': { |
| | | }, |
| | | '& .opt': { |
| | | width: 200 |
| | | width: 260 |
| | | }, |
| | | })); |
| | | |
| | |
| | | <SearchInput source="condition" alwaysOn />, |
| | | <DateInput label='common.time.after' source="timeStart" alwaysOn />, |
| | | <DateInput label='common.time.before' source="timeEnd" alwaysOn />, |
| | | |
| | | <TextInput source="code" label="table.field.wave.code" />, |
| | | <SelectInput source="type" label="table.field.wave.type" |
| | | choices={[ |
| | |
| | | <NumberInput source="anfme" label="table.field.wave.anfme" />, |
| | | <NumberInput source="qty" label="table.field.wave.qty" />, |
| | | <NumberInput source="orderNum" label="table.field.wave.orderNum" />, |
| | | |
| | | <TextInput label="common.field.memo" source="memo" />, |
| | | <SelectInput |
| | | label="common.field.status" |
| | |
| | | const WaveList = () => { |
| | | const translate = useTranslate(); |
| | | const [createDialog, setCreateDialog] = useState(false); |
| | | const [detailDialog, setDetailDialog] = useState(false); |
| | | const [select, setSelectIds] = useState({}); |
| | | const [drawerVal, setDrawerVal] = useState(false); |
| | | |
| | | return ( |
| | |
| | | > |
| | | <StyledDatagrid |
| | | preferenceKey='wave' |
| | | bulkActionButtons={ |
| | | <PublicTaskButton /> |
| | | } |
| | | bulkActionButtons={false} |
| | | rowClick={(id, resource, record) => false} |
| | | expand={false} |
| | | expandSingle={false} |
| | |
| | | <BooleanField source="statusBool" label="common.field.status" sortable={false} /> |
| | | <TextField source="memo" label="common.field.memo" sortable={false} /> |
| | | <WrapperField cellClassName="opt" label="common.field.opt"> |
| | | <PublicTaskButton setSelectIds={setSelectIds} setDetailDialog={setDetailDialog} /> |
| | | <EditButton sx={{ padding: '1px', fontSize: '.75rem' }} /> |
| | | <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode={OPERATE_MODE} /> |
| | | </WrapperField> |
| | | </StyledDatagrid> |
| | | </List> |
| | | <ItemToTaskModal |
| | | open={detailDialog} |
| | | record={select} |
| | | setOpen={setDetailDialog} |
| | | /> |
| | | <WaveCreate |
| | | open={createDialog} |
| | | setOpen={setCreateDialog} |
| | |
| | | |
| | | export default WaveList; |
| | | |
| | | const PublicTaskButton = () => { |
| | | const PublicTaskButton = ({ setSelectIds, setDetailDialog }) => { |
| | | const record = useRecordContext(); |
| | | const { selectedIds, onUnselectItems } = useListContext(); |
| | | const notify = useNotify(); |
| | | const refresh = useRefresh(); |
| | | const redirect = useRedirect(); |
| | | |
| | | const pubClick = async (event) => { |
| | | event.stopPropagation(); |
| | | console.log('=========>'); |
| | | |
| | | onUnselectItems(); |
| | | const res = await request.post(`/wave/public/task`, { ids: selectedIds }); |
| | | if (res?.data?.code === 200) { |
| | | notify(res.data.msg); |
| | | redirect("/task") |
| | | } else { |
| | | notify(res.data.msg); |
| | | setSelectIds(record); |
| | | setDetailDialog(true); |
| | | } |
| | | refresh(); |
| | | } |
| | | |
| | | return ( |
| | | <Button |
| | | onClick={pubClick} |
| | | label={"toolbar.createTask"} |
| | | startIcon={<PublicIcon />} |
| | | />); |
| | | <ConfirmButton label={"toolbar.createTask"} startIcon={<PublicIcon />} onConfirm={pubClick} /> |
| | | ); |
| | | } |
| | |
| | | DateInput, |
| | | SelectInput, |
| | | NumberInput, |
| | | |
| | | Button, |
| | | } from 'react-admin'; |
| | | import { Box, Typography, Card, Stack, Drawer } from '@mui/material'; |
| | |
| | | |
| | | const filters = [ |
| | | <SearchInput source="condition" alwaysOn />, |
| | | <DateInput label='common.time.after' source="timeStart" alwaysOn />, |
| | | <DateInput label='common.time.before' source="timeEnd" alwaysOn />, |
| | | |
| | | <DateInput label='common.time.after' source="timeStart" />, |
| | | <DateInput label='common.time.before' source="timeEnd" />, |
| | | <NumberInput source="waveId" label="table.field.waveItem.waveId" />, |
| | | <TextInput source="waveCode" label="table.field.waveItem.waveCode" />, |
| | | <NumberInput source="matnrId" label="table.field.waveItem.matnrId" />, |
| | |
| | | <TextInput source="fieldsIndex" label="table.field.waveItem.fieldsIndex" />, |
| | | <NumberInput source="anfme" label="table.field.waveItem.anfme" />, |
| | | <NumberInput source="workQty" label="table.field.waveItem.workQty" />, |
| | | |
| | | <TextInput label="common.field.memo" source="memo" />, |
| | | <SelectInput |
| | | label="common.field.status" |
| | |
| | | |
| | | const WaveItemList = () => { |
| | | const translate = useTranslate(); |
| | | |
| | | const [createDialog, setCreateDialog] = useState(false); |
| | | const [drawerVal, setDrawerVal] = useState(false); |
| | | |
| | |
| | | <TextField source="fieldsIndex" label="table.field.waveItem.fieldsIndex" /> |
| | | <NumberField source="anfme" label="table.field.waveItem.anfme" /> |
| | | <NumberField source="workQty" label="table.field.waveItem.workQty" /> |
| | | |
| | | <ReferenceField source="updateBy" label="common.field.updateBy" reference="user" link={false} sortable={false}> |
| | | <TextField source="nickname" /> |
| | | </ReferenceField> |
New file |
| | |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.List; |
| | | |
| | | public class MinimalCombinationSum { |
| | | |
| | | private static List<Double> bestSolution; |
| | | private static double target; |
| | | private static double minDifference; |
| | | private static final double EPSILON = 1e-10; |
| | | |
| | | public static List<Double> findBestCombination(Double[] candidates, double targetSum) { |
| | | bestSolution = null; |
| | | target = targetSum; |
| | | minDifference = Double.MAX_VALUE; |
| | | Arrays.sort(candidates); |
| | | backtrack(candidates, 0, new ArrayList<>(), 0.0); |
| | | |
| | | // 如果没有精确解,返回最接近的近似解 |
| | | return bestSolution != null ? bestSolution : new ArrayList<>(); |
| | | } |
| | | |
| | | private static void backtrack(Double[] candidates, int start, |
| | | List<Double> current, double currentSum) { |
| | | // 计算当前和与目标的差值 |
| | | double difference = Math.abs(currentSum - target); |
| | | |
| | | // 如果找到更优解(差值更小或差值相同但组合更短) |
| | | if (difference < minDifference - EPSILON || |
| | | (Math.abs(difference - minDifference) < EPSILON && |
| | | (bestSolution == null || current.size() < bestSolution.size()))) { |
| | | minDifference = difference; |
| | | bestSolution = new ArrayList<>(current); |
| | | } |
| | | |
| | | // 如果已经找到精确解,不需要继续搜索更长的组合 |
| | | if (minDifference < EPSILON) { |
| | | return; |
| | | } |
| | | |
| | | // 遍历候选数字 |
| | | for (int i = start; i < candidates.length; i++) { |
| | | double num = candidates[i]; |
| | | |
| | | // 剪枝:如果当前和已经远大于目标值,跳过 |
| | | if (currentSum + num > target + minDifference + EPSILON) { |
| | | continue; |
| | | } |
| | | |
| | | // 跳过重复数字 |
| | | if (i > start && Math.abs(candidates[i] - candidates[i - 1]) < EPSILON) { |
| | | continue; |
| | | } |
| | | |
| | | current.add(num); |
| | | backtrack(candidates, i + 1, current, currentSum + num); |
| | | current.remove(current.size() - 1); |
| | | } |
| | | } |
| | | |
| | | public static void main(String[] args) { |
| | | Double[] candidates1 = {1.5, 2.3, 3.1, 4.7}; |
| | | double target1 = 3.8; |
| | | System.out.println("候选数字: " + Arrays.toString(candidates1)); |
| | | System.out.println("目标值: " + target1); |
| | | List<Double> result1 = findBestCombination(candidates1, target1); |
| | | printResult(result1, target1); |
| | | |
| | | // Double[] candidates2 = {0.8, 1.2, 1.7, 2.5}; |
| | | // double target2 = 3.9; |
| | | // System.out.println("\n候选数字: " + Arrays.toString(candidates2)); |
| | | // System.out.println("目标值: " + target2); |
| | | // List<Double> result2 = findBestCombination(candidates2, target2); |
| | | // printResult(result2, target2); |
| | | } |
| | | |
| | | private static void printResult(List<Double> result, double target) { |
| | | if (result.isEmpty()) { |
| | | System.out.println("无解"); |
| | | } else { |
| | | double sum = result.stream().mapToDouble(Double::doubleValue).sum(); |
| | | if (Math.abs(sum - target) < EPSILON) { |
| | | System.out.printf("精确解: %s (和=%.2f)\n", result, sum); |
| | | } else { |
| | | System.out.printf("近似解: %s (和=%.2f, 与目标相差: %.2f)\n", |
| | | result, sum, sum - target); |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | itemList.forEach(asnOrderItem -> { |
| | | LocItem item = new LocItem(); |
| | | BeanUtils.copyProperties(asnOrderItem, item); |
| | | item.setId(loc.getId()) |
| | | item.setLocId(loc.getId()) |
| | | .setId(null) |
| | | .setLocCode(loc.getCode()) |
| | | .setOrderId(order.getId()) |
| | | .setOrderItemId(asnOrderItem.getId()) |
| | | .setWkType(Short.parseShort(order.getWkType())) |
| | |
| | | } |
| | | //获取当前库存信息 |
| | | LocItem stockItem = locItemService.getOne(new LambdaQueryWrapper<LocItem>() |
| | | .eq(LocItem::getOrderItemId, asnOrderItem.getId()) |
| | | // .eq(LocItem::getOrderItemId, asnOrderItem.getId()) |
| | | .eq(LocItem::getFieldsIndex, asnOrderItem.getFieldsIndex()) |
| | | .eq(LocItem::getBatch, asnOrderItem.getBatch()) |
| | | .eq(LocItem::getMatnrId, asnOrderItem.getMatnrId())); |
| | | //SET 当前库存数量 |
| | |
| | | package com.vincent.rsf.server.manager.controller; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.vincent.rsf.framework.common.Cools; |
| | | import com.vincent.rsf.framework.common.R; |
| | | import com.vincent.rsf.framework.exception.CoolException; |
| | | import com.vincent.rsf.server.common.domain.PageResult; |
| | | import com.vincent.rsf.server.common.utils.ExcelUtil; |
| | | import com.vincent.rsf.server.common.annotation.OperationLog; |
| | | import com.vincent.rsf.server.common.domain.BaseParam; |
| | | import com.vincent.rsf.server.common.domain.KeyValVo; |
| | | import com.vincent.rsf.server.common.domain.PageParam; |
| | | import com.vincent.rsf.server.manager.entity.Wave; |
| | | import com.vincent.rsf.server.manager.entity.WaveItem; |
| | | import com.vincent.rsf.server.manager.service.WaveService; |
| | | import com.vincent.rsf.server.system.controller.BaseController; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import org.apache.commons.lang3.StringUtils; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.security.access.prepost.PreAuthorize; |
| | | import org.springframework.web.bind.annotation.*; |
| | |
| | | } |
| | | |
| | | |
| | | |
| | | @PreAuthorize("hasAuthority('manager:wave:update')") |
| | | @ApiOperation("波次下发任务") |
| | | @PostMapping("/wave/public/task") |
| | |
| | | return waveService.publicTask(map); |
| | | } |
| | | |
| | | @PreAuthorize("hasAuthority('manager:wave:list')") |
| | | @ApiOperation("波次出库任务预览") |
| | | @PostMapping("/wave/locs/preview/page") |
| | | public R mergeWavePreview(@RequestBody Map<String, Object> map) { |
| | | if (Cools.isEmpty(map.get("waveId")) || StringUtils.isBlank(map.get("waveId").toString())) { |
| | | throw new CoolException("参数不能为空!!"); |
| | | } |
| | | Long waveId = Long.parseLong(map.get("waveId").toString()); |
| | | List<WaveItem> waveItems = waveService.mergeWavePreview(waveId); |
| | | PageResult<WaveItem> pageResult = new PageResult<>(); |
| | | pageResult.setRecords(waveItems).setTotal(Long.parseLong(waveItems.size() + "")); |
| | | return R.ok().add(pageResult); |
| | | } |
| | | |
| | | } |
| | |
| | | @ApiModelProperty(value= "主单ID") |
| | | private Long locId; |
| | | |
| | | @ApiModelProperty("库位编码") |
| | | private String locCode; |
| | | |
| | | /** |
| | | * 单据ID |
| | | */ |
| | |
| | | @ApiModelProperty("执行数量") |
| | | private Double workQty; |
| | | |
| | | @ApiModelProperty("完成数量") |
| | | private Double qty; |
| | | |
| | | /** |
| | | * 库存批次 |
| | | */ |
| | |
| | | @ApiModelProperty(value= "波次号") |
| | | private String code; |
| | | |
| | | @ApiModelProperty("执行数量") |
| | | private Double workQty; |
| | | |
| | | /** |
| | | * 波次类型 0: 手动 1: 自动 |
| | | */ |
| | |
| | | package com.vincent.rsf.server.manager.entity; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.TableLogic; |
| | | import com.baomidou.mybatisplus.annotation.*; |
| | | |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.Date; |
| | | |
| | |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.Date; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableLogic; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | |
| | | @ApiModelProperty(value= "修改人员") |
| | | private Long updateBy; |
| | | |
| | | @ApiModelProperty("目标库位") |
| | | @TableField(exist = false) |
| | | private String stockLocs; |
| | | |
| | | /** |
| | | * 备注 |
| | | */ |
| | |
| | | ASN_EXCE_STATUS_TASK_CLOSE("4", "已关闭"), |
| | | OUT_STOCK_STATUS_TASK_INIT("5", "初始化"), |
| | | OUT_STOCK_STATUS_TASK_EXCE("6", "待处理"), |
| | | |
| | | OUT_STOCK_STATUS_TASK_WAVE("7", "生成波次"), |
| | | |
| | | OUT_STOCK_STATUS_TASK_WORKING("8", "作业中") |
| | | ; |
| | | AsnExceStatus(String val, String desc) { |
| | |
| | | import com.baomidou.mybatisplus.extension.service.IService; |
| | | import com.vincent.rsf.framework.common.R; |
| | | import com.vincent.rsf.server.manager.entity.Wave; |
| | | import com.vincent.rsf.server.manager.entity.WaveItem; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | public interface WaveService extends IService<Wave> { |
| | |
| | | * @time 2025/4/25 16:24 |
| | | */ |
| | | R publicTask(Map<String, Object> map); |
| | | |
| | | /** |
| | | * @author Ryan |
| | | * @description 预览波次下发任务 |
| | | * @param |
| | | * @return |
| | | * @time 2025/4/27 11:08 |
| | | */ |
| | | List<WaveItem> mergeWavePreview(Long waveId); |
| | | } |
| | |
| | | import com.vincent.rsf.server.manager.enums.WaveExceStatus; |
| | | import com.vincent.rsf.server.manager.mapper.AsnOrderMapper; |
| | | import com.vincent.rsf.server.manager.service.*; |
| | | import com.vincent.rsf.server.manager.utils.GroupMergeUtil; |
| | | import com.vincent.rsf.server.manager.utils.OptimalAlgorithmUtil; |
| | | import com.vincent.rsf.server.system.constant.SerialRuleCode; |
| | | import com.vincent.rsf.server.system.utils.SerialRuleUtils; |
| | | import org.apache.commons.lang3.StringUtils; |
| | |
| | | .setWaveCode(wave.getCode()); |
| | | items.add(item); |
| | | }); |
| | | List<WaveItem> waveItems = GroupMergeUtil.groupAndMerge(items, |
| | | List<WaveItem> waveItems = OptimalAlgorithmUtil.groupAndMerge(items, |
| | | (p1, p2) -> new WaveItem( |
| | | p1.getWaveId(), |
| | | p1.getWaveCode(), |
| | |
| | | Loc loc = locService.getOne(new LambdaQueryWrapper<Loc>().eq(Loc::getCode, task.getTargLoc()), false); |
| | | LocItem item = new LocItem(); |
| | | BeanUtils.copyProperties(taskItem, item); |
| | | item.setLocId(loc.getId()).setType(taskItem.getOrderType()); |
| | | item.setLocCode(loc.getCode()).setId(null).setLocId(loc.getId()).setType(taskItem.getOrderType()); |
| | | locItems.add(item); |
| | | }); |
| | | if (!locItemService.saveBatch(locItems)) { |
| | |
| | | package com.vincent.rsf.server.manager.service.impl; |
| | | |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
| | | import com.vincent.rsf.framework.common.Cools; |
| | | import com.vincent.rsf.framework.common.R; |
| | | import com.vincent.rsf.framework.exception.CoolException; |
| | | import com.vincent.rsf.server.manager.entity.AsnOrder; |
| | | import com.vincent.rsf.server.manager.entity.AsnOrderItem; |
| | | import com.vincent.rsf.server.manager.entity.LocItem; |
| | | import com.vincent.rsf.server.manager.entity.WaveItem; |
| | | import com.vincent.rsf.server.manager.enums.AsnExceStatus; |
| | | import com.vincent.rsf.server.manager.enums.WaveExceStatus; |
| | | import com.vincent.rsf.server.manager.mapper.WaveMapper; |
| | | import com.vincent.rsf.server.manager.entity.Wave; |
| | | import com.vincent.rsf.server.manager.service.AsnOrderItemService; |
| | | import com.vincent.rsf.server.manager.service.AsnOrderService; |
| | | import com.vincent.rsf.server.manager.service.WaveService; |
| | | import com.vincent.rsf.server.manager.service.*; |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import com.vincent.rsf.server.manager.utils.OptimalAlgorithmUtil; |
| | | import org.apache.commons.lang3.StringUtils; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.stream.Collectors; |
| | | |
| | | @Service("waveService") |
| | | public class WaveServiceImpl extends ServiceImpl<WaveMapper, Wave> implements WaveService { |
| | | |
| | | @Autowired |
| | | private AsnOrderItemService asnOrderItemService; |
| | | |
| | | @Autowired |
| | | private AsnOrderService asnOrderService; |
| | | @Autowired |
| | | private WaveItemService waveItemService; |
| | | @Autowired |
| | | private TaskService taskService; |
| | | @Autowired |
| | | private TaskItemService taskItemService; |
| | | @Autowired |
| | | private LocItemService locItemService; |
| | | |
| | | /** |
| | | * @author Ryan |
| | | * @description 波次任务下发 |
| | | * @param |
| | | * @return |
| | | * @author Ryan |
| | | * @description 波次任务下发 |
| | | * @time 2025/4/25 16:24 |
| | | */ |
| | | @Override |
| | |
| | | if (Objects.isNull(waves) || waves.isEmpty()) { |
| | | throw new CoolException("波次数据不存在!!"); |
| | | } |
| | | List<Long> list = waves.stream().map(Wave::getId).collect(Collectors.toList()); |
| | | List<WaveItem> waveItems = waveItemService.list(new LambdaQueryWrapper<WaveItem>().in(WaveItem::getWaveId, list)); |
| | | if (waveItems.isEmpty()) { |
| | | throw new CoolException("波次明细不存在!!"); |
| | | } |
| | | List<Long> orderIds = waveItems.stream().map(WaveItem::getOrderId).collect(Collectors.toList()); |
| | | |
| | | List<AsnOrder> orders = asnOrderService.list(new LambdaQueryWrapper<AsnOrder>().eq(AsnOrder::getId, ids)); |
| | | |
| | | |
| | | asnOrderItemService.list(new LambdaQueryWrapper<AsnOrderItem>().eq(AsnOrderItem::getAsnId, ids)); |
| | | |
| | | /**查询每条明细匹配的库位*/ |
| | | try { |
| | | List<WaveItem> items = getLocs(waveItems); |
| | | } catch (Exception e) { |
| | | throw new CoolException("库位获取失败!!!"); |
| | | } |
| | | //TODO 1. 根据波次明细生成出库任务 |
| | | // 2. 根据物料SKU寻找符合物料库位 {1. 根据物料编码,批次,动态字段 查询符合的库位,再根据库位中物料的数量选择最适合的库位 2. 判断当前订单是全拖出库还是拣料入库} |
| | | // 3. 修改主单、波次执行数量 |
| | | // 4. 判断全仓出库或拣料出库 |
| | | List<AsnOrder> orders = asnOrderService.list(new LambdaQueryWrapper<AsnOrder>().in(AsnOrder::getId, orderIds)); |
| | | /**修改原出库单状态*/ |
| | | if (!asnOrderService.update(new LambdaQueryWrapper<AsnOrder>() |
| | | .eq(AsnOrder::getExceStatus, AsnExceStatus.OUT_STOCK_STATUS_TASK_WORKING.val) |
| | | .in(AsnOrder::getId, orders))) { |
| | | throw new CoolException("出库单据状态修改失败!!"); |
| | | } |
| | | if (!this.update(new LambdaUpdateWrapper<Wave>().set(Wave::getExceStatus, WaveExceStatus.WAVE_EXCE_STATUS_TASK).in(Wave::getId,ids))) { |
| | | throw new CoolException("波次状态修改失败!!"); |
| | | } |
| | | |
| | | return R.ok(); |
| | | } |
| | | |
| | | /** |
| | | * @param |
| | | * @return |
| | | * @author Ryan |
| | | * @description 预览波次下发任务 |
| | | * @time 2025/4/27 11:09 |
| | | */ |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public List<WaveItem> mergeWavePreview(Long waveId) { |
| | | Wave wave = this.getById(waveId); |
| | | if (Objects.isNull(wave)) { |
| | | throw new CoolException("波次不能存在!!"); |
| | | } |
| | | List<WaveItem> waveItems = waveItemService.list(new LambdaQueryWrapper<WaveItem>().eq(WaveItem::getWaveId, waveId)); |
| | | if (waveItems.isEmpty()) { |
| | | throw new CoolException("波次明细不存在!!"); |
| | | } |
| | | List<WaveItem> itemPreview = null; |
| | | try { |
| | | itemPreview = getLocs(waveItems); |
| | | } catch (Exception e) { |
| | | throw new CoolException("库位获取失败!!!!"); |
| | | } |
| | | return itemPreview; |
| | | } |
| | | |
| | | /** |
| | | * @param |
| | | * @param waveItems |
| | | * @return |
| | | * @author Ryan |
| | | * @description 根据物料编码,批次,动态字段 查询符合的库位,再根据库位中物料的数量选择最适合的库位 |
| | | * @time 2025/4/27 09:26 |
| | | */ |
| | | private synchronized List<WaveItem> getLocs(List<WaveItem> waveItems) throws Exception { |
| | | //TODO 根据物料编码,批次,动态字段 查询符合的库位,再根据库位中物料的数量选择最适合的库位 |
| | | waveItems.forEach(waveItem -> { |
| | | List<LocItem> locItems = locItemService.list(new QueryWrapper<LocItem>() |
| | | .select("id", "loc_id", "loc_code", "order_id", "SUM(anfme) anfme", "SUM(work_qty) work_qty", "splr_batch", "fields_index", "matnr_code") |
| | | .lambda() |
| | | .eq(LocItem::getMatnrCode, waveItem.getMatnrCode()) |
| | | .eq(LocItem::getSplrBatch, waveItem.getSplrBatch()) |
| | | .eq(StringUtils.isNotBlank(waveItem.getFieldsIndex()), LocItem::getFieldsIndex, waveItem.getFieldsIndex()) |
| | | .groupBy(LocItem::getMatnrCode, LocItem::getSplrBatch, LocItem::getFieldsIndex, LocItem::getId)); |
| | | |
| | | Double[] doubles = locItems.stream().map(LocItem::getAnfme).toArray(Double[]::new); |
| | | List<Double> result = OptimalAlgorithmUtil.findBestCombination(doubles, waveItem.getAnfme()); |
| | | |
| | | |
| | | String locs = JSONArray.toJSONString(new ArrayList<>()); |
| | | |
| | | if (!locItems.isEmpty()) { |
| | | List<String> codes = locItems.stream().map(LocItem::getLocCode).collect(Collectors.toList()); |
| | | locs = JSONArray.toJSONString(codes); |
| | | } |
| | | waveItem.setStockLocs(locs); |
| | | }); |
| | | return waveItems; |
| | | } |
| | | } |
New file |
| | |
| | | package com.vincent.rsf.server.manager.utils; |
| | | |
| | | import java.util.*; |
| | | import java.util.function.BinaryOperator; |
| | | import java.util.function.Function; |
| | | import java.util.stream.Collectors; |
| | | |
| | | /** |
| | | * @author Ryan |
| | | * @description 根据对象多个属性合并处理 |
| | | * @param |
| | | * @return |
| | | * @time 2025/4/25 10:55 |
| | | */ |
| | | public class OptimalAlgorithmUtil { |
| | | |
| | | private static List<Double> bestSolution; |
| | | private static double target; |
| | | private static double minDifference; |
| | | private static final double EPSILON = 1e-10; |
| | | |
| | | /** |
| | | * 根据多个属性分组合并对象列表 |
| | | * |
| | | * @param list 待分组的对象列表 |
| | | * @param mergers 合并函数,定义如何合并同一组中的对象 |
| | | * @param keyGetters 分组属性的getter方法数组 |
| | | * @param <T> 对象类型 |
| | | * @param <K> 分组键类型 |
| | | * @return 分组合并后的对象列表 |
| | | */ |
| | | @SafeVarargs |
| | | public static <T, K> List<T> groupAndMerge( |
| | | List<T> list, |
| | | BinaryOperator<T> mergers, |
| | | Function<T, K>... keyGetters) { |
| | | |
| | | return new ArrayList<>(list.stream() |
| | | .collect(Collectors.toMap( |
| | | item -> new GroupingKeys<>(Arrays.stream(keyGetters) |
| | | .map(getter -> getter.apply(item)) |
| | | .toArray()), |
| | | Function.identity(), |
| | | mergers)) |
| | | .values()); |
| | | } |
| | | |
| | | /** |
| | | * @author Ryan |
| | | * @description 用于存储多个分组键的内部类 |
| | | * @param |
| | | * @return |
| | | * @time 2025/4/25 10:56 |
| | | */ |
| | | private static class GroupingKeys<K> { |
| | | private final Object[] keys; |
| | | private final int hashCode; |
| | | |
| | | public GroupingKeys(Object[] keys) { |
| | | this.keys = keys; |
| | | this.hashCode = Arrays.deepHashCode(keys); |
| | | } |
| | | |
| | | @Override |
| | | public boolean equals(Object o) { |
| | | if (this == o) { |
| | | return true; |
| | | } |
| | | if (o == null || getClass() != o.getClass()) { |
| | | return false; |
| | | } |
| | | GroupingKeys<?> that = (GroupingKeys<?>) o; |
| | | return Arrays.deepEquals(keys, that.keys); |
| | | } |
| | | |
| | | @Override |
| | | public int hashCode() { |
| | | return hashCode; |
| | | } |
| | | } |
| | | |
| | | |
| | | public static List<Double> findBestCombination(Double[] candidates, double targetSum) { |
| | | bestSolution = null; |
| | | target = targetSum; |
| | | minDifference = Double.MAX_VALUE; |
| | | Arrays.sort(candidates); |
| | | backtrack(candidates, 0, new ArrayList<>(), 0.0); |
| | | |
| | | // 如果没有精确解,返回最接近的近似解 |
| | | return bestSolution != null ? bestSolution : new ArrayList<>(); |
| | | } |
| | | |
| | | private static void backtrack(Double[] candidates, int start, |
| | | List<Double> current, double currentSum) { |
| | | // 计算当前和与目标的差值 |
| | | double difference = Math.abs(currentSum - target); |
| | | |
| | | // 如果找到更优解(差值更小或差值相同但组合更短) |
| | | if (difference < minDifference - EPSILON || |
| | | (Math.abs(difference - minDifference) < EPSILON && |
| | | (bestSolution == null || current.size() < bestSolution.size()))) { |
| | | minDifference = difference; |
| | | bestSolution = new ArrayList<>(current); |
| | | } |
| | | |
| | | // 如果已经找到精确解,不需要继续搜索更长的组合 |
| | | if (minDifference < EPSILON) { |
| | | return; |
| | | } |
| | | |
| | | // 遍历候选数字 |
| | | for (int i = start; i < candidates.length; i++) { |
| | | double num = candidates[i]; |
| | | |
| | | // 剪枝:如果当前和已经远大于目标值,跳过 |
| | | if (currentSum + num > target + minDifference + EPSILON) { |
| | | continue; |
| | | } |
| | | |
| | | // 跳过重复数字 |
| | | if (i > start && Math.abs(candidates[i] - candidates[i - 1]) < EPSILON) { |
| | | continue; |
| | | } |
| | | |
| | | current.add(num); |
| | | backtrack(candidates, i + 1, current, currentSum + num); |
| | | current.remove(current.size() - 1); |
| | | } |
| | | } |
| | | |
| | | } |