diff --git a/docs/superpowers/plans/2026-08-14-lms-daily-plan.md b/docs/superpowers/plans/2026-08-14-lms-daily-plan.md index d7c4f42a..0cfe74a5 100644 --- a/docs/superpowers/plans/2026-08-14-lms-daily-plan.md +++ b/docs/superpowers/plans/2026-08-14-lms-daily-plan.md @@ -426,7 +426,7 @@ git commit -m "feat: 增加LMS日计划管理接口" - [ ] **步骤 4:实现开始订单和详情** -开始订单弹窗展示当前数量并允许填写追加数量。详情抽屉展示三类状态时间和按时间倒序的操作记录。 +开始订单弹窗展示原计划量、确认计划量、已有追加、本次追加和开始后总量;确认计划量为必填正整数,ERP 与人工来源都可在开始时确认。服务端在同一事务内原子更新计划量、累计追加量和订单状态,并分别记录数量调整、追加和开始日志。管理页为未开始订单提供填写原因的取消入口。详情抽屉展示三类状态时间和按时间倒序的操作记录。 - [ ] **步骤 5:运行局部 ESLint** diff --git a/docs/superpowers/specs/2026-08-14-lms-daily-plan-design.md b/docs/superpowers/specs/2026-08-14-lms-daily-plan-design.md index 9ca74eb3..7cec148c 100644 --- a/docs/superpowers/specs/2026-08-14-lms-daily-plan-design.md +++ b/docs/superpowers/specs/2026-08-14-lms-daily-plan-design.md @@ -31,6 +31,7 @@ - 人工新增日计划。 - 编辑未开始的人工日计划。 - 对未开始订单执行“开始订单”。 +- 对未开始订单执行“取消订单”,填写原因后可从作业页恢复。 - 已结束订单只在本页面查询,不进入作业页面。 查询条件在桌面端一行多项展示,包括日计划编号或 ERP 订单号、计划日期范围、管芯编码或名称、来源和订单状态。 @@ -136,7 +137,7 @@ ERP 推送不允许直接修改顺序、权重、当前完成数量或作业状 人工创建字段包括计划日期、人工订单号、管芯物料、计划数量、装箱信息、顺序和权重。人工订单号允许手填;未填写时使用 LMS 日计划编号作为订单号。 -只有未开始的人工来源日计划允许编辑基本信息。ERP 来源计划通过 ERP 幂等推送维护,管理页面只读。开始订单时允许再次确认计划数量,并可填写追加数量;开始成功后进入日计划作业页面。 +只有未开始的人工来源日计划允许编辑基本信息。ERP 来源计划通过 ERP 幂等推送维护,管理页面只读。开始订单时 ERP 和人工来源均允许再次确认计划数量,并可填写追加数量;计划数量、累计追加数量和开始状态在同一事务中原子更新,开始成功后进入日计划作业页面。 追加数量仅允许订单进行中时执行,必须大于零,按累计值保存并记录每次追加明细。 diff --git a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/dailyplan/vo/DailyPlanStartReqVO.java b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/dailyplan/vo/DailyPlanStartReqVO.java index 40f9eeda..5541e1f0 100644 --- a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/dailyplan/vo/DailyPlanStartReqVO.java +++ b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/admin/dailyplan/vo/DailyPlanStartReqVO.java @@ -9,6 +9,10 @@ import lombok.EqualsAndHashCode; @Data @EqualsAndHashCode(callSuper = true) public class DailyPlanStartReqVO extends DailyPlanVersionReqVO { + @NotNull(message = "确认计划数量不能为空") + @Min(value = 1, message = "确认计划数量必须为正整数") + private Integer planQty; + @NotNull(message = "追加数量不能为空") @Min(value = 0, message = "追加数量不能小于0") private Integer appendQty; diff --git a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/dailyplan/DailyPlanMapper.java b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/dailyplan/DailyPlanMapper.java index 3091da39..4fe6dfe1 100644 --- a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/dailyplan/DailyPlanMapper.java +++ b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/dailyplan/DailyPlanMapper.java @@ -94,6 +94,9 @@ public interface DailyPlanMapper extends BaseMapperX { */ int updateStateWithVersion(@Param("plan") DailyPlanDO plan); + /** 原子确认数量并开始未开始订单。 */ + int startOrderWithVersion(@Param("plan") DailyPlanDO plan); + /** * 乐观锁恢复已取消的订单状态,并清空前一订单状态。 */ diff --git a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanServiceImpl.java b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanServiceImpl.java index 788cd21d..1e4ab7a8 100644 --- a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanServiceImpl.java +++ b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanServiceImpl.java @@ -359,19 +359,32 @@ public class DailyPlanServiceImpl implements DailyPlanService { checkAffected(dailyPlanMapper.updateNotStartedPlan(update)); } - /** 开始订单并写入追加数量。 */ + /** 开始订单,同时确认计划数量并写入本次追加数量。 */ @Override @Transactional(rollbackFor = Exception.class) public void startOrder(DailyPlanStartReqVO reqVO) { DailyPlanDO current = validateVersion(reqVO.getId(), reqVO.getVersion()); validateOrderStatus(current, DailyPlanOrderStatusEnum.NOT_STARTED, "开始订单"); DailyPlanDO update = newUpdate(current); + int confirmedPlanQty = reqVO.getPlanQty(); + int appendQty = safeAddQuantity(current.getAppendQty(), reqVO.getAppendQty()); + safeAddQuantity(confirmedPlanQty, appendQty); // 校验开始后的需求总量不会溢出。 + update.setPlanQty(confirmedPlanQty); update.setOrderStatus(DailyPlanOrderStatusEnum.IN_PROGRESS.getCode()); update.setOrderStartTime(LocalDateTime.now()); - update.setAppendQty(safeAddQuantity(current.getAppendQty(), reqVO.getAppendQty())); - checkAffected(dailyPlanMapper.updateStateWithVersion(update)); + update.setAppendQty(appendQty); + checkAffected(dailyPlanMapper.startOrderWithVersion(update)); + if (!current.getPlanQty().equals(confirmedPlanQty)) { + insertLog(current.getDailyPlanId(), TARGET_QUANTITY, TYPE_UPDATE, + String.valueOf(current.getPlanQty()), String.valueOf(confirmedPlanQty), + confirmedPlanQty - current.getPlanQty(), null); + } + if (reqVO.getAppendQty() > 0) { + insertLog(current.getDailyPlanId(), TARGET_QUANTITY, TYPE_APPEND, + String.valueOf(current.getAppendQty()), String.valueOf(appendQty), reqVO.getAppendQty(), null); + } insertLog(current.getDailyPlanId(), TARGET_ORDER, TYPE_START, String.valueOf(current.getOrderStatus()), - String.valueOf(update.getOrderStatus()), reqVO.getAppendQty(), null); + String.valueOf(update.getOrderStatus()), null, null); } /** 追加进行中订单的数量。 */ diff --git a/nl-module-lms/nl-module-lms-server/src/main/resources/mapper/dailyplan/DailyPlanMapper.xml b/nl-module-lms/nl-module-lms-server/src/main/resources/mapper/dailyplan/DailyPlanMapper.xml index ae69906e..a2c7987f 100644 --- a/nl-module-lms/nl-module-lms-server/src/main/resources/mapper/dailyplan/DailyPlanMapper.xml +++ b/nl-module-lms/nl-module-lms-server/src/main/resources/mapper/dailyplan/DailyPlanMapper.xml @@ -246,6 +246,20 @@ AND deleted = b'0' + + UPDATE lms_daily_plan + SET plan_qty = #{plan.planQty}, + append_qty = #{plan.appendQty}, + order_status = 1, + order_start_time = #{plan.orderStartTime}, + updater = #{plan.updater}, + version = version + 1 + WHERE daily_plan_id = #{plan.dailyPlanId} + AND version = #{plan.version} + AND order_status = 0 + AND deleted = b'0' + + UPDATE lms_daily_plan SET sort_seq = #{sortSeq}, updater = #{updater}, version = version + 1 diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/lms/dailyplan/index.ts b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/lms/dailyplan/index.ts index faece6a9..7cb193ae 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/lms/dailyplan/index.ts +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/api/lms/dailyplan/index.ts @@ -84,6 +84,7 @@ export namespace LmsDailyPlanApi { export interface StartReq extends VersionReq { appendQty: number; + planQty: number; } export interface AppendReq extends VersionReq { diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/daily-plan/management/index.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/daily-plan/management/index.vue index 5f63735d..0e1307f7 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/daily-plan/management/index.vue +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/daily-plan/management/index.vue @@ -7,6 +7,7 @@ import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { getDailyPlanManagementPage } from '#/api/lms/dailyplan'; +import Operation from '../work/modules/operation.vue'; import { useGridColumns, useGridFormSchema } from './data'; import Detail from './modules/detail.vue'; import Form from './modules/form.vue'; @@ -27,6 +28,10 @@ const [StartOrderModal, startOrderModalApi] = useVbenModal({ connectedComponent: StartOrder, destroyOnClose: true, }); +const [OperationModal, operationModalApi] = useVbenModal({ + connectedComponent: Operation, + destroyOnClose: true, +}); function handleRefresh() { gridApi.query(); @@ -48,6 +53,10 @@ function handleStart(row: LmsDailyPlanApi.DailyPlan) { startOrderModalApi.setData(row).open(); } +function handleCancel(row: LmsDailyPlanApi.DailyPlan) { + operationModalApi.setData({ row, type: 'cancelOrder' }).open(); +} + const [Grid, gridApi] = useVbenVxeGrid({ formOptions: { schema: useGridFormSchema(), @@ -82,6 +91,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ + diff --git a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/daily-plan/management/modules/start-order.vue b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/daily-plan/management/modules/start-order.vue index 55004064..9b57f997 100644 --- a/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/daily-plan/management/modules/start-order.vue +++ b/nl-ui/nl-ui-admin-vben/yudao-ui-admin-vben/apps/web-antdv-next/src/views/lms/daily-plan/management/modules/start-order.vue @@ -11,12 +11,17 @@ import { startDailyPlanOrder } from '#/api/lms/dailyplan'; const emit = defineEmits(['success']); const row = ref(); +const planQty = ref(1); const appendQty = ref(0); -const totalAfterStart = computed(() => (row.value?.totalQty ?? 0) + appendQty.value); +const totalAfterStart = computed(() => planQty.value + (row.value?.appendQty ?? 0) + appendQty.value); const [Modal, modalApi] = useVbenModal({ async onConfirm() { if (!row.value) return; + if (!Number.isInteger(planQty.value) || planQty.value <= 0) { + message.warning('确认计划数量必须为正整数'); + return; + } if (!Number.isInteger(appendQty.value) || appendQty.value < 0) { message.warning('追加数量必须为不小于 0 的整数'); return; @@ -26,6 +31,7 @@ const [Modal, modalApi] = useVbenModal({ await startDailyPlanOrder({ id: row.value.dailyPlanId, version: row.value.version, + planQty: planQty.value, appendQty: appendQty.value, }); await modalApi.close(); @@ -38,10 +44,12 @@ const [Modal, modalApi] = useVbenModal({ onOpenChange(isOpen: boolean) { if (!isOpen) { row.value = undefined; + planQty.value = 1; appendQty.value = 0; return; } row.value = modalApi.getData(); + planQty.value = row.value.planQty; appendQty.value = 0; }, }); @@ -52,7 +60,10 @@ const [Modal, modalApi] = useVbenModal({ {{ row.planCode }} {{ row.materialCode }} / {{ row.materialName }} - {{ row.planQty }} + {{ row.planQty }} + + + {{ row.appendQty }} diff --git a/sql/mysql/lms_daily_plan_menu.sql b/sql/mysql/lms_daily_plan_menu.sql index 32ae6424..1ee9b71c 100644 --- a/sql/mysql/lms_daily_plan_menu.sql +++ b/sql/mysql/lms_daily_plan_menu.sql @@ -56,6 +56,7 @@ CROSS JOIN ( UNION ALL SELECT 4, '日计划启动', 'lms:daily-plan:start' UNION ALL SELECT 5, '日计划排程', 'lms:daily-plan:schedule' UNION ALL SELECT 6, '日计划策略', 'lms:daily-plan:strategy' + UNION ALL SELECT 7, '日计划操作', 'lms:daily-plan:operate' ) permission WHERE menu.id = COALESCE( (SELECT fixed.id FROM system_menu fixed WHERE fixed.id=2093000000000000100 AND fixed.deleted=b'0'), @@ -63,7 +64,10 @@ WHERE menu.id = COALESCE( WHERE fallback.component='lms/daily-plan/management/index' AND fallback.deleted=b'0') ) AND menu.deleted=b'0' - AND NOT EXISTS (SELECT 1 FROM system_menu existing WHERE existing.permission=permission.code AND existing.deleted=b'0'); + AND NOT EXISTS ( + SELECT 1 FROM system_menu existing + WHERE existing.parent_id=menu.id AND existing.permission=permission.code AND existing.deleted=b'0' + ); -- 作业页面独立挂载自身实际使用的按钮权限,避免仅授权作业菜单时按钮全部不可见。 INSERT INTO system_menu