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

feat(S4): 采购退货单/IQC退货查询/供应商发货单多项优化 - 采购退货单: 主键RecID手动生成(MAX+1)、TenantId注入、已退数量>0隐藏删除按钮、选择收货单不填入已退数量 - IQC退货查询: 新增全量CSV导出按钮 - 供应商发货单: QC模式仅附件可编辑、状态驱动按钮显隐、tenant_id隔离 - 交货单关闭校验增加tenant_id、Upload白名单加text/csv

Pengxy 1 день назад
Родитель
Сommit
109e205348

+ 162 - 162
Web/package.json

@@ -1,164 +1,164 @@
 {
-	"name": "admin.net",
-	"type": "module",
-	"version": "2.4.191",
-	"packageManager": "pnpm@10.32.1",
-	"lastBuildTime": "2026.03.15",
-	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",
-	"author": "zuohuaijun",
-	"license": "MIT",
-	"scripts": {
-		"dev": "vite",
-		"build": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite build",
-		"prepare": "node scripts/install-git-hooks.cjs",
-		"lint-fix": "eslint --fix src/",
-		"format": "prettier --write .",
-		"build-api": "cd api_build/ && build.bat",
-		"build-approvalFlow-api": "cd api_build/ && build.bat approvalFlow",
-		"build-dingTalk-api": "cd api_build/ && build.bat dingTalk",
-		"build-goView-api": "cd api_build/ && build.bat goView",
-		"build-all-api": "npm run build-api && npm run build-approvalFlow-api &&  npm run build-dingTalk-api && npm run build-goView-api",
-		"build-api-ps": "cd api_build/ && pwsh -ExecutionPolicy Bypass -File build.ps1",
-		"build-approvalFlow-api-ps": "cd api_build/ && pwsh -ExecutionPolicy Bypass -File build.ps1 approvalFlow",
-		"build-dingTalk-api-ps": "cd api_build/ && pwsh -ExecutionPolicy Bypass -File build.ps1 dingTalk",
-		"build-goView-api-ps": "cd api_build/ && pwsh -ExecutionPolicy Bypass -File build.ps1 goView",
-		"build-all-api-ps": "npm run build-api-ps && npm run build-approvalFlow-api-ps &&  npm run build-dingTalk-api-ps && npm run build-goView-api-ps",
-		"translate": "node scripts/translate.cjs",
-		"test": "vitest",
-		"test:run": "vitest run"
-	},
-	"dependencies": {
-		"@element-plus/icons-vue": "^2.3.2",
-		"@logicflow/core": "^2.2.0",
-		"@logicflow/extension": "^2.2.0",
-		"@logicflow/vue-node-registry": "^1.2.0",
-		"@microsoft/signalr": "^10.0.0",
-		"@vue-office/docx": "^1.6.3",
-		"@vue-office/excel": "^1.7.14",
-		"@vue-office/pdf": "^2.0.10",
-		"@vueuse/core": "^14.2.1",
-		"@wangeditor/editor": "^5.1.23",
-		"@wangeditor/editor-for-vue": "^5.1.12",
-		"animate.css": "^4.1.1",
-		"async-validator": "^4.2.5",
-		"axios": "^1.13.6",
-		"countup.js": "^2.10.0",
-		"cropperjs": "^1.6.2",
-		"echarts": "^6.0.0",
-		"echarts-gl": "^2.0.9",
-		"echarts-wordcloud": "^2.1.0",
-		"element-plus": "^2.13.6",
-		"ezuikit-js": "^8.2.6",
-		"js-cookie": "^3.0.5",
-		"js-table2excel": "^1.1.2",
-		"json-editor-vue": "^0.18.1",
-		"jsplumb": "^2.15.6",
-		"lodash-es": "^4.17.23",
-		"md-editor-v3": "^6.4.0",
-		"mitt": "^3.0.1",
-		"monaco-editor": "^0.55.1",
-		"mqtt": "^5.15.0",
-		"nprogress": "^0.2.0",
-		"pinia": "^3.0.4",
-		"print-js": "^1.6.0",
-		"push.js": "^1.0.12",
-		"qrcodejs2-fixes": "^0.0.2",
-		"qs": "^6.15.0",
-		"relation-graph": "^2.2.11",
-		"screenfull": "^6.0.2",
-		"sm-crypto-v2": "^1.15.1",
-		"sortablejs": "^1.15.7",
-		"splitpanes": "^4.0.4",
-		"vcrontab-3": "^3.3.22",
-		"vform3-builds": "^3.0.10",
-		"vue": "^3.5.30",
-		"vue-clipboard3": "^2.0.0",
-		"vue-demi": "^0.14.10",
-		"vue-draggable-plus": "^0.6.1",
-		"vue-grid-layout": "3.0.0-beta1",
-		"vue-i18n": "^11.3.0",
-		"vue-json-pretty": "^2.6.0",
-		"vue-plugin-hiprint": "^0.0.60",
-		"vue-router": "^4.5.1",
-		"vue-signature-pad": "^3.0.2",
-		"vue3-tree-org": "^4.2.2",
-		"xlsx-js-style": "^1.2.0"
-	},
-	"devDependencies": {
-		"@eslint/eslintrc": "^3.3.5",
-		"@eslint/js": "^10.0.1",
-		"@playwright/test": "^1.59.1",
-		"@plugin-web-update-notification/vite": "^2.0.2",
-		"@rollup/pluginutils": "^5.3.0",
-		"@types/lodash-es": "^4.17.12",
-		"@types/node": "^22.19.15",
-		"@types/nprogress": "^0.2.3",
-		"@types/sortablejs": "^1.15.9",
-		"@typescript-eslint/eslint-plugin": "^8.57.0",
-		"@typescript-eslint/parser": "^8.57.0",
-		"@vitejs/plugin-vue": "^6.0.5",
-		"@vitejs/plugin-vue-jsx": "^5.1.5",
-		"@vue/compiler-sfc": "^3.5.30",
-		"cli-progress": "^3.12.0",
-		"code-inspector-plugin": "^1.4.4",
-		"colors": "^1.4.0",
-		"dotenv": "^17.3.1",
-		"esbuild": "^0.27.4",
-		"eslint": "^10.0.3",
-		"eslint-plugin-vue": "^10.8.0",
-		"globals": "^17.4.0",
-		"jsdom": "^26.1.0",
-		"less": "^4.6.4",
-		"prettier": "^3.8.1",
-		"rollup-plugin-visualizer": "^7.0.1",
-		"sass": "^1.98.0",
-		"terser": "^5.46.0",
-		"typescript": "^5.9.3",
-		"vite": "^8.0.0",
-		"vite-auto-i18n-plugin": "^1.1.16",
-		"vite-plugin-cdn-import": "^1.0.1",
-		"vite-plugin-compression2": "^2.5.1",
-		"vite-plugin-vue-setup-extend": "^0.4.0",
-		"vitest": "^3.2.4",
-		"vue-eslint-parser": "^10.4.0",
-		"vue-tsc": "^3.2.6"
-	},
-	"pnpm": {
-		"onlyBuiltDependencies": [
-			"@vue-office/docx",
-			"@vue-office/excel",
-			"@vue-office/pdf"
-		],
-		"ignoredBuiltDependencies": [
-			"@parcel/watcher",
-			"core-js",
-			"es5-ext",
-			"esbuild",
-			"json-editor-vue",
-			"less",
-			"vue-demi"
-		],
-		"overrides": {
-			"rollup": "4.43.0"
-		}
-	},
-	"browserslist": [
-		"> 1%",
-		"last 2 versions",
-		"not dead"
-	],
-	"engines": {
-		"node": ">=18.0.0",
-		"npm": ">= 7.0.0"
-	},
-	"keywords": [
-		"admin.net",
-		"vue",
-		"vue3",
-		"vuejs/vue-next",
-		"element-ui",
-		"element-plus",
-		"vue-next-admin",
-		"next-admin"
-	]
+  "name": "admin.net",
+  "type": "module",
+  "version": "2.4.192",
+  "packageManager": "pnpm@10.32.1",
+  "lastBuildTime": "2026.03.15",
+  "description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",
+  "author": "zuohuaijun",
+  "license": "MIT",
+  "scripts": {
+    "dev": "vite",
+    "build": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite build",
+    "prepare": "node scripts/install-git-hooks.cjs",
+    "lint-fix": "eslint --fix src/",
+    "format": "prettier --write .",
+    "build-api": "cd api_build/ && build.bat",
+    "build-approvalFlow-api": "cd api_build/ && build.bat approvalFlow",
+    "build-dingTalk-api": "cd api_build/ && build.bat dingTalk",
+    "build-goView-api": "cd api_build/ && build.bat goView",
+    "build-all-api": "npm run build-api && npm run build-approvalFlow-api &&  npm run build-dingTalk-api && npm run build-goView-api",
+    "build-api-ps": "cd api_build/ && pwsh -ExecutionPolicy Bypass -File build.ps1",
+    "build-approvalFlow-api-ps": "cd api_build/ && pwsh -ExecutionPolicy Bypass -File build.ps1 approvalFlow",
+    "build-dingTalk-api-ps": "cd api_build/ && pwsh -ExecutionPolicy Bypass -File build.ps1 dingTalk",
+    "build-goView-api-ps": "cd api_build/ && pwsh -ExecutionPolicy Bypass -File build.ps1 goView",
+    "build-all-api-ps": "npm run build-api-ps && npm run build-approvalFlow-api-ps &&  npm run build-dingTalk-api-ps && npm run build-goView-api-ps",
+    "translate": "node scripts/translate.cjs",
+    "test": "vitest",
+    "test:run": "vitest run"
+  },
+  "dependencies": {
+    "@element-plus/icons-vue": "^2.3.2",
+    "@logicflow/core": "^2.2.0",
+    "@logicflow/extension": "^2.2.0",
+    "@logicflow/vue-node-registry": "^1.2.0",
+    "@microsoft/signalr": "^10.0.0",
+    "@vue-office/docx": "^1.6.3",
+    "@vue-office/excel": "^1.7.14",
+    "@vue-office/pdf": "^2.0.10",
+    "@vueuse/core": "^14.2.1",
+    "@wangeditor/editor": "^5.1.23",
+    "@wangeditor/editor-for-vue": "^5.1.12",
+    "animate.css": "^4.1.1",
+    "async-validator": "^4.2.5",
+    "axios": "^1.13.6",
+    "countup.js": "^2.10.0",
+    "cropperjs": "^1.6.2",
+    "echarts": "^6.0.0",
+    "echarts-gl": "^2.0.9",
+    "echarts-wordcloud": "^2.1.0",
+    "element-plus": "^2.13.6",
+    "ezuikit-js": "^8.2.6",
+    "js-cookie": "^3.0.5",
+    "js-table2excel": "^1.1.2",
+    "json-editor-vue": "^0.18.1",
+    "jsplumb": "^2.15.6",
+    "lodash-es": "^4.17.23",
+    "md-editor-v3": "^6.4.0",
+    "mitt": "^3.0.1",
+    "monaco-editor": "^0.55.1",
+    "mqtt": "^5.15.0",
+    "nprogress": "^0.2.0",
+    "pinia": "^3.0.4",
+    "print-js": "^1.6.0",
+    "push.js": "^1.0.12",
+    "qrcodejs2-fixes": "^0.0.2",
+    "qs": "^6.15.0",
+    "relation-graph": "^2.2.11",
+    "screenfull": "^6.0.2",
+    "sm-crypto-v2": "^1.15.1",
+    "sortablejs": "^1.15.7",
+    "splitpanes": "^4.0.4",
+    "vcrontab-3": "^3.3.22",
+    "vform3-builds": "^3.0.10",
+    "vue": "^3.5.30",
+    "vue-clipboard3": "^2.0.0",
+    "vue-demi": "^0.14.10",
+    "vue-draggable-plus": "^0.6.1",
+    "vue-grid-layout": "3.0.0-beta1",
+    "vue-i18n": "^11.3.0",
+    "vue-json-pretty": "^2.6.0",
+    "vue-plugin-hiprint": "^0.0.60",
+    "vue-router": "^4.5.1",
+    "vue-signature-pad": "^3.0.2",
+    "vue3-tree-org": "^4.2.2",
+    "xlsx-js-style": "^1.2.0"
+  },
+  "devDependencies": {
+    "@eslint/eslintrc": "^3.3.5",
+    "@eslint/js": "^10.0.1",
+    "@playwright/test": "^1.59.1",
+    "@plugin-web-update-notification/vite": "^2.0.2",
+    "@rollup/pluginutils": "^5.3.0",
+    "@types/lodash-es": "^4.17.12",
+    "@types/node": "^22.19.15",
+    "@types/nprogress": "^0.2.3",
+    "@types/sortablejs": "^1.15.9",
+    "@typescript-eslint/eslint-plugin": "^8.57.0",
+    "@typescript-eslint/parser": "^8.57.0",
+    "@vitejs/plugin-vue": "^6.0.5",
+    "@vitejs/plugin-vue-jsx": "^5.1.5",
+    "@vue/compiler-sfc": "^3.5.30",
+    "cli-progress": "^3.12.0",
+    "code-inspector-plugin": "^1.4.4",
+    "colors": "^1.4.0",
+    "dotenv": "^17.3.1",
+    "esbuild": "^0.27.4",
+    "eslint": "^10.0.3",
+    "eslint-plugin-vue": "^10.8.0",
+    "globals": "^17.4.0",
+    "jsdom": "^26.1.0",
+    "less": "^4.6.4",
+    "prettier": "^3.8.1",
+    "rollup-plugin-visualizer": "^7.0.1",
+    "sass": "^1.98.0",
+    "terser": "^5.46.0",
+    "typescript": "^5.9.3",
+    "vite": "^8.0.0",
+    "vite-auto-i18n-plugin": "^1.1.16",
+    "vite-plugin-cdn-import": "^1.0.1",
+    "vite-plugin-compression2": "^2.5.1",
+    "vite-plugin-vue-setup-extend": "^0.4.0",
+    "vitest": "^3.2.4",
+    "vue-eslint-parser": "^10.4.0",
+    "vue-tsc": "^3.2.6"
+  },
+  "pnpm": {
+    "onlyBuiltDependencies": [
+      "@vue-office/docx",
+      "@vue-office/excel",
+      "@vue-office/pdf"
+    ],
+    "ignoredBuiltDependencies": [
+      "@parcel/watcher",
+      "core-js",
+      "es5-ext",
+      "esbuild",
+      "json-editor-vue",
+      "less",
+      "vue-demi"
+    ],
+    "overrides": {
+      "rollup": "4.43.0"
+    }
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ],
+  "engines": {
+    "node": ">=18.0.0",
+    "npm": ">= 7.0.0"
+  },
+  "keywords": [
+    "admin.net",
+    "vue",
+    "vue3",
+    "vuejs/vue-next",
+    "element-ui",
+    "element-plus",
+    "vue-next-admin",
+    "next-admin"
+  ]
 }

+ 60 - 17
Web/src/views/aidop/s4/delivery/supplierDeliveryManagementList.vue

@@ -20,7 +20,7 @@
 			<el-button @click="onExport">导出</el-button>
 			<el-popover placement="bottom" width="220" trigger="click">
 				<template #reference><el-button text>列设置</el-button></template>
-				<el-checkbox v-for="item in toggleItems" :key="item.key" :model-value="col[item.key]" @change="(v) => setColumnVisible(item.key, Boolean(v))">
+				<el-checkbox v-for="item in toggleItems" :key="item.key" :model-value="col[item.key]" @change="(v: boolean) => setColumnVisible(item.key, v)">
 					{{ item.label }}
 				</el-checkbox>
 			</el-popover>
@@ -220,9 +220,32 @@ function getSelectedIdsAndSupplier() {
 }
 
 function onCreateShipment() {
-	const payload = getSelectedIdsAndSupplier();
-	if (!payload) return;
-	shipmentDialog.ids = payload.ids;
+	if (!selectedRows.value.length) {
+		ElMessage.warning('请先勾选数据');
+		return;
+	}
+	// 先校验所有勾选行的供应商一致性(不同供应商不允许生成发货单)
+	const supplier = selectedRows.value[0].gysdm || '';
+	if (!supplier) {
+		ElMessage.warning('所选数据缺少供应商代码');
+		return;
+	}
+	if (selectedRows.value.some((x) => (x.gysdm || '') !== supplier)) {
+		ElMessage.warning('请选择相同供应商的数据');
+		return;
+	}
+	// 只取已发布的行(sffb 非空且不为 'X')
+	const publishedRows = selectedRows.value.filter((x) => x.sffb && x.sffb !== 'X');
+	if (!publishedRows.length) {
+		ElMessage.warning('所选数据中没有已发布的计划,请先发布再生成发货单');
+		return;
+	}
+	const ids = publishedRows.map((x) => (x.id != null && x.id !== '' ? String(x.id) : '')).filter(Boolean).join(',');
+	if (!ids) {
+		ElMessage.warning('所选已发布数据缺少ID');
+		return;
+	}
+	shipmentDialog.ids = ids;
 	shipmentDialog.visible = true;
 }
 
@@ -277,19 +300,39 @@ async function onCloseDelivery(row: SupplierDeliveryRow) {
 	await loadList();
 }
 
-function onExport() {
-	const headers = ['发布日期', '物料编码', '物料描述', '采购组', '供应商代码', '供应商名称', '采购单号', '采购单行号', '采购订单数量', '待交数量', '交货单号', '到货日期', '交货数量', '交期回复', '在途量', '在检量', '入库量', '不合格量', '退货量', '备注'];
-	const lines = rows.value.map((r) => [
-		fmtDate(r.sffb), r.wlbm ?? '', r.wlms ?? '', r.buyer ?? '', r.gysdm ?? '', r.gysmc ?? '', r.cgdd ?? '', r.ddhh ?? '', r.jhdsl ?? '', r.dfhsl ?? '',
-		r.dsnum ?? '', fmtDate(r.requestdate), r.schedqty ?? '', fmtDate(r.jqhfnew), r.ztsl ?? '', r.zsl1 ?? '', r.rksl ?? '', r.bhgsl ?? '', r.thsl ?? '', r.bz ?? '',
-	]);
-	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);
+async function onExport() {
+	try {
+		const data = await fetchSupplierDeliveryList({
+			cgdd: query.cgdd || undefined,
+			dsNum: query.dsNum || undefined,
+			wlbm: query.wlbm || undefined,
+			gys: query.gys || undefined,
+			sortField: query.sortField || undefined,
+			sortOrder: query.sortOrder || undefined,
+			page: 1,
+			pageSize: 99999,
+		});
+		const list = data.list || [];
+		if (!list.length) {
+			ElMessage.warning('无数据可导出');
+			return;
+		}
+		const headers = ['发布日期', '物料编码', '物料描述', '采购组', '供应商代码', '供应商名称', '采购单号', '采购单行号', '采购订单数量', '待交数量', '交货单号', '到货日期', '交货数量', '交期回复', '在途量', '在检量', '入库量', '不合格量', '退货量', '备注'];
+		const lines = list.map((r) => [
+			fmtDate(r.sffb), r.wlbm ?? '', r.wlms ?? '', r.buyer ?? '', r.gysdm ?? '', r.gysmc ?? '', r.cgdd ?? '', r.ddhh ?? '', r.jhdsl ?? '', r.dfhsl ?? '',
+			r.dsnum ?? '', fmtDate(r.requestdate), r.schedqty ?? '', fmtDate(r.jqhfnew), r.ztsl ?? '', r.zsl1 ?? '', r.rksl ?? '', r.bhgsl ?? '', r.thsl ?? '', r.bz ?? '',
+		]);
+		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(`导出成功,共 ${list.length} 条数据`);
+	} catch (e: any) {
+		ElMessage.error(e?.message || '导出失败');
+	}
 }
 
 onMounted(loadList);

+ 44 - 28
Web/src/views/aidop/s4/delivery/supplierShipmentForm.vue

@@ -1,7 +1,7 @@
 <template>
 	<AidopDemoShell :title="title" :show-bar="!embedded">
 		<div v-loading="loading">
-			<div class="top-toolbar" v-if="isView">
+			<div class="top-toolbar" v-if="isView || isQc">
 				<el-button type="primary" @click="onPrintShippingNote">打印送货单</el-button>
 				<el-button @click="onCancel">返回</el-button>
 			</div>
@@ -20,14 +20,14 @@
 								type="date"
 								value-format="YYYY-MM-DD"
 								style="width: 100%"
-								:disabled="isView"
+								:disabled="fieldLocked"
 								@change="recalcYjdhrq"
 							/>
 						</el-form-item>
 					</el-col>
 					<el-col :span="12">
 						<el-form-item label="物流时长(天)">
-							<el-input v-model="form.wlsc" :disabled="isView" placeholder="整数天" @change="recalcYjdhrq" @input="recalcYjdhrq" />
+							<el-input v-model="form.wlsc" :disabled="fieldLocked" placeholder="整数天" @change="recalcYjdhrq" @input="recalcYjdhrq" />
 						</el-form-item>
 					</el-col>
 					<el-col :span="12">
@@ -35,12 +35,12 @@
 							<el-date-picker v-model="form.yjdhrq" type="date" value-format="YYYY-MM-DD" style="width: 100%" :disabled="true" />
 						</el-form-item>
 					</el-col>
-					<el-col :span="12"><el-form-item label="供应商名称"><el-input v-model="form.shPurchaseName" :disabled="isView" /></el-form-item></el-col>
-					<el-col :span="12"><el-form-item label="供应商编号"><el-input v-model="form.shPurchaseNum" :disabled="isView" /></el-form-item></el-col>
-					<el-col :span="12"><el-form-item label="物流单号"><el-input v-model="form.wldh" :disabled="isView" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="供应商名称"><el-input v-model="form.shPurchaseName" :disabled="fieldLocked" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="供应商编号"><el-input v-model="form.shPurchaseNum" :disabled="fieldLocked" /></el-form-item></el-col>
+					<el-col :span="12"><el-form-item label="物流单号"><el-input v-model="form.wldh" :disabled="fieldLocked" /></el-form-item></el-col>
 					<el-col :span="12">
 						<el-form-item label="偏差申请">
-							<el-radio-group v-model="form.sfpc" :disabled="isView">
+							<el-radio-group v-model="form.sfpc" :disabled="fieldLocked">
 								<el-radio :value="0">否</el-radio>
 								<el-radio :value="1">是</el-radio>
 							</el-radio-group>
@@ -51,29 +51,30 @@
 							<ShipmentAttachment v-model="form.chbg" :readonly="isView" />
 						</el-form-item>
 					</el-col>
-					<el-col :span="24"><el-form-item label="偏差说明"><el-input v-model="form.pcsm" type="textarea" :rows="2" :disabled="isView" /></el-form-item></el-col>
+					<el-col :span="24"><el-form-item label="偏差说明"><el-input v-model="form.pcsm" type="textarea" :rows="2" :disabled="fieldLocked" /></el-form-item></el-col>
 				</el-row>
 			</el-form>
 
 			<el-divider content-position="left">发货明细</el-divider>
-			<div class="sub-toolbar" v-if="!isView">
+			<div class="sub-toolbar" v-if="!isView && !isQc">
 				<el-button type="primary" @click="addDetail">添加</el-button>
 			</div>
 			<el-table :data="form.details" border stripe>
 				<el-table-column label="行号" width="80"><template #default="{ row, $index }">{{ row.hh || $index + 1 }}</template></el-table-column>
-				<el-table-column label="订单号" width="130"><template #default="{ row }"><el-input v-model="row.poBill" :disabled="isView" /></template></el-table-column>
-				<el-table-column label="采购订单行" width="100"><template #default="{ row }"><el-input v-model="row.poBillLine" :disabled="isView" /></template></el-table-column>
-				<el-table-column label="订单类型" width="100"><template #default="{ row }"><el-input v-model="row.orderType" :disabled="isView" /></template></el-table-column>
-				<el-table-column label="物料编号" width="130"><template #default="{ row }"><el-input v-model="row.shMaterialCode" :disabled="isView" /></template></el-table-column>
-				<el-table-column label="物料名称" min-width="160"><template #default="{ row }"><el-input v-model="row.shMaterialName" :disabled="isView" /></template></el-table-column>
-				<el-table-column label="图号" width="120"><template #default="{ row }"><el-input v-model="row.th" :disabled="isView" /></template></el-table-column>
+				<el-table-column label="订单号" width="130"><template #default="{ row }"><el-input v-model="row.poBill" :disabled="fieldLocked" /></template></el-table-column>
+				<el-table-column label="采购订单行" width="100"><template #default="{ row }"><el-input v-model="row.poBillLine" :disabled="fieldLocked" /></template></el-table-column>
+				<el-table-column label="订单类型" width="100"><template #default="{ row }"><el-input v-model="row.orderType" :disabled="fieldLocked" /></template></el-table-column>
+				<el-table-column label="物料编号" width="130"><template #default="{ row }"><el-input v-model="row.shMaterialCode" :disabled="fieldLocked" /></template></el-table-column>
+				<el-table-column label="物料名称" min-width="160"><template #default="{ row }"><el-input v-model="row.shMaterialName" :disabled="fieldLocked" /></template></el-table-column>
+				<el-table-column label="图号" width="120"><template #default="{ row }"><el-input v-model="row.th" :disabled="fieldLocked" /></template></el-table-column>
 				<el-table-column label="送货数量" width="100">
 					<template #default="{ row }">
 						<el-input-number
 							v-model="row.shDeliveryQuantity"
 							:controls="false"
 							:min="0"
-							:disabled="isView"
+							:max="row.djsl ?? undefined"
+							:disabled="fieldLocked"
 							style="width: 100%"
 							@change="() => recalcDetailBqsl(row)"
 						/>
@@ -85,7 +86,7 @@
 							v-model="row.bzsl"
 							:controls="false"
 							:min="0"
-							:disabled="isView"
+							:disabled="fieldLocked"
 							style="width: 100%"
 							@change="() => recalcDetailBqsl(row)"
 						/>
@@ -96,22 +97,22 @@
 						<el-input-number v-model="row.bqsl" :controls="false" :min="0" :disabled="true" style="width: 100%" />
 					</template>
 				</el-table-column>
-				<el-table-column label="单位" width="90"><template #default="{ row }"><el-input v-model="row.shMaterialDw" :disabled="isView" /></template></el-table-column>
+				<el-table-column label="单位" width="90"><template #default="{ row }"><el-input v-model="row.shMaterialDw" :disabled="fieldLocked" /></template></el-table-column>
 				<el-table-column label="生产日期" width="120">
 					<template #default="{ row }">
-						<el-date-picker v-model="row.scrq" type="date" value-format="YYYY-MM-DD" :disabled="isView" style="width: 100%" />
+						<el-date-picker v-model="row.scrq" type="date" value-format="YYYY-MM-DD" :disabled="fieldLocked" style="width: 100%" />
 					</template>
 				</el-table-column>
-				<el-table-column label="生产批号" width="120"><template #default="{ row }"><el-input v-model="row.scph" :disabled="isView" /></template></el-table-column>
-				<el-table-column label="备注" min-width="140"><template #default="{ row }"><el-input v-model="row.remarks" :disabled="isView" /></template></el-table-column>
-				<el-table-column label="待交数量" width="100"><template #default="{ row }"><el-input-number v-model="row.djsl" :controls="false" :disabled="isView" style="width: 100%" /></template></el-table-column>
+				<el-table-column label="生产批号" width="120"><template #default="{ row }"><el-input v-model="row.scph" :disabled="fieldLocked" /></template></el-table-column>
+				<el-table-column label="备注" min-width="140"><template #default="{ row }"><el-input v-model="row.remarks" :disabled="fieldLocked" /></template></el-table-column>
+				<el-table-column label="待交数量" width="100"><template #default="{ row }"><el-input-number v-model="row.djsl" :controls="false" :disabled="fieldLocked" style="width: 100%" /></template></el-table-column>
 				<el-table-column label="检验报告" min-width="220">
 					<template #default="{ row }">
 						<ShipmentAttachment v-model="row.jybb" :readonly="isView" />
 					</template>
 				</el-table-column>
-				<el-table-column label="交货单号" width="120"><template #default="{ row }"><el-input v-model="row.jhdbh" :disabled="isView" /></template></el-table-column>
-				<el-table-column v-if="!isView" label="操作" width="110" fixed="right">
+				<el-table-column label="交货单号" width="120"><template #default="{ row }"><el-input v-model="row.jhdbh" :disabled="fieldLocked" /></template></el-table-column>
+				<el-table-column v-if="!isView && !isQc" label="操作" width="110" fixed="right">
 					<template #default="{ $index }">
 						<el-button link type="danger" @click="removeDetail($index)">删除</el-button>
 					</template>
@@ -167,7 +168,7 @@ const route = useRoute();
 const router = useRouter();
 const mode = computed(() => {
 	if (props.embedded) return 'create' as const;
-	return String(route.query.mode || 'create') as 'create' | 'edit' | 'view';
+	return String(route.query.mode || 'create') as 'create' | 'edit' | 'view' | 'qc';
 });
 const id = computed(() => {
 	if (props.embedded) return 0;
@@ -182,7 +183,12 @@ const autoPrint = computed(() => {
 	return String(route.query.autoPrint || '') === '1';
 });
 const isView = computed(() => mode.value === 'view');
-const title = computed(() => (mode.value === 'create' ? '发货单新增' : mode.value === 'edit' ? '发货单编辑' : '发货单查看'));
+const isQc = computed(() => mode.value === 'qc');
+/** 非查看且非质检模式时才允许编辑常规字段;质检模式仅允许上传附件 */
+const fieldLocked = computed(() => isView.value || isQc.value);
+const title = computed(() =>
+	mode.value === 'create' ? '发货单新增' : mode.value === 'edit' ? '发货单编辑' : mode.value === 'qc' ? '质检报告' : '发货单查看'
+);
 const shddhPlaceholder = computed(() => (isView.value ? '' : '保存后自动生成'));
 
 const loading = ref(false);
@@ -248,7 +254,7 @@ function recalcYjdhrq() {
 
 async function afterFormLoaded() {
 	await nextTick();
-	if (!isView.value) {
+	if (!isView.value && !isQc.value) {
 		recalcYjdhrq();
 		for (const row of form.details) recalcDetailBqsl(row);
 	}
@@ -295,7 +301,7 @@ async function loadData() {
 			await afterFormLoaded();
 			return;
 		}
-		if ((mode.value === 'edit' || mode.value === 'view') && id.value > 0) {
+		if ((mode.value === 'edit' || mode.value === 'view' || mode.value === 'qc') && id.value > 0) {
 			const detail = await fetchSupplierShipmentDetail(id.value);
 			setForm(detail);
 			await afterFormLoaded();
@@ -313,6 +319,16 @@ function onPrintShippingNote() {
 }
 
 async function onSave() {
+	// 校验送货数量不能超过待交数量
+	for (let i = 0; i < form.details.length; i++) {
+		const row = form.details[i]!;
+		const qty = Number(row.shDeliveryQuantity) || 0;
+		const max = Number(row.djsl) || 0;
+		if (max > 0 && qty > max) {
+			ElMessage.warning(`第 ${i + 1} 行送货数量(${qty})超过待交数量(${max}),请调整`);
+			return;
+		}
+	}
 	saving.value = true;
 	try {
 		recalcYjdhrq();

+ 10 - 6
Web/src/views/aidop/s4/delivery/supplierShipmentList.vue

@@ -28,7 +28,7 @@
 			<el-button @click="onExport">导出</el-button>
 			<el-popover placement="bottom" width="220" trigger="click">
 				<template #reference><el-button text>列设置</el-button></template>
-				<el-checkbox v-for="item in toggleItems" :key="item.key" :model-value="col[item.key]" @change="(v) => setColumnVisible(item.key, Boolean(v))">
+				<el-checkbox v-for="item in toggleItems" :key="item.key" :model-value="col[item.key]" @change="(v: boolean) => setColumnVisible(item.key, v)">
 					{{ item.label }}
 				</el-checkbox>
 			</el-popover>
@@ -55,12 +55,12 @@
 			<el-table-column v-if="col.th" prop="th" label="图号" width="110" sortable="custom" />
 			<el-table-column label="操作" width="360" fixed="right">
 				<template #default="{ row }">
-					<el-button link type="primary" @click="onPlaceholder('generate-label', row)">生成标签</el-button>
+					<el-button v-if="!isLocked(row)" link type="primary" @click="onPlaceholder('generate-label', row)">生成标签</el-button>
 					<el-button link type="primary" @click="onPrintShippingNote(row)">打印送货单</el-button>
 					<el-button link type="primary" @click="onPrintLabel(row)">打印标签</el-button>
-					<el-button link type="primary" @click="openForm('edit', row.mid)">质检报告</el-button>
-					<el-button link type="primary" @click="openForm('edit', row.mid)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<el-button v-if="!isLocked(row)" link type="primary" @click="openForm('qc', row.mid)">质检报告</el-button>
+					<el-button v-if="!isLocked(row)" link type="primary" @click="openForm('edit', row.mid)">编辑</el-button>
+					<el-button v-if="!isLocked(row)" link type="danger" @click="onDelete(row)">删除</el-button>
 					<el-button link @click="openForm('view', row.mid)">查看</el-button>
 				</template>
 			</el-table-column>
@@ -138,6 +138,10 @@ const toggleItems: Array<{ key: ColumnKey; label: string }> = [
 	{ key: 'wldh', label: '物流单号' }, { key: 'dycs', label: '已打次数' }, { key: 'shzt', label: '送货状态' }, { key: 'th', label: '图号' },
 ];
 
+function isLocked(row: SupplierShipmentRow) {
+	return row.shzt === '收货中' || row.shzt === '完成';
+}
+
 function setColumnVisible(key: ColumnKey, visible: boolean) {
 	col[key] = visible;
 }
@@ -176,7 +180,7 @@ function resetQuery() {
 	loadList();
 }
 
-function openForm(mode: 'create' | 'edit' | 'view', id?: number) {
+function openForm(mode: 'create' | 'edit' | 'view' | 'qc', id?: number) {
 	router.push({
 		path: '/aidop/s4/delivery/supplier-shipment-form',
 		query: { mode, id: id ? String(id) : undefined },

+ 59 - 0
Web/src/views/aidop/s4/return/iqcReturnQueryList.vue

@@ -14,6 +14,7 @@
 		</el-form>
 
 		<div class="toolbar">
+			<el-button type="success" :loading="exporting" @click="exportCsv">导出</el-button>
 			<el-popover placement="bottom" width="220" trigger="click">
 				<template #reference><el-button text>列设置</el-button></template>
 				<el-checkbox
@@ -86,6 +87,7 @@ const query = reactive({
 });
 
 const loading = ref(false);
+const exporting = ref(false);
 const rows = ref<IqcReturnListRow[]>([]);
 const total = ref(0);
 
@@ -178,6 +180,63 @@ function resetQuery() {
 	loadList();
 }
 
+async function exportCsv() {
+	exporting.value = true;
+	try {
+		const data = await fetchIqcReturnList({
+			...query,
+			page: 1,
+			pageSize: 99999,
+			sortField: query.sortField,
+			sortOrder: query.sortOrder,
+		});
+		const all = data.list || [];
+		if (!all.length) {
+			ElMessage.warning('当前查询无数据,无法导出');
+			return;
+		}
+		const headers = ['单据编号', '采购单号', '批次', '物料编号', '供应商', '供应商编码', '供应商名称', '物料名称', '规格型号', '检验数量', '不合格', '处理方式', '退货原因', '检验状态', '申请时间', '检验完成'];
+		const lines = all.map((r: IqcReturnListRow) => [
+			r.fBillNo ?? '',
+			r.ordNbr ?? '',
+			r.pch ?? '',
+			r.fMaterialCfg ?? '',
+			r.sortName ?? '',
+			r.supp ?? '',
+			r.gysmc ?? '',
+			r.wlmc ?? '',
+			r.ggxh ?? '',
+			r.frInsQty ?? '',
+			r.bhgsl ?? '',
+			r.clfs ?? '',
+			r.thyy ?? '',
+			r.fInspectStatus ?? '',
+			fmtDateTime(r.fApplyTime),
+			fmtDate(r.fInspeEndDate),
+		]);
+		const esc = (v: unknown) => {
+			const s = String(v);
+			return /[,"\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
+		};
+		const csv = [headers, ...lines].map((row) => row.map(esc).join(',')).join('\n');
+		const blob = new Blob([`\uFEFF${csv}`], { type: 'text/csv;charset=utf-8;' });
+		const url = URL.createObjectURL(blob);
+		const a = document.createElement('a');
+		const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
+		a.href = url;
+		a.download = `IQC退货查询_${dateStr}.csv`;
+		document.body.appendChild(a);
+		a.click();
+		document.body.removeChild(a);
+		URL.revokeObjectURL(url);
+		ElMessage.success(`已导出 ${all.length} 条数据`);
+	} catch (e: any) {
+		ElMessage.error(e?.message || '导出失败');
+	} finally {
+		exporting.value = false;
+	}
+}
+
 onMounted(() => loadList());
 onActivated(() => loadList());
 </script>

+ 3 - 3
Web/src/views/aidop/s4/return/purchaseReturnOrderForm.vue

@@ -105,7 +105,7 @@
 				</el-table-column>
 				<el-table-column label="已退数量" width="110">
 					<template #default="{ row, $index }">
-						<el-input-number v-if="canEditDetail($index)" v-model="row.rctQty" :controls="false" class="w100num" />
+						<el-input-number v-if="canEditDetail($index)" v-model="row.rctQty" :controls="false" disabled class="w100num" />
 						<span v-else>{{ row.rctQty }}</span>
 					</template>
 				</el-table-column>
@@ -146,7 +146,7 @@
 				<el-table-column v-if="!isView" label="操作" width="140" fixed="right">
 					<template #default="{ $index }">
 						<el-button link type="primary" @click="toggleEditRow($index)">{{ editingIdx === $index ? '完成' : '编辑' }}</el-button>
-						<el-button link type="danger" @click="removeDetailRow($index)">删除</el-button>
+						<el-button v-if="!(Number(details[$index]?.rctQty) > 0)" link type="danger" @click="removeDetailRow($index)">删除</el-button>
 					</template>
 				</el-table-column>
 			</el-table>
@@ -336,7 +336,7 @@ async function onRcReceiverChanged() {
 			ordLine: Number(x.ordLine ?? 0),
 			qtyOrded: Number(x.qtyOrded ?? 0),
 			qtyReceived: Number(x.qtyReceived ?? x.qtyReturn ?? 0),
-			rctQty: Number(x.rctQty ?? 0),
+			rctQty: 0,
 			lotSerial: x.lotSerial,
 			um: x.um,
 			location: String(x.location ?? ''),

+ 1 - 1
Web/src/views/aidop/s4/return/purchaseReturnOrderList.vue

@@ -43,7 +43,7 @@
 				<template #default="{ row }">
 					<el-button link type="primary" @click="openForm('view', row)">查看</el-button>
 					<el-button v-if="truthy(row.isPlan)" link type="warning" @click="openForm('edit', row)">编辑</el-button>
-					<el-button v-if="truthy(row.isPlan)" link type="danger" @click="onDelete(row)">删除</el-button>
+					<el-button v-if="truthy(row.isPlan) && !(Number(row.rctQty) > 0)" link type="danger" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>

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


+ 159 - 0
doc/db/s4_purordrct_seed_20260617.sql

@@ -0,0 +1,159 @@
+-- 采购收货单测试数据(PurOrdRctMaster + PurOrdRctDetail)
+-- 幂等:使用固定编号 + NOT EXISTS 防重复插入
+-- 执行前请确认租户ID,默认为 797403760988229
+
+SET @tenant_id := 797403760988229;
+
+-- ===========================================================================
+-- 1) 收货单主表 PurOrdRctMaster(RctType='rc',5 条)
+-- ===========================================================================
+INSERT INTO PurOrdRctMaster
+(
+  RecID, Domain, RctType, Receiver, RctDate, OrdNbr, Site, BusinessID,
+  CreateUser, UpdateUser, CreateTime, UpdateTime,
+  IsActive, IsConfirm, Typed, Department, Supp, `Print`, IsChanged,
+  TaxClass, TaxIn, Remark, Curr, IsExport, Terms, IsPlan,
+  TermsofTrade, IsBackwash, tenant_id
+)
+SELECT
+  60617000 + p.idx AS RecID,
+  '100' AS Domain,
+  'rc' AS RctType,
+  p.receiver AS Receiver,
+  DATE_ADD('2026-06-17', INTERVAL p.idx - 1 DAY) AS RctDate,
+  p.ordNbr AS OrdNbr,
+  'AIDOP' AS Site,
+  0 AS BusinessID,
+  'seed-20260617' AS CreateUser,
+  'seed-20260617' AS UpdateUser,
+  NOW() AS CreateTime,
+  NOW() AS UpdateTime,
+  1 AS IsActive,
+  0 AS IsConfirm,
+  'C' AS Typed,
+  'D-PROD' AS Department,
+  p.supp AS Supp,
+  0 AS `Print`,
+  0 AS IsChanged,
+  '' AS TaxClass,
+  0 AS TaxIn,
+  CONCAT('采购收货测试-', p.ordNbr) AS Remark,
+  'CNY' AS Curr,
+  0 AS IsExport,
+  '' AS Terms,
+  0 AS IsPlan,
+  '' AS TermsofTrade,
+  0 AS IsBackwash,
+  @tenant_id AS tenant_id
+FROM (
+  SELECT 1 idx, 'RC-TEST-20260617-01' receiver, 'PO-UAT-20260604-01' ordNbr, 'VEN00060' supp UNION ALL
+  SELECT 2,       'RC-TEST-20260617-02',       'PO-UAT-20260604-02',       'VEN00060' UNION ALL
+  SELECT 3,       'RC-TEST-20260617-03',       'PO-UAT-20260604-03',       'VEN00060' UNION ALL
+  SELECT 4,       'RC-TEST-20260617-04',       'PO-UAT-20260604-04',       'VEN00060' UNION ALL
+  SELECT 5,       'RC-TEST-20260617-05',       'PO-UAT-20260604-05',       'VEN00060'
+) p
+WHERE NOT EXISTS (
+  SELECT 1 FROM PurOrdRctMaster x
+  WHERE x.Receiver = p.receiver AND x.tenant_id = @tenant_id
+);
+
+-- ===========================================================================
+-- 2) 收货单明细 PurOrdRctDetail(每单 2 行,共 10 条)
+-- ===========================================================================
+INSERT INTO PurOrdRctDetail
+(
+  RecID, Domain, RctType, Receiver, Line, ItemNum, Dimension1, Dimension2,
+  OrdNbr, RctDate, POCost, UM, QtyOrded, RctQty, Site, Supp, Location,
+  QtyReceived, Potype, BusinessID, CreateUser, UpdateUser, CreateTime, UpdateTime,
+  IsActive, IsConfirm, Typed, IsChanged, TaxClass, TaxIn, UMConversion,
+  Curr, IsClosed, OrdLine, IsRounding, BlanketLine, ProjectLine,
+  RctLine, QtyReturn, PurOrdRctRecID, QcQty, RctNbr, Remark, tenant_id
+)
+SELECT
+  606170000 + p.idx * 10 + d.Line AS RecID,
+  '100' AS Domain,
+  'rc' AS RctType,
+  p.receiver AS Receiver,
+  d.Line AS Line,
+  d.ItemNum,
+  COALESCE(d.Dimension1, '') AS Dimension1,
+  COALESCE(d.Dimension2, '') AS Dimension2,
+  p.ordNbr AS OrdNbr,
+  DATE_ADD('2026-06-17', INTERVAL p.idx - 1 DAY) AS RctDate,
+  COALESCE(d.PurCost, 10.00) AS POCost,
+  COALESCE(d.UM, 'EA') AS UM,
+  COALESCE(d.QtyOrded, 100) AS QtyOrded,
+  ROUND(COALESCE(d.QtyOrded, 100) * 0.85, 5) AS RctQty,
+  'AIDOP' AS Site,
+  p.supp AS Supp,
+  COALESCE(d.Location, 'WH-01') AS Location,
+  ROUND(COALESCE(d.QtyOrded, 100) * 0.85, 5) AS QtyReceived,
+  COALESCE(d.Potype, 'PT') AS Potype,
+  0 AS BusinessID,
+  'seed-20260617' AS CreateUser,
+  'seed-20260617' AS UpdateUser,
+  NOW() AS CreateTime,
+  NOW() AS UpdateTime,
+  1 AS IsActive,
+  0 AS IsConfirm,
+  'C' AS Typed,
+  0 AS IsChanged,
+  '' AS TaxClass,
+  0 AS TaxIn,
+  1 AS UMConversion,
+  'CNY' AS Curr,
+  0 AS IsClosed,
+  d.Line AS OrdLine,
+  0 AS IsRounding,
+  0 AS BlanketLine,
+  0 AS ProjectLine,
+  d.Line AS RctLine,
+  0 AS QtyReturn,
+  60617000 + p.idx AS PurOrdRctRecID,
+  ROUND(COALESCE(d.QtyOrded, 100) * 0.85, 5) AS QcQty,
+  p.receiver AS RctNbr,
+  CONCAT('采购收货测试明细-', p.ordNbr, '-L', d.Line) AS Remark,
+  @tenant_id AS tenant_id
+FROM (
+  SELECT 1 idx, 'RC-TEST-20260617-01' receiver, 'PO-UAT-20260604-01' ordNbr, 'VEN00060' supp UNION ALL
+  SELECT 2,       'RC-TEST-20260617-02',       'PO-UAT-20260604-02',       'VEN00060' UNION ALL
+  SELECT 3,       'RC-TEST-20260617-03',       'PO-UAT-20260604-03',       'VEN00060' UNION ALL
+  SELECT 4,       'RC-TEST-20260617-04',       'PO-UAT-20260604-04',       'VEN00060' UNION ALL
+  SELECT 5,       'RC-TEST-20260617-05',       'PO-UAT-20260604-05',       'VEN00060'
+) p
+LEFT JOIN (
+  SELECT PurOrd, Line,
+         MIN(ItemNum) AS ItemNum,
+         MIN(Dimension1) AS Dimension1,
+         MIN(Dimension2) AS Dimension2,
+         MAX(QtyOrded) AS QtyOrded,
+         MIN(UM) AS UM,
+         MIN(Location) AS Location,
+         MAX(PurCost) AS PurCost,
+         MIN(Potype) AS Potype
+  FROM PurOrdDetail
+  WHERE tenant_id = @tenant_id
+    AND PurOrd LIKE 'PO-UAT-20260604-%'
+    AND Line IN (1, 2)
+  GROUP BY PurOrd, Line
+) d ON d.PurOrd = p.ordNbr
+WHERE d.ItemNum IS NOT NULL
+  AND NOT EXISTS (
+    SELECT 1 FROM PurOrdRctDetail x
+    WHERE x.RctType = 'rc' AND x.Receiver = p.receiver AND x.Line = d.Line AND x.tenant_id = @tenant_id
+  );
+
+-- ===========================================================================
+-- 3) 验收:查看插入结果
+-- ===========================================================================
+SELECT '=== 收货单主表 ===' AS info;
+SELECT RecID, Receiver, RctDate, OrdNbr, Supp, Remark, tenant_id
+FROM PurOrdRctMaster
+WHERE Receiver LIKE 'RC-TEST-20260617-%' AND tenant_id = @tenant_id
+ORDER BY RecID;
+
+SELECT '=== 收货单明细 ===' AS info;
+SELECT RecID, Receiver, Line, ItemNum, OrdNbr, QtyOrded, RctQty, QtyReceived, QcQty, Remark
+FROM PurOrdRctDetail
+WHERE Receiver LIKE 'RC-TEST-20260617-%' AND tenant_id = @tenant_id
+ORDER BY RecID;

+ 10 - 0
doc/migrations/2026-06-17_scm_shbq_add_tenant_id.sql

@@ -0,0 +1,10 @@
+-- scm_shbq / scm_shbqhis 增加 tenant_id 列,支持多租户隔离
+-- 执行前请备份相关表
+
+-- 1. scm_shbq 加列
+ALTER TABLE scm_shbq
+  ADD COLUMN tenant_id BIGINT NULL DEFAULT NULL AFTER jhdhh;
+
+-- 2. scm_shbqhis 加列(归档表结构保持一致)
+ALTER TABLE scm_shbqhis
+  ADD COLUMN tenant_id BIGINT NULL DEFAULT NULL AFTER jhdhh;

+ 1 - 1
server/Admin.NET.Application/Configuration/Upload.json

@@ -4,7 +4,7 @@
   "Upload": {
     "Path": "upload/{yyyy}/{MM}/{dd}", // 文件上传目录
     "MaxSize": 51200, // 文件最大限制KB:1024*50
-    "ContentType": [ "image/jpg", "image/png", "image/jpeg", "image/gif", "image/bmp", "text/plain", "text/xml", "application/pdf", "application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "video/mp4", "application/wps-office.docx", "application/wps-office.xlsx", "application/wps-office.pptx", "application/vnd.android.package-archive" ],
+    "ContentType": [ "image/jpg", "image/png", "image/jpeg", "image/gif", "image/bmp", "text/plain", "text/xml", "text/csv", "application/pdf", "application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "video/mp4", "application/wps-office.docx", "application/wps-office.xlsx", "application/wps-office.pptx", "application/vnd.android.package-archive" ],
     "EnableMd5": false // 启用文件MDF5验证-防止重复上传
   },
   "OSSProvider": {

+ 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.185</AssemblyVersion>
-    <FileVersion>1.0.185</FileVersion>
-    <Version>1.0.185</Version>
+    <AssemblyVersion>1.0.186</AssemblyVersion>
+    <FileVersion>1.0.186</FileVersion>
+    <Version>1.0.186</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 11 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/ProcurementExecution/PurchaseReturnService.cs

@@ -432,14 +432,17 @@ public class PurchaseReturnService : IDynamicApiController, ITransient
                     IsActive = true,
                     IsConfirm = false,
                     IsPlan = true,
+                    TenantId = tenantId,
                     CreateUser = account,
                     CreateTime = now,
                     UpdateUser = account,
                     UpdateTime = now,
                 };
 
-                var newId = await _db.Insertable(master).ExecuteReturnIdentityAsync();
-                master.RecID = Convert.ToInt32(newId);
+                // PurOrdRctMaster 无 AUTO_INCREMENT,需手动生成 RecID
+                master.RecID = await _db.Ado.GetIntAsync(
+                    "SELECT IFNULL(MAX(RecID), 0) + 1 FROM PurOrdRctMaster");
+                await _masterRep.InsertAsync(master);
 
                 await SaveDetailsAsync(master.RecID, domain, receiver, "pt", input.Details, account, now);
 
@@ -494,6 +497,10 @@ public class PurchaseReturnService : IDynamicApiController, ITransient
         var dbById = dbRows.ToDictionary(d => d.RecID);
         var inputIds = new HashSet<int>(entries.Where(e => e.RecID is > 0).Select(e => e.RecID!.Value));
 
+        // PurOrdRctDetail 无 AUTO_INCREMENT,提前取一次 MAX 并在循环中递增
+        var nextDetailId = await _db.Ado.GetIntAsync(
+            "SELECT IFNULL(MAX(RecID), 0) + 1 FROM PurOrdRctDetail");
+
         for (var i = 0; i < entries.Count; i++)
         {
             var e = entries[i];
@@ -515,6 +522,8 @@ public class PurchaseReturnService : IDynamicApiController, ITransient
             {
                 var detailNew = new PurOrdRctDetail();
                 ApplyDetailInput(detailNew, e, domain, receiver, masterRecId, account, now, lineNo, loc);
+                detailNew.RecID = nextDetailId++;
+                detailNew.TenantId = tenantId;
                 detailNew.CreateUser = account;
                 detailNew.CreateTime = now;
                 detailNew.UpdateUser = account;

+ 16 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/ProcurementExecution/SupplierDeliveryManagementService.cs

@@ -290,6 +290,22 @@ public class SupplierDeliveryManagementService : IDynamicApiController, ITransie
     public async Task<object> CloseDelivery([FromBody] DeliveryCloseInput input)
     {
         var tenantId = AidopTenantHelper.Resolve(App.HttpContext);
+
+        // 检查是否已有关联的发货单明细(scm_shdzb.jhdbh = 交货单号)
+        var shipmentCount = await _db.Ado.GetIntAsync(
+            """
+            SELECT COUNT(*)
+            FROM scm_shdzb d
+            INNER JOIN scm_shd m ON CAST(m.id AS CHAR) = d.glid
+            WHERE d.jhdbh = @jhdbh
+              AND m.tenant_id = @TenantId
+              AND IFNULL(m.state, 1) <> 0
+            """,
+            new List<SugarParameter> { new("@jhdbh", input.DsNum), new("@TenantId", tenantId) });
+
+        if (shipmentCount > 0)
+            throw Oops.Oh("此发货单还未收完货,无法关闭交货单");
+
         await _db.Ado.ExecuteCommandAsync(
             "UPDATE srm_polist_ds SET status='C' WHERE DSNum=@jhdbh AND tenant_id=@TenantId",
             new List<SugarParameter> { new("@jhdbh", input.DsNum), new("@TenantId", tenantId) });

+ 8 - 7
server/Plugins/Admin.NET.Plugin.AiDOP/ProcurementExecution/SupplierShipmentService.cs

@@ -311,11 +311,11 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
                     INSERT INTO scm_shbqhis
                     (glid, sh_material_code, sh_material_name, sh_material_ggxh, sh_delivery_quantity,
                      sh_material_dw, remarks, bzsl, order_type, po_billno, shdh, shdhh, scrq, scph, xh,
-                     gysdm, gysmc, po_billline, bbh, th, yt, ccrq, shpc, jhdbh, jhdhh)
+                     gysdm, gysmc, po_billline, bbh, th, yt, ccrq, shpc, jhdbh, jhdhh, tenant_id)
                     SELECT
                      glid, sh_material_code, sh_material_name, sh_material_ggxh, sh_delivery_quantity,
                      sh_material_dw, remarks, bzsl, order_type, po_billno, shdh, shdhh, scrq, scph, xh,
-                     gysdm, gysmc, po_billline, bbh, th, yt, ccrq, shpc, jhdbh, jhdhh
+                     gysdm, gysmc, po_billline, bbh, th, yt, ccrq, shpc, jhdbh, jhdhh, tenant_id
                     FROM scm_shbq WHERE shdh=@shdh;
                     """,
                     new List<SugarParameter> { new("@shdh", shddh) });
@@ -496,7 +496,7 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
                     // 多箱:前 N-1 箱按包装数量
                     while (remainLabels > 1)
                     {
-                        await InsertLabelAsync(shddh, row, packQty, xh, gysdm, gysmc);
+                        await InsertLabelAsync(shddh, row, packQty, xh, gysdm, gysmc, tenantId);
                         remainQty -= packQty;
                         remainLabels--;
                         xh++;
@@ -504,7 +504,7 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
 
                     if (remainQty > 0)
                     {
-                        await InsertLabelAsync(shddh, row, remainQty, xh, gysdm, gysmc);
+                        await InsertLabelAsync(shddh, row, remainQty, xh, gysdm, gysmc, tenantId);
                     }
                 }
 
@@ -634,7 +634,7 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
         }
     }
 
-    private async Task InsertLabelAsync(string shddh, LabelSourceRow row, decimal qty, int xh, string gysdm, string gysmc)
+    private async Task InsertLabelAsync(string shddh, LabelSourceRow row, decimal qty, int xh, string gysdm, string gysmc, long tenantId)
     {
         var jhdhh3 = Right(row.Jhdhh, 3);
         var xh4 = Right(xh.ToString(), 4);
@@ -645,11 +645,11 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
             INSERT INTO scm_shbq
             (glid, sh_material_code, sh_material_name, sh_material_ggxh, sh_delivery_quantity,
              sh_material_dw, remarks, bzsl, order_type, po_billno, shdh, shdhh, scrq, scph, xh,
-             gysdm, gysmc, po_billline, bbh, th, yt, ccrq, shpc, jhdbh, jhdhh)
+             gysdm, gysmc, po_billline, bbh, th, yt, ccrq, shpc, jhdbh, jhdhh, tenant_id)
             VALUES
             (@glid, @wlbm, @wlmc, @ggxh, @qty,
              @dw, @bz, @bzsl, @ddlx, @ddh, @shdh, @hh, @scrq, @scph, @xh,
-             @gysdm, @gysmc, @pohh, @bbh, @th, @yt, @ccrq, @shpc, @jhdbh, @jhdhh);
+             @gysdm, @gysmc, @pohh, @bbh, @th, @yt, @ccrq, @shpc, @jhdbh, @jhdhh, @TenantId);
             """,
             new List<SugarParameter>
             {
@@ -678,6 +678,7 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
                 new("@shpc", row.Shpc ?? string.Empty),
                 new("@jhdbh", row.Jhdbh ?? string.Empty),
                 new("@jhdhh", row.Jhdhh ?? string.Empty),
+                new("@TenantId", tenantId),
             });
     }