Переглянути джерело

feat: 订单编号自动生成(SO+日期+流水号)、closed默认0、审批流面板移至变更表单

- 新增表单订单编号改为只读,保存后自动生成SO+yyyyMMdd+4位流水号

- 新建订单closed字段默认为0

- 编辑表单移除审批流提交按钮,仅变更表单显示审批流面板

- 产品设计表单BOM单位用量与图纸设计周期字段增强

- 审批流PropertyCommon.vue及ProductStructure实体相关改动

- 前端版本 2.4.155→2.4.156,后端版本 1.0.120→1.0.121
Pengxy 1 місяць тому
батько
коміт
e7d7c12532

+ 1 - 1
Web/package.json

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

+ 59 - 0
Web/src/views/aidop/api/productDesign.ts

@@ -31,10 +31,12 @@ export interface ProductDesignRow {
 	drawingPlanEnd?: string | null;
 	drawingActualStart?: string | null;
 	drawingActualEnd?: string | null;
+	drawingDesignCycle?: number | null;
 	applicant?: string | null;
 	applyDate?: string | null;
 	productModel?: string | null;
 	itemNum?: string | null;
+	productName?: string | null;
 	createTime?: string | null;
 	updateTime?: string | null;
 }
@@ -47,6 +49,7 @@ export interface ProductDesignBomLine {
 	itemNum?: string | null;
 	itemName?: string | null;
 	processCode?: string | null;
+	qty?: number | null;
 	fixedLossQty?: number | null;
 	batchNo?: string | null;
 }
@@ -74,10 +77,12 @@ export interface ProductDesignDetail {
 	drawingPlanEnd?: string | null;
 	drawingActualStart?: string | null;
 	drawingActualEnd?: string | null;
+	drawingDesignCycle?: number | null;
 	applicant?: string | null;
 	applyDate?: string | null;
 	productModel?: string | null;
 	itemNum?: string | null;
+	productName?: string | null;
 	language?: string | null;
 	lineRemark?: string | null;
 	createUser?: string | null;
@@ -99,10 +104,12 @@ export interface ProductDesignSaveParams {
 	drawingPlanEnd?: string | null;
 	drawingActualStart?: string | null;
 	drawingActualEnd?: string | null;
+	drawingDesignCycle?: number | null;
 	applicant?: string | null;
 	applyDate?: string | null;
 	productModel?: string | null;
 	itemNum?: string | null;
+	productName?: string | null;
 	language?: string | null;
 	lineRemark?: string | null;
 	boms: ProductDesignBomLine[];
@@ -128,3 +135,55 @@ export function saveProductDesign(body: ProductDesignSaveParams) {
 export function deleteProductDesign(id: number) {
 	return service.post<unknown>('/api/Order/productdesign/delete', { id }).then((r) => unwrapFurionBody<unknown>(r.data));
 }
+
+/** BOM+工艺查询返回 */
+export interface BomAndRoutingResult {
+	boms: BomQueryRow[];
+	routings: RoutingQueryRow[];
+	/** 图纸设计周期(小时),从 ItemMaster.drawing_design 查询 */
+	drawingDesignCycle?: number | null;
+}
+
+export interface BomQueryRow {
+	parentItem?: string | null;
+	itemNum?: string | null;
+	itemName?: string | null;
+	op?: string | null;
+	qty?: number | null;
+	structureType?: string | null;
+	emtType?: string | null;
+	qtyConsumed?: number | null;
+}
+
+export interface RoutingQueryRow {
+	descr?: string | null;
+	op?: number | null;
+	parentOp?: number | null;
+	milestoneOp?: string | null;
+}
+
+export function fetchBomAndRouting(itemNum: string) {
+	return service
+		.get<unknown>('/api/Order/productdesign/bom-routing', { params: { itemNum } })
+		.then((r) => unwrapFurionBody<BomAndRoutingResult>(r.data));
+}
+
+// ──────────────── 下拉选项 ────────────────
+export interface DropdownOption {
+	value: string;
+	label: string;
+}
+
+/** 获取合同下拉选项 */
+export function fetchContractOptions(keyword?: string) {
+	return service
+		.get<unknown>('/api/Order/productdesign/contract-options', { params: { keyword } })
+		.then((r) => unwrapFurionBody<DropdownOption[]>(r.data));
+}
+
+/** 获取用户下拉选项 */
+export function fetchUserOptions(keyword?: string) {
+	return service
+		.get<unknown>('/api/Order/productdesign/user-options', { params: { keyword } })
+		.then((r) => unwrapFurionBody<DropdownOption[]>(r.data));
+}

+ 212 - 14
Web/src/views/aidop/business/productDesignForm.vue

@@ -19,7 +19,23 @@
 				</el-col>
 				<el-col :span="6">
 					<el-form-item label="合同号">
-						<el-input v-model="form.contractNo" :disabled="isView" placeholder="请输入合同号" clearable />
+						<el-select
+							v-model="form.contractNo"
+							:disabled="isView"
+							filterable
+							remote
+							:remote-method="loadContractOptions"
+							placeholder="请输入合同号"
+							clearable
+							style="width: 100%"
+						>
+							<el-option
+								v-for="item in contractOptions"
+								:key="item.value"
+								:label="item.label"
+								:value="item.value"
+							/>
+						</el-select>
 					</el-form-item>
 				</el-col>
 				<el-col :span="6">
@@ -32,12 +48,29 @@
 				</el-col>
 				<el-col :span="6">
 					<el-form-item label="设计负责人">
-						<el-input v-model="form.designLeadName" :disabled="isView" placeholder="姓名" clearable />
+						<el-select
+							v-model="form.designLeadAccount"
+							:disabled="isView"
+							filterable
+							remote
+							:remote-method="loadUserOptions"
+							placeholder="请选择负责人"
+							clearable
+							style="width: 100%"
+							@change="onDesignLeadChange"
+						>
+							<el-option
+								v-for="item in userOptions"
+								:key="item.value"
+								:label="item.label"
+								:value="item.value"
+							/>
+						</el-select>
 					</el-form-item>
 				</el-col>
 				<el-col :span="6">
 					<el-form-item label="负责人账号">
-						<el-input v-model="form.designLeadAccount" :disabled="isView" placeholder="可选" clearable />
+						<el-input :model-value="form.designLeadAccount" disabled placeholder="自动填入" />
 					</el-form-item>
 				</el-col>
 				<el-col :span="6">
@@ -67,6 +100,19 @@
 						/>
 					</el-form-item>
 				</el-col>
+				<el-col :span="6">
+					<el-form-item label="图纸设计周期(h)">
+						<el-input-number
+							v-model="form.drawingDesignCycle"
+							:min="0"
+							:step="1"
+							:disabled="isView"
+							placeholder="小时"
+							style="width: 100%"
+							controls-position="right"
+						/>
+					</el-form-item>
+				</el-col>
 				<el-col :span="6">
 					<el-form-item label="实际开始">
 						<el-date-picker
@@ -112,16 +158,18 @@
 			<div class="pd-section-main">
 				<div class="cr-info-table">
 					<div class="cr-row">
-						<div class="cr-label">序号</div>
-						<div class="cr-value"><el-input model-value="1" disabled /></div>
+						<div class="cr-label">产品编码</div>
+						<div class="cr-value pd-with-suffix">
+							<el-input v-model="form.itemNum" :disabled="isView" placeholder="产品编码" clearable />
+							<el-button v-if="!isView" :icon="Search" @click="openItemPick('main')" />
+						</div>
 						<div class="cr-label">产品型号</div>
 						<div class="cr-value">
 							<el-input v-model="form.productModel" :disabled="isView" placeholder="产品型号" />
 						</div>
-						<div class="cr-label">物料编码</div>
-						<div class="cr-value pd-with-suffix">
-							<el-input v-model="form.itemNum" :disabled="isView" placeholder="物料编码" clearable />
-							<el-button v-if="!isView" :icon="Search" @click="openItemPick('main')" />
+						<div class="cr-label">产品名称</div>
+						<div class="cr-value">
+							<el-input v-model="form.productName" :disabled="isView" placeholder="产品名称" />
 						</div>
 					</div>
 					<div class="cr-row">
@@ -146,7 +194,7 @@
 			<div class="pd-vside">物料</div>
 			<div class="pd-section-main">
 				<div class="pd-subhead">
-					<span class="pd-subtitle">标准 BOM</span>
+					<span class="pd-subtitle">制造 BOM</span>
 					<el-button v-if="!isView" type="primary" size="small" @click="addRootBom">
 						<el-icon><Plus /></el-icon> 新增根级
 					</el-button>
@@ -187,6 +235,19 @@
 							<el-input v-model="row.processCode" size="small" :disabled="isView" placeholder="工序" />
 						</template>
 					</el-table-column>
+					<el-table-column label="单位用量" width="110">
+						<template #default="{ row }">
+							<el-input-number
+								v-model="row.qty"
+								size="small"
+								:disabled="isView"
+								:controls="true"
+								:precision="5"
+								:placeholder="'用量'"
+								style="width: 100%"
+							/>
+						</template>
+					</el-table-column>
 					<el-table-column label="固定损耗数量" width="110">
 						<template #default="{ row }">
 							<el-input-number
@@ -273,8 +334,12 @@ import type { ItemRow } from '../api/universalSelector';
 import {
 	fetchProductDesignDetail,
 	saveProductDesign,
+	fetchBomAndRouting,
+	fetchContractOptions,
+	fetchUserOptions,
 	type ProductDesignBomLine,
 	type ProductDesignRoutingLine,
+	type DropdownOption,
 } from '../api/productDesign';
 
 const props = withDefaults(
@@ -308,10 +373,12 @@ const form = reactive({
 	drawingPlanEnd: '' as string | undefined,
 	drawingActualStart: '' as string | undefined,
 	drawingActualEnd: '' as string | undefined,
+	drawingDesignCycle: undefined as number | undefined,
 	applicant: '',
 	applyDate: '' as string | undefined,
 	productModel: '',
 	itemNum: '',
+	productName: '',
 	language: 'zh',
 	lineRemark: '',
 	boms: [] as ProductDesignBomLine[],
@@ -372,6 +439,46 @@ function bomRowTreeDepth(row: ProductDesignBomLine): number {
 const itemDlgVisible = ref(false);
 const pickTarget = ref<'main' | ProductDesignBomLine>('main');
 
+// ── 合同下拉 ──
+const contractOptions = ref<DropdownOption[]>([]);
+
+async function loadContractOptions(keyword: string) {
+	try {
+		contractOptions.value = await fetchContractOptions(keyword || undefined);
+	} catch {
+		contractOptions.value = [];
+	}
+}
+
+// 打开下拉时预载(keyword 为空)
+loadContractOptions('');
+
+// ── 用户下拉 ──
+const userOptions = ref<DropdownOption[]>([]);
+
+async function loadUserOptions(keyword: string) {
+	try {
+		userOptions.value = await fetchUserOptions(keyword || undefined);
+	} catch {
+		userOptions.value = [];
+	}
+}
+
+// 打开下拉时预载
+loadUserOptions('');
+
+/** 选择用户后自动填入姓名 */
+function onDesignLeadChange(account: string) {
+	const opt = userOptions.value.find((u) => u.value === account);
+	if (opt) {
+		// label 格式: "RealName (Account)"
+		const match = opt.label.match(/^(.+?)\s*\(/);
+		form.designLeadName = match ? match[1] : opt.label;
+	} else {
+		form.designLeadName = '';
+	}
+}
+
 function emptyBomLine(id: string, parentBomId: string | null | undefined): ProductDesignBomLine {
 	return {
 		id: bomRowIdKey(id),
@@ -379,6 +486,7 @@ function emptyBomLine(id: string, parentBomId: string | null | undefined): Produ
 		itemNum: '',
 		itemName: '',
 		processCode: '',
+		qty: undefined,
 		fixedLossQty: undefined,
 		batchNo: '',
 	};
@@ -445,16 +553,83 @@ function openItemPick(target: 'main' | ProductDesignBomLine) {
 	itemDlgVisible.value = true;
 }
 
-function onItemPicked(v: ItemRow) {
+async function onItemPicked(v: ItemRow) {
 	const num = v.itemNum ?? '';
 	const name = v.descr ?? '';
 	if (pickTarget.value === 'main') {
 		form.itemNum = num;
-	} else if (pickTarget.value !== 'main') {
+		form.productName = name;
+		itemDlgVisible.value = false;
+		if (num) await loadBomAndRouting(num);
+	} else {
 		pickTarget.value.itemNum = num;
 		pickTarget.value.itemName = name;
+		itemDlgVisible.value = false;
+	}
+}
+
+async function loadBomAndRouting(itemNum: string) {
+	try {
+		const result = await fetchBomAndRouting(itemNum);
+
+		// 构建 BOM 树:扁平行 → 父子层级结构
+		const bomMap = new Map<string, { id: string; itemNum: string }>();
+		const newBoms: ProductDesignBomLine[] = [];
+		let seq = 0;
+
+		for (const row of result.boms) {
+			seq++;
+			const tempId = String(nextBomTempId());
+			const itemNumVal = row.itemNum ?? '';
+			const parentItemVal = row.parentItem ?? '';
+
+			// 父行 ID:查找 parentItem 对应行的临时 ID(仅存首次出现,保证引用根层)
+			let parentBomId: string | null = null;
+			if (parentItemVal && parentItemVal !== itemNum) {
+				const parentEntry = bomMap.get(parentItemVal);
+				parentBomId = parentEntry?.id ?? null;
+			}
+
+			const bomLine: ProductDesignBomLine = {
+				id: tempId,
+				parentBomId,
+				seq,
+				itemNum: itemNumVal,
+				itemName: row.itemName ?? '',
+				processCode: row.op ?? '',
+				qty: row.qty ?? undefined,
+				fixedLossQty: row.qtyConsumed ?? undefined,
+				batchNo: '',
+			};
+
+			newBoms.push(bomLine);
+			// 仅存首次出现,保证子行引用到最浅层父节点
+			if (!bomMap.has(itemNumVal)) {
+				bomMap.set(itemNumVal, { id: tempId, itemNum: itemNumVal });
+			}
+		}
+
+		form.boms = newBoms;
+
+		// 设置图纸设计周期
+		if (result.drawingDesignCycle != null) {
+			form.drawingDesignCycle = result.drawingDesignCycle;
+		}
+
+		// 构建工艺路线
+		let routingSeq = 0;
+		form.routings = result.routings.map((r) => ({
+			id: undefined,
+			seq: ++routingSeq,
+			opName: r.descr ?? '',
+			opCode: r.op != null ? String(r.op) : '',
+			isKeyProcess: r.milestoneOp === '1' ? 1 : undefined,
+			productionLine: '',
+			routeCode: '',
+		}));
+	} catch (e) {
+		console.error('获取BOM/工艺失败:', e);
 	}
-	itemDlgVisible.value = false;
 }
 
 function resetFormForCreate() {
@@ -469,10 +644,12 @@ function resetFormForCreate() {
 	form.drawingPlanEnd = undefined;
 	form.drawingActualStart = undefined;
 	form.drawingActualEnd = undefined;
+	form.drawingDesignCycle = undefined;
 	form.applicant = userInfo.userInfos?.realName || userInfo.userInfos?.account || '管理员';
 	form.applyDate = new Date().toISOString().slice(0, 10);
 	form.productModel = '';
 	form.itemNum = '';
+	form.productName = '';
 	form.language = 'zh';
 	form.lineRemark = '';
 	form.boms = [];
@@ -501,10 +678,12 @@ async function loadDetail() {
 		form.drawingPlanEnd = d.drawingPlanEnd ?? undefined;
 		form.drawingActualStart = d.drawingActualStart ?? undefined;
 		form.drawingActualEnd = d.drawingActualEnd ?? undefined;
+		form.drawingDesignCycle = d.drawingDesignCycle ?? undefined;
 		form.applicant = d.applicant ?? '';
 		form.applyDate = d.applyDate ?? undefined;
 		form.productModel = d.productModel ?? '';
 		form.itemNum = d.itemNum ?? '';
+		form.productName = d.productName ?? '';
 		form.language = d.language || 'zh';
 		form.lineRemark = d.lineRemark ?? '';
 		const bomRows = Array.isArray(d.boms) ? d.boms : [];
@@ -517,6 +696,7 @@ async function loadDetail() {
 				itemNum: b.itemNum ?? '',
 				itemName: b.itemName ?? '',
 				processCode: b.processCode ?? '',
+				qty: b.qty ?? undefined,
 				fixedLossQty: b.fixedLossQty ?? undefined,
 				batchNo: b.batchNo ?? '',
 			};
@@ -556,11 +736,27 @@ onMounted(() => {
 	if (props.mode === 'create' && !props.designId) resetFormForCreate();
 });
 
+// 图纸设计周期变化时,自动刷新图纸计划结束时间
+watch(
+	() => [form.drawingPlanStart, form.drawingDesignCycle] as const,
+	([planStart, cycle]) => {
+		if (planStart && cycle != null && cycle > 0) {
+			const d = new Date(planStart);
+			if (!isNaN(d.getTime())) {
+				d.setHours(d.getHours() + cycle);
+				// 格式化为 YYYY-MM-DD HH:mm:ss
+				const pad = (n: number) => String(n).padStart(2, '0');
+				form.drawingPlanEnd = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
+			}
+		}
+	}
+);
+
 async function onSave() {
 	saving.value = true;
 	try {
 		await saveProductDesign({
-			id: form.id,
+			id: form.id != null ? Number(form.id) : undefined,
 			contractNo: form.contractNo || undefined,
 			productKind: form.productKind,
 			designLeadAccount: form.designLeadAccount || undefined,
@@ -570,10 +766,12 @@ async function onSave() {
 			drawingPlanEnd: form.drawingPlanEnd || undefined,
 			drawingActualStart: form.drawingActualStart || undefined,
 			drawingActualEnd: form.drawingActualEnd || undefined,
+			drawingDesignCycle: form.drawingDesignCycle,
 			applicant: form.applicant || undefined,
 			applyDate: form.applyDate || undefined,
 			productModel: form.productModel || undefined,
 			itemNum: form.itemNum || undefined,
+			productName: form.productName || undefined,
 			language: form.language || undefined,
 			lineRemark: form.lineRemark || undefined,
 			boms: form.boms,

+ 11 - 0
Web/src/views/aidop/business/salesOrderChangeForm.vue

@@ -66,6 +66,16 @@
 			</el-table>
 		</el-form>
 
+		<!-- 审批流程面板 -->
+		<ApprovalPanel
+			v-if="form.seOrderId"
+			bizType="ORDER_REVIEW"
+			:bizId="form.seOrderId"
+			:bizNo="form.billNo"
+			:title="`订单评审-${form.billNo}`"
+			@refresh="loadDetail"
+		/>
+
 		<div class="footer">
 			<el-button @click="$emit('cancel')">关闭</el-button>
 			<el-button :loading="saving" @click="onSave">保存</el-button>
@@ -77,6 +87,7 @@
 <script setup lang="ts">
 import { computed, onMounted, reactive, ref } from 'vue';
 import { ElMessage, FormInstance, FormRules } from 'element-plus';
+import ApprovalPanel from '/@/views/approvalFlow/component/ApprovalPanel.vue';
 import { createSeOrderChange, fetchSeOrderDetail, type SeOrderChangeSave } from '../api/seOrderReview';
 
 const props = defineProps<{ orderId: number | null }>();

+ 2 - 13
Web/src/views/aidop/business/salesOrderForm.vue

@@ -4,7 +4,7 @@
 			<el-row :gutter="16">
 				<el-col :span="12">
 					<el-form-item label="订单编号" prop="billNo">
-						<el-input v-model="form.billNo" :disabled="isView" />
+						<el-input v-model="form.billNo" :readonly="isCreate" :disabled="isView" :placeholder="isCreate ? '保存后自动生成' : ''" />
 					</el-form-item>
 				</el-col>
 				<el-col :span="12">
@@ -118,16 +118,6 @@
 			</el-table>
 		</el-form>
 
-		<!-- 审批流程面板 -->
-		<ApprovalPanel
-			v-if="form.id"
-			bizType="ORDER_REVIEW"
-			:bizId="form.id"
-			:bizNo="form.billNo"
-			:title="`订单评审-${form.billNo}`"
-			@refresh="loadDetail"
-		/>
-
 		<div class="footer">
 			<el-button @click="$emit('cancel')">取消</el-button>
 			<el-button type="primary" :disabled="isView" :loading="saving" @click="onSave">保存</el-button>
@@ -148,7 +138,6 @@ import { computed, onMounted, reactive, ref } from 'vue';
 import { ElMessage, FormInstance, FormRules } from 'element-plus';
 import SelectCustomer from './selectCustomer.vue';
 import SelectItem from './selectItem.vue';
-import ApprovalPanel from '/@/views/approvalFlow/component/ApprovalPanel.vue';
 import { fetchSeOrderDetail, saveSeOrder, type SeOrderEntry, type SeOrderUpsert } from '../api/seOrderReview';
 import { type CustomerRow, type ItemRow } from '../api/universalSelector';
 
@@ -163,6 +152,7 @@ const emit = defineEmits<{
 }>();
 
 const isView = computed(() => props.mode === 'view');
+const isCreate = computed(() => props.mode === 'create');
 const loading = ref(false);
 const saving = ref(false);
 const formRef = ref<FormInstance>();
@@ -184,7 +174,6 @@ const form = reactive<SeOrderUpsert>({
 });
 
 const rules = computed<FormRules>(() => ({
-	billNo: [{ required: true, message: '必填', trigger: 'blur' }],
 	orderType: [{ required: true, message: '必填', trigger: 'change' }],
 	customName: [{ required: true, message: '必填', trigger: 'change' }],
 }));

+ 16 - 4
Web/src/views/approvalFlow/component/LogicFlow/Property/PropertyCommon.vue

@@ -244,6 +244,7 @@
 
 <script setup lang="ts">
 import { reactive, ref, computed, watch } from 'vue';
+import { ElMessage } from 'element-plus';
 import { getAPI } from '/@/utils/axios-utils';
 import { SysUserApi, SysRoleApi, SysOrgApi } from '/@/api-services/api';
 
@@ -415,13 +416,18 @@ watch(
 	}
 );
 
+// 根据已保存的 ids/names 构建用户选项(names 来自已持久化的 approverNames,无需额外 API)
+const fetchUsersByIds = (ids: number[], names: string[]): Array<{ id: number; account: string; realName: string }> => {
+	return ids.map((id, i) => ({ id, account: '', realName: names[i] || String(id) }));
+};
+
 // 节点数据变化时回显
 const restoreSelection = async () => {
 	const ids = formData.approverIds ? formData.approverIds.split(',').map(Number).filter(Boolean) : [];
 	const names = formData.approverNames ? formData.approverNames.split(',') : [];
 
 	if (formData.approverType === 'SpecificUser' && ids.length) {
-		userOptions.value = ids.map((id, i) => ({ id, account: '', realName: names[i] || String(id) }));
+		userOptions.value = fetchUsersByIds(ids, names);
 		selectedUserIds.value = ids;
 	} else if (formData.approverType === 'Role' && ids.length) {
 		await loadRoles();
@@ -437,9 +443,10 @@ const restoreEscalationSelection = async () => {
 	const names = formData.escalationApproverNames ? formData.escalationApproverNames.split(',') : [];
 
 	if (formData.escalationApproverType === 'SpecificUser' && ids.length) {
-		for (const [i, id] of ids.entries()) {
-			if (!userOptions.value.some((o) => o.id === id)) {
-				userOptions.value.push({ id, account: '', realName: names[i] || String(id) });
+		const fetched = fetchUsersByIds(ids, names);
+		for (const u of fetched) {
+			if (!userOptions.value.some((o) => o.id === u.id)) {
+				userOptions.value.push(u);
 			}
 		}
 		escSelectedUserIds.value = ids;
@@ -482,6 +489,11 @@ const addCondition = () => {
 const saveProperties = () => {
 	const data: Record<string, any> = {};
 	if (isUserTask.value) {
+		const needsApprover = ['SpecificUser', 'Role', 'Department'].includes(formData.approverType);
+		if (needsApprover && !formData.approverIds) {
+			ElMessage.warning('请选择审批人');
+			return;
+		}
 		data.nodeName = formData.nodeName;
 		data.approverType = formData.approverType;
 		data.approverIds = formData.approverIds;

+ 6 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Manufacturing/AdoS0ProductStructure.cs

@@ -22,6 +22,12 @@ public class AdoS0ProductStructureMaster : ITenantIdFilter
     [SugarColumn(ColumnDescription = "子项物料", ColumnDataType = "bigint")]
     public long ComponentMaterialId { get; set; }
 
+    [SugarColumn(ColumnDescription = "父项物料编码", Length = 50)]
+    public string ParentItem { get; set; }
+
+    [SugarColumn(ColumnDescription = "子项物料编码", Length = 50)]
+    public string ComponentItem { get; set; }
+
     [SugarColumn(ColumnName = "Qty", ColumnDescription = "标准用量", DecimalDigits = 5, Length = 18)]
     public decimal Qty { get; set; } = 1m;
 

+ 35 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Order/Dto/ProductDesignDto.cs

@@ -20,6 +20,7 @@ public class ProductDesignBomInput
     public string? ItemNum { get; set; }
     public string? ItemName { get; set; }
     public string? ProcessCode { get; set; }
+    public decimal? Qty { get; set; }
     public decimal? FixedLossQty { get; set; }
     public string? BatchNo { get; set; }
 }
@@ -48,10 +49,13 @@ public class ProductDesignSaveInput
     public string? DrawingPlanEnd { get; set; }
     public string? DrawingActualStart { get; set; }
     public string? DrawingActualEnd { get; set; }
+    /// <summary>图纸设计周期(小时)</summary>
+    public int? DrawingDesignCycle { get; set; }
     public string? Applicant { get; set; }
     public string? ApplyDate { get; set; }
     public string? ProductModel { get; set; }
     public string? ItemNum { get; set; }
+    public string? ProductName { get; set; }
     public string? Language { get; set; }
     public string? LineRemark { get; set; }
     public List<ProductDesignBomInput> Boms { get; set; } = new();
@@ -63,3 +67,34 @@ public class ProductDesignDeleteInput
     [Required(ErrorMessage = "Id不能为空")]
     public long Id { get; set; }
 }
+
+/// <summary>BOM+工艺查询返回</summary>
+public class BomAndRoutingOutput
+{
+    public List<BomQueryRow> Boms { get; set; } = new();
+    public List<RoutingQueryRow> Routings { get; set; } = new();
+    /// <summary>图纸设计周期(小时),从 ItemMaster.drawing_design 查询</summary>
+    public int? DrawingDesignCycle { get; set; }
+}
+
+/// <summary>BOM CTE 查询行</summary>
+public class BomQueryRow
+{
+    public string? ParentItem { get; set; }
+    public string? ItemNum { get; set; }
+    public string? ItemName { get; set; }
+    public string? Op { get; set; }
+    public decimal? Qty { get; set; }
+    public string? StructureType { get; set; }
+    public string? EmtType { get; set; }
+    public decimal? QtyConsumed { get; set; }
+}
+
+/// <summary>工艺路线查询行</summary>
+public class RoutingQueryRow
+{
+    public string? Descr { get; set; }
+    public int? Op { get; set; }
+    public int? ParentOp { get; set; }
+    public string? MilestoneOp { get; set; }
+}

+ 1 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Order/Dto/SeOrderDto.cs

@@ -54,8 +54,7 @@ public class SeOrderSaveInput
 {
     public long? Id { get; set; }
 
-    [Required(ErrorMessage = "订单编号不能为空")]
-    public string BillNo { get; set; } = string.Empty;
+    public string? BillNo { get; set; }
 
     public int OrderType { get; set; } = 2;
     public long? CustomId { get; set; }

+ 7 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Order/Entity/ProductDesign.cs

@@ -35,6 +35,10 @@ public class ProductDesign
     [SugarColumn(ColumnName = "DrawingPlanEnd", IsNullable = true)]
     public DateTime? DrawingPlanEnd { get; set; }
 
+    /// <summary>图纸设计周期(小时)</summary>
+    [SugarColumn(ColumnName = "DrawingDesignCycle", IsNullable = true)]
+    public int? DrawingDesignCycle { get; set; }
+
     [SugarColumn(ColumnName = "DrawingActualStart", IsNullable = true)]
     public DateTime? DrawingActualStart { get; set; }
 
@@ -53,6 +57,9 @@ public class ProductDesign
     [SugarColumn(ColumnName = "ItemNum", Length = 64, IsNullable = true)]
     public string? ItemNum { get; set; }
 
+    [SugarColumn(ColumnName = "ProductName", Length = 256, IsNullable = true)]
+    public string? ProductName { get; set; }
+
     /// <summary>zh / en</summary>
     [SugarColumn(ColumnName = "Language", Length = 16, IsNullable = true)]
     public string? Language { get; set; }

+ 4 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Order/Entity/SeOrder.cs

@@ -73,4 +73,8 @@ public class SeOrder
 
     [SugarColumn(ColumnName = "factory_id")]
     public long? FactoryId { get; set; }
+
+    /// <summary>0=未关闭 1=已关闭</summary>
+    [SugarColumn(ColumnName = "closed")]
+    public int? Closed { get; set; }
 }

+ 100 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Order/ProductDesignService.cs

@@ -1,4 +1,5 @@
 using Yitter.IdGenerator;
+using Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
 
 namespace Admin.NET.Plugin.AiDOP.Order;
 
@@ -57,10 +58,12 @@ public class ProductDesignService : IDynamicApiController, ITransient
             drawingPlanEnd = u.DrawingPlanEnd?.ToString("yyyy-MM-dd HH:mm"),
             drawingActualStart = u.DrawingActualStart?.ToString("yyyy-MM-dd HH:mm"),
             drawingActualEnd = u.DrawingActualEnd?.ToString("yyyy-MM-dd HH:mm"),
+            drawingDesignCycle = u.DrawingDesignCycle,
             applicant = u.Applicant,
             applyDate = u.ApplyDate?.ToString("yyyy-MM-dd"),
             productModel = u.ProductModel,
             itemNum = u.ItemNum,
+            productName = u.ProductName,
             createTime = u.CreateTime?.ToString("yyyy-MM-dd HH:mm:ss"),
             updateTime = u.UpdateTime?.ToString("yyyy-MM-dd HH:mm:ss"),
         }).ToList();
@@ -91,10 +94,12 @@ public class ProductDesignService : IDynamicApiController, ITransient
             drawingPlanEnd = m.DrawingPlanEnd?.ToString("yyyy-MM-dd HH:mm:ss"),
             drawingActualStart = m.DrawingActualStart?.ToString("yyyy-MM-dd HH:mm:ss"),
             drawingActualEnd = m.DrawingActualEnd?.ToString("yyyy-MM-dd HH:mm:ss"),
+            drawingDesignCycle = m.DrawingDesignCycle,
             applicant = m.Applicant,
             applyDate = m.ApplyDate?.ToString("yyyy-MM-dd"),
             productModel = m.ProductModel,
             itemNum = m.ItemNum,
+            productName = m.ProductName,
             language = m.Language,
             lineRemark = m.LineRemark,
             createUser = m.CreateUser,
@@ -200,6 +205,54 @@ public class ProductDesignService : IDynamicApiController, ITransient
         }
     }
 
+    [DisplayName("根据产品编码获取BOM和工艺")]
+    [HttpGet("productdesign/bom-routing")]
+    public async Task<BomAndRoutingOutput> GetBomAndRouting([FromQuery] string itemNum)
+    {
+        if (string.IsNullOrWhiteSpace(itemNum))
+            return new BomAndRoutingOutput();
+
+        // 查询图纸设计周期
+        var drawingDesignCycle = await _db.Queryable<AdoS0ItemMaster>()
+            .Where(x => x.ItemNum == itemNum && x.IsActive == true)
+            .Select(x => x.DrawingDesign)
+            .FirstAsync();
+
+        var bomSql = @"
+WITH RECURSIVE temp(ParentItem,ComponentItem,op,qty,StructureType,QtyConsumed) AS (
+    SELECT ParentItem,ComponentItem,op,qty,StructureType,QtyConsumed
+    FROM ProductStructureMaster WHERE ParentItem=@itemNum
+    UNION ALL
+    SELECT c.ParentItem,c.ComponentItem,c.op,
+           CAST(c.qty+(c.qty*(c.Scrap+c.QtyExchd)/100) AS DECIMAL(15,8)) AS qty,
+           c.StructureType,c.QtyConsumed
+    FROM ProductStructureMaster c
+    INNER JOIN temp p ON c.ParentItem=p.ComponentItem
+    INNER JOIN ItemMaster parentIm ON c.ParentItem=parentIm.ItemNum AND parentIm.PurMfg<>'P'
+)
+SELECT psm.ParentItem,psm.ComponentItem AS ItemNum,im.Descr AS ItemName,
+       CASE WHEN IFNULL(pso.Op,0)=0 THEN psm.Op ELSE pso.Op END AS Op,
+       psm.qty AS Qty,psm.StructureType,im.EMTType AS EmtType,psm.QtyConsumed
+FROM temp psm
+LEFT JOIN ItemMaster im ON psm.ComponentItem=im.ItemNum
+LEFT JOIN ProductStructureOp pso ON pso.ParentItem=psm.ParentItem
+    AND pso.ComponentItem=psm.ComponentItem AND pso.ProductItem=@itemNum";
+
+        var routingSql = @"
+SELECT Descr,Op,ParentOp,CAST(MilestoneOp AS CHAR(5)) AS MilestoneOp
+FROM RoutingOpDetail WHERE RoutingCode=@itemNum";
+
+        var boms = await _db.Ado.SqlQueryAsync<BomQueryRow>(bomSql, new { itemNum });
+        var routings = await _db.Ado.SqlQueryAsync<RoutingQueryRow>(routingSql, new { itemNum });
+
+        return new BomAndRoutingOutput
+        {
+            Boms = boms,
+            Routings = routings,
+            DrawingDesignCycle = drawingDesignCycle
+        };
+    }
+
     private static ProductDesign MapMaster(ProductDesign entity, ProductDesignSaveInput input, string user, DateTime now, bool isNew)
     { 
         entity.ContractNo = string.IsNullOrWhiteSpace(input.ContractNo) ? null : input.ContractNo.Trim();
@@ -211,10 +264,12 @@ public class ProductDesignService : IDynamicApiController, ITransient
         entity.DrawingPlanEnd = ParseOptionalDate(input.DrawingPlanEnd);
         entity.DrawingActualStart = ParseOptionalDate(input.DrawingActualStart);
         entity.DrawingActualEnd = ParseOptionalDate(input.DrawingActualEnd);
+        entity.DrawingDesignCycle = input.DrawingDesignCycle;
         entity.Applicant = string.IsNullOrWhiteSpace(input.Applicant) ? null : input.Applicant.Trim();
         entity.ApplyDate = ParseOptionalDate(input.ApplyDate);
         entity.ProductModel = string.IsNullOrWhiteSpace(input.ProductModel) ? null : input.ProductModel.Trim();
         entity.ItemNum = string.IsNullOrWhiteSpace(input.ItemNum) ? null : input.ItemNum.Trim();
+        entity.ProductName = string.IsNullOrWhiteSpace(input.ProductName) ? null : input.ProductName.Trim();
         entity.Language = string.IsNullOrWhiteSpace(input.Language) ? null : input.Language.Trim();
         entity.Qty = null;
         entity.LineRemark = string.IsNullOrWhiteSpace(input.LineRemark) ? null : input.LineRemark.Trim();
@@ -268,7 +323,7 @@ public class ProductDesignService : IDynamicApiController, ITransient
                 existing.ItemNum = string.IsNullOrWhiteSpace(d.ItemNum) ? null : d.ItemNum.Trim();
                 existing.ItemName = string.IsNullOrWhiteSpace(d.ItemName) ? null : d.ItemName.Trim();
                 existing.ProcessCode = string.IsNullOrWhiteSpace(d.ProcessCode) ? null : d.ProcessCode.Trim();
-                existing.Qty = null;
+                existing.Qty = d.Qty;
                 existing.FixedLossQty = d.FixedLossQty;
                 existing.BatchNo = string.IsNullOrWhiteSpace(d.BatchNo) ? null : d.BatchNo.Trim();
                 await _bomRep.UpdateAsync(existing);
@@ -287,7 +342,7 @@ public class ProductDesignService : IDynamicApiController, ITransient
                     ItemNum = string.IsNullOrWhiteSpace(d.ItemNum) ? null : d.ItemNum.Trim(),
                     ItemName = string.IsNullOrWhiteSpace(d.ItemName) ? null : d.ItemName.Trim(),
                     ProcessCode = string.IsNullOrWhiteSpace(d.ProcessCode) ? null : d.ProcessCode.Trim(),
-                    Qty = null,
+                    Qty = d.Qty,
                     FixedLossQty = d.FixedLossQty,
                     BatchNo = string.IsNullOrWhiteSpace(d.BatchNo) ? null : d.BatchNo.Trim(),
                 };
@@ -394,4 +449,47 @@ public class ProductDesignService : IDynamicApiController, ITransient
         foreach (var toDelete in dbRows.Where(u => !inputIds.Contains(u.Id)))
             await _routingRep.DeleteAsync(u => u.Id == toDelete.Id);
     }
+
+    /// <summary>获取合同下拉选项</summary>
+    [DisplayName("获取合同下拉选项")]
+    [HttpGet("productdesign/contract-options")]
+    public async Task<List<object>> GetContractOptions([FromQuery] string? keyword)
+    {
+        var tenantId = _userManager.TenantId;
+        var sql = string.IsNullOrWhiteSpace(keyword)
+            ? "SELECT BillNo, CONCAT(BillNo, ' | ', IFNULL(Title,'')) AS Label FROM ado_contract_review WHERE tenant_id = @tenantId ORDER BY RecID DESC LIMIT 50"
+            : "SELECT BillNo, CONCAT(BillNo, ' | ', IFNULL(Title,'')) AS Label FROM ado_contract_review WHERE tenant_id = @tenantId AND (BillNo LIKE @kw OR Title LIKE @kw) ORDER BY RecID DESC LIMIT 50";
+
+        var rows = await _db.Ado.SqlQueryAsync<ContractOptionRow>(sql,
+            new { tenantId, kw = string.IsNullOrWhiteSpace(keyword) ? null : $"%{keyword.Trim()}%" });
+        return rows.Select(r => (object)new { value = r.BillNo, label = r.Label }).ToList();
+    }
+
+    /// <summary>获取用户下拉选项</summary>
+    [DisplayName("获取用户下拉选项")]
+    [HttpGet("productdesign/user-options")]
+    public async Task<List<object>> GetUserOptions([FromQuery] string? keyword)
+    {
+        var tenantId = _userManager.TenantId;
+        var sql = string.IsNullOrWhiteSpace(keyword)
+            ? "SELECT Id, Account, RealName FROM SysUser WHERE TenantId = @tenantId AND Status = 1 ORDER BY OrderNo LIMIT 200"
+            : "SELECT Id, Account, RealName FROM SysUser WHERE TenantId = @tenantId AND Status = 1 AND (Account LIKE @kw OR RealName LIKE @kw) ORDER BY OrderNo LIMIT 200";
+
+        var rows = await _db.Ado.SqlQueryAsync<UserOptionRow>(sql,
+            new { tenantId, kw = string.IsNullOrWhiteSpace(keyword) ? null : $"%{keyword.Trim()}%" });
+        return rows.Select(r => (object)new { value = r.Account, label = $"{r.RealName} ({r.Account})" }).ToList();
+    }
+
+    private sealed class ContractOptionRow
+    {
+        public string BillNo { get; set; } = string.Empty;
+        public string Label { get; set; } = string.Empty;
+    }
+
+    private sealed class UserOptionRow
+    {
+        public long Id { get; set; }
+        public string Account { get; set; } = string.Empty;
+        public string RealName { get; set; } = string.Empty;
+    }
 }

+ 38 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Order/SeOrderService.cs

@@ -241,12 +241,20 @@ public class SeOrderService : IDynamicApiController, ITransient
         {
             // ── 新增:参照 SysJobService.AddJobDetail ──
             var tenantId = _userManager.TenantId;
+
+            // 如果未填写订单编号,自动生成:SO + yyyyMMdd + 4位流水号
+            if (string.IsNullOrWhiteSpace(input.BillNo))
+            {
+                input.BillNo = await GenerateBillNoAsync(tenantId);
+            }
+
             var isExist = await _seOrderRep.IsAnyAsync(u => u.BillNo == input.BillNo && u.IsDeleted == 0 && u.TenantId == tenantId);
             if (isExist) throw Oops.Oh("订单编号已存在");
 
             var entity = input.Adapt<SeOrder>();
             entity.Id = YitIdHelper.NextId();
             entity.IsDeleted = 0;
+            entity.Closed = 0;
             entity.CreateTime = DateTime.Now;
             entity.TenantId = tenantId;
             await _seOrderRep.InsertAsync(entity);
@@ -361,6 +369,36 @@ public class SeOrderService : IDynamicApiController, ITransient
         }
     }
 
+    /// <summary>
+    /// 生成订单编号:SO + yyyyMMdd + 4位流水号,例如 SO202605210001
+    /// </summary>
+    private async Task<string> GenerateBillNoAsync(long tenantId)
+    {
+        var today = DateTime.Now.ToString("yyyyMMdd");
+        var prefix = $"SO{today}";
+
+        // 查询当天已有的最大订单编号
+        var maxBillNo = await _db.Queryable<SeOrder>()
+            .Where(u => u.BillNo!.StartsWith(prefix) && u.IsDeleted == 0 && u.TenantId == tenantId)
+            .OrderBy(u => u.BillNo, OrderByType.Desc)
+            .Select(u => u.BillNo)
+            .FirstAsync();
+
+        if (string.IsNullOrEmpty(maxBillNo))
+        {
+            return $"{prefix}0001";
+        }
+
+        // 取后4位转为数字并加1,不足4位补零
+        var last4 = maxBillNo.Length >= 4 ? maxBillNo[^4..] : maxBillNo;
+        if (int.TryParse(last4, out var serial))
+        {
+            return $"{prefix}{(serial + 1).ToString().PadLeft(4, '0')}";
+        }
+
+        return $"{prefix}0001";
+    }
+
     // ══════════════════════════════════════════════════════════════
     // 订单评审 / 交期确认 — 已改为前端直接调用外部接口,此处仅保留存根
     // ══════════════════════════════════════════════════════════════