浏览代码

feat: 产线日历/休息/节假日修复及工单看板搜索提示 (Web 2.4.186 / Server 1.0.168)

- 产线工作日历: 修复实体映射CLR异常(IsWorkDay/AllowOverTime类型不匹配)

- 产线休息时间: 修复实体映射(RestTime decimal/Period string/Line nullable)

- 产线休息时间: 新增重复校验(同产线+同RestTimePoint提示重叠)

- 产线节假日: 新增日期重复校验(同日期提示已配置节假日)

- 产线工作日历表单: shiftsStart时间字符串转decimal提交

- 可执行日计划: 计划日期列改造及默认未来3天范围

- 工单执行进度看板: 搜索无结果时提示信息

- 发货单列表: 已发货子表隐藏删除按钮

- 工单工序排产: 生产排程传tenantId/导出/同步工艺路线校验
Pengxy 20 小时之前
父节点
当前提交
4e61f1377f
共有 23 个文件被更改,包括 383 次插入112 次删除
  1. 1 1
      Web/package.json
  2. 1 0
      Web/src/views/aidop/api/asnShipper.ts
  3. 1 1
      Web/src/views/aidop/api/qualityLineRest.ts
  4. 1 1
      Web/src/views/aidop/api/shopCalendarWorkCtr.ts
  5. 5 1
      Web/src/views/aidop/business/asnShipperList.vue
  6. 39 10
      Web/src/views/aidop/production/executableDailyPlanList.vue
  7. 17 2
      Web/src/views/aidop/production/shopCalendarWorkCtrForm.vue
  8. 3 0
      Web/src/views/aidop/production/workOrderProgressDashboardList.vue
  9. 72 8
      Web/src/views/aidop/production/workOrderSchedulingList.vue
  10. 二进制
      doc/S1产销协同_UAT测试报告v1.0.xlsx
  11. 二进制
      doc/S2制造协同_UAT测试报告v1.0.xlsx
  12. 3 24
      server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj
  13. 16 16
      server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0MaterialsController.cs
  14. 16 16
      server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0ItemMaster.cs
  15. 8 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Order/AsnShipperService.cs
  16. 5 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Production/Dto/ExecutableDailyPlanDto.cs
  17. 7 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Production/ExecutableDailyPlanService.cs
  18. 17 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Production/HolidayMasterService.cs
  19. 23 5
      server/Plugins/Admin.NET.Plugin.AiDOP/Production/QualityLineWorkDetailService.cs
  20. 2 2
      server/Plugins/Admin.NET.Plugin.AiDOP/Production/ShopCalendarWorkCtrService.cs
  21. 128 6
      server/Plugins/Admin.NET.Plugin.AiDOP/Production/WorkOrderSchedulingService.cs
  22. 16 16
      server/Plugins/Admin.NET.Plugin.AiDOP/Universal/Entity/ItemMaster.cs
  23. 2 2
      server/Plugins/Admin.NET.Plugin.AiDOP/WorkOrder/Entity/MesMorder.cs

+ 1 - 1
Web/package.json

@@ -1,7 +1,7 @@
 {
 {
 	"name": "admin.net",
 	"name": "admin.net",
 	"type": "module",
 	"type": "module",
-	"version": "2.4.190",
+	"version": "2.4.186",
 	"packageManager": "pnpm@10.32.1",
 	"packageManager": "pnpm@10.32.1",
 	"lastBuildTime": "2026.03.15",
 	"lastBuildTime": "2026.03.15",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",

+ 1 - 0
Web/src/views/aidop/api/asnShipper.ts

@@ -15,6 +15,7 @@ export interface AsnShipperRow {
 	shipDate?: string | null;
 	shipDate?: string | null;
 	status?: string | null;
 	status?: string | null;
 	remark?: string | null;
 	remark?: string | null;
+	hasShipped?: boolean;
 }
 }
 
 
 export interface AsnShipperDetailItem {
 export interface AsnShipperDetailItem {

+ 1 - 1
Web/src/views/aidop/api/qualityLineRest.ts

@@ -40,7 +40,7 @@ export interface QualityLineRestDetail {
 	remark?: string | null;
 	remark?: string | null;
 	descr1?: string | null;
 	descr1?: string | null;
 	descr2?: string | null;
 	descr2?: string | null;
-	period?: number | null;
+	period?: string | null;
 	endDateTime?: string | null;
 	endDateTime?: string | null;
 }
 }
 
 

+ 1 - 1
Web/src/views/aidop/api/shopCalendarWorkCtr.ts

@@ -42,7 +42,7 @@ export interface ShopCalendarDetail {
 	workCtr?: string | null;
 	workCtr?: string | null;
 	teamType?: string | null;
 	teamType?: string | null;
 	allowOverTime?: number | null;
 	allowOverTime?: number | null;
-	isWorkDay?: boolean | null;
+	isWorkDay?: number | null;
 	shiftsStart1Text?: string | null;
 	shiftsStart1Text?: string | null;
 	shiftsStart2Text?: string | null;
 	shiftsStart2Text?: string | null;
 }
 }

+ 5 - 1
Web/src/views/aidop/business/asnShipperList.vue

@@ -49,7 +49,7 @@
 				<template #default="{ row }">
 				<template #default="{ row }">
 					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
 					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
 					<el-button link type="info" @click="openView(row)">查看</el-button>
 					<el-button link type="info" @click="openView(row)">查看</el-button>
-					<el-button link type="danger" :disabled="row.status === 'C'" @click="onDelete(row)">删除</el-button>
+					<el-button link type="danger" v-if="!row.hasShipped" :disabled="row.status === 'C'" @click="onDelete(row)">删除</el-button>
 				</template>
 				</template>
 			</el-table-column>
 			</el-table-column>
 		</el-table>
 		</el-table>
@@ -138,6 +138,10 @@ async function onDelete(row: AsnShipperRow) {
 		ElMessage.warning('已关闭的发货单不允许删除');
 		ElMessage.warning('已关闭的发货单不允许删除');
 		return;
 		return;
 	}
 	}
+	if (row.hasShipped) {
+		ElMessage.warning('子表已有发货数量,不允许删除');
+		return;
+	}
 	try {
 	try {
 		await ElMessageBox.confirm(`确认删除发货单「${row.id1 || row.id}」?`, '删除确认', { type: 'warning' });
 		await ElMessageBox.confirm(`确认删除发货单「${row.id1 || row.id}」?`, '删除确认', { type: 'warning' });
 	} catch {
 	} catch {

+ 39 - 10
Web/src/views/aidop/production/executableDailyPlanList.vue

@@ -45,7 +45,17 @@
 					style="width: 150px"
 					style="width: 150px"
 				/>
 				/>
 			</el-form-item>
 			</el-form-item>
-			<el-form-item label="开工时间>=">
+			<el-form-item label="计划日期≤">
+				<el-date-picker
+					v-model="query.planDateMax"
+					type="date"
+					value-format="YYYY-MM-DD"
+					placeholder="留空=不限"
+					clearable
+					style="width: 150px"
+				/>
+			</el-form-item>
+			<el-form-item label="开工时间≥">
 				<el-date-picker
 				<el-date-picker
 					v-model="query.workStartFrom"
 					v-model="query.workStartFrom"
 					type="datetime"
 					type="datetime"
@@ -101,11 +111,8 @@
 				</template>
 				</template>
 			</el-table-column>
 			</el-table-column>
 			<el-table-column v-if="colOn('workOrds')" prop="workOrds" label="生产指令" width="140" fixed="left" show-overflow-tooltip />
 			<el-table-column v-if="colOn('workOrds')" prop="workOrds" label="生产指令" width="140" fixed="left" show-overflow-tooltip />
-			<el-table-column v-if="colOn('workStartTime')" label="开工日期" width="120" show-overflow-tooltip>
-				<template #default="{ row }">{{ fmtDt(row.workStartTime) }}</template>
-			</el-table-column>
-			<el-table-column v-if="colOn('workEndTime')" label="结束时间" width="120" show-overflow-tooltip>
-				<template #default="{ row }">{{ fmtDt(row.workEndTime) }}</template>
+			<el-table-column v-if="colOn('planDate')" label="计划日期" width="120" show-overflow-tooltip>
+				<template #default="{ row }">{{ fmtDate(row.planDate) }}</template>
 			</el-table-column>
 			</el-table-column>
 			<el-table-column v-if="colOn('lotSerial')" prop="lotSerial" label="批次" width="120" show-overflow-tooltip />
 			<el-table-column v-if="colOn('lotSerial')" prop="lotSerial" label="批次" width="120" show-overflow-tooltip />
 			<el-table-column v-if="colOn('itemNum')" prop="itemNum" label="物料编码" width="130" show-overflow-tooltip />
 			<el-table-column v-if="colOn('itemNum')" prop="itemNum" label="物料编码" width="130" show-overflow-tooltip />
@@ -172,8 +179,7 @@ const workCtrOptions = ['WC000005', 'WC000006', 'WC000007', 'WC000008', 'WC00000
 const allColumns = [
 const allColumns = [
 	{ key: 'status', label: '状态' },
 	{ key: 'status', label: '状态' },
 	{ key: 'workOrds', label: '生产指令' },
 	{ key: 'workOrds', label: '生产指令' },
-	{ key: 'workStartTime', label: '开工日期' },
-	{ key: 'workEndTime', label: '结束时间' },
+	{ key: 'planDate', label: '计划日期' },
 	{ key: 'lotSerial', label: '批次' },
 	{ key: 'lotSerial', label: '批次' },
 	{ key: 'itemNum', label: '物料编码' },
 	{ key: 'itemNum', label: '物料编码' },
 	{ key: 'descr', label: '物料名称' },
 	{ key: 'descr', label: '物料名称' },
@@ -201,6 +207,24 @@ function colOn(key: string) {
 	return visibleColKeys.value.includes(key);
 	return visibleColKeys.value.includes(key);
 }
 }
 
 
+/** 将日期格式化为 yyyy-MM-dd */
+function toYMD(d: Date): string {
+	const y = d.getFullYear();
+	const m = String(d.getMonth() + 1).padStart(2, '0');
+	const day = String(d.getDate()).padStart(2, '0');
+	return `${y}-${m}-${day}`;
+}
+
+/** 默认计划日期范围:今天 ~ 今天 + 3 天 */
+function defaultPlanDateRange(): { min: string; max: string } {
+	const today = new Date();
+	const max = new Date(today);
+	max.setDate(today.getDate() + 3);
+	return { min: toYMD(today), max: toYMD(max) };
+}
+
+const defaultRange = defaultPlanDateRange();
+
 const query = reactive({
 const query = reactive({
 	workOrds: '',
 	workOrds: '',
 	itemNum: '',
 	itemNum: '',
@@ -212,7 +236,9 @@ const query = reactive({
 	opStdOp: '',
 	opStdOp: '',
 	workStartFrom: '' as string,
 	workStartFrom: '' as string,
 	/** 不传时后端用「计划日期≥今天」;选过去日期可拉出历史日计划行 */
 	/** 不传时后端用「计划日期≥今天」;选过去日期可拉出历史日计划行 */
-	planDateMin: '' as string,
+	planDateMin: defaultRange.min as string,
+	/** 计划日期上限,默认今天 + 3 天 */
+	planDateMax: defaultRange.max as string,
 	page: 1,
 	page: 1,
 	pageSize: 20,
 	pageSize: 20,
 });
 });
@@ -251,6 +277,7 @@ function doSearch() {
 }
 }
 
 
 function resetQuery() {
 function resetQuery() {
+	const d = defaultPlanDateRange();
 	query.workOrds = '';
 	query.workOrds = '';
 	query.itemNum = '';
 	query.itemNum = '';
 	query.batch = '';
 	query.batch = '';
@@ -260,7 +287,8 @@ function resetQuery() {
 	query.occupyEquipmentType = '';
 	query.occupyEquipmentType = '';
 	query.opStdOp = '';
 	query.opStdOp = '';
 	query.workStartFrom = '';
 	query.workStartFrom = '';
-	query.planDateMin = '';
+	query.planDateMin = d.min;
+	query.planDateMax = d.max;
 	query.page = 1;
 	query.page = 1;
 	loadList();
 	loadList();
 }
 }
@@ -279,6 +307,7 @@ async function loadList() {
 			opStdOp: query.opStdOp || undefined,
 			opStdOp: query.opStdOp || undefined,
 			workStartFrom: query.workStartFrom || undefined,
 			workStartFrom: query.workStartFrom || undefined,
 			planDateMin: query.planDateMin || undefined,
 			planDateMin: query.planDateMin || undefined,
+			planDateMax: query.planDateMax || undefined,
 			page: query.page,
 			page: query.page,
 			pageSize: query.pageSize,
 			pageSize: query.pageSize,
 		});
 		});

+ 17 - 2
Web/src/views/aidop/production/shopCalendarWorkCtrForm.vue

@@ -127,6 +127,21 @@ async function loadDetail() {
 	form.shiftsStart2 = d.shiftsStart2Text || '';
 	form.shiftsStart2 = d.shiftsStart2Text || '';
 }
 }
 
 
+/**
+ * 将 "HH:mm" 时间字符串转为后端存储的 decimal 格式。
+ * 例如 "08:30" → 8.30,"12:36" → 12.36,"23:59" → 23.59
+ * 后端 DecimalHoursToTimeString 反向转换时:floor(8.30)=8, round((8.30-8)*60)=30 → "08:30"
+ */
+function timeStrToDecimal(timeStr: string): number | undefined {
+	if (!timeStr) return undefined;
+	const parts = timeStr.split(':');
+	if (parts.length !== 2) return undefined;
+	const h = parseInt(parts[0], 10);
+	const m = parseInt(parts[1], 10);
+	if (Number.isNaN(h) || Number.isNaN(m)) return undefined;
+	return h + m / 100;
+}
+
 async function submit() {
 async function submit() {
 	await formRef.value?.validate();
 	await formRef.value?.validate();
 	saving.value = true;
 	saving.value = true;
@@ -138,9 +153,9 @@ async function submit() {
 			prodLine: form.prodLine,
 			prodLine: form.prodLine,
 			weekDay: form.weekDay,
 			weekDay: form.weekDay,
 			workCtr: form.workCtr || undefined,
 			workCtr: form.workCtr || undefined,
-			shiftsStart1: form.shiftsStart1 || undefined,
+			shiftsStart1: timeStrToDecimal(form.shiftsStart1),
 			shiftsHours1: form.shiftsHours1,
 			shiftsHours1: form.shiftsHours1,
-			shiftsStart2: form.shiftsStart2 || undefined,
+			shiftsStart2: timeStrToDecimal(form.shiftsStart2),
 			shiftsHours2: form.shiftsHours2,
 			shiftsHours2: form.shiftsHours2,
 			ufld1: form.ufld1 || undefined,
 			ufld1: form.ufld1 || undefined,
 		});
 		});

+ 3 - 0
Web/src/views/aidop/production/workOrderProgressDashboardList.vue

@@ -200,6 +200,9 @@ async function loadList() {
 		});
 		});
 		rows.value = data.list;
 		rows.value = data.list;
 		total.value = data.total;
 		total.value = data.total;
+		if (query.workOrd && data.total === 0) {
+			ElMessage.warning('未找到匹配的工单;清除搜索可恢复默认视图');
+		}
 	} catch (e: any) {
 	} catch (e: any) {
 		ElMessage.error(e?.message || '加载失败');
 		ElMessage.error(e?.message || '加载失败');
 	} finally {
 	} finally {

+ 72 - 8
Web/src/views/aidop/production/workOrderSchedulingList.vue

@@ -36,6 +36,7 @@
 				<el-button type="danger" :disabled="!selectedRows.length" :loading="closing" @click="onBatchClose">工单关闭</el-button>
 				<el-button type="danger" :disabled="!selectedRows.length" :loading="closing" @click="onBatchClose">工单关闭</el-button>
 				<el-button type="primary" :loading="scheduling" @click="onProductionSchedule">生产排程</el-button>
 				<el-button type="primary" :loading="scheduling" @click="onProductionSchedule">生产排程</el-button>
 				<el-button type="success" :loading="syncing" @click="onSyncMaterial">同步物料需求</el-button>
 				<el-button type="success" :loading="syncing" @click="onSyncMaterial">同步物料需求</el-button>
+				<el-button :loading="exporting" @click="onExport">导出</el-button>
 			</div>
 			</div>
 			<el-popover placement="bottom-end" :width="280" trigger="click">
 			<el-popover placement="bottom-end" :width="280" trigger="click">
 				<template #reference>
 				<template #reference>
@@ -243,18 +244,19 @@ function resetQuery() {
 	loadList();
 	loadList();
 }
 }
 
 
-/** 生产排程 / 同步物料等外部接口使用当前登录用户组织ID作为 domain */
+/** 生产排程 / 同步物料等外部接口使用当前登录用户租户ID作为 domain */
 async function resolveDomainFromCurrentUser(): Promise<string | null> {
 async function resolveDomainFromCurrentUser(): Promise<string | null> {
 	// 确保 userInfos 已加载(部分页面首次进入时 store 可能还是空)
 	// 确保 userInfos 已加载(部分页面首次进入时 store 可能还是空)
-	if (!userInfoStore.userInfos?.orgId) {
+	if (!userInfoStore.userInfos?.currentTenantId && !userInfoStore.userInfos?.tenantId) {
 		await userInfoStore.setUserInfos();
 		await userInfoStore.setUserInfos();
 	}
 	}
-	const orgId = userInfoStore.userInfos?.orgId;
-	if (!orgId) {
-		ElMessage.warning('当前登录用户缺少组织ID(orgId),无法执行生产排程');
+	const u = userInfoStore.userInfos;
+	const tid = u?.currentTenantId ?? u?.tenantId;
+	if (tid === undefined || tid === null || String(tid).trim() === '') {
+		ElMessage.warning('当前登录用户缺少租户ID(tenantId),无法执行生产排程');
 		return null;
 		return null;
 	}
 	}
-	return String(orgId);
+	return String(tid);
 }
 }
 
 
 async function loadList() {
 async function loadList() {
@@ -291,15 +293,16 @@ async function onBatchClose() {
 		return;
 		return;
 	}
 	}
 	const ids = selectedRows.value.map((r) => r.id).join(',');
 	const ids = selectedRows.value.map((r) => r.id).join(',');
+	const workords = selectedRows.value.map((r) => r.workOrd).join(',');
 	try {
 	try {
-		await ElMessageBox.confirm(`确认关闭以下工单?RecID:${ids}`, '工单关闭', { type: 'warning' });
+		await ElMessageBox.confirm(`确认关闭以下工单?工单编号:${workords}`, '工单关闭', { type: 'warning' });
 	} catch {
 	} catch {
 		return;
 		return;
 	}
 	}
 	closing.value = true;
 	closing.value = true;
 	try {
 	try {
 		await closeWorkOrders(ids);
 		await closeWorkOrders(ids);
-		ElMessage.success('已提交关闭');
+		ElMessage.success('关闭成功');
 		loadList();
 		loadList();
 	} catch (e: any) {
 	} catch (e: any) {
 		ElMessage.error(e?.message || '关闭失败');
 		ElMessage.error(e?.message || '关闭失败');
@@ -387,6 +390,10 @@ function openRoutingTab(row: WorkOrderSchedulingRow) {
 }
 }
 
 
 async function onSyncRouting(row: WorkOrderSchedulingRow) {
 async function onSyncRouting(row: WorkOrderSchedulingRow) {
+	if ((row.qtyCompleted ?? 0) > 0) {
+		ElMessage.warning('工单已有完工数量,不允许修改工序明细');
+		return;
+	}
 	try {
 	try {
 		await ElMessageBox.confirm('确认同步该工单的工艺路线?', '同步工艺路线', { type: 'warning' });
 		await ElMessageBox.confirm('确认同步该工单的工艺路线?', '同步工艺路线', { type: 'warning' });
 	} catch {
 	} catch {
@@ -411,6 +418,63 @@ async function onUrgent(row: WorkOrderSchedulingRow, urgent: number) {
 	}
 	}
 }
 }
 
 
+const exporting = ref(false);
+
+function statusText(v?: string | null) {
+	const opt = statusOptions.find((o) => o.v === v);
+	return opt ? opt.l : v ?? '';
+}
+
+async function onExport() {
+	exporting.value = true;
+	try {
+		const data = await fetchWorkOrderSchedulingList({
+			workOrd: query.workOrd || undefined,
+			lotSerial: query.lotSerial || undefined,
+			itemNum: query.itemNum || undefined,
+			startDateFrom: query.startDateFrom || undefined,
+			status: query.status || undefined,
+			page: 1,
+			pageSize: 99999,
+		});
+		const allRows = data.list ?? [];
+		if (!allRows.length) {
+			ElMessage.warning('当前查询条件下无数据可导出');
+			return;
+		}
+		const headers = ['优先级', '工单编号', '生产批号', '类型', '物料编码', '物料名称', '规格型号', '工单数量', '齐套数量', '完成数量', '计划开工', '实际开工', '状态'];
+		const lines = allRows.map((r) => [
+			r.priority ?? '',
+			r.workOrd ?? '',
+			r.lotSerial ?? '',
+			r.issueSite ?? '',
+			r.itemNum ?? '',
+			r.descr ?? '',
+			r.descr1 ?? '',
+			r.qtyOrded ?? '',
+			r.locationStock ?? '',
+			r.qtyCompleted ?? '',
+			r.planDate ? String(r.planDate).slice(0, 10) : '',
+			r.prodDate ? String(r.prodDate).slice(0, 10) : '',
+			statusText(r.status),
+		]);
+		const csv = [headers, ...lines]
+			.map((row) => row.map((v) => `"${String(v ?? '').replace(/"/g, '""')}"`).join(','))
+			.join('\n');
+		const blob = new Blob([`\uFEFF${csv}`], { type: 'text/csv;charset=utf-8;' });
+		const a = document.createElement('a');
+		a.href = URL.createObjectURL(blob);
+		a.download = `工单工序排产_${new Date().toISOString().slice(0, 10)}.csv`;
+		a.click();
+		URL.revokeObjectURL(a.href);
+		ElMessage.success(`已导出 ${allRows.length} 条数据`);
+	} catch (e: any) {
+		ElMessage.error(e?.message || '导出失败');
+	} finally {
+		exporting.value = false;
+	}
+}
+
 onMounted(() => loadList());
 onMounted(() => loadList());
 </script>
 </script>
 
 

二进制
doc/S1产销协同_UAT测试报告v1.0.xlsx


二进制
doc/S2制造协同_UAT测试报告v1.0.xlsx


+ 3 - 24
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.180</AssemblyVersion>
-    <FileVersion>1.0.180</FileVersion>
-    <Version>1.0.180</Version>
+    <AssemblyVersion>1.0.168</AssemblyVersion>
+    <FileVersion>1.0.168</FileVersion>
+    <Version>1.0.168</Version>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
@@ -115,27 +115,6 @@
     <None Update="UpdateScripts\1.0.157.sql">
     <None Update="UpdateScripts\1.0.157.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
     </None>
-    <None Update="UpdateScripts\1.0.158.sql">
-      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
-    </None>
-    <None Update="UpdateScripts\1.0.159.sql">
-      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
-    </None>
-    <None Update="UpdateScripts\1.0.174.sql">
-      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
-    </None>
-    <None Update="UpdateScripts\1.0.176.sql">
-      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
-    </None>
-    <None Update="UpdateScripts\1.0.177.sql">
-      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
-    </None>
-    <None Update="UpdateScripts\1.0.178.sql">
-      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
-    </None>
-    <None Update="UpdateScripts\1.0.180.sql">
-      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
-    </None>
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 16 - 16
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Sales/AdoS0MaterialsController.cs

@@ -170,8 +170,8 @@ public class AdoS0MaterialsController : ControllerBase
         NetWeight = dto.NetWeight,
         NetWeight = dto.NetWeight,
         NetWeightUM = dto.NetWeightUM,
         NetWeightUM = dto.NetWeightUM,
         Inspect = dto.Inspect,
         Inspect = dto.Inspect,
-        PurLT = dto.PurLT,
-        InsLT = dto.InsLT,
+        PurLT = dto.PurLT.GetValueOrDefault(),
+        InsLT = dto.InsLT.GetValueOrDefault(),
         MfgLT = dto.MfgLT,
         MfgLT = dto.MfgLT,
         Length = dto.Length,
         Length = dto.Length,
         Size = dto.Size,
         Size = dto.Size,
@@ -181,16 +181,16 @@ public class AdoS0MaterialsController : ControllerBase
         LocationType = dto.LocationType,
         LocationType = dto.LocationType,
         CommodityCode = dto.CommodityCode,
         CommodityCode = dto.CommodityCode,
         Rev = dto.Rev,
         Rev = dto.Rev,
-        MaxOrd = dto.MaxOrd,
-        MinOrd = dto.MinOrd,
-        OrdMult = dto.OrdMult,
-        MinOrdSales = dto.MinOrdSales,
+        MaxOrd = dto.MaxOrd.GetValueOrDefault(),
+        MinOrd = dto.MinOrd.GetValueOrDefault(),
+        OrdMult = dto.OrdMult.GetValueOrDefault(),
+        MinOrdSales = dto.MinOrdSales.GetValueOrDefault(),
         AutoLotNums = dto.AutoLotNums,
         AutoLotNums = dto.AutoLotNums,
         Install = dto.Install,
         Install = dto.Install,
-        SafetyStk = dto.SafetyStk,
+        SafetyStk = dto.SafetyStk.GetValueOrDefault(),
         DaysBetweenPM = dto.DaysBetweenPM,
         DaysBetweenPM = dto.DaysBetweenPM,
         ExpireAlarmDay = dto.ExpireAlarmDay,
         ExpireAlarmDay = dto.ExpireAlarmDay,
-        StockTurnOver = dto.StockTurnOver,
+        StockTurnOver = dto.StockTurnOver.GetValueOrDefault(),
         LotSerialControl = dto.LotSerialControl,
         LotSerialControl = dto.LotSerialControl,
         AllocateSingleLot = dto.AllocateSingleLot,
         AllocateSingleLot = dto.AllocateSingleLot,
         Planner = dto.Planner,
         Planner = dto.Planner,
@@ -233,8 +233,8 @@ public class AdoS0MaterialsController : ControllerBase
         entity.NetWeight = dto.NetWeight;
         entity.NetWeight = dto.NetWeight;
         entity.NetWeightUM = dto.NetWeightUM;
         entity.NetWeightUM = dto.NetWeightUM;
         entity.Inspect = dto.Inspect;
         entity.Inspect = dto.Inspect;
-        entity.PurLT = dto.PurLT;
-        entity.InsLT = dto.InsLT;
+        entity.PurLT = dto.PurLT.GetValueOrDefault();
+        entity.InsLT = dto.InsLT.GetValueOrDefault();
         entity.MfgLT = dto.MfgLT;
         entity.MfgLT = dto.MfgLT;
         entity.Length = dto.Length;
         entity.Length = dto.Length;
         entity.Size = dto.Size;
         entity.Size = dto.Size;
@@ -244,16 +244,16 @@ public class AdoS0MaterialsController : ControllerBase
         entity.LocationType = dto.LocationType;
         entity.LocationType = dto.LocationType;
         entity.CommodityCode = dto.CommodityCode;
         entity.CommodityCode = dto.CommodityCode;
         entity.Rev = dto.Rev;
         entity.Rev = dto.Rev;
-        entity.MaxOrd = dto.MaxOrd;
-        entity.MinOrd = dto.MinOrd;
-        entity.OrdMult = dto.OrdMult;
-        entity.MinOrdSales = dto.MinOrdSales;
+        entity.MaxOrd = dto.MaxOrd.GetValueOrDefault();
+        entity.MinOrd = dto.MinOrd.GetValueOrDefault();
+        entity.OrdMult = dto.OrdMult.GetValueOrDefault();
+        entity.MinOrdSales = dto.MinOrdSales.GetValueOrDefault();
         entity.AutoLotNums = dto.AutoLotNums;
         entity.AutoLotNums = dto.AutoLotNums;
         entity.Install = dto.Install;
         entity.Install = dto.Install;
-        entity.SafetyStk = dto.SafetyStk;
+        entity.SafetyStk = dto.SafetyStk.GetValueOrDefault();
         entity.DaysBetweenPM = dto.DaysBetweenPM;
         entity.DaysBetweenPM = dto.DaysBetweenPM;
         entity.ExpireAlarmDay = dto.ExpireAlarmDay;
         entity.ExpireAlarmDay = dto.ExpireAlarmDay;
-        entity.StockTurnOver = dto.StockTurnOver;
+        entity.StockTurnOver = dto.StockTurnOver.GetValueOrDefault();
         entity.LotSerialControl = dto.LotSerialControl;
         entity.LotSerialControl = dto.LotSerialControl;
         entity.AllocateSingleLot = dto.AllocateSingleLot;
         entity.AllocateSingleLot = dto.AllocateSingleLot;
         entity.Planner = dto.Planner;
         entity.Planner = dto.Planner;

+ 16 - 16
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Sales/AdoS0ItemMaster.cs

@@ -61,11 +61,11 @@ public class AdoS0ItemMaster : ITenantIdFilter
     [SugarColumn(ColumnName = "Inspect", ColumnDescription = "检验", ColumnDataType = "boolean")]
     [SugarColumn(ColumnName = "Inspect", ColumnDescription = "检验", ColumnDataType = "boolean")]
     public bool Inspect { get; set; }
     public bool Inspect { get; set; }
 
 
-    [SugarColumn(ColumnName = "PurLT", ColumnDescription = "供应提前期", IsNullable = true)]
-    public int? PurLT { get; set; }
+    [SugarColumn(ColumnName = "PurLT", ColumnDescription = "供应提前期")]
+    public int PurLT { get; set; }
 
 
-    [SugarColumn(ColumnName = "InsLT", ColumnDescription = "检验天数", IsNullable = true)]
-    public int? InsLT { get; set; }
+    [SugarColumn(ColumnName = "InsLT", ColumnDescription = "检验天数")]
+    public int InsLT { get; set; }
 
 
     [SugarColumn(ColumnName = "MfgLT", ColumnDescription = "备料提前期", IsNullable = true)]
     [SugarColumn(ColumnName = "MfgLT", ColumnDescription = "备料提前期", IsNullable = true)]
     public int? MfgLT { get; set; }
     public int? MfgLT { get; set; }
@@ -94,17 +94,17 @@ public class AdoS0ItemMaster : ITenantIdFilter
     [SugarColumn(ColumnName = "Rev", ColumnDescription = "版本", Length = 50, IsNullable = true)]
     [SugarColumn(ColumnName = "Rev", ColumnDescription = "版本", Length = 50, IsNullable = true)]
     public string? Rev { get; set; }
     public string? Rev { get; set; }
 
 
-    [SugarColumn(ColumnName = "MaxOrd", ColumnDescription = "最大订单", ColumnDataType = "decimal(18,5)", IsNullable = true)]
-    public decimal? MaxOrd { get; set; }
+    [SugarColumn(ColumnName = "MaxOrd", ColumnDescription = "最大订单", ColumnDataType = "decimal(18,5)")]
+    public decimal MaxOrd { get; set; }
 
 
-    [SugarColumn(ColumnName = "MinOrd", ColumnDescription = "最小订单", ColumnDataType = "decimal(18,5)", IsNullable = true)]
-    public decimal? MinOrd { get; set; }
+    [SugarColumn(ColumnName = "MinOrd", ColumnDescription = "最小订单", ColumnDataType = "decimal(18,5)")]
+    public decimal MinOrd { get; set; }
 
 
-    [SugarColumn(ColumnName = "OrdMult", ColumnDescription = "订单倍数", ColumnDataType = "decimal(18,5)", IsNullable = true)]
-    public decimal? OrdMult { get; set; }
+    [SugarColumn(ColumnName = "OrdMult", ColumnDescription = "订单倍数", ColumnDataType = "decimal(18,5)")]
+    public decimal OrdMult { get; set; }
 
 
-    [SugarColumn(ColumnName = "MinOrdSales", ColumnDescription = "起订量", ColumnDataType = "decimal(18,5)", IsNullable = true)]
-    public decimal? MinOrdSales { get; set; }
+    [SugarColumn(ColumnName = "MinOrdSales", ColumnDescription = "起订量", ColumnDataType = "decimal(18,5)")]
+    public decimal MinOrdSales { get; set; }
 
 
     [SugarColumn(ColumnName = "AutoLotNums", ColumnDescription = "自动批次号", ColumnDataType = "boolean")]
     [SugarColumn(ColumnName = "AutoLotNums", ColumnDescription = "自动批次号", ColumnDataType = "boolean")]
     public bool AutoLotNums { get; set; }
     public bool AutoLotNums { get; set; }
@@ -112,8 +112,8 @@ public class AdoS0ItemMaster : ITenantIdFilter
     [SugarColumn(ColumnName = "Install", ColumnDescription = "是否打标", ColumnDataType = "boolean")]
     [SugarColumn(ColumnName = "Install", ColumnDescription = "是否打标", ColumnDataType = "boolean")]
     public bool Install { get; set; }
     public bool Install { get; set; }
 
 
-    [SugarColumn(ColumnName = "SafetyStk", ColumnDescription = "安全库存", ColumnDataType = "decimal(18,5)", IsNullable = true)]
-    public decimal? SafetyStk { get; set; }
+    [SugarColumn(ColumnName = "SafetyStk", ColumnDescription = "安全库存", ColumnDataType = "decimal(18,5)")]
+    public decimal SafetyStk { get; set; }
 
 
     [SugarColumn(ColumnName = "DaysBetweenPM", ColumnDescription = "保质期(天)", IsNullable = true)]
     [SugarColumn(ColumnName = "DaysBetweenPM", ColumnDescription = "保质期(天)", IsNullable = true)]
     public int? DaysBetweenPM { get; set; }
     public int? DaysBetweenPM { get; set; }
@@ -121,8 +121,8 @@ public class AdoS0ItemMaster : ITenantIdFilter
     [SugarColumn(ColumnName = "expire_alarm_day", ColumnDescription = "过期预警天数", IsNullable = true)]
     [SugarColumn(ColumnName = "expire_alarm_day", ColumnDescription = "过期预警天数", IsNullable = true)]
     public int? ExpireAlarmDay { get; set; }
     public int? ExpireAlarmDay { get; set; }
 
 
-    [SugarColumn(ColumnName = "StockTurnOver", ColumnDescription = "存货周转率", ColumnDataType = "decimal(18,5)", IsNullable = true)]
-    public decimal? StockTurnOver { get; set; }
+    [SugarColumn(ColumnName = "StockTurnOver", ColumnDescription = "存货周转率", ColumnDataType = "decimal(18,5)")]
+    public decimal StockTurnOver { get; set; }
 
 
     [SugarColumn(ColumnName = "LotSerialControl", ColumnDescription = "批次先进先出提醒", ColumnDataType = "boolean")]
     [SugarColumn(ColumnName = "LotSerialControl", ColumnDescription = "批次先进先出提醒", ColumnDataType = "boolean")]
     public bool LotSerialControl { get; set; }
     public bool LotSerialControl { get; set; }

+ 8 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Order/AsnShipperService.cs

@@ -79,7 +79,13 @@ public class AsnShipperService : IDynamicApiController, ITransient
                 TRIM(CONCAT(IFNULL(b.Department,''), ' ', IFNULL(b.Descr,''))) AS DepartmentName,
                 TRIM(CONCAT(IFNULL(b.Department,''), ' ', IFNULL(b.Descr,''))) AS DepartmentName,
                 a.ShipDate,
                 a.ShipDate,
                 a.Status,
                 a.Status,
-                a.Remark
+                a.Remark,
+                CASE WHEN EXISTS (
+                    SELECT 1 FROM ASNBOLShipperDetail d
+                    WHERE d.ASNBOLShipperRecID = a.RecID
+                      AND d.tenant_id = a.tenant_id
+                      AND IFNULL(d.RealQty, 0) > 0
+                ) THEN 1 ELSE 0 END AS HasShipped
             FROM ASNBOLShipperMaster a
             FROM ASNBOLShipperMaster a
             LEFT JOIN DepartmentMaster b
             LEFT JOIN DepartmentMaster b
                 ON a.Department = b.Department
                 ON a.Department = b.Department
@@ -539,6 +545,7 @@ public class AsnShipperService : IDynamicApiController, ITransient
         public DateTime? ShipDate { get; set; }
         public DateTime? ShipDate { get; set; }
         public string? Status { get; set; }
         public string? Status { get; set; }
         public string? Remark { get; set; }
         public string? Remark { get; set; }
+        public bool HasShipped { get; set; }
     }
     }
 
 
     private sealed class SimpleKvRow
     private sealed class SimpleKvRow

+ 5 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Production/Dto/ExecutableDailyPlanDto.cs

@@ -42,6 +42,11 @@ public class ExecutableDailyPlanListInput
     /// </summary>
     /// </summary>
     public string? PlanDateMin { get; set; }
     public string? PlanDateMin { get; set; }
 
 
+    /// <summary>
+    /// 计划日期上限(含)。格式 yyyy-MM-dd;不传时不限上限。
+    /// </summary>
+    public string? PlanDateMax { get; set; }
+
     /// <summary>排序字段</summary>
     /// <summary>排序字段</summary>
     public string? OrderBy { get; set; }
     public string? OrderBy { get; set; }
 
 

+ 7 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Production/ExecutableDailyPlanService.cs

@@ -45,6 +45,13 @@ public class ExecutableDailyPlanService : IDynamicApiController, ITransient
         {
         {
             innerWhere.Add("DATE(p.PlanDate) >= CURDATE()");
             innerWhere.Add("DATE(p.PlanDate) >= CURDATE()");
         }
         }
+        if (!string.IsNullOrWhiteSpace(input.PlanDateMax))
+        {
+            var pdx = input.PlanDateMax.Trim();
+            if (pdx.Length > 10) pdx = pdx[..10];
+            innerWhere.Add("DATE(p.PlanDate) <= @PlanDateMax");
+            pars.Add(new SugarParameter("@PlanDateMax", pdx));
+        }
         if (!string.IsNullOrWhiteSpace(input.Domain))
         if (!string.IsNullOrWhiteSpace(input.Domain))
         {
         {
             innerWhere.Add("p.`Domain` = @Domain");
             innerWhere.Add("p.`Domain` = @Domain");

+ 17 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Production/HolidayMasterService.cs

@@ -83,6 +83,23 @@ public class HolidayMasterService : IDynamicApiController, ITransient
         var site = Truncate(input.Site.Trim(), 4);
         var site = Truncate(input.Site.Trim(), 4);
         var dated = input.Dated ?? throw Oops.Oh("日期不能为空");
         var dated = input.Dated ?? throw Oops.Oh("日期不能为空");
 
 
+        // 同一日期重复校验(编辑时排除自身)
+        var dupWhere = "DATE(Dated) = DATE(@Dated) AND IsActive = 1 AND tenant_id = @TenantId";
+        var dupPars = new List<SugarParameter>
+        {
+            new("@Dated", dated),
+            new("@TenantId", tenantId)
+        };
+        if (input.Id is not null and not 0)
+        {
+            dupWhere += " AND RecID <> @ExcludeId";
+            dupPars.Add(new SugarParameter("@ExcludeId", input.Id.Value));
+        }
+        var dupCount = await _db.Ado.GetIntAsync(
+            $"SELECT COUNT(*) FROM HolidayMaster WHERE {dupWhere}", dupPars);
+        if (dupCount > 0)
+            throw Oops.Oh("该日期已配置节假日");
+
         if (input.Id is null or 0)
         if (input.Id is null or 0)
         {
         {
             await _db.Ado.ExecuteCommandAsync(
             await _db.Ado.ExecuteCommandAsync(

+ 23 - 5
server/Plugins/Admin.NET.Plugin.AiDOP/Production/QualityLineWorkDetailService.cs

@@ -97,6 +97,24 @@ public class QualityLineWorkDetailService : IDynamicApiController, ITransient
 
 
         var tenantId = AidopTenantHelper.Resolve(App.HttpContext);
         var tenantId = AidopTenantHelper.Resolve(App.HttpContext);
 
 
+        // 同一产线 + 相同休息开始时间 重复校验(编辑时排除自身)
+        var dupWhere = "ProdLine = @ProdLine AND RestTimePoint = @RestTimePoint AND IsActive = 1 AND tenant_id = @TenantId";
+        var dupPars = new List<SugarParameter>
+        {
+            new("@ProdLine", prodLine),
+            new("@RestTimePoint", restPoint),
+            new("@TenantId", tenantId)
+        };
+        if (input.Id is not null and not 0)
+        {
+            dupWhere += " AND RecID <> @ExcludeId";
+            dupPars.Add(new SugarParameter("@ExcludeId", input.Id.Value));
+        }
+        var dupCount = await _db.Ado.GetIntAsync(
+            $"SELECT COUNT(*) FROM QualityLineWorkDetail WHERE {dupWhere}", dupPars);
+        if (dupCount > 0)
+            throw Oops.Oh("休息时段与已有配置重叠");
+
         if (input.Id is null or 0)
         if (input.Id is null or 0)
         {
         {
             await _db.Ado.ExecuteCommandAsync(
             await _db.Ado.ExecuteCommandAsync(
@@ -221,9 +239,9 @@ public class QualityLineWorkDetailService : IDynamicApiController, ITransient
     {
     {
         public string? ProdLine { get; set; }
         public string? ProdLine { get; set; }
         public string? LineDesc { get; set; }
         public string? LineDesc { get; set; }
-        public int Line { get; set; }
+        public int? Line { get; set; }
         public string? RestTimePoint { get; set; }
         public string? RestTimePoint { get; set; }
-        public int RestTime { get; set; }
+        public decimal? RestTime { get; set; }
         public long Id { get; set; }
         public long Id { get; set; }
         public string? Descr1 { get; set; }
         public string? Descr1 { get; set; }
         public string? EndDateTime { get; set; }
         public string? EndDateTime { get; set; }
@@ -239,13 +257,13 @@ public class QualityLineWorkDetailService : IDynamicApiController, ITransient
         public string? Site { get; set; }
         public string? Site { get; set; }
         public string? ProdLine { get; set; }
         public string? ProdLine { get; set; }
         public string? WorkShift { get; set; }
         public string? WorkShift { get; set; }
-        public int Line { get; set; }
+        public int? Line { get; set; }
         public string? RestTimePoint { get; set; }
         public string? RestTimePoint { get; set; }
-        public int RestTime { get; set; }
+        public decimal? RestTime { get; set; }
         public string? Remark { get; set; }
         public string? Remark { get; set; }
         public string? Descr1 { get; set; }
         public string? Descr1 { get; set; }
         public string? Descr2 { get; set; }
         public string? Descr2 { get; set; }
-        public short Period { get; set; }
+        public string? Period { get; set; }
         public string? EndDateTime { get; set; }
         public string? EndDateTime { get; set; }
     }
     }
 
 

+ 2 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Production/ShopCalendarWorkCtrService.cs

@@ -322,8 +322,8 @@ public class ShopCalendarWorkCtrService : IDynamicApiController, ITransient
         public string? Site { get; set; }
         public string? Site { get; set; }
         public string? WorkCtr { get; set; }
         public string? WorkCtr { get; set; }
         public string? TeamType { get; set; }
         public string? TeamType { get; set; }
-        public decimal? AllowOverTime { get; set; }
-        public bool? IsWorkDay { get; set; }
+        public int? AllowOverTime { get; set; }
+        public int? IsWorkDay { get; set; }
         public string? ShiftsStart1Text { get; set; }
         public string? ShiftsStart1Text { get; set; }
         public string? ShiftsStart2Text { get; set; }
         public string? ShiftsStart2Text { get; set; }
     }
     }

+ 128 - 6
server/Plugins/Admin.NET.Plugin.AiDOP/Production/WorkOrderSchedulingService.cs

@@ -19,6 +19,15 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
         _userManager = userManager;
         _userManager = userManager;
     }
     }
 
 
+    /// <summary>
+    /// 工单信息类(用于查询结果映射)
+    /// </summary>
+    private class WorkOrderInfo
+    {
+        public string WorkOrd { get; set; } = string.Empty;
+        public string Domain { get; set; } = string.Empty;
+    }
+
     /// <summary>
     /// <summary>
     /// 主表 WorkOrdMaster 按工单号 +「公司域或租户 id」匹配:<c>domain</c> 可为原 <c>Domain</c> 列,或与 <c>tenant_id</c> 相同的字符串(列表已做 COALESCE 回传)。
     /// 主表 WorkOrdMaster 按工单号 +「公司域或租户 id」匹配:<c>domain</c> 可为原 <c>Domain</c> 列,或与 <c>tenant_id</c> 相同的字符串(列表已做 COALESCE 回传)。
     /// </summary>
     /// </summary>
@@ -681,14 +690,127 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
     // ══════════════════════════════════════════════════════════════
     // ══════════════════════════════════════════════════════════════
     // 工单关闭 POST /api/Production/scheduling/close
     // 工单关闭 POST /api/Production/scheduling/close
     // ══════════════════════════════════════════════════════════════
     // ══════════════════════════════════════════════════════════════
-    [DisplayName("工单关闭(存储过程)")]
+    [DisplayName("工单关闭")]
     [HttpPost("scheduling/close")]
     [HttpPost("scheduling/close")]
     public async Task<object> Close([FromBody] WorkOrderCloseInput input)
     public async Task<object> Close([FromBody] WorkOrderCloseInput input)
     {
     {
+        var ids = input.Ids.Trim();
+        if (string.IsNullOrWhiteSpace(ids))
+            throw Oops.Oh("工单主键不能为空");
+
+        try
+        {
+            // 开启事务
+            _db.Ado.BeginTran();
+
+            // 根据 RecID 获取工单号和域信息
+            // 注意:存储过程 pr_MES_CloseWorkOrders 需要两个参数:域和工单号(竖线分隔)
+            // 但当前调用只传递 RecID,因此需要根据 RecID 查询对应的工单和域
+            
+            var workOrders = await _db.Ado.SqlQueryAsync<WorkOrderInfo>(
+                "SELECT WorkOrd AS WorkOrd, COALESCE(NULLIF(TRIM(`Domain`), ''), CAST(tenant_id AS CHAR(20))) AS Domain FROM WorkOrdMaster WHERE RecID IN (" + BuildInClause(ids) + ")",
+                GetInParameters(ids));
+            
+            if (!workOrders.Any())
+                throw Oops.Oh("未找到对应的工单");
+
+            // 为每个域执行关闭操作
+            var domains = workOrders.Select(x => x.Domain).Distinct();
+            foreach (var domain in domains)
+            {
+                // 获取该域的工单号列表(竖线分隔)
+                var domainWorkOrds = string.Join("|", workOrders.Where(x => x.Domain == domain).Select(x => x.WorkOrd));
+                
+                // 执行关闭逻辑(模拟存储过程)
+                await ExecuteCloseWorkOrders(domain, domainWorkOrds);
+            }
+
+            _db.Ado.CommitTran();
+            return new { message = "工单关闭成功" };
+        }
+        catch (Exception ex)
+        {
+            _db.Ado.RollbackTran();
+            return new { message = $"工单关闭失败: {ex.Message}" };
+        }
+    }
+
+    /// <summary>
+    /// 构建 IN 子句参数
+    /// </summary>
+    private string BuildInClause(string ids)
+    {
+        var idList = ids.Split(',').Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x));
+        return string.Join(",", idList.Select((_, i) => $"@Id{i}"));
+    }
+
+    /// <summary>
+    /// 获取 IN 子句参数
+    /// </summary>
+    private SugarParameter[] GetInParameters(string ids)
+    {
+        var idList = ids.Split(',').Select(x => x.Trim()).Where(x => !string.IsNullOrWhiteSpace(x));
+        return idList.Select((id, i) => new SugarParameter($"@Id{i}", id)).ToArray();
+    }
+
+    /// <summary>
+    /// 执行关闭工单逻辑(模拟存储过程 pr_MES_CloseWorkOrders)
+    /// </summary>
+    private async Task ExecuteCloseWorkOrders(string domain, string workOrds)
+    {
+        if (string.IsNullOrWhiteSpace(workOrds))
+            return;
+
+        // 存储过程使用竖线分隔,但FIND_IN_SET需要逗号分隔
+        var workOrdsForFindInSet = workOrds.Replace("|", ",");
+        
+        // 1. 更新工单状态为C
+        await _db.Ado.ExecuteCommandAsync(
+            "UPDATE WorkOrdMaster SET Status = 'C', UpdateUser = 'proc' WHERE tenant_id = @Domain AND FIND_IN_SET(WorkOrd, @WorkOrds) > 0",
+            new SugarParameter("@Domain", domain),
+            new SugarParameter("@WorkOrds", workOrdsForFindInSet));
+
+        // 2. 更新领料单状态为C
+        await _db.Ado.ExecuteCommandAsync(
+            "UPDATE NbrMaster SET Status = 'C' WHERE Type = 'SM' AND tenant_id = @Domain AND FIND_IN_SET(WorkOrd, @WorkOrds) > 0",
+            new SugarParameter("@Domain", domain),
+            new SugarParameter("@WorkOrds", workOrdsForFindInSet));
+
+        // 3. 更新领料单明细状态为C
+        await _db.Ado.ExecuteCommandAsync(
+            "UPDATE NbrDetail SET Status = 'C' WHERE tenant_id = @Domain AND Type = 'SM' AND Nbr IN (SELECT Nbr FROM NbrMaster WHERE Type = 'SM' AND Domain = @Domain AND FIND_IN_SET(WorkOrd, @WorkOrds) > 0)",
+            new SugarParameter("@Domain", domain),
+            new SugarParameter("@WorkOrds", workOrdsForFindInSet));
+
+        // 4. 更新退料单状态为C
+        await _db.Ado.ExecuteCommandAsync(
+            "UPDATE NbrMaster SET Status = 'C' WHERE Type = 'WOD' AND tenant_id = @Domain AND FIND_IN_SET(WorkOrd, @WorkOrds) > 0",
+            new SugarParameter("@Domain", domain),
+            new SugarParameter("@WorkOrds", workOrdsForFindInSet));
+
+        // 5. 更新退料单明细状态为C
+        await _db.Ado.ExecuteCommandAsync(
+            "UPDATE NbrDetail SET Status = 'C' WHERE tenant_id = @Domain AND Type = 'WOD' AND Nbr IN (SELECT Nbr FROM NbrMaster WHERE Type = 'WOD' AND Domain = @Domain AND FIND_IN_SET(WorkOrd, @WorkOrds) > 0)",
+            new SugarParameter("@Domain", domain),
+            new SugarParameter("@WorkOrds", workOrdsForFindInSet));
+
+        // 6. 删除库存占用
+        await _db.Ado.ExecuteCommandAsync(
+            "DELETE FROM ic_item_stockoccupy WHERE tenant_id = @Domain AND FIND_IN_SET(morder_mo, @WorkOrds) > 0",
+            new SugarParameter("@Domain", domain),
+            new SugarParameter("@WorkOrds", workOrdsForFindInSet));
+
+        // 7. 删除PO占用
+        await _db.Ado.ExecuteCommandAsync(
+            "DELETE FROM srm_po_occupy WHERE tenant_id = @Domain AND FIND_IN_SET(morder_mo, @WorkOrds) > 0",
+            new SugarParameter("@Domain", domain),
+            new SugarParameter("@WorkOrds", workOrdsForFindInSet));
+
+        // 8. 删除工单占用
         await _db.Ado.ExecuteCommandAsync(
         await _db.Ado.ExecuteCommandAsync(
-            "CALL pr_MES_CloseWorkOrders(@Ids)",
-            new SugarParameter("@Ids", input.Ids.Trim()));
-        return new { message = "已提交关闭" };
+            "DELETE FROM mes_mooccupy WHERE tenant_id = @Domain AND FIND_IN_SET(moo_mo, @WorkOrds) > 0",
+            new SugarParameter("@Domain", domain),
+            new SugarParameter("@WorkOrds", workOrdsForFindInSet));
     }
     }
 
 
     // ══════════════════════════════════════════════════════════════
     // ══════════════════════════════════════════════════════════════
@@ -729,7 +851,7 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
                     `Descr`, `Domain`, `ChargeCode`, `Machine`, `RunCrew`, `MilestoneOp`, `OP`, `StdOp`, `ItemNum`, `WorkCtr`,
                     `Descr`, `Domain`, `ChargeCode`, `Machine`, `RunCrew`, `MilestoneOp`, `OP`, `StdOp`, `ItemNum`, `WorkCtr`,
                     `Ufld1`, `Ufld3`, `Setup`, `MachinesperOp`, `Labor`, `RunTime`, `MachBdnRate`, `WorkCode`, `StdSetupTime`, `Engineer`,
                     `Ufld1`, `Ufld3`, `Setup`, `MachinesperOp`, `Labor`, `RunTime`, `MachBdnRate`, `WorkCode`, `StdSetupTime`, `Engineer`,
                     `WorkOrd`, `WorkOrdMasterRecID`, `ERPfld1`, `QtyOrded`, `ProcessOut`, `ProcessOutDay`, `ProcessOutSupp`,
                     `WorkOrd`, `WorkOrdMasterRecID`, `ERPfld1`, `QtyOrded`, `ProcessOut`, `ProcessOutDay`, `ProcessOutSupp`,
-                    `IsActive`, `Status`, `ProdLine`, `CreateTime`, `CreateUser`, `tenant_id`,`CommentIndex`,`WaitTime`
+                    `IsActive`, `Status`, `ProdLine`, `CreateTime`, `CreateUser`, `tenant_id`,`CommentIndex`,`WaitTime`,`QtyComplete`,`ParentOp`
                 )
                 )
                 SELECT
                 SELECT
                     a.`Descr`,
                     a.`Descr`,
@@ -764,7 +886,7 @@ public class WorkOrderSchedulingService : IDynamicApiController, ITransient
                     LEFT(IFNULL(b.`Line`, ''), 8),
                     LEFT(IFNULL(b.`Line`, ''), 8),
                     NOW(3),
                     NOW(3),
                     @CreateUser,
                     @CreateUser,
-                    w.`tenant_id`,CAST(IFNULL(a.`MilestoneOp`, 0) AS UNSIGNED),0
+                    w.`tenant_id`,CAST(IFNULL(a.`MilestoneOp`, 0) AS UNSIGNED),0,0,0
                 FROM WorkOrdMaster w
                 FROM WorkOrdMaster w
                 LEFT JOIN RoutingOpDetail a ON w.`ItemNum` = a.`RoutingCode`
                 LEFT JOIN RoutingOpDetail a ON w.`ItemNum` = a.`RoutingCode`
                 LEFT JOIN ProdLineDetail b ON a.`RoutingCode` = b.`Part` AND a.`Op` = b.`Op`
                 LEFT JOIN ProdLineDetail b ON a.`RoutingCode` = b.`Part` AND a.`Op` = b.`Op`

+ 16 - 16
server/Plugins/Admin.NET.Plugin.AiDOP/Universal/Entity/ItemMaster.cs

@@ -79,11 +79,11 @@ public class ItemMaster
 	[SugarColumn(ColumnName = "IssuePolicy", ColumnDataType = "tinyint(1)")]
 	[SugarColumn(ColumnName = "IssuePolicy", ColumnDataType = "tinyint(1)")]
 	public bool IssuePolicy { get; set; }
 	public bool IssuePolicy { get; set; }
 
 
-	[SugarColumn(ColumnName = "PurLT", IsNullable = true)]
-	public int? PurLT { get; set; }
+	[SugarColumn(ColumnName = "PurLT")]
+	public int PurLT { get; set; }
 
 
-	[SugarColumn(ColumnName = "InsLT", IsNullable = true)]
-	public int? InsLT { get; set; }
+	[SugarColumn(ColumnName = "InsLT")]
+	public int InsLT { get; set; }
 
 
 	[SugarColumn(ColumnName = "MfgLT", IsNullable = true)]
 	[SugarColumn(ColumnName = "MfgLT", IsNullable = true)]
 	public int? MfgLT { get; set; }
 	public int? MfgLT { get; set; }
@@ -103,17 +103,17 @@ public class ItemMaster
 	[SugarColumn(ColumnName = "CommodityCode", Length = 100, IsNullable = true)]
 	[SugarColumn(ColumnName = "CommodityCode", Length = 100, IsNullable = true)]
 	public string? CommodityCode { get; set; }
 	public string? CommodityCode { get; set; }
 
 
-	[SugarColumn(ColumnName = "MaxOrd", ColumnDataType = "decimal(18,5)", IsNullable = true)]
-	public decimal? MaxOrd { get; set; }
+	[SugarColumn(ColumnName = "MaxOrd", ColumnDataType = "decimal(18,5)")]
+	public decimal MaxOrd { get; set; }
 
 
-	[SugarColumn(ColumnName = "MinOrd", ColumnDataType = "decimal(18,5)", IsNullable = true)]
-	public decimal? MinOrd { get; set; }
+	[SugarColumn(ColumnName = "MinOrd", ColumnDataType = "decimal(18,5)")]
+	public decimal MinOrd { get; set; }
 
 
-	[SugarColumn(ColumnName = "OrdMult", ColumnDataType = "decimal(18,5)", IsNullable = true)]
-	public decimal? OrdMult { get; set; }
+	[SugarColumn(ColumnName = "OrdMult", ColumnDataType = "decimal(18,5)")]
+	public decimal OrdMult { get; set; }
 
 
-	[SugarColumn(ColumnName = "MinOrdSales", ColumnDataType = "decimal(18,5)", IsNullable = true)]
-	public decimal? MinOrdSales { get; set; }
+	[SugarColumn(ColumnName = "MinOrdSales", ColumnDataType = "decimal(18,5)")]
+	public decimal MinOrdSales { get; set; }
 
 
 	[SugarColumn(ColumnName = "AutoLotNums", ColumnDataType = "tinyint(1)")]
 	[SugarColumn(ColumnName = "AutoLotNums", ColumnDataType = "tinyint(1)")]
 	public bool AutoLotNums { get; set; }
 	public bool AutoLotNums { get; set; }
@@ -121,8 +121,8 @@ public class ItemMaster
 	[SugarColumn(ColumnName = "Install", ColumnDataType = "tinyint(1)")]
 	[SugarColumn(ColumnName = "Install", ColumnDataType = "tinyint(1)")]
 	public bool Install { get; set; }
 	public bool Install { get; set; }
 
 
-	[SugarColumn(ColumnName = "SafetyStk", ColumnDataType = "decimal(18,5)", IsNullable = true)]
-	public decimal? SafetyStk { get; set; }
+	[SugarColumn(ColumnName = "SafetyStk", ColumnDataType = "decimal(18,5)")]
+	public decimal SafetyStk { get; set; }
 
 
 	[SugarColumn(ColumnName = "DaysBetweenPM", IsNullable = true)]
 	[SugarColumn(ColumnName = "DaysBetweenPM", IsNullable = true)]
 	public int? DaysBetweenPM { get; set; }
 	public int? DaysBetweenPM { get; set; }
@@ -130,8 +130,8 @@ public class ItemMaster
 	[SugarColumn(ColumnName = "ExpireAlarmDays", IsNullable = true)]
 	[SugarColumn(ColumnName = "ExpireAlarmDays", IsNullable = true)]
 	public int? ExpireAlarmDays { get; set; }
 	public int? ExpireAlarmDays { get; set; }
 
 
-	[SugarColumn(ColumnName = "StockTurnOver", ColumnDataType = "decimal(18,5)", IsNullable = true)]
-	public decimal? StockTurnOver { get; set; }
+	[SugarColumn(ColumnName = "StockTurnOver", ColumnDataType = "decimal(18,5)")]
+	public decimal StockTurnOver { get; set; }
 
 
 	[SugarColumn(ColumnName = "LotSerialControl", ColumnDataType = "tinyint(1)")]
 	[SugarColumn(ColumnName = "LotSerialControl", ColumnDataType = "tinyint(1)")]
 	public bool LotSerialControl { get; set; }
 	public bool LotSerialControl { get; set; }

+ 2 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/WorkOrder/Entity/MesMorder.cs

@@ -115,8 +115,8 @@ public class MesMorder
     [SugarColumn(ColumnName = "MaterialSituation", Length = 128, IsNullable = true)]
     [SugarColumn(ColumnName = "MaterialSituation", Length = 128, IsNullable = true)]
     public string? MaterialSituation { get; set; }
     public string? MaterialSituation { get; set; }
 
 
-    [SugarColumn(ColumnName = "urgent", IsNullable = true)]
-    public int? Urgent { get; set; }
+    [SugarColumn(ColumnName = "urgent")]
+    public int Urgent { get; set; }
 
 
     [SugarColumn(ColumnName = "create_by", IsNullable = true)]
     [SugarColumn(ColumnName = "create_by", IsNullable = true)]
     public long? CreateBy { get; set; }
     public long? CreateBy { get; set; }