Przeglądaj źródła

feat(aidop): add S3 delivery schedule and fix CS0854 update action

Implement S3 material delivery schedule frontend/backend/menu integration and
resolve expression tree optional-argument compile failure in smart ops update.

Made-with: Cursor
Pengxy 2 tygodni temu
rodzic
commit
e88adafc7f

+ 1 - 1
Web/package.json

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

+ 70 - 0
Web/src/views/aidop/s3/api/deliverySchedule.ts

@@ -0,0 +1,70 @@
+import service from '/@/utils/request';
+
+export interface Paged<T> {
+	total: number;
+	page: number;
+	pageSize: number;
+	list: T[];
+}
+
+export interface DeliveryScheduleRow {
+	id: number;
+	dsNum?: string | null;
+	status?: string | null;
+	itemNum?: string | null;
+	descr?: string | null;
+	um?: string | null;
+	purGroup?: string | null;
+	supplierCode?: string | null;
+	supplier?: string | null;
+	requestDate?: string | null;
+	needDate?: string | null;
+	schedQty?: number | null;
+	sentQty?: number | null;
+	restQty?: number | null;
+	lastSentDate?: string | null;
+	lastSentQty?: number | null;
+	submitDate?: string | null;
+	poNumber?: string | null;
+	poLine?: number | null;
+}
+
+export interface PurchaseOrderRow {
+	purOrd?: string | null;
+	line?: number | null;
+	itemNum?: string | null;
+	um?: string | null;
+	supp?: string | null;
+	name?: string | null;
+	dueDate?: string | null;
+	qtyOrded?: number | null;
+	buyer?: string | null;
+}
+
+export function fetchDeliveryScheduleList(params: Record<string, unknown>) {
+	return service.get<Paged<DeliveryScheduleRow>>('/api/Supply/delivery-schedule/list', { params }).then((r) => r.data);
+}
+
+export function publishDeliverySchedule(ids: string) {
+	return service.post('/api/Supply/delivery-schedule/publish', { ids }).then((r) => r.data);
+}
+
+export function unpublishDeliverySchedule(id: number) {
+	return service.post(`/api/Supply/delivery-schedule/unpublish/${id}`).then((r) => r.data);
+}
+
+export function cancelDeliverySchedule(id: number) {
+	return service.post(`/api/Supply/delivery-schedule/cancel/${id}`).then((r) => r.data);
+}
+
+export function generateDeliverySchedule() {
+	return service.post('/api/Supply/delivery-schedule/generate').then((r) => r.data);
+}
+
+export function batchGenerateDeliverySchedule(poNumber: string) {
+	return service.post('/api/Supply/delivery-schedule/batch-generate', { poNumber }).then((r) => r.data);
+}
+
+export function fetchPurchaseOrderPage(params: Record<string, unknown>) {
+	return service.get<Paged<PurchaseOrderRow>>('/api/Supply/delivery-schedule/purchase-order/page', { params }).then((r) => r.data);
+}

+ 80 - 0
Web/src/views/aidop/s3/supply/deliveryScheduleBatchForm.vue

@@ -0,0 +1,80 @@
+<template>
+	<div>
+		<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
+			<el-form-item label="采购单号" prop="poNumber">
+				<el-input v-model="form.poNumber" :disabled="true" placeholder="请选择采购单" />
+				<el-button class="ml8" @click="selectorVisible = true">选择</el-button>
+			</el-form-item>
+			<el-form-item label="物料编码">
+				<el-input v-model="form.itemNum" :disabled="true" />
+			</el-form-item>
+			<el-form-item label="供应商">
+				<el-input v-model="form.supplier" :disabled="true" />
+			</el-form-item>
+		</el-form>
+
+		<div class="footer">
+			<el-button @click="$emit('cancel')">取消</el-button>
+			<el-button type="primary" :loading="saving" @click="onSave">保存</el-button>
+		</div>
+
+		<el-dialog v-model="selectorVisible" title="选择采购单" width="980px" append-to-body destroy-on-close>
+			<SelectPurchaseOrder @picked="onPick" />
+		</el-dialog>
+	</div>
+</template>
+
+<script setup lang="ts" name="aidopS3SupplyDeliveryScheduleBatchForm">
+import { reactive, ref } from 'vue';
+import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
+import SelectPurchaseOrder from './selectPurchaseOrder.vue';
+import { batchGenerateDeliverySchedule, type PurchaseOrderRow } from '../api/deliverySchedule';
+
+const emit = defineEmits<{
+	(e: 'cancel'): void;
+	(e: 'saved'): void;
+}>();
+
+const formRef = ref<FormInstance>();
+const saving = ref(false);
+const selectorVisible = ref(false);
+const form = reactive({
+	poNumber: '',
+	itemNum: '',
+	supplier: '',
+});
+const rules: FormRules = {
+	poNumber: [{ required: true, message: '请选择采购单号', trigger: 'change' }],
+};
+
+function onPick(v: PurchaseOrderRow) {
+	form.poNumber = v.purOrd || '';
+	form.itemNum = v.itemNum || '';
+	form.supplier = v.name || v.supp || '';
+	selectorVisible.value = false;
+}
+
+async function onSave() {
+	await formRef.value?.validate();
+	saving.value = true;
+	try {
+		await batchGenerateDeliverySchedule(form.poNumber);
+		ElMessage.success('批量添加成功');
+		emit('saved');
+	} finally {
+		saving.value = false;
+	}
+}
+</script>
+
+<style scoped lang="scss">
+.footer {
+	margin-top: 14px;
+	display: flex;
+	justify-content: flex-end;
+	gap: 10px;
+}
+.ml8 {
+	margin-left: 8px;
+}
+</style>

+ 314 - 0
Web/src/views/aidop/s3/supply/deliveryScheduleList.vue

@@ -0,0 +1,314 @@
+<template>
+	<AidopDemoShell :title="pageTitle" subtitle="物料交货计划">
+		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
+			<el-form-item label="采购单号">
+				<el-input v-model="query.poNumber" clearable placeholder="模糊查询" style="width: 150px" />
+			</el-form-item>
+			<el-form-item label="交货单号">
+				<el-input v-model="query.dsNum" clearable placeholder="模糊查询" style="width: 150px" />
+			</el-form-item>
+			<el-form-item label="物料编码">
+				<el-input v-model="query.itemNum" clearable placeholder="模糊查询" style="width: 150px" />
+			</el-form-item>
+			<el-form-item label="供应商">
+				<el-input v-model="query.supplier" clearable placeholder="模糊查询" style="width: 150px" />
+			</el-form-item>
+			<el-form-item label="状态">
+				<el-select v-model="query.status" clearable style="width: 120px">
+					<el-option label="初始" value="N" />
+					<el-option label="已发布" value="P" />
+					<el-option label="关闭" value="C" />
+				</el-select>
+			</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="success" :loading="generating" @click="onGenerate">生成交货单</el-button>
+			<el-button type="primary" :disabled="!selectedPublishableIds.length" @click="onPublish">发布</el-button>
+			<el-button @click="onExport">导出</el-button>
+			<el-button type="warning" @click="openBatchAdd">批量添加</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" v-model="item.visible">{{ item.label }}</el-checkbox>
+			</el-popover>
+		</div>
+
+		<el-table :data="rows" v-loading="loading" border stripe style="width: 100%" @selection-change="onSelectionChange" @sort-change="onSortChange">
+			<el-table-column type="selection" width="44" fixed="left" />
+			<el-table-column v-if="col.dsNum" prop="dsNum" label="交货单号" width="140" fixed="left" sortable="custom" />
+			<el-table-column v-if="col.status" prop="status" label="状态" width="90" fixed="left" align="center" sortable="custom">
+				<template #default="{ row }">
+					<el-tag :type="statusTag(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
+				</template>
+			</el-table-column>
+			<el-table-column v-if="col.itemNum" prop="itemNum" label="物料编码" width="130" sortable="custom" />
+			<el-table-column v-if="col.descr" prop="descr" label="物料描述" min-width="140" show-overflow-tooltip />
+			<el-table-column v-if="col.purGroup" prop="purGroup" label="采购组" width="100" />
+			<el-table-column v-if="col.supplierCode" prop="supplierCode" label="供应商" width="120" />
+			<el-table-column v-if="col.supplier" prop="supplier" label="供应商名称" min-width="140" show-overflow-tooltip />
+			<el-table-column v-if="col.requestDate" prop="requestDate" label="交货日期" width="110" sortable="custom" />
+			<el-table-column v-if="col.needDate" prop="needDate" label="需求日期" width="110" sortable="custom" />
+			<el-table-column v-if="col.schedQty" prop="schedQty" label="需求数量" width="100" align="right" sortable="custom" />
+			<el-table-column v-if="col.sentQty" prop="sentQty" label="已交数量" width="100" align="right" />
+			<el-table-column v-if="col.restQty" prop="restQty" label="待交数量" width="100" align="right" />
+			<el-table-column v-if="col.lastSentDate" prop="lastSentDate" label="最近交货日期" width="120" />
+			<el-table-column v-if="col.lastSentQty" prop="lastSentQty" label="最近交货数量" width="120" align="right" />
+			<el-table-column v-if="col.submitDate" prop="submitDate" label="发布日期" width="110" sortable="custom" />
+			<el-table-column v-if="col.poNumber" prop="poNumber" label="采购单号" width="120" sortable="custom" />
+			<el-table-column v-if="col.poLine" prop="poLine" label="采购单行号" width="100" />
+			<el-table-column label="行操作" width="170" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="warning" :disabled="row.status !== 'P'" @click="onUnPublish(row)">取消发布</el-button>
+					<el-button link type="danger" :disabled="row.status === 'C'" @click="onCancel(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>
+
+		<el-dialog v-model="batchVisible" title="批量添加交货单" width="680px" destroy-on-close>
+			<DeliveryScheduleBatchForm @cancel="batchVisible = false" @saved="onBatchSaved" />
+		</el-dialog>
+	</AidopDemoShell>
+</template>
+
+<script setup lang="ts" name="aidopS3SupplyDeliveryScheduleList">
+import { computed, onMounted, reactive, ref } from 'vue';
+import { useRoute } from 'vue-router';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
+import DeliveryScheduleBatchForm from './deliveryScheduleBatchForm.vue';
+import {
+	cancelDeliverySchedule,
+	fetchDeliveryScheduleList,
+	generateDeliverySchedule,
+	publishDeliverySchedule,
+	type DeliveryScheduleRow,
+	unpublishDeliverySchedule,
+} from '../api/deliverySchedule';
+
+const route = useRoute();
+const pageTitle = computed(() => (route.meta?.title as string) || '物料交货计划');
+
+const query = reactive({
+	poNumber: '',
+	dsNum: '',
+	itemNum: '',
+	supplier: '',
+	status: '' as string,
+	page: 1,
+	pageSize: 10,
+	sortField: '',
+	sortOrder: '',
+});
+const loading = ref(false);
+const generating = ref(false);
+const rows = ref<DeliveryScheduleRow[]>([]);
+const total = ref(0);
+const selectedRows = ref<DeliveryScheduleRow[]>([]);
+const batchVisible = ref(false);
+
+const col = reactive({
+	dsNum: true,
+	status: true,
+	itemNum: true,
+	descr: true,
+	purGroup: true,
+	supplierCode: true,
+	supplier: true,
+	requestDate: true,
+	needDate: true,
+	schedQty: true,
+	sentQty: true,
+	restQty: true,
+	lastSentDate: true,
+	lastSentQty: true,
+	submitDate: true,
+	poNumber: true,
+	poLine: true,
+});
+
+const toggleItems = computed(() => [
+	{ key: 'dsNum', label: '交货单号', visible: col.dsNum },
+	{ key: 'status', label: '状态', visible: col.status },
+	{ key: 'itemNum', label: '物料编码', visible: col.itemNum },
+	{ key: 'descr', label: '物料描述', visible: col.descr },
+	{ key: 'purGroup', label: '采购组', visible: col.purGroup },
+	{ key: 'supplierCode', label: '供应商', visible: col.supplierCode },
+	{ key: 'supplier', label: '供应商名称', visible: col.supplier },
+	{ key: 'requestDate', label: '交货日期', visible: col.requestDate },
+	{ key: 'needDate', label: '需求日期', visible: col.needDate },
+	{ key: 'schedQty', label: '需求数量', visible: col.schedQty },
+	{ key: 'sentQty', label: '已交数量', visible: col.sentQty },
+	{ key: 'restQty', label: '待交数量', visible: col.restQty },
+	{ key: 'lastSentDate', label: '最近交货日期', visible: col.lastSentDate },
+	{ key: 'lastSentQty', label: '最近交货数量', visible: col.lastSentQty },
+	{ key: 'submitDate', label: '发布日期', visible: col.submitDate },
+	{ key: 'poNumber', label: '采购单号', visible: col.poNumber },
+	{ key: 'poLine', label: '采购单行号', visible: col.poLine },
+]);
+
+const selectedPublishableIds = computed(() => selectedRows.value.filter((x) => x.status === 'N').map((x) => x.id));
+
+function statusText(v?: string | null) {
+	return v === 'P' ? '已发布' : v === 'C' ? '关闭' : '初始';
+}
+function statusTag(v?: string | null) {
+	return v === 'P' ? 'success' : v === 'C' ? 'danger' : 'info';
+}
+
+function onSelectionChange(sel: DeliveryScheduleRow[]) {
+	selectedRows.value = sel;
+}
+
+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 fetchDeliveryScheduleList({
+			poNumber: query.poNumber || undefined,
+			dsNum: query.dsNum || undefined,
+			itemNum: query.itemNum || undefined,
+			supplier: query.supplier || undefined,
+			status: query.status || undefined,
+			page: query.page,
+			pageSize: query.pageSize,
+			sortField: query.sortField || undefined,
+			sortOrder: query.sortOrder || undefined,
+		});
+		rows.value = (data.list || []).map((x) => ({
+			...x,
+			requestDate: x.requestDate ? String(x.requestDate).slice(0, 10) : '',
+			needDate: x.needDate ? String(x.needDate).slice(0, 10) : '',
+			lastSentDate: x.lastSentDate ? String(x.lastSentDate).slice(0, 10) : '',
+			submitDate: x.submitDate ? String(x.submitDate).slice(0, 10) : '',
+		}));
+		total.value = data.total || 0;
+	} finally {
+		loading.value = false;
+	}
+}
+
+function doSearch() {
+	query.page = 1;
+	loadList();
+}
+
+function resetQuery() {
+	query.poNumber = '';
+	query.dsNum = '';
+	query.itemNum = '';
+	query.supplier = '';
+	query.status = '';
+	query.page = 1;
+	query.sortField = '';
+	query.sortOrder = '';
+	loadList();
+}
+
+async function onGenerate() {
+	generating.value = true;
+	try {
+		await generateDeliverySchedule();
+		ElMessage.success('生成交货单成功');
+		await loadList();
+	} finally {
+		generating.value = false;
+	}
+}
+
+async function onPublish() {
+	if (!selectedPublishableIds.value.length) {
+		ElMessage.warning('请勾选状态为初始的数据');
+		return;
+	}
+	await publishDeliverySchedule(selectedPublishableIds.value.join(','));
+	ElMessage.success('发布成功');
+	await loadList();
+}
+
+async function onUnPublish(row: DeliveryScheduleRow) {
+	await unpublishDeliverySchedule(row.id);
+	ElMessage.success('取消发布成功');
+	await loadList();
+}
+
+async function onCancel(row: DeliveryScheduleRow) {
+	await ElMessageBox.confirm(`确定作废交货单 ${row.dsNum || ''} 吗?`, '作废确认', {
+		confirmButtonText: '作废',
+		cancelButtonText: '取消',
+		type: 'warning',
+	});
+	await cancelDeliverySchedule(row.id);
+	ElMessage.success('作废成功');
+	await loadList();
+}
+
+function openBatchAdd() {
+	batchVisible.value = true;
+}
+
+async function onBatchSaved() {
+	batchVisible.value = false;
+	await loadList();
+}
+
+function onExport() {
+	const headers = [
+		'交货单号', '状态', '物料编码', '物料描述', '采购组', '供应商', '供应商名称', '交货日期', '需求日期',
+		'需求数量', '已交数量', '待交数量', '最近交货日期', '最近交货数量', '发布日期', '采购单号', '采购单行号',
+	];
+	const lines = rows.value.map((r) => [
+		r.dsNum ?? '', statusText(r.status), r.itemNum ?? '', r.descr ?? '', r.purGroup ?? '', r.supplierCode ?? '',
+		r.supplier ?? '', r.requestDate ?? '', r.needDate ?? '', r.schedQty ?? '', r.sentQty ?? '',
+		r.restQty ?? '', r.lastSentDate ?? '', r.lastSentQty ?? '', r.submitDate ?? '', r.poNumber ?? '', r.poLine ?? '',
+	]);
+	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>

+ 132 - 0
Web/src/views/aidop/s3/supply/selectPurchaseOrder.vue

@@ -0,0 +1,132 @@
+<template>
+	<div>
+		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
+			<el-form-item label="采购单号">
+				<el-input v-model="query.purOrd" placeholder="输入采购单号" clearable style="width: 180px" />
+			</el-form-item>
+			<el-form-item label="物料编码">
+				<el-input v-model="query.itemNum" placeholder="输入物料编码" clearable style="width: 180px" />
+			</el-form-item>
+			<el-form-item label="供应商">
+				<el-input v-model="query.supp" placeholder="输入供应商" clearable style="width: 180px" />
+			</el-form-item>
+			<el-form-item>
+				<el-button type="primary" @click="doSearch">查询</el-button>
+				<el-button @click="reset">重置</el-button>
+			</el-form-item>
+		</el-form>
+
+		<el-table
+			:data="rows"
+			v-loading="loading"
+			border
+			stripe
+			style="width: 100%"
+			height="420"
+			@row-dblclick="onDblClick"
+			@sort-change="onSortChange"
+		>
+			<el-table-column prop="purOrd" label="采购单号" width="140" sortable="custom" />
+			<el-table-column prop="line" label="行号" width="70" sortable="custom" />
+			<el-table-column prop="itemNum" label="物料编码" width="140" sortable="custom" />
+			<el-table-column prop="um" label="单位" width="80" />
+			<el-table-column prop="supp" label="供应商代码" width="130" sortable="custom" />
+			<el-table-column prop="name" label="供应商" min-width="140" show-overflow-tooltip sortable="custom" />
+			<el-table-column prop="dueDate" label="需求日期" width="120" sortable="custom" />
+			<el-table-column prop="qtyOrded" label="需求数量" width="110" align="right" sortable="custom" />
+			<el-table-column prop="buyer" label="采购组" width="100" />
+		</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>
+	</div>
+</template>
+
+<script setup lang="ts" name="aidopS3SupplySelectPurchaseOrder">
+import { onMounted, reactive, ref } from 'vue';
+import { fetchPurchaseOrderPage, type PurchaseOrderRow } from '../api/deliverySchedule';
+
+const emit = defineEmits<{ (e: 'picked', v: PurchaseOrderRow): void }>();
+
+const query = reactive({
+	purOrd: '',
+	itemNum: '',
+	supp: '',
+	page: 1,
+	pageSize: 10,
+	sortField: '',
+	sortOrder: '',
+});
+const loading = ref(false);
+const rows = ref<PurchaseOrderRow[]>([]);
+const total = ref(0);
+
+async function loadList() {
+	loading.value = true;
+	try {
+		const data = await fetchPurchaseOrderPage({
+			purOrd: query.purOrd || undefined,
+			itemNum: query.itemNum || undefined,
+			supp: query.supp || undefined,
+			page: query.page,
+			pageSize: query.pageSize,
+			sortField: query.sortField || undefined,
+			sortOrder: query.sortOrder || undefined,
+		});
+		rows.value = (data.list || []).map((x) => ({
+			...x,
+			dueDate: x.dueDate ? String(x.dueDate).slice(0, 10) : '',
+		}));
+		total.value = data.total || 0;
+	} finally {
+		loading.value = false;
+	}
+}
+
+function doSearch() {
+	query.page = 1;
+	loadList();
+}
+
+function reset() {
+	query.purOrd = '';
+	query.itemNum = '';
+	query.supp = '';
+	query.page = 1;
+	query.sortField = '';
+	query.sortOrder = '';
+	loadList();
+}
+
+function onSortChange({ prop, order }: { prop: string; order: string | null }) {
+	query.sortField = prop || '';
+	query.sortOrder = order === 'ascending' ? 'asc' : order === 'descending' ? 'desc' : '';
+	loadList();
+}
+
+function onDblClick(row: PurchaseOrderRow) {
+	emit('picked', row);
+}
+
+onMounted(loadList);
+</script>
+
+<style scoped lang="scss">
+.mb12 {
+	margin-bottom: 12px;
+}
+.pager {
+	margin-top: 10px;
+	display: flex;
+	justify-content: flex-end;
+}
+</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.73</AssemblyVersion>
-    <FileVersion>1.0.73</FileVersion>
-    <Version>1.0.73</Version>
+    <AssemblyVersion>1.0.74</AssemblyVersion>
+    <FileVersion>1.0.74</FileVersion>
+    <Version>1.0.74</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 7 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/AdoSmartOpsImprovementPlanController.cs

@@ -217,10 +217,15 @@ public class AdoSmartOpsImprovementPlanController : ControllerBase
         var status = NormalizeManualActionStatus(input.Status);
         actions[index].Status = status;
         actions[index].CompletedAt = ResolveActionCompletedAt(status, input.CompletedAt);
+        var actionItemsJson = JsonSerializer.Serialize(actions);
+        var now = DateTime.Now;
 
         await _db.Updateable<AdoSmartOpsImprovementPlan>()
-            .SetColumns(x => x.ActionItemsJson == JsonSerializer.Serialize(actions))
-            .SetColumns(x => x.UpdateTime == DateTime.Now)
+            .SetColumns(x => new AdoSmartOpsImprovementPlan
+            {
+                ActionItemsJson = actionItemsJson,
+                UpdateTime = now
+            })
             .Where(x => x.Id == plan.Id)
             .ExecuteCommandAsync();
 

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

@@ -1201,6 +1201,21 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
             OrderNo = 10,
             Remark = "S3 物料需求计划(ic_demandschedule)"
         };
+
+        yield return new SysMenu
+        {
+            Id = 1329003100002L,
+            Pid = s3MaterialPlanDirId,
+            Title = "物料交货计划",
+            Path = "/aidop/s3/material-plan/delivery-schedule",
+            Name = "aidopS3DeliverySchedule",
+            Component = "/aidop/s3/supply/deliveryScheduleList",
+            Icon = "ele-Document",
+            Type = MenuTypeEnum.Menu,
+            CreateTime = ct,
+            OrderNo = 20,
+            Remark = "S3 物料交货计划(srm_polist_ds)"
+        };
     }
 
     private static readonly (string Code, string L1, (string Title, string Desc, string Complexity, string Days, string Note)[] Leaves)[] ModuleDefinitions =

+ 329 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Supply/DeliveryScheduleService.cs

@@ -0,0 +1,329 @@
+namespace Admin.NET.Plugin.AiDOP.Supply;
+
+/// <summary>
+/// 物料交货计划服务
+/// </summary>
+[ApiDescriptionSettings(Order = 306, Description = "物料交货计划")]
+[Route("api/Supply")]
+[AllowAnonymous]
+[NonUnify]
+public class DeliveryScheduleService : IDynamicApiController, ITransient
+{
+    private readonly ISqlSugarClient _db;
+    private readonly SqlSugarRepository<PolistDeliverySchedule> _rep;
+    private readonly UserManager _userManager;
+
+    public DeliveryScheduleService(
+        ISqlSugarClient db,
+        SqlSugarRepository<PolistDeliverySchedule> rep,
+        UserManager userManager)
+    {
+        _db = db;
+        _rep = rep;
+        _userManager = userManager;
+    }
+
+    [DisplayName("物料交货计划列表")]
+    [HttpGet("delivery-schedule/list")]
+    public async Task<object> GetList([FromQuery] DeliveryScheduleListInput input)
+    {
+        var page = input.Page <= 0 ? 1 : input.Page;
+        var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
+        var offset = (page - 1) * pageSize;
+
+        var pars = new List<SugarParameter>();
+        var where = new List<string> { "ss.isactive = 1" };
+        if (_userManager.TenantId > 0)
+        {
+            where.Add("ss.tenant_id = @TenantId");
+            pars.Add(new SugarParameter("@TenantId", _userManager.TenantId));
+        }
+        if (!string.IsNullOrWhiteSpace(input.PoNumber))
+        {
+            where.Add("ss.ponumber LIKE @PoNumber");
+            pars.Add(new SugarParameter("@PoNumber", $"%{input.PoNumber.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.DsNum))
+        {
+            where.Add("ss.dsnum LIKE @DsNum");
+            pars.Add(new SugarParameter("@DsNum", $"%{input.DsNum.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.ItemNum))
+        {
+            where.Add("ss.itemnum LIKE @ItemNum");
+            pars.Add(new SugarParameter("@ItemNum", $"%{input.ItemNum.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.Supplier))
+        {
+            where.Add("(ss.suppliercode LIKE @Supplier OR ss.supplier LIKE @Supplier)");
+            pars.Add(new SugarParameter("@Supplier", $"%{input.Supplier.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.Status))
+        {
+            where.Add("ss.status = @Status");
+            pars.Add(new SugarParameter("@Status", input.Status.Trim()));
+        }
+
+        var orderBy = BuildListOrderBy(input.SortField, input.SortOrder);
+        var fromSql = $"""
+            FROM srm_polist_ds ss
+            LEFT JOIN ItemMaster im ON ss.domain = im.Domain AND ss.itemnum = im.ItemNum
+            WHERE {string.Join(" AND ", where)}
+            """;
+
+        var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) {fromSql}", pars);
+        var list = await _db.Ado.SqlQueryAsync<DeliveryScheduleListRow>(
+            $"""
+            SELECT
+                ss.Id AS Id,
+                ss.domain AS Domain,
+                ss.icdsid AS IcdsId,
+                ss.dsnum AS DsNum,
+                ss.status AS Status,
+                ss.itemnum AS ItemNum,
+                im.Descr AS Descr,
+                ss.um AS Um,
+                ss.purgroup AS PurGroup,
+                ss.suppliercode AS SupplierCode,
+                ss.supplier AS Supplier,
+                ss.submitdate AS SubmitDate,
+                ss.requestdate AS RequestDate,
+                ss.needdate AS NeedDate,
+                ss.ponumber AS PoNumber,
+                ss.poline AS PoLine,
+                ss.schedqty AS SchedQty,
+                ss.lastsentdate AS LastSentDate,
+                ss.lastsentqty AS LastSentQty,
+                ss.sentqty AS SentQty,
+                ss.restqty AS RestQty
+            {fromSql}
+            ORDER BY {orderBy}
+            LIMIT {pageSize} OFFSET {offset}
+            """,
+            pars);
+
+        return new { total, page, pageSize, list };
+    }
+
+    [DisplayName("生成交货单")]
+    [HttpPost("delivery-schedule/generate")]
+    public async Task<object> Generate()
+    {
+        await ExecuteGenerateProcedure(string.Empty);
+        return new { message = "生成交货单成功" };
+    }
+
+    [DisplayName("批量添加交货单")]
+    [HttpPost("delivery-schedule/batch-generate")]
+    public async Task<object> BatchGenerate([FromBody] DeliveryScheduleGenerateInput input)
+    {
+        if (string.IsNullOrWhiteSpace(input.PoNumber))
+            throw Oops.Oh("采购单号不能为空");
+
+        await ExecuteGenerateProcedure(input.PoNumber.Trim());
+        return new { message = "批量添加成功" };
+    }
+
+    [DisplayName("发布交货单")]
+    [HttpPost("delivery-schedule/publish")]
+    public async Task<object> Publish([FromBody] DeliveryScheduleBatchIdsInput input)
+    {
+        var ids = ParseIds(input.Ids);
+        if (!ids.Any()) throw Oops.Oh("请勾选要发布的数据");
+
+        var account = _userManager.Account ?? "system";
+        var affected = await _db.Updateable<PolistDeliverySchedule>()
+            .SetColumns(x => new PolistDeliverySchedule
+            {
+                Status = "P",
+                SubmitDate = DateTime.Now,
+                UpdateUser = account,
+                UpdateTime = DateTime.Now
+            })
+            .Where(x => ids.Contains(x.Id) && x.IsActive == 1 && x.Status == "N")
+            .ExecuteCommandAsync();
+        return new { affected, message = affected > 0 ? "发布成功" : "无可发布数据" };
+    }
+
+    [DisplayName("取消发布交货单")]
+    [HttpPost("delivery-schedule/unpublish/{id:long}")]
+    public async Task<object> UnPublish(long id)
+    {
+        var row = await _rep.GetFirstAsync(x => x.Id == id && x.IsActive == 1) ?? throw Oops.Oh("交货单不存在");
+        row.Status = "N";
+        row.UpdateUser = _userManager.Account;
+        row.UpdateTime = DateTime.Now;
+        await _rep.UpdateAsync(row);
+        return new { message = "取消发布成功" };
+    }
+
+    [DisplayName("作废交货单")]
+    [HttpPost("delivery-schedule/cancel/{id:long}")]
+    public async Task<object> Cancel(long id)
+    {
+        var row = await _rep.GetFirstAsync(x => x.Id == id && x.IsActive == 1) ?? throw Oops.Oh("交货单不存在");
+        row.Status = "C";
+        row.UpdateUser = _userManager.Account;
+        row.UpdateTime = DateTime.Now;
+        await _rep.UpdateAsync(row);
+        return new { message = "作废成功" };
+    }
+
+    [DisplayName("选择采购单分页")]
+    [HttpGet("delivery-schedule/purchase-order/page")]
+    public async Task<object> GetPurchaseOrderPage([FromQuery] PurchaseOrderSelectInput input)
+    {
+        var page = input.Page <= 0 ? 1 : input.Page;
+        var pageSize = input.PageSize <= 0 ? 10 : input.PageSize;
+        var offset = (page - 1) * pageSize;
+
+        var pars = new List<SugarParameter>();
+        var where = new List<string>
+        {
+            "IFNULL(m.Status,'') <> 'C'",
+            "IFNULL(d.Status,'') <> 'C'",
+            "(IFNULL(ds.dsnum, '') = '' OR (ds.status = 'C' AND d.QtyOrded - d.RctQty > 0))"
+        };
+        if (!string.IsNullOrWhiteSpace(input.PurOrd))
+        {
+            where.Add("m.PurOrd LIKE @PurOrd");
+            pars.Add(new SugarParameter("@PurOrd", $"%{input.PurOrd.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.ItemNum))
+        {
+            where.Add("d.ItemNum LIKE @ItemNum");
+            pars.Add(new SugarParameter("@ItemNum", $"%{input.ItemNum.Trim()}%"));
+        }
+        if (!string.IsNullOrWhiteSpace(input.Supp))
+        {
+            where.Add("(m.Supp LIKE @Supp OR s.Name LIKE @Supp)");
+            pars.Add(new SugarParameter("@Supp", $"%{input.Supp.Trim()}%"));
+        }
+
+        var orderBy = BuildPurchaseOrderOrderBy(input.SortField, input.SortOrder);
+        var fromSql = $"""
+            FROM PurOrdMaster m
+            INNER JOIN PurOrdDetail d ON m.RecID = d.PurOrdRecID
+            LEFT JOIN srm_polist_ds ds ON d.PurOrd = ds.ponumber AND d.Line = ds.poline AND ds.isactive = 1
+            LEFT JOIN ConsigneeAddressMaster s ON m.Domain = s.Domain AND m.Supp = s.Address AND s.Typed = 'Supp'
+            WHERE {string.Join(" AND ", where)}
+            """;
+        var distinctSql = $"SELECT DISTINCT m.PurOrd, d.Line, d.ItemNum {fromSql}";
+
+        var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) FROM ({distinctSql}) t", pars);
+        var list = await _db.Ado.SqlQueryAsync<PurchaseOrderSelectRow>(
+            $"""
+            SELECT DISTINCT
+                m.PurOrd AS PurOrd,
+                d.Line AS Line,
+                d.ItemNum AS ItemNum,
+                d.UM AS Um,
+                m.Supp AS Supp,
+                s.Name AS Name,
+                d.DueDate AS DueDate,
+                d.QtyOrded AS QtyOrded,
+                m.Buyer AS Buyer
+            {fromSql}
+            ORDER BY {orderBy}
+            LIMIT {pageSize} OFFSET {offset}
+            """,
+            pars);
+
+        return new { total, page, pageSize, list };
+    }
+
+    private async Task ExecuteGenerateProcedure(string poNumber)
+    {
+        var account = _userManager.Account ?? "system";
+        var orgNo = _userManager.OrgId.ToString();
+
+        await _db.Ado.ExecuteCommandAsync(
+            "CALL pr_MES_GenerateJHD(@PoNumber, @OrgNo, @Account)",
+            new SugarParameter("@PoNumber", poNumber),
+            new SugarParameter("@OrgNo", orgNo),
+            new SugarParameter("@Account", account));
+    }
+
+    private static List<long> ParseIds(string ids)
+    {
+        return ids.Split(',', StringSplitOptions.RemoveEmptyEntries)
+            .Select(x => long.TryParse(x.Trim(), out var id) ? id : 0)
+            .Where(x => x > 0)
+            .Distinct()
+            .ToList();
+    }
+
+    private static string BuildListOrderBy(string? sortField, string? sortOrder)
+    {
+        var dir = string.Equals(sortOrder, "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC";
+        return sortField?.ToLowerInvariant() switch
+        {
+            "dsnum" => $"ss.dsnum {dir}",
+            "status" => $"ss.status {dir}",
+            "itemnum" => $"ss.itemnum {dir}",
+            "requestdate" => $"ss.requestdate {dir}",
+            "needdate" => $"ss.needdate {dir}",
+            "schedqty" => $"ss.schedqty {dir}",
+            "sentqty" => $"ss.sentqty {dir}",
+            "restqty" => $"ss.restqty {dir}",
+            "submitdate" => $"ss.submitdate {dir}",
+            "ponumber" => $"ss.ponumber {dir}",
+            _ => "ss.Id DESC"
+        };
+    }
+
+    private static string BuildPurchaseOrderOrderBy(string? sortField, string? sortOrder)
+    {
+        var dir = string.Equals(sortOrder, "asc", StringComparison.OrdinalIgnoreCase) ? "ASC" : "DESC";
+        return sortField?.ToLowerInvariant() switch
+        {
+            "purord" => $"m.PurOrd {dir}",
+            "line" => $"d.Line {dir}",
+            "itemnum" => $"d.ItemNum {dir}",
+            "supp" => $"m.Supp {dir}",
+            "name" => $"s.Name {dir}",
+            "duedate" => $"d.DueDate {dir}",
+            "qtyorded" => $"d.QtyOrded {dir}",
+            "buyer" => $"m.Buyer {dir}",
+            _ => "m.PurOrd DESC, d.Line ASC"
+        };
+    }
+
+    private sealed class DeliveryScheduleListRow
+    {
+        public long Id { get; set; }
+        public string? Domain { get; set; }
+        public long? IcdsId { get; set; }
+        public string? DsNum { get; set; }
+        public string? Status { get; set; }
+        public string? ItemNum { get; set; }
+        public string? Descr { get; set; }
+        public string? Um { get; set; }
+        public string? PurGroup { get; set; }
+        public string? SupplierCode { get; set; }
+        public string? Supplier { get; set; }
+        public DateTime? SubmitDate { get; set; }
+        public DateTime? RequestDate { get; set; }
+        public DateTime? NeedDate { get; set; }
+        public string? PoNumber { get; set; }
+        public int? PoLine { get; set; }
+        public decimal? SchedQty { get; set; }
+        public DateTime? LastSentDate { get; set; }
+        public decimal? LastSentQty { get; set; }
+        public decimal? SentQty { get; set; }
+        public decimal? RestQty { get; set; }
+    }
+
+    private sealed class PurchaseOrderSelectRow
+    {
+        public string? PurOrd { get; set; }
+        public int? Line { get; set; }
+        public string? ItemNum { get; set; }
+        public string? Um { get; set; }
+        public string? Supp { get; set; }
+        public string? Name { get; set; }
+        public DateTime? DueDate { get; set; }
+        public decimal? QtyOrded { get; set; }
+        public string? Buyer { get; set; }
+    }
+}

+ 35 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Supply/Dto/DeliveryScheduleDto.cs

@@ -0,0 +1,35 @@
+namespace Admin.NET.Plugin.AiDOP.Supply;
+
+public class DeliveryScheduleListInput
+{
+    public int Page { get; set; } = 1;
+    public int PageSize { get; set; } = 10;
+    public string? PoNumber { get; set; }
+    public string? DsNum { get; set; }
+    public string? ItemNum { get; set; }
+    public string? Supplier { get; set; }
+    public string? Status { get; set; }
+    public string? SortField { get; set; }
+    public string? SortOrder { get; set; }
+}
+
+public class DeliveryScheduleBatchIdsInput
+{
+    public string Ids { get; set; } = string.Empty;
+}
+
+public class DeliveryScheduleGenerateInput
+{
+    public string PoNumber { get; set; } = string.Empty;
+}
+
+public class PurchaseOrderSelectInput
+{
+    public int Page { get; set; } = 1;
+    public int PageSize { get; set; } = 10;
+    public string? PurOrd { get; set; }
+    public string? ItemNum { get; set; }
+    public string? Supp { get; set; }
+    public string? SortField { get; set; }
+    public string? SortOrder { get; set; }
+}

+ 92 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Supply/Entity/PolistDeliverySchedule.cs

@@ -0,0 +1,92 @@
+namespace Admin.NET.Plugin.AiDOP.Supply;
+
+/// <summary>
+/// 物料交货单(srm_polist_ds)
+/// </summary>
+[SugarTable("srm_polist_ds", "PO行交货单")]
+public class PolistDeliverySchedule
+{
+    [SugarColumn(ColumnName = "Id", IsPrimaryKey = true, IsIdentity = false)]
+    public long Id { get; set; }
+
+    [SugarColumn(ColumnName = "domain", Length = 8)]
+    public string Domain { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "icdsid")]
+    public long IcdsId { get; set; }
+
+    [SugarColumn(ColumnName = "dsnum", Length = 128)]
+    public string DsNum { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "status", Length = 10)]
+    public string Status { get; set; } = "N";
+
+    [SugarColumn(ColumnName = "itemnum", Length = 128)]
+    public string ItemNum { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "um", Length = 124)]
+    public string Um { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "purgroup", Length = 50)]
+    public string PurGroup { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "suppliercode", Length = 50)]
+    public string SupplierCode { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "supplier", Length = 50)]
+    public string Supplier { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "submitdate", IsNullable = true)]
+    public DateTime? SubmitDate { get; set; }
+
+    [SugarColumn(ColumnName = "requestdate", IsNullable = true)]
+    public DateTime? RequestDate { get; set; }
+
+    [SugarColumn(ColumnName = "needdate", IsNullable = true)]
+    public DateTime? NeedDate { get; set; }
+
+    [SugarColumn(ColumnName = "ponumber", Length = 50)]
+    public string PoNumber { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "poline")]
+    public int PoLine { get; set; }
+
+    [SugarColumn(ColumnName = "schedqty")]
+    public decimal SchedQty { get; set; }
+
+    [SugarColumn(ColumnName = "lastsentdate", IsNullable = true)]
+    public DateTime? LastSentDate { get; set; }
+
+    [SugarColumn(ColumnName = "lastsentqty")]
+    public decimal LastSentQty { get; set; }
+
+    [SugarColumn(ColumnName = "sentqty")]
+    public decimal SentQty { get; set; }
+
+    [SugarColumn(ColumnName = "restqty")]
+    public decimal RestQty { get; set; }
+
+    [SugarColumn(ColumnName = "createuser", Length = 24, IsNullable = true)]
+    public string? CreateUser { get; set; }
+
+    [SugarColumn(ColumnName = "updateuser", Length = 24, IsNullable = true)]
+    public string? UpdateUser { get; set; }
+
+    [SugarColumn(ColumnName = "createtime", IsNullable = true)]
+    public DateTime? CreateTime { get; set; }
+
+    [SugarColumn(ColumnName = "updatetime", IsNullable = true)]
+    public DateTime? UpdateTime { get; set; }
+
+    [SugarColumn(ColumnName = "remarks", Length = 500)]
+    public string Remarks { get; set; } = string.Empty;
+
+    [SugarColumn(ColumnName = "isactive")]
+    public int IsActive { get; set; } = 1;
+
+    [SugarColumn(ColumnName = "returnqty", IsNullable = true)]
+    public decimal? ReturnQty { get; set; }
+
+    [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
+    public long? TenantId { get; set; }
+}