Просмотр исходного кода

feat: 计划联动交付跟踪复用OrderDeliveryFlow、ASN发货方、UAT测试报告等多项优化

- LinkagePlan: 列表返回加businessId,交付跟踪复用OrderDeliveryFlow组件
- ASN Shipper: 新增下拉刷新、发货方信息表单等
- 订单交付流程: 优化展示逻辑
- 销售订单变更表单: 简化
- 工单下达列表: 细微调整
- 需求评审明细: 新增字段
- UAT测试报告: S1-S4更新及新增
- OrderChangeReviewBizHandler: 新增变更评审业务处理器
- 版本: Web 2.4.178 / Server 1.0.149
Pengxy 1 месяц назад
Родитель
Сommit
55baf7f006

+ 1 - 1
Web/package.json

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

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

@@ -99,3 +99,33 @@ export function fetchDepartmentOptions(keyword?: string) {
 		.get<SimpleKv[]>('/api/Order/asnshipper/options/departments', { params: { keyword } })
 		.then((r) => r.data);
 }
+
+export function fetchLocationOptions(keyword?: string) {
+	return service
+		.get<SimpleKv[]>('/api/Order/asnshipper/options/locations', { params: { keyword } })
+		.then((r) => r.data);
+}
+
+/** 销售单物料明细行(发货单新建时自动填入) */
+export interface SalesOrdEntryItem {
+	entrySeq?: number | null;
+	itemNumber?: string | null;
+	itemName?: string | null;
+	specification?: string | null;
+	unit?: string | null;
+	qty?: number | null;
+}
+
+/** 销售单明细响应 */
+export interface SalesOrdEntriesData {
+	ordNbr: string;
+	customNo?: string | null;
+	customName?: string | null;
+	entries: SalesOrdEntryItem[];
+}
+
+export function fetchSalesOrdEntries(ordNbr: string) {
+	return service
+		.get<SalesOrdEntriesData>(`/api/Order/asnshipper/salesord/${encodeURIComponent(ordNbr)}/entries`)
+		.then((r) => r.data);
+}

+ 7 - 0
Web/src/views/aidop/api/linkagePlan.ts

@@ -56,6 +56,7 @@ export interface LinkagePlanRow {
 	fyEndTime?: string | null;
 	levelNum?: number | null;
 	background?: string | null;
+	businessId?: number | null;
 }
 
 export interface LinkagePlanDetail extends LinkagePlanRow {
@@ -78,4 +79,10 @@ export function refreshLinkagePlanData() {
 	return service
 		.post<unknown>('/api/Order/linkageplan/refresh')
 		.then((r) => unwrapFurionBody<unknown>(r.data));
+}
+
+export function runLinkagePlanProcedure() {
+	return service
+		.post<unknown>('/api/Order/linkageplan/run')
+		.then((r) => unwrapFurionBody<unknown>(r.data));
 }

+ 72 - 4
Web/src/views/aidop/business/asnShipperForm.vue

@@ -131,10 +131,20 @@
 						<span v-else>{{ row.um }}</span>
 					</template>
 				</el-table-column>
-				<el-table-column label="库位" width="100">
+				<el-table-column label="库位" width="180">
 					<template #default="{ row, $index }">
 						<template v-if="editingRows.has($index) && !isView">
-							<el-input v-model="row.location" size="small" />
+							<el-select
+								v-model="row.location"
+								filterable
+								placeholder="选择库位"
+								size="small"
+								style="width: 100%"
+								@focus="searchLocations()"
+								@visible-change="(v: boolean) => v && searchLocations()"
+							>
+								<el-option v-for="o in locationOptions" :key="o.value" :label="o.label" :value="o.value" />
+							</el-select>
 						</template>
 						<span v-else>{{ row.location }}</span>
 					</template>
@@ -223,6 +233,8 @@ import {
 	fetchAsnShipperDetail,
 	fetchCustomerOptions,
 	fetchDepartmentOptions,
+	fetchLocationOptions,
+	fetchSalesOrdEntries,
 	fetchSalesOrdOptions,
 	saveAsnShipper,
 	type AsnShipperDetailItem,
@@ -318,9 +330,65 @@ async function searchDepartments(kw: string) {
 	}
 }
 
-function onSalesOrdChange(val: string) {
+// ── 库位下拉 ──
+const locationOptions = ref<SimpleKv[]>([]);
+const locationLoading = ref(false);
+
+async function searchLocations(kw?: string) {
+	locationLoading.value = true;
+	try {
+		locationOptions.value = (await fetchLocationOptions(kw || undefined)) ?? [];
+	} finally {
+		locationLoading.value = false;
+	}
+}
+
+async function onSalesOrdChange(val: string) {
 	// 销售单选择后同步到 ordNbr
 	form.ordNbr = val;
+	if (!val) {
+		form.soldTo = null;
+		form.details = [];
+		return;
+	}
+	// 新建模式下,选择销售单后自动拉取物料明细填充到发货明细行
+	if (!isCreate.value) return;
+	try {
+		const data = await fetchSalesOrdEntries(val);
+		// 自动填充客户
+		if (data.customNo) {
+			form.soldTo = data.customNo;
+			// 回填客户下拉选项,确保显示"编号+名称"
+			const custLabel = data.customName
+				? `${data.customNo} ${data.customName}`
+				: data.customNo;
+			customerOptions.value = [{ value: data.customNo, label: custLabel }];
+		}
+		// 自动填充明细行
+		if (data.entries && data.entries.length > 0) {
+			form.details = data.entries.map((e, idx) => ({
+				recID: null,
+				line: idx + 1,
+				ordNbr: data.ordNbr,
+				ordLine: e.entrySeq ?? null,
+				containerItem: e.itemNumber ?? '',
+				descr: e.itemName ?? '',
+				um: e.unit ?? '',
+				location: '',
+				lotSerial: '',
+				qtyToShip: e.qty ?? 0,
+				pickingQty: null,
+				realQty: null,
+				status: '',
+				remark: '',
+			} as AsnShipperDetailItem));
+			ElMessage.success(`已自动填入 ${data.entries.length} 行物料明细`);
+		} else {
+			form.details = [];
+		}
+	} catch (e: any) {
+		ElMessage.warning('获取销售单明细失败:' + (e?.message || '未知错误'));
+	}
 }
 
 // ── 加载详情 ──
@@ -443,7 +511,7 @@ function applyPresetRows() {
 }
 
 onMounted(async () => {
-	await Promise.all([searchSalesOrds(''), searchCustomers(''), searchDepartments('')]);
+	await Promise.all([searchSalesOrds(''), searchCustomers(''), searchDepartments(''), searchLocations()]);
 	if (props.mode === 'create') {
 		applyPresetRows();
 	} else {

+ 31 - 106
Web/src/views/aidop/business/linkagePlanList.vue

@@ -21,7 +21,8 @@
 				<el-form-item>
 					<el-button type="primary" @click="handleSearch">查询</el-button>
 					<el-button @click="handleReset">重置</el-button>
-					<el-button type="success" @click="handleRefreshData">刷新数据</el-button>
+					<el-button :loading="loading" @click="refreshList">刷新</el-button>
+					<el-button type="success" @click="handleRefreshData">同步数据</el-button>
 				</el-form-item>
 			</el-form>
 		</div>
@@ -121,92 +122,9 @@
 			/>
 		</div>
 
-		<!-- 交付跟踪弹窗 -->
-		<el-dialog v-model="trackDialogVisible" title="交付跟踪" width="80%" destroy-on-close>
-			<div v-if="trackingData">
-				<el-descriptions :column="2" border>
-					<el-descriptions-item label="订单编号">{{ trackingData.billNo }}</el-descriptions-item>
-					<el-descriptions-item label="客户编码">{{ trackingData.customNo }}</el-descriptions-item>
-					<el-descriptions-item label="物料代码">{{ trackingData.itemNumber }}</el-descriptions-item>
-					<el-descriptions-item label="物料名称">{{ trackingData.descr }}</el-descriptions-item>
-					<el-descriptions-item label="规格型号">{{ trackingData.descr1 }}</el-descriptions-item>
-					<el-descriptions-item label="数量">{{ trackingData.qty }}</el-descriptions-item>
-				</el-descriptions>
-				
-				<el-divider content-position="left">时间节点</el-divider>
-				
-				<el-timeline>
-					<el-timeline-item v-if="trackingData.updateTime" :timestamp="trackingData.updateTime" placement="top">
-						生产通知
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.bomStart" :timestamp="trackingData.bomStart" placement="top">
-						BOM设计开始
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.bomEnd" :timestamp="trackingData.bomEnd" placement="top">
-						BOM设计结束
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.lineStart" :timestamp="trackingData.lineStart" placement="top">
-						工艺设计开始
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.lineEnd" :timestamp="trackingData.lineEnd" placement="top">
-						工艺设计结束
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.needTime" :timestamp="trackingData.needTime" placement="top">
-						物料需求
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.cgNeedTime" :timestamp="trackingData.cgNeedTime" placement="top">
-						采购下单
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.cgEnd" :timestamp="trackingData.cgEnd" placement="top">
-						采购到货
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.blStart" :timestamp="trackingData.blStart" placement="top">
-						备料开始
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.blEnd" :timestamp="trackingData.blEnd" placement="top">
-						备料结束
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.productStart" :timestamp="trackingData.productStart" placement="top">
-						生产开始
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.productEnd" :timestamp="trackingData.productEnd" placement="top">
-						生产结束
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.startTime" :timestamp="trackingData.startTime" placement="top">
-						报工开始
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.endTime" :timestamp="trackingData.endTime" placement="top">
-						报工结束
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.ipqcJyStart" :timestamp="trackingData.ipqcJyStart" placement="top">
-						IQC检验开始
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.ipqcJyEnd" :timestamp="trackingData.ipqcJyEnd" placement="top">
-						IQC检验结束
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.fqcJyStart" :timestamp="trackingData.fqcJyStart" placement="top">
-						FQC检验开始
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.fqcJyEnd" :timestamp="trackingData.fqcJyEnd" placement="top">
-						FQC检验结束
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.rkStart" :timestamp="trackingData.rkStart" placement="top">
-						入库开始
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.rkEnd" :timestamp="trackingData.rkEnd" placement="top">
-						入库结束
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.fyStartTime" :timestamp="trackingData.fyStartTime" placement="top">
-						发运开始
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.fyEndTime" :timestamp="trackingData.fyEndTime" placement="top">
-						发运结束
-					</el-timeline-item>
-					<el-timeline-item v-if="trackingData.sysCapacityDate" :timestamp="trackingData.sysCapacityDate" placement="top">
-						合同交期
-					</el-timeline-item>
-				</el-timeline>
-			</div>
+		<!-- 交付跟踪(价值流) -->
+		<el-dialog v-model="flowVisible" title="交付跟踪" width="1200px" destroy-on-close>
+			<OrderDeliveryFlow :entry-id="flowEntryId" />
 		</el-dialog>
 	</div>
 </template>
@@ -214,12 +132,15 @@
 <script setup lang="ts" name="aidopBusinessLinkagePlanList">
 import { onMounted, reactive, ref } from 'vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
-import { fetchLinkagePlanList, fetchLinkagePlanDetail, refreshLinkagePlanData, type LinkagePlanRow, type LinkagePlanDetail } from '../api/linkagePlan';
+import { fetchLinkagePlanList, refreshLinkagePlanData, runLinkagePlanProcedure, type LinkagePlanRow } from '../api/linkagePlan';
+import OrderDeliveryFlow from './orderDeliveryFlow.vue';
 
 const loading = ref(false);
 const tableData = ref<LinkagePlanRow[]>([]);
-const trackDialogVisible = ref(false);
-const trackingData = ref<LinkagePlanDetail | null>(null);
+
+// ── 交付跟踪(价值流)──
+const flowVisible = ref(false);
+const flowEntryId = ref<number | null>(null);
 
 const searchForm = reactive({
 	billNo: '',
@@ -261,6 +182,20 @@ async function loadData() {
 	}
 }
 
+async function refreshList() {
+	try {
+		loading.value = true;
+		await runLinkagePlanProcedure();
+		ElMessage.success('计划联动刷新成功');
+		await loadData();
+	} catch (error: any) {
+		console.error('刷新失败:', error);
+		ElMessage.error(error?.message || '刷新失败');
+	} finally {
+		loading.value = false;
+	}
+}
+
 function handleSearch() {
 	pagination.page = 1;
 	loadData();
@@ -286,15 +221,13 @@ function handleCurrentChange(page: number) {
 	loadData();
 }
 
-async function handleTrackDelivery(row: LinkagePlanRow) {
-	try {
-		const detail = await fetchLinkagePlanDetail(row.id);
-		trackingData.value = detail;
-		trackDialogVisible.value = true;
-	} catch (error) {
-		console.error('获取详情失败:', error);
-		ElMessage.error('获取详情失败');
+function handleTrackDelivery(row: LinkagePlanRow) {
+	if (row.businessId == null) {
+		ElMessage.warning('该行无关联订单明细,无法跟踪');
+		return;
 	}
+	flowEntryId.value = row.businessId;
+	flowVisible.value = true;
 }
 
 function getRowStyle({ row }: { row: LinkagePlanRow }) {
@@ -366,12 +299,4 @@ onMounted(() => {
 		}
 	}
 }
-
-:deep(.el-timeline) {
-	padding-left: 0;
-	
-	.el-timeline-item {
-		padding-bottom: 12px;
-	}
-}
 </style>

+ 27 - 21
Web/src/views/aidop/business/orderDeliveryFlow.vue

@@ -7,29 +7,29 @@
 				<el-row :gutter="20">
 					<el-col :span="8">
 						<el-form-item label="物料编码:">
-							<el-input :model-value="data.basic.itemNumber" readonly />
+							<el-input :model-value="nv(data.basic.itemNumber)" readonly />
 						</el-form-item>
 					</el-col>
 					<el-col :span="8">
 						<el-form-item label="物料名称:">
-							<el-input :model-value="data.basic.itemName" readonly />
+							<el-input :model-value="nv(data.basic.itemName)" readonly />
 						</el-form-item>
 					</el-col>
 					<el-col :span="8">
 						<el-form-item label="规格型号:">
-							<el-input :model-value="data.basic.specification" readonly />
+							<el-input :model-value="nv(data.basic.specification)" readonly />
 						</el-form-item>
 					</el-col>
 				</el-row>
 				<el-row :gutter="20">
 					<el-col :span="8">
 						<el-form-item label="订单数量:">
-							<el-input :model-value="data.basic.qty" readonly />
+							<el-input :model-value="nv(data.basic.qty)" readonly />
 						</el-form-item>
 					</el-col>
 					<el-col :span="8">
 						<el-form-item label="客户单号:">
-							<el-input :model-value="data.basic.billFrom" readonly />
+							<el-input :model-value="nv(data.basic.billFrom)" readonly />
 						</el-form-item>
 					</el-col>
 					<el-col :span="8">
@@ -58,18 +58,18 @@
 			<!-- Tab1 订单信息 -->
 			<el-tab-pane label="订单信息" name="tab1">
 				<el-descriptions v-if="data?.tab1" :column="3" border size="small">
-					<el-descriptions-item label="订单编号">{{ data.tab1.billNo }}</el-descriptions-item>
-					<el-descriptions-item label="客户名称">{{ data.tab1.customName }}</el-descriptions-item>
-					<el-descriptions-item label="客户级别">{{ data.tab1.customLevel }}</el-descriptions-item>
-					<el-descriptions-item label="状态">{{ data.tab1.state }}</el-descriptions-item>
+					<el-descriptions-item label="订单编号">{{ nv(data.tab1.billNo) }}</el-descriptions-item>
+					<el-descriptions-item label="客户名称">{{ nv(data.tab1.customName) }}</el-descriptions-item>
+					<el-descriptions-item label="客户级别">{{ nv(data.tab1.customLevel) }}</el-descriptions-item>
+					<el-descriptions-item label="状态">{{ nv(data.tab1.state) }}</el-descriptions-item>
 					<el-descriptions-item label="签订日期">{{ fmtDate(data.tab1.date) }}</el-descriptions-item>
-					<el-descriptions-item label="制单人">{{ data.tab1.createByName }}</el-descriptions-item>
+					<el-descriptions-item label="制单人">{{ nv(data.tab1.createByName) }}</el-descriptions-item>
 					<el-descriptions-item label="订单类别">{{ orderTypeLabel(data.tab1.orderType) }}</el-descriptions-item>
 					<el-descriptions-item label="审核日期">{{ fmtDate(data.tab1.auditDate) }}</el-descriptions-item>
-					<el-descriptions-item label="审核人">{{ data.tab1.auditor }}</el-descriptions-item>
-					<el-descriptions-item label="订单来源">{{ data.tab1.billFrom }}</el-descriptions-item>
-					<el-descriptions-item label="业务员">{{ data.tab1.empName }}</el-descriptions-item>
-					<el-descriptions-item label="业务员编号">{{ data.tab1.empNo }}</el-descriptions-item>
+					<el-descriptions-item label="审核人">{{ nv(data.tab1.auditor) }}</el-descriptions-item>
+					<el-descriptions-item label="订单来源">{{ nv(data.tab1.billFrom) }}</el-descriptions-item>
+					<el-descriptions-item label="业务员">{{ nv(data.tab1.empName) }}</el-descriptions-item>
+					<el-descriptions-item label="业务员编号">{{ nv(data.tab1.empNo) }}</el-descriptions-item>
 				</el-descriptions>
 			</el-tab-pane>
 
@@ -81,34 +81,34 @@
 						<el-row :gutter="20">
 							<el-col :span="8">
 								<el-form-item label="系统预计交货日期:">
-									<el-input :model-value="data.tab2.review.sysCapacityDate" readonly />
+									<el-input :model-value="nv(data.tab2.review.sysCapacityDate)" readonly />
 								</el-form-item>
 							</el-col>
 							<el-col :span="8">
 								<el-form-item label="新增工单:">
-									<el-input :model-value="data.tab2.review.morderNo" readonly />
+									<el-input :model-value="nv(data.tab2.review.morderNo)" readonly />
 								</el-form-item>
 							</el-col>
 							<el-col :span="8">
 								<el-form-item label="交期评审确认:">
-									<el-input :model-value="data.tab2.review.createTime" readonly />
+									<el-input :model-value="nv(data.tab2.review.createTime)" readonly />
 								</el-form-item>
 							</el-col>
 						</el-row>
 						<el-row :gutter="20">
 							<el-col :span="8">
 								<el-form-item label="新增工单数量:">
-									<el-input :model-value="data.tab2.review.makeQty" readonly />
+									<el-input :model-value="nv(data.tab2.review.makeQty)" readonly />
 								</el-form-item>
 							</el-col>
 							<el-col :span="8">
 								<el-form-item label="可用库存占用:">
-									<el-input :model-value="data.tab2.review.useQty" readonly />
+									<el-input :model-value="nv(data.tab2.review.useQty)" readonly />
 								</el-form-item>
 							</el-col>
 							<el-col :span="8">
 								<el-form-item label="在制占用量:">
-									<el-input :model-value="data.tab2.review.moQty" readonly />
+									<el-input :model-value="nv(data.tab2.review.moQty)" readonly />
 								</el-form-item>
 							</el-col>
 						</el-row>
@@ -124,7 +124,7 @@
 					<el-table-column prop="changeReason" label="变更原因" min-width="140" show-overflow-tooltip />
 					<el-table-column prop="changeContent" label="变更内容" min-width="160" show-overflow-tooltip />
 					<el-table-column label="申请时间" width="160" align="center">
-						<template #default="{ row }">{{ row.updateTime }}</template>
+						<template #default="{ row }">{{ fmtDateTime(row.updateTime) }}</template>
 					</el-table-column>
 				</el-table>
 			</el-tab-pane>
@@ -408,6 +408,12 @@ function fmtDateTime(v?: string | null) {
 	return v.slice(0, 16).replace('T', ' ');
 }
 
+function nv(v: unknown) {
+	if (v === null || v === undefined) return '';
+	const s = String(v);
+	return s === 'null' || s === 'Null' || s === 'NULL' ? '' : s;
+}
+
 function orderTypeLabel(v?: number | null) {
 	return v === 1 ? '销售' : v === 2 ? '计划' : '—';
 }

+ 30 - 3
Web/src/views/aidop/business/orderList.vue

@@ -62,11 +62,12 @@
 				</template>
 			</el-table-column>
 
-			<el-table-column label="行操作" width="220" fixed="right" align="center">
+			<el-table-column label="行操作" width="300" fixed="right" align="center">
 				<template #default="{ row }">
 					<el-button link type="primary" v-if="canEdit(row)" @click="openEdit(row)">编辑</el-button>
 					<el-button link type="primary" @click="openView(row)">查看</el-button>
 					<el-button link type="warning" v-if="canChange(row)" @click="openChange(row)">变更</el-button>
+					<el-button link type="success" v-if="canReorder(row)" @click="onReorder(row)">3级计划重排</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -96,7 +97,7 @@
 <script setup lang="ts" name="aidopBusinessOrderList">
 import { computed, onMounted, reactive, ref } from 'vue';
 import { useRoute } from 'vue-router';
-import { ElMessage } from 'element-plus';
+import { ElMessage, ElMessageBox } from 'element-plus';
 import { formatDate } from '/@/utils/formatTime';
 import AidopDemoShell from '../components/AidopDemoShell.vue';
 import SalesOrderForm from './salesOrderForm.vue';
@@ -183,13 +184,39 @@ function flowStateTagType(state?: string | null) {
 }
 
 function canEdit(r: SeOrderReviewRow) {
-	return r.state === '新建' || r.state === '评审';
+	if (r.state === '新建' || r.state === '评审') return true;
+	if (r.state === '再评审' && r.flowState === '审批完成') return true;
+	return false;
 }
 
 function canChange(r: SeOrderReviewRow) {
 	return r.state === '确认' && r.flowState !== '审批中';
 }
 
+function canReorder(r: SeOrderReviewRow) {
+	return r.state === '再评审' && r.flowState === '审批完成';
+}
+
+async function onReorder(row: SeOrderReviewRow) {
+	try {
+		await ElMessageBox.confirm(
+			'请确认订单信息已修改完,此按钮只能点击一次',
+			'3级计划重排',
+			{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
+		);
+		const resp = await fetch(
+			`http://123.60.180.165:8087/api/business/resource-examine/OrderChangeRefresh?ids=${row.id}`,
+			{ method: 'POST' }
+		);
+		if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+		ElMessage.success('3级计划重排成功');
+		await loadList();
+	} catch (e: any) {
+		if (e === 'cancel' || e === 'close') return;
+		ElMessage.error(e?.message || '3级计划重排请求失败');
+	}
+}
+
 function resetQuery() {
 	query.billNo = '';
 	query.customNo = '';

+ 6 - 21
Web/src/views/aidop/business/salesOrderChangeForm.vue

@@ -69,17 +69,16 @@
 		<!-- 审批流程面板 -->
 		<ApprovalPanel
 			v-if="form.seOrderId"
-			bizType="ORDER_REVIEW"
+			bizType="ORDER_CHANGE_REVIEW"
 			:bizId="form.seOrderId"
 			:bizNo="form.billNo"
-			:title="`订单评审-${form.billNo}`"
+			:title="`订单变更审批-${form.billNo}`"
 			@refresh="loadDetail"
 		/>
 
 		<div class="footer">
 			<el-button @click="$emit('cancel')">关闭</el-button>
 			<el-button :loading="saving" @click="onSave">保存</el-button>
-			<el-button type="primary" :loading="submitting" @click="onSubmit">发送</el-button>
 		</div>
 	</div>
 </template>
@@ -95,7 +94,6 @@ const emit = defineEmits<{ (e: 'cancel'): void; (e: 'submitted'): void }>();
 
 const loading = ref(false);
 const saving = ref(false);
-const submitting = ref(false);
 const formRef = ref<FormInstance>();
 
 const form = reactive<SeOrderChangeSave>({
@@ -148,32 +146,19 @@ async function loadDetail() {
 	}
 }
 
-async function doSave(submit: boolean) {
+async function onSave() {
 	await formRef.value?.validate().catch(() => Promise.reject());
 	if (!props.orderId) return;
-	await createSeOrderChange(props.orderId, form);
-	ElMessage.success(submit ? '已发送变更申请' : '已保存');
-	emit('submitted');
-}
-
-async function onSave() {
 	saving.value = true;
 	try {
-		await doSave(false);
+		await createSeOrderChange(props.orderId, form);
+		ElMessage.success('已保存');
+		emit('submitted');
 	} finally {
 		saving.value = false;
 	}
 }
 
-async function onSubmit() {
-	submitting.value = true;
-	try {
-		await doSave(true);
-	} finally {
-		submitting.value = false;
-	}
-}
-
 onMounted(loadDetail);
 </script>
 

+ 3 - 1
Web/src/views/aidop/business/workOrderDispatchList.vue

@@ -44,7 +44,9 @@
 		>
 			<el-table-column type="selection" width="44" fixed="left" />
 			<el-table-column prop="workOrd" label="工单编号" width="140" fixed="left" show-overflow-tooltip sortable />
-			<el-table-column prop="priority" label="优先级" width="80" align="center" sortable />
+			<el-table-column label="优先级" width="90" align="center" sortable>
+				<template #default="{ row }">{{ row.priority != null ? Number(row.priority).toFixed(2) : '' }}</template>
+			</el-table-column>
 			<el-table-column prop="itemNum" label="物料编码" width="130" show-overflow-tooltip sortable />
 			<el-table-column prop="descr" label="物料名称" min-width="130" show-overflow-tooltip sortable />
 			<el-table-column prop="descr1" label="规格型号" min-width="120" show-overflow-tooltip sortable />

+ 13 - 0
Web/src/views/aidop/s1/sales-kanban/RequirementExamineDetailList.vue

@@ -50,6 +50,9 @@
 		</div>
 
 		<template v-else>
+			<div class="toolbar">
+				<el-button type="primary" :loading="loading" @click="refreshList">刷新</el-button>
+			</div>
 			<el-table
 				v-loading="loading"
 				:data="treeData"
@@ -261,6 +264,10 @@ function doSearch() {
 	void loadList();
 }
 
+function refreshList() {
+	void loadList();
+}
+
 function resetQuery() {
 	query.billNo = '';
 	query.morderNo = '';
@@ -279,6 +286,12 @@ function resetQuery() {
 .mb12 {
 	margin-bottom: 12px;
 }
+.toolbar {
+	display: flex;
+	align-items: center;
+	gap: 8px;
+	margin-bottom: 12px;
+}
 .hint-box {
 	padding: 24px 0;
 }

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


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


BIN
doc/S3供应协同_UAT测试报告v1.0.xlsx


BIN
doc/S4采购执行_UAT测试报告v1.0.xlsx


+ 15 - 51
doc/tools/GenerateUatReport/Program.cs

@@ -1,64 +1,28 @@
 using ClosedXML.Excel;
 
-var path = @"d:\DEMONET\doc\S1产销协同_UAT测试报告v1.0.xlsx";
+var path = @"d:\DEMONET\doc\S4采购执行_UAT测试报告v1.0.xlsx";
 using var wb = new XLWorkbook(path);
 
-var outputPath = @"d:\DEMONET\doc\report_verify.txt";
-using var writer = new StreamWriter(outputPath, false, System.Text.Encoding.UTF8);
-
-writer.WriteLine("=== Sheets ===");
 int totalNormal = 0, totalEx = 0;
 foreach (var ws in wb.Worksheets)
 {
     var lastRow = ws.LastRowUsed()?.RowNumber() ?? 0;
-    writer.WriteLine($"  [{ws.Name}] rows={lastRow}");
-    
-    // Count exception rows (colored)
-    if (ws.Name != "目录")
+    Console.WriteLine($"  [{ws.Name}] rows={lastRow}");
+    if (ws.Name == "目录") continue;
+    for (int r = 12; r <= lastRow; r++)
     {
-        for (int r = 12; r <= lastRow; r++)
+        var cell = ws.Cell(r, 1);
+        if (cell.IsEmpty()) continue;
+        var fill = cell.Style.Fill.BackgroundColor;
+        var val = cell.Value.ToString();
+        if (fill.HasValue && fill.ToString() == "FFFCE4D6")
         {
-            var cell = ws.Cell(r, 1);
-            if (!cell.IsEmpty())
-            {
-                var fill = cell.Style.Fill.BackgroundColor;
-                if (fill.HasValue && fill.ToString() == "FFFCE4D6")
-                    totalEx++;
-                else if (!fill.HasValue || fill.ToString() == "FFFFFFFF" || fill.ToString() == "FFFFFF")
-                {
-                    var val = cell.Value.ToString();
-                    if (!val.Contains("【异常】") && !val.Contains("▼"))
-                        totalNormal++;
-                }
-            }
+            if (!val.Contains("▼") && !val.Contains("注:")) totalEx++;
         }
+        else if (val.StartsWith("4.") && !val.Contains("[异常]"))
+            totalNormal++;
     }
 }
-writer.WriteLine();
-writer.WriteLine($"Total normal cases: {totalNormal}");
-writer.WriteLine($"Total exception cases: {totalEx}");
-writer.WriteLine($"Grand total: {totalNormal + totalEx}");
-
-writer.WriteLine();
-writer.WriteLine("=== 1 合同评审 (all rows) ===");
-var ws1 = wb.Worksheet("1 合同评审");
-var lastR = ws1.LastRowUsed()?.RowNumber() ?? 0;
-for (int r = 1; r <= lastR; r++)
-{
-    var vals = new List<string>();
-    for (int c = 1; c <= 20; c++)
-    {
-        var cell = ws1.Cell(r, c);
-        if (!cell.IsEmpty())
-        {
-            var fill = cell.Style.Fill.BackgroundColor;
-            var tag = fill.HasValue && fill.ToString() == "FFFCE4D6" ? "[EX]" : "";
-            vals.Add($"{cell.Address}{tag}={cell.Value}");
-        }
-    }
-    if (vals.Count > 0)
-        writer.WriteLine($"  Row {r}: {string.Join(" | ", vals)}");
-}
-
-writer.WriteLine();
-writer.WriteLine("=== DONE ===");
+Console.WriteLine($"正常用例: {totalNormal}");
+Console.WriteLine($"异常用例: {totalEx}");
+Console.WriteLine($"合计: {totalNormal + totalEx}");

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

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.148</AssemblyVersion>
-    <FileVersion>1.0.148</FileVersion>
-    <Version>1.0.148</Version>
+    <AssemblyVersion>1.0.149</AssemblyVersion>
+    <FileVersion>1.0.149</FileVersion>
+    <Version>1.0.149</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 70 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Order/AsnShipperService.cs

@@ -333,6 +333,46 @@ public class AsnShipperService : IDynamicApiController, ITransient
         return new { message = "删除成功" };
     }
 
+    // ══════════════════════════════════════════════════════════════
+    // 销售单明细行 GET /api/Order/asnshipper/salesord/{ordNbr}/entries
+    // ══════════════════════════════════════════════════════════════
+    /// <summary>获取销售单明细行(用于发货单新建时自动填入)🚚</summary>
+    [DisplayName("获取销售单明细行")]
+    [HttpGet("asnshipper/salesord/{ordNbr}/entries")]
+    public async Task<object> GetSalesOrdEntries(string ordNbr)
+    {
+        var tenantId = _userManager.TenantId;
+
+        // 查询销售单主表,获取客户信息
+        var order = await _db.Queryable<SeOrder>()
+            .Where(u => u.BillNo == ordNbr && u.TenantId == tenantId && u.IsDeleted == 0)
+            .Select(u => new { u.CustomNo, u.CustomName })
+            .FirstAsync();
+
+        // 查询销售单明细行(物料信息)
+        var entries = await _db.Queryable<SeOrderEntry>()
+            .Where(u => u.BillNo == ordNbr && u.TenantId == tenantId && u.IsDeleted == 0)
+            .OrderBy(u => u.EntrySeq)
+            .Select(u => new
+            {
+                entrySeq = u.EntrySeq,
+                itemNumber = u.ItemNumber,
+                itemName = u.ItemName,
+                specification = u.Specification,
+                unit = u.Unit,
+                qty = u.Qty,
+            })
+            .ToListAsync();
+
+        return new
+        {
+            ordNbr,
+            customNo = order?.CustomNo,
+            customName = order?.CustomName,
+            entries,
+        };
+    }
+
     // ══════════════════════════════════════════════════════════════
     // 下拉数据源 GET /api/Order/asnshipper/options/salesords
     // ══════════════════════════════════════════════════════════════
@@ -424,6 +464,36 @@ public class AsnShipperService : IDynamicApiController, ITransient
         return list;
     }
 
+    // ══════════════════════════════════════════════════════════════
+    // 下拉数据源 GET /api/Order/asnshipper/options/locations
+    // ══════════════════════════════════════════════════════════════
+    /// <summary>获取库位下拉列表 🚚</summary>
+    [DisplayName("获取库位下拉")]
+    [HttpGet("asnshipper/options/locations")]
+    public async Task<object> GetLocationOptions([FromQuery] string? keyword)
+    {
+        var tenantId = _userManager.TenantId;
+        var pars = new List<SugarParameter> { new("@TenantId", tenantId) };
+        var where = "tenant_id = @TenantId AND is_active = 1";
+        if (!string.IsNullOrWhiteSpace(keyword))
+        {
+            where += " AND (location LIKE @kw OR descr LIKE @kw)";
+            pars.Add(new SugarParameter("@kw", $"%{keyword.Trim()}%"));
+        }
+
+        var sql = $"""
+            SELECT location AS Value,
+                TRIM(CONCAT(location, ' ', IFNULL(descr, ''))) AS Label
+            FROM LocationMaster
+            WHERE {where}
+            ORDER BY location
+            LIMIT 100
+            """;
+
+        var list = await _db.Ado.SqlQueryAsync<SimpleKvRow>(sql, pars);
+        return list;
+    }
+
     // ──────────────── 号生成 ────────────────
 
     /// <summary>

+ 5 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Order/Entity/LinkagePlan.cs

@@ -4,7 +4,7 @@
 [SugarTable("LinkagePlan", "璁″垝鑱斿姩鐪嬫澘")]
 public class LinkagePlan
 {
-    [SugarColumn(ColumnName = "id", IsPrimaryKey = true, IsIdentity = false)]
+    [SugarColumn(ColumnName = "id", IsPrimaryKey = true, IsIdentity = true)]
     public long Id { get; set; }
 
     /// <summary>璁㈠崟缂栧彿</summary>
@@ -144,10 +144,13 @@ public class LinkagePlan
     public int? LevelNum { get; set; }
 
     /// <summary>鑳屾櫙鑹?/summary>
-    [SugarColumn(ColumnName = "background", Length = 32, IsNullable = true)]
+    [SugarColumn(ColumnName = "background", Length = 3200, IsNullable = true)]
     public string? Background { get; set; }
 
     /// <summary>绉熸埛ID锛堝绉熸埛闅旂锛?/summary>
     [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
     public long? TenantId { get; set; }
+
+    [SugarColumn(ColumnName = "businessid", IsNullable = true)]
+    public long? BusinessId { get; set; }
 }

+ 16 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Order/LinkagePlanService.cs

@@ -79,6 +79,7 @@ public class LinkagePlanService : IDynamicApiController, ITransient
             fyEndTime = u.FyEndTime,
             levelNum = u.LevelNum,
             background = u.Background,
+            businessId = u.BusinessId,
         }).ToList();
 
         return new { total = paged.Total, page = input.Page, pageSize = input.PageSize, list };
@@ -155,4 +156,19 @@ public class LinkagePlanService : IDynamicApiController, ITransient
             throw Oops.Oh($"刷新数据失败: {ex.Message}");
         }
     }
+
+    [DisplayName("执行计划联动存储过程")]
+    [HttpPost("linkageplan/run")]
+    public async Task<object> RunLinkagePlanProcedure()
+    {
+        try
+        {
+            await _db.Ado.ExecuteCommandAsync("CALL pr_Mes_LinkagePlan('')");
+            return new { message = "计划联动刷新成功" };
+        }
+        catch (Exception ex)
+        {
+            throw Oops.Oh($"执行存储过程失败: {ex.Message}");
+        }
+    }
 }

+ 86 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Order/OrderChangeReviewBizHandler.cs

@@ -0,0 +1,86 @@
+using Admin.NET.Plugin.ApprovalFlow;
+using Admin.NET.Plugin.ApprovalFlow.Service;
+
+namespace Admin.NET.Plugin.AiDOP.Order;
+
+/// <summary>
+/// 订单变更审批业务回调
+/// 注册 BizType = "ORDER_CHANGE_REVIEW"
+/// </summary>
+public class OrderChangeReviewBizHandler : IFlowBizHandler, ITransient
+{
+    public string BizType => "ORDER_CHANGE_REVIEW";
+
+    private readonly SqlSugarRepository<SeOrder> _seOrderRep;
+    private readonly SqlSugarRepository<SeOrderEntry> _seOrderEntryRep;
+
+    public OrderChangeReviewBizHandler(
+        SqlSugarRepository<SeOrder> seOrderRep,
+        SqlSugarRepository<SeOrderEntry> seOrderEntryRep)
+    {
+        _seOrderRep = seOrderRep;
+        _seOrderEntryRep = seOrderEntryRep;
+    }
+
+    public async Task OnFlowStarted(long bizId, long instanceId)
+    {
+        try
+        {
+            await _seOrderRep.AsUpdateable()
+                .SetColumns(o => o.FlowState == "审批中")
+                .Where(o => o.Id == bizId)
+                .ExecuteCommandAsync();
+        }
+        catch (Exception) { /* 表不存在时静默跳过 */ }
+    }
+
+    public async Task OnFlowCompleted(long bizId, long instanceId, FlowInstanceStatusEnum finalStatus, long? lastApproverId)
+    {
+        var state = finalStatus switch
+        {
+            FlowInstanceStatusEnum.Approved => "审批完成",
+            FlowInstanceStatusEnum.Rejected => "已拒绝",
+            FlowInstanceStatusEnum.Cancelled => "已撤销",
+            _ => "已终止",
+        };
+
+        try
+        {
+            await _seOrderRep.AsUpdateable()
+                .SetColumns(o => o.FlowState == state)
+                .Where(o => o.Id == bizId)
+                .ExecuteCommandAsync();
+
+            // 审批通过时将订单明细进度重置为 0(新建/再评审)
+            if (finalStatus == FlowInstanceStatusEnum.Approved)
+            {
+                await _seOrderEntryRep.AsUpdateable()
+                    .SetColumns(e => e.Progress == "0")
+                    .Where(e => e.SeOrderId == bizId)
+                    .ExecuteCommandAsync();
+            }
+        }
+        catch (Exception) { /* 表不存在时静默跳过 */ }
+    }
+
+    public Task OnNodeCompleted(long bizId, long instanceId, string nodeId, string nodeName, long? approverUserId)
+    {
+        return Task.CompletedTask;
+    }
+
+    public async Task<Dictionary<string, object>> GetBizData(long bizId)
+    {
+        try
+        {
+            var order = await _seOrderRep.GetByIdAsync(bizId);
+            if (order == null) return new Dictionary<string, object>();
+            return new Dictionary<string, object>
+            {
+                ["urgent"] = order.Urgent ?? 0,
+                ["customLevel"] = order.CustomLevel ?? 0,
+                ["orderType"] = order.OrderType ?? 0,
+            };
+        }
+        catch (Exception) { return new Dictionary<string, object>(); }
+    }
+}

+ 20 - 8
server/Plugins/Admin.NET.Plugin.AiDOP/Order/OrderDeliveryService.cs

@@ -102,7 +102,7 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
             NULL                                                        AS CustomOrderBillNo,
             NULL                                                        AS CustomOrderItemNo,
             NULL                                                        AS CustomLevel,
-            d.order_qty                                                 AS Qty,
+            COALESCE(se.qty, d.order_qty)                               AS Qty,
             d.shipped_qty                                               AS DeliverCount,
             NULL                                                        AS BomNumber,
             NULL                                                        AS EmpName,
@@ -125,8 +125,20 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
             d.actual_ship_date                                          AS ShipDate,
             NULL                                                        AS Recid,
             IF(IFNULL(d.planned_ship_qty, 0) > 0, '是', '否')          AS Spstatus,
-            NULL                                                        AS MoentryMono
+            me.mono_list                                                AS MoentryMono
         FROM dwd_ship_trans d
+        LEFT JOIN crm_seorderentry se
+               ON se.id = d.order_entry_id
+              AND se.tenant_id = d.tenant_id
+              AND se.IsDeleted = 0
+        LEFT JOIN (
+            SELECT soentry_id, tenant_id,
+                   GROUP_CONCAT(moentry_mono SEPARATOR ', ') AS mono_list
+            FROM mes_moentry
+            WHERE IsDeleted = 0
+            GROUP BY soentry_id, tenant_id
+        ) me ON me.soentry_id = d.order_entry_id
+            AND me.tenant_id = d.tenant_id
         WHERE {innerWhere}
         """;
 
@@ -179,7 +191,7 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
                 d.item_name          AS ItemName,
                 d.item_spec          AS Specification,
                 d.order_qty          AS Qty,
-                so.customer_order_no AS BillFrom,
+                COALESCE(so.customer_order_no, '') AS BillFrom,
                 d.plan_delivery_date AS PlanDate,
                 CASE
                     WHEN d.delivery_status = 'COMPLETED' THEN '7'
@@ -211,15 +223,15 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
                 so.order_no          AS BillNo,
                 so.customer_name     AS CustomName,
                 NULL                 AS CustomLevel,
-                so.flow_state        AS State,
+                COALESCE(so.flow_state, '')        AS State,
                 so.order_date        AS Date,
-                NULL                 AS CreateByName,
+                ''                 AS CreateByName,
                 so.order_type        AS OrderType,
                 so.update_time       AS AuditDate,
-                NULL                 AS Auditor,
+                ''                 AS Auditor,
                 so.customer_order_no AS BillFrom,
-                so.planner_name      AS EmpName,
-                so.planner_no        AS EmpNo
+                COALESCE(so.planner_name, '')      AS EmpName,
+                COALESCE(so.planner_no, '')        AS EmpNo
             FROM mdp_std_so so
             WHERE so.order_entry_id = @Id
               AND so.tenant_id = @TenantId