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

feat: 新增工单物料齐套看板功能,后端版本升至1.0.84,前端升至2.4.117

Pengxy 2 месяцев назад
Родитель
Сommit
8369d09940

+ 76 - 0
Web/src/views/aidop/s3/api/workOrderMaterialReadiness.ts

@@ -0,0 +1,76 @@
+import service from '/@/utils/request';
+
+export interface Paged<T> {
+	total: number;
+	page: number;
+	pageSize: number;
+	list: T[];
+}
+
+export interface WorkOrderMaterialReadinessRow {
+	rowNum?: string | null;
+	seOrder?: string | null;
+	sePriority?: number | null;
+	workord?: string | null;
+	priority?: number | null;
+	morderType?: string | null;
+	lotSerial?: string | null;
+	op?: string | null;
+	opOrdDate?: string | null;
+	bomNumber?: string | null;
+	descr?: string | null;
+	descr1?: string | null;
+	productionNumber?: number | null;
+	checktime?: string | null;
+	pmbom?: string | null;
+	itemType?: string | null;
+	ownerApplication?: string | null;
+	itemNum?: string | null;
+	componentItemDescr?: string | null;
+	componentItemDrawing?: string | null;
+	qtyRequired?: number | null;
+	componentItemUM?: string | null;
+	componentItemKittingTime?: string | null;
+	qtyRec?: number | null;
+	recDate?: string | null;
+	shortage?: number | null;
+	qtyFrom?: number | null;
+	fromDate?: string | null;
+	zjsl?: number | null;
+	jykssj?: string | null;
+	ztsl?: number | null;
+	jhshrq?: string | null;
+	jqsl?: number | null;
+	hfjq?: string | null;
+	purOrdQty?: number | null;
+	purOrdorddate?: string | null;
+	purOrds?: string | null;
+	contractDate?: string | null;
+	contractNO?: string | null;
+	supplierNumber?: string | null;
+	supplierName?: string | null;
+	buyer?: string | null;
+	buyerUser?: string | null;
+}
+
+export interface WorkOrderMaterialReadinessListParams {
+	page: number;
+	pageSize: number;
+	seOrder?: string;
+	workord?: string;
+	bomNumber?: string;
+	itemNum?: string;
+	supplierName?: string;
+	sortField?: string;
+	sortOrder?: string;
+}
+
+export function fetchWorkOrderMaterialReadinessList(params: WorkOrderMaterialReadinessListParams) {
+	return service
+		.get<Paged<WorkOrderMaterialReadinessRow>>('/api/Supply/work-order-material-readiness/list', { params })
+		.then((r) => r.data);
+}
+
+export function refreshWorkOrderMaterialReadiness() {
+	return service.post('/api/Supply/work-order-material-readiness/refresh').then((r) => r.data);
+}

+ 251 - 0
Web/src/views/aidop/s3/supply/workOrderMaterialReadinessKanban.vue

@@ -0,0 +1,251 @@
+<template>
+	<AidopDemoShell :title="pageTitle" subtitle="工单物料齐套上线看板">
+		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
+			<el-form-item label="订单号">
+				<el-input v-model="query.seOrder" clearable placeholder="模糊查询" style="width: 170px" />
+			</el-form-item>
+			<el-form-item label="工单号">
+				<el-input v-model="query.workord" clearable placeholder="模糊查询" style="width: 170px" />
+			</el-form-item>
+			<el-form-item label="产品编码">
+				<el-input v-model="query.bomNumber" clearable placeholder="精确查询" style="width: 160px" />
+			</el-form-item>
+			<el-form-item label="物料编码">
+				<el-input v-model="query.itemNum" clearable placeholder="精确查询" style="width: 160px" />
+			</el-form-item>
+			<el-form-item label="供应商名称">
+				<el-input v-model="query.supplierName" clearable placeholder="模糊查询" style="width: 170px" />
+			</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" plain :loading="refreshing" @click="onRefresh">刷新</el-button>
+			<el-popover placement="bottom" width="240" 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 style="width: 100%" @sort-change="onSortChange">
+			<el-table-column
+				v-for="item in visibleColumns"
+				:key="item.key"
+				:prop="item.key"
+				:label="item.label"
+				:min-width="item.minWidth || 120"
+				:width="item.width"
+				:align="item.align || 'left'"
+				:fixed="item.fixed"
+				:sortable="item.sortable ? 'custom' : false"
+				:show-overflow-tooltip="item.overflow ?? true"
+				:resizable="true"
+			>
+				<template v-if="item.format === 'date'" #default="{ row }">{{ fmtDate(row[item.key]) }}</template>
+				<template v-else-if="item.format === 'num'" #default="{ row }">{{ fmtNumber(row[item.key]) }}</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="aidopS3WorkOrderMaterialReadiness">
+import { computed, onMounted, reactive, ref } from 'vue';
+import { useRoute } from 'vue-router';
+import { ElMessage } from 'element-plus';
+import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
+import {
+	fetchWorkOrderMaterialReadinessList,
+	refreshWorkOrderMaterialReadiness,
+	type WorkOrderMaterialReadinessRow,
+} from '../api/workOrderMaterialReadiness';
+
+const route = useRoute();
+const pageTitle = computed(() => (route.meta?.title as string) || '工单物料齐套上线看板');
+
+const query = reactive({
+	seOrder: '',
+	workord: '',
+	bomNumber: '',
+	itemNum: '',
+	supplierName: '',
+	page: 1,
+	pageSize: 10,
+	sortField: '',
+	sortOrder: '',
+});
+
+const loading = ref(false);
+const refreshing = ref(false);
+const rows = ref<WorkOrderMaterialReadinessRow[]>([]);
+const total = ref(0);
+
+const columns = [
+	{ key: 'rowNum', label: '序号', width: 90, fixed: undefined, sortable: true },
+	{ key: 'seOrder', label: '订单号', width: 160, fixed: 'left', sortable: true },
+	{ key: 'sePriority', label: '订单优先级', width: 120, sortable: true, align: 'right', format: 'num' },
+	{ key: 'workord', label: '工单号', width: 170, sortable: true },
+	{ key: 'priority', label: '工单优先级', width: 120, sortable: true, align: 'right', format: 'num' },
+	{ key: 'morderType', label: '工单类型', width: 120, sortable: true },
+	{ key: 'lotSerial', label: '生产批号', width: 130, sortable: true },
+	{ key: 'op', label: '工序', width: 90, sortable: true, align: 'right' },
+	{ key: 'opOrdDate', label: '工序开工日期', width: 140, sortable: true, format: 'date' },
+	{ key: 'bomNumber', label: '产品编码', width: 150, sortable: true },
+	{ key: 'descr', label: '产品描述', minWidth: 160, sortable: true },
+	{ key: 'descr1', label: '规格型号', minWidth: 140, sortable: true },
+	{ key: 'productionNumber', label: '生产数量', width: 110, sortable: true, align: 'right', format: 'num' },
+	{ key: 'checktime', label: 'MRP日期', width: 140, sortable: true, format: 'date' },
+	{ key: 'pmbom', label: 'BOM版本', width: 110, sortable: true },
+	{ key: 'itemType', label: '物料类型', width: 110, sortable: true },
+	{ key: 'ownerApplication', label: '物料属性', width: 110, sortable: true },
+	{ key: 'itemNum', label: '物料编码', width: 140, sortable: true },
+	{ key: 'componentItemDescr', label: '物料描述', minWidth: 160, sortable: true },
+	{ key: 'componentItemDrawing', label: '物料图号', width: 130, sortable: true },
+	{ key: 'qtyRequired', label: '需求数量', width: 110, sortable: true, align: 'right', format: 'num' },
+	{ key: 'componentItemUM', label: '单位', width: 90, sortable: true },
+	{ key: 'componentItemKittingTime', label: '需求日期', width: 130, sortable: true, format: 'date' },
+	{ key: 'qtyRec', label: '发料数量', width: 110, sortable: true, align: 'right', format: 'num' },
+	{ key: 'recDate', label: '发料日期', width: 130, sortable: true, format: 'date' },
+	{ key: 'shortage', label: '欠料数量', width: 110, sortable: true, align: 'right', format: 'num' },
+	{ key: 'qtyFrom', label: '备料数量', width: 110, sortable: true, align: 'right', format: 'num' },
+	{ key: 'fromDate', label: '备料日期', width: 130, sortable: true, format: 'date' },
+	{ key: 'zjsl', label: '在检数量', width: 110, sortable: true, align: 'right', format: 'num' },
+	{ key: 'jykssj', label: '检验申请日期', width: 140, sortable: true, format: 'date' },
+	{ key: 'ztsl', label: '在途数量', width: 110, sortable: true, align: 'right', format: 'num' },
+	{ key: 'jhshrq', label: '发货日期', width: 130, sortable: true, format: 'date' },
+	{ key: 'jqsl', label: '承诺数量', width: 110, sortable: true, align: 'right', format: 'num' },
+	{ key: 'hfjq', label: '承诺交期', width: 130, sortable: true, format: 'date' },
+	{ key: 'purOrdQty', label: '采购数量', width: 110, sortable: true, align: 'right', format: 'num' },
+	{ key: 'purOrdorddate', label: '采购日期', width: 130, sortable: true, format: 'date' },
+	{ key: 'purOrds', label: '采购单号', width: 150, sortable: true },
+	{ key: 'contractDate', label: '合同日期', width: 130, sortable: true, format: 'date' },
+	{ key: 'contractNO', label: '合同编号', width: 150, sortable: true },
+	{ key: 'supplierNumber', label: '供应商编码', width: 130, sortable: true },
+	{ key: 'supplierName', label: '供应商名称', minWidth: 150, sortable: true },
+	{ key: 'buyer', label: '采购组', width: 100, sortable: true },
+	{ key: 'buyerUser', label: '采购员', width: 100, sortable: true },
+] as const;
+
+type ColumnDef = (typeof columns)[number];
+type ColumnKey = ColumnDef['key'];
+
+const col = reactive(
+	Object.fromEntries(columns.map((item) => [item.key, true])) as Record<ColumnKey, boolean>
+);
+
+const toggleItems = columns.map((item) => ({ key: item.key, label: item.label }));
+const visibleColumns = computed(() => columns.filter((item) => col[item.key]));
+
+function fmtDate(v?: string | number | null) {
+	if (!v) return '';
+	return String(v).slice(0, 10);
+}
+function fmtNumber(v?: string | number | null) {
+	if (v === null || v === undefined || v === '') return '';
+	const n = Number(v);
+	return Number.isNaN(n) ? String(v) : n.toLocaleString('zh-CN');
+}
+
+async function loadList() {
+	loading.value = true;
+	try {
+		const data = await fetchWorkOrderMaterialReadinessList({
+			page: query.page,
+			pageSize: query.pageSize,
+			seOrder: query.seOrder || undefined,
+			workord: query.workord || undefined,
+			bomNumber: query.bomNumber || undefined,
+			itemNum: query.itemNum || undefined,
+			supplierName: query.supplierName || undefined,
+			sortField: query.sortField || undefined,
+			sortOrder: query.sortOrder || undefined,
+		});
+		rows.value = data.list || [];
+		total.value = data.total || 0;
+	} finally {
+		loading.value = false;
+	}
+}
+
+function doSearch() {
+	query.page = 1;
+	loadList();
+}
+
+function resetQuery() {
+	query.seOrder = '';
+	query.workord = '';
+	query.bomNumber = '';
+	query.itemNum = '';
+	query.supplierName = '';
+	query.page = 1;
+	query.sortField = '';
+	query.sortOrder = '';
+	loadList();
+}
+
+async function onRefresh() {
+	refreshing.value = true;
+	try {
+		await refreshWorkOrderMaterialReadiness();
+		ElMessage.success('刷新成功');
+		await loadList();
+	} finally {
+		refreshing.value = false;
+	}
+}
+
+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();
+}
+
+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>

+ 74 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/AidopMenuLinkSync.cs

@@ -607,10 +607,20 @@ public static class AidopMenuLinkSync
             "/aidop/s3/procurement/outsource-order", "aidopS3OutsourceOrder",
             "/aidop/s3/supply/outsourceOrderList", "ele-List", 30, "S3 委外加工订单(PurOrdMaster/PW)");
 
-        EnsureS3LeafMenu(
+        EnsureS3DirMenu(
             db, ct, 1329003100010L, s3RootId, "供应协同看板",
-            "/aidop/s3/supply-kanban", "aidopS3SupplyKanban",
-            "/aidop/kanban/s3", "ele-DataBoard", 102, "S3 供应协同看板");
+            "/aidop/s3/supply-kanban", "aidopS3SupplyKanbanDir",
+            "/aidop/s3/supply-kanban/dashboard", "S3 供应协同看板目录");
+
+        EnsureS3LeafMenu(
+            db, ct, 1329003100015L, 1329003100010L, "供应协同看板",
+            "/aidop/s3/supply-kanban/dashboard", "aidopS3SupplyKanban",
+            "/aidop/kanban/s3", "ele-DataBoard", 10, "S3 供应协同看板");
+
+        EnsureS3LeafMenu(
+            db, ct, 1329003100016L, 1329003100010L, "工单物料齐套上线看板",
+            "/aidop/s3/supply-kanban/work-order-material-readiness", "aidopS3WorkOrderMaterialReadiness",
+            "/aidop/s3/supply/workOrderMaterialReadinessKanban", "ele-List", 20, "S3 工单物料齐套上线看板");
     }
 
     private static void EnsureS3LeafMenu(
@@ -672,4 +682,65 @@ public static class AidopMenuLinkSync
             .UpdateColumns(m => new { m.Pid, m.Title, m.Path, m.Name, m.Component, m.Icon, m.Type, m.OrderNo, m.Remark })
             .ExecuteCommand();
     }
+
+    private static void EnsureS3DirMenu(
+        ISqlSugarClient db,
+        DateTime ct,
+        long id,
+        long pid,
+        string title,
+        string path,
+        string name,
+        string redirect,
+        string remark)
+    {
+        var row = db.Queryable<SysMenu>().First(m => m.Id == id);
+        if (row == null)
+        {
+            db.Insertable(new SysMenu
+            {
+                Id = id,
+                Pid = pid,
+                Title = title,
+                Path = path,
+                Name = name,
+                Component = "Layout",
+                Icon = "ele-Folder",
+                Type = MenuTypeEnum.Dir,
+                Redirect = redirect,
+                CreateTime = ct,
+                OrderNo = 102,
+                Status = StatusEnum.Enable,
+                Remark = remark
+            }).ExecuteCommand();
+            return;
+        }
+
+        var needFix = row.Pid != pid
+                      || row.Title != title
+                      || row.Path != path
+                      || row.Name != name
+                      || row.Component != "Layout"
+                      || row.Icon != "ele-Folder"
+                      || row.Type != MenuTypeEnum.Dir
+                      || row.Redirect != redirect
+                      || row.OrderNo != 102
+                      || row.Remark != remark;
+        if (!needFix)
+            return;
+
+        row.Pid = pid;
+        row.Title = title;
+        row.Path = path;
+        row.Name = name;
+        row.Component = "Layout";
+        row.Icon = "ele-Folder";
+        row.Type = MenuTypeEnum.Dir;
+        row.Redirect = redirect;
+        row.OrderNo = 102;
+        row.Remark = remark;
+        db.Updateable(row)
+            .UpdateColumns(m => new { m.Pid, m.Title, m.Path, m.Name, m.Component, m.Icon, m.Type, m.Redirect, m.OrderNo, m.Remark })
+            .ExecuteCommand();
+    }
 }

+ 32 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/SysMenuSeedData.cs

@@ -1312,14 +1312,45 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
             Pid = s3RootDirId,
             Title = "供应协同看板",
             Path = "/aidop/s3/supply-kanban",
+            Name = "aidopS3SupplyKanbanDir",
+            Component = "Layout",
+            Icon = "ele-Folder",
+            Type = MenuTypeEnum.Dir,
+            CreateTime = ct,
+            OrderNo = 102,
+            Redirect = "/aidop/s3/supply-kanban/dashboard",
+            Remark = "S3 供应协同看板目录"
+        };
+
+        yield return new SysMenu
+        {
+            Id = 1329003100015L,
+            Pid = 1329003100010L,
+            Title = "供应协同看板",
+            Path = "/aidop/s3/supply-kanban/dashboard",
             Name = "aidopS3SupplyKanban",
             Component = "/aidop/kanban/s3",
             Icon = "ele-DataBoard",
             Type = MenuTypeEnum.Menu,
             CreateTime = ct,
-            OrderNo = 102,
+            OrderNo = 10,
             Remark = "S3 供应协同看板"
         };
+
+        yield return new SysMenu
+        {
+            Id = 1329003100016L,
+            Pid = 1329003100010L,
+            Title = "工单物料齐套上线看板",
+            Path = "/aidop/s3/supply-kanban/work-order-material-readiness",
+            Name = "aidopS3WorkOrderMaterialReadiness",
+            Component = "/aidop/s3/supply/workOrderMaterialReadinessKanban",
+            Icon = "ele-List",
+            Type = MenuTypeEnum.Menu,
+            CreateTime = ct,
+            OrderNo = 20,
+            Remark = "S3 工单物料齐套上线看板"
+        };
     }
 
     private static readonly (string Code, string L1, (string Title, string Desc, string Complexity, string Days, string Note)[] Leaves)[] ModuleDefinitions =

+ 61 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Supply/Dto/WorkOrderMaterialReadinessDto.cs

@@ -0,0 +1,61 @@
+namespace Admin.NET.Plugin.AiDOP.Supply;
+
+public class WorkOrderMaterialReadinessListInput
+{
+    public int Page { get; set; } = 1;
+    public int PageSize { get; set; } = 10;
+    public string? SeOrder { get; set; }
+    public string? Workord { get; set; }
+    public string? BomNumber { get; set; }
+    public string? ItemNum { get; set; }
+    public string? SupplierName { get; set; }
+    public string? SortField { get; set; }
+    public string? SortOrder { get; set; }
+}
+
+public class WorkOrderMaterialReadinessRow
+{
+    public string? RowNum { get; set; }
+    public string? SeOrder { get; set; }
+    public decimal? SePriority { get; set; }
+    public string? Workord { get; set; }
+    public decimal? Priority { get; set; }
+    public string? MorderType { get; set; }
+    public string? LotSerial { get; set; }
+    public string? Op { get; set; }
+    public DateTime? OpOrdDate { get; set; }
+    public string? BomNumber { get; set; }
+    public string? Descr { get; set; }
+    public string? Descr1 { get; set; }
+    public decimal? ProductionNumber { get; set; }
+    public DateTime? Checktime { get; set; }
+    public string? PMBOM { get; set; }
+    public string? ItemType { get; set; }
+    public string? OwnerApplication { get; set; }
+    public string? ItemNum { get; set; }
+    public string? ComponentItemDescr { get; set; }
+    public string? ComponentItemDrawing { get; set; }
+    public decimal? QtyRequired { get; set; }
+    public string? ComponentItemUM { get; set; }
+    public DateTime? ComponentItemKittingTime { get; set; }
+    public decimal? QtyRec { get; set; }
+    public DateTime? RecDate { get; set; }
+    public decimal? Shortage { get; set; }
+    public decimal? QtyFrom { get; set; }
+    public DateTime? FromDate { get; set; }
+    public decimal? Zjsl { get; set; }
+    public DateTime? Jykssj { get; set; }
+    public decimal? Ztsl { get; set; }
+    public DateTime? Jhshrq { get; set; }
+    public decimal? Jqsl { get; set; }
+    public DateTime? Hfjq { get; set; }
+    public decimal? PurOrdQty { get; set; }
+    public DateTime? PurOrdorddate { get; set; }
+    public string? PurOrds { get; set; }
+    public DateTime? ContractDate { get; set; }
+    public string? ContractNO { get; set; }
+    public string? SupplierNumber { get; set; }
+    public string? SupplierName { get; set; }
+    public string? Buyer { get; set; }
+    public string? BuyerUser { get; set; }
+}

+ 177 - 10
server/Plugins/Admin.NET.Plugin.AiDOP/Supply/ProcessOutsourceOrderService.cs

@@ -155,22 +155,164 @@ public class ProcessOutsourceOrderService : IDynamicApiController, ITransient
         if (string.IsNullOrWhiteSpace(input.WorkOrd)) throw Oops.Oh("请选择工单");
         var workOrd = input.WorkOrd.Trim();
 
-        await _db.Ado.ExecuteCommandAsync(
-            "CALL pr_MES_GeneratePW(@WorkOrd)",
+        // 先筛出“当前工单且未生成采购单”的外协供应商清单(等价于存储过程内 @v_temp_supp)。
+        var suppliers = await _db.Ado.SqlQueryAsync<SuppRow>(
+            """
+            SELECT DISTINCT d.Supp AS Supp
+            FROM WorkOrdMaster m
+            LEFT JOIN WorkOrdRouting r ON m.WorkOrd=r.WorkOrd
+            LEFT JOIN RoutingOpDetail d ON r.Op=d.Op AND r.ItemNum=d.RoutingCode
+            LEFT JOIN PurOrdMaster p ON m.WorkOrd=p.WorkOrd AND d.Supp=p.Supp
+            WHERE r.ProcessOut=1
+              AND m.WorkOrd=@WorkOrd
+              AND p.RecID IS NULL
+              AND IFNULL(TRIM(d.Supp),'')<>''
+            ORDER BY d.Supp
+            """,
             new SugarParameter("@WorkOrd", workOrd));
+        if (suppliers.Count == 0)
+            throw Oops.Oh("当前工单无需生成外协采购订单。");
 
-        var created = (await _db.Ado.SqlQueryAsync<IdPurOrdRow>(
+        var domain = await _db.Ado.GetStringAsync(
             """
-            SELECT RecID AS Id, PurOrd AS PurOrd
-            FROM PurOrdMaster
-            WHERE WorkOrd=@WorkOrd AND Potype='PW'
-            ORDER BY RecID DESC
+            SELECT IFNULL(TRIM(Domain),'')
+            FROM WorkOrdMaster
+            WHERE WorkOrd=@WorkOrd
             LIMIT 1
             """,
-            new SugarParameter("@WorkOrd", workOrd))).FirstOrDefault();
-        if (created == null) throw Oops.Oh("工序外协订单生成失败");
+            new SugarParameter("@WorkOrd", workOrd)) ?? string.Empty;
+
+        var nbrRows = await _db.Ado.SqlQueryAsync<OrdNbrRow>(
+            "CALL pr_SFM_GetOrdNbr(@Domain,@NbrType,@Increment,@UserNo,@IsDopCall)",
+            new SugarParameter("@Domain", domain),
+            new SugarParameter("@NbrType", "pw"),
+            new SugarParameter("@Increment", suppliers.Count),
+            new SugarParameter("@UserNo", string.Empty),
+            new SugarParameter("@IsDopCall", 1));
+        if (nbrRows.Count < suppliers.Count || nbrRows.Any(x => string.IsNullOrWhiteSpace(x.NbrResult)))
+            throw Oops.Oh("当前采购单号生成失败,请检查单号规则维护。");
+
+        var created = new List<IdPurOrdRow>();
+        try
+        {
+            _db.Ado.BeginTran();
+            for (var i = 0; i < suppliers.Count; i++)
+            {
+                var supp = suppliers[i].Supp!.Trim();
+                var purOrd = nbrRows[i].NbrResult!.Trim();
+                var now = DateTime.Now;
+
+                await _db.Ado.ExecuteCommandAsync(
+                    """
+                    INSERT INTO PurOrdMaster
+                    (Buyer,Domain,PurOrd,OrdDate,ReqBy,Status,Supp,CreateUser,CreateTime,IsActive,IsConfirm,Potype,Department,`Usage`,FSTID,ERPWorkOrd,WorkOrd,tenant_id)
+                    SELECT
+                        @Buyer,
+                        IFNULL(TRIM(m.Domain),''),
+                        @PurOrd,
+                        @Now,
+                        'PO',
+                        '',
+                        @Supp,
+                        @CreateUser,
+                        @Now,
+                        1,
+                        1,
+                        'PW',
+                        m.Department,
+                        '外协',
+                        1,
+                        m.Batch,
+                        m.WorkOrd,
+                        @TenantId
+                    FROM WorkOrdMaster m
+                    WHERE m.WorkOrd=@WorkOrd
+                    LIMIT 1
+                    """,
+                    new SugarParameter("@Buyer", _userManager.Account),
+                    new SugarParameter("@PurOrd", purOrd),
+                    new SugarParameter("@Now", now),
+                    new SugarParameter("@Supp", supp),
+                    new SugarParameter("@CreateUser", _userManager.Account),
+                    new SugarParameter("@WorkOrd", workOrd),
+                    new SugarParameter("@TenantId", _userManager.TenantId <= 0 ? null : _userManager.TenantId));
+
+                var masterId = await _db.Ado.GetIntAsync(
+                    "SELECT IFNULL(MAX(RecID),0) FROM PurOrdMaster WHERE PurOrd=@PurOrd",
+                    new SugarParameter("@PurOrd", purOrd));
+                if (masterId <= 0)
+                    throw Oops.Oh("采购订单生成失败。");
+
+                var lines = await _db.Ado.SqlQueryAsync<CreateLineRow>(
+                    """
+                    SELECT
+                        IFNULL(i.PMBOM,'') AS PMBOM,
+                        IFNULL(r.Op,0) AS Op,
+                        r.ItemNum AS ItemNum,
+                        IFNULL(i.UM,'') AS UM,
+                        IFNULL(m.QtyOrded,0) AS QtyOrded,
+                        IFNULL(i.Rev,'') AS Rev,
+                        IFNULL(m.Department,'') AS Department,
+                        m.WorkOrd AS WorkOrd,
+                        IFNULL(i.Drawing,'') AS Drawing,
+                        IFNULL(m.RecID,0) AS WorkOrdID,
+                        IFNULL(d.ProcessOutDay,0) AS ProcessOutDay
+                    FROM WorkOrdMaster m
+                    LEFT JOIN WorkOrdRouting r ON m.WorkOrd=r.WorkOrd
+                    LEFT JOIN RoutingOpDetail d ON r.Op=d.Op AND r.ItemNum=d.RoutingCode
+                    LEFT JOIN ItemMaster i ON r.ItemNum=i.ItemNum
+                    WHERE r.ProcessOut=1
+                      AND m.WorkOrd=@WorkOrd
+                      AND d.Supp=@Supp
+                    ORDER BY r.RecID
+                    """,
+                    new SugarParameter("@WorkOrd", workOrd),
+                    new SugarParameter("@Supp", supp));
+
+                for (var lineIndex = 0; lineIndex < lines.Count; lineIndex++)
+                {
+                    var row = lines[lineIndex];
+                    await _db.Ado.ExecuteCommandAsync(
+                        """
+                        INSERT INTO PurOrdDetail
+                        (ActiveRlseID1,Domain,DueDate,Line,Location,PurOrd,Op,ItemNum,RctQty,QtyOrded,Status,UM,WorkOrdID,CreateUser,CreateTime,IsActive,IsConfirm,Potype,`Usage`,ReceiptQty,Rev,Department,WorkOrd,Drawing,PurOrdRecID,tenant_id)
+                        VALUES
+                        (@ActiveRlseID1,@Domain,DATE_ADD(@Now, INTERVAL @ProcessOutDay DAY),@Line,'5008',@PurOrd,@Op,@ItemNum,0,@QtyOrded,'',@UM,@WorkOrdID,@CreateUser,@Now,1,1,'PW','外协',0,@Rev,@Department,@WorkOrd,@Drawing,@PurOrdRecID,@TenantId)
+                        """,
+                        new SugarParameter("@ActiveRlseID1", row.PMBOM),
+                        new SugarParameter("@Domain", domain),
+                        new SugarParameter("@Now", now),
+                        new SugarParameter("@ProcessOutDay", row.ProcessOutDay),
+                        new SugarParameter("@Line", lineIndex + 1),
+                        new SugarParameter("@PurOrd", purOrd),
+                        new SugarParameter("@Op", row.Op),
+                        new SugarParameter("@ItemNum", row.ItemNum),
+                        new SugarParameter("@QtyOrded", row.QtyOrded),
+                        new SugarParameter("@UM", row.UM),
+                        new SugarParameter("@WorkOrdID", row.WorkOrdID),
+                        new SugarParameter("@CreateUser", _userManager.Account),
+                        new SugarParameter("@Rev", row.Rev),
+                        new SugarParameter("@Department", row.Department),
+                        new SugarParameter("@WorkOrd", row.WorkOrd),
+                        new SugarParameter("@Drawing", row.Drawing),
+                        new SugarParameter("@PurOrdRecID", masterId),
+                        new SugarParameter("@TenantId", _userManager.TenantId <= 0 ? null : _userManager.TenantId));
+                }
+
+                created.Add(new IdPurOrdRow { Id = masterId, PurOrd = purOrd });
+            }
+
+            _db.Ado.CommitTran();
+        }
+        catch
+        {
+            _db.Ado.RollbackTran();
+            throw Oops.Oh("采购订单生成失败。");
+        }
 
-        return new { id = created.Id, purOrd = created.PurOrd, message = "生成成功" };
+        var latest = created.OrderByDescending(x => x.Id).FirstOrDefault();
+        if (latest == null) throw Oops.Oh("采购订单生成失败。");
+        return new { id = latest.Id, purOrd = latest.PurOrd, message = "采购订单生成成功。" };
     }
 
     [DisplayName("保存工序外协订单")]
@@ -543,6 +685,31 @@ public class ProcessOutsourceOrderService : IDynamicApiController, ITransient
         public string? PurOrd { get; set; }
     }
 
+    private sealed class SuppRow
+    {
+        public string? Supp { get; set; }
+    }
+
+    private sealed class OrdNbrRow
+    {
+        public string? NbrResult { get; set; }
+    }
+
+    private sealed class CreateLineRow
+    {
+        public string? PMBOM { get; set; }
+        public int Op { get; set; }
+        public string? ItemNum { get; set; }
+        public string? UM { get; set; }
+        public decimal QtyOrded { get; set; }
+        public string? Rev { get; set; }
+        public string? Department { get; set; }
+        public string? WorkOrd { get; set; }
+        public string? Drawing { get; set; }
+        public long WorkOrdID { get; set; }
+        public int ProcessOutDay { get; set; }
+    }
+
     private sealed class DbDetailRow
     {
         public int Id { get; set; }

+ 181 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Supply/WorkOrderMaterialReadinessService.cs

@@ -0,0 +1,181 @@
+namespace Admin.NET.Plugin.AiDOP.Supply;
+
+/// <summary>
+/// 工单物料齐套上线看板服务
+/// </summary>
+[ApiDescriptionSettings(Order = 312, Description = "工单物料齐套上线看板")]
+[Route("api/Supply")]
+[AllowAnonymous]
+[NonUnify]
+public class WorkOrderMaterialReadinessService : IDynamicApiController, ITransient
+{
+    private readonly ISqlSugarClient _db;
+
+    public WorkOrderMaterialReadinessService(ISqlSugarClient db)
+    {
+        _db = db;
+    }
+
+    [DisplayName("工单物料齐套上线看板列表")]
+    [HttpGet("work-order-material-readiness/list")]
+    public async Task<object> GetList([FromQuery] WorkOrderMaterialReadinessListInput input)
+    {
+        var page = input.Page <= 0 ? 1 : input.Page;
+        var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
+        var offset = (page - 1) * pageSize;
+
+        var where = new List<string> { "1=1" };
+        var pars = new List<SugarParameter>();
+
+        if (!string.IsNullOrWhiteSpace(input.SeOrder))
+        {
+            where.Add("a.SeOrder LIKE @SeOrder");
+            pars.Add(new SugarParameter("@SeOrder", $"%{input.SeOrder.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.Workord))
+        {
+            where.Add("a.workord LIKE @Workord");
+            pars.Add(new SugarParameter("@Workord", $"%{input.Workord.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.BomNumber))
+        {
+            where.Add("a.bom_number = @BomNumber");
+            pars.Add(new SugarParameter("@BomNumber", input.BomNumber.Trim()));
+        }
+        if (!string.IsNullOrWhiteSpace(input.ItemNum))
+        {
+            where.Add("a.ItemNum = @ItemNum");
+            pars.Add(new SugarParameter("@ItemNum", input.ItemNum.Trim()));
+        }
+        if (!string.IsNullOrWhiteSpace(input.SupplierName))
+        {
+            where.Add("a.supplier_name LIKE @SupplierName");
+            pars.Add(new SugarParameter("@SupplierName", $"%{input.SupplierName.Trim()}%"));
+        }
+
+        var fromSql = $"FROM WorkOrdTemp a WHERE {string.Join(" AND ", where)}";
+        var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) {fromSql}", pars);
+        var orderBy = BuildOrderBy(input.SortField, input.SortOrder);
+
+        var list = await _db.Ado.SqlQueryAsync<WorkOrderMaterialReadinessRow>(
+            $"""
+            SELECT
+                a.RowNum AS RowNum,
+                a.SeOrder AS SeOrder,
+                a.SePriority AS SePriority,
+                a.workord AS Workord,
+                a.Priority AS Priority,
+                a.morder_type AS MorderType,
+                a.LotSerial AS LotSerial,
+                a.Op AS Op,
+                a.OpOrdDate AS OpOrdDate,
+                a.bom_number AS BomNumber,
+                a.Descr AS Descr,
+                a.Descr1 AS Descr1,
+                a.production_number AS ProductionNumber,
+                a.checktime AS Checktime,
+                a.PMBOM AS PMBOM,
+                a.ItemType AS ItemType,
+                a.OwnerApplication AS OwnerApplication,
+                a.ItemNum AS ItemNum,
+                a.ComponentItemDescr AS ComponentItemDescr,
+                a.ComponentItemDrawing AS ComponentItemDrawing,
+                a.QtyRequired AS QtyRequired,
+                a.ComponentItemUM AS ComponentItemUM,
+                a.ComponentItemKittingTime AS ComponentItemKittingTime,
+                a.QtyRec AS QtyRec,
+                a.RecDate AS RecDate,
+                a.shortage AS Shortage,
+                a.QtyFrom AS QtyFrom,
+                a.FromDate AS FromDate,
+                a.zjsl AS Zjsl,
+                a.jykssj AS Jykssj,
+                a.ztsl AS Ztsl,
+                a.jhshrq AS Jhshrq,
+                a.jqsl AS Jqsl,
+                a.hfjq AS Hfjq,
+                a.PurOrdQty AS PurOrdQty,
+                a.PurOrdorddate AS PurOrdorddate,
+                a.PurOrds AS PurOrds,
+                a.ContractDate AS ContractDate,
+                a.ContractNO AS ContractNO,
+                a.supplier_number AS SupplierNumber,
+                a.supplier_name AS SupplierName,
+                a.Buyer AS Buyer,
+                a.BuyerUser AS BuyerUser
+            {fromSql}
+            ORDER BY {orderBy}
+            LIMIT {pageSize} OFFSET {offset}
+            """,
+            pars);
+
+        return new { total, page, pageSize, list };
+    }
+
+    [DisplayName("刷新工单物料齐套上线看板")]
+    [HttpPost("work-order-material-readiness/refresh")]
+    public async Task<object> Refresh()
+    {
+        try
+        {
+            await _db.Ado.ExecuteCommandAsync("CALL pr_MES_Report_DayWorkOrdDetailShortage_New('')");
+            return new { message = "刷新成功" };
+        }
+        catch (Exception ex)
+        {
+            throw Oops.Oh($"刷新失败: {ex.Message}");
+        }
+    }
+
+    private static string BuildOrderBy(string? sortField, string? sortOrder)
+    {
+        var dir = string.Equals(sortOrder, "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC";
+        return sortField?.ToLowerInvariant() switch
+        {
+            "rownum" => $"a.RowNum {dir}",
+            "seorder" => $"a.SeOrder {dir}",
+            "sepriority" => $"a.SePriority {dir}",
+            "workord" => $"a.workord {dir}",
+            "priority" => $"a.Priority {dir}",
+            "mordertype" => $"a.morder_type {dir}",
+            "lotserial" => $"a.LotSerial {dir}",
+            "op" => $"a.Op {dir}",
+            "oporddate" => $"a.OpOrdDate {dir}",
+            "bomnumber" => $"a.bom_number {dir}",
+            "descr" => $"a.Descr {dir}",
+            "descr1" => $"a.Descr1 {dir}",
+            "productionnumber" => $"a.production_number {dir}",
+            "checktime" => $"a.checktime {dir}",
+            "pmbom" => $"a.PMBOM {dir}",
+            "itemtype" => $"a.ItemType {dir}",
+            "ownerapplication" => $"a.OwnerApplication {dir}",
+            "itemnum" => $"a.ItemNum {dir}",
+            "componentitemdescr" => $"a.ComponentItemDescr {dir}",
+            "componentitemdrawing" => $"a.ComponentItemDrawing {dir}",
+            "qtyrequired" => $"a.QtyRequired {dir}",
+            "componentitemum" => $"a.ComponentItemUM {dir}",
+            "componentitemkittingtime" => $"a.ComponentItemKittingTime {dir}",
+            "qtyrec" => $"a.QtyRec {dir}",
+            "recdate" => $"a.RecDate {dir}",
+            "shortage" => $"a.shortage {dir}",
+            "qtyfrom" => $"a.QtyFrom {dir}",
+            "fromdate" => $"a.FromDate {dir}",
+            "zjsl" => $"a.zjsl {dir}",
+            "jykssj" => $"a.jykssj {dir}",
+            "ztsl" => $"a.ztsl {dir}",
+            "jhshrq" => $"a.jhshrq {dir}",
+            "jqsl" => $"a.jqsl {dir}",
+            "hfjq" => $"a.hfjq {dir}",
+            "purordqty" => $"a.PurOrdQty {dir}",
+            "purordorddate" => $"a.PurOrdorddate {dir}",
+            "purords" => $"a.PurOrds {dir}",
+            "contractdate" => $"a.ContractDate {dir}",
+            "contractno" => $"a.ContractNO {dir}",
+            "suppliernumber" => $"a.supplier_number {dir}",
+            "suppliername" => $"a.supplier_name {dir}",
+            "buyer" => $"a.Buyer {dir}",
+            "buyeruser" => $"a.BuyerUser {dir}",
+            _ => "a.Priority ASC, a.workord ASC"
+        };
+    }
+}