Sfoglia il codice sorgente

feat: 新增S4交货与发货管理并对齐工单实体

新增 S4 供应商交货管理/发货单前后端与菜单补链能力,同时按数据库结构调整工单相关实体字段映射并修正联表类型转换;同步前后端版本号到 Web 2.4.119 / server 1.0.86。

Co-authored-by: Cursor <cursoragent@cursor.com>
Pengxy 2 settimane fa
parent
commit
9eafc61fa9
19 ha cambiato i file con 2190 aggiunte e 23 eliminazioni
  1. 1 1
      Web/package.json
  2. 127 0
      Web/src/views/aidop/s4/api/procurementExecution.ts
  3. 233 0
      Web/src/views/aidop/s4/delivery/supplierDeliveryManagementList.vue
  4. 199 0
      Web/src/views/aidop/s4/delivery/supplierShipmentForm.vue
  5. 207 0
      Web/src/views/aidop/s4/delivery/supplierShipmentList.vue
  6. 3 3
      server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj
  7. 98 0
      server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/AidopMenuLinkSync.cs
  8. 94 0
      server/Plugins/Admin.NET.Plugin.AiDOP/ProcurementExecution/Dto/ProcurementExecutionDto.cs
  9. 90 0
      server/Plugins/Admin.NET.Plugin.AiDOP/ProcurementExecution/Entity/ScmJhjhJq.cs
  10. 93 0
      server/Plugins/Admin.NET.Plugin.AiDOP/ProcurementExecution/Entity/ScmShd.cs
  11. 96 0
      server/Plugins/Admin.NET.Plugin.AiDOP/ProcurementExecution/Entity/ScmShdzb.cs
  12. 338 0
      server/Plugins/Admin.NET.Plugin.AiDOP/ProcurementExecution/SupplierDeliveryManagementService.cs
  13. 531 0
      server/Plugins/Admin.NET.Plugin.AiDOP/ProcurementExecution/SupplierShipmentService.cs
  14. 1 1
      server/Plugins/Admin.NET.Plugin.AiDOP/Production/ExecutableDailyPlanService.cs
  15. 61 0
      server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/SysMenuSeedData.cs
  16. 1 1
      server/Plugins/Admin.NET.Plugin.AiDOP/WorkOrder/Entity/MesMoentry.cs
  17. 4 4
      server/Plugins/Admin.NET.Plugin.AiDOP/WorkOrder/Entity/MesMorder.cs
  18. 6 6
      server/Plugins/Admin.NET.Plugin.AiDOP/WorkOrder/Entity/WorkOrdDetail.cs
  19. 7 7
      server/Plugins/Admin.NET.Plugin.AiDOP/WorkOrder/Entity/WorkOrdRouting.cs

+ 1 - 1
Web/package.json

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

+ 127 - 0
Web/src/views/aidop/s4/api/procurementExecution.ts

@@ -0,0 +1,127 @@
+import service from '/@/utils/request';
+
+export interface Paged<T> {
+	total: number;
+	page: number;
+	pageSize: number;
+	list: T[];
+}
+
+export interface SupplierDeliveryRow {
+	id?: string;
+	sffb?: string;
+	wlbm?: string;
+	wlms?: string;
+	buyer?: string;
+	gysdm?: string;
+	gysmc?: string;
+	cgdd?: string;
+	ddhh?: number;
+	jhdsl?: number;
+	dfhsl?: number;
+	dsnum?: string;
+	requestdate?: string;
+	schedqty?: number;
+	jqhfnew?: string;
+	ztsl?: number;
+	zsl1?: number;
+	rksl?: number;
+	bhgsl?: number;
+	thsl?: number;
+	bz?: string;
+}
+
+export interface SupplierShipmentRow {
+	mid: number;
+	id: number;
+	shddh?: string;
+	poBill?: string;
+	usage?: string;
+	jhshrq?: string;
+	shMaterialCode?: string;
+	shMaterialName?: string;
+	shDeliveryQuantity?: number;
+	sfpc?: number;
+	pcrksl?: number;
+	shPurchaseName?: string;
+	shpc?: string;
+	scph?: string;
+	wldh?: string;
+	dycs?: number;
+	shzt?: string;
+	th?: string;
+	state?: number;
+}
+
+export interface SupplierShipmentDetailRow {
+	id?: number | null;
+	hh?: number | null;
+	poBill?: string;
+	poBillLine?: string;
+	orderType?: string;
+	shMaterialCode?: string;
+	shMaterialName?: string;
+	th?: string;
+	shDeliveryQuantity?: number | null;
+	bzsl?: number | null;
+	bqsl?: number | null;
+	shMaterialDw?: string;
+	scrq?: string;
+	scph?: string;
+	remarks?: string;
+	djsl?: number | null;
+	jybb?: string;
+	jhdbh?: string;
+}
+
+export interface SupplierShipmentFormData {
+	id?: number | null;
+	shddh?: string;
+	jhshrq?: string;
+	wlsc?: string;
+	yjdhrq?: string;
+	shPurchaseName?: string;
+	shPurchaseNum?: string;
+	wldh?: string;
+	sfpc?: number;
+	chbg?: string;
+	pcsm?: string;
+	details: SupplierShipmentDetailRow[];
+}
+
+export function fetchSupplierDeliveryList(params: any) {
+	return service.get<Paged<SupplierDeliveryRow>>('/api/ProcurementExecution/supplier-delivery/list', { params }).then((r) => r.data);
+}
+
+export function publishSupplierDelivery(ids: string) {
+	return service.post('/api/ProcurementExecution/supplier-delivery/publish', { ids }).then((r) => r.data);
+}
+
+export function closeDeliveryOrder(dsNum: string) {
+	return service.post('/api/ProcurementExecution/supplier-delivery/close', { dsNum }).then((r) => r.data);
+}
+
+export function fetchSupplierShipmentList(params: any) {
+	return service.get<Paged<SupplierShipmentRow>>('/api/ProcurementExecution/supplier-shipment/list', { params }).then((r) => r.data);
+}
+
+export function fetchSupplierShipmentDetail(id: number) {
+	return service.get<SupplierShipmentFormData>(`/api/ProcurementExecution/supplier-shipment/${id}`).then((r) => r.data);
+}
+
+export function fetchSupplierShipmentDraft(ids: string) {
+	return service.get<SupplierShipmentFormData>('/api/ProcurementExecution/supplier-shipment/create-draft', { params: { ids } }).then((r) => r.data);
+}
+
+export function saveSupplierShipment(body: SupplierShipmentFormData) {
+	return service.post('/api/ProcurementExecution/supplier-shipment/save', body).then((r) => r.data);
+}
+
+export function deleteSupplierShipment(id: number) {
+	return service.post('/api/ProcurementExecution/supplier-shipment/delete', { id }).then((r) => r.data);
+}
+
+export function shipmentPlaceholderAction(action: 'generate-label' | 'print-shipping-note' | 'print-label', id: number) {
+	return service.post(`/api/ProcurementExecution/supplier-shipment/${action}`, { id }).then((r) => r.data);
+}
+

+ 233 - 0
Web/src/views/aidop/s4/delivery/supplierDeliveryManagementList.vue

@@ -0,0 +1,233 @@
+<template>
+	<AidopDemoShell :title="pageTitle" subtitle="供应商交货管理">
+		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
+			<el-form-item label="采购单号"><el-input v-model="query.cgdd" clearable style="width: 150px" /></el-form-item>
+			<el-form-item label="交货单号"><el-input v-model="query.dsNum" clearable style="width: 150px" /></el-form-item>
+			<el-form-item label="物料编码"><el-input v-model="query.wlbm" clearable style="width: 150px" /></el-form-item>
+			<el-form-item label="供应商"><el-input v-model="query.gys" clearable style="width: 160px" /></el-form-item>
+			<el-form-item>
+				<el-button type="primary" @click="doSearch">查询</el-button>
+				<el-button @click="resetQuery">重置</el-button>
+			</el-form-item>
+		</el-form>
+
+		<div class="toolbar">
+			<el-button type="primary" @click="onCreateShipment">生成发货单</el-button>
+			<el-button type="success" @click="onPublish">发布</el-button>
+			<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))">
+					{{ item.label }}
+				</el-checkbox>
+			</el-popover>
+		</div>
+
+		<el-table :data="rows" v-loading="loading" border stripe @selection-change="onSelectionChange" @sort-change="onSortChange">
+			<el-table-column type="selection" width="44" fixed="left" />
+			<el-table-column v-if="col.sffb" prop="sffb" label="发布日期" width="120" fixed="left" sortable="custom">
+				<template #default="{ row }">{{ fmtDate(row.sffb) }}</template>
+			</el-table-column>
+			<el-table-column v-if="col.wlbm" prop="wlbm" label="物料编码" width="120" sortable="custom" />
+			<el-table-column v-if="col.wlms" prop="wlms" label="物料描述" min-width="150" show-overflow-tooltip sortable="custom" />
+			<el-table-column v-if="col.buyer" prop="buyer" label="采购组" width="100" sortable="custom" />
+			<el-table-column v-if="col.gysdm" prop="gysdm" label="供应商代码" width="120" sortable="custom" />
+			<el-table-column v-if="col.gysmc" prop="gysmc" label="供应商名称" min-width="140" show-overflow-tooltip sortable="custom" />
+			<el-table-column v-if="col.cgdd" prop="cgdd" label="采购单号" width="140" sortable="custom" />
+			<el-table-column v-if="col.ddhh" prop="ddhh" label="采购单行号" width="100" sortable="custom" />
+			<el-table-column v-if="col.jhdsl" prop="jhdsl" label="采购订单数量" width="120" align="right" sortable="custom" />
+			<el-table-column v-if="col.dfhsl" prop="dfhsl" label="待交数量" width="100" align="right" sortable="custom" />
+			<el-table-column v-if="col.dsnum" prop="dsnum" label="交货单号" width="130" sortable="custom" />
+			<el-table-column v-if="col.requestdate" prop="requestdate" label="到货日期" width="120" sortable="custom">
+				<template #default="{ row }">{{ fmtDate(row.requestdate) }}</template>
+			</el-table-column>
+			<el-table-column v-if="col.schedqty" prop="schedqty" label="交货数量" width="100" align="right" sortable="custom" />
+			<el-table-column v-if="col.jqhfnew" prop="jqhfnew" label="交期回复" width="120" sortable="custom">
+				<template #default="{ row }">{{ fmtDate(row.jqhfnew) }}</template>
+			</el-table-column>
+			<el-table-column v-if="col.ztsl" prop="ztsl" label="在途量" width="90" align="right" sortable="custom" />
+			<el-table-column v-if="col.zsl1" prop="zsl1" label="在检量" width="90" align="right" sortable="custom" />
+			<el-table-column v-if="col.rksl" prop="rksl" label="入库量" width="90" align="right" sortable="custom" />
+			<el-table-column v-if="col.bhgsl" prop="bhgsl" label="不合格量" width="100" align="right" sortable="custom" />
+			<el-table-column v-if="col.thsl" prop="thsl" label="退货量" width="90" align="right" sortable="custom" />
+			<el-table-column v-if="col.bz" prop="bz" label="备注" min-width="140" show-overflow-tooltip sortable="custom" />
+			<el-table-column label="操作" width="110" fixed="right">
+				<template #default="{ row }">
+					<el-button link type="danger" @click="onCloseDelivery(row)">交货单关闭</el-button>
+				</template>
+			</el-table-column>
+		</el-table>
+
+		<div class="pager">
+			<el-pagination
+				v-model:current-page="query.page"
+				v-model:page-size="query.pageSize"
+				:total="total"
+				:page-sizes="[10, 20, 50]"
+				layout="total, sizes, prev, pager, next"
+				@current-change="loadList"
+				@size-change="loadList"
+			/>
+		</div>
+	</AidopDemoShell>
+</template>
+
+<script setup lang="ts" name="aidopS4SupplierDeliveryManagementList">
+import { computed, onMounted, reactive, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
+import { closeDeliveryOrder, fetchSupplierDeliveryList, publishSupplierDelivery, type SupplierDeliveryRow } from '../api/procurementExecution';
+
+const route = useRoute();
+const router = useRouter();
+const pageTitle = computed(() => (route.meta?.title as string) || '供应商交货管理');
+
+const query = reactive({
+	cgdd: '',
+	dsNum: '',
+	wlbm: '',
+	gys: '',
+	page: 1,
+	pageSize: 10,
+	sortField: '',
+	sortOrder: '',
+});
+
+const loading = ref(false);
+const rows = ref<SupplierDeliveryRow[]>([]);
+const total = ref(0);
+const selectedRows = ref<SupplierDeliveryRow[]>([]);
+
+const col = reactive({
+	sffb: true, wlbm: true, wlms: true, buyer: true, gysdm: true, gysmc: true, cgdd: true, ddhh: true,
+	jhdsl: true, dfhsl: true, dsnum: true, requestdate: true, schedqty: true, jqhfnew: true, ztsl: true,
+	zsl1: true, rksl: true, bhgsl: true, thsl: true, bz: true,
+});
+
+type ColumnKey = keyof typeof col;
+const toggleItems: Array<{ key: ColumnKey; label: string }> = [
+	{ key: 'sffb', label: '发布日期' }, { key: 'wlbm', label: '物料编码' }, { key: 'wlms', label: '物料描述' }, { key: 'buyer', label: '采购组' },
+	{ key: 'gysdm', label: '供应商代码' }, { key: 'gysmc', label: '供应商名称' }, { key: 'cgdd', label: '采购单号' }, { key: 'ddhh', label: '采购单行号' },
+	{ key: 'jhdsl', label: '采购订单数量' }, { key: 'dfhsl', label: '待交数量' }, { key: 'dsnum', label: '交货单号' }, { key: 'requestdate', label: '到货日期' },
+	{ key: 'schedqty', label: '交货数量' }, { key: 'jqhfnew', label: '交期回复' }, { key: 'ztsl', label: '在途量' }, { key: 'zsl1', label: '在检量' },
+	{ key: 'rksl', label: '入库量' }, { key: 'bhgsl', label: '不合格量' }, { key: 'thsl', label: '退货量' }, { key: 'bz', label: '备注' },
+];
+
+function fmtDate(v?: string | null) {
+	return v ? String(v).slice(0, 10) : '';
+}
+function onSelectionChange(val: SupplierDeliveryRow[]) {
+	selectedRows.value = val;
+}
+function setColumnVisible(key: ColumnKey, visible: boolean) {
+	col[key] = visible;
+}
+function onSortChange({ prop, order }: { prop: string; order: string | null }) {
+	query.sortField = prop || '';
+	query.sortOrder = order === 'ascending' ? 'asc' : order === 'descending' ? 'desc' : '';
+	loadList();
+}
+
+async function loadList() {
+	loading.value = true;
+	try {
+		const data = await fetchSupplierDeliveryList({ ...query });
+		rows.value = data.list || [];
+		total.value = data.total || 0;
+	} finally {
+		loading.value = false;
+	}
+}
+
+function doSearch() {
+	query.page = 1;
+	loadList();
+}
+function resetQuery() {
+	query.cgdd = '';
+	query.dsNum = '';
+	query.wlbm = '';
+	query.gys = '';
+	query.page = 1;
+	query.sortField = '';
+	query.sortOrder = '';
+	loadList();
+}
+
+function getSelectedIdsAndSupplier() {
+	if (!selectedRows.value.length) {
+		ElMessage.warning('请先勾选数据');
+		return null;
+	}
+	const supplier = selectedRows.value[0].gysdm || '';
+	if (!supplier) {
+		ElMessage.warning('所选数据缺少供应商代码');
+		return null;
+	}
+	const notSame = selectedRows.value.some((x) => (x.gysdm || '') !== supplier);
+	if (notSame) {
+		ElMessage.warning('请选择相同供应商的数据');
+		return null;
+	}
+	const ids = selectedRows.value.map((x) => x.id).filter(Boolean).join(',');
+	if (!ids) {
+		ElMessage.warning('所选数据缺少ID');
+		return null;
+	}
+	return { ids, supplier };
+}
+
+function onCreateShipment() {
+	const payload = getSelectedIdsAndSupplier();
+	if (!payload) return;
+	router.push({
+		path: '/aidop/s4/delivery/supplier-shipment-form',
+		query: { mode: 'create', ids: payload.ids },
+	});
+}
+
+async function onPublish() {
+	const payload = getSelectedIdsAndSupplier();
+	if (!payload) return;
+	await publishSupplierDelivery(payload.ids);
+	ElMessage.success('发布成功');
+	await loadList();
+}
+
+async function onCloseDelivery(row: SupplierDeliveryRow) {
+	if (!row.dsnum) {
+		ElMessage.warning('当前行无交货单号');
+		return;
+	}
+	await ElMessageBox.confirm(`确认关闭交货单 ${row.dsnum} 吗?`, '交货单关闭', { type: 'warning' });
+	await closeDeliveryOrder(row.dsnum);
+	ElMessage.success('关闭成功');
+	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);
+}
+
+onMounted(loadList);
+</script>
+
+<style scoped lang="scss">
+@import '/@/views/aidop/styles/aidop-demo.scss';
+.mb12 { margin-bottom: 12px; }
+.toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
+.pager { margin-top: 12px; display: flex; justify-content: flex-end; }
+</style>
+

+ 199 - 0
Web/src/views/aidop/s4/delivery/supplierShipmentForm.vue

@@ -0,0 +1,199 @@
+<template>
+	<AidopDemoShell :title="title">
+		<div v-loading="loading">
+			<el-form :model="form" label-width="120px">
+				<el-row :gutter="12">
+					<el-col :span="12"><el-form-item label="送货单号"><el-input v-model="form.shddh" :disabled="isView" /></el-form-item></el-col>
+					<el-col :span="12">
+						<el-form-item label="计划发货日期">
+							<el-date-picker v-model="form.jhshrq" type="date" value-format="YYYY-MM-DD" style="width: 100%" :disabled="isView" />
+						</el-form-item>
+					</el-col>
+					<el-col :span="12"><el-form-item label="物流时长"><el-input v-model="form.wlsc" :disabled="isView" /></el-form-item></el-col>
+					<el-col :span="12">
+						<el-form-item label="预计到货日期">
+							<el-date-picker v-model="form.yjdhrq" type="date" value-format="YYYY-MM-DD" style="width: 100%" :disabled="isView" />
+						</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-radio-group v-model="form.sfpc" :disabled="isView">
+								<el-radio :value="0">否</el-radio>
+								<el-radio :value="1">是</el-radio>
+							</el-radio-group>
+						</el-form-item>
+					</el-col>
+					<el-col :span="12">
+						<el-form-item label="偏差申请附件">
+							<el-upload :auto-upload="false" :show-file-list="false" :disabled="isView" :on-change="(file) => (form.chbg = file.name)">
+								<el-button :disabled="isView">上传</el-button>
+							</el-upload>
+							<span class="upload-name">{{ form.chbg }}</span>
+						</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-row>
+			</el-form>
+
+			<el-divider content-position="left">发货明细</el-divider>
+			<div class="sub-toolbar" v-if="!isView">
+				<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="100"><template #default="{ row }"><el-input-number v-model="row.shDeliveryQuantity" :controls="false" :min="0" :disabled="isView" style="width: 100%" /></template></el-table-column>
+				<el-table-column label="每箱数量" width="100"><template #default="{ row }"><el-input-number v-model="row.bzsl" :controls="false" :min="0" :disabled="isView" style="width: 100%" /></template></el-table-column>
+				<el-table-column label="标签数量" width="100"><template #default="{ row }"><el-input-number v-model="row.bqsl" :controls="false" :min="0" :disabled="isView" 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="120">
+					<template #default="{ row }">
+						<el-date-picker v-model="row.scrq" type="date" value-format="YYYY-MM-DD" :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="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="130">
+					<template #default="{ row }">
+						<el-upload :auto-upload="false" :show-file-list="false" :disabled="isView" :on-change="(file) => (row.jybb = file.name)">
+							<el-button :disabled="isView">上传</el-button>
+						</el-upload>
+						<div class="upload-name">{{ row.jybb }}</div>
+					</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">
+					<template #default="{ $index }">
+						<el-button link type="danger" @click="removeDetail($index)">删除</el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+
+			<div class="footer">
+				<el-button @click="onCancel">取消</el-button>
+				<el-button v-if="!isView" type="primary" :loading="saving" @click="onSave">保存</el-button>
+			</div>
+		</div>
+	</AidopDemoShell>
+</template>
+
+<script setup lang="ts" name="aidopS4SupplierShipmentForm">
+import { computed, onMounted, reactive, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { ElMessage } from 'element-plus';
+import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
+import {
+	fetchSupplierShipmentDetail,
+	fetchSupplierShipmentDraft,
+	saveSupplierShipment,
+	type SupplierShipmentDetailRow,
+	type SupplierShipmentFormData,
+} from '../api/procurementExecution';
+
+const route = useRoute();
+const router = useRouter();
+const mode = computed(() => String(route.query.mode || 'create') as 'create' | 'edit' | 'view');
+const id = computed(() => Number(route.query.id || 0));
+const ids = computed(() => String(route.query.ids || ''));
+const isView = computed(() => mode.value === 'view');
+const title = computed(() => (mode.value === 'create' ? '发货单新增' : mode.value === 'edit' ? '发货单编辑' : '发货单查看'));
+
+const loading = ref(false);
+const saving = ref(false);
+const form = reactive<SupplierShipmentFormData>({
+	id: null,
+	shddh: '',
+	jhshrq: '',
+	wlsc: '',
+	yjdhrq: '',
+	shPurchaseName: '',
+	shPurchaseNum: '',
+	wldh: '',
+	sfpc: 0,
+	chbg: '',
+	pcsm: '',
+	details: [],
+});
+
+function setForm(data: SupplierShipmentFormData) {
+	form.id = data.id || null;
+	form.shddh = data.shddh || '';
+	form.jhshrq = data.jhshrq || '';
+	form.wlsc = data.wlsc || '';
+	form.yjdhrq = data.yjdhrq || '';
+	form.shPurchaseName = data.shPurchaseName || '';
+	form.shPurchaseNum = data.shPurchaseNum || '';
+	form.wldh = data.wldh || '';
+	form.sfpc = data.sfpc ?? 0;
+	form.chbg = data.chbg || '';
+	form.pcsm = data.pcsm || '';
+	form.details = (data.details || []).map((d, i) => ({ ...d, hh: d.hh || i + 1 }));
+}
+
+function addDetail() {
+	form.details.push({
+		id: null, hh: form.details.length + 1, poBill: '', poBillLine: '', orderType: '', shMaterialCode: '',
+		shMaterialName: '', th: '', shDeliveryQuantity: 0, bzsl: 0, bqsl: 0, shMaterialDw: '', scrq: '', scph: '', remarks: '',
+		djsl: 0, jybb: '', jhdbh: '',
+	});
+}
+function removeDetail(index: number) {
+	form.details.splice(index, 1);
+}
+
+async function loadData() {
+	loading.value = true;
+	try {
+		if (mode.value === 'create' && ids.value) {
+			const draft = await fetchSupplierShipmentDraft(ids.value);
+			setForm(draft);
+			return;
+		}
+		if ((mode.value === 'edit' || mode.value === 'view') && id.value > 0) {
+			const detail = await fetchSupplierShipmentDetail(id.value);
+			setForm(detail);
+			return;
+		}
+	} finally {
+		loading.value = false;
+	}
+}
+
+async function onSave() {
+	saving.value = true;
+	try {
+		await saveSupplierShipment({
+			...form,
+			id: form.id || undefined,
+			details: form.details.map((d: SupplierShipmentDetailRow, i) => ({ ...d, hh: d.hh || i + 1 })),
+		});
+		ElMessage.success('保存成功');
+		router.push('/aidop/s4/delivery/supplier-shipment');
+	} finally {
+		saving.value = false;
+	}
+}
+
+function onCancel() {
+	router.push('/aidop/s4/delivery/supplier-shipment');
+}
+
+onMounted(loadData);
+</script>
+
+<style scoped lang="scss">
+@import '/@/views/aidop/styles/aidop-demo.scss';
+.sub-toolbar { margin-bottom: 8px; }
+.footer { display: flex; justify-content: flex-end; gap: 10px; margin-top: 12px; }
+.upload-name { margin-left: 8px; color: #606266; font-size: 12px; }
+</style>
+

+ 207 - 0
Web/src/views/aidop/s4/delivery/supplierShipmentList.vue

@@ -0,0 +1,207 @@
+<template>
+	<AidopDemoShell :title="pageTitle" subtitle="供应商发货单">
+		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
+			<el-form-item label="发货日期">
+				<el-date-picker v-model="query.jhshrqFrom" type="date" value-format="YYYY-MM-DD" style="width: 150px" />
+			</el-form-item>
+			<el-form-item label="供应商名称"><el-input v-model="query.gysmc" clearable style="width: 150px" /></el-form-item>
+			<el-form-item label="发货单编号"><el-input v-model="query.shddh" clearable style="width: 150px" /></el-form-item>
+			<el-form-item label="物流单号"><el-input v-model="query.wldh" clearable style="width: 150px" /></el-form-item>
+			<el-form-item label="订单编号"><el-input v-model="query.poBill" clearable style="width: 150px" /></el-form-item>
+			<el-form-item label="物料编码"><el-input v-model="query.shMaterialCode" clearable style="width: 150px" /></el-form-item>
+			<el-form-item label="送货状态">
+				<el-select v-model="query.shzt" clearable style="width: 130px">
+					<el-option label="待收" value="待收" />
+					<el-option label="收货中" value="收货中" />
+					<el-option label="完成" value="完成" />
+				</el-select>
+			</el-form-item>
+			<el-form-item label="客户批次号"><el-input v-model="query.shpc" clearable style="width: 150px" /></el-form-item>
+			<el-form-item>
+				<el-button type="primary" @click="doSearch">查询</el-button>
+				<el-button @click="resetQuery">重置</el-button>
+			</el-form-item>
+		</el-form>
+
+		<div class="toolbar">
+			<el-button type="primary" @click="openForm('create')">添加</el-button>
+			<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))">
+					{{ item.label }}
+				</el-checkbox>
+			</el-popover>
+		</div>
+
+		<el-table :data="rows" v-loading="loading" border stripe @sort-change="onSortChange" :row-class-name="rowClassName">
+			<el-table-column v-if="col.shddh" prop="shddh" label="发货单编号" width="140" fixed="left" sortable="custom" />
+			<el-table-column v-if="col.shzt" prop="shzt" label="送货状态" width="100" fixed="left" sortable="custom" />
+			<el-table-column v-if="col.poBill" prop="poBill" label="订单编号" width="130" sortable="custom" />
+			<el-table-column v-if="col.usage" prop="usage" label="物料类型" width="100" sortable="custom" />
+			<el-table-column v-if="col.jhshrq" prop="jhshrq" label="发货日期" width="120" sortable="custom" />
+			<el-table-column v-if="col.shMaterialCode" prop="shMaterialCode" label="物料编码" width="120" sortable="custom" />
+			<el-table-column v-if="col.shMaterialName" prop="shMaterialName" label="物料名称" min-width="150" show-overflow-tooltip sortable="custom" />
+			<el-table-column v-if="col.shDeliveryQuantity" prop="shDeliveryQuantity" label="发货数量" width="100" align="right" sortable="custom" />
+			<el-table-column v-if="col.sfpc" label="是否偏差" width="90" sortable="custom">
+				<template #default="{ row }">{{ row.sfpc === 1 ? '是' : '否' }}</template>
+			</el-table-column>
+			<el-table-column v-if="col.pcrksl" prop="pcrksl" label="合格入库" width="100" align="right" sortable="custom" />
+			<el-table-column v-if="col.shPurchaseName" prop="shPurchaseName" label="供应商名称" min-width="140" show-overflow-tooltip sortable="custom" />
+			<el-table-column v-if="col.shpc" prop="shpc" label="客户批次号" width="120" sortable="custom" />
+			<el-table-column v-if="col.scph" prop="scph" label="供应商批号" width="120" sortable="custom" />
+			<el-table-column v-if="col.wldh" prop="wldh" label="物流单号" width="120" sortable="custom" />
+			<el-table-column v-if="col.dycs" prop="dycs" label="已打次数" width="100" sortable="custom" />
+			<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 link type="primary" @click="onPlaceholder('print-shipping-note', row)">打印送货单</el-button>
+					<el-button link type="primary" @click="onPlaceholder('print-label', 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 link @click="openForm('view', row.mid)">查看</el-button>
+				</template>
+			</el-table-column>
+		</el-table>
+
+		<div class="pager">
+			<el-pagination
+				v-model:current-page="query.page"
+				v-model:page-size="query.pageSize"
+				:total="total"
+				:page-sizes="[10, 20, 50]"
+				layout="total, sizes, prev, pager, next"
+				@current-change="loadList"
+				@size-change="loadList"
+			/>
+		</div>
+	</AidopDemoShell>
+</template>
+
+<script setup lang="ts" name="aidopS4SupplierShipmentList">
+import { computed, onMounted, reactive, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
+import { deleteSupplierShipment, fetchSupplierShipmentList, shipmentPlaceholderAction, type SupplierShipmentRow } from '../api/procurementExecution';
+
+const route = useRoute();
+const router = useRouter();
+const pageTitle = computed(() => (route.meta?.title as string) || '供应商发货单');
+
+const query = reactive({
+	jhshrqFrom: '',
+	gysmc: '',
+	shddh: '',
+	wldh: '',
+	poBill: '',
+	shMaterialCode: '',
+	shzt: '',
+	shpc: '',
+	page: 1,
+	pageSize: 10,
+	sortField: '',
+	sortOrder: '',
+});
+
+const loading = ref(false);
+const rows = ref<SupplierShipmentRow[]>([]);
+const total = ref(0);
+
+const col = reactive({
+	shddh: true, poBill: true, usage: true, jhshrq: true, shMaterialCode: true, shMaterialName: true,
+	shDeliveryQuantity: true, sfpc: true, pcrksl: true, shPurchaseName: true, shpc: true, scph: true, wldh: true, dycs: true, shzt: true, th: true,
+});
+type ColumnKey = keyof typeof col;
+const toggleItems: Array<{ key: ColumnKey; label: string }> = [
+	{ key: 'shddh', label: '发货单编号' }, { key: 'poBill', label: '订单编号' }, { key: 'usage', label: '物料类型' }, { key: 'jhshrq', label: '发货日期' },
+	{ key: 'shMaterialCode', label: '物料编码' }, { key: 'shMaterialName', label: '物料名称' }, { key: 'shDeliveryQuantity', label: '发货数量' }, { key: 'sfpc', label: '是否偏差' },
+	{ key: 'pcrksl', label: '合格入库' }, { key: 'shPurchaseName', label: '供应商名称' }, { key: 'shpc', label: '客户批次号' }, { key: 'scph', label: '供应商批号' },
+	{ key: 'wldh', label: '物流单号' }, { key: 'dycs', label: '已打次数' }, { key: 'shzt', label: '送货状态' }, { key: 'th', label: '图号' },
+];
+
+function setColumnVisible(key: ColumnKey, visible: boolean) {
+	col[key] = visible;
+}
+function onSortChange({ prop, order }: { prop: string; order: string | null }) {
+	query.sortField = prop || '';
+	query.sortOrder = order === 'ascending' ? 'asc' : order === 'descending' ? 'desc' : '';
+	loadList();
+}
+function rowClassName({ row }: { row: SupplierShipmentRow }) {
+	return row.state === 0 ? 'row-deleted' : '';
+}
+
+async function loadList() {
+	loading.value = true;
+	try {
+		const data = await fetchSupplierShipmentList({ ...query });
+		rows.value = data.list || [];
+		total.value = data.total || 0;
+	} finally {
+		loading.value = false;
+	}
+}
+
+function doSearch() {
+	query.page = 1;
+	loadList();
+}
+function resetQuery() {
+	Object.assign(query, {
+		jhshrqFrom: '', gysmc: '', shddh: '', wldh: '', poBill: '', shMaterialCode: '', shzt: '', shpc: '', page: 1, sortField: '', sortOrder: '',
+	});
+	loadList();
+}
+
+function openForm(mode: 'create' | 'edit' | 'view', id?: number) {
+	router.push({
+		path: '/aidop/s4/delivery/supplier-shipment-form',
+		query: { mode, id: id ? String(id) : undefined },
+	});
+}
+
+async function onDelete(row: SupplierShipmentRow) {
+	await ElMessageBox.confirm(`确认删除发货单 ${row.shddh || ''} 吗?`, '删除确认', {
+		confirmButtonText: '删除',
+		cancelButtonText: '取消',
+		type: 'warning',
+	});
+	await deleteSupplierShipment(row.mid);
+	ElMessage.success('删除成功');
+	await loadList();
+}
+
+async function onPlaceholder(action: 'generate-label' | 'print-shipping-note' | 'print-label', row: SupplierShipmentRow) {
+	const ret = await shipmentPlaceholderAction(action, row.mid);
+	ElMessage.info(ret?.message || '功能预留');
+}
+
+function onExport() {
+	const headers = ['发货单编号', '订单编号', '物料类型', '发货日期', '物料编码', '物料名称', '发货数量', '是否偏差', '合格入库', '供应商名称', '客户批次号', '供应商批号', '物流单号', '已打次数', '送货状态', '图号'];
+	const lines = rows.value.map((r) => [
+		r.shddh ?? '', r.poBill ?? '', r.usage ?? '', r.jhshrq ?? '', r.shMaterialCode ?? '', r.shMaterialName ?? '', r.shDeliveryQuantity ?? '',
+		r.sfpc === 1 ? '是' : '否', r.pcrksl ?? '', r.shPurchaseName ?? '', r.shpc ?? '', r.scph ?? '', r.wldh ?? '', r.dycs ?? '', r.shzt ?? '', r.th ?? '',
+	]);
+	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);
+}
+
+onMounted(loadList);
+</script>
+
+<style scoped lang="scss">
+@import '/@/views/aidop/styles/aidop-demo.scss';
+.mb12 { margin-bottom: 12px; }
+.toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
+.pager { margin-top: 12px; display: flex; justify-content: flex-end; }
+:deep(.row-deleted .el-table__cell) { background: #ffe7e7 !important; }
+</style>
+

+ 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.85</AssemblyVersion>
-    <FileVersion>1.0.85</FileVersion>
-    <Version>1.0.85</Version>
+    <AssemblyVersion>1.0.86</AssemblyVersion>
+    <FileVersion>1.0.86</FileVersion>
+    <Version>1.0.86</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 98 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/AidopMenuLinkSync.cs

@@ -30,6 +30,7 @@ public static class AidopMenuLinkSync
         NormalizeS2OperationPlanLeafMenus(db);
         NormalizeS2WorkOrderProgressKanbanMenu(db);
         NormalizeS3SupplyMenus(db);
+        NormalizeS4DeliveryMenus(db);
         EnsureLinkagePlanMenu(db);
         RemoveDeprecatedS8DashboardChildMenu(db);
         NormalizeS8MenuParents(db);
@@ -628,6 +629,103 @@ public static class AidopMenuLinkSync
             "/aidop/s3/supply/workOrderMaterialReadinessKanban", "ele-List", 20, "S3 工单物料齐套上线看板");
     }
 
+    /// <summary>
+    /// S4「交货管理」目录与子菜单纠偏/补齐。
+    /// </summary>
+    private static void NormalizeS4DeliveryMenus(ISqlSugarClient db)
+    {
+        const long s4RootId = 1321000005000L;
+        const long deliveryDirId = 1322000000012L;
+        if (!db.Queryable<SysMenu>().Any(m => m.Id == deliveryDirId))
+            return;
+
+        var dir = db.Queryable<SysMenu>().First(m => m.Id == deliveryDirId);
+        if (dir != null)
+        {
+            var needFix = dir.Pid != s4RootId
+                          || dir.Title != "交货管理"
+                          || dir.Path != "/aidop/s4/delivery"
+                          || dir.Name != "aidopS4Delivery"
+                          || dir.Component != "Layout"
+                          || dir.Type != MenuTypeEnum.Dir
+                          || dir.Icon != "ele-Folder"
+                          || dir.Redirect != "/aidop/s4/delivery/supplier-delivery-management";
+            if (needFix)
+            {
+                dir.Pid = s4RootId;
+                dir.Title = "交货管理";
+                dir.Path = "/aidop/s4/delivery";
+                dir.Name = "aidopS4Delivery";
+                dir.Component = "Layout";
+                dir.Type = MenuTypeEnum.Dir;
+                dir.Icon = "ele-Folder";
+                dir.Redirect = "/aidop/s4/delivery/supplier-delivery-management";
+                db.Updateable(dir)
+                    .UpdateColumns(m => new { m.Pid, m.Title, m.Path, m.Name, m.Component, m.Type, m.Icon, m.Redirect })
+                    .ExecuteCommand();
+            }
+        }
+
+        var ct = DateTime.Parse("2022-02-10 00:00:00");
+        EnsureS3LeafMenu(
+            db, ct, 1329004100001L, deliveryDirId, "供应商交货管理",
+            "/aidop/s4/delivery/supplier-delivery-management", "aidopS4SupplierDeliveryManagement",
+            "/aidop/s4/delivery/supplierDeliveryManagementList", "ele-Document", 10, "S4 供应商交货管理");
+        EnsureS3LeafMenu(
+            db, ct, 1329004100002L, deliveryDirId, "供应商发货单",
+            "/aidop/s4/delivery/supplier-shipment", "aidopS4SupplierShipment",
+            "/aidop/s4/delivery/supplierShipmentList", "ele-Tickets", 20, "S4 供应商发货单");
+
+        var formMenu = db.Queryable<SysMenu>().First(m => m.Id == 1329004100003L);
+        if (formMenu == null)
+        {
+            db.Insertable(new SysMenu
+            {
+                Id = 1329004100003L,
+                Pid = deliveryDirId,
+                Title = "发货单表单",
+                Path = "/aidop/s4/delivery/supplier-shipment-form",
+                Name = "aidopS4SupplierShipmentForm",
+                Component = "/aidop/s4/delivery/supplierShipmentForm",
+                Icon = "ele-Document",
+                Type = MenuTypeEnum.Menu,
+                IsHide = true,
+                CreateTime = ct,
+                OrderNo = 90,
+                Status = StatusEnum.Enable,
+                Remark = "S4 发货单新增/编辑/查看表单"
+            }).ExecuteCommand();
+            return;
+        }
+
+        var formNeedFix = formMenu.Pid != deliveryDirId
+                          || formMenu.Path != "/aidop/s4/delivery/supplier-shipment-form"
+                          || formMenu.Name != "aidopS4SupplierShipmentForm"
+                          || formMenu.Component != "/aidop/s4/delivery/supplierShipmentForm"
+                          || formMenu.Title != "发货单表单"
+                          || formMenu.Icon != "ele-Document"
+                          || formMenu.Type != MenuTypeEnum.Menu
+                          || formMenu.OrderNo != 90
+                          || formMenu.IsHide != true
+                          || formMenu.Remark != "S4 发货单新增/编辑/查看表单";
+        if (!formNeedFix)
+            return;
+
+        formMenu.Pid = deliveryDirId;
+        formMenu.Path = "/aidop/s4/delivery/supplier-shipment-form";
+        formMenu.Name = "aidopS4SupplierShipmentForm";
+        formMenu.Component = "/aidop/s4/delivery/supplierShipmentForm";
+        formMenu.Title = "发货单表单";
+        formMenu.Icon = "ele-Document";
+        formMenu.Type = MenuTypeEnum.Menu;
+        formMenu.OrderNo = 90;
+        formMenu.IsHide = true;
+        formMenu.Remark = "S4 发货单新增/编辑/查看表单";
+        db.Updateable(formMenu)
+            .UpdateColumns(m => new { m.Pid, m.Path, m.Name, m.Component, m.Title, m.Icon, m.Type, m.OrderNo, m.IsHide, m.Remark })
+            .ExecuteCommand();
+    }
+
     private static void EnsureS3LeafMenu(
         ISqlSugarClient db,
         DateTime ct,

+ 94 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/ProcurementExecution/Dto/ProcurementExecutionDto.cs

@@ -0,0 +1,94 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Admin.NET.Plugin.AiDOP.ProcurementExecution.Dto;
+
+public class SupplierDeliveryListInput
+{
+    public int Page { get; set; } = 1;
+    public int PageSize { get; set; } = 10;
+    public string? Cgdd { get; set; }
+    public string? DsNum { get; set; }
+    public string? Wlbm { get; set; }
+    public string? Gys { get; set; }
+    public string? SortField { get; set; }
+    public string? SortOrder { get; set; }
+}
+
+public class SupplierDeliveryBatchInput
+{
+    [Required(ErrorMessage = "ids不能为空")]
+    public string Ids { get; set; } = string.Empty;
+}
+
+public class DeliveryCloseInput
+{
+    [Required(ErrorMessage = "交货单号不能为空")]
+    public string DsNum { get; set; } = string.Empty;
+}
+
+public class SupplierShipmentListInput
+{
+    public int Page { get; set; } = 1;
+    public int PageSize { get; set; } = 10;
+    public string? JhshrqFrom { get; set; }
+    public string? Gysmc { get; set; }
+    public string? Shddh { get; set; }
+    public string? Wldh { get; set; }
+    public string? PoBill { get; set; }
+    public string? ShMaterialCode { get; set; }
+    public string? Shzt { get; set; }
+    public string? Shpc { get; set; }
+    public string? SortField { get; set; }
+    public string? SortOrder { get; set; }
+}
+
+public class SupplierShipmentSaveInput
+{
+    public long? Id { get; set; }
+    public string? Shddh { get; set; }
+    public string? Jhshrq { get; set; }
+    public string? Wlsc { get; set; }
+    public string? Yjdhrq { get; set; }
+    public string? ShPurchaseName { get; set; }
+    public string? ShPurchaseNum { get; set; }
+    public string? Wldh { get; set; }
+    public int? Sfpc { get; set; }
+    public string? Chbg { get; set; }
+    public string? Pcsm { get; set; }
+    public List<SupplierShipmentDetailInput> Details { get; set; } = new();
+}
+
+public class SupplierShipmentDetailInput
+{
+    public long? Id { get; set; }
+    public int? Hh { get; set; }
+    public string? PoBill { get; set; }
+    public string? PoBillLine { get; set; }
+    public string? OrderType { get; set; }
+    public string? ShMaterialCode { get; set; }
+    public string? ShMaterialName { get; set; }
+    public string? Th { get; set; }
+    public decimal? ShDeliveryQuantity { get; set; }
+    public decimal? Bzsl { get; set; }
+    public decimal? Bqsl { get; set; }
+    public string? ShMaterialDw { get; set; }
+    public string? Scrq { get; set; }
+    public string? Scph { get; set; }
+    public string? Remarks { get; set; }
+    public decimal? Djsl { get; set; }
+    public string? Jybb { get; set; }
+    public string? Jhdbh { get; set; }
+}
+
+public class SupplierShipmentDeleteInput
+{
+    [Required]
+    public long Id { get; set; }
+}
+
+public class SupplierShipmentOperationInput
+{
+    [Required]
+    public long Id { get; set; }
+}
+

+ 90 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/ProcurementExecution/Entity/ScmJhjhJq.cs

@@ -0,0 +1,90 @@
+namespace Admin.NET.Plugin.AiDOP.ProcurementExecution.Entity;
+
+/// <summary>
+/// 交期发布/回复表(scm_jhjh_jq)
+/// </summary>
+[SugarTable("scm_jhjh_jq")]
+public class ScmJhjhJq
+{
+    [SugarColumn(ColumnName = "id", IsPrimaryKey = true, Length = 36)]
+    public string Id { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "glid", IsNullable = true, Length = 50)]
+    public string? Glid { get; set; }
+
+    [SugarColumn(ColumnName = "wlbm", IsNullable = true, Length = 500)]
+    public string? Wlbm { get; set; }
+
+    [SugarColumn(ColumnName = "wlms", IsNullable = true, Length = 500)]
+    public string? Wlms { get; set; }
+
+    [SugarColumn(ColumnName = "wlgg", IsNullable = true, Length = 500)]
+    public string? Wlgg { get; set; }
+
+    [SugarColumn(ColumnName = "cgdd", IsNullable = true, Length = 500)]
+    public string? Cgdd { get; set; }
+
+    [SugarColumn(ColumnName = "jhdsl", IsNullable = true, Length = 500)]
+    public string? Jhdsl { get; set; }
+
+    [SugarColumn(ColumnName = "wjhsl", IsNullable = true, Length = 500)]
+    public string? Wjhsl { get; set; }
+
+    [SugarColumn(ColumnName = "jhd", IsNullable = true, Length = 500)]
+    public string? Jhd { get; set; }
+
+    [SugarColumn(ColumnName = "yjjhrq", IsNullable = true, Length = 500)]
+    public string? Yjjhrq { get; set; }
+
+    [SugarColumn(ColumnName = "jqhf", IsNullable = true, Length = 500)]
+    public string? Jqhf { get; set; }
+
+    [SugarColumn(ColumnName = "type", IsNullable = true, Length = 50)]
+    public string? Type { get; set; }
+
+    [SugarColumn(ColumnName = "flag", IsNullable = true)]
+    public int? Flag { get; set; }
+
+    [SugarColumn(ColumnName = "scrq", IsNullable = true, Length = 500)]
+    public string? Scrq { get; set; }
+
+    [SugarColumn(ColumnName = "scrid", IsNullable = true, Length = 500)]
+    public string? Scrid { get; set; }
+
+    [SugarColumn(ColumnName = "scrxm", IsNullable = true, Length = 500)]
+    public string? Scrxm { get; set; }
+
+    [SugarColumn(ColumnName = "gysdm", IsNullable = true, Length = 500)]
+    public string? Gysdm { get; set; }
+
+    [SugarColumn(ColumnName = "gysmc", IsNullable = true, Length = 500)]
+    public string? Gysmc { get; set; }
+
+    [SugarColumn(ColumnName = "hfrid", IsNullable = true, Length = 500)]
+    public string? Hfrid { get; set; }
+
+    [SugarColumn(ColumnName = "hfrxm", IsNullable = true, Length = 500)]
+    public string? Hfrxm { get; set; }
+
+    [SugarColumn(ColumnName = "hfsj", IsNullable = true, Length = 500)]
+    public string? Hfsj { get; set; }
+
+    [SugarColumn(ColumnName = "qhdj", IsNullable = true, Length = 500)]
+    public string? Qhdj { get; set; }
+
+    [SugarColumn(ColumnName = "ddhh", IsNullable = true)]
+    public int? Ddhh { get; set; }
+
+    [SugarColumn(ColumnName = "dw", IsNullable = true, Length = 50)]
+    public string? Dw { get; set; }
+
+    [SugarColumn(ColumnName = "bzsl", IsNullable = true, Length = 50)]
+    public string? Bzsl { get; set; }
+
+    [SugarColumn(ColumnName = "ly", IsNullable = true, Length = 50)]
+    public string? Ly { get; set; }
+
+    [SugarColumn(ColumnName = "glid1", IsNullable = true, Length = 50)]
+    public string? Glid1 { get; set; }
+}
+

+ 93 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/ProcurementExecution/Entity/ScmShd.cs

@@ -0,0 +1,93 @@
+namespace Admin.NET.Plugin.AiDOP.ProcurementExecution.Entity;
+
+/// <summary>
+/// 供应商发货单主表(scm_shd)
+/// </summary>
+[SugarTable("scm_shd")]
+public class ScmShd
+{
+    [SugarColumn(ColumnName = "id", IsPrimaryKey = true)]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "sh_purchase_id", IsNullable = true)]
+    public long? ShPurchaseId { get; set; }
+
+    [SugarColumn(ColumnName = "sh_purchase_name", IsNullable = true, Length = 255)]
+    public string? ShPurchaseName { get; set; }
+
+    [SugarColumn(ColumnName = "sh_purchase_num", IsNullable = true, Length = 255)]
+    public string? ShPurchaseNum { get; set; }
+
+    [SugarColumn(ColumnName = "sh_purchase_address", IsNullable = true, Length = 255)]
+    public string? ShPurchaseAddress { get; set; }
+
+    [SugarColumn(ColumnName = "sh_purchase_lxr", IsNullable = true, Length = 255)]
+    public string? ShPurchaseLxr { get; set; }
+
+    [SugarColumn(ColumnName = "sh_purchase_phone", IsNullable = true, Length = 255)]
+    public string? ShPurchasePhone { get; set; }
+
+    [SugarColumn(ColumnName = "client", IsNullable = true)]
+    public long? Client { get; set; }
+
+    [SugarColumn(ColumnName = "delivery_Address", IsNullable = true, Length = 255)]
+    public string? DeliveryAddress { get; set; }
+
+    [SugarColumn(ColumnName = "expected_consignee", IsNullable = true, Length = 255)]
+    public string? ExpectedConsignee { get; set; }
+
+    [SugarColumn(ColumnName = "consignee_phone", IsNullable = true, Length = 255)]
+    public string? ConsigneePhone { get; set; }
+
+    [SugarColumn(ColumnName = "estimated_delivery_date", IsNullable = true)]
+    public DateTime? EstimatedDeliveryDate { get; set; }
+
+    [SugarColumn(ColumnName = "po_billno", IsNullable = true, Length = 255)]
+    public string? PoBillNo { get; set; }
+
+    [SugarColumn(ColumnName = "shddh", IsNullable = true, Length = 255)]
+    public string? Shddh { get; set; }
+
+    [SugarColumn(ColumnName = "jhshrq", IsNullable = true, Length = 50)]
+    public string? Jhshrq { get; set; }
+
+    [SugarColumn(ColumnName = "tjrid", IsNullable = true, Length = 50)]
+    public string? Tjrid { get; set; }
+
+    [SugarColumn(ColumnName = "tjrxm", IsNullable = true, Length = 50)]
+    public string? Tjrxm { get; set; }
+
+    [SugarColumn(ColumnName = "tjrq", IsNullable = true, Length = 50)]
+    public string? Tjrq { get; set; }
+
+    [SugarColumn(ColumnName = "scbq", IsNullable = true)]
+    public int? Scbq { get; set; }
+
+    [SugarColumn(ColumnName = "chbg", IsNullable = true)]
+    public string? Chbg { get; set; }
+
+    [SugarColumn(ColumnName = "sfpc", IsNullable = true)]
+    public int? Sfpc { get; set; }
+
+    [SugarColumn(ColumnName = "pcsm", IsNullable = true)]
+    public string? Pcsm { get; set; }
+
+    [SugarColumn(ColumnName = "wlsc", IsNullable = true, Length = 500)]
+    public string? Wlsc { get; set; }
+
+    [SugarColumn(ColumnName = "yjdhrq", IsNullable = true, Length = 500)]
+    public string? Yjdhrq { get; set; }
+
+    [SugarColumn(ColumnName = "state", IsNullable = true)]
+    public int? State { get; set; }
+
+    [SugarColumn(ColumnName = "shzt", IsNullable = true, Length = 50)]
+    public string? Shzt { get; set; }
+
+    [SugarColumn(ColumnName = "wldh", IsNullable = true, Length = 50)]
+    public string? Wldh { get; set; }
+
+    [SugarColumn(ColumnName = "dycs", IsNullable = true)]
+    public int? Dycs { get; set; }
+}
+

+ 96 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/ProcurementExecution/Entity/ScmShdzb.cs

@@ -0,0 +1,96 @@
+namespace Admin.NET.Plugin.AiDOP.ProcurementExecution.Entity;
+
+/// <summary>
+/// 供应商发货单子表(scm_shdzb)
+/// </summary>
+[SugarTable("scm_shdzb")]
+public class ScmShdzb
+{
+    [SugarColumn(ColumnName = "id", IsPrimaryKey = true)]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "glid", IsNullable = true, Length = 255)]
+    public string? Glid { get; set; }
+
+    [SugarColumn(ColumnName = "sh_material_code", IsNullable = true, Length = 255)]
+    public string? ShMaterialCode { get; set; }
+
+    [SugarColumn(ColumnName = "sh_material_name", IsNullable = true, Length = 255)]
+    public string? ShMaterialName { get; set; }
+
+    [SugarColumn(ColumnName = "sh_material_ggxh", IsNullable = true, Length = 255)]
+    public string? ShMaterialGgxh { get; set; }
+
+    [SugarColumn(ColumnName = "sh_delivery_quantity", IsNullable = true)]
+    public decimal? ShDeliveryQuantity { get; set; }
+
+    [SugarColumn(ColumnName = "sh_material_dw", IsNullable = true, Length = 255)]
+    public string? ShMaterialDw { get; set; }
+
+    [SugarColumn(ColumnName = "remarks", IsNullable = true, Length = 255)]
+    public string? Remarks { get; set; }
+
+    [SugarColumn(ColumnName = "bzsl", IsNullable = true)]
+    public decimal? Bzsl { get; set; }
+
+    [SugarColumn(ColumnName = "bqsl", IsNullable = true)]
+    public decimal? Bqsl { get; set; }
+
+    [SugarColumn(ColumnName = "order_type", IsNullable = true, Length = 255)]
+    public string? OrderType { get; set; }
+
+    [SugarColumn(ColumnName = "po_bill", IsNullable = true, Length = 255)]
+    public string? PoBill { get; set; }
+
+    [SugarColumn(ColumnName = "po_billline", IsNullable = true, Length = 50)]
+    public string? PoBillLine { get; set; }
+
+    [SugarColumn(ColumnName = "hh", IsNullable = true)]
+    public int? Hh { get; set; }
+
+    [SugarColumn(ColumnName = "scrq", IsNullable = true, Length = 255)]
+    public string? Scrq { get; set; }
+
+    [SugarColumn(ColumnName = "scph", IsNullable = true, Length = 255)]
+    public string? Scph { get; set; }
+
+    [SugarColumn(ColumnName = "th", IsNullable = true, Length = 255)]
+    public string? Th { get; set; }
+
+    [SugarColumn(ColumnName = "bbh", IsNullable = true, Length = 255)]
+    public string? Bbh { get; set; }
+
+    [SugarColumn(ColumnName = "djsl", IsNullable = true)]
+    public decimal? Djsl { get; set; }
+
+    [SugarColumn(ColumnName = "ccrq", IsNullable = true, Length = 255)]
+    public string? Ccrq { get; set; }
+
+    [SugarColumn(ColumnName = "cgyt", IsNullable = true, Length = 255)]
+    public string? Cgyt { get; set; }
+
+    [SugarColumn(ColumnName = "jybb", IsNullable = true)]
+    public string? Jybb { get; set; }
+
+    [SugarColumn(ColumnName = "jhdbh", IsNullable = true, Length = 50)]
+    public string? Jhdbh { get; set; }
+
+    [SugarColumn(ColumnName = "jhdhh", IsNullable = true, Length = 50)]
+    public string? Jhdhh { get; set; }
+
+    [SugarColumn(ColumnName = "shpc", IsNullable = true, Length = 50)]
+    public string? Shpc { get; set; }
+
+    [SugarColumn(ColumnName = "shzt", IsNullable = true, Length = 255)]
+    public string? Shzt { get; set; }
+
+    [SugarColumn(ColumnName = "rksl", IsNullable = true)]
+    public decimal? Rksl { get; set; }
+
+    [SugarColumn(ColumnName = "thsl", IsNullable = true)]
+    public decimal? Thsl { get; set; }
+
+    [SugarColumn(ColumnName = "po_billno", IsNullable = true, Length = 255)]
+    public string? PoBillNo { get; set; }
+}
+

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

@@ -0,0 +1,338 @@
+using Admin.NET.Plugin.AiDOP.ProcurementExecution.Dto;
+
+namespace Admin.NET.Plugin.AiDOP.ProcurementExecution;
+
+/// <summary>
+/// S4 供应商交货管理
+/// </summary>
+[ApiDescriptionSettings(Order = 320, Description = "S4供应商交货管理")]
+[Route("api/ProcurementExecution")]
+[AllowAnonymous]
+[NonUnify]
+public class SupplierDeliveryManagementService : IDynamicApiController, ITransient
+{
+    private readonly ISqlSugarClient _db;
+    private readonly UserManager _userManager;
+
+    public SupplierDeliveryManagementService(ISqlSugarClient db, UserManager userManager)
+    {
+        _db = db;
+        _userManager = userManager;
+    }
+
+    [DisplayName("供应商交货管理列表")]
+    [HttpGet("supplier-delivery/list")]
+    public async Task<object> GetList([FromQuery] SupplierDeliveryListInput input)
+    {
+        var pars = new List<SugarParameter>();
+        var conditions = new List<string>();
+        if (!string.IsNullOrWhiteSpace(input.Cgdd))
+        {
+            conditions.Add("v.cgdd LIKE @cgdd");
+            pars.Add(new SugarParameter("@cgdd", $"%{input.Cgdd.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.DsNum))
+        {
+            conditions.Add("v.dsnum LIKE @dsnum");
+            pars.Add(new SugarParameter("@dsnum", $"%{input.DsNum.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.Wlbm))
+        {
+            conditions.Add("v.wlbm LIKE @wlbm");
+            pars.Add(new SugarParameter("@wlbm", $"%{input.Wlbm.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.Gys))
+        {
+            conditions.Add("(v.gysdm LIKE @gys OR v.gysmc LIKE @gys)");
+            pars.Add(new SugarParameter("@gys", $"%{input.Gys.Trim()}%"));
+        }
+
+        var where = conditions.Count > 0 ? $" WHERE {string.Join(" AND ", conditions)} " : string.Empty;
+        var orderBy = BuildOrderBy(input.SortField, input.SortOrder);
+        var offset = (input.Page - 1) * input.PageSize;
+        var wrapped = $"{BuildBaseSql()} {where}";
+
+        var total = await _db.Ado.GetIntAsync($"SELECT COUNT(*) FROM ({wrapped}) t", pars);
+        var list = await _db.Ado.SqlQueryAsync<SupplierDeliveryListRow>(
+            $"SELECT * FROM ({wrapped}) t {orderBy} LIMIT {input.PageSize} OFFSET {offset}", pars);
+
+        return new { total, page = input.Page, pageSize = input.PageSize, list };
+    }
+
+    [DisplayName("供应商交货管理发布")]
+    [HttpPost("supplier-delivery/publish")]
+    public async Task<object> Publish([FromBody] SupplierDeliveryBatchInput input)
+    {
+        var userId = _userManager.UserId.ToString();
+        var userName = _userManager.Account ?? "system";
+        var sql = """
+            INSERT INTO `scm_jhjh_jq`
+            (
+              `id`,`glid`, `wlbm`, `wlms`, `wlgg`, `cgdd`,
+              `jhdsl`, `wjhsl`, `jhd`, `yjjhrq`, `jqhf`,
+              `type`, `flag`, `scrq`, `scrid`, `scrxm`,
+              `gysdm`, `gysmc`, `hfrid`, `hfrxm`, `hfsj`,
+              `qhdj`, `ddhh`, `dw`, `bzsl`, `ly`,`glid1`
+            )
+            SELECT
+              UUID(),
+              CAST(d.RecID AS CHAR(50)) AS glid,
+              d.ItemNum AS wlbm,
+              i.Descr AS wlms,
+              i.Descr1 AS wlgg,
+              p.PurOrd AS cgdd,
+              CAST(d.QtyOrded AS CHAR(20)) AS jhdsl,
+              (d.QtyOrded - d.RctQty - d.ReceiptQty + d.QtyReturned) AS wjhsl,
+              '' AS jhd,
+              DATE_FORMAT(DATE(d.DueDate), '%Y-%m-%d') AS yjjhrq,
+              '' AS jqhf,
+              p.Potype AS `type`,
+              2 AS flag,
+              '' AS scrq,
+              '' AS scrid,
+              '' AS scrxm,
+              p.Supp AS gysdm,
+              s.SortName AS gysmc,
+              @hfrid AS hfrid,
+              @hfrxm AS hfrxm,
+              DATE_FORMAT(NOW(), '%Y-%m-%d') AS hfsj,
+              '' AS qhdj,
+              d.Line AS ddhh,
+              d.UM AS dw,
+              d.StdPackQty AS bzsl,
+              '2' AS ly,
+              CAST(IFNULL(ds.id, d.RecID) AS CHAR(50)) AS glid1
+            FROM PurOrdMaster p
+            LEFT JOIN PurOrdDetail d
+              ON p.Domain = d.Domain
+             AND p.Potype = d.Potype
+             AND p.PurOrd = d.PurOrd
+            LEFT JOIN SuppMaster s
+              ON p.Domain = s.Domain
+             AND p.Supp = s.Supp
+            LEFT JOIN ItemMaster i
+              ON d.Domain = i.Domain
+             AND d.ItemNum = i.ItemNum
+            LEFT JOIN srm_polist_ds ds
+              ON p.PurOrd = ds.ponumber
+             AND p.Line = ds.poline
+            WHERE p.IsActive = 1
+              AND IFNULL(p.Status, '') <> 'C'
+              AND IFNULL(d.Status, '') <> 'C'
+              AND FIND_IN_SET(CAST(IFNULL(ds.id, d.RecID) AS CHAR(50)), REPLACE(IFNULL(@ids, ''), ' ', '')) > 0
+              AND NOT EXISTS (
+                    SELECT 1 FROM scm_jhjh_jq x
+                    WHERE x.flag = 2
+                      AND x.glid1 = CAST(IFNULL(ds.id, d.RecID) AS CHAR(50))
+              );
+            """;
+        await _db.Ado.ExecuteCommandAsync(sql, new List<SugarParameter>
+        {
+            new("@ids", input.Ids),
+            new("@hfrid", userId),
+            new("@hfrxm", userName)
+        });
+        return new { message = "发布成功" };
+    }
+
+    [DisplayName("交货单关闭")]
+    [HttpPost("supplier-delivery/close")]
+    public async Task<object> CloseDelivery([FromBody] DeliveryCloseInput input)
+    {
+        await _db.Ado.ExecuteCommandAsync(
+            "UPDATE srm_polist_ds SET status='C' WHERE DSNum=@jhdbh",
+            new List<SugarParameter> { new("@jhdbh", input.DsNum) });
+        return new { message = "交货单关闭成功" };
+    }
+
+    private static string BuildOrderBy(string? sortField, string? sortOrder)
+    {
+        var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
+        {
+            ["sffb"] = "t.sffb",
+            ["wlbm"] = "t.wlbm",
+            ["wlms"] = "t.wlms",
+            ["buyer"] = "t.buyer",
+            ["gysdm"] = "t.gysdm",
+            ["gysmc"] = "t.gysmc",
+            ["cgdd"] = "t.cgdd",
+            ["ddhh"] = "t.ddhh",
+            ["jhdsl"] = "t.jhdsl",
+            ["dfhsl"] = "t.dfhsl",
+            ["dsnum"] = "t.dsnum",
+            ["requestdate"] = "t.requestdate",
+            ["schedqty"] = "t.schedqty",
+            ["jqhfnew"] = "t.jqhfnew",
+            ["ztsl"] = "t.ztsl",
+            ["zsl1"] = "t.zsl1",
+            ["rksl"] = "t.rksl",
+            ["bhgsl"] = "t.bhgsl",
+            ["thsl"] = "t.thsl",
+            ["bz"] = "t.bz"
+        };
+        var field = map.TryGetValue(sortField ?? string.Empty, out var sqlField) ? sqlField : "t.requestdate";
+        var order = string.Equals(sortOrder, "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC";
+        return $" ORDER BY {field} {order} ";
+    }
+
+    private static string BuildBaseSql() => """
+        SELECT
+            v.*,
+            CASE
+                WHEN v.jhdsl - IFNULL(v.zfhl, 0) + IFNULL(v.thsl, 0) > v.wjhsl THEN v.wjhsl
+                ELSE v.jhdsl - IFNULL(v.zfhl, 0) + IFNULL(v.thsl, 0)
+            END AS dfhsl,
+            CASE
+                WHEN IFNULL(v.zsl18, 0) - IFNULL(v.thsl, 0) <= IFNULL(v.zshl, 0) THEN 0
+                ELSE IFNULL(v.zsl18, 0) - IFNULL(v.thsl, 0) - IFNULL(v.zshl, 0)
+            END AS ztsl,
+            IFNULL(v.zsl, 0) AS zsl1,
+            v.thsl AS bhgsl,
+            CONCAT(
+                CASE
+                    WHEN v.sfyqnew = 'wait' OR v.sfyqnew = 'NO' THEN 'jqhfnew:yellow'
+                    WHEN v.sfyqnew = 'refuse' THEN 'jqhfnew:red'
+                    WHEN v.sfyqnew = 'OK' THEN 'jqhfnew:#99FF00'
+                    ELSE ''
+                END,
+                v.sffbgrdnew
+            ) AS background,
+            CASE
+                WHEN v.jhdsl - IFNULL(v.zfhl, 0) + IFNULL(v.thsl, 0) <= 0 THEN '完成'
+                ELSE '待交'
+            END AS yjzt
+        FROM
+        (
+            SELECT
+                p.PurOrd,
+                p.Line,
+                p.ItemNum,
+                s.fhsl - IFNULL(dor.thsl, 0) AS zfhl,
+                p.ReceiptQty + p.RctQty AS zshl,
+                i.zjsl AS zsl,
+                CASE WHEN p.PurOrd LIKE 'DO%' THEN p.RctQty ELSE p.RctQty - IFNULL(i.zjsl, 0) END AS rksl,
+                CASE
+                    WHEN p.PurOrd LIKE 'DO%' THEN IF(p.QtyReturned > 0, p.QtyReturned, dor.thsl)
+                    ELSE IF(p.QtyReturned > 0, p.QtyReturned, p.QtyReturned + dor.thsl)
+                END AS thsl,
+                s.fhsl AS zsl18,
+                sh.jhdyj,
+                jy.jhdzj,
+                IFNULL(ds.SchedQty, 0) - IFNULL(sh.jhdyj, 0) AS jhddj,
+                p.QtyOrded - p.RctQty - p.ReceiptQty + p.QtyReturned AS wjhsl,
+                p.QtyOrded AS jhdsl,
+                ds.SchedQty,
+                ds.DSNum,
+                CASE WHEN b2.id IS NULL THEN 'X' ELSE b2.hfsj END AS sffb,
+                p.ItemNum AS wlbm,
+                im.Descr AS wlms,
+                ds.supplier AS gysmc,
+                p.PurOrd AS cgdd,
+                p.Line AS ddhh,
+                pm.Buyer AS buyer,
+                IFNULL(ds.requestDate, p.DueDate) AS requestdate,
+                IFNULL(b.jqhf, '') AS jqhfnew,
+                IFNULL(b.fpjh, '') AS fpjhnew,
+                ds.SentQty,
+                IFNULL(im.Drawing, p.Drawing) AS th,
+                IFNULL(im.Rev, p.Rev) AS bbh,
+                IFNULL(ds.id, p.RecID) AS id,
+                '' AS jhd,
+                ds.suppliercode AS gysdm,
+                CASE WHEN IFNULL(ds.id, '') = '' THEN p.Remarks ELSE ds.Remarks END AS bz,
+                '' AS shd,
+                ds.id AS jhdid,
+                p.DueDate AS jhrq,
+                p.RecID AS polid,
+                im.Descr1 AS wlgg,
+                CASE WHEN b2.id IS NULL THEN '' ELSE ';sffb:#99FF00' END AS sffbgrdnew,
+                CASE
+                    WHEN IFNULL(b.jqhf, '') = '' THEN 'wait'
+                    WHEN TIMESTAMPDIFF(DAY, IFNULL(ds.requestDate, p.DueDate), b.jqhf) <= 2 THEN 'OK'
+                    WHEN TIMESTAMPDIFF(DAY, IFNULL(ds.requestDate, p.DueDate), b.jqhf) > 2 THEN 'NO'
+                    WHEN b.jqhf = '' AND IFNULL(b.qhdj, '') <> '' THEN 'refuse'
+                    ELSE 'wait'
+                END AS sfyqnew
+            FROM PurOrdDetail p
+            LEFT JOIN PurOrdMaster pm ON p.PurOrd = pm.PurOrd
+            LEFT JOIN (
+                SELECT SUM(sh_delivery_quantity) AS fhsl, po_bill, po_billline
+                FROM scm_shdzb
+                GROUP BY po_bill, po_billline
+            ) s ON s.po_bill = p.PurOrd AND s.po_billline = p.Line
+            LEFT JOIN (
+                SELECT SUM(Qty) AS zjsl, PurOrd, PurLine
+                FROM MissedPrint
+                WHERE Status = 'I'
+                GROUP BY PurOrd, PurLine
+            ) i ON i.PurOrd = p.PurOrd AND i.PurLine = p.Line
+            LEFT JOIN (
+                SELECT SUM(QtyReturn) AS thsl, OrdNbr, OrdLine
+                FROM PurOrdRctDetail
+                WHERE rcttype IN ('pt', 'temp')
+                GROUP BY OrdNbr, OrdLine
+            ) dor ON dor.OrdNbr = p.PurOrd AND dor.OrdLine = p.Line
+            LEFT JOIN srm_polist_ds ds ON p.PurOrd = ds.ponumber AND p.Line = ds.poline
+            LEFT JOIN (
+                SELECT SUM(sh_delivery_quantity) AS jhdyj, jhdbh
+                FROM scm_shdzb
+                GROUP BY jhdbh
+            ) sh ON ds.dsnum = sh.jhdbh
+            LEFT JOIN (
+                SELECT SUM(IFNULL(Qty, 0)) AS jhdzj, PurOrdDetBatchNbr
+                FROM MissedPrint
+                WHERE Status = 'I'
+                GROUP BY PurOrdDetBatchNbr
+            ) jy ON ds.dsnum = jy.PurOrdDetBatchNbr
+            LEFT JOIN (
+                SELECT glid1, MIN(id) AS id, MIN(DATE_FORMAT(hfsj, '%Y.%m.%d')) AS hfsj
+                FROM scm_jhjh_jq
+                WHERE flag = 2
+                GROUP BY glid1
+            ) b2 ON ds.id = b2.glid1
+            LEFT JOIN ItemMaster im ON p.ItemNum = im.ItemNum
+            LEFT JOIN (
+                SELECT
+                    glid,
+                    GROUP_CONCAT(CONCAT(CAST(jhdsl AS CHAR(10)), '(', DATE_FORMAT(jqhf, '%Y-%m-%d'), ')') SEPARATOR '<br>') AS fpjh,
+                    GROUP_CONCAT(qhdj SEPARATOR '') AS qhdj,
+                    MAX(jqhf) AS jqhf
+                FROM scm_jhjh_jq
+                WHERE flag = 0
+                GROUP BY glid
+            ) b ON ds.id = b.glid
+            WHERE p.Status <> 'C'
+              AND ds.schedqty >= 0
+              AND ds.isactive = 1
+              AND ds.status = 'P'
+        ) v
+        """;
+
+    private sealed class SupplierDeliveryListRow
+    {
+        public string? Sffb { get; set; }
+        public string? Wlbm { get; set; }
+        public string? Wlms { get; set; }
+        public string? Buyer { get; set; }
+        public string? Gysdm { get; set; }
+        public string? Gysmc { get; set; }
+        public string? Cgdd { get; set; }
+        public int? Ddhh { get; set; }
+        public decimal? Jhdsl { get; set; }
+        public decimal? Dfhsl { get; set; }
+        public string? Dsnum { get; set; }
+        public DateTime? Requestdate { get; set; }
+        public decimal? Schedqty { get; set; }
+        public string? Jqhfnew { get; set; }
+        public decimal? Ztsl { get; set; }
+        public decimal? Zsl1 { get; set; }
+        public decimal? Rksl { get; set; }
+        public decimal? Bhgsl { get; set; }
+        public decimal? Thsl { get; set; }
+        public string? Bz { get; set; }
+        public string? Id { get; set; }
+        public decimal? Wjhsl { get; set; }
+        public decimal? Zfhl { get; set; }
+    }
+}
+

+ 531 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/ProcurementExecution/SupplierShipmentService.cs

@@ -0,0 +1,531 @@
+using Admin.NET.Plugin.AiDOP.ProcurementExecution.Dto;
+using Admin.NET.Plugin.AiDOP.ProcurementExecution.Entity;
+using Yitter.IdGenerator;
+
+namespace Admin.NET.Plugin.AiDOP.ProcurementExecution;
+
+/// <summary>
+/// S4 供应商发货单(列表 + 表单)
+/// </summary>
+[ApiDescriptionSettings(Order = 321, Description = "S4供应商发货单")]
+[Route("api/ProcurementExecution")]
+[AllowAnonymous]
+[NonUnify]
+public class SupplierShipmentService : IDynamicApiController, ITransient
+{
+    private readonly ISqlSugarClient _db;
+    private readonly SqlSugarRepository<ScmShd> _masterRep;
+    private readonly SqlSugarRepository<ScmShdzb> _detailRep;
+    private readonly UserManager _userManager;
+
+    public SupplierShipmentService(
+        ISqlSugarClient db,
+        SqlSugarRepository<ScmShd> masterRep,
+        SqlSugarRepository<ScmShdzb> detailRep,
+        UserManager userManager)
+    {
+        _db = db;
+        _masterRep = masterRep;
+        _detailRep = detailRep;
+        _userManager = userManager;
+    }
+
+    [DisplayName("供应商发货单列表")]
+    [HttpGet("supplier-shipment/list")]
+    public async Task<object> GetList([FromQuery] SupplierShipmentListInput input)
+    {
+        var pars = new List<SugarParameter>();
+        var conditions = new List<string> { "IFNULL(m.state, 1) <> 0" };
+        if (!string.IsNullOrWhiteSpace(input.JhshrqFrom))
+        {
+            conditions.Add("m.jhshrq >= @jhshrqFrom");
+            pars.Add(new SugarParameter("@jhshrqFrom", input.JhshrqFrom.Trim()));
+        }
+        if (!string.IsNullOrWhiteSpace(input.Gysmc))
+        {
+            conditions.Add("m.sh_purchase_name LIKE @gysmc");
+            pars.Add(new SugarParameter("@gysmc", $"%{input.Gysmc.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.Shddh))
+        {
+            conditions.Add("m.shddh LIKE @shddh");
+            pars.Add(new SugarParameter("@shddh", $"%{input.Shddh.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.Wldh))
+        {
+            conditions.Add("m.wldh LIKE @wldh");
+            pars.Add(new SugarParameter("@wldh", $"%{input.Wldh.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.PoBill))
+        {
+            conditions.Add("m.po_bill LIKE @poBill");
+            pars.Add(new SugarParameter("@poBill", $"%{input.PoBill.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.ShMaterialCode))
+        {
+            conditions.Add("m.sh_material_code LIKE @shMaterialCode");
+            pars.Add(new SugarParameter("@shMaterialCode", $"%{input.ShMaterialCode.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.Shzt))
+        {
+            conditions.Add("m.shzt = @shzt");
+            pars.Add(new SugarParameter("@shzt", input.Shzt.Trim()));
+        }
+        if (!string.IsNullOrWhiteSpace(input.Shpc))
+        {
+            conditions.Add("m.shpc LIKE @shpc");
+            pars.Add(new SugarParameter("@shpc", $"%{input.Shpc.Trim()}%"));
+        }
+
+        var where = $" WHERE {string.Join(" AND ", conditions)} ";
+        var orderBy = BuildListOrderBy(input.SortField, input.SortOrder);
+        var offset = (input.Page - 1) * input.PageSize;
+        var wrapped = $"{BuildListSql()} {where}";
+
+        var total = await _db.Ado.GetIntAsync($"SELECT COUNT(*) FROM ({wrapped}) t", pars);
+        var list = await _db.Ado.SqlQueryAsync<SupplierShipmentListRow>(
+            $"SELECT * FROM ({wrapped}) t {orderBy} LIMIT {input.PageSize} OFFSET {offset}", pars);
+
+        return new { total, page = input.Page, pageSize = input.PageSize, list };
+    }
+
+    [DisplayName("获取发货单详情")]
+    [HttpGet("supplier-shipment/{id:long}")]
+    public async Task<object> GetDetail(long id)
+    {
+        var master = await _masterRep.GetFirstAsync(x => x.Id == id) ?? throw Oops.Oh("发货单不存在");
+        var details = await _detailRep.AsQueryable().Where(x => x.Glid == id.ToString()).OrderBy(x => x.Hh).ToListAsync();
+        return new
+        {
+            id = master.Id,
+            shddh = master.Shddh,
+            jhshrq = master.Jhshrq,
+            wlsc = master.Wlsc,
+            yjdhrq = master.Yjdhrq,
+            shPurchaseName = master.ShPurchaseName,
+            shPurchaseNum = master.ShPurchaseNum,
+            wldh = master.Wldh,
+            sfpc = master.Sfpc ?? 0,
+            chbg = master.Chbg,
+            pcsm = master.Pcsm,
+            state = master.State ?? 1,
+            details = details.Select(d => new
+            {
+                id = d.Id,
+                hh = d.Hh,
+                poBill = d.PoBill,
+                poBillLine = d.PoBillLine,
+                orderType = d.OrderType,
+                shMaterialCode = d.ShMaterialCode,
+                shMaterialName = d.ShMaterialName,
+                th = d.Th,
+                shDeliveryQuantity = d.ShDeliveryQuantity,
+                bzsl = d.Bzsl,
+                bqsl = d.Bqsl,
+                shMaterialDw = d.ShMaterialDw,
+                scrq = d.Scrq,
+                scph = d.Scph,
+                remarks = d.Remarks,
+                djsl = d.Djsl,
+                jybb = d.Jybb,
+                jhdbh = d.Jhdbh
+            })
+        };
+    }
+
+    [DisplayName("发货单新增草稿")]
+    [HttpGet("supplier-shipment/create-draft")]
+    public async Task<object> GetCreateDraft([FromQuery] string ids)
+    {
+        var pars = new List<SugarParameter> { new("@ids", ids) };
+        var rows = await _db.Ado.SqlQueryAsync<SupplierShipmentDraftRow>(
+            """
+            SELECT
+                CAST(IFNULL(ds.id, p.RecID) AS CHAR(50)) AS sourceId,
+                ds.suppliercode AS gysdm,
+                ds.supplier AS gysmc,
+                p.PurOrd AS poBill,
+                p.Line AS poBillLine,
+                p.Potype AS orderType,
+                p.ItemNum AS shMaterialCode,
+                im.Descr AS shMaterialName,
+                IFNULL(im.Drawing, p.Drawing) AS th,
+                IFNULL(ds.SchedQty, p.QtyOrded - p.RctQty - p.ReceiptQty + p.QtyReturned) AS shDeliveryQuantity,
+                p.StdPackQty AS bzsl,
+                p.UM AS shMaterialDw,
+                (p.QtyOrded - p.RctQty - p.ReceiptQty + p.QtyReturned) AS djsl,
+                ds.DSNum AS jhdbh,
+                im.Descr1 AS shMaterialGgxh
+            FROM PurOrdDetail p
+            LEFT JOIN srm_polist_ds ds ON p.PurOrd = ds.ponumber AND p.Line = ds.poline
+            LEFT JOIN ItemMaster im ON p.ItemNum = im.ItemNum
+            WHERE FIND_IN_SET(CAST(IFNULL(ds.id, p.RecID) AS CHAR(50)), REPLACE(IFNULL(@ids, ''), ' ', '')) > 0
+            ORDER BY p.PurOrd, p.Line
+            """, pars);
+
+        if (rows.Count == 0)
+            throw Oops.Oh("未找到可生成的交货明细");
+
+        return new
+        {
+            shddh = string.Empty,
+            jhshrq = DateTime.Now.ToString("yyyy-MM-dd"),
+            wlsc = string.Empty,
+            yjdhrq = string.Empty,
+            shPurchaseName = rows[0].Gysmc,
+            shPurchaseNum = rows[0].Gysdm,
+            wldh = string.Empty,
+            sfpc = 0,
+            chbg = string.Empty,
+            pcsm = string.Empty,
+            details = rows.Select((r, i) => new
+            {
+                id = (long?)null,
+                hh = i + 1,
+                poBill = r.PoBill,
+                poBillLine = r.PoBillLine?.ToString(),
+                orderType = r.OrderType,
+                shMaterialCode = r.ShMaterialCode,
+                shMaterialName = r.ShMaterialName,
+                th = r.Th,
+                shDeliveryQuantity = r.ShDeliveryQuantity,
+                bzsl = r.Bzsl,
+                bqsl = 0,
+                shMaterialDw = r.ShMaterialDw,
+                scrq = string.Empty,
+                scph = string.Empty,
+                remarks = string.Empty,
+                djsl = r.Djsl,
+                jybb = string.Empty,
+                jhdbh = r.Jhdbh
+            })
+        };
+    }
+
+    [DisplayName("保存发货单")]
+    [ApiDescriptionSettings(Name = "SaveSupplierShipment"), HttpPost("supplier-shipment/save")]
+    public async Task<object> Save([FromBody] SupplierShipmentSaveInput input)
+    {
+        var now = DateTime.Now;
+        var userId = _userManager.UserId.ToString();
+        var userName = _userManager.Account ?? "system";
+        var shipDate = string.IsNullOrWhiteSpace(input.Jhshrq) ? now.ToString("yyyy-MM-dd") : input.Jhshrq!.Trim();
+
+        if (input.Id is null or 0)
+        {
+            var newId = YitIdHelper.NextId();
+            var entity = new ScmShd
+            {
+                Id = newId,
+                Shddh = string.IsNullOrWhiteSpace(input.Shddh) ? $"SH{DateTime.Now:yyyyMMddHHmmss}" : input.Shddh!.Trim(),
+                Jhshrq = shipDate,
+                Wlsc = input.Wlsc,
+                Yjdhrq = input.Yjdhrq,
+                ShPurchaseName = input.ShPurchaseName,
+                ShPurchaseNum = input.ShPurchaseNum,
+                Wldh = input.Wldh,
+                Sfpc = input.Sfpc ?? 0,
+                Chbg = input.Chbg,
+                Pcsm = input.Pcsm,
+                State = 1,
+                Shzt = "待收",
+                Tjrid = userId,
+                Tjrxm = userName,
+                Tjrq = now.ToString("yyyy-MM-dd")
+            };
+            await _masterRep.InsertAsync(entity);
+            await SaveDetailsAsync(newId, input.Details);
+            return new { id = newId, message = "新增成功" };
+        }
+
+        var master = await _masterRep.GetFirstAsync(x => x.Id == input.Id.Value) ?? throw Oops.Oh("发货单不存在");
+        master.Shddh = input.Shddh;
+        master.Jhshrq = shipDate;
+        master.Wlsc = input.Wlsc;
+        master.Yjdhrq = input.Yjdhrq;
+        master.ShPurchaseName = input.ShPurchaseName;
+        master.ShPurchaseNum = input.ShPurchaseNum;
+        master.Wldh = input.Wldh;
+        master.Sfpc = input.Sfpc ?? 0;
+        master.Chbg = input.Chbg;
+        master.Pcsm = input.Pcsm;
+        await _masterRep.UpdateAsync(master);
+        await SaveDetailsAsync(input.Id.Value, input.Details);
+        return new { id = input.Id, message = "编辑成功" };
+    }
+
+    [DisplayName("删除发货单")]
+    [ApiDescriptionSettings(Name = "DeleteSupplierShipment"), HttpPost("supplier-shipment/delete")]
+    public async Task<object> Delete([FromBody] SupplierShipmentDeleteInput input)
+    {
+        var master = await _masterRep.GetFirstAsync(x => x.Id == input.Id) ?? throw Oops.Oh("发货单不存在");
+        master.State = 0;
+        await _masterRep.UpdateAsync(master);
+        return new { message = "删除成功" };
+    }
+
+    [DisplayName("生成标签(预留)")]
+    [HttpPost("supplier-shipment/generate-label")]
+    public Task<object> GenerateLabel([FromBody] SupplierShipmentOperationInput input)
+        => Task.FromResult<object>(new { message = $"发货单{input.Id}:功能预留,暂未启用" });
+
+    [DisplayName("打印送货单(预留)")]
+    [HttpPost("supplier-shipment/print-shipping-note")]
+    public Task<object> PrintShippingNote([FromBody] SupplierShipmentOperationInput input)
+        => Task.FromResult<object>(new { message = $"发货单{input.Id}:功能预留,暂未启用" });
+
+    [DisplayName("打印标签(预留)")]
+    [HttpPost("supplier-shipment/print-label")]
+    public Task<object> PrintLabel([FromBody] SupplierShipmentOperationInput input)
+        => Task.FromResult<object>(new { message = $"发货单{input.Id}:功能预留,暂未启用" });
+
+    private async Task SaveDetailsAsync(long masterId, List<SupplierShipmentDetailInput> inputDetails)
+    {
+        var dbDetails = await _detailRep.AsQueryable().Where(x => x.Glid == masterId.ToString()).ToListAsync();
+        var dbById = dbDetails.ToDictionary(x => x.Id);
+        var inputIds = new HashSet<long>(inputDetails.Where(d => d.Id is > 0).Select(d => d.Id!.Value));
+
+        for (var i = 0; i < inputDetails.Count; i++)
+        {
+            var d = inputDetails[i];
+            if (d.Id is > 0 && dbById.TryGetValue(d.Id.Value, out var existing))
+            {
+                existing.Hh = d.Hh ?? (i + 1);
+                existing.PoBill = d.PoBill;
+                existing.PoBillLine = d.PoBillLine;
+                existing.OrderType = d.OrderType;
+                existing.ShMaterialCode = d.ShMaterialCode;
+                existing.ShMaterialName = d.ShMaterialName;
+                existing.Th = d.Th;
+                existing.ShDeliveryQuantity = d.ShDeliveryQuantity;
+                existing.Bzsl = d.Bzsl;
+                existing.Bqsl = d.Bqsl;
+                existing.ShMaterialDw = d.ShMaterialDw;
+                existing.Scrq = d.Scrq;
+                existing.Scph = d.Scph;
+                existing.Remarks = d.Remarks;
+                existing.Djsl = d.Djsl;
+                existing.Jybb = d.Jybb;
+                existing.Jhdbh = d.Jhdbh;
+                await _detailRep.UpdateAsync(existing);
+            }
+            else
+            {
+                var detail = new ScmShdzb
+                {
+                    Id = YitIdHelper.NextId(),
+                    Glid = masterId.ToString(),
+                    Hh = d.Hh ?? (i + 1),
+                    PoBill = d.PoBill,
+                    PoBillLine = d.PoBillLine,
+                    OrderType = d.OrderType,
+                    ShMaterialCode = d.ShMaterialCode,
+                    ShMaterialName = d.ShMaterialName,
+                    Th = d.Th,
+                    ShDeliveryQuantity = d.ShDeliveryQuantity,
+                    Bzsl = d.Bzsl,
+                    Bqsl = d.Bqsl,
+                    ShMaterialDw = d.ShMaterialDw,
+                    Scrq = d.Scrq,
+                    Scph = d.Scph,
+                    Remarks = d.Remarks,
+                    Djsl = d.Djsl,
+                    Jybb = d.Jybb,
+                    Jhdbh = d.Jhdbh
+                };
+                await _detailRep.InsertAsync(detail);
+            }
+        }
+
+        foreach (var old in dbDetails.Where(x => !inputIds.Contains(x.Id)))
+        {
+            await _detailRep.DeleteAsync(x => x.Id == old.Id);
+        }
+    }
+
+    private static string BuildListOrderBy(string? sortField, string? sortOrder)
+    {
+        var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
+        {
+            ["shddh"] = "t.shddh",
+            ["poBill"] = "t.po_bill",
+            ["jhshrq"] = "t.jhshrq",
+            ["shMaterialCode"] = "t.sh_material_code",
+            ["shMaterialName"] = "t.sh_material_name",
+            ["shDeliveryQuantity"] = "t.sh_delivery_quantity",
+            ["sfpc"] = "t.sfpc",
+            ["pcrksl"] = "t.pcrksl",
+            ["shPurchaseName"] = "t.sh_purchase_name",
+            ["shpc"] = "t.shpc",
+            ["scph"] = "t.scph",
+            ["wldh"] = "t.wldh",
+            ["dycs"] = "t.dycs",
+            ["shzt"] = "t.shzt"
+        };
+        var field = map.TryGetValue(sortField ?? string.Empty, out var sqlField) ? sqlField : "t.mid";
+        var order = string.Equals(sortOrder, "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC";
+        return $" ORDER BY {field} {order} ";
+    }
+
+    private static string BuildListSql() => """
+        SELECT
+            m.mid,
+            m.id,
+            m.sh_purchase_id,
+            m.sh_purchase_name,
+            m.sh_purchase_num,
+            m.sh_purchase_address,
+            m.sh_purchase_lxr,
+            m.sh_purchase_phone,
+            m.client,
+            m.delivery_Address,
+            m.expected_consignee,
+            m.consignee_phone,
+            m.estimated_delivery_date,
+            m.po_billno,
+            m.shddh,
+            m.jhshrq,
+            m.tjrid,
+            m.tjrxm,
+            m.tjrq,
+            m.scbq,
+            m.chbg,
+            m.sfpc,
+            m.pcsm,
+            m.wlsc,
+            m.yjdhrq,
+            m.state,
+            m.shzt,
+            m.wldh,
+            m.dycs,
+            m.gys,
+            m.sh_material_code,
+            m.sh_material_name,
+            m.sh_material_ggxh,
+            m.hh,
+            m.po_bill,
+            m.shpc,
+            m.scph,
+            m.sh_delivery_quantity,
+            m.th,
+            n.rksl,
+            n.bhgsl,
+            n.thsl,
+            n.zshl,
+            n.Delivery,
+            n.pc,
+            l.pcrksl,
+            po.potype,
+            po.Usage
+        FROM
+        (
+            SELECT
+                a.id mid,
+                b.id,
+                a.sh_purchase_id,
+                a.sh_purchase_name,
+                a.sh_purchase_num,
+                a.sh_purchase_address,
+                a.sh_purchase_lxr,
+                a.sh_purchase_phone,
+                a.client,
+                a.delivery_Address,
+                a.expected_consignee,
+                a.consignee_phone,
+                a.estimated_delivery_date,
+                a.po_billno,
+                a.shddh,
+                a.jhshrq,
+                a.tjrid,
+                a.tjrxm,
+                a.tjrq,
+                a.scbq,
+                a.chbg,
+                a.sfpc,
+                a.pcsm,
+                a.wlsc,
+                a.yjdhrq,
+                a.state,
+                IFNULL(a.shzt, '待收') shzt,
+                a.wldh,
+                a.dycs,
+                CONCAT(IFNULL(a.sh_purchase_num, ''), IFNULL(a.sh_purchase_name, '')) gys,
+                b.sh_material_code,
+                b.sh_material_name,
+                b.sh_material_ggxh,
+                b.hh,
+                b.po_bill,
+                c.shpc,
+                b.scph,
+                b.sh_delivery_quantity,
+                b.th
+            FROM scm_shd a
+            LEFT JOIN scm_shdzb b ON a.id = b.glid
+            LEFT JOIN (SELECT DISTINCT glid, shpc FROM scm_shbq) c ON b.id = c.glid
+        ) m
+        LEFT JOIN
+        (
+            SELECT
+                Delivery,
+                LotSerial pc,
+                SUM(ReceiptQty) zshl,
+                AVG(yssl) rksl,
+                SUM(QtyReturn) bhgsl,
+                SUM(QtyReturned) thsl
+            FROM vscm_cgshrk
+            GROUP BY Delivery, LotSerial
+        ) n ON m.shddh = n.Delivery AND m.shpc = n.pc
+        LEFT JOIN
+        (
+            SELECT LotSerial, SUM(QtyChange) pcrksl
+            FROM InvTransHist
+            WHERE QtyChange > 0 AND Reason LIKE '%收货'
+            GROUP BY LotSerial
+        ) l ON m.shpc = l.LotSerial
+        LEFT JOIN PurOrdMaster po ON m.po_bill = po.purord
+        """;
+
+    private sealed class SupplierShipmentListRow
+    {
+        public long Mid { get; set; }
+        public long Id { get; set; }
+        public string? ShPurchaseName { get; set; }
+        public string? ShPurchaseNum { get; set; }
+        public string? Shddh { get; set; }
+        public string? Jhshrq { get; set; }
+        public int? Sfpc { get; set; }
+        public string? Wldh { get; set; }
+        public int? Dycs { get; set; }
+        public string? Shzt { get; set; }
+        public string? ShMaterialCode { get; set; }
+        public string? ShMaterialName { get; set; }
+        public decimal? ShDeliveryQuantity { get; set; }
+        public decimal? Pcrksl { get; set; }
+        public string? PoBill { get; set; }
+        public string? Usage { get; set; }
+        public string? Shpc { get; set; }
+        public string? Scph { get; set; }
+        public string? Th { get; set; }
+        public int? State { get; set; }
+    }
+
+    private sealed class SupplierShipmentDraftRow
+    {
+        public string? SourceId { get; set; }
+        public string? Gysdm { get; set; }
+        public string? Gysmc { get; set; }
+        public string? PoBill { get; set; }
+        public int? PoBillLine { get; set; }
+        public string? OrderType { get; set; }
+        public string? ShMaterialCode { get; set; }
+        public string? ShMaterialName { get; set; }
+        public string? Th { get; set; }
+        public decimal? ShDeliveryQuantity { get; set; }
+        public decimal? Bzsl { get; set; }
+        public string? ShMaterialDw { get; set; }
+        public decimal? Djsl { get; set; }
+        public string? Jhdbh { get; set; }
+        public string? ShMaterialGgxh { get; set; }
+    }
+}
+

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

@@ -232,7 +232,7 @@ public class ExecutableDailyPlanService : IDynamicApiController, ITransient
         LEFT JOIN ItemMaster i ON p.ItemNum = i.ItemNum AND p.`Domain` = i.`Domain`
         LEFT JOIN WorkOrdMaster w ON p.WorkOrds = w.WorkOrd AND p.`Domain` = w.`Domain`
         LEFT JOIN WorkOrdRouting r ON p.Op = r.OP AND p.ItemNum = r.ItemNum AND p.WorkOrds = r.WorkOrd
-        LEFT JOIN mes_morder m ON w.WorkOrd = m.morder_no AND w.`Domain` = m.factory_id
+        LEFT JOIN mes_morder m ON w.WorkOrd = m.morder_no AND CAST(w.`Domain` AS CHAR(64)) = CAST(m.factory_id AS CHAR(64))
         LEFT JOIN ProdLineDetail pd ON p.`Domain` = pd.`Domain` AND p.ItemNum = pd.Part AND p.Line = pd.Line AND p.Op = pd.Op
         LEFT JOIN ScheduleResultOpMaster sm ON sm.`Domain` = p.`Domain` AND p.UDate2 = sm.WorkActivateTime AND p.ItemNum = sm.ItemNum AND sm.WorkOrd = p.WorkOrds AND sm.Op = p.Op
         LEFT JOIN WorkCtrMaster wm ON wm.WorkCtr = sm.WorkCtr AND wm.`Domain` = sm.`Domain`

+ 61 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/SysMenuSeedData.cs

@@ -100,6 +100,8 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
             list.Add(m);
         foreach (var m in BuildS3SupplyMenus(ct))
             list.Add(m);
+        foreach (var m in BuildS4DeliveryMenus(ct))
+            list.Add(m);
         foreach (var m in BuildS1SalesKanbanMenus(ct))
             list.Add(m);
         foreach (var m in BuildS8CollaborationMenus(ct))
@@ -135,6 +137,9 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
             s3Procurement.Redirect = "/aidop/s3/procurement/purchase-request";
             s3Procurement.Remark = "S3 采购管理";
         }
+        var s4Delivery = list.FirstOrDefault(x => x.Id == 1322000000012L);
+        if (s4Delivery != null)
+            s4Delivery.Redirect = "/aidop/s4/delivery/supplier-delivery-management";
 
         // S8:复用自动生成的首项菜单位,直接作为「异常监控看板」页,避免再出现中间层「异常管理」目录
         var s8Dashboard = list.FirstOrDefault(x => x.Id == 1322000000027L);
@@ -241,6 +246,7 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
         { ("S2", 2), "/aidop/s2/operation-plan" },
         { ("S2", 3), "/aidop/s2/collaboration-kanban" },
         { ("S3", 1), "/aidop/s3/material-plan" },
+        { ("S4", 2), "/aidop/s4/delivery" },
     };
 
     /// <summary>S1 等模块下叶子菜单 route name 覆盖。</summary>
@@ -252,6 +258,7 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
         { ("S2", 2), "aidopS2OperationPlan" },
         { ("S2", 3), "aidopS2CollaborationKanban" },
         { ("S3", 1), "aidopS3MaterialPlan" },
+        { ("S4", 2), "aidopS4Delivery" },
     };
 
     /// <summary>
@@ -267,8 +274,62 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
         { ("S2", 2) }, // 作业计划 → 目录(可执行日计划、产线日历等)
         { ("S2", 3) }, // 制造协同看板 → 目录(工单执行进度看板)
         { ("S3", 1) }, // 物料计划 → 目录(物料需求计划)
+        { ("S4", 2) }, // 交货管理 → 目录(供应商交货管理、供应商发货单)
     };
 
+    private static IEnumerable<SysMenu> BuildS4DeliveryMenus(DateTime ct)
+    {
+        const long deliveryDirId = 1322000000012L;
+        const long baseId = 1329004100000L;
+
+        yield return new SysMenu
+        {
+            Id = baseId + 1,
+            Pid = deliveryDirId,
+            Title = "供应商交货管理",
+            Path = "/aidop/s4/delivery/supplier-delivery-management",
+            Name = "aidopS4SupplierDeliveryManagement",
+            Component = "/aidop/s4/delivery/supplierDeliveryManagementList",
+            Icon = "ele-Document",
+            Type = MenuTypeEnum.Menu,
+            CreateTime = ct,
+            OrderNo = 10,
+            Remark = "S4 供应商交货管理"
+        };
+
+        yield return new SysMenu
+        {
+            Id = baseId + 2,
+            Pid = deliveryDirId,
+            Title = "供应商发货单",
+            Path = "/aidop/s4/delivery/supplier-shipment",
+            Name = "aidopS4SupplierShipment",
+            Component = "/aidop/s4/delivery/supplierShipmentList",
+            Icon = "ele-Tickets",
+            Type = MenuTypeEnum.Menu,
+            CreateTime = ct,
+            OrderNo = 20,
+            Remark = "S4 供应商发货单"
+        };
+
+        // 隐藏路由:用于「生成发货单 / 编辑 / 查看」跳转
+        yield return new SysMenu
+        {
+            Id = baseId + 3,
+            Pid = deliveryDirId,
+            Title = "发货单表单",
+            Path = "/aidop/s4/delivery/supplier-shipment-form",
+            Name = "aidopS4SupplierShipmentForm",
+            Component = "/aidop/s4/delivery/supplierShipmentForm",
+            Icon = "ele-Document",
+            Type = MenuTypeEnum.Menu,
+            CreateTime = ct,
+            OrderNo = 90,
+            IsHide = true,
+            Remark = "S4 发货单新增/编辑/查看表单"
+        };
+    }
+
     private static string BuildRemark(string code, (string Title, string Desc, string Complexity, string Days, string Note) leaf)
     {
         var notePart = string.IsNullOrWhiteSpace(leaf.Note) ? "" : $" | {leaf.Note}";

+ 1 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/WorkOrder/Entity/MesMoentry.cs

@@ -59,7 +59,7 @@ public class MesMoentry
     public int? IsDeleted { get; set; }
 
     [SugarColumn(ColumnName = "factory_id", Length = 64, IsNullable = true)]
-    public string? FactoryId { get; set; }
+    public long? FactoryId { get; set; }
 
     [SugarColumn(ColumnName = "org_id", IsNullable = true)]
     public long? OrgId { get; set; }

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

@@ -91,8 +91,8 @@ public class MesMorder
     [SugarColumn(ColumnName = "unit", Length = 32, IsNullable = true)]
     public string? Unit { get; set; }
 
-    [SugarColumn(ColumnName = "morder_progress", IsNullable = true)]
-    public decimal? MorderProgress { get; set; }
+    [SugarColumn(ColumnName = "morder_progress", Length = 1000, IsNullable = true)]
+    public string? MorderProgress { get; set; }
 
     [SugarColumn(ColumnName = "morder_production_number", IsNullable = true)]
     public decimal? MorderProductionNumber { get; set; }
@@ -139,8 +139,8 @@ public class MesMorder
     [SugarColumn(ColumnName = "IsDeleted", IsNullable = true)]
     public int? IsDeleted { get; set; }
 
-    [SugarColumn(ColumnName = "factory_id", Length = 64, IsNullable = true)]
-    public string? FactoryId { get; set; }
+    [SugarColumn(ColumnName = "factory_id", IsNullable = true)]
+    public long? FactoryId { get; set; }
 
     [SugarColumn(ColumnName = "org_id", IsNullable = true)]
     public long? OrgId { get; set; }

+ 6 - 6
server/Plugins/Admin.NET.Plugin.AiDOP/WorkOrder/Entity/WorkOrdDetail.cs

@@ -8,11 +8,11 @@ public class WorkOrdDetail
 {
     /// <summary>自增列</summary>
     [SugarColumn(ColumnName = "RecID", IsPrimaryKey = true, IsIdentity = true)]
-    public long RecID { get; set; }
+    public int RecID { get; set; }
 
     /// <summary>工单流水号</summary>
     [SugarColumn(ColumnName = "WorkOrdMasterRecID", IsNullable = true)]
-    public long? WorkOrdMasterRecID { get; set; }
+    public long WorkOrdMasterRecID { get; set; }
 
     [SugarColumn(ColumnName = "Domain", Length = 8, IsNullable = true)]
     public string? Domain { get; set; }
@@ -27,7 +27,7 @@ public class WorkOrdDetail
     public string? ItemNum { get; set; }
 
     [SugarColumn(ColumnName = "Op", Length = 6, IsNullable = true)]
-    public string? Op { get; set; }
+    public int? Op { get; set; }
 
     [SugarColumn(ColumnName = "Location", Length = 10, IsNullable = true)]
     public string? Location { get; set; }
@@ -42,7 +42,7 @@ public class WorkOrdDetail
     public string? Typed { get; set; }
 
     [SugarColumn(ColumnName = "QtyRequired", IsNullable = true)]
-    public decimal? QtyRequired { get; set; }
+    public decimal QtyRequired { get; set; }
 
     [SugarColumn(ColumnName = "QtyIssued", IsNullable = true)]
     public decimal? QtyIssued { get; set; }
@@ -51,7 +51,7 @@ public class WorkOrdDetail
     public decimal? QtyPicked { get; set; }
 
     [SugarColumn(ColumnName = "QtyPosted", IsNullable = true)]
-    public decimal? QtyPosted { get; set; }
+    public decimal QtyPosted { get; set; }
 
     [SugarColumn(ColumnName = "QtyReturned", IsNullable = true)]
     public decimal? QtyReturned { get; set; }
@@ -105,7 +105,7 @@ public class WorkOrdDetail
     public DateTime? UpdateTime { get; set; }
 
     [SugarColumn(ColumnName = "IsActive", IsNullable = true)]
-    public int? IsActive { get; set; }
+    public bool IsActive { get; set; }
 
     [SugarColumn(ColumnName = "IsConfirm", IsNullable = true)]
     public int? IsConfirm { get; set; }

+ 7 - 7
server/Plugins/Admin.NET.Plugin.AiDOP/WorkOrder/Entity/WorkOrdRouting.cs

@@ -8,11 +8,11 @@ public class WorkOrdRouting
 {
     /// <summary>自增列</summary>
     [SugarColumn(ColumnName = "RecID", IsPrimaryKey = true, IsIdentity = true)]
-    public long RecID { get; set; }
+    public int RecID { get; set; }
 
     /// <summary>工单流水号</summary>
     [SugarColumn(ColumnName = "WorkOrdMasterRecID", IsNullable = true)]
-    public long? WorkOrdMasterRecID { get; set; }
+    public long WorkOrdMasterRecID { get; set; }
 
     [SugarColumn(ColumnName = "Domain", Length = 8, IsNullable = true)]
     public string? Domain { get; set; }
@@ -21,7 +21,7 @@ public class WorkOrdRouting
     public string? WorkOrd { get; set; }
 
     [SugarColumn(ColumnName = "OP", Length = 6, IsNullable = true)]
-    public string? OP { get; set; }
+    public int OP { get; set; }
 
     [SugarColumn(ColumnName = "ItemNum", Length = 30, IsNullable = true)]
     public string? ItemNum { get; set; }
@@ -72,7 +72,7 @@ public class WorkOrdRouting
     public decimal? ActSetupTime { get; set; }
 
     [SugarColumn(ColumnName = "RunCrew", IsNullable = true)]
-    public int? RunCrew { get; set; }
+    public decimal RunCrew { get; set; }
 
     [SugarColumn(ColumnName = "SetupCrew", IsNullable = true)]
     public int? SetupCrew { get; set; }
@@ -111,13 +111,13 @@ public class WorkOrdRouting
     public string? NextOp1 { get; set; }
 
     [SugarColumn(ColumnName = "ParentOp", Length = 6, IsNullable = true)]
-    public string? ParentOp { get; set; }
+    public int ParentOp { get; set; }
 
     [SugarColumn(ColumnName = "IsCheckOp", IsNullable = true)]
     public int? IsCheckOp { get; set; }
 
     [SugarColumn(ColumnName = "ProcessOut", IsNullable = true)]
-    public int? ProcessOut { get; set; }
+    public int ProcessOut { get; set; }
 
     [SugarColumn(ColumnName = "Ufld1", Length = 64, IsNullable = true)]
     public string? Ufld1 { get; set; }
@@ -144,7 +144,7 @@ public class WorkOrdRouting
     public DateTime? UpdateTime { get; set; }
 
     [SugarColumn(ColumnName = "IsActive", IsNullable = true)]
-    public int? IsActive { get; set; }
+    public bool IsActive { get; set; }
 
     [SugarColumn(ColumnName = "IsConfirm", IsNullable = true)]
     public int? IsConfirm { get; set; }