Bladeren bron

chore: bump version 2.4.120/1.0.87 - S4 delivery management enhancements and production scheduling updates

Pengxy 2 maanden geleden
bovenliggende
commit
b78de8d729

+ 1 - 1
Web/package.json

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

+ 23 - 8
Web/src/views/aidop/api/workOrderScheduling.ts

@@ -32,18 +32,33 @@ export interface WorkOrderSchedulingRow {
 
 /** 生成生产排程计划(POST + query domain) */
 export function productionSchedule(domain: string) {
-	return axios
-		.post(`${EXTERNAL_RESOURCE_BASE}/api/business/resource-examine/productionschedule`, null, {
-			params: { domain },
-		})
-		.then((r) => r.data);
+	// 注意:项目对 axios 默认实例注入了 Authorization;外部接口不需要携带登录态
+	// 这里用 fetch 确保请求头不带 Authorization
+	const url = `${EXTERNAL_RESOURCE_BASE}/api/business/resource-examine/productionschedule?domain=${encodeURIComponent(domain)}`;
+	return fetch(url, { method: 'POST' }).then(async (r) => {
+		if (!r.ok) throw new Error(`生产排程接口失败(HTTP ${r.status})`);
+		const text = await r.text();
+		try {
+			return text ? JSON.parse(text) : {};
+		} catch {
+			return text;
+		}
+	});
 }
 
 /** 同步物料需求(GET + query domain) */
 export function syncMaterialRequirement(domain: string) {
-	return axios
-		.get(`${EXTERNAL_RESOURCE_BASE}/api/business/resource-examine/AutomaticPrAdjustDate`, { params: { domain } })
-		.then((r) => r.data);
+	// 注意:项目对 axios 默认实例注入了 Authorization;外部接口不需要携带登录态
+	const url = `${EXTERNAL_RESOURCE_BASE}/api/business/resource-examine/AutomaticPrAdjustDate?domain=${encodeURIComponent(domain)}`;
+	return fetch(url, { method: 'GET' }).then(async (r) => {
+		if (!r.ok) throw new Error(`同步物料需求接口失败(HTTP ${r.status})`);
+		const text = await r.text();
+		try {
+			return text ? JSON.parse(text) : {};
+		} catch {
+			return text;
+		}
+	});
 }
 
 /** 优先级调整保存(GET,与 replenishment 接口约定一致) */

+ 13 - 16
Web/src/views/aidop/production/workOrderSchedulingList.vue

@@ -143,6 +143,7 @@ import { ElMessage, ElMessageBox } from 'element-plus';
 import AidopDemoShell from '../components/AidopDemoShell.vue';
 import WorkOrderSchedulingPriorityForm from './workOrderSchedulingPriorityForm.vue';
 import WorkOrderSchedulingViewForm from './workOrderSchedulingViewForm.vue';
+import { useUserInfo } from '/@/stores/userInfo';
 import {
 	closeWorkOrders,
 	fetchWorkOrderSchedulingList,
@@ -157,6 +158,7 @@ import {
 const route = useRoute();
 const router = useRouter();
 const pageTitle = computed(() => (route.meta?.title as string) || '工单工序排产');
+const userInfoStore = useUserInfo();
 
 const statusOptions = [
 	{ v: 'w', l: 'w 投产' },
@@ -231,23 +233,18 @@ function resetQuery() {
 	loadList();
 }
 
-/** 生产排程 / 同步物料等外部接口需单一公司域名:从已选行解析 */
-function resolveDomainForToolbar(): string | null {
-	if (!selectedRows.value.length) {
-		ElMessage.warning('请先勾选至少一条工单,用于确定公司域名');
-		return null;
-	}
-	const d = (selectedRows.value[0]?.domain ?? '').trim();
-	if (!d) {
-		ElMessage.warning('所选工单缺少公司域名');
-		return null;
+/** 生产排程 / 同步物料等外部接口使用当前登录用户组织ID作为 domain */
+async function resolveDomainFromCurrentUser(): Promise<string | null> {
+	// 确保 userInfos 已加载(部分页面首次进入时 store 可能还是空)
+	if (!userInfoStore.userInfos?.orgId) {
+		await userInfoStore.setUserInfos();
 	}
-	const allSame = selectedRows.value.every((r) => (r.domain ?? '').trim() === d);
-	if (!allSame) {
-		ElMessage.warning('所选工单的公司域名不一致,请仅勾选同一域名的工单');
+	const orgId = userInfoStore.userInfos?.orgId;
+	if (!orgId) {
+		ElMessage.warning('当前登录用户缺少组织ID(orgId),无法执行生产排程');
 		return null;
 	}
-	return d;
+	return String(orgId);
 }
 
 async function loadList() {
@@ -304,7 +301,7 @@ async function onBatchClose() {
 const scheduling = ref(false);
 
 async function onProductionSchedule() {
-	const domain = resolveDomainForToolbar();
+	const domain = await resolveDomainFromCurrentUser();
 	if (!domain) return;
 	scheduling.value = true;
 	try {
@@ -320,7 +317,7 @@ async function onProductionSchedule() {
 const syncing = ref(false);
 
 async function onSyncMaterial() {
-	const domain = resolveDomainForToolbar();
+	const domain = await resolveDomainFromCurrentUser();
 	if (!domain) return;
 	syncing.value = true;
 	try {

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

@@ -97,6 +97,14 @@ export function publishSupplierDelivery(ids: string) {
 	return service.post('/api/ProcurementExecution/supplier-delivery/publish', { ids }).then((r) => r.data);
 }
 
+export function replySupplierDeliveryByPlanDate(ids: string) {
+	return service.post('/api/ProcurementExecution/supplier-delivery/reply-by-plan-date', { ids }).then((r) => r.data);
+}
+
+export function replySupplierDeliveryDueDate(ids: string, jqhf: string) {
+	return service.post('/api/ProcurementExecution/supplier-delivery/reply-due-date', { ids, jqhf }).then((r) => r.data);
+}
+
 export function closeDeliveryOrder(dsNum: string) {
 	return service.post('/api/ProcurementExecution/supplier-delivery/close', { dsNum }).then((r) => r.data);
 }
@@ -125,3 +133,33 @@ export function shipmentPlaceholderAction(action: 'generate-label' | 'print-ship
 	return service.post(`/api/ProcurementExecution/supplier-shipment/${action}`, { id }).then((r) => r.data);
 }
 
+export interface SupplierShipmentLabelRow {
+	glid?: number | null;
+	wlbm?: string | null;
+	wlmc?: string | null;
+	ggxh?: string | null;
+	zxsl?: number | null;
+	dw?: string | null;
+	remarks?: string | null;
+	bz?: number | null;
+	ddlx?: string | null;
+	ddh?: string | null;
+	shdh?: string | null;
+	shdhh?: string | null;
+	scrq?: string | null;
+	scph?: string | null;
+	xh?: string | null;
+	gysmc?: string | null;
+	th?: string | null;
+	bbh?: string | null;
+	yt?: string | null;
+	ccrq?: string | null;
+	shpc?: string | null;
+}
+
+export function fetchSupplierShipmentLabelData(shddh: string) {
+	return service
+		.get<{ list: SupplierShipmentLabelRow[] }>(`/api/ProcurementExecution/supplier-shipment/label-data`, { params: { shddh } })
+		.then((r) => r.data);
+}
+

+ 44 - 1
Web/src/views/aidop/s4/delivery/supplierDeliveryManagementList.vue

@@ -14,6 +14,8 @@
 		<div class="toolbar">
 			<el-button type="primary" @click="onCreateShipment">生成发货单</el-button>
 			<el-button type="success" @click="onPublish">发布</el-button>
+			<el-button type="warning" @click="onReplyByPlanDate">按计划日期回复</el-button>
+			<el-button type="primary" @click="openReplyDueDate">回复交期</el-button>
 			<el-button @click="onExport">导出</el-button>
 			<el-popover placement="bottom" width="220" trigger="click">
 				<template #reference><el-button text>列设置</el-button></template>
@@ -70,6 +72,18 @@
 			/>
 		</div>
 	</AidopDemoShell>
+
+	<el-dialog v-model="replyDialog.visible" title="回复交期" width="420px" destroy-on-close>
+		<el-form label-width="90px">
+			<el-form-item label="交期日期">
+				<el-date-picker v-model="replyDialog.jqhf" type="date" value-format="YYYY-MM-DD" placeholder="请选择日期" style="width: 100%" />
+			</el-form-item>
+		</el-form>
+		<template #footer>
+			<el-button @click="replyDialog.visible = false">取消</el-button>
+			<el-button type="primary" @click="confirmReplyDueDate">确认</el-button>
+		</template>
+	</el-dialog>
 </template>
 
 <script setup lang="ts" name="aidopS4SupplierDeliveryManagementList">
@@ -77,7 +91,7 @@ import { computed, onMounted, reactive, ref } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
-import { closeDeliveryOrder, fetchSupplierDeliveryList, publishSupplierDelivery, type SupplierDeliveryRow } from '../api/procurementExecution';
+import { closeDeliveryOrder, fetchSupplierDeliveryList, publishSupplierDelivery, replySupplierDeliveryByPlanDate, replySupplierDeliveryDueDate, type SupplierDeliveryRow } from '../api/procurementExecution';
 
 const route = useRoute();
 const router = useRouter();
@@ -98,6 +112,7 @@ const loading = ref(false);
 const rows = ref<SupplierDeliveryRow[]>([]);
 const total = ref(0);
 const selectedRows = ref<SupplierDeliveryRow[]>([]);
+const replyDialog = reactive({ visible: false, jqhf: '' });
 
 const col = reactive({
 	sffb: true, wlbm: true, wlms: true, buyer: true, gysdm: true, gysmc: true, cgdd: true, ddhh: true,
@@ -195,6 +210,34 @@ async function onPublish() {
 	await loadList();
 }
 
+async function onReplyByPlanDate() {
+	const payload = getSelectedIdsAndSupplier();
+	if (!payload) return;
+	await replySupplierDeliveryByPlanDate(payload.ids);
+	ElMessage.success('已按计划日期回复');
+	await loadList();
+}
+
+function openReplyDueDate() {
+	const payload = getSelectedIdsAndSupplier();
+	if (!payload) return;
+	replyDialog.jqhf = '';
+	replyDialog.visible = true;
+}
+
+async function confirmReplyDueDate() {
+	const payload = getSelectedIdsAndSupplier();
+	if (!payload) return;
+	if (!replyDialog.jqhf) {
+		ElMessage.warning('请选择交期日期');
+		return;
+	}
+	await replySupplierDeliveryDueDate(payload.ids, replyDialog.jqhf);
+	replyDialog.visible = false;
+	ElMessage.success('回复交期成功');
+	await loadList();
+}
+
 async function onCloseDelivery(row: SupplierDeliveryRow) {
 	if (!row.dsnum) {
 		ElMessage.warning('当前行无交货单号');

+ 39 - 1
Web/src/views/aidop/s4/delivery/supplierShipmentForm.vue

@@ -1,6 +1,11 @@
 <template>
 	<AidopDemoShell :title="title">
 		<div v-loading="loading">
+			<div class="top-toolbar" v-if="isView">
+				<el-button type="primary" @click="onPrintShippingNote">打印送货单</el-button>
+				<el-button @click="onCancel">返回</el-button>
+			</div>
+
 			<el-form :model="form" label-width="120px">
 				<el-row :gutter="12">
 					<el-col :span="12"><el-form-item label="送货单号"><el-input v-model="form.shddh" :disabled="isView" /></el-form-item></el-col>
@@ -87,7 +92,7 @@
 </template>
 
 <script setup lang="ts" name="aidopS4SupplierShipmentForm">
-import { computed, onMounted, reactive, ref } from 'vue';
+import { computed, nextTick, onMounted, reactive, ref } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import { ElMessage } from 'element-plus';
 import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
@@ -104,6 +109,7 @@ const router = useRouter();
 const mode = computed(() => String(route.query.mode || 'create') as 'create' | 'edit' | 'view');
 const id = computed(() => Number(route.query.id || 0));
 const ids = computed(() => String(route.query.ids || ''));
+const autoPrint = computed(() => String(route.query.autoPrint || '') === '1');
 const isView = computed(() => mode.value === 'view');
 const title = computed(() => (mode.value === 'create' ? '发货单新增' : mode.value === 'edit' ? '发货单编辑' : '发货单查看'));
 
@@ -161,6 +167,11 @@ async function loadData() {
 		if ((mode.value === 'edit' || mode.value === 'view') && id.value > 0) {
 			const detail = await fetchSupplierShipmentDetail(id.value);
 			setForm(detail);
+			if (mode.value === 'view' && autoPrint.value) {
+				// 等表单渲染完成后再触发浏览器打印
+				await nextTick();
+				window.print();
+			}
 			return;
 		}
 	} finally {
@@ -168,6 +179,10 @@ async function loadData() {
 	}
 }
 
+function onPrintShippingNote() {
+	window.print();
+}
+
 async function onSave() {
 	saving.value = true;
 	try {
@@ -192,8 +207,31 @@ onMounted(loadData);
 
 <style scoped lang="scss">
 @import '/@/views/aidop/styles/aidop-demo.scss';
+.top-toolbar { display: flex; gap: 8px; margin-bottom: 12px; }
 .sub-toolbar { margin-bottom: 8px; }
 .footer { display: flex; justify-content: flex-end; gap: 10px; margin-top: 12px; }
 .upload-name { margin-left: 8px; color: #606266; font-size: 12px; }
+
+@media print {
+	.top-toolbar,
+	.footer,
+	.sub-toolbar,
+	:deep(.el-upload),
+	:deep(.el-button) {
+		display: none !important;
+	}
+
+	/* 尽量让表单内容铺满打印页 */
+	:deep(.el-input__wrapper),
+	:deep(.el-textarea__inner) {
+		box-shadow: none !important;
+	}
+
+	:deep(.el-divider__text) {
+		background: transparent !important;
+	}
+
+	/* 打印时去掉页面外边距由浏览器控制,这里只做最小干预 */
+}
 </style>
 

+ 52 - 3
Web/src/views/aidop/s4/delivery/supplierShipmentList.vue

@@ -57,7 +57,7 @@
 				<template #default="{ row }">
 					<el-button link type="primary" @click="onPlaceholder('generate-label', row)">生成标签</el-button>
 					<el-button link type="primary" @click="onPlaceholder('print-shipping-note', row)">打印送货单</el-button>
-					<el-button link type="primary" @click="onPlaceholder('print-label', row)">打印标签</el-button>
+					<el-button link type="primary" @click="onPrintLabel(row)">打印标签</el-button>
 					<el-button link type="primary" @click="openForm('edit', row.mid)">质检报告</el-button>
 					<el-button link type="primary" @click="openForm('edit', row.mid)">编辑</el-button>
 					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
@@ -85,11 +85,19 @@ import { computed, onMounted, reactive, ref } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import AidopDemoShell from '/@/views/aidop/components/AidopDemoShell.vue';
-import { deleteSupplierShipment, fetchSupplierShipmentList, shipmentPlaceholderAction, type SupplierShipmentRow } from '../api/procurementExecution';
+import { deleteSupplierShipment, fetchSupplierShipmentLabelData, fetchSupplierShipmentList, shipmentPlaceholderAction, type SupplierShipmentRow } from '../api/procurementExecution';
+import printDialog from '/@/views/system/print/component/hiprint/preview.vue';
+import { hiprint } from 'vue-plugin-hiprint';
+import { getAPI } from '/@/utils/axios-utils';
+import { SysPrintApi } from '/@/api-services/api';
+import type { SysPrint } from '/@/api-services/models';
+import { formatDate } from '/@/utils/formatTime';
 
 const route = useRoute();
 const router = useRouter();
 const pageTitle = computed(() => (route.meta?.title as string) || '供应商发货单');
+const printDialogRef = ref();
+const PRINT_LABEL_TEMPLATE_NAME = '送货标签';
 
 const query = reactive({
 	jhshrqFrom: '',
@@ -175,8 +183,47 @@ async function onDelete(row: SupplierShipmentRow) {
 }
 
 async function onPlaceholder(action: 'generate-label' | 'print-shipping-note' | 'print-label', row: SupplierShipmentRow) {
+	if (action === 'print-shipping-note') {
+		router.push({
+			path: '/aidop/s4/delivery/supplier-shipment-form',
+			query: { mode: 'view', id: String(row.mid), autoPrint: '1' },
+		});
+		return;
+	}
 	const ret = await shipmentPlaceholderAction(action, row.mid);
-	ElMessage.info(ret?.message || '功能预留');
+	const msg = ret?.message || ret?.msg || '功能预留';
+	if (ret?.success === true) ElMessage.success(msg);
+	else if (ret?.success === false) ElMessage.error(msg);
+	else ElMessage.info(msg);
+}
+
+async function onPrintLabel(row: SupplierShipmentRow) {
+	if (!row?.shddh) {
+		ElMessage.warning('当前行缺少发货单编号(shddh),无法打印');
+		return;
+	}
+	const data = await fetchSupplierShipmentLabelData(row.shddh);
+	const items = data?.list ?? [];
+	if (items.length === 0) {
+		ElMessage.warning(`未查询到标签数据:${row.shddh}`);
+		return;
+	}
+
+	const res = await getAPI(SysPrintApi).apiSysPrintPrintNameGet(PRINT_LABEL_TEMPLATE_NAME);
+	const printTemplate = res.data.result as SysPrint;
+	const template = JSON.parse(printTemplate.template);
+
+	const printDate = formatDate(new Date(), 'YYYY-mm-dd HH:MM:SS');
+	// 兼容:模板既可能用“明细列表”,也可能用“单行字段”
+	const printData = {
+		shddh: row.shddh,
+		printDate,
+		list: items,
+		items,
+		...(items[0] ?? {}),
+	};
+
+	printDialogRef.value.showDialog(new hiprint.PrintTemplate({ template }), printData, template.panels[0].width);
 }
 
 function onExport() {
@@ -205,3 +252,5 @@ onMounted(loadList);
 :deep(.row-deleted .el-table__cell) { background: #ffe7e7 !important; }
 </style>
 
+<printDialog ref="printDialogRef" :title="'打印送货标签'" />
+

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

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

@@ -20,6 +20,15 @@ public class SupplierDeliveryBatchInput
     public string Ids { get; set; } = string.Empty;
 }
 
+public class SupplierDeliveryReplyDueDateInput
+{
+    [Required(ErrorMessage = "ids不能为空")]
+    public string Ids { get; set; } = string.Empty;
+
+    [Required(ErrorMessage = "jqhf不能为空")]
+    public string Jqhf { get; set; } = string.Empty;
+}
+
 public class DeliveryCloseInput
 {
     [Required(ErrorMessage = "交货单号不能为空")]

+ 226 - 48
server/Plugins/Admin.NET.Plugin.AiDOP/ProcurementExecution/SupplierDeliveryManagementService.cs

@@ -68,62 +68,51 @@ public class SupplierDeliveryManagementService : IDynamicApiController, ITransie
         var sql = """
             INSERT INTO `scm_jhjh_jq`
             (
-              `id`,`glid`, `wlbm`, `wlms`, `wlgg`, `cgdd`,
+              `glid1`,
+              `wlbm`, `wlms`, `wlgg`, `cgdd`,
               `jhdsl`, `wjhsl`, `jhd`, `yjjhrq`, `jqhf`,
               `type`, `flag`, `scrq`, `scrid`, `scrxm`,
-              `gysdm`, `gysmc`, `hfrid`, `hfrxm`, `hfsj`,
-              `qhdj`, `ddhh`, `dw`, `bzsl`, `ly`,`glid1`
+              `gysdm`, `gysmc`,
+              `hfrid`, `hfrxm`, `hfsj`,
+              `qhdj`, `ddhh`, `dw`, `bzsl`, `ly`
             )
             SELECT
-              UUID(),
-              CAST(d.RecID AS CHAR(50)) AS glid,
-              d.ItemNum AS wlbm,
-              i.Descr AS wlms,
-              i.Descr1 AS wlgg,
-              p.PurOrd AS cgdd,
-              CAST(d.QtyOrded AS CHAR(20)) AS jhdsl,
-              (d.QtyOrded - d.RctQty - d.ReceiptQty + d.QtyReturned) AS wjhsl,
-              '' AS jhd,
-              DATE_FORMAT(DATE(d.DueDate), '%Y-%m-%d') AS yjjhrq,
-              '' AS jqhf,
-              p.Potype AS `type`,
+              ds.id AS glid1,
+              ds.itemnum AS wlbm,
+              im.Descr AS wlms,
+              im.Descr1 AS wlgg,
+              ds.ponumber AS cgdd,
+              ds.schedqty AS jhdsl,
+              a.wjhsl AS wjhsl,
+              ds.dsnum AS jhd,
+              a.yjjhrq AS yjjhrq,
+              DATE_FORMAT(DATE(a.yjjhrq), '%Y-%m-%d') AS jqhf,
+              a.`type` AS `type`,
               2 AS flag,
-              '' AS scrq,
-              '' AS scrid,
-              '' AS scrxm,
-              p.Supp AS gysdm,
-              s.SortName AS gysmc,
+              a.scrq AS scrq,
+              a.scrid AS scrid,
+              a.scrxm AS scrxm,
+              a.gysdm AS gysdm,
+              a.gysmc AS gysmc,
               @hfrid AS hfrid,
               @hfrxm AS hfrxm,
-              DATE_FORMAT(NOW(), '%Y-%m-%d') AS hfsj,
-              '' AS qhdj,
-              d.Line AS ddhh,
-              d.UM AS dw,
-              d.StdPackQty AS bzsl,
-              '2' AS ly,
-              CAST(IFNULL(ds.id, d.RecID) AS CHAR(50)) AS glid1
-            FROM PurOrdMaster p
-            LEFT JOIN PurOrdDetail d
-              ON p.Domain = d.Domain
-             AND p.Potype = d.Potype
-             AND p.PurOrd = d.PurOrd
-            LEFT JOIN SuppMaster s
-              ON p.Domain = s.Domain
-             AND p.Supp = s.Supp
-            LEFT JOIN ItemMaster i
-              ON d.Domain = i.Domain
-             AND d.ItemNum = i.ItemNum
-            LEFT JOIN srm_polist_ds ds
-              ON p.PurOrd = ds.ponumber
-             AND p.Line = ds.poline
-            WHERE p.IsActive = 1
-              AND IFNULL(p.Status, '') <> 'C'
-              AND IFNULL(d.Status, '') <> 'C'
-              AND FIND_IN_SET(CAST(IFNULL(ds.id, d.RecID) AS CHAR(50)), REPLACE(IFNULL(@ids, ''), ' ', '')) > 0
+              DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s') AS hfsj,
+              a.qhdj AS qhdj,
+              a.ddhh AS ddhh,
+              a.dw AS dw,
+              a.bzsl AS bzsl,
+              a.ly AS ly
+            FROM srm_polist_ds ds
+            LEFT JOIN ItemMaster im
+              ON ds.itemnum = im.ItemNum
+            LEFT JOIN vscm_jhjh a
+              ON a.cgdd = ds.ponumber
+             AND a.ddhh = ds.poline
+            WHERE FIND_IN_SET(CAST(ds.id AS CHAR(50)), REPLACE(IFNULL(@ids, ''), ' ', '')) > 0
               AND NOT EXISTS (
-                    SELECT 1 FROM scm_jhjh_jq x
-                    WHERE x.flag = 2
-                      AND x.glid1 = CAST(IFNULL(ds.id, d.RecID) AS CHAR(50))
+                SELECT 1 FROM scm_jhjh_jq x
+                WHERE x.glid1 = ds.id
+                  AND FIND_IN_SET(CAST(x.glid1 AS CHAR(50)), REPLACE(IFNULL(@ids, ''), ' ', '')) > 0
               );
             """;
         await _db.Ado.ExecuteCommandAsync(sql, new List<SugarParameter>
@@ -135,6 +124,195 @@ public class SupplierDeliveryManagementService : IDynamicApiController, ITransie
         return new { message = "发布成功" };
     }
 
+    [DisplayName("供应商交货管理按计划日期回复")]
+    [HttpPost("supplier-delivery/reply-by-plan-date")]
+    public async Task<object> ReplyByPlanDate([FromBody] SupplierDeliveryBatchInput input)
+    {
+        if (string.IsNullOrWhiteSpace(input.Ids))
+            throw Oops.Oh("缺少勾选行ID(ids)");
+
+        var userId = _userManager.UserId.ToString();
+        var userName = _userManager.Account ?? "system";
+
+        // 说明:
+        // - 需求来自 SQLServer 写法:STRING_SPLIT(@ids, ',')
+        // - MySQL 使用 FIND_IN_SET 判断逗号分隔集合
+        var sql = """
+            INSERT INTO `scm_jhjh_jq`
+            (
+              `glid`, `wlbm`, `wlms`, `wlgg`, `cgdd`, `jhdsl`, `wjhsl`,
+              `jhd`, `yjjhrq`, `jqhf`, `type`, `flag`,
+              `scrq`, `scrid`, `scrxm`,
+              `gysdm`, `gysmc`,
+              `hfrid`, `hfrxm`, `hfsj`,
+              `qhdj`, `ddhh`, `dw`, `bzsl`, `ly`
+            )
+            SELECT
+              x.id AS glid,
+              x.wlbm,
+              x.wlms,
+              x.wlgg,
+              x.cgdd,
+              x.jhdsl,
+              x.wjhsl,
+              x.jhd,
+              x.yjjhrq,
+              x.jqhf,
+              x.type,
+              0 AS flag,
+              x.scrq,
+              x.scrid,
+              x.scrxm,
+              x.gysdm,
+              x.gysmc,
+              @userid AS hfrid,
+              @username AS hfrxm,
+              DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s') AS hfsj,
+              x.qhdj,
+              x.ddhh,
+              x.dw,
+              x.bzsl,
+              x.ly
+            FROM (
+              SELECT
+                CAST(IFNULL(ds.id, a.id) AS CHAR(50)) AS id,
+                a.wlbm,
+                a.wlms,
+                a.wlgg,
+                a.cgdd,
+                a.jhdsl,
+                a.wjhsl,
+                a.jhd,
+                a.yjjhrq,
+                DATE_FORMAT(DATE(a.yjjhrq), '%Y-%m-%d') AS jqhf,
+                a.type,
+                a.scrq,
+                a.scrid,
+                a.scrxm,
+                a.gysdm,
+                a.gysmc,
+                a.qhdj,
+                a.ddhh,
+                a.dw,
+                a.bzsl,
+                a.ly
+              FROM vscm_jhjh a
+              LEFT JOIN (
+                SELECT * FROM srm_polist_ds
+                WHERE status = 'P' AND isactive = 1 AND restQTY > 0
+              ) ds
+                ON a.cgdd = ds.ponumber AND a.ddhh = ds.poline
+            ) x
+            WHERE FIND_IN_SET(x.id, REPLACE(IFNULL(@ids, ''), ' ', '')) > 0
+              AND NOT EXISTS (
+                SELECT 1 FROM scm_jhjh_jq t
+                WHERE t.flag = 0 AND t.glid = x.id
+              );
+            """;
+
+        await _db.Ado.ExecuteCommandAsync(sql, new List<SugarParameter>
+        {
+            new("@ids", input.Ids),
+            new("@userid", userId),
+            new("@username", userName),
+        });
+
+        return new { message = "按计划日期回复成功" };
+    }
+
+    [DisplayName("供应商交货管理回复交期")]
+    [HttpPost("supplier-delivery/reply-due-date")]
+    public async Task<object> ReplyDueDate([FromBody] SupplierDeliveryReplyDueDateInput input)
+    {
+        if (string.IsNullOrWhiteSpace(input.Ids))
+            throw Oops.Oh("缺少勾选行ID(ids)");
+        if (string.IsNullOrWhiteSpace(input.Jqhf))
+            throw Oops.Oh("缺少交期回复日期(jqhf)");
+
+        var userId = _userManager.UserId.ToString();
+        var userName = _userManager.Account ?? "system";
+
+        // 需求 SQLServer 写法:STRING_SPLIT(@ids, ',')
+        // MySQL 使用 FIND_IN_SET 判断逗号分隔集合
+        var sql = """
+            INSERT INTO `scm_jhjh_jq`
+            (
+              `glid`, `wlbm`, `wlms`, `wlgg`, `cgdd`,
+              `jhdsl`, `wjhsl`, `jhd`, `yjjhrq`, `jqhf`,
+              `type`, `flag`,
+              `scrq`, `scrid`, `scrxm`,
+              `gysdm`, `gysmc`,
+              `hfrid`, `hfrxm`, `hfsj`,
+              `qhdj`, `ddhh`, `dw`, `bzsl`, `ly`
+            )
+            SELECT
+              x.id AS glid,
+              x.wlbm,
+              x.wlms,
+              x.wlgg,
+              x.cgdd,
+              x.jhdsl,
+              x.wjhsl,
+              x.jhd,
+              x.yjjhrq,
+              @jqhf AS jqhf,
+              x.type,
+              0 AS flag,
+              x.scrq,
+              x.scrid,
+              x.scrxm,
+              x.gysdm,
+              x.gysmc,
+              @userid AS hfrid,
+              @username AS hfrxm,
+              DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s') AS hfsj,
+              x.qhdj,
+              x.ddhh,
+              x.dw,
+              x.bzsl,
+              x.ly
+            FROM (
+              SELECT
+                CAST(IFNULL(ds.id, a.id) AS CHAR(50)) AS id,
+                a.wlbm,
+                a.wlms,
+                a.wlgg,
+                a.cgdd,
+                a.jhdsl,
+                a.wjhsl,
+                a.jhd,
+                a.yjjhrq,
+                a.type,
+                a.scrq,
+                a.scrid,
+                a.scrxm,
+                a.gysdm,
+                a.gysmc,
+                a.qhdj,
+                a.ddhh,
+                a.dw,
+                a.bzsl,
+                a.ly
+              FROM vscm_jhjh a
+              LEFT JOIN (
+                SELECT * FROM srm_polist_ds
+                WHERE status='P' AND isactive=1 AND restQTY>0
+              ) ds ON a.cgdd = ds.ponumber AND a.ddhh = ds.poline
+            ) x
+            WHERE FIND_IN_SET(x.id, REPLACE(IFNULL(@ids, ''), ' ', '')) > 0;
+            """;
+
+        await _db.Ado.ExecuteCommandAsync(sql, new List<SugarParameter>
+        {
+            new("@ids", input.Ids),
+            new("@jqhf", input.Jqhf.Trim()),
+            new("@userid", userId),
+            new("@username", userName),
+        });
+
+        return new { message = "回复交期成功" };
+    }
+
     [DisplayName("交货单关闭")]
     [HttpPost("supplier-delivery/close")]
     public async Task<object> CloseDelivery([FromBody] DeliveryCloseInput input)

+ 600 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/ProcurementExecution/SupplierShipmentService.cs

@@ -17,6 +17,8 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
     private readonly SqlSugarRepository<ScmShd> _masterRep;
     private readonly SqlSugarRepository<ScmShdzb> _detailRep;
     private readonly UserManager _userManager;
+    private const int ShpcSerialWidth = 3; // yyMMdd + 3位流水:260508001
+    private const int LabelXhStart = 10001;
 
     public SupplierShipmentService(
         ISqlSugarClient db,
@@ -266,8 +268,540 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
 
     [DisplayName("生成标签(预留)")]
     [HttpPost("supplier-shipment/generate-label")]
-    public Task<object> GenerateLabel([FromBody] SupplierShipmentOperationInput input)
-        => Task.FromResult<object>(new { message = $"发货单{input.Id}:功能预留,暂未启用" });
+    public async Task<object> GenerateLabel([FromBody] SupplierShipmentOperationInput input)
+    {
+        if (input.Id <= 0)
+            throw Oops.Oh("缺少发货单ID(id)");
+
+        var userName = _userManager.Account ?? "system";
+
+        var master = await _masterRep.GetFirstAsync(x => x.Id == input.Id) ?? throw Oops.Oh("发货单不存在");
+        if (string.IsNullOrWhiteSpace(master.Shddh))
+            throw Oops.Oh("发货单缺少发货单编号(shddh)");
+
+        var shddh = master.Shddh.Trim();
+        var gysmc = master.ShPurchaseName ?? string.Empty;
+        var gysdm = master.ShPurchaseNum ?? string.Empty;
+
+        // Domain(工厂编码)
+        var domain = await _db.Ado.GetStringAsync(
+            "SELECT Domain FROM GeneralizedCodeMaster WHERE FldName='SystemConfig' AND Val='CompanyCode' LIMIT 1");
+        domain = string.IsNullOrWhiteSpace(domain) ? string.Empty : domain.Trim();
+
+        string? lockKey = null;
+        try
+        {
+            var tran = await _db.Ado.UseTranAsync(async () =>
+            {
+                // 1) 归档旧标签
+                await _db.Ado.ExecuteCommandAsync(
+                    """
+                    INSERT INTO scm_shbqhis
+                    (glid, sh_material_code, sh_material_name, sh_material_ggxh, sh_delivery_quantity,
+                     sh_material_dw, remarks, bzsl, order_type, po_billno, shdh, shdhh, scrq, scph, xh,
+                     gysdm, gysmc, po_billline, bbh, th, yt, ccrq, shpc, jhdbh, jhdhh)
+                    SELECT
+                     glid, sh_material_code, sh_material_name, sh_material_ggxh, sh_delivery_quantity,
+                     sh_material_dw, remarks, bzsl, order_type, po_billno, shdh, shdhh, scrq, scph, xh,
+                     gysdm, gysmc, po_billline, bbh, th, yt, ccrq, shpc, jhdbh, jhdhh
+                    FROM scm_shbq WHERE shdh=@shdh;
+                    """,
+                    new List<SugarParameter> { new("@shdh", shddh) });
+
+                await _db.Ado.ExecuteCommandAsync(
+                    "DELETE FROM scm_shbq WHERE shdh=@shdh",
+                    new List<SugarParameter> { new("@shdh", shddh) });
+
+                // 2) 需要补 shpc 的明细(按物料+供应商批号去重)
+                var needShpc = await _db.Ado.SqlQueryAsync<NeedShpcRow>(
+                    """
+                    SELECT DISTINCT
+                        b.sh_material_code AS wlbm,
+                        IFNULL(b.scph, '') AS scph
+                    FROM scm_shd a
+                    INNER JOIN scm_shdzb b ON a.id = b.glid
+                    WHERE a.shddh = @shddh
+                      AND IFNULL(b.sh_material_code, '') <> ''
+                      AND IFNULL(b.shpc, '') = '';
+                    """,
+                    new List<SugarParameter> { new("@shddh", shddh) });
+
+                if (needShpc.Count > 0)
+                {
+                    // 并发安全:按天加锁 + 取最大流水 + 批量生成
+                    var prefix = DateTime.Now.ToString("yyMMdd");
+                    lockKey = $"scm_shpc_seq_{prefix}";
+                    await AcquireMySqlLockAsync(lockKey, 10);
+
+                    var nextSerial = await GetNextDailySerialAsync(prefix, domain);
+                    var pairs = needShpc
+                        .OrderBy(x => x.Wlbm ?? string.Empty)
+                        .ThenBy(x => x.Scph ?? string.Empty)
+                        .Select((x, idx) => new
+                        {
+                            x.Wlbm,
+                            x.Scph,
+                            Shpc = $"{prefix}{(nextSerial + idx).ToString().PadLeft(ShpcSerialWidth, '0')}"
+                        })
+                        .ToList();
+
+                    // 更新 scm_shdzb.shpc(同一发货单下:物料+供应商批号相同的行统一批次号)
+                    foreach (var p in pairs)
+                    {
+                        await _db.Ado.ExecuteCommandAsync(
+                            """
+                            UPDATE scm_shdzb b
+                            INNER JOIN scm_shd a ON a.id = b.glid
+                            SET b.shpc = @shpc
+                            WHERE a.shddh = @shddh
+                              AND b.sh_material_code = @wlbm
+                              AND IFNULL(b.scph, '') = IFNULL(@scph, '')
+                              AND IFNULL(b.shpc, '') = '';
+                            """,
+                            new List<SugarParameter>
+                            {
+                                new("@shpc", p.Shpc),
+                                new("@shddh", shddh),
+                                new("@wlbm", p.Wlbm ?? string.Empty),
+                                new("@scph", p.Scph ?? string.Empty),
+                            });
+                    }
+
+                    // 兼容:如果线上存在 scm_shdshph,则同步写入(仓库脚本里未包含该表)
+                    var shdshphExists = await TableExistsAsync("scm_shdshph");
+                    if (shdshphExists)
+                    {
+                        foreach (var p in pairs)
+                        {
+                            await _db.Ado.ExecuteCommandAsync(
+                                """
+                                INSERT INTO scm_shdshph (shpc, wlbm, scph, shdh, xh, gysdm, create_time)
+                                SELECT @shpc, @wlbm, @scph, @shdh, @xh, @gysdm, NOW()
+                                FROM DUAL
+                                WHERE NOT EXISTS (
+                                    SELECT 1 FROM scm_shdshph
+                                    WHERE shdh=@shdh AND wlbm=@wlbm AND IFNULL(scph,'')=IFNULL(@scph,'')
+                                );
+                                """,
+                                new List<SugarParameter>
+                                {
+                                    new("@shpc", p.Shpc),
+                                    new("@wlbm", p.Wlbm ?? string.Empty),
+                                    new("@scph", p.Scph ?? string.Empty),
+                                    new("@shdh", shddh),
+                                    new("@xh", p.Shpc),
+                                    new("@gysdm", gysdm),
+                                });
+                        }
+                    }
+
+                    // 推进 NbrControl.NextValue,确保续号(仅当没有历史 shpc 且使用了 NbrControl 作为起点时也能正确续)
+                    // 这里直接推进到“本批次最后一个流水号”,下次取号将从 NextValue+1 开始。
+                    var lastSerial = nextSerial + pairs.Count - 1;
+                    await BumpNbrControlNextValueAsync(domain, lastSerial);
+                }
+
+                // 3) 补 jhdhh(同一发货单:为空的明细按 id 递增,起始 max(jhdhh) or 1000)
+                var shdid = await _db.Ado.GetLongAsync(
+                    "SELECT id FROM scm_shd WHERE shddh=@shddh LIMIT 1",
+                    new List<SugarParameter> { new("@shddh", shddh) });
+
+                var startNo = await _db.Ado.GetIntAsync(
+                    "SELECT IFNULL(MAX(CAST(IFNULL(jhdhh,'') AS SIGNED)), 1000) FROM scm_shdzb WHERE glid=@glid",
+                    new List<SugarParameter> { new("@glid", shdid.ToString()) });
+
+                var jhdhhNeed = await _db.Ado.SqlQueryAsync<IdOnlyRow>(
+                    "SELECT id FROM scm_shdzb WHERE glid=@glid AND IFNULL(jhdhh,'')='' ORDER BY id",
+                    new List<SugarParameter> { new("@glid", shdid.ToString()) });
+
+                var seq = startNo;
+                foreach (var r in jhdhhNeed)
+                {
+                    seq++;
+                    await _db.Ado.ExecuteCommandAsync(
+                        "UPDATE scm_shdzb SET jhdhh=@jhdhh WHERE id=@id",
+                        new List<SugarParameter>
+                        {
+                            new("@jhdhh", seq.ToString()),
+                            new("@id", r.Id)
+                        });
+                }
+
+                // 4) 生成 scm_shbq(按 bqsl 拆分;每箱 bzsl,最后一箱余数)
+                var sourceRows = await _db.Ado.SqlQueryAsync<LabelSourceRow>(
+                    """
+                    SELECT
+                        a.shddh AS shddh,
+                        b.id AS detailId,
+                        b.glid AS glid,
+                        b.sh_material_code AS wlbm,
+                        b.sh_material_name AS wlmc,
+                        b.sh_material_ggxh AS ggxh,
+                        b.sh_delivery_quantity AS shsl,
+                        b.sh_material_dw AS dw,
+                        b.remarks AS bz,
+                        b.bzsl AS bzsl,
+                        b.bqsl AS bqsl,
+                        b.order_type AS ddlx,
+                        b.po_bill AS ddh,
+                        CAST(IFNULL(b.po_billline, '0') AS SIGNED) AS po_billline,
+                        b.hh AS hh,
+                        b.scrq AS scrq,
+                        b.scph AS scph,
+                        IFNULL(im.Drawing, pd.Drawing) AS th,
+                        IFNULL(im.Rev, pd.Rev) AS bbh,
+                        a.sh_purchase_name AS gysmc,
+                        a.sh_purchase_num AS gysdm,
+                        pm.Usage AS usage,
+                        b.ccrq AS ccrq,
+                        a.jhshrq AS jhshrq,
+                        b.jhdbh AS jhdbh,
+                        b.jhdhh AS jhdhh,
+                        b.shpc AS shpc
+                    FROM scm_shd a
+                    INNER JOIN scm_shdzb b ON a.id = b.glid
+                    LEFT JOIN PurOrdMaster pm ON pm.PurOrd = b.po_bill
+                    LEFT JOIN PurOrdDetail pd ON pd.PurOrd = b.po_bill AND pd.Line = b.po_billline
+                    LEFT JOIN ItemMaster im ON im.ItemNum = b.sh_material_code
+                    WHERE a.shddh = @shddh;
+                    """,
+                    new List<SugarParameter> { new("@shddh", shddh) });
+
+                foreach (var row in sourceRows)
+                {
+                    var totalQty = row.Shsl ?? 0m;
+                    var packQty = row.Bzsl ?? 0m;
+                    var labelCnt = (int)Math.Max(0, row.Bqsl ?? 0m);
+                    if (totalQty <= 0 || labelCnt <= 0)
+                        continue;
+
+                    var xh = LabelXhStart;
+                    var remainQty = totalQty;
+                    var remainLabels = labelCnt;
+
+                    // 多箱:前 N-1 箱按包装数量
+                    while (remainLabels > 1)
+                    {
+                        await InsertLabelAsync(shddh, row, packQty, xh, gysdm, gysmc);
+                        remainQty -= packQty;
+                        remainLabels--;
+                        xh++;
+                    }
+
+                    if (remainQty > 0)
+                    {
+                        await InsertLabelAsync(shddh, row, remainQty, xh, gysdm, gysmc);
+                    }
+                }
+
+                // 5) 更新发货单状态
+                await _db.Ado.ExecuteCommandAsync(
+                    "UPDATE scm_shd SET shzt='待收', state=2, dycs=0 WHERE shddh=@shddh",
+                    new List<SugarParameter> { new("@shddh", shddh) });
+
+                // 6) MissedPrint:作废旧未打印(U)
+                await _db.Ado.ExecuteCommandAsync(
+                    """
+                    UPDATE MissedPrint SET
+                        PurOrd = CONCAT('作废_', IFNULL(PurOrd, '')),
+                        BarCode = CONCAT(CAST(RecID AS CHAR(20)), '_', IFNULL(BarCode, '')),
+                        Status = 'C',
+                        RelatedBarCode = '',
+                        UpdateTime = NOW(),
+                        UpdateUser = @u
+                    WHERE Domain = @domain
+                      AND ShipperNbr = @shddh
+                      AND Status = 'U';
+                    """,
+                    new List<SugarParameter>
+                    {
+                        new("@u", userName),
+                        new("@domain", domain),
+                        new("@shddh", shddh),
+                    });
+
+                // 7) MissedPrint:插入待打印(U)
+                await _db.Ado.ExecuteCommandAsync(
+                    """
+                    INSERT INTO MissedPrint
+                    (
+                        Domain, Site, PrintTime,
+                        ItemNum, Descr, Product, Carton, OrdNbr,
+                        PackingQty, Qty, Location, Status,
+                        Supply, LotSerial, CartonQty, BarCode,
+                        MoldNum, SuppLotSerial, ShipperNbr, ShipperLine,
+                        ProdDate, PurOrd, PurLine, PurQty, WorkOrd,
+                        LabelFormat, StandItem, EffSize, GP12CheckedQty, NetWeight,
+                        Remark, CreateTime, UpdateTime, CreateUser, UpdateUser,
+                        LevelChar, PurOrdDetBatchNbr, FirmString5, ExpireDate,
+                        Printer, Company, Checker, Position
+                    )
+                    SELECT
+                        @domain AS Domain,
+                        @domain AS Site,
+                        NULL AS PrintTime,
+                        s.sh_material_code AS ItemNum,
+                        s.sh_material_name AS Descr,
+                        s.sh_material_ggxh AS Product,
+                        RIGHT(s.xh, 4) AS Carton,
+                        s.po_billno AS OrdNbr,
+                        s.bzsl AS PackingQty,
+                        s.sh_delivery_quantity AS Qty,
+                        NULL AS Location,
+                        'U' AS Status,
+                        s.gysdm AS Supply,
+                        s.shpc AS LotSerial,
+                        1 AS CartonQty,
+                        s.xh AS BarCode,
+                        NULL AS MoldNum,
+                        s.scph AS SuppLotSerial,
+                        s.shdh AS ShipperNbr,
+                        s.shdhh AS ShipperLine,
+                        STR_TO_DATE(s.scrq, '%Y-%m-%d') AS ProdDate,
+                        s.po_billno AS PurOrd,
+                        s.po_billline AS PurLine,
+                        s.sh_delivery_quantity AS PurQty,
+                        NULL AS WorkOrd,
+                        'cl01' AS LabelFormat,
+                        s.po_billno AS StandItem,
+                        s.po_billline AS EffSize,
+                        s.sh_delivery_quantity AS GP12CheckedQty,
+                        0 AS NetWeight,
+                        s.remarks AS Remark,
+                        NOW() AS CreateTime,
+                        NOW() AS UpdateTime,
+                        @u AS CreateUser,
+                        @u AS UpdateUser,
+                        s.bbh AS LevelChar,
+                        s.jhdbh AS PurOrdDetBatchNbr,
+                        p.ActiveRlseID1 AS FirmString5,
+                        CASE
+                            WHEN i.SuppWarranty = 1 THEN
+                                CASE
+                                    WHEN UPPER(i.WarrantyCode)='D' THEN DATE_ADD(STR_TO_DATE(s.scrq, '%Y-%m-%d'), INTERVAL IFNULL(i.DaysBetweenPM,0) DAY)
+                                    WHEN UPPER(i.WarrantyCode)='Y' THEN DATE_ADD(STR_TO_DATE(s.scrq, '%Y-%m-%d'), INTERVAL IFNULL(i.DaysBetweenPM,0) YEAR)
+                                    WHEN UPPER(i.WarrantyCode)='M' THEN DATE_ADD(STR_TO_DATE(s.scrq, '%Y-%m-%d'), INTERVAL IFNULL(i.DaysBetweenPM,0) MONTH)
+                                    ELSE DATE_ADD(STR_TO_DATE(s.scrq, '%Y-%m-%d'), INTERVAL IFNULL(i.DaysBetweenPM,0) DAY)
+                                END
+                            ELSE NULL
+                        END AS ExpireDate,
+                        '' AS Printer,
+                        '' AS Company,
+                        '' AS Checker,
+                        '' AS Position
+                    FROM scm_shbq s
+                    LEFT JOIN PurOrdDetail p ON p.PurOrd = s.po_billno AND p.Line = s.po_billline
+                    LEFT JOIN ItemMaster i ON i.ItemNum = s.sh_material_code
+                    WHERE s.shdh = @shddh;
+                    """,
+                    new List<SugarParameter>
+                    {
+                        new("@domain", domain),
+                        new("@u", userName),
+                        new("@shddh", shddh),
+                    });
+            });
+
+            if (!tran.IsSuccess)
+            {
+                var msg = $"生成标签失败,{tran.ErrorMessage}";
+                return new { success = false, msg, message = msg };
+            }
+
+            var ok = "生成标签成功!";
+            return new { success = true, msg = ok, message = ok };
+        }
+        finally
+        {
+            if (!string.IsNullOrWhiteSpace(lockKey))
+            {
+                await ReleaseMySqlLockAsync(lockKey);
+            }
+        }
+    }
+
+    private async Task InsertLabelAsync(string shddh, LabelSourceRow row, decimal qty, int xh, string gysdm, string gysmc)
+    {
+        var jhdhh3 = Right(row.Jhdhh, 3);
+        var xh4 = Right(xh.ToString(), 4);
+        var bar = $"{shddh}{jhdhh3}{xh4}";
+
+        await _db.Ado.ExecuteCommandAsync(
+            """
+            INSERT INTO scm_shbq
+            (glid, sh_material_code, sh_material_name, sh_material_ggxh, sh_delivery_quantity,
+             sh_material_dw, remarks, bzsl, order_type, po_billno, shdh, shdhh, scrq, scph, xh,
+             gysdm, gysmc, po_billline, bbh, th, yt, ccrq, shpc, jhdbh, jhdhh)
+            VALUES
+            (@glid, @wlbm, @wlmc, @ggxh, @qty,
+             @dw, @bz, @bzsl, @ddlx, @ddh, @shdh, @hh, @scrq, @scph, @xh,
+             @gysdm, @gysmc, @pohh, @bbh, @th, @yt, @ccrq, @shpc, @jhdbh, @jhdhh);
+            """,
+            new List<SugarParameter>
+            {
+                new("@glid", row.DetailId),
+                new("@wlbm", row.Wlbm ?? string.Empty),
+                new("@wlmc", row.Wlmc ?? string.Empty),
+                new("@ggxh", row.Ggxh ?? string.Empty),
+                new("@qty", qty),
+                new("@dw", row.Dw ?? string.Empty),
+                new("@bz", row.Bz ?? string.Empty),
+                new("@bzsl", row.Bzsl ?? 0m),
+                new("@ddlx", row.Ddlx ?? string.Empty),
+                new("@ddh", row.Ddh ?? string.Empty),
+                new("@shdh", shddh),
+                new("@hh", row.Hh ?? 0),
+                new("@scrq", row.Scrq ?? string.Empty),
+                new("@scph", row.Scph ?? string.Empty),
+                new("@xh", bar),
+                new("@gysdm", gysdm),
+                new("@gysmc", gysmc),
+                new("@pohh", row.PoBillLine ?? 0),
+                new("@bbh", row.Bbh ?? string.Empty),
+                new("@th", row.Th ?? string.Empty),
+                new("@yt", row.Usage ?? string.Empty),
+                new("@ccrq", row.Ccrq ?? string.Empty),
+                new("@shpc", row.Shpc ?? string.Empty),
+                new("@jhdbh", row.Jhdbh ?? string.Empty),
+                new("@jhdhh", row.Jhdhh ?? string.Empty),
+            });
+    }
+
+    private static string Right(string? s, int len)
+    {
+        if (string.IsNullOrEmpty(s)) return new string('0', len);
+        var t = s.Trim();
+        return t.Length <= len ? t.PadLeft(len, '0') : t[^len..];
+    }
+
+    private async Task AcquireMySqlLockAsync(string key, int timeoutSeconds)
+    {
+        var ok = await _db.Ado.GetIntAsync("SELECT GET_LOCK(@k, @t)", new List<SugarParameter>
+        {
+            new("@k", key),
+            new("@t", timeoutSeconds),
+        });
+        if (ok != 1)
+            throw Oops.Oh($"生成批次号锁等待超时({key})");
+    }
+
+    private Task ReleaseMySqlLockAsync(string key)
+        => _db.Ado.ExecuteCommandAsync("SELECT RELEASE_LOCK(@k)", new List<SugarParameter> { new("@k", key) });
+
+    private async Task<int> GetNextDailySerialAsync(string prefix, string domain)
+    {
+        // 从已存在的批次号中取当天最大流水:yyMMdd + 3位
+        // 优先 scm_shdzb,其次 scm_shbq(兼容历史数据来源)
+        var max1 = await _db.Ado.GetStringAsync(
+            "SELECT MAX(shpc) FROM scm_shdzb WHERE shpc LIKE @pfx",
+            new List<SugarParameter> { new("@pfx", $"{prefix}%") });
+        var max2 = await _db.Ado.GetStringAsync(
+            "SELECT MAX(shpc) FROM scm_shbq WHERE shpc LIKE @pfx",
+            new List<SugarParameter> { new("@pfx", $"{prefix}%") });
+
+        var max = string.CompareOrdinal(max1 ?? string.Empty, max2 ?? string.Empty) >= 0 ? max1 : max2;
+        if (string.IsNullOrWhiteSpace(max) || max!.Length < prefix.Length + ShpcSerialWidth)
+        {
+            // 兜底:读取 NbrControl(NbrType='WoLot')的 NextValue + 1
+            // 说明:有些环境可能会清理历史批次号表;此时用 NbrControl 保证连续。
+            var nv = await _db.Ado.GetIntAsync(
+                """
+                SELECT IFNULL(NextValue, 0)
+                FROM NbrControl
+                WHERE NbrType='WoLot'
+                  AND (
+                        domain_code = @d
+                        OR Domain = @d
+                        OR IFNULL(@d,'') = ''
+                      )
+                ORDER BY RecID
+                LIMIT 1
+                """,
+                new List<SugarParameter> { new("@d", domain ?? string.Empty) });
+            // 用户要求:NextValue+1 为下一流水号起点
+            return Math.Max(1, nv + 1);
+        }
+
+        var suffix = max.Substring(prefix.Length);
+        return int.TryParse(suffix, out var n) ? n + 1 : 1;
+    }
+
+    private async Task BumpNbrControlNextValueAsync(string domain, int lastSerial)
+    {
+        // 保守推进:仅当新值更大时更新,避免其它流程并发推进导致回退。
+        await _db.Ado.ExecuteCommandAsync(
+            """
+            UPDATE NbrControl
+            SET NextValue = CASE
+                WHEN IFNULL(NextValue, 0) < @v THEN @v
+                ELSE IFNULL(NextValue, 0)
+            END,
+                UpdateTime = NOW()
+            WHERE NbrType = 'WoLot'
+              AND (
+                    domain_code = @d
+                    OR Domain = @d
+                    OR IFNULL(@d,'') = ''
+                  );
+            """,
+            new List<SugarParameter>
+            {
+                new("@d", domain ?? string.Empty),
+                new("@v", lastSerial),
+            });
+    }
+
+    private async Task<bool> TableExistsAsync(string tableName)
+    {
+        var cnt = await _db.Ado.GetIntAsync(
+            """
+            SELECT COUNT(*)
+            FROM information_schema.tables
+            WHERE table_schema = DATABASE()
+              AND table_name = @t
+            """,
+            new List<SugarParameter> { new("@t", tableName) });
+        return cnt > 0;
+    }
+
+    private sealed class NeedShpcRow
+    {
+        public string? Wlbm { get; set; }
+        public string? Scph { get; set; }
+    }
+
+    private sealed class IdOnlyRow
+    {
+        public long Id { get; set; }
+    }
+
+    private sealed class LabelSourceRow
+    {
+        public string? Shddh { get; set; }
+        public long DetailId { get; set; }
+        public string? Glid { get; set; }
+        public string? Wlbm { get; set; }
+        public string? Wlmc { get; set; }
+        public string? Ggxh { get; set; }
+        public decimal? Shsl { get; set; }
+        public string? Dw { get; set; }
+        public string? Bz { get; set; }
+        public decimal? Bzsl { get; set; }
+        public decimal? Bqsl { get; set; }
+        public string? Ddlx { get; set; }
+        public string? Ddh { get; set; }
+        public int? PoBillLine { get; set; }
+        public int? Hh { get; set; }
+        public string? Scrq { get; set; }
+        public string? Scph { get; set; }
+        public string? Th { get; set; }
+        public string? Bbh { get; set; }
+        public string? Usage { get; set; }
+        public string? Ccrq { get; set; }
+        public string? Jhdbh { get; set; }
+        public string? Jhdhh { get; set; }
+        public string? Shpc { get; set; }
+    }
 
     [DisplayName("打印送货单(预留)")]
     [HttpPost("supplier-shipment/print-shipping-note")]
@@ -279,6 +813,45 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
     public Task<object> PrintLabel([FromBody] SupplierShipmentOperationInput input)
         => Task.FromResult<object>(new { message = $"发货单{input.Id}:功能预留,暂未启用" });
 
+    [DisplayName("发货单标签数据(scm_shbq)")]
+    [HttpGet("supplier-shipment/label-data")]
+    public async Task<object> GetLabelData([FromQuery] string shddh)
+    {
+        if (string.IsNullOrWhiteSpace(shddh))
+            throw Oops.Oh("缺少发货单编号(shddh)");
+
+        var pars = new List<SugarParameter> { new("@shddh", shddh.Trim()) };
+        const string sql = """
+                           SELECT DISTINCT
+                               glid,
+                               sh_material_code AS wlbm,
+                               sh_material_name AS wlmc,
+                               CONCAT(IFNULL(sh_material_name, ''), IFNULL(sh_material_ggxh, '')) AS ggxh,
+                               sh_delivery_quantity AS zxsl,
+                               sh_material_dw AS dw,
+                               remarks,
+                               bzsl AS bz,
+                               order_type AS ddlx,
+                               po_billno AS ddh,
+                               shdh AS shdh,
+                               shdhh AS shdhh,
+                               DATE_FORMAT(scrq, '%Y.%m.%d') AS scrq,
+                               scph,
+                               xh,
+                               gysmc,
+                               th,
+                               bbh,
+                               yt,
+                               ccrq,
+                               shpc
+                           FROM scm_shbq
+                           WHERE shdh = @shddh
+                           """;
+
+        var list = await _db.Ado.SqlQueryAsync<SupplierShipmentLabelRow>(sql, pars);
+        return new { list };
+    }
+
     private async Task SaveDetailsAsync(long masterId, List<SupplierShipmentDetailInput> inputDetails)
     {
         var dbDetails = await _detailRep.AsQueryable().Where(x => x.Glid == masterId.ToString()).ToListAsync();
@@ -527,5 +1100,30 @@ public class SupplierShipmentService : IDynamicApiController, ITransient
         public string? Jhdbh { get; set; }
         public string? ShMaterialGgxh { get; set; }
     }
+
+    private sealed class SupplierShipmentLabelRow
+    {
+        public long? Glid { get; set; }
+        public string? Wlbm { get; set; }
+        public string? Wlmc { get; set; }
+        public string? Ggxh { get; set; }
+        public decimal? Zxsl { get; set; }
+        public string? Dw { get; set; }
+        public string? Remarks { get; set; }
+        public decimal? Bz { get; set; }
+        public string? Ddlx { get; set; }
+        public string? Ddh { get; set; }
+        public string? Shdh { get; set; }
+        public string? Shdhh { get; set; }
+        public string? Scrq { get; set; }
+        public string? Scph { get; set; }
+        public string? Xh { get; set; }
+        public string? Gysmc { get; set; }
+        public string? Th { get; set; }
+        public string? Bbh { get; set; }
+        public string? Yt { get; set; }
+        public string? Ccrq { get; set; }
+        public string? Shpc { get; set; }
+    }
 }
 

+ 2 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Production/Dto/ShopCalendarWorkCtrDto.cs

@@ -43,13 +43,13 @@ public class ShopCalendarWorkCtrSaveInput
     public string? WorkCtr { get; set; }
 
     /// <summary>班次1开始,小数小时或 HH:mm</summary>
-    public string? ShiftsStart1 { get; set; }
+    public decimal? ShiftsStart1 { get; set; }
 
     /// <summary>班次1时长(小时)</summary>
     public decimal? ShiftsHours1 { get; set; }
 
     /// <summary>班次2开始(可选)</summary>
-    public string? ShiftsStart2 { get; set; }
+    public decimal? ShiftsStart2 { get; set; }
 
     /// <summary>班次2时长(可选)</summary>
     public decimal? ShiftsHours2 { get; set; }

+ 3 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/Production/Entity/PeriodSequenceDet.cs

@@ -8,7 +8,7 @@ public class PeriodSequenceDet
 {
     /// <summary>自增列</summary>
     [SugarColumn(ColumnName = "RecID", IsPrimaryKey = true, IsIdentity = true)]
-    public long RecID { get; set; }
+    public int RecID { get; set; }
 
     [SugarColumn(ColumnName = "Domain", Length = 8, IsNullable = true)]
     public string? Domain { get; set; }
@@ -23,7 +23,7 @@ public class PeriodSequenceDet
     public string? Line { get; set; }
 
     [SugarColumn(ColumnName = "Op", Length = 6, IsNullable = true)]
-    public string? Op { get; set; }
+    public int? Op { get; set; }
 
     [SugarColumn(ColumnName = "WorkCtr", Length = 8, IsNullable = true)]
     public string? WorkCtr { get; set; }
@@ -35,7 +35,7 @@ public class PeriodSequenceDet
     public DateTime? PlanDate { get; set; }
 
     [SugarColumn(ColumnName = "Period", Length = 8, IsNullable = true)]
-    public string? Period { get; set; }
+    public short? Period { get; set; }
 
     [SugarColumn(ColumnName = "Sequence", IsNullable = true)]
     public int? Sequence { get; set; }

+ 3 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/Production/Entity/ScheduleResultOpMaster.cs

@@ -26,7 +26,7 @@ public class ScheduleResultOpMaster
     public string? ItemNum { get; set; }
 
     [SugarColumn(ColumnName = "Op", Length = 6, IsNullable = true)]
-    public string? Op { get; set; }
+    public int? Op { get; set; }
 
     [SugarColumn(ColumnName = "OpDescr", Length = 256, IsNullable = true)]
     public string? OpDescr { get; set; }
@@ -65,7 +65,7 @@ public class ScheduleResultOpMaster
     public DateTime? WorkEndTime { get; set; }
 
     [SugarColumn(ColumnName = "WorkActivateTime", IsNullable = true)]
-    public decimal? WorkActivateTime { get; set; }
+    public DateTime? WorkActivateTime { get; set; }
 
     [SugarColumn(ColumnName = "DeviceAllocationCount", IsNullable = true)]
     public int? DeviceAllocationCount { get; set; }
@@ -89,7 +89,7 @@ public class ScheduleResultOpMaster
     public string? SkillNo { get; set; }
 
     [SugarColumn(ColumnName = "AssignedPersonnelCount", IsNullable = true)]
-    public int? AssignedPersonnelCount { get; set; }
+    public decimal? AssignedPersonnelCount { get; set; }
 
     [SugarColumn(ColumnName = "Remark", Length = 512, IsNullable = true)]
     public string? Remark { get; set; }

+ 4 - 4
server/Plugins/Admin.NET.Plugin.AiDOP/Production/Entity/ShopCalendarWorkCtr.cs

@@ -35,25 +35,25 @@ public class ShopCalendarWorkCtr
     public int? AllowOverTime { get; set; }
 
     [SugarColumn(ColumnName = "ShiftsStart1", Length = 8, IsNullable = true)]
-    public string? ShiftsStart1 { get; set; }
+    public decimal? ShiftsStart1 { get; set; }
 
     [SugarColumn(ColumnName = "ShiftsHours1", IsNullable = true)]
     public decimal? ShiftsHours1 { get; set; }
 
     [SugarColumn(ColumnName = "ShiftsStart2", Length = 8, IsNullable = true)]
-    public string? ShiftsStart2 { get; set; }
+    public decimal? ShiftsStart2 { get; set; }
 
     [SugarColumn(ColumnName = "ShiftsHours2", IsNullable = true)]
     public decimal? ShiftsHours2 { get; set; }
 
     [SugarColumn(ColumnName = "ShiftsStart3", Length = 8, IsNullable = true)]
-    public string? ShiftsStart3 { get; set; }
+    public decimal? ShiftsStart3 { get; set; }
 
     [SugarColumn(ColumnName = "ShiftsHours3", IsNullable = true)]
     public decimal? ShiftsHours3 { get; set; }
 
     [SugarColumn(ColumnName = "ShiftsStart4", Length = 8, IsNullable = true)]
-    public string? ShiftsStart4 { get; set; }
+    public decimal? ShiftsStart4 { get; set; }
 
     [SugarColumn(ColumnName = "ShiftsHours4", IsNullable = true)]
     public decimal? ShiftsHours4 { get; set; }

+ 2 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Production/ShopCalendarWorkCtrService.cs

@@ -118,8 +118,8 @@ public class ShopCalendarWorkCtrService : IDynamicApiController, ITransient
         var workCtr = string.IsNullOrWhiteSpace(input.WorkCtr) ? null : Truncate(input.WorkCtr.Trim(), 8);
         var ufld1 = string.IsNullOrWhiteSpace(input.Ufld1) ? null : Truncate(input.Ufld1.Trim(), 8);
 
-        var s1 = NormalizeShiftDecimal(ParseTimeToDecimal(input.ShiftsStart1));
-        var s2 = NormalizeShiftDecimal(ParseTimeToDecimal(input.ShiftsStart2));
+        var s1 = NormalizeShiftDecimal(input.ShiftsStart1);
+        var s2 = NormalizeShiftDecimal(input.ShiftsStart2);
         var h1 = input.ShiftsHours1 ?? 0m;
         var h2 = input.ShiftsHours2 ?? 0m;
 

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

@@ -14,7 +14,7 @@ public class WorkOrdDetail
     [SugarColumn(ColumnName = "WorkOrdMasterRecID", IsNullable = true)]
     public long WorkOrdMasterRecID { get; set; }
 
-    [SugarColumn(ColumnName = "Domain", Length = 8, IsNullable = true)]
+    [SugarColumn(ColumnName = "Domain", Length = 80, IsNullable = true)]
     public string? Domain { get; set; }
 
     [SugarColumn(ColumnName = "WorkOrd", Length = 64, IsNullable = true)]