Commit 818c00db9fac9bcb3f4b435551d043ffeedf8f88

Authored by “wangming”
1 parent 614b30c6

feat(douyinLogistics): 抖音物流功能优化

- 移除商家实际收入的可编辑输入框,改为只读显示
- 在商品信息中增加ERP商品名称显示字段
- 添加异常订单筛选选项和抖音SKU名称搜索功能
- 实现卖家备注回传抖音功能,包括弹窗表单和API调用
- 优化订单状态本地即时更新逻辑,避免页面闪烁
- 增加批量创建运单后的本地状态同步机制
- 完善订单列表的数据过滤和搜索条件支持
Showing 67 changed files with 3453 additions and 1791 deletions
Antis.Erp.Plat/antis-ncc-admin/src/views/douyinLogistics/CreateWaybill.vue
... ... @@ -144,20 +144,11 @@
144 144 <span class="order-kv-label">优惠金额</span>
145 145 <span class="order-kv-val amount-text amount-discount">{{ formatDiscountFen(form.orderAmountFen, form.payAmountFen) }}</span>
146 146 </div>
147   - <div class="order-kv-row order-kv-row--editor">
  147 + <div class="order-kv-row">
148 148 <span class="order-kv-label">商家实际收入</span>
149   - <div class="order-kv-editor">
150   - <el-input-number
151   - v-model="form.merchantIncome"
152   - :min="0"
153   - :precision="2"
154   - :step="1"
155   - :controls="false"
156   - size="small"
157   - class="order-kv-income-input"
158   - :disabled="waybillSubmitBlocked"
159   - />
160   - </div>
  149 + <span class="order-kv-val amount-text amount-readonly">
  150 + {{ form.merchantIncome !== null && form.merchantIncome !== undefined ? `¥${Number(form.merchantIncome).toFixed(2)}` : '' }}
  151 + </span>
161 152 </div>
162 153 </div>
163 154 </el-col>
... ... @@ -184,6 +175,9 @@
184 175 <div class="product-thumb-placeholder" v-else>暂无图片</div>
185 176 <div class="product-info">
186 177 <div class="product-name">{{ item.product_name }}</div>
  178 + <div class="product-name-erp">
  179 + ERP:{{ item.erp_spmc || '-' }}
  180 + </div>
187 181 <div v-if="getSkuCode(item)" class="product-sku">
188 182 <span class="sku-label">SKUID:</span>{{ getSkuCode(item) }}
189 183 <span v-if="formatSpec(item.spec)" class="sku-spec">({{ formatSpec(item.spec) }})</span>
... ... @@ -318,8 +312,8 @@
318 312 <template slot-scope="scope">
319 313 <div class="product-name-cell">
320 314 <div class="product-name-main">{{ scope.row.product_name || '无' }}</div>
321   - <div v-if="scope.row.erp_spmc && String(scope.row.erp_spmc) !== String(scope.row.product_name)" class="product-name-erp">
322   - ERP:{{ scope.row.erp_spmc }}
  315 + <div class="product-name-erp">
  316 + ERP:{{ scope.row.erp_spmc || '-' }}
323 317 </div>
324 318 </div>
325 319 </template>
... ... @@ -1185,6 +1179,7 @@ const loadOrderDetail = async () =&gt; {
1185 1179 selectedSerialNumbers: item.selectedSerialNumbers || [], // 序列号列表
1186 1180 spxlhLoaded: false, // 序列号类型是否已加载
1187 1181 spxlhType: item.spxlhType || '', // 序列号类型
  1182 + erp_spmc: item.erp_spmc || item.ErpSpmc || item.erpProductName || item.spmc || item.Spmc || '', // ERP 商品名称
1188 1183 erpSpbh: '' // ERP 商品主键 Id,用于 UpdateSerialNumberStatus 手动录入序列号
1189 1184 }
1190 1185  
... ... @@ -1275,6 +1270,7 @@ const addProduct = async (product) =&gt; {
1275 1270 // 添加商品到清单
1276 1271 const newProduct = {
1277 1272 product_name: product.spmc || '',
  1273 + erp_spmc: product.spmc || '',
1278 1274 product_pic: resolveProductPicUrl(productPic || product.imageUrl || ''),
1279 1275 // ERP 添加的商品没有抖音图,ERP 图直接填 erp_pic,用于"商品清单"展示
1280 1276 douyin_pic: '',
... ... @@ -1850,7 +1846,6 @@ const handleSubmit = async () =&gt; {
1850 1846 remark: form.value.remark,
1851 1847 douyinOrderIds: form.value.orderId_Douyin || '',
1852 1848 mergedOrderInternalIds: mergedInternalIds.value.length > 1 ? mergedInternalIds.value : undefined,
1853   - merchantIncome: form.value.merchantIncome,
1854 1849 fhr: form.value.fhr
1855 1850 })
1856 1851  
... ... @@ -2430,6 +2425,10 @@ onMounted(() =&gt; {
2430 2425 color: #e6a23c;
2431 2426 }
2432 2427  
  2428 +.amount-readonly {
  2429 + color: #606266;
  2430 +}
  2431 +
2433 2432 /* ── 商品明细(订单信息右列):多件时纵向滚动,避免把整页撑过长 ── */
2434 2433 .order-product-list {
2435 2434 max-height: min(380px, 42vh);
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/douyinLogistics/OrderList.vue
... ... @@ -89,6 +89,7 @@
89 89 <el-option label="已发货(全部)" :value="1" />
90 90 <el-option label="已发货-待提交发货单" value="shipped_pending_form" />
91 91 <el-option label="已发货-已提交发货单" value="shipped_submitted_form" />
  92 + <el-option label="异常订单(取消/退款)" value="abnormal" />
92 93 <el-option label="已取消" :value="2" />
93 94 <el-option label="已退款" :value="3" />
94 95 <el-option label="退款中" :value="4" />
... ... @@ -109,6 +110,9 @@
109 110 <el-form-item label="商品名称">
110 111 <el-input v-model="filterForm.productName" placeholder="商品关键词" clearable class="filter-w--product" />
111 112 </el-form-item>
  113 + <el-form-item label="抖音SKU名称">
  114 + <el-input v-model="filterForm.skuName" placeholder="SKU名称关键词" clearable class="filter-w--sku" />
  115 + </el-form-item>
112 116 <el-form-item label="同步时间">
113 117 <el-date-picker
114 118 v-model="filterForm.createTimeRange"
... ... @@ -375,6 +379,9 @@
375 379 <div v-if="row.sellerWords" style="color: #67c23a; white-space: pre-line;" :title="row.sellerWords">
376 380 <span style="color: #909399; font-size: 10px;">卖家:</span>{{ row.sellerWords }}
377 381 </div>
  382 + <div v-if="row.status === 1" style="margin-top: 4px;">
  383 + <el-button type="text" size="mini" @click="openRemarkDialog(row)">编辑并回传抖音</el-button>
  384 + </div>
378 385 <div v-if="!row.buyerWords && !row.sellerWords" style="color: #909399;">
379 386 -
380 387 </div>
... ... @@ -474,13 +481,38 @@
474 481 <el-button type="primary" :loading="manualShipLoading" @click="handleManualShip">确认发货</el-button>
475 482 </div>
476 483 </el-dialog>
  484 +
  485 + <el-dialog :visible.sync="remarkDialogVisible" title="备注回传抖音" width="560px" :close-on-click-modal="false">
  486 + <el-form label-width="90px" @submit.native.prevent>
  487 + <el-form-item label="订单号">
  488 + <span>{{ remarkForm.orderDisplay }}</span>
  489 + </el-form-item>
  490 + <el-form-item label="买家备注">
  491 + <el-input v-model="remarkForm.buyerWords" type="textarea" :rows="3" disabled />
  492 + </el-form-item>
  493 + <el-form-item label="卖家备注">
  494 + <el-input
  495 + v-model="remarkForm.sellerWords"
  496 + type="textarea"
  497 + :rows="4"
  498 + maxlength="500"
  499 + show-word-limit
  500 + placeholder="请输入卖家备注,保存后会同步到抖音"
  501 + />
  502 + </el-form-item>
  503 + </el-form>
  504 + <div slot="footer">
  505 + <el-button @click="remarkDialogVisible = false">取消</el-button>
  506 + <el-button type="primary" :loading="remarkSaving" @click="handleSaveRemark">保存并回传抖音</el-button>
  507 + </div>
  508 + </el-dialog>
477 509 </div>
478 510 </template>
479 511  
480 512 <script>
481 513 import { ref, onMounted } from 'vue'
482 514 import { Message as ElMessage, MessageBox as ElMessageBox } from 'element-ui'
483   -import { getOrders, syncOrders, createWaybill, initDatabase, shipToDouyin, manualShip, createSalesOrder, clearAndResync, getShops, splitMergedOrders, unsplitOrders, getTokenByCredentials, getAuthorizeUrl } from '@/api/douyinLogistics'
  515 +import { getOrders, syncOrders, createWaybill, initDatabase, shipToDouyin, manualShip, createSalesOrder, clearAndResync, getShops, splitMergedOrders, unsplitOrders, getTokenByCredentials, getAuthorizeUrl, updateSellerRemark } from '@/api/douyinLogistics'
484 516 import { getDouyinLogisticsBaseOrigin } from '@/utils/douyinLogisticsAxios'
485 517 import router from '@/router'
486 518  
... ... @@ -497,12 +529,13 @@ const selectedOrders = ref([])
497 529 const shopList = ref([])
498 530 const filterForm = ref({
499 531 shopId: undefined,
500   - status: 0,
  532 + status: undefined,
501 533 orderId: '',
502 534 receiverName: '',
503 535 receiverPhone: '',
504 536 trackingNumber: '',
505 537 productName: '',
  538 + skuName: '',
506 539 createTimeRange: null,
507 540 payTimeRange: null
508 541 })
... ... @@ -537,6 +570,8 @@ const buildListFilters = (overrideStatus, includeSort = true) =&gt; {
537 570 } else if (effectiveStatus === 'shipped_submitted_form') {
538 571 filters.status = 1
539 572 filters.shipmentFormSubmitted = true
  573 + } else if (effectiveStatus === 'abnormal') {
  574 + filters.abnormalOnly = true
540 575 } else if (effectiveStatus !== undefined && effectiveStatus !== null) {
541 576 filters.status = effectiveStatus
542 577 }
... ... @@ -565,6 +600,10 @@ const buildListFilters = (overrideStatus, includeSort = true) =&gt; {
565 600 if (productName) {
566 601 filters.productName = productName
567 602 }
  603 + const skuName = (filterForm.value.skuName || '').trim()
  604 + if (skuName) {
  605 + filters.skuName = skuName
  606 + }
568 607  
569 608 if (filterForm.value.createTimeRange && Array.isArray(filterForm.value.createTimeRange) && filterForm.value.createTimeRange.length === 2) {
570 609 filters.createTimeStart = filterForm.value.createTimeRange[0] + ' 00:00:00'
... ... @@ -601,7 +640,7 @@ const isStatActive = (key) =&gt; {
601 640 if (key === 'shipped') {
602 641 return s === 1 || s === 'shipped_pending_form' || s === 'shipped_submitted_form'
603 642 }
604   - if (key === 'abnormal') return s === 2 || s === 3 || s === 4
  643 + if (key === 'abnormal') return s === 'abnormal' || s === 2 || s === 3 || s === 4
605 644 return false
606 645 }
607 646  
... ... @@ -613,8 +652,7 @@ const applyStatFilter = (key) =&gt; {
613 652 } else if (key === 'shipped') {
614 653 filterForm.value.status = 1
615 654 } else {
616   - filterForm.value.status = 2
617   - ElMessage.info('已切换为「已取消」。可在「订单状态」中选择已退款、退款中等类型。')
  655 + filterForm.value.status = 'abnormal'
618 656 }
619 657 handleSearch()
620 658 }
... ... @@ -679,6 +717,39 @@ const refreshListAndStats = async () =&gt; {
679 717 await Promise.all([fetchOrders(), fetchStats()])
680 718 }
681 719  
  720 +/** 当前是否在“待发货”视图(创建运单成功后应立即移出) */
  721 +const isPendingListView = () => filterForm.value.status === 0
  722 +
  723 +/** 本地即时迁移订单状态,避免等待手动刷新 */
  724 +const applyWaybillTransitionLocal = (orderIds) => {
  725 + if (!Array.isArray(orderIds) || orderIds.length === 0) return
  726 + const idSet = new Set(orderIds.map(id => Number(id)))
  727 + let movedCount = 0
  728 +
  729 + // 本地先把订单标记为“已发货-未提交发货单”
  730 + orders.value.forEach((row) => {
  731 + if (idSet.has(Number(row.id))) {
  732 + if (row.status === 0) movedCount++
  733 + row.status = 1
  734 + row.isPendingShipmentForm = true
  735 + }
  736 + })
  737 +
  738 + // 在“待发货”视图下,立即移出列表,避免用户误判
  739 + if (isPendingListView()) {
  740 + orders.value = orders.value.filter(row => !idSet.has(Number(row.id)))
  741 + pagination.value.total = Math.max(0, Number(pagination.value.total || 0) - movedCount)
  742 + }
  743 +
  744 + // 统计卡片即时变更(有值时才更新,避免 null 场景闪烁)
  745 + if (stats.value.pending !== null && stats.value.pending !== undefined) {
  746 + stats.value.pending = Math.max(0, Number(stats.value.pending || 0) - movedCount)
  747 + }
  748 + if (stats.value.shipped !== null && stats.value.shipped !== undefined) {
  749 + stats.value.shipped = Number(stats.value.shipped || 0) + movedCount
  750 + }
  751 +}
  752 +
682 753 // 获取订单列表
683 754 const fetchOrders = async () => {
684 755 const requestId = ++listRequestSeq.value
... ... @@ -858,12 +929,13 @@ const handleSortChange = ({ prop, order }) =&gt; {
858 929 const handleReset = () => {
859 930 filterForm.value = {
860 931 shopId: undefined,
861   - status: 0,
  932 + status: undefined,
862 933 orderId: '',
863 934 receiverName: '',
864 935 receiverPhone: '',
865 936 trackingNumber: '',
866 937 productName: '',
  938 + skuName: '',
867 939 createTimeRange: null,
868 940 payTimeRange: null
869 941 }
... ... @@ -923,6 +995,7 @@ const handleBatchCreateWaybill = async () =&gt; {
923 995 let successCount = 0
924 996 let failCount = 0
925 997 const errors = []
  998 + const successOrderIds = []
926 999  
927 1000 const concurrency = 3
928 1001 const queue = ordersWithoutWaybill.slice()
... ... @@ -943,6 +1016,7 @@ const handleBatchCreateWaybill = async () =&gt; {
943 1016 }
944 1017  
945 1018 successCount++
  1019 + successOrderIds.push(order.id)
946 1020 } catch (error) {
947 1021 failCount++
948 1022 const errorMsg = (error.response && error.response.data && error.response.data.message) || error.message || '未知错误'
... ... @@ -952,9 +1026,11 @@ const handleBatchCreateWaybill = async () =&gt; {
952 1026 })
953 1027 await Promise.all(workers)
954 1028  
955   - // 刷新订单列表
956   - await fetchOrders()
957   - fetchStats()
  1029 + // 先本地即时迁移,再延迟回源刷新,避免“必须手动刷新才更新”
  1030 + applyWaybillTransitionLocal(successOrderIds)
  1031 + setTimeout(() => {
  1032 + refreshListAndStats()
  1033 + }, 1200)
958 1034  
959 1035 // 清空选择
960 1036 selectedOrders.value = []
... ... @@ -1060,10 +1136,12 @@ const handleCreateWaybill = async (order) =&gt; {
1060 1136 const errorMsg = (error.response && error.response.data && error.response.data.message) || error.message || '未知错误'
1061 1137 ElMessage.warning('同步抖音发货失败:' + errorMsg)
1062 1138 }
1063   -
1064   - // 最后统一刷新列表和统计
1065   - await fetchOrders()
1066   - fetchStats()
  1139 +
  1140 + // 先本地即时迁移,再延迟回源刷新,避免“必须手动刷新才更新”
  1141 + applyWaybillTransitionLocal([order.id])
  1142 + setTimeout(() => {
  1143 + refreshListAndStats()
  1144 + }, 1200)
1067 1145  
1068 1146 // 创建运单后不会自动向打印机推送;网络打印机也需在浏览器里选「打印」并选中对应打印机
1069 1147 try {
... ... @@ -1154,6 +1232,15 @@ const manualShipForm = ref({
1154 1232 trackingNumber: ''
1155 1233 })
1156 1234  
  1235 +const remarkDialogVisible = ref(false)
  1236 +const remarkSaving = ref(false)
  1237 +const remarkForm = ref({
  1238 + orderId: 0,
  1239 + orderDisplay: '',
  1240 + buyerWords: '',
  1241 + sellerWords: ''
  1242 +})
  1243 +
1157 1244 const openManualShipDialog = (order) => {
1158 1245 manualShipForm.value = {
1159 1246 orderId: order.id,
... ... @@ -1193,6 +1280,46 @@ const handleManualShip = async () =&gt; {
1193 1280 }
1194 1281 }
1195 1282  
  1283 +const openRemarkDialog = (order) => {
  1284 + if (!order || order.status !== 1) {
  1285 + ElMessage.warning('仅已发货订单支持备注回传')
  1286 + return
  1287 + }
  1288 + remarkForm.value = {
  1289 + orderId: Number(order.id) || 0,
  1290 + orderDisplay: order.orderId || '',
  1291 + buyerWords: order.buyerWords || '',
  1292 + sellerWords: order.sellerWords || ''
  1293 + }
  1294 + remarkDialogVisible.value = true
  1295 +}
  1296 +
  1297 +const handleSaveRemark = async () => {
  1298 + const oid = Number(remarkForm.value.orderId) || 0
  1299 + if (!oid) {
  1300 + ElMessage.warning('订单信息无效')
  1301 + return
  1302 + }
  1303 + try {
  1304 + remarkSaving.value = true
  1305 + const res = await updateSellerRemark(oid, remarkForm.value.sellerWords || '')
  1306 + const data = res && res.data ? res.data : {}
  1307 + if (data.syncSuccess) {
  1308 + ElMessage.success(data.message || '卖家备注已同步到抖音')
  1309 + } else {
  1310 + ElMessage.warning(data.message || '备注已保存,但同步抖音失败')
  1311 + }
  1312 + const row = orders.value.find((x) => Number(x.id) === oid)
  1313 + if (row) row.sellerWords = remarkForm.value.sellerWords || ''
  1314 + remarkDialogVisible.value = false
  1315 + } catch (error) {
  1316 + const msg = (error.response && error.response.data && error.response.data.message) || error.message || '未知错误'
  1317 + ElMessage.error('备注回传失败:' + msg)
  1318 + } finally {
  1319 + remarkSaving.value = false
  1320 + }
  1321 +}
  1322 +
1196 1323 // 手动创建发货单(合并单跳转到合并编辑页)
1197 1324 const handleManualCreateWaybill = (order) => {
1198 1325 const ids = order.mergedOrderIds && order.mergedOrderIds.length > 1
... ... @@ -1410,6 +1537,8 @@ onMounted(() =&gt; {
1410 1537 handleShipToDouyin,
1411 1538 openManualShipDialog,
1412 1539 handleManualShip,
  1540 + openRemarkDialog,
  1541 + handleSaveRemark,
1413 1542 onCompanyChange,
1414 1543 handleSizeChange,
1415 1544 handlePageChange,
... ... @@ -1424,6 +1553,9 @@ onMounted(() =&gt; {
1424 1553 manualShipVisible,
1425 1554 manualShipLoading,
1426 1555 manualShipForm,
  1556 + remarkDialogVisible,
  1557 + remarkSaving,
  1558 + remarkForm,
1427 1559 logisticsCompanyOptions
1428 1560 }
1429 1561 }
... ... @@ -1652,6 +1784,10 @@ onMounted(() =&gt; {
1652 1784 width: 160px;
1653 1785 }
1654 1786  
  1787 +.filter-w--sku {
  1788 + width: 170px;
  1789 +}
  1790 +
1655 1791 .filter-w--range {
1656 1792 width: 220px;
1657 1793 }
... ... @@ -1671,7 +1807,8 @@ onMounted(() =&gt; {
1671 1807 .filter-w--status,
1672 1808 .filter-w--text,
1673 1809 .filter-w--phone,
1674   - .filter-w--product {
  1810 + .filter-w--product,
  1811 + .filter-w--sku {
1675 1812 width: 118px;
1676 1813 }
1677 1814  
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtBjdbd/Form.vue
... ... @@ -52,14 +52,14 @@
52 52 <el-table-column type="index" width="50" label="序号" align="center" />
53 53 <el-table-column prop="rkck" label="入库仓库">
54 54 <template slot-scope="scope">
55   - <el-select v-model="scope.row.rkck" placeholder="请选择" clearable disabled>
  55 + <el-select size="mini" v-model="scope.row.rkck" placeholder="请选择" clearable disabled>
56 56 <el-option v-for="(item, index) in rkckOptions" :key="index" :label="item.F_mdmc" :value="item.F_Id" :disabled="item.disabled"></el-option>
57 57 </el-select>
58 58 </template>
59 59 </el-table-column>
60 60 <el-table-column prop="ckck" label="出库仓库">
61 61 <template slot-scope="scope">
62   - <el-select v-model="scope.row.ckck" placeholder="请选择" clearable disabled>
  62 + <el-select size="mini" v-model="scope.row.ckck" placeholder="请选择" clearable disabled>
63 63 <el-option v-for="(item, index) in ckckOptions" :key="index" :label="item.F_mdmc" :value="item.F_Id" :disabled="item.disabled"></el-option>
64 64 </el-select>
65 65 </template>
... ... @@ -67,6 +67,7 @@
67 67 <el-table-column prop="spbh" label="商品编号" width="180">
68 68 <template slot-scope="scope">
69 69 <el-select
  70 + size="mini"
70 71 v-model="scope.row.spbh"
71 72 placeholder="请选择商品"
72 73 filterable
... ... @@ -86,40 +87,34 @@
86 87 </el-table-column>
87 88 <el-table-column prop="spmc" label="商品名称">
88 89 <template slot-scope="scope">
89   - <el-input v-model="scope.row.spmc" placeholder="请输入" clearable ></el-input>
  90 + <el-input size="mini" v-model="scope.row.spmc" placeholder="请输入" clearable ></el-input>
90 91 </template>
91 92 </el-table-column>
92 93 <el-table-column prop="dw" label="单位" v-if="false">
93 94 <template slot-scope="scope">
94   - <el-input v-model="scope.row.dw" placeholder="请输入" clearable ></el-input>
  95 + <el-input size="mini" v-model="scope.row.dw" placeholder="请输入" clearable ></el-input>
95 96 </template>
96 97 </el-table-column>
97   - <el-table-column prop="kucun" label="账面库存" width="100">
  98 + <el-table-column prop="kucun" label="账面库存" width="110" align="right">
98 99 <template slot-scope="scope">
99   - <div>
100   - <div style="display: flex; align-items: center;">
101   - <input
102   - type="text"
103   - :value="scope.row.kucun"
104   - placeholder="自动获取"
105   - readonly
106   - style="flex: 1; padding: 5px; border: 1px solid #dcdfe6; border-radius: 4px;"
107   - />
108   - <button
109   - @click="getStockQuantity(scope.row)"
110   - :disabled="scope.row.loadingStock"
111   - style="margin-left: 5px; padding: 5px; border: 1px solid #dcdfe6; border-radius: 4px; background: white; cursor: pointer;"
112   - >
113   - <i class="el-icon-refresh"></i>
114   - </button>
115   - </div>
116   -
  100 + <div class="bjdbd-kucun-cell">
  101 + <span class="bjdbd-kucun-text">{{ scope.row.kucun == null || scope.row.kucun === '' ? '-' : scope.row.kucun }}</span>
  102 + <el-button
  103 + size="mini"
  104 + type="text"
  105 + icon="el-icon-refresh"
  106 + :loading="scope.row.loadingStock"
  107 + :disabled="!!isDetail"
  108 + @click="getStockQuantity(scope.row)"
  109 + class="bjdbd-kucun-refresh"
  110 + ></el-button>
117 111 </div>
118 112 </template>
119 113 </el-table-column>
120   - <el-table-column prop="sl" label="销售数量" v-if="false">
  114 + <el-table-column prop="sl" label="需变价数量" width="110">
121 115 <template slot-scope="scope">
122 116 <el-input
  117 + size="mini"
123 118 v-model="scope.row.sl"
124 119 placeholder="请输入"
125 120 clearable
... ... @@ -130,79 +125,36 @@
130 125 <el-table-column prop="dj" label="成本单价" width="110">
131 126 <template slot-scope="scope">
132 127 <el-input
  128 + size="mini"
133 129 v-model="scope.row.dj"
134   - placeholder="参考成本,过账以服务端为准"
  130 + placeholder="自动获取"
135 131 clearable
136   - :readonly="!!isDetail"
137   - @input="handleAmountChange(scope.row)"
  132 + readonly
  133 + :style='{"width":"100%"}'
138 134 ></el-input>
139 135 </template>
140 136 </el-table-column>
141   - <el-table-column prop="bjhcb" label="变价后成本" width="120">
  137 + <el-table-column prop="bjhcb" label="变价后成本" width="130">
142 138 <template slot-scope="scope">
143 139 <el-input
  140 + size="mini"
144 141 v-model="scope.row.bjhcb"
145   - placeholder="自动计算"
  142 + placeholder="请输入"
146 143 clearable
147   - readonly
  144 + :readonly="!!isDetail"
148 145 :style='{"width":"100%"}'
  146 + @input="handleBjhcbInput(scope.row)"
149 147 ></el-input>
150 148 </template>
151 149 </el-table-column>
152 150 <el-table-column prop="je" label="销售总额" v-if="false">
153 151 <template slot-scope="scope">
154   - <el-input v-model="scope.row.je" placeholder="请输入" clearable readonly></el-input>
  152 + <el-input size="mini" v-model="scope.row.je" placeholder="请输入" clearable readonly></el-input>
155 153 </template>
156 154 </el-table-column>
157 155 <el-table-column prop="description" label="备注">
158 156 <template slot-scope="scope">
159   - <el-input v-model="scope.row.description" placeholder="请输入备注" clearable></el-input>
160   - </template>
161   - </el-table-column>
162   - <el-table-column prop="serialNumberType" label="序列号类型" width="140">
163   - <template slot-scope="scope">
164   - <div style="display: flex; align-items: center; gap: 5px;">
165   - <el-tag
166   - :type="getSerialNumberTypeTagType(scope.row)"
167   - size="mini"
168   - :title="getSerialNumberTypeDescription(scope.row)"
169   - >
170   - {{ getSerialNumberTypeText(scope.row) }}
171   - </el-tag>
172   - <el-button
173   - v-if="scope.row.spbh"
174   - size="mini"
175   - type="text"
176   - icon="el-icon-refresh"
177   - @click="refreshSerialNumberType(scope.row)"
178   - :title="'刷新序列号类型信息'"
179   - ></el-button>
180   - </div>
181   - </template>
182   - </el-table-column>
183   - <el-table-column prop="selectedSerialNumbers" label="选择序列号" width="150">
184   - <template slot-scope="scope">
185   - <div class="serial-number-selector">
186   - <div v-if="scope.row.selectedSerialNumbers && scope.row.selectedSerialNumbers.length > 0" class="selected-serial-numbers">
187   - <el-tag
188   - v-for="(serialNumber, index) in scope.row.selectedSerialNumbers"
189   - :key="index"
190   - size="mini"
191   - type="warning"
192   - style="margin: 2px;"
193   - >
194   - {{ serialNumber }}
195   - </el-tag>
196   - </div>
197   - <el-button
198   - size="mini"
199   - type="primary"
200   - @click="openSerialNumberSelect(scope.row)"
201   - :disabled="!scope.row.spbh"
202   - >
203   - 选择序列号
204   - </el-button>
205   - </div>
  157 + <el-input size="mini" v-model="scope.row.description" placeholder="请输入备注" clearable></el-input>
206 158 </template>
207 159 </el-table-column>
208 160 <el-table-column label="操作" width="50">
... ... @@ -274,19 +226,16 @@
274 226 </span>
275 227 <!-- 商品条码选择弹窗 -->
276 228 <BarcodeSelect ref="barcodeSelect" @select="handleBarcodeSelect" />
277   - <!-- 序列号选择弹窗 -->
278   - <SerialNumberSelect ref="serialNumberSelect" @confirm="handleSerialNumberSelect" />
279 229 </el-dialog>
280 230 </template>
281 231 <script>
282 232 import request from '@/utils/request'
283 233 import { previewDataInterface } from '@/api/systemData/dataInterface'
284 234 import BarcodeSelect from '../wtCgrkd/BarcodeSelect.vue'
285   - import SerialNumberSelect from './SerialNumberSelect.vue'
286 235 import { getAccountSelector } from '@/api/extend/wtAccount'
287 236 import { validateMxNoEmptyProductRows } from '@/utils/validateBillMxEmptyRows'
288 237 export default {
289   - components: { BarcodeSelect, SerialNumberSelect },
  238 + components: { BarcodeSelect },
290 239 props: [],
291 240 data() {
292 241 return {
... ... @@ -448,111 +397,6 @@
448 397 }
449 398 },
450 399  
451   - // 批量获取所有明细商品的序列号类型信息
452   - async getAllProductSerialNumberTypes() {
453   - console.log('开始批量获取所有商品的序列号类型信息...');
454   - const productIds = this.dataForm.wtXsckdMxList
455   - .filter(row => row.spbh)
456   - .map(row => row.spbh);
457   -
458   - console.log('需要获取信息的商品ID列表:', productIds);
459   -
460   - // 并行获取所有商品信息
461   - const productPromises = productIds.map(productId => this.getProductInfo(productId));
462   - const productResults = await Promise.allSettled(productPromises);
463   -
464   - // 统计结果
465   - let successCount = 0;
466   - let failCount = 0;
467   - const failedProducts = [];
468   -
469   - productResults.forEach((result, index) => {
470   - if (result.status === 'fulfilled' && result.value) {
471   - successCount++;
472   - console.log(`商品 ${productIds[index]} 信息获取成功`);
473   - } else {
474   - failCount++;
475   - const failedProduct = this.dataForm.wtXsckdMxList.find(row => row.spbh === productIds[index]);
476   - if (failedProduct) {
477   - failedProducts.push(failedProduct.spmc || productIds[index]);
478   - }
479   - console.error(`商品 ${productIds[index]} 信息获取失败`);
480   - }
481   - });
482   -
483   - console.log(`批量获取完成: 成功 ${successCount} 个,失败 ${failCount} 个`);
484   -
485   - if (failedProducts.length > 0) {
486   - console.log('获取失败的商品:', failedProducts);
487   - }
488   -
489   - return {
490   - successCount,
491   - failCount,
492   - failedProducts
493   - };
494   - },
495   -
496   - // 手动设置商品序列号类型(临时解决方案)
497   - setProductSerialNumberType(productId, serialNumberType) {
498   - const key = String(productId);
499   - const product = this.productCache.get(key);
500   - if (product) {
501   - product.spxlhType = serialNumberType;
502   - console.log(`手动设置商品 ${productId} 的序列号类型为: ${serialNumberType}`);
503   - } else {
504   - // 如果缓存中没有,创建一个默认对象
505   - const defaultProduct = {
506   - id: productId,
507   - spmc: '未知商品',
508   - spxlhType: serialNumberType
509   - };
510   - this.productCache.set(key, defaultProduct);
511   - console.log(`为商品 ${productId} 创建默认对象并设置序列号类型为: ${serialNumberType}`);
512   - }
513   - },
514   -
515   - // 显示所有明细商品的序列号类型信息
516   - showAllProductSerialNumberTypes() {
517   - console.log('=== 所有明细商品的序列号类型信息 ===');
518   - let needSerialNumberCount = 0;
519   - let noNeedSerialNumberCount = 0;
520   - let unknownCount = 0;
521   -
522   - this.dataForm.wtXsckdMxList.forEach((row, index) => {
523   - if (row.spbh) {
524   - const key = String(row.spbh);
525   - const product = this.productCache.get(key);
526   - if (product && product.spxlhType) {
527   - const spxlhType = product.spxlhType;
528   - const needsSerialNumber = spxlhType === '1' || spxlhType === 1 || spxlhType === '入1出1' || spxlhType === '2' || spxlhType === 2 || spxlhType === '入0出1';
529   - const hasSerialNumbers = row.selectedSerialNumbers && row.selectedSerialNumbers.length > 0;
530   - const serialNumberCount = hasSerialNumbers ? row.selectedSerialNumbers.length : 0;
531   - const detailQuantity = parseInt(row.sl) || 0;
532   -
533   - if (needsSerialNumber) {
534   - needSerialNumberCount++;
535   - console.log(`第${index + 1}行: ${row.spmc} (${row.spbh}) - 序列号类型: ${spxlhType} [需要序列号] - 已选择: ${serialNumberCount}个 - 明细数量: ${detailQuantity} - 状态: ${serialNumberCount === detailQuantity ? '✓ 正确' : '✗ 不匹配'}`);
536   - } else {
537   - noNeedSerialNumberCount++;
538   - console.log(`第${index + 1}行: ${row.spmc} (${row.spbh}) - 序列号类型: ${spxlhType} [不需要序列号]`);
539   - }
540   - } else {
541   - unknownCount++;
542   - console.log(`第${index + 1}行: ${row.spmc} (${row.spbh}) - 序列号类型: 未获取到`);
543   - }
544   - } else {
545   - console.log(`第${index + 1}行: 未选择商品`);
546   - }
547   - });
548   -
549   - console.log(`=== 统计信息 ===`);
550   - console.log(`需要序列号的商品: ${needSerialNumberCount} 个`);
551   - console.log(`不需要序列号的商品: ${noNeedSerialNumberCount} 个`);
552   - console.log(`序列号类型未知的商品: ${unknownCount} 个`);
553   - console.log('=== 序列号类型信息显示完成 ===');
554   - },
555   -
556 400 setFullName(item, row) {
557 401 var md = this.spbhOptions.filter(u => u.F_Id == item);
558 402 if (md && md.length) {
... ... @@ -561,19 +405,6 @@
561 405 if (row.spbh && row.ckck) {
562 406 this.getStockQuantity(row);
563 407 }
564   - if (row.spbh) {
565   - const productId = String(row.spbh);
566   - // 选择新商品时先置为false
567   - this.$set(row, 'spxlhLoaded', false);
568   - this.getProductInfo(productId).then(product => {
569   - // 触发当前行的响应式刷新
570   - this.$set(row, 'spxlhLoaded', true);
571   - this.$forceUpdate();
572   - if (!(product && product.spxlhType)) {
573   - this.$message.warning('无法获取序列号类型信息');
574   - }
575   - });
576   - }
577 408 },
578 409  
579 410  
... ... @@ -702,7 +533,13 @@
702 533 buildPayload(isDraft) {
703 534 const bjsxNum = parseFloat(this.dataForm.bjsx)
704 535 const mx = (this.dataForm.wtXsckdMxList || []).map(r => {
705   - const { productQuery, spxlhLoaded, loadingStock, ...rest } = r
  536 + // 剔除前端临时字段,避免写入后端
  537 + const { productQuery, spxlhLoaded, loadingStock, bjhcbManual, xlhList, selectedSerialNumbers, ...rest } = r
  538 + // 明细变价后成本强制落数值
  539 + if (rest.bjhcb !== undefined && rest.bjhcb !== null && rest.bjhcb !== '') {
  540 + const n = parseFloat(rest.bjhcb)
  541 + rest.bjhcb = isNaN(n) ? null : n
  542 + }
706 543 return rest
707 544 })
708 545 return {
... ... @@ -710,7 +547,6 @@
710 547 bjsx: isNaN(bjsxNum) ? null : bjsxNum,
711 548 isDraft: !!isDraft,
712 549 djlx: '变价调拨单',
713   - // 正式保存一律待审核(含从草稿转正);草稿仅保存草稿
714 550 djzt: isDraft ? '草稿' : '待审核',
715 551 wtXsckdMxList: mx
716 552 }
... ... @@ -721,11 +557,10 @@
721 557 return '请选择出库仓库与入库仓库'
722 558 if (this.dataForm.cjck === this.dataForm.rkck)
723 559 return '出库仓库与入库仓库不能相同'
724   - const coef = parseFloat(this.dataForm.bjsx)
725   - if (this.dataForm.bjsx === '' || this.dataForm.bjsx === undefined || isNaN(coef) || coef <= 0)
726   - return '请输入大于 0 的变价系数(%)'
727 560 if (!this.dataForm.wtXsckdMxList || !this.dataForm.wtXsckdMxList.length)
728 561 return '请添加至少一条明细'
  562 + const coef = parseFloat(this.dataForm.bjsx)
  563 + const hasCoef = !isNaN(coef) && coef > 0
729 564 let hasSp = false
730 565 for (let i = 0; i < this.dataForm.wtXsckdMxList.length; i++) {
731 566 const row = this.dataForm.wtXsckdMxList[i]
... ... @@ -733,7 +568,14 @@
733 568 hasSp = true
734 569 const q = parseInt(row.sl, 10)
735 570 if (!q || q <= 0)
736   - return `第${i + 1}行请填写有效数量或选择序列号`
  571 + return `第${i + 1}行请填写有效的需变价数量`
  572 + const stock = parseInt(row.kucun, 10)
  573 + if (!isNaN(stock) && stock >= 0 && q > stock)
  574 + return `第${i + 1}行需变价数量(${q})不能超过账面库存(${stock})`
  575 + const bjhcb = parseFloat(row.bjhcb)
  576 + // 每行必须给出变价后成本:若未填写单行值,则必须提供了变价系数%
  577 + if ((isNaN(bjhcb) || bjhcb <= 0) && !hasCoef)
  578 + return `第${i + 1}行请填写变价后成本,或在表头填写变价系数%`
737 579 }
738 580 if (!hasSp) return '请至少选择一条商品明细'
739 581 return ''
... ... @@ -761,12 +603,9 @@
761 603 method: 'get'
762 604 }).then(res =>{
763 605 _this.dataForm = res.data;
764   - console.log('编辑时加载的数据:', _this.dataForm);
765   - console.log('明细数据:', _this.dataForm.wtXsckdMxList);
766 606 if (_this.dataForm.bjsx != null && _this.dataForm.bjsx !== '')
767 607 _this.$set(_this.dataForm, 'bjsx', String(_this.dataForm.bjsx))
768   -
769   - // 为每个明细项添加productQuery字段
  608 +
770 609 if (_this.dataForm.wtXsckdMxList) {
771 610 _this.dataForm.wtXsckdMxList.forEach(item => {
772 611 if (!item.hasOwnProperty('productQuery')) {
... ... @@ -778,20 +617,18 @@
778 617 }
779 618 if (item.bjhcb != null && item.bjhcb !== '') {
780 619 _this.$set(item, 'bjhcb', Number(item.bjhcb).toFixed(4))
  620 + // 已落库的 bjhcb 视为用户已确认值,避免被表头变价系数再次覆盖
  621 + _this.$set(item, 'bjhcbManual', true)
  622 + }
  623 + // 回填账面库存(若后端 kucun 已带出则保留)
  624 + if (item.spbh && item.ckck) {
  625 + _this.getStockQuantity(item);
781 626 }
782 627 });
783 628 }
784   -
785   - // 初始化时计算总金额
  629 +
786 630 _this.calculateTotalAmount();
787   - // 同步明细表出库仓库
788 631 _this.syncDetailWarehouses();
789   - // 恢复序列号信息
790   - _this.restoreSerialNumbers();
791   - // 如果有变价系数,计算所有明细的变价后成本
792   - if (_this.dataForm.bjsx && _this.dataForm.bjsx !== '') {
793   - _this.calculateAllAdjustedCosts();
794   - }
795 632 })
796 633 }
797 634 else{
... ... @@ -799,345 +636,92 @@
799 636 }
800 637 })
801 638 },
802   - // 保存草稿:只做基础表单校验,不做序列号强校验,保存时带isDraft:true字段
  639 + // 保存草稿:仅做基础表单校验,不做业务强校验,保存时带 isDraft:true
803 640 async saveDraft() {
804   - // 确保单据类型字段赋值
805 641 this.dataForm.djlx = '变价调拨单';
806 642 this.$refs['elForm'].validate(async (valid) => {
807 643 if (valid) {
808 644 try {
809 645 const draftData = this.buildPayload(true);
810   - let res;
811 646 if (!this.dataForm.id) {
812   - res = await request({
  647 + await request({
813 648 url: `/api/Extend/WtXsckd`,
814 649 method: 'post',
815 650 data: draftData
816 651 });
817   -
818   - // 保存序列号信息(草稿状态)
819   - await this.updateSerialNumberStatus(res.data.id, true);
820 652 } else {
821   - res = await request({
  653 + await request({
822 654 url: '/api/Extend/WtXsckd/' + this.dataForm.id,
823 655 method: 'PUT',
824 656 data: draftData
825 657 });
826   -
827   - // 保存序列号信息(草稿状态)
828   - await this.updateSerialNumberStatus(this.dataForm.id, true);
829 658 }
830 659 this.visible = false;
831 660 this.$emit('refresh', true);
832 661 this.$message.success('草稿已保存');
833 662 } catch (error) {
834 663 console.error('保存草稿失败:', error);
835   - this.$message.error('保存草稿失败,请重试');
  664 + const msg = (error && error.message) ? error.message : '保存草稿失败,请重试'
  665 + this.$message.error(msg);
836 666 }
837 667 }
838 668 });
839 669 },
840   - async dataFormSubmit() {
841   - const basicErr = this.validateBjdFormalBasics()
842   - if (basicErr) {
843   - this.$message.error(basicErr)
844   - return
845   - }
846   - const mxCheck = validateMxNoEmptyProductRows(this.dataForm.wtXsckdMxList)
847   - if (!mxCheck.valid) {
848   - this.$message.warning(`第 ${mxCheck.emptyLineNos.join('、')} 行未选择商品,请先删除空白行后再提交`)
849   - return
850   - }
851   - // 1. 明细校验:序列号数量与销售数量一致性
852   - let validationErrors = [];
853   - for (let i = 0; i < this.dataForm.wtXsckdMxList.length; i++) {
854   - const row = this.dataForm.wtXsckdMxList[i];
855   - if (row.spbh) {
856   - const key = String(row.spbh);
857   - const product = this.productCache.get(key);
858   - if (product && (product.spxlhType === '1' || product.spxlhType === 1 || product.spxlhType === '2' || product.spxlhType === 2)) {
859   - if (!row.selectedSerialNumbers || row.selectedSerialNumbers.length === 0) {
860   - validationErrors.push(`第${i + 1}行商品"${row.spmc}"需要选择序列号`);
861   - } else {
862   - const serialNumberCount = row.selectedSerialNumbers.length;
863   - const detailQuantity = parseInt(row.sl) || 0;
864   - if (serialNumberCount !== detailQuantity) {
865   - validationErrors.push(`第${i + 1}行商品"${row.spmc}"的数量(${detailQuantity})与已选择的序列号数量(${serialNumberCount})不一致,请重新选择序列号或调整数量`);
866   - }
867   - }
868   - }
869   - }
870   - }
871   - if (validationErrors.length > 0) {
872   - this.$message.error(validationErrors[0]);
873   - return; // 阻止提交
874   - }
875   - // 2. 表单校验
876   - this.$refs['elForm'].validate(async (valid) => {
877   - if (!valid) return;
878   - // 确保单据类型字段赋值
879   - this.dataForm.djlx = '变价调拨单';
880   - // 确保单据状态为待审核
881   - this.dataForm.djzt = '待审核';
882   - // 检查是否有明细数据
883   - if (!this.dataForm.wtXsckdMxList || this.dataForm.wtXsckdMxList.length === 0) {
884   - console.log('没有明细数据,跳过序列号验证');
885   - // 继续执行表单验证
886   - this.$refs['elForm'].validate(async (valid) => {
887   - if (valid) {
888   - // 先批量获取所有商品的序列号类型信息
889   - console.log('开始批量获取商品序列号类型信息...');
890   - const batchResult = await this.getAllProductSerialNumberTypes();
891   -
892   - if (batchResult.failCount > 0) {
893   - console.warn(`有 ${batchResult.failCount} 个商品无法获取序列号类型信息:`, batchResult.failedProducts);
894   -
895   - // 临时解决方案:只为真正无法获取序列号类型的商品设置默认值
896   - console.log('应用临时解决方案:为无法获取序列号类型的商品设置默认值');
897   - this.dataForm.wtXsckdMxList.forEach((row, index) => {
898   - if (row.spbh) {
899   - const key = String(row.spbh);
900   - const product = this.productCache.get(key);
901   - // 只有当商品信息完全不存在时才设置默认值
902   - if (!product) {
903   - // 为这些商品设置默认的序列号类型(入0出0 - 不需要序列号)
904   - this.setProductSerialNumberType(row.spbh, '入0出0');
905   - console.log(`为第${index + 1}行商品"${row.spmc}"设置默认序列号类型: 入0出0(因为无法获取商品信息)`);
906   - } else if (!product.spxlhType) {
907   - // 如果商品信息存在但没有序列号类型,也设置默认值
908   - this.setProductSerialNumberType(row.spbh, '入0出0');
909   - console.log(`为第${index + 1}行商品"${row.spmc}"设置默认序列号类型: 入0出0(因为商品信息中没有序列号类型)`);
910   - }
911   - }
912   - });
913   - }
914   -
915   - // 显示所有商品的序列号类型信息(调试用)
916   - this.showAllProductSerialNumberTypes();
917   -
918   - // 进行序列号验证
919   - console.log('开始检查序列号...');
920   - validationErrors = [];
921   -
922   - for (let i = 0; i < this.dataForm.wtXsckdMxList.length; i++) {
923   - const row = this.dataForm.wtXsckdMxList[i];
924   - if (row.spbh) {
925   - // 从缓存中获取商品信息
926   - const key = String(row.spbh);
927   - const product = this.productCache.get(key);
928   -
929   - if (product && product.spxlhType) {
930   - const spxlhType = product.spxlhType;
931   - console.log(`第${i + 1}行商品 ${row.spmc} 的序列号类型: ${spxlhType}`);
932   -
933   - // 检查是否需要强制序列号选择(只判断'1'和'2')
934   - if (spxlhType === '1' || spxlhType === '2') {
935   - console.log(`第${i + 1}行商品 ${row.spmc} 需要强制选择序列号`);
936   -
937   - if (!row.selectedSerialNumbers || row.selectedSerialNumbers.length === 0) {
938   - validationErrors.push(`第${i + 1}行商品"${row.spmc}"需要选择序列号`);
939   - console.log(`第${i + 1}行商品"${row.spmc}"未选择序列号,已添加到验证错误`);
940   - } else {
941   - // 检查序列号数量与明细数量是否一致
942   - const serialNumberCount = row.selectedSerialNumbers.length;
943   - const detailQuantity = parseInt(row.sl) || 0;
944   -
945   - if (serialNumberCount !== detailQuantity) {
946   - validationErrors.push(`第${i + 1}行商品"${row.spmc}"的序列号数量(${serialNumberCount})与明细数量(${detailQuantity})不一致,请重新选择序列号或调整数量`);
947   - console.log(`第${i + 1}行商品"${row.spmc}"序列号数量不匹配,已添加到验证错误`);
948   - } else {
949   - console.log(`第${i + 1}行商品序列号验证通过: ${serialNumberCount}个序列号,数量${detailQuantity}`);
950   - }
951   - }
952   - } else {
953   - console.log(`第${i + 1}行商品不需要序列号选择,序列号类型: ${spxlhType}`);
954   - }
955   - } else {
956   - console.log(`第${i + 1}行商品"${row.spmc}"无法获取序列号类型信息`);
957   - // 如果无法获取商品信息,记录错误但不阻止提交
958   - validationErrors.push(`第${i + 1}行商品"${row.spmc}"无法获取序列号类型信息,请确认商品配置`);
959   - }
960   - }
961   - }
962   -
963   - // 如果有验证错误,显示错误信息并询问是否继续
964   - if (validationErrors.length > 0) {
965   - const errorMessage = validationErrors.join('\n');
966   - console.log('序列号验证发现以下问题:', validationErrors);
967   -
968   - // 添加详细的验证结果说明
969   - let detailedMessage = '序列号验证发现以下问题:\n\n';
970   - detailedMessage += errorMessage;
971   - detailedMessage += '\n\n';
972   - detailedMessage += '说明:\n';
973   - detailedMessage += '- 序列号类型为"入1出1"或"入0出1"的商品必须选择序列号\n';
974   - detailedMessage += '- 序列号数量必须与明细数量完全一致\n';
975   - detailedMessage += '- 请为上述商品选择正确的序列号后重新保存';
976   -
977   - await this.$alert(detailedMessage, '序列号验证警告', {
978   - confirmButtonText: '确定',
979   - type: 'warning',
980   - dangerouslyUseHTMLString: false
981   - });
982   - return; // 用户取消,不继续保存
983   - }
984   -
985   - console.log('序列号检查完成,开始表单验证...');
986   -
987   - this.$refs['elForm'].validate(async (valid) => {
988   - if (valid) {
989   - console.log('表单验证通过,继续提交...');
990   - try {
991   - if (!this.dataForm.id) {
992   - const res = await request({
993   - url: `/api/Extend/WtXsckd`,
994   - method: 'post',
995   - data: this.buildPayload(false),
996   - })
997   -
998   - // 更新序列号状态
999   - await this.updateSerialNumberStatus(res.data.id)
1000   -
1001   - // 仅关闭弹窗并刷新,不弹窗提示
1002   - this.visible = false;
1003   - this.$emit('refresh', true);
1004   - } else {
1005   - const res = await request({
1006   - url: '/api/Extend/WtXsckd/' + this.dataForm.id,
1007   - method: 'PUT',
1008   - data: this.buildPayload(false)
1009   - })
1010   -
1011   - // 更新序列号状态
1012   - await this.updateSerialNumberStatus(this.dataForm.id)
1013   -
1014   - // 仅关闭弹窗并刷新,不弹窗提示
1015   - this.visible = false;
1016   - this.$emit('refresh', true);
1017   - }
1018   - } catch (error) {
1019   - console.error('保存失败:', error)
1020   - this.$message.error('保存失败,请重试')
1021   - }
1022   - }
1023   - })
1024   - }
1025   - });
1026   - return;
1027   - }
1028   - // 序列号数量校验
1029   - validationErrors = [];
1030   - for (let i = 0; i < this.dataForm.wtXsckdMxList.length; i++) {
1031   - const row = this.dataForm.wtXsckdMxList[i];
1032   - if (row.spbh) {
1033   - const key = String(row.spbh);
1034   - const product = this.productCache.get(key);
1035   - if (product && product.spxlhType) {
1036   - const spxlhType = product.spxlhType;
1037   - if (spxlhType === '1' || spxlhType === '2') {
1038   - if (!row.selectedSerialNumbers || row.selectedSerialNumbers.length === 0) {
1039   - validationErrors.push(`第${i + 1}行商品"${row.spmc}"需要选择序列号`);
1040   - } else {
1041   - const serialNumberCount = row.selectedSerialNumbers.length;
1042   - const detailQuantity = parseInt(row.sl) || 0;
1043   - if (serialNumberCount !== detailQuantity) {
1044   - validationErrors.push(`第${i + 1}行商品"${row.spmc}"的数量(${detailQuantity})与已选择的序列号数量(${serialNumberCount})不一致,请重新选择序列号或调整数量`);
1045   - }
1046   - }
1047   - }
1048   - }
1049   - }
1050   - }
1051   - if (validationErrors.length > 0) {
1052   - this.$message.error(validationErrors[0]);
1053   - return; // 阻止提交
1054   - }
1055   - // 继续执行表单验证
1056   - this.$refs['elForm'].validate(async (valid) => {
1057   - if (valid) {
1058   - console.log('表单验证通过,继续提交...');
1059   - try {
1060   - if (!this.dataForm.id) {
1061   - const res = await request({
1062   - url: `/api/Extend/WtXsckd`,
1063   - method: 'post',
1064   - data: this.buildPayload(false),
1065   - })
1066   -
1067   - // 更新序列号状态
1068   - await this.updateSerialNumberStatus(res.data.id)
1069   -
1070   - // 仅关闭弹窗并刷新,不弹窗提示
1071   - this.visible = false;
1072   - this.$emit('refresh', true);
1073   - } else {
1074   - const res = await request({
1075   - url: '/api/Extend/WtXsckd/' + this.dataForm.id,
1076   - method: 'PUT',
1077   - data: this.buildPayload(false)
1078   - })
1079   -
1080   - // 更新序列号状态
1081   - await this.updateSerialNumberStatus(this.dataForm.id)
1082   -
1083   - // 仅关闭弹窗并刷新,不弹窗提示
1084   - this.visible = false;
1085   - this.$emit('refresh', true);
1086   - }
1087   - } catch (error) {
1088   - console.error('保存失败:', error)
1089   - this.$message.error('保存失败,请重试')
1090   - }
1091   - }
1092   - })
1093   - });
1094   - },
1095   -
1096   - // 更新序列号状态
1097   - async updateSerialNumberStatus(documentId, isDraft = false) {
  670 + async dataFormSubmit() {
  671 + const basicErr = this.validateBjdFormalBasics()
  672 + if (basicErr) {
  673 + this.$message.error(basicErr)
  674 + return
  675 + }
  676 + const mxCheck = validateMxNoEmptyProductRows(this.dataForm.wtXsckdMxList)
  677 + if (!mxCheck.valid) {
  678 + this.$message.warning(`第 ${mxCheck.emptyLineNos.join('、')} 行未选择商品,请先删除空白行后再提交`)
  679 + return
  680 + }
  681 + // 表单校验
  682 + this.$refs['elForm'].validate(async (valid) => {
  683 + if (!valid) return;
  684 + this.dataForm.djlx = '变价调拨单';
  685 + this.dataForm.djzt = '待审核';
1098 686 try {
1099   - // 收集所有选择的序列号
1100   - const allSerialNumbers = []
1101   - this.dataForm.wtXsckdMxList.forEach(row => {
1102   - if (row.selectedSerialNumbers && row.selectedSerialNumbers.length > 0) {
1103   - allSerialNumbers.push(...row.selectedSerialNumbers)
1104   - }
1105   - })
1106   -
1107   - if (allSerialNumbers.length > 0) {
  687 + if (!this.dataForm.id) {
1108 688 await request({
1109   - url: '/api/Extend/WtXsckd/UpdateSerialNumberStatus',
  689 + url: `/api/Extend/WtXsckd`,
1110 690 method: 'post',
1111   - data: allSerialNumbers,
1112   - params: {
1113   - outDocumentId: documentId,
1114   - outDocumentType: '变价调拨单',
1115   - outWarehouse: this.dataForm.cjck,
1116   - isDraft: isDraft
1117   - }
  691 + data: this.buildPayload(false),
  692 + })
  693 + } else {
  694 + await request({
  695 + url: '/api/Extend/WtXsckd/' + this.dataForm.id,
  696 + method: 'PUT',
  697 + data: this.buildPayload(false)
1118 698 })
1119 699 }
  700 + this.visible = false;
  701 + this.$emit('refresh', true);
1120 702 } catch (error) {
1121   - console.error('更新序列号状态失败:', error)
1122   - // 不阻止主流程,只记录错误
  703 + console.error('保存失败:', error)
  704 + const msg = (error && error.message) ? error.message : '保存失败,请重试'
  705 + this.$message.error(msg)
1123 706 }
1124   - },
  707 + });
  708 + },
1125 709 addHandleWtXsckdMxEntityList() {
1126   - let item = {
1127   - rkck:undefined,
1128   - ckck:this.dataForm.cjck, // 自动使用主表的出库仓库
1129   - spbh:undefined,
1130   - spmc:undefined,
1131   - sptm:undefined,
1132   - dw:undefined,
1133   - kucun:undefined, // 库存数量
1134   - sl:undefined,
1135   - dj:undefined,
1136   - je:undefined,
1137   - bjhcb:undefined, // 变价后成本
1138   - selectedSerialNumbers: [], // 添加序列号数组
1139   - loadingStock: false, // 库存加载状态
1140   - productQuery: '', // 为每一行添加独立的查询条件
  710 + const item = {
  711 + rkck: this.dataForm.rkck,
  712 + ckck: this.dataForm.cjck,
  713 + spbh: undefined,
  714 + spmc: undefined,
  715 + sptm: undefined,
  716 + dw: undefined,
  717 + kucun: undefined,
  718 + sl: undefined,
  719 + dj: undefined,
  720 + je: undefined,
  721 + bjhcb: undefined,
  722 + bjhcbManual: false,
  723 + loadingStock: false,
  724 + productQuery: '',
1141 725 }
1142 726 this.dataForm.wtXsckdMxList.push(item)
1143 727 },
... ... @@ -1231,11 +815,9 @@
1231 815 dj: cbj ? Number(cbj).toFixed(4) : undefined,
1232 816 je: undefined,
1233 817 bjhcb: undefined,
1234   - selectedSerialNumbers: [],
  818 + bjhcbManual: false,
1235 819 loadingStock: false,
1236 820 productQuery: '',
1237   - spxlhLoaded: false,
1238   - xlhList: []
1239 821 };
1240 822 this.dataForm.wtXsckdMxList.push(row);
1241 823 appendedRows.push(row);
... ... @@ -1246,14 +828,6 @@
1246 828 this.$message.info('没有需要新增的商品,明细已包含该仓库全部库存');
1247 829 return;
1248 830 }
1249   - // 批量预热商品档案缓存(序列号类型等),避免整列显示"加载中..."
1250   - Promise.all(appendedRows.map(r =>
1251   - this.getProductInfo(r.spbh).catch(() => null)
1252   - )).then(() => {
1253   - appendedRows.forEach(r => {
1254   - this.$set(r, 'spxlhLoaded', true);
1255   - });
1256   - });
1257 831 this.$nextTick(() => {
1258 832 if (this.dataForm.bjsx && this.dataForm.bjsx !== '') {
1259 833 this.calculateAllAdjustedCosts();
... ... @@ -1281,64 +855,22 @@
1281 855 }
1282 856 },
1283 857  
1284   - // 打开序列号选择弹窗
1285   - openSerialNumberSelect(row) {
1286   - if (!row.spbh) {
1287   - this.$message.warning('请先选择商品')
1288   - return
1289   - }
1290   - // 传递已选序列号,便于弹窗回显
1291   - this.$refs.serialNumberSelect.open(
1292   - row.spbh,
1293   - row.ckck || this.dataForm.cjck,
1294   - row.selectedSerialNumbers || [],
1295   - '变价调拨单' // 传递单据类型,查询在库序列号
1296   - );
1297   - },
1298   -
1299   - // 处理序列号选择
1300   - handleSerialNumberSelect(selectedSerialNumbers) {
1301   - // 找到当前编辑的行并填充序列号
1302   - const currentRow = this.dataForm.wtXsckdMxList.find(item =>
1303   - item.spbh === this.$refs.serialNumberSelect.currentProductCode
1304   - )
1305   -
1306   - if (currentRow) {
1307   - currentRow.selectedSerialNumbers = selectedSerialNumbers
1308   - // 自动设置数量为选择的序列号数量
1309   - currentRow.sl = selectedSerialNumbers.length.toString()
1310   - // 自动计算金额
1311   - if (currentRow.dj) {
1312   - currentRow.je = (parseFloat(currentRow.sl) * parseFloat(currentRow.dj)).toFixed(2)
1313   - }
1314   - // 更新总收款金额
1315   - this.calculateTotalAmount();
1316   -
1317   - console.log(`商品 ${currentRow.spmc} 选择了 ${selectedSerialNumbers.length} 个序列号,数量已自动更新为 ${currentRow.sl}`);
1318   - }
1319   - },
  858 + /** 需变价数量变化:重算金额与行变价后成本(若未手工录入 bjhcb 且存在系数) */
1320 859 handleAmountChange(row) {
1321 860 const sl = parseFloat(row.sl) || 0;
1322 861 const dj = parseFloat(row.dj) || 0;
1323 862 row.je = (sl * dj).toFixed(2);
1324   - // 自动计算总收款金额
1325 863 this.calculateTotalAmount();
1326   -
1327   - // 当成本单价变化时,重新计算变价后成本
1328   - if (this.dataForm.bjsx && this.dataForm.bjsx !== '') {
  864 +
  865 + // 只有用户尚未手工填写 bjhcb 时才根据变价系数自动计算
  866 + if (!row.bjhcbManual && this.dataForm.bjsx && this.dataForm.bjsx !== '') {
1329 867 this.calculateRowAdjustedCost(row);
1330 868 }
1331   -
1332   - // 检查数量与序列号数量是否一致
1333   - if (row.selectedSerialNumbers && row.selectedSerialNumbers.length > 0) {
1334   - const serialNumberCount = row.selectedSerialNumbers.length;
1335   - if (sl !== serialNumberCount) {
1336   - this.$message.error(`商品"${row.spmc}"的销售数量(${sl})与已选择的序列号数量(${serialNumberCount})不一致,请先调整序列号数量!`);
1337   - // 恢复为序列号数量
1338   - row.sl = serialNumberCount.toString();
1339   - row.je = (serialNumberCount * dj).toFixed(2);
1340   - }
1341   - }
  869 + },
  870 +
  871 + /** 手工修改变价后成本:标记为手动录入,避免被变价系数批量覆盖 */
  872 + handleBjhcbInput(row) {
  873 + this.$set(row, 'bjhcbManual', true);
1342 874 },
1343 875  
1344 876 // 处理主表出库仓库变化
... ... @@ -1486,245 +1018,6 @@
1486 1018 this.dataForm.skje = (totalAmount - ysje).toFixed(2);
1487 1019 },
1488 1020  
1489   - // 恢复序列号信息
1490   - async restoreSerialNumbers() {
1491   - console.log('开始恢复序列号信息...');
1492   - console.log('完整的dataForm:', JSON.stringify(this.dataForm, null, 2));
1493   -
1494   - if (this.dataForm.wtXsckdMxList && this.dataForm.wtXsckdMxList.length > 0) {
1495   - console.log('明细列表:', JSON.stringify(this.dataForm.wtXsckdMxList, null, 2));
1496   -
1497   - for (let i = 0; i < this.dataForm.wtXsckdMxList.length; i++) {
1498   - const row = this.dataForm.wtXsckdMxList[i];
1499   - console.log(`第${i + 1}行完整数据:`, JSON.stringify(row, null, 2));
1500   -
1501   - if (row.spbh) {
1502   - // 使用后端返回的序列号信息
1503   - if (row.selectedSerialNumbers && row.selectedSerialNumbers.length > 0) {
1504   - console.log(`第${i + 1}行商品恢复序列号:`, row.selectedSerialNumbers);
1505   - } else {
1506   - row.selectedSerialNumbers = [];
1507   - console.log(`第${i + 1}行商品无序列号信息,spbh: ${row.spbh}`);
1508   - }
1509   - }
1510   - }
1511   - } else {
1512   - console.log('明细列表为空或未定义');
1513   - }
1514   - },
1515   -
1516   - // 获取序列号类型显示文本
1517   - getSerialNumberTypeText(row) {
1518   - if (!row.spbh) return '未选择';
1519   - if (!row.spxlhLoaded) return '加载中...';
1520   - const key = String(row.spbh);
1521   - const product = this.productCache.get(key);
1522   - if (product && product.spxlhType) {
1523   - const spxlhType = product.spxlhType;
1524   - if (spxlhType === '1') return '入1出1';
1525   - if (spxlhType === '2') return '入0出1';
1526   - if (spxlhType === '3') return '入0出0';
1527   - return spxlhType;
1528   - }
1529   - return '未知';
1530   - },
1531   -
1532   - // 获取序列号类型标签颜色
1533   - getSerialNumberTypeTagType(row) {
1534   - if (!row.spbh) return 'info';
1535   - if (!row.spxlhLoaded) return 'info';
1536   - const key = String(row.spbh);
1537   - const product = this.productCache.get(key);
1538   - if (product && product.spxlhType) {
1539   - const spxlhType = product.spxlhType;
1540   - if (spxlhType === '1' || spxlhType === '2') return 'warning'; // 需要序列号 - 橙色警告
1541   - if (spxlhType === '3') return 'success'; // 不需要序列号 - 绿色
1542   - }
1543   - return 'info'; // 未知 - 蓝色
1544   - },
1545   -
1546   - // 获取序列号类型描述
1547   - getSerialNumberTypeDescription(row) {
1548   - if (!row.spbh) return '请先选择商品';
1549   - if (!row.spxlhLoaded) return '加载中...';
1550   - const key = String(row.spbh);
1551   - const product = this.productCache.get(key);
1552   - if (product && product.spxlhType) {
1553   - const spxlhType = product.spxlhType;
1554   - if (spxlhType === '1') return '入库和出库都需要序列号';
1555   - if (spxlhType === '2') return '入库不需要,出库需要序列号';
1556   - if (spxlhType === '3') return '入库和出库都不需要序列号';
1557   - }
1558   - return '序列号类型信息未知';
1559   - },
1560   -
1561   - // 刷新序列号类型信息
1562   - async refreshSerialNumberType(row) {
1563   - if (!row.spbh) {
1564   - this.$message.warning('请先选择商品');
1565   - return;
1566   - }
1567   -
1568   - try {
1569   - // 清除缓存中的商品信息
1570   - const key = String(row.spbh);
1571   - this.productCache.delete(key);
1572   - console.log(`清除商品 ${row.spbh} 的缓存信息`);
1573   -
1574   - // 重新获取商品信息
1575   - const product = await this.getProductInfo(row.spbh);
1576   - if (product && product.spxlhType) {
1577   - console.log(`刷新成功: 商品 ${row.spmc} 的序列号类型: ${product.spxlhType}`);
1578   - this.$message.success(`序列号类型已刷新: ${product.spxlhType}`);
1579   - } else {
1580   - console.warn(`刷新失败: 商品 ${row.spmc} 无法获取序列号类型信息`);
1581   - this.$message.warning('无法获取序列号类型信息');
1582   - }
1583   -
1584   - // 强制更新视图
1585   - this.$forceUpdate();
1586   - } catch (error) {
1587   - console.error(`刷新序列号类型失败:`, error);
1588   - this.$message.error('刷新序列号类型失败');
1589   - }
1590   - },
1591   -
1592   - // 调试:检查商品缓存内容
1593   - debugProductCache() {
1594   - console.log('=== 商品缓存调试信息 ===');
1595   - console.log('缓存大小:', this.productCache.size);
1596   -
1597   - if (this.productCache.size === 0) {
1598   - console.log('商品缓存为空');
1599   - return;
1600   - }
1601   -
1602   - this.productCache.forEach((product, productId) => {
1603   - console.log(`商品ID: ${productId}`);
1604   - console.log(` 商品名称: ${product.spmc || '未知'}`);
1605   - console.log(` 序列号类型: ${product.spxlhType || '未设置'}`);
1606   - console.log(` 完整数据:`, product);
1607   - });
1608   -
1609   - console.log('=== 商品选项数据 ===');
1610   - console.log('商品选项数量:', this.spbhOptions.length);
1611   -
1612   - // 查找Tomtoc硬壳商品
1613   - const tomtocProducts = this.spbhOptions.filter(item =>
1614   - item.F_Spmc && item.F_Spmc.includes('Tomtoc')
1615   - );
1616   - console.log('Tomtoc相关商品:', tomtocProducts);
1617   -
1618   - console.log('=== 调试信息结束 ===');
1619   - },
1620   -
1621   - // 测试序列号验证
1622   - async testSerialNumberValidation() {
1623   - console.log('=== 测试序列号验证 ===');
1624   - console.log('明细数据:', this.dataForm.wtXsckdMxList);
1625   -
1626   - if (!this.dataForm.wtXsckdMxList || this.dataForm.wtXsckdMxList.length === 0) {
1627   - this.$message.info('没有明细数据');
1628   - return;
1629   - }
1630   -
1631   - // 先批量获取所有商品的序列号类型信息
1632   - console.log('开始批量获取商品序列号类型信息...');
1633   - const batchResult = await this.getAllProductSerialNumberTypes();
1634   -
1635   - if (batchResult.failCount > 0) {
1636   - console.warn(`有 ${batchResult.failCount} 个商品无法获取序列号类型信息:`, batchResult.failedProducts);
1637   -
1638   - // 临时解决方案:只为真正无法获取序列号类型的商品设置默认值
1639   - console.log('应用临时解决方案:为无法获取序列号类型的商品设置默认值');
1640   - this.dataForm.wtXsckdMxList.forEach((row, index) => {
1641   - if (row.spbh) {
1642   - const key = String(row.spbh);
1643   - const product = this.productCache.get(key);
1644   - // 只有当商品信息完全不存在时才设置默认值
1645   - if (!product) {
1646   - // 为这些商品设置默认的序列号类型(入0出0 - 不需要序列号)
1647   - this.setProductSerialNumberType(row.spbh, '入0出0');
1648   - console.log(`为第${index + 1}行商品"${row.spmc}"设置默认序列号类型: 入0出0(因为无法获取商品信息)`);
1649   - } else if (!product.spxlhType) {
1650   - // 如果商品信息存在但没有序列号类型,也设置默认值
1651   - this.setProductSerialNumberType(row.spbh, '入0出0');
1652   - console.log(`为第${index + 1}行商品"${row.spmc}"设置默认序列号类型: 入0出0(因为商品信息中没有序列号类型)`);
1653   - }
1654   - }
1655   - });
1656   - }
1657   -
1658   - // 显示所有商品的序列号类型信息(调试用)
1659   - this.showAllProductSerialNumberTypes();
1660   -
1661   - // 进行序列号验证
1662   - console.log('开始检查序列号...');
1663   - let validationErrors = [];
1664   -
1665   - for (let i = 0; i < this.dataForm.wtXsckdMxList.length; i++) {
1666   - const row = this.dataForm.wtXsckdMxList[i];
1667   - if (row.spbh) {
1668   - // 从缓存中获取商品信息
1669   - const key = String(row.spbh);
1670   - const product = this.productCache.get(key);
1671   -
1672   - if (product && product.spxlhType) {
1673   - const spxlhType = product.spxlhType;
1674   - console.log(`第${i + 1}行商品 ${row.spmc} 的序列号类型: ${spxlhType}`);
1675   -
1676   - // 检查是否需要强制序列号选择(只判断'1'和'2')
1677   - if (spxlhType === '1' || spxlhType === '2') {
1678   - console.log(`第${i + 1}行商品 ${row.spmc} 需要强制选择序列号`);
1679   -
1680   - if (!row.selectedSerialNumbers || row.selectedSerialNumbers.length === 0) {
1681   - validationErrors.push(`第${i + 1}行商品"${row.spmc}"需要选择序列号`);
1682   - console.log(`第${i + 1}行商品"${row.spmc}"未选择序列号,已添加到验证错误`);
1683   - } else {
1684   - // 检查序列号数量与明细数量是否一致
1685   - const serialNumberCount = row.selectedSerialNumbers.length;
1686   - const detailQuantity = parseInt(row.sl) || 0;
1687   -
1688   - if (serialNumberCount !== detailQuantity) {
1689   - validationErrors.push(`第${i + 1}行商品"${row.spmc}"的序列号数量(${serialNumberCount})与明细数量(${detailQuantity})不一致,请重新选择序列号或调整数量`);
1690   - console.log(`第${i + 1}行商品"${row.spmc}"序列号数量不匹配,已添加到验证错误`);
1691   - } else {
1692   - console.log(`第${i + 1}行商品序列号验证通过: ${serialNumberCount}个序列号,数量${detailQuantity}`);
1693   - }
1694   - }
1695   - } else {
1696   - console.log(`第${i + 1}行商品不需要序列号选择,序列号类型: ${spxlhType}`);
1697   - }
1698   - } else {
1699   - console.log(`第${i + 1}行商品"${row.spmc}"无法获取序列号类型信息`);
1700   - // 如果无法获取商品信息,记录错误但不阻止提交
1701   - validationErrors.push(`第${i + 1}行商品"${row.spmc}"无法获取序列号类型信息,请确认商品配置`);
1702   - }
1703   - }
1704   - }
1705   -
1706   - // 如果有验证错误,显示错误信息并询问是否继续
1707   - if (validationErrors.length > 0) {
1708   - const errorMessage = validationErrors.join('\n');
1709   - console.log('序列号验证发现以下问题:', validationErrors);
1710   -
1711   - // 添加详细的验证结果说明
1712   - let detailedMessage = '序列号验证发现以下问题:\n\n';
1713   - detailedMessage += errorMessage;
1714   - detailedMessage += '\n\n';
1715   - detailedMessage += '说明:\n';
1716   - detailedMessage += '- 序列号类型为"入1出1"或"入0出1"的商品必须选择序列号\n';
1717   - detailedMessage += '- 序列号数量必须与明细数量完全一致\n';
1718   - detailedMessage += '- 请为上述商品选择正确的序列号后重新保存';
1719   -
1720   - await this.$alert(detailedMessage, '序列号验证结果', {
1721   - confirmButtonText: '确定',
1722   - type: 'warning'
1723   - });
1724   - } else {
1725   - this.$message.success('序列号验证通过!');
1726   - }
1727   - },
1728 1021 getSummaries(param) {
1729 1022 const { columns, data } = param;
1730 1023 const sums = [];
... ... @@ -1760,42 +1053,23 @@
1760 1053 this.$set(row, 'je', undefined)
1761 1054 this.$set(row, 'description', '')
1762 1055 this.$set(row, 'kucun', undefined)
1763   - this.$set(row, 'spxlhLoaded', false)
1764   - this.$set(row, 'xlhList', [])
  1056 + this.$set(row, 'bjhcb', undefined)
  1057 + this.$set(row, 'bjhcbManual', false)
1765 1058 return
1766 1059 }
1767   - // 选中商品后可自动回填商品名称等信息
1768 1060 const product = this.spbhOptions.find(item => item.F_Id === row.spbh);
1769 1061 if (product) {
1770 1062 row.spmc = product.F_Spmc || '';
1771   - // 选中商品后,立即加载商品信息(如序列号类型)
1772   - this.getProductInfo(row.spbh).then(productInfo => {
1773   - // 触发当前行的响应式刷新
1774   - this.$set(row, 'spxlhLoaded', true);
1775   - this.$forceUpdate();
1776   - if (!(productInfo && productInfo.spxlhType)) {
1777   - this.$message.warning('无法获取序列号类型信息');
1778   - }
1779   - });
1780   -
1781   - // 检查出库仓库并获取库存
1782   - console.log('商品选择变化 - 商品ID:', row.spbh, '出库仓库:', row.ckck, '主表出库仓库:', this.dataForm.cjck);
1783   -
1784   - // 如果明细行没有出库仓库,使用主表的出库仓库
1785   - if (!row.ckck && this.dataForm.cjck) {
1786   - row.ckck = this.dataForm.cjck;
1787   - console.log('使用主表出库仓库:', row.ckck);
1788   - }
1789   -
1790   - // 如果主表也没有出库仓库,提示用户先选择
  1063 +
  1064 + // 明细行默认使用主表出库/入库仓库
  1065 + if (!row.ckck && this.dataForm.cjck) row.ckck = this.dataForm.cjck;
  1066 + if (!row.rkck && this.dataForm.rkck) row.rkck = this.dataForm.rkck;
  1067 +
1791 1068 if (!row.ckck) {
1792   - console.log('出库仓库未设置,无法获取库存');
1793 1069 this.$message.warning('请先选择出库仓库');
1794 1070 return;
1795 1071 }
1796   -
1797   - // 如果已选择出库仓库,自动获取库存(内部会再拉成本单价)
1798   - console.log('开始获取库存...');
  1072 + // 自动获取库存 + 成本单价(成本来自 wt_sp_cost 的调出仓加权平均成本)
1799 1073 await this.getStockQuantity(row);
1800 1074 }
1801 1075 },
... ... @@ -1822,77 +1096,82 @@
1822 1096 });
1823 1097 },
1824 1098  
1825   - // 计算所有明细的变价后成本
  1099 + /**
  1100 + * 按变价系数自动填充每行"变价后成本":
  1101 + * - 仅针对未手工录入(bjhcbManual !== true)的行
  1102 + * - 变价系数为空时不清空用户已录入的 bjhcb
  1103 + */
1826 1104 calculateAllAdjustedCosts() {
1827 1105 if (!this.dataForm.bjsx || this.dataForm.bjsx === '') {
1828   - // 如果没有输入变价系数,清空所有变价后成本
1829 1106 this.dataForm.wtXsckdMxList.forEach(row => {
1830   - this.$set(row, 'bjhcb', '');
  1107 + if (!row.bjhcbManual) this.$set(row, 'bjhcb', '');
1831 1108 });
1832 1109 return;
1833 1110 }
1834   -
1835 1111 const coefficient = parseFloat(this.dataForm.bjsx);
1836   - if (isNaN(coefficient)) {
1837   - this.$message.warning('请输入有效的变价系数');
  1112 + if (isNaN(coefficient) || coefficient <= 0) {
1838 1113 return;
1839 1114 }
1840   -
1841   - // 按照公式计算:变价后成本 = 成本 / (1 - 系数/100)
1842   - // 例如:成本100,系数95,则变价后成本 = 100 / (1 - 95/100) = 100 / 0.05 = 2000
1843   - // 但根据你的要求:成本100,系数95,变价后成本是105.26
1844   - // 这个公式应该是:变价后成本 = 成本 / (系数/100)
1845   - // 例如:成本100,系数95,则变价后成本 = 100 / (95/100) = 100 / 0.95 = 105.26
1846   -
  1115 + // 变价后成本 = 成本单价 / (系数/100),例:100 / (95/100) = 105.2632
1847 1116 this.dataForm.wtXsckdMxList.forEach(row => {
1848   - if (row.dj !== undefined && row.dj !== null && row.dj !== '') {
1849   - const cost = parseFloat(row.dj);
1850   - if (!isNaN(cost) && cost > 0) {
1851   - const adjustedCost = cost / (coefficient / 100);
1852   - this.$set(row, 'bjhcb', adjustedCost.toFixed(4));
1853   - } else {
1854   - this.$set(row, 'bjhcb', '');
1855   - }
  1117 + if (row.bjhcbManual) return;
  1118 + const cost = parseFloat(row.dj);
  1119 + if (!isNaN(cost) && cost > 0) {
  1120 + this.$set(row, 'bjhcb', (cost / (coefficient / 100)).toFixed(4));
1856 1121 } else {
1857 1122 this.$set(row, 'bjhcb', '');
1858 1123 }
1859 1124 });
1860 1125 this.$forceUpdate();
1861 1126 },
1862   -
1863   - // 计算单行的变价后成本
  1127 +
  1128 + /** 单行根据系数重算变价后成本(仅在未手工录入时) */
1864 1129 calculateRowAdjustedCost(row) {
  1130 + if (row.bjhcbManual) return;
1865 1131 if (!this.dataForm.bjsx || this.dataForm.bjsx === '' || !row.dj || row.dj === '') {
1866 1132 this.$set(row, 'bjhcb', '');
1867 1133 return;
1868 1134 }
1869   -
1870 1135 const coefficient = parseFloat(this.dataForm.bjsx);
1871 1136 const cost = parseFloat(row.dj);
1872   -
1873   - if (isNaN(coefficient) || isNaN(cost) || cost <= 0) {
  1137 + if (isNaN(coefficient) || coefficient <= 0 || isNaN(cost) || cost <= 0) {
1874 1138 this.$set(row, 'bjhcb', '');
1875 1139 return;
1876 1140 }
1877   -
1878   - // 按照公式计算:变价后成本 = 成本 / (系数/100)
1879   - // 例如:成本100,系数95,则变价后成本 = 100 / (95/100) = 100 / 0.95 = 105.26
1880   - const adjustedCost = cost / (coefficient / 100);
1881   - this.$set(row, 'bjhcb', adjustedCost.toFixed(4));
  1141 + this.$set(row, 'bjhcb', (cost / (coefficient / 100)).toFixed(4));
1882 1142 }
1883 1143 }
1884 1144 }
1885 1145 </script>
1886 1146  
1887 1147 <style scoped>
1888   -.serial-number-selector {
1889   - min-height: 30px;
  1148 +/* 账面库存:文本 + 小刷新按钮(与其他 mini 输入框高度对齐) */
  1149 +.bjdbd-kucun-cell {
  1150 + display: flex;
  1151 + align-items: center;
  1152 + justify-content: flex-end;
  1153 + line-height: 28px;
  1154 + height: 28px;
  1155 +}
  1156 +.bjdbd-kucun-text {
  1157 + flex: 1;
  1158 + text-align: right;
  1159 + padding-right: 6px;
  1160 + color: #303133;
  1161 + font-size: 13px;
  1162 + overflow: hidden;
  1163 + text-overflow: ellipsis;
  1164 + white-space: nowrap;
  1165 +}
  1166 +.bjdbd-kucun-refresh {
  1167 + padding: 0 4px !important;
  1168 + height: 24px;
1890 1169 }
1891 1170  
1892   -.selected-serial-numbers {
1893   - display: flex;
1894   - flex-wrap: wrap;
1895   - gap: 2px;
1896   - margin-bottom: 5px;
  1171 +/* 明细表内所有 mini 控件统一高度,避免行高差异 */
  1172 +.el-table--mini .el-input--mini .el-input__inner,
  1173 +.el-table--mini .el-input--mini .el-input__icon {
  1174 + height: 28px;
  1175 + line-height: 28px;
1897 1176 }
1898 1177 </style>
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtBsd/Form.vue
... ... @@ -513,12 +513,12 @@
513 513  
514 514  
515 515 getcjckOptions(){
516   - previewDataInterface('681758216954053893').then(res => {
  516 + return previewDataInterface('681758216954053893').then(res => {
517 517 this.cjckOptions = res.data
518 518 });
519 519 },
520 520 getrkckOptions(){
521   - previewDataInterface('681758216954053893').then(res => {
  521 + return previewDataInterface('681758216954053893').then(res => {
522 522 this.rkckOptions = res.data
523 523 });
524 524 },
... ... @@ -543,10 +543,35 @@
543 543 // });
544 544 // },
545 545 getckckOptions(){
546   - previewDataInterface('681758216954053893').then(res => {
  546 + return previewDataInterface('681758216954053893').then(res => {
547 547 this.ckckOptions = res.data
548 548 });
549 549 },
  550 + /**
  551 + * 详情接口返回时后端已把 ckck/rkck/cjck/rkck 替换为展示名称(门店名称),
  552 + * 但编辑页的 el-select 绑定的是 ID;这里把名称反查回 ID,避免:
  553 + * 1) 仓库下拉显示空
  554 + * 2) 按名称查询库存失败 → 账面库存为 0 不显示
  555 + */
  556 + resolveIdByName(val, options) {
  557 + if (val == null || val === '') return val;
  558 + const v = String(val).trim();
  559 + if (!options || !options.length) return val;
  560 + // 若已经是合法 ID(命中选项 F_Id),直接返回
  561 + if (options.some(o => o && String(o.F_Id) === v)) return val;
  562 + // 否则尝试按名称反查
  563 + const hit = options.find(o => o && (o.F_mdmc === v || o.F_ckmc === v || o.F_name === v));
  564 + return hit ? hit.F_Id : val;
  565 + },
  566 + async ensureWarehouseOptionsReady() {
  567 + const tasks = [];
  568 + if (!this.cjckOptions || !this.cjckOptions.length) tasks.push(this.getcjckOptions());
  569 + if (!this.rkckOptions || !this.rkckOptions.length) tasks.push(this.getrkckOptions());
  570 + if (!this.ckckOptions || !this.ckckOptions.length) tasks.push(this.getckckOptions());
  571 + if (tasks.length > 0) {
  572 + try { await Promise.all(tasks); } catch (e) { /* 选项加载失败时降级继续 */ }
  573 + }
  574 + },
550 575 // getspbhOptions(){
551 576 // previewDataInterface('675937572047815941').then(res => {
552 577 // this.spbhOptions = res.data
... ... @@ -612,26 +637,86 @@
612 637 request({
613 638 url: '/api/Extend/WtXsckd/' +id,
614 639 method: 'get'
615   - }).then(res =>{
  640 + }).then(async res =>{
616 641 _this.dataForm = res.data;
617 642 console.log('编辑时加载的数据:', _this.dataForm);
618 643 console.log('明细数据:', _this.dataForm.wtXsckdMxList);
619 644  
620   - // 为每个明细项添加productQuery字段
  645 + // 后端详情接口会把 cjck / 明细 ckck 等字段由 ID 替换为展示名称;
  646 + // 编辑页 el-select 绑的是 ID,且 getStockQuantity 也需要 ID,
  647 + // 这里等仓库选项加载好后做一次「名称 → ID」反查,解决账面库存不显示问题
  648 + await _this.ensureWarehouseOptionsReady();
  649 + _this.dataForm.cjck = _this.resolveIdByName(_this.dataForm.cjck, _this.cjckOptions);
  650 + _this.dataForm.rkck = _this.resolveIdByName(_this.dataForm.rkck, _this.rkckOptions);
  651 +
621 652 if (_this.dataForm.wtXsckdMxList) {
622 653 _this.dataForm.wtXsckdMxList.forEach(item => {
623 654 if (!item.hasOwnProperty('productQuery')) {
624 655 _this.$set(item, 'productQuery', '');
625 656 }
  657 + if (!item.hasOwnProperty('loadingStock')) {
  658 + _this.$set(item, 'loadingStock', false);
  659 + }
  660 + // kucun 后端未落库,这里先显式声明为响应式属性,
  661 + // 后续 getStockQuantity 在 catch/else 分支中直接赋值才能触发视图更新
  662 + if (!item.hasOwnProperty('kucun')) {
  663 + _this.$set(item, 'kucun', undefined);
  664 + }
  665 + // 明细仓库字段同样做名称→ID 反查
  666 + _this.$set(item, 'ckck', _this.resolveIdByName(item.ckck, _this.ckckOptions));
  667 + if (item.rkck) {
  668 + _this.$set(item, 'rkck', _this.resolveIdByName(item.rkck, _this.rkckOptions));
  669 + }
  670 + // 金额回显:若后端 je 为 0 但 sl/dj 有值,则按 sl*dj 重算;否则回退使用 cbje
  671 + const slNum = parseFloat(item.sl) || 0;
  672 + const djNum = parseFloat(item.dj) || 0;
  673 + const jeNum = parseFloat(item.je) || 0;
  674 + const cbjeNum = parseFloat(item.cbje) || 0;
  675 + if (jeNum === 0) {
  676 + if (slNum > 0 && djNum > 0) {
  677 + _this.$set(item, 'je', (slNum * djNum).toFixed(2));
  678 + } else if (cbjeNum > 0) {
  679 + _this.$set(item, 'je', cbjeNum.toFixed(2));
  680 + if (djNum === 0) {
  681 + const cbdjNum = parseFloat(item.cbdj) || 0;
  682 + if (cbdjNum > 0) _this.$set(item, 'dj', cbdjNum.toFixed(4));
  683 + }
  684 + }
  685 + }
626 686 });
627 687 }
628 688  
629   - // 初始化时计算总金额
630 689 _this.calculateTotalAmount();
631   - // 同步明细表出库仓库
632 690 _this.syncDetailWarehouses();
633   - // 恢复序列号信息
634 691 _this.restoreSerialNumbers();
  692 + // 序列号类型(spxlhType)只缓存在 productCache 中,且 row.spxlhLoaded
  693 + // 仅在 handleProductChange 里被置 true;打开已有单据需要主动拉一次,
  694 + // 否则「序列号类型」列会一直显示「加载中...」
  695 + if (_this.dataForm.wtXsckdMxList && _this.dataForm.wtXsckdMxList.length > 0) {
  696 + _this.dataForm.wtXsckdMxList.forEach(item => {
  697 + if (!item.hasOwnProperty('spxlhLoaded')) {
  698 + _this.$set(item, 'spxlhLoaded', false);
  699 + }
  700 + });
  701 + _this.getAllProductSerialNumberTypes().then(() => {
  702 + _this.dataForm.wtXsckdMxList.forEach(item => {
  703 + if (item.spbh) {
  704 + _this.$set(item, 'spxlhLoaded', true);
  705 + }
  706 + });
  707 + _this.$forceUpdate();
  708 + }).catch(err => {
  709 + console.error('批量获取序列号类型失败:', err);
  710 + });
  711 + }
  712 + // 账面库存不落库,重新点开必须按商品+仓库查询刷新(此时 ckck 已是 ID)
  713 + if (_this.dataForm.wtXsckdMxList) {
  714 + _this.dataForm.wtXsckdMxList.forEach(item => {
  715 + if (item.spbh && item.ckck) {
  716 + _this.getStockQuantity(item);
  717 + }
  718 + });
  719 + }
635 720 })
636 721 }
637 722 else{
... ... @@ -639,10 +724,26 @@
639 724 }
640 725 })
641 726 },
  727 + // 校验所有明细的报损数量不超过账面库存
  728 + validateBsdSlNotExceedKucun() {
  729 + const list = this.dataForm.wtXsckdMxList || [];
  730 + for (let i = 0; i < list.length; i++) {
  731 + const row = list[i];
  732 + if (!row || !row.spbh) continue;
  733 + const sl = parseFloat(row.sl);
  734 + const kucun = parseFloat(row.kucun);
  735 + if (!isNaN(kucun) && !isNaN(sl) && sl > kucun) {
  736 + this.$message.error(`第${i + 1}行商品"${row.spmc || ''}"报损数量(${sl})超过账面库存(${kucun}),请先修正`);
  737 + return false;
  738 + }
  739 + }
  740 + return true;
  741 + },
642 742 // 保存草稿:只做基础表单校验,不做序列号强校验,保存时带isDraft:true字段
643 743 async saveDraft() {
644 744 // 确保单据类型字段赋值
645 745 this.dataForm.djlx = '报损单';
  746 + if (!this.validateBsdSlNotExceedKucun()) return;
646 747 this.$refs['elForm'].validate(async (valid) => {
647 748 if (valid) {
648 749 try {
... ... @@ -685,6 +786,7 @@
685 786 this.$message.warning(`第 ${mxCheck.emptyLineNos.join('、')} 行未选择商品,请先删除空白行后再提交`)
686 787 return
687 788 }
  789 + if (!this.validateBsdSlNotExceedKucun()) return;
688 790 // 1. 明细校验:序列号数量与销售数量一致性
689 791 let validationErrors = [];
690 792 for (let i = 0; i < this.dataForm.wtXsckdMxList.length; i++) {
... ... @@ -1032,8 +1134,15 @@
1032 1134 }
1033 1135 },
1034 1136 handleAmountChange(row) {
1035   - const sl = parseFloat(row.sl) || 0;
  1137 + let sl = parseFloat(row.sl) || 0;
1036 1138 const dj = parseFloat(row.dj) || 0;
  1139 + // 报损数量不能超过账面库存
  1140 + const kucun = parseFloat(row.kucun);
  1141 + if (!isNaN(kucun) && sl > kucun) {
  1142 + this.$message.warning(`商品"${row.spmc || ''}"报损数量(${sl})不能超过账面库存(${kucun})`);
  1143 + sl = kucun;
  1144 + row.sl = kucun.toString();
  1145 + }
1037 1146 row.je = (sl * dj).toFixed(2);
1038 1147 // 自动计算总收款金额
1039 1148 this.calculateTotalAmount();
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtByd/Form.vue
... ... @@ -513,12 +513,12 @@
513 513  
514 514  
515 515 getcjckOptions(){
516   - previewDataInterface('681758216954053893').then(res => {
  516 + return previewDataInterface('681758216954053893').then(res => {
517 517 this.cjckOptions = res.data
518 518 });
519 519 },
520 520 getrkckOptions(){
521   - previewDataInterface('681758216954053893').then(res => {
  521 + return previewDataInterface('681758216954053893').then(res => {
522 522 this.rkckOptions = res.data
523 523 });
524 524 },
... ... @@ -543,10 +543,31 @@
543 543 // });
544 544 // },
545 545 getckckOptions(){
546   - previewDataInterface('681758216954053893').then(res => {
  546 + return previewDataInterface('681758216954053893').then(res => {
547 547 this.ckckOptions = res.data
548 548 });
549 549 },
  550 + /**
  551 + * 后端详情接口会把仓库字段由 ID 替换成展示名称,编辑页绑定 ID,
  552 + * 这里做名称→ID 反查,确保下拉能回显、按 ID 查账面库存生效
  553 + */
  554 + resolveIdByName(val, options) {
  555 + if (val == null || val === '') return val;
  556 + const v = String(val).trim();
  557 + if (!options || !options.length) return val;
  558 + if (options.some(o => o && String(o.F_Id) === v)) return val;
  559 + const hit = options.find(o => o && (o.F_mdmc === v || o.F_ckmc === v || o.F_name === v));
  560 + return hit ? hit.F_Id : val;
  561 + },
  562 + async ensureWarehouseOptionsReady() {
  563 + const tasks = [];
  564 + if (!this.cjckOptions || !this.cjckOptions.length) tasks.push(this.getcjckOptions());
  565 + if (!this.rkckOptions || !this.rkckOptions.length) tasks.push(this.getrkckOptions());
  566 + if (!this.ckckOptions || !this.ckckOptions.length) tasks.push(this.getckckOptions());
  567 + if (tasks.length > 0) {
  568 + try { await Promise.all(tasks); } catch (e) { /* 选项加载失败时降级继续 */ }
  569 + }
  570 + },
550 571 // getspbhOptions(){
551 572 // previewDataInterface('675937572047815941').then(res => {
552 573 // this.spbhOptions = res.data
... ... @@ -612,26 +633,86 @@
612 633 request({
613 634 url: '/api/Extend/WtXsckd/' +id,
614 635 method: 'get'
615   - }).then(res =>{
  636 + }).then(async res =>{
616 637 _this.dataForm = res.data;
617 638 console.log('编辑时加载的数据:', _this.dataForm);
618 639 console.log('明细数据:', _this.dataForm.wtXsckdMxList);
619 640  
620   - // 为每个明细项添加productQuery字段
  641 + // 后端详情接口会把 cjck / 明细 ckck 字段由 ID 替换为展示名称;
  642 + // 编辑页 el-select 绑的是 ID,且 getStockQuantity 也需要 ID,
  643 + // 这里等仓库选项加载好后做一次「名称 → ID」反查
  644 + await _this.ensureWarehouseOptionsReady();
  645 + _this.dataForm.cjck = _this.resolveIdByName(_this.dataForm.cjck, _this.cjckOptions);
  646 + _this.dataForm.rkck = _this.resolveIdByName(_this.dataForm.rkck, _this.rkckOptions);
  647 +
621 648 if (_this.dataForm.wtXsckdMxList) {
622 649 _this.dataForm.wtXsckdMxList.forEach(item => {
623 650 if (!item.hasOwnProperty('productQuery')) {
624 651 _this.$set(item, 'productQuery', '');
625 652 }
  653 + if (!item.hasOwnProperty('loadingStock')) {
  654 + _this.$set(item, 'loadingStock', false);
  655 + }
  656 + // kucun 后端未落库,这里先显式声明为响应式属性,
  657 + // 后续 getStockQuantity 在 catch/else 分支中直接赋值才能触发视图更新
  658 + if (!item.hasOwnProperty('kucun')) {
  659 + _this.$set(item, 'kucun', undefined);
  660 + }
  661 + // 明细仓库字段同样做名称→ID 反查
  662 + _this.$set(item, 'ckck', _this.resolveIdByName(item.ckck, _this.ckckOptions));
  663 + if (item.rkck) {
  664 + _this.$set(item, 'rkck', _this.resolveIdByName(item.rkck, _this.rkckOptions));
  665 + }
  666 + // 金额回显:若后端 je 为 0 但 sl/dj 有值,则按 sl*dj 重算;否则回退使用 cbje
  667 + const slNum = parseFloat(item.sl) || 0;
  668 + const djNum = parseFloat(item.dj) || 0;
  669 + const jeNum = parseFloat(item.je) || 0;
  670 + const cbjeNum = parseFloat(item.cbje) || 0;
  671 + if (jeNum === 0) {
  672 + if (slNum > 0 && djNum > 0) {
  673 + _this.$set(item, 'je', (slNum * djNum).toFixed(2));
  674 + } else if (cbjeNum > 0) {
  675 + _this.$set(item, 'je', cbjeNum.toFixed(2));
  676 + if (djNum === 0) {
  677 + const cbdjNum = parseFloat(item.cbdj) || 0;
  678 + if (cbdjNum > 0) _this.$set(item, 'dj', cbdjNum.toFixed(4));
  679 + }
  680 + }
  681 + }
626 682 });
627 683 }
628 684  
629   - // 初始化时计算总金额
630 685 _this.calculateTotalAmount();
631   - // 同步明细表出库仓库
632 686 _this.syncDetailWarehouses();
633   - // 恢复序列号信息
634 687 _this.restoreSerialNumbers();
  688 + // 序列号类型(spxlhType)只缓存在 productCache 中,且 row.spxlhLoaded
  689 + // 仅在 handleProductChange 里被置 true;打开已有单据需要主动拉一次,
  690 + // 否则「序列号类型」列会一直显示「加载中...」
  691 + if (_this.dataForm.wtXsckdMxList && _this.dataForm.wtXsckdMxList.length > 0) {
  692 + _this.dataForm.wtXsckdMxList.forEach(item => {
  693 + if (!item.hasOwnProperty('spxlhLoaded')) {
  694 + _this.$set(item, 'spxlhLoaded', false);
  695 + }
  696 + });
  697 + _this.getAllProductSerialNumberTypes().then(() => {
  698 + _this.dataForm.wtXsckdMxList.forEach(item => {
  699 + if (item.spbh) {
  700 + _this.$set(item, 'spxlhLoaded', true);
  701 + }
  702 + });
  703 + _this.$forceUpdate();
  704 + }).catch(err => {
  705 + console.error('批量获取序列号类型失败:', err);
  706 + });
  707 + }
  708 + // 账面库存不落库,重新点开必须按商品+仓库查询刷新(此时 ckck 已是 ID)
  709 + if (_this.dataForm.wtXsckdMxList) {
  710 + _this.dataForm.wtXsckdMxList.forEach(item => {
  711 + if (item.spbh && item.ckck) {
  712 + _this.getStockQuantity(item);
  713 + }
  714 + });
  715 + }
635 716 })
636 717 }
637 718 else{
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtCgrkd/Form.vue
1 1 <template>
2 2 <el-dialog :title="!dataForm.id ? '新建' : isDetail ? '详情':'编辑'" :close-on-click-modal="false" :visible.sync="visible" class="NCC-dialog NCC-dialog_center" lock-scroll width="80%">
3 3 <el-row :gutter="15" class="" >
4   - <el-form ref="elForm" :model="dataForm" size="small" label-width="100px" label-position="right" :disabled="!!isDetail" :rules="rules">
  4 + <el-form ref="elForm" :model="dataForm" size="small" label-width="100px" label-position="right" :disabled="formDisabled" :rules="rules">
5 5 <el-col :span="12" v-if="false">
6 6 <el-form-item label="单据编号" prop="id">
7 7 <el-input v-model="dataForm.id" placeholder="请输入" clearable readonly :style='{"width":"100%"}' >
... ... @@ -196,8 +196,10 @@
196 196 </el-row>
197 197 <span slot="footer" class="dialog-footer">
198 198 <el-button @click="visible = false">取 消</el-button>
199   - <el-button type="default" @click="saveDraft()" v-if="!isDetail">保存草稿</el-button>
200   - <el-button type="primary" @click="dataFormSubmit()" v-if="!isDetail">保 存</el-button>
  199 + <template v-if="!formDisabled">
  200 + <el-button type="default" @click="saveDraft()">保存草稿</el-button>
  201 + <el-button type="primary" @click="dataFormSubmit()">提交审核</el-button>
  202 + </template>
201 203 </span>
202 204  
203 205 <!-- 商品条码选择弹窗 -->
... ... @@ -237,6 +239,9 @@
237 239  
238 240 },
239 241 rules: {
  242 + gys: [
  243 + { required: true, message: '请选择往来单位', trigger: 'change' }
  244 + ]
240 245 },
241 246 cjckOptions : [],
242 247 // rkckOptions : [],
... ... @@ -249,6 +254,15 @@
249 254 }
250 255 },
251 256 watch: {},
  257 + computed: {
  258 + isBillLockedForEdit() {
  259 + const z = this.dataForm && this.dataForm.djzt != null ? String(this.dataForm.djzt).trim() : ''
  260 + return z === '待审核' || z === '已审核' || z === '一级已审' || z === '待二级' || z === '一级已审/待二级'
  261 + },
  262 + formDisabled() {
  263 + return !!this.isDetail || this.isBillLockedForEdit
  264 + }
  265 + },
252 266 created() {
253 267 this.getcjckOptions();
254 268 // this.getrkckOptions();
... ... @@ -390,13 +404,22 @@ setFullName(item,row){
390 404 });
391 405 }
392 406 })
  407 + } else {
  408 + // 新建时 resetFields 会清空默认值,这里重新回填经手人/类型/状态
  409 + const user = (this.$store && this.$store.getters && this.$store.getters.userInfo) ? this.$store.getters.userInfo : {}
  410 + this.dataForm.djrq = Date.now()
  411 + this.dataForm.jsr = user.userId || user.id || ''
  412 + this.dataForm.djlx = '采购入库单'
  413 + if (!this.dataForm.djzt) this.dataForm.djzt = '草稿'
393 414 }
394 415 })
395 416 },
396 417 async saveDraft() {
  418 + if (this.formDisabled) return
397 419 await this.submitWithMode(true)
398 420 },
399 421 async dataFormSubmit() {
  422 + if (this.formDisabled) return
400 423 await this.submitWithMode(false)
401 424 },
402 425 async submitWithMode(isDraftMode) {
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtCgrkd/index.vue
... ... @@ -16,13 +16,18 @@
16 16 </el-col>
17 17 <el-col :span="6">
18 18 <el-form-item label="入库仓库">
19   - <el-select v-model="query.cjck" placeholder="入库仓库" clearable >
  19 + <el-select v-model="query.rkck" placeholder="入库仓库" clearable >
20 20 <el-option v-for="(item, index) in cjckOptions" :key="index" :label="item.F_mdmc" :value="item.F_Id" />
21 21 </el-select>
22 22 </el-form-item>
23 23 </el-col>
24 24 <template v-if="showAll">
25 25 <el-col :span="6">
  26 + <el-form-item label="往来单位">
  27 + <el-input v-model="query.gys" placeholder="往来单位" clearable />
  28 + </el-form-item>
  29 + </el-col>
  30 + <el-col :span="6">
26 31 <el-form-item label="经手人">
27 32 <userSelect v-model="query.jsr" placeholder="请选择经手人" />
28 33 </el-form-item>
... ... @@ -84,26 +89,27 @@
84 89 <screenfull isContainer />
85 90 </div>
86 91 </div>
87   - <NCC-table v-loading="listLoading" :data="list" has-c @selection-change="handleSelectionChange">
88   - <el-table-column prop="id" label="单据编号" align="left" />
89   - <el-table-column prop="djrq" label="单据日期" align="left" :formatter="ncc.tableDateFormat" />
90   - <el-table-column label="入库仓库" prop="cjck" align="left">
91   - <template slot-scope="scope">{{ scope.row.cjck | dynamicText(cjckOptions) }}</template>
  92 + <NCC-table v-loading="listLoading" :data="list" has-c @selection-change="handleSelectionChange">
  93 + <el-table-column prop="id" label="单据编号" align="left" show-overflow-tooltip class-name="cell-nowrap" />
  94 + <el-table-column prop="djrq" label="单据日期" align="left" :formatter="ncc.tableDateFormat" show-overflow-tooltip class-name="cell-nowrap" />
  95 + <el-table-column label="入库仓库" prop="rkck" align="left" show-overflow-tooltip class-name="cell-nowrap">
  96 + <template slot-scope="scope">{{ scope.row.rkck | dynamicText(cjckOptions) }}</template>
92 97 </el-table-column>
93   - <el-table-column prop="jsr" label="经手人" align="left" />
94   - <el-table-column label="付款账户" prop="skzh" align="left">
  98 + <el-table-column prop="jsr" label="经手人" align="left" show-overflow-tooltip class-name="cell-nowrap" />
  99 + <el-table-column prop="gys" label="往来单位" align="left" min-width="140" show-overflow-tooltip class-name="cell-nowrap" />
  100 + <el-table-column label="付款账户" prop="skzh" align="left" show-overflow-tooltip class-name="cell-nowrap">
95 101 <template slot-scope="scope">{{ scope.row.skzh | dynamicText(skzhOptions) }}</template>
96 102 </el-table-column>
97   - <el-table-column prop="skje" label="付款金额" align="left" />
98   - <el-table-column prop="shr" label="审核人" align="left">
  103 + <el-table-column prop="skje" label="付款金额" align="left" show-overflow-tooltip class-name="cell-nowrap" />
  104 + <el-table-column prop="shr" label="审核人" align="left" show-overflow-tooltip class-name="cell-nowrap">
99 105 <template slot-scope="scope">{{ getShrDisplay(scope.row) }}</template>
100 106 </el-table-column>
101   - <el-table-column prop="gzr" label="过账人" align="left">
  107 + <el-table-column prop="gzr" label="过账人" align="left" show-overflow-tooltip class-name="cell-nowrap">
102 108 <template slot-scope="scope">{{ getGzrDisplay(scope.row) }}</template>
103 109 </el-table-column>
104   - <el-table-column prop="bz" label="备注" align="left" />
105   - <el-table-column prop="djlx" label="单据类型" align="left" />
106   - <el-table-column prop="djzt" label="审核状态" align="left">
  110 + <el-table-column prop="bz" label="备注" align="left" show-overflow-tooltip class-name="cell-nowrap" />
  111 + <el-table-column prop="djlx" label="单据类型" align="left" show-overflow-tooltip class-name="cell-nowrap" />
  112 + <el-table-column prop="djzt" label="审核状态" align="left" show-overflow-tooltip class-name="cell-nowrap">
107 113 <template slot-scope="scope">
108 114 <el-tag v-if="scope.row.djzt === '已审核'" type="success">已审核</el-tag>
109 115 <el-tag v-else-if="scope.row.djzt === '一级已审'" type="">一级已审</el-tag>
... ... @@ -117,19 +123,14 @@
117 123 <ncc-table-summary-cell :row="scope.row" fields="zy,Zy" />
118 124 </template>
119 125 </el-table-column>
120   - <el-table-column label="操作" fixed="right" width="310">
  126 + <el-table-column label="操作" fixed="right" width="310" show-overflow-tooltip class-name="cell-nowrap">
121 127 <template slot-scope="scope">
122 128 <el-button type="text" @click="openDetail(scope.row.id)">查看</el-button>
123   - <el-button
124   - type="text"
125   - :disabled="scope.row.djzt && scope.row.djzt !== '草稿' && scope.row.djzt !== '审核不通过'"
126   - @click="addOrUpdateHandle(scope.row.id)"
127   - >编辑</el-button>
  129 + <el-button v-if="isDraftRow(scope.row)" type="text" @click="addOrUpdateHandle(scope.row.id)">编辑</el-button>
128 130 <el-button type="text" @click="handleApprove(scope.row.id)" v-if="scope.row.djzt === '待审核' || !scope.row.djzt" style="color:#E6A23C">一级审核</el-button>
129 131 <el-button type="text" @click="handleApprove(scope.row.id)" v-if="scope.row.djzt === '一级已审' || scope.row.djzt === '待二级' || scope.row.djzt === '一级已审/待二级'" style="color:#409EFF">二级审核</el-button>
130   - <el-button type="text" @click="handleReject(scope.row.id)" v-if="canRejectAuditPurchaseInbound(scope.row)" style="color:#F56C6C">审核不通过</el-button>
131 132 <el-button type="text" @click="handleReverseApproval(scope.row.id)" v-if="scope.row.djzt === '已审核'" style="color:#F56C6C">反审</el-button>
132   - <el-button type="text" @click="handleDel(scope.row.id)" class="NCC-table-delBtn" >删除</el-button>
  133 + <el-button v-if="isDraftRow(scope.row)" type="text" @click="handleDel(scope.row.id)" class="NCC-table-delBtn">删除</el-button>
133 134 </template>
134 135 </el-table-column>
135 136 </NCC-table>
... ... @@ -147,7 +148,7 @@
147 148 import DetailView from './detail-view'
148 149 import ExportBox from './ExportBox'
149 150 import { previewDataInterface } from '@/api/systemData/dataInterface'
150   - import { promptApprovalRemark, postApprovePurchaseInbound, postRejectGeneric } from '@/utils/wtRejectApproval'
  151 + import { promptApprovalRemark, postApprovePurchaseInbound } from '@/utils/wtRejectApproval'
151 152 import { getAccountSelector } from '@/api/extend/wtAccount'
152 153 export default {
153 154 components: { NCCForm, DetailView, ExportBox },
... ... @@ -157,7 +158,8 @@
157 158 query: {
158 159 id:undefined,
159 160 djrq:undefined,
160   - cjck:undefined,
  161 + rkck:undefined,
  162 + gys: undefined,
161 163 jsr:undefined,
162 164 skzh:undefined,
163 165 skje:undefined,
... ... @@ -204,6 +206,9 @@
204 206 this.getskzhOptions();
205 207 },
206 208 methods: {
  209 + isDraftRow(row) {
  210 + return row && String(row.djzt || '').trim() === '草稿'
  211 + },
207 212 getShrDisplay(row) {
208 213 const status = row && row.djzt
209 214 if (status === '一级已审' || status === '已审核') return (row && row.shr) || ''
... ... @@ -295,24 +300,6 @@
295 300 }
296 301 }).catch(() => {});
297 302 },
298   - canRejectAuditPurchaseInbound(row) {
299   - const z = row && row.djzt
300   - if (!row || z === '已审核' || z === '草稿' || z === '审核不通过') return false
301   - return (z === '待审核' || !z) || z === '一级已审' || z === '待二级' || z === '一级已审/待二级'
302   - },
303   - handleReject(id) {
304   - promptApprovalRemark(this, '审核不通过')
305   - .then(reason => postRejectGeneric(id, reason))
306   - .then(res => {
307   - if (res.data && res.data.success) {
308   - this.$message({ type: 'success', message: res.data.message || '已标记审核不通过' })
309   - this.initData()
310   - } else {
311   - this.$message({ type: 'error', message: (res.data && res.data.message) || '操作失败' })
312   - }
313   - })
314   - .catch(() => {})
315   - },
316 303 handleReverseApproval(id) {
317 304 this.$confirm('确认反审该单据?反审后单据将恢复为草稿状态,可重新编辑。', '反审确认', {
318 305 type: 'warning'
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtCgthd/Form.vue
1 1 <template>
2   - <el-dialog :title="isNew ? '新建' : isDetail ? '详情':'编辑'" :close-on-click-modal="false" :visible.sync="visible" class="NCC-dialog NCC-dialog_center" lock-scroll width="80%" append-to-body>
  2 + <el-dialog :title="dialogTitle" :close-on-click-modal="false" :visible.sync="visible" class="NCC-dialog NCC-dialog_center" lock-scroll width="80%" append-to-body>
3 3 <el-row :gutter="15" class="" >
4   - <el-form ref="elForm" :model="dataForm" size="small" label-width="100px" label-position="right" :disabled="!!isDetail" :rules="rules">
  4 + <el-form ref="elForm" :model="dataForm" size="small" label-width="100px" label-position="right" :disabled="formDisabled" :rules="rules">
5 5 <el-col :span="12">
6 6 <el-form-item label="单据编号" prop="id">
7 7 <el-input v-model="dataForm.id" placeholder="保存后自动生成" readonly :style='{"width":"100%"}' />
... ... @@ -245,7 +245,10 @@
245 245 </el-row>
246 246 <span slot="footer" class="dialog-footer">
247 247 <el-button @click="visible = false">取 消</el-button>
248   - <el-button type="primary" @click="dataFormSubmit()" v-if="!isDetail">确 定</el-button>
  248 + <template v-if="!formDisabled">
  249 + <el-button @click="saveDraft">保存草稿</el-button>
  250 + <el-button type="primary" @click="dataFormSubmit">提交审核</el-button>
  251 + </template>
249 252 </span>
250 253 <!-- 序列号选择弹窗 -->
251 254 <SerialNumberSelect ref="serialNumberSelect" @confirm="handleSerialNumberSelect" />
... ... @@ -305,6 +308,18 @@
305 308 totalJe() {
306 309 return (this.dataForm.wtXsckdMxList || []).reduce((sum, row) => sum + (parseFloat(row.je) || 0), 0)
307 310 },
  311 + isBillLockedForEdit() {
  312 + const z = this.dataForm && this.dataForm.djzt != null ? String(this.dataForm.djzt).trim() : ''
  313 + return z === '待审核' || z === '已审核' || z === '一级已审' || z === '待二级' || z === '一级已审/待二级'
  314 + },
  315 + formDisabled() {
  316 + return !!this.isDetail || this.isBillLockedForEdit
  317 + },
  318 + dialogTitle() {
  319 + if (this.isNew) return '新建'
  320 + if (this.isDetail || this.isBillLockedForEdit) return '详情'
  321 + return '编辑'
  322 + }
308 323 },
309 324 watch: {},
310 325 created() {
... ... @@ -448,9 +463,27 @@
448 463 this.dataForm.wtXsckdMxList = [];
449 464 }
450 465 this.dataForm.wtXsckdMxList.forEach(item => {
451   - if (!item.selectedSerialNumbers) {
452   - this.$set(item, 'selectedSerialNumbers', []);
  466 + // 兼容后端可能返回 selectedSerialNumbers / serialNumbers / serialNumber / xlh
  467 + const normalizeSnList = (val) => {
  468 + if (Array.isArray(val)) return val.map(s => String(s == null ? '' : s).trim()).filter(Boolean)
  469 + if (val == null || val === '') return []
  470 + const txt = String(val).trim()
  471 + if (!txt) return []
  472 + if ((txt.startsWith('[') && txt.endsWith(']')) || (txt.startsWith('{') && txt.endsWith('}'))) {
  473 + try {
  474 + const parsed = JSON.parse(txt)
  475 + if (Array.isArray(parsed)) return parsed.map(s => String(s == null ? '' : s).trim()).filter(Boolean)
  476 + } catch (e) {}
  477 + }
  478 + return txt.split(/[\n,,;;\s]+/).map(s => s.trim()).filter(Boolean)
453 479 }
  480 + const snList = normalizeSnList(
  481 + item.selectedSerialNumbers != null
  482 + ? item.selectedSerialNumbers
  483 + : (item.serialNumbers != null ? item.serialNumbers : (item.serialNumber != null ? item.serialNumber : item.xlh))
  484 + )
  485 + this.$set(item, 'selectedSerialNumbers', snList);
  486 + this.$set(item, 'serialNumbers', snList);
454 487 if (!item.hasOwnProperty('spxlhLoaded')) {
455 488 this.$set(item, 'spxlhLoaded', false);
456 489 }
... ... @@ -472,12 +505,26 @@
472 505 this.$set(item, 'thdj', item.thdj || item.dj || undefined);
473 506 }
474 507 });
  508 + // 编辑回填:补拉商品信息,确保序列号类型可显示
  509 + this.dataForm.wtXsckdMxList.forEach(item => {
  510 + if (item && item.spbh) {
  511 + this.getProductInfo(item.spbh).then(() => {
  512 + this.$set(item, 'spxlhLoaded', true);
  513 + this.$forceUpdate();
  514 + });
  515 + }
  516 + });
475 517 this.syncDetailWarehouses();
476 518 this.updateTotalAmount();
477 519 this.reloadInboundOrdersForExistingRows();
478 520 })
479 521 } else {
480 522 this.dataForm.wtXsckdMxList = [];
  523 + this.dataForm.djrq = Date.now();
  524 + const user = (this.$store && this.$store.getters && this.$store.getters.userInfo) ? this.$store.getters.userInfo : {};
  525 + this.dataForm.jsr = user.userId || user.id || '';
  526 + this.dataForm.djlx = '采购退货单';
  527 + this.dataForm.djzt = '草稿';
481 528 // 预生成单据编号
482 529 request({
483 530 url: '/api/Extend/WtXsckd/Actions/GenerateBillNo',
... ... @@ -491,7 +538,15 @@
491 538 }
492 539 })
493 540 },
  541 + saveDraft() {
  542 + if (this.formDisabled) return
  543 + this.submitCore(true)
  544 + },
494 545 dataFormSubmit() {
  546 + if (this.formDisabled) return
  547 + this.submitCore(false)
  548 + },
  549 + submitCore(isDraftMode) {
495 550 const mxCheck = validateMxNoEmptyProductRows(this.dataForm.wtXsckdMxList)
496 551 if (!mxCheck.valid) {
497 552 this.$message.warning(`第 ${mxCheck.emptyLineNos.join('、')} 行未选择商品,请先删除空白行后再提交`)
... ... @@ -574,14 +629,19 @@
574 629  
575 630 this.$refs['elForm'].validate((valid) => {
576 631 if (valid) {
  632 + const payload = {
  633 + ...this.dataForm,
  634 + isDraft: !!isDraftMode,
  635 + djzt: isDraftMode ? '草稿' : '待审核'
  636 + }
577 637 if (this.isNew) {
578 638 request({
579 639 url: `/api/Extend/WtXsckd`,
580 640 method: 'post',
581   - data: this.dataForm,
  641 + data: payload,
582 642 }).then((res) => {
583 643 this.$message({
584   - message: res.msg,
  644 + message: isDraftMode ? '草稿保存成功' : (res.msg || '保存成功,已提交审核'),
585 645 type: 'success',
586 646 duration: 1000,
587 647 onClose: () => {
... ... @@ -594,10 +654,10 @@
594 654 request({
595 655 url: '/api/Extend/WtXsckd/' + this.dataForm.id,
596 656 method: 'PUT',
597   - data: this.dataForm
  657 + data: payload
598 658 }).then((res) => {
599 659 this.$message({
600   - message: res.msg,
  660 + message: isDraftMode ? '草稿保存成功' : (res.msg || '保存成功,已提交审核'),
601 661 type: 'success',
602 662 duration: 1000,
603 663 onClose: () => {
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtCgthd/detail-view.vue
... ... @@ -251,9 +251,25 @@ export default {
251 251 },
252 252 methods: {
253 253 serialList(row) {
254   - const raw = row && row.selectedSerialNumbers
255   - if (!raw || !raw.length) return []
256   - return raw.map(s => (s == null ? '' : String(s)).trim()).filter(Boolean)
  254 + if (!row) return []
  255 + // 兼容多来源:selectedSerialNumbers / serialNumbers / serialNumber / xlh
  256 + const raw = row.selectedSerialNumbers != null
  257 + ? row.selectedSerialNumbers
  258 + : (row.serialNumbers != null ? row.serialNumbers : (row.serialNumber != null ? row.serialNumber : row.xlh))
  259 + if (Array.isArray(raw)) {
  260 + return raw.map(s => (s == null ? '' : String(s)).trim()).filter(Boolean)
  261 + }
  262 + if (raw == null || raw === '') return []
  263 + // 兼容后端可能返回 JSON 字符串或逗号/换行分隔字符串
  264 + const text = String(raw).trim()
  265 + if (!text) return []
  266 + if ((text.startsWith('[') && text.endsWith(']')) || (text.startsWith('{') && text.endsWith('}'))) {
  267 + try {
  268 + const arr = JSON.parse(text)
  269 + if (Array.isArray(arr)) return arr.map(s => (s == null ? '' : String(s)).trim()).filter(Boolean)
  270 + } catch (e) {}
  271 + }
  272 + return text.split(/[\n,,;;\s]+/).map(s => s.trim()).filter(Boolean)
257 273 },
258 274 serialPreview(row) {
259 275 const all = this.serialList(row)
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtCgthd/index.vue
... ... @@ -123,16 +123,25 @@
123 123 <el-table-column prop="gzr" label="过账人" align="left" />
124 124 <el-table-column prop="bz" label="备注" align="left" />
125 125 <el-table-column prop="djlx" label="单据类型" align="left" />
  126 + <el-table-column prop="djzt" label="审核状态" align="left" width="120">
  127 + <template slot-scope="scope">
  128 + <el-tag v-if="scope.row.djzt === '已审核'" type="success">已审核</el-tag>
  129 + <el-tag v-else-if="scope.row.djzt === '草稿'" type="info">草稿</el-tag>
  130 + <el-tag v-else type="warning">{{ scope.row.djzt || '待审核' }}</el-tag>
  131 + </template>
  132 + </el-table-column>
126 133 <el-table-column label="摘要" align="left" min-width="200" show-overflow-tooltip class-name="cell-nowrap">
127 134 <template slot-scope="scope">
128 135 <ncc-table-summary-cell :row="scope.row" fields="zy,Zy" />
129 136 </template>
130 137 </el-table-column>
131   - <el-table-column label="操作" fixed="right" width="140">
  138 + <el-table-column label="操作" fixed="right" width="260">
132 139 <template slot-scope="scope">
133 140 <el-button type="text" @click="openDetail(scope.row.id)" >查看</el-button>
134   - <el-button type="text" @click="addOrUpdateHandle(scope.row.id)" >编辑</el-button>
135   - <el-button type="text" @click="handleDel(scope.row.id)" class="NCC-table-delBtn" >删除</el-button>
  141 + <el-button v-if="isDraftRow(scope.row)" type="text" @click="addOrUpdateHandle(scope.row.id)">编辑</el-button>
  142 + <el-button v-if="isPendingAudit(scope.row)" type="text" style="color:#409EFF" @click="handleApprove(scope.row.id)">审核</el-button>
  143 + <el-button v-if="scope.row.djzt === '已审核'" type="text" style="color:#E6A23C" @click="handleReverse(scope.row.id)">反审</el-button>
  144 + <el-button v-if="isDraftRow(scope.row)" type="text" @click="handleDel(scope.row.id)" class="NCC-table-delBtn">删除</el-button>
136 145 </template>
137 146 </el-table-column>
138 147 </NCC-table>
... ... @@ -214,6 +223,51 @@
214 223 this.getgysOptions();
215 224 },
216 225 methods: {
  226 + isDraftRow(row) {
  227 + return row && String(row.djzt || '').trim() === '草稿'
  228 + },
  229 + isPendingAudit(row) {
  230 + const z = row && String(row.djzt || '').trim()
  231 + return z === '待审核' || z === ''
  232 + },
  233 + handleApprove(id) {
  234 + this.$confirm('确认审核该采购退货单?', '提示', { type: 'warning' }).then(() => {
  235 + request({
  236 + url: `/api/Extend/WtXsckd/ApproveGeneric/${id}`,
  237 + method: 'POST',
  238 + data: {}
  239 + }).then(res => {
  240 + const d = (res && res.data) || {}
  241 + const ok = d.success === true || String(res && res.code) === '200'
  242 + const msg = d.message || res.msg || (ok ? '审核成功' : '审核失败')
  243 + if (ok) {
  244 + this.$message.success(msg)
  245 + this.initData()
  246 + } else {
  247 + this.$message.error(msg)
  248 + }
  249 + })
  250 + }).catch(() => {})
  251 + },
  252 + handleReverse(id) {
  253 + this.$confirm('反审后单据将恢复草稿并可编辑,是否继续?', '反审确认', { type: 'warning' }).then(() => {
  254 + request({
  255 + url: `/api/Extend/WtXsckd/ReverseApproval/${id}`,
  256 + method: 'POST',
  257 + data: {}
  258 + }).then(res => {
  259 + const d = (res && res.data) || {}
  260 + const ok = d.success === true || String(res && res.code) === '200'
  261 + const msg = d.message || res.msg || (ok ? '反审成功' : '反审失败')
  262 + if (ok) {
  263 + this.$message.success(msg)
  264 + this.initData()
  265 + } else {
  266 + this.$message.error(msg)
  267 + }
  268 + })
  269 + }).catch(() => {})
  270 + },
217 271 getcjckOptions(){
218 272 previewDataInterface('681758216954053893').then(res => {
219 273 this.cjckOptions = res.data
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtCwdj_fkd/Form.vue
1 1 <template>
2   - <el-dialog :title="!dataForm.id ? '新建' : isDetail ? '详情':'编辑'" :close-on-click-modal="false" :visible.sync="visible" class="NCC-dialog NCC-dialog_center" lock-scroll width="90%">
  2 + <el-dialog :title="dialogTitle" :close-on-click-modal="false" :visible.sync="visible" class="NCC-dialog NCC-dialog_center" lock-scroll width="90%">
3 3 <el-row :gutter="15" class="" >
4   - <el-form ref="elForm" :model="dataForm" size="small" label-width="100px" label-position="right" :disabled="!!isDetail" :rules="rules">
  4 + <el-form ref="elForm" :model="dataForm" size="small" label-width="100px" label-position="right" :disabled="formDisabled" :rules="rules">
5 5 <el-col :span="8">
6 6 <el-form-item label="单据编号" prop="id">
7 7 <el-input v-model="dataForm.id" placeholder="请输入" clearable readonly :style='{"width":"100%"}' >
... ... @@ -23,8 +23,7 @@
23 23 </el-col>
24 24 <el-col :span="8">
25 25 <el-form-item label="经手人" prop="jsr">
26   - <user-select v-model="dataForm.jsr" placeholder="请选择" clearable >
27   - </user-select>
  26 + <el-input :value="jsrDisplayText" readonly placeholder="由明细所选账号带出" />
28 27 </el-form-item>
29 28 </el-col>
30 29 <el-col :span="16">
... ... @@ -33,6 +32,11 @@
33 32 </el-input>
34 33 </el-form-item>
35 34 </el-col>
  35 + <el-col :span="8" v-if="dataForm.id">
  36 + <el-form-item label="审核状态">
  37 + <el-input :value="auditStatusText" readonly />
  38 + </el-form-item>
  39 + </el-col>
36 40 <el-col :span="24" v-if="dataForm.id">
37 41 <el-form-item label="业务摘要">
38 42 <ncc-bill-summary :value="dataForm.billZy" mode="block" />
... ... @@ -45,7 +49,7 @@
45 49 <!-- 费用项目 / 固定资产:按需求取消 -->
46 50 <el-table-column prop="zhbh" label="账号">
47 51 <template slot-scope="scope">
48   - <el-select v-model="scope.row.zhbh" placeholder="请选择" clearable filterable>
  52 + <el-select v-model="scope.row.zhbh" placeholder="请选择" clearable filterable @change="syncFsjeFromMx">
49 53 <el-option v-for="(item, index) in zhbhOptions" :key="index" :label="item.fullName" :value="item.id" :disabled="item.disabled"></el-option>
50 54 </el-select>
51 55 </template>
... ... @@ -73,13 +77,13 @@
73 77 <el-input v-model="scope.row.djlx" placeholder="请输入" clearable ></el-input>
74 78 </template>
75 79 </el-table-column>
76   - <el-table-column label="操作" width="50">
  80 + <el-table-column v-if="!formDisabled" label="操作" width="50">
77 81 <template slot-scope="scope">
78 82 <el-button size="mini" type="text" class="NCC-table-delBtn" @click="handleDelWtCwdjmxEntityList(scope.$index)">删除</el-button>
79 83 </template>
80 84 </el-table-column>
81 85 </el-table>
82   - <div class="table-actions" @click="addHandleWtCwdjmxEntityList()">
  86 + <div v-if="!formDisabled" class="table-actions" @click="addHandleWtCwdjmxEntityList()">
83 87 <el-button type="text" icon="el-icon-plus">新增</el-button>
84 88 </div>
85 89 </el-form-item>
... ... @@ -113,7 +117,10 @@
113 117 </el-row>
114 118 <span slot="footer" class="dialog-footer">
115 119 <el-button @click="visible = false">取 消</el-button>
116   - <el-button type="primary" @click="dataFormSubmit()" v-if="!isDetail">确 定</el-button>
  120 + <template v-if="!formDisabled">
  121 + <el-button @click="submitAsDraft">保存草稿</el-button>
  122 + <el-button type="primary" @click="submitForAudit">提 交</el-button>
  123 + </template>
117 124 </span>
118 125 </el-dialog>
119 126 </template>
... ... @@ -130,9 +137,9 @@
130 137 loading: false,
131 138 visible: false,
132 139 isDetail: false,
  140 + pendingDjzt: '待审核',
133 141 dataForm: {
134   - id:'',
135   - id:undefined,
  142 + id: undefined,
136 143 ldrq:undefined,
137 144 wldw:undefined,
138 145 jsr:undefined,
... ... @@ -143,6 +150,7 @@
143 150 skzh:undefined,
144 151 fsje:undefined,
145 152 djlx:undefined,
  153 + djzt: undefined,
146 154 },
147 155 rules: {
148 156 wldw: [{ required: true, message: '请选择往来单位', trigger: 'change' }],
... ... @@ -159,6 +167,39 @@
159 167 const rows = Array.isArray(this.dataForm.wtCwdjmxList) ? this.dataForm.wtCwdjmxList : []
160 168 return rows.reduce((s, r) => s + (Number(r.ybje) || 0), 0)
161 169 },
  170 + auditStatusText() {
  171 + const z = this.dataForm.djzt != null && this.dataForm.djzt !== '' ? String(this.dataForm.djzt).trim() : ''
  172 + if (z) return z
  173 + return '—'
  174 + },
  175 + isBillLockedForEdit() {
  176 + const z = this.auditStatusText
  177 + return z === '待审核' || z === '已审核'
  178 + },
  179 + formDisabled() {
  180 + return !!this.isDetail || this.isBillLockedForEdit
  181 + },
  182 + dialogTitle() {
  183 + if (!this.dataForm.id) return '新建'
  184 + if (this.isDetail) return '详情'
  185 + const z = this.auditStatusText
  186 + if (z === '已审核') return '查看(已审核)'
  187 + if (z === '待审核') return '查看(待审核)'
  188 + if (z === '草稿') return '编辑'
  189 + return '编辑'
  190 + },
  191 + jsrDisplayText() {
  192 + const rows = Array.isArray(this.dataForm.wtCwdjmxList) ? this.dataForm.wtCwdjmxList : []
  193 + for (let i = 0; i < rows.length; i++) {
  194 + const r = rows[i]
  195 + if (r && r.zhbh) {
  196 + const opt = (this.zhbhOptions || []).find(o => o && o.id === r.zhbh)
  197 + if (opt && opt.fullName) return opt.fullName
  198 + }
  199 + }
  200 + const j = this.dataForm.jsr
  201 + return (j != null && j !== '') ? String(j) : '—'
  202 + },
162 203 },
163 204 watch: {},
164 205 created() {
... ... @@ -215,6 +256,7 @@
215 256 this.dataForm.id = id || 0;
216 257 this.visible = true;
217 258 this.isDetail = isDetail || false;
  259 + this.pendingDjzt = '待审核'
218 260 this.$nextTick(() => {
219 261 this.$refs['elForm'].resetFields();
220 262 if (this.dataForm.id) {
... ... @@ -227,18 +269,28 @@
227 269 } else {
228 270 // 新建时设置默认值
229 271 this.dataForm.ldrq = Date.now(); // 录单日期默认为当前日期
230   - var user = (this.$store && this.$store.getters && this.$store.getters.userInfo) ? this.$store.getters.userInfo : {};
231   - this.dataForm.jsr = user.userId || user.id || ''; // 经手人默认为当前用户
232 272 this.dataForm.djlx = '付款单'; // 主表单据类型默认值
  273 + this.dataForm.djzt = '草稿'
233 274 this.dataForm.wtCwdjmxList = []
234 275 this.addHandleWtCwdjmxEntityList()
235 276 }
236 277 })
237 278 },
  279 + submitAsDraft() {
  280 + this.pendingDjzt = '草稿'
  281 + this.dataFormSubmit()
  282 + },
  283 + submitForAudit() {
  284 + this.pendingDjzt = '待审核'
  285 + this.dataFormSubmit()
  286 + },
238 287 dataFormSubmit() {
  288 + if (this.formDisabled) return
239 289 this.syncFsjeFromMx()
240 290 this.$refs['elForm'].validate((valid) => {
241 291 if (valid) {
  292 + this.dataForm.djlx = '付款单';
  293 + this.$set(this.dataForm, 'djzt', this.pendingDjzt || '待审核')
242 294 if (!this.dataForm.id) {
243 295 request({
244 296 url: `/api/Extend/WtCwdj`,
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtCwdj_fkd/index.vue
... ... @@ -24,7 +24,7 @@
24 24 <template v-if="showAll">
25 25 <el-col :span="6">
26 26 <el-form-item label="经手人">
27   - <userSelect v-model="query.jsr" placeholder="请选择经手人" />
  27 + <el-input v-model="query.jsr" placeholder="账户名称等" clearable />
28 28 </el-form-item>
29 29 </el-col>
30 30 <el-col :span="6">
... ... @@ -68,16 +68,25 @@
68 68 <template slot-scope="scope">{{ scope.row.wldw | dynamicText(wldwOptions) }}</template>
69 69 </el-table-column>
70 70 <el-table-column prop="jsr" label="经手人" align="left" />
  71 + <el-table-column prop="djzt" label="审核状态" align="left" width="110">
  72 + <template slot-scope="scope">
  73 + <el-tag v-if="scope.row.djzt === '已审核'" type="success">已审核</el-tag>
  74 + <el-tag v-else-if="scope.row.djzt === '草稿'" type="info">草稿</el-tag>
  75 + <el-tag v-else type="warning">{{ scope.row.djzt || '待审核' }}</el-tag>
  76 + </template>
  77 + </el-table-column>
71 78 <el-table-column label="摘要" align="left" min-width="200">
72 79 <template slot-scope="scope">
73 80 <ncc-table-summary-cell :row="scope.row" fields="billZy,BillZy,zy,Zy" />
74 81 </template>
75 82 </el-table-column>
76   - <el-table-column label="操作" fixed="right" width="140">
  83 + <el-table-column label="操作" fixed="right" width="240">
77 84 <template slot-scope="scope">
78 85 <el-button type="text" @click="openDetail(scope.row.id)" >查看</el-button>
79   - <el-button type="text" @click="addOrUpdateHandle(scope.row.id)" >编辑</el-button>
80   - <el-button type="text" @click="handleDel(scope.row.id)" class="NCC-table-delBtn" >删除</el-button>
  86 + <el-button type="text" v-if="isDraftRow(scope.row)" @click="addOrUpdateHandle(scope.row.id)" >编辑</el-button>
  87 + <el-button type="text" v-if="isPendingAudit(scope.row)" @click="handleApprove(scope.row.id)" style="color:#409EFF">审核</el-button>
  88 + <el-button type="text" v-if="scope.row.djzt === '已审核'" @click="handleReverse(scope.row.id)" style="color:#E6A23C">反审</el-button>
  89 + <el-button type="text" v-if="isDraftRow(scope.row)" @click="handleDel(scope.row.id)" class="NCC-table-delBtn" >删除</el-button>
81 90 </template>
82 91 </el-table-column>
83 92 </NCC-table>
... ... @@ -140,6 +149,48 @@
140 149 this.getskzhOptions();
141 150 },
142 151 methods: {
  152 + isDraftRow(row) {
  153 + return row && String(row.djzt || '').trim() === '草稿'
  154 + },
  155 + isPendingAudit(row) {
  156 + const z = row && String(row.djzt || '').trim()
  157 + return z === '待审核' || z === ''
  158 + },
  159 + handleApprove(id) {
  160 + this.$confirm('确认审核该付款单?', '提示', { type: 'warning' }).then(() => {
  161 + request({
  162 + url: `/api/Extend/WtCwdj/Actions/ApprovePayment/${id}`,
  163 + method: 'POST',
  164 + data: {}
  165 + }).then(res => {
  166 + this.$message({ type: 'success', message: res.msg || '审核成功', duration: 1200 })
  167 + this.initData()
  168 + })
  169 + }).catch(() => {})
  170 + },
  171 + handleReverse(id) {
  172 + if (!id) return
  173 + this.$confirm('反审后单据将恢复为草稿,可再次编辑;摘要汇总表中对应记录将移除。是否继续?', '反审确认', { type: 'warning' })
  174 + .then(() => {
  175 + return request({
  176 + url: `/api/Extend/WtCwdj/Actions/ReversePaymentAudit/${id}`,
  177 + method: 'POST',
  178 + data: {}
  179 + })
  180 + })
  181 + .then((res) => {
  182 + const d = (res && res.data) || {}
  183 + const ok = d.success === true || String(res && res.code) === '200'
  184 + const msg = d.message || res.msg || (ok ? '反审成功' : '操作失败')
  185 + if (ok) {
  186 + this.$message.success(msg)
  187 + this.initData()
  188 + } else {
  189 + this.$message.error(msg)
  190 + }
  191 + })
  192 + .catch(() => {})
  193 + },
143 194 getwldwOptions(){
144 195 previewDataInterface('716168694526379269').then(res => {
145 196 this.wldwOptions = res.data
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtCwdj_fyd/Form.vue
... ... @@ -51,11 +51,9 @@
51 51 <template slot-scope="scope">
52 52 <el-select
53 53 v-model="scope.row.fyxmbh"
54   - placeholder="请选择或输入"
  54 + placeholder="请选择费用项目"
55 55 clearable
56   - filterable
57   - allow-create
58   - default-first-option>
  56 + filterable>
59 57 <el-option v-for="(item, index) in fyxmbhOptions" :key="index" :label="item.fullName" :value="item.id" :disabled="item.disabled"></el-option>
60 58 </el-select>
61 59 </template>
... ... @@ -143,6 +141,10 @@
143 141 import { getDictionaryDataSelector } from '@/api/systemData/dictionary'
144 142 import { previewDataInterface } from '@/api/systemData/dataInterface'
145 143 import { getAccountSelector } from '@/api/extend/wtAccount'
  144 +
  145 + /** 数据字典「费用项目」字典类型 id(与 /api/system/DictionaryData/{id} 一致) */
  146 + const FYXM_DICT_ID = '816913803978474757'
  147 +
146 148 export default {
147 149 components: {},
148 150 props: [],
... ... @@ -223,8 +225,8 @@
223 225 });
224 226 },
225 227 getfyxmbhOptions(){
226   - getDictionaryDataSelector('715562947862070533').then(res => {
227   - this.fyxmbhOptions = res.data.list
  228 + getDictionaryDataSelector(FYXM_DICT_ID).then(res => {
  229 + this.fyxmbhOptions = (res.data && res.data.list) ? res.data.list : []
228 230 });
229 231 },
230 232 getgdzcbhOptions(){
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtCwdj_fyd/detail-view.vue
... ... @@ -145,6 +145,9 @@ import { getDictionaryDataSelector } from &#39;@/api/systemData/dictionary&#39;
145 145 import { dynamicText } from '@/filters'
146 146 import { getAccountSelector } from '@/api/extend/wtAccount'
147 147  
  148 +/** 数据字典「费用项目」字典类型 id */
  149 +const FYXM_DICT_ID = '816913803978474757'
  150 +
148 151 export default {
149 152 name: 'WtCwdjFydDetailView',
150 153 data() {
... ... @@ -242,8 +245,8 @@ export default {
242 245 getAccountSelector().then(res => {
243 246 this.zhbhOptions = res.data.list || []
244 247 })
245   - getDictionaryDataSelector('715562947862070533').then(res => {
246   - this.fyxmbhOptions = res.data.list || []
  248 + getDictionaryDataSelector(FYXM_DICT_ID).then(res => {
  249 + this.fyxmbhOptions = (res.data && res.data.list) ? res.data.list : []
247 250 })
248 251 }
249 252 }
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtCwdj_skd/Form.vue
1 1 <template>
2   - <el-dialog :title="!dataForm.id ? '新建' : isDetail ? '详情':'编辑'" :close-on-click-modal="false" :visible.sync="visible" class="NCC-dialog NCC-dialog_center" lock-scroll width="90%">
  2 + <el-dialog :title="dialogTitle" :close-on-click-modal="false" :visible.sync="visible" class="NCC-dialog NCC-dialog_center" lock-scroll width="90%">
3 3 <el-row :gutter="15" class="" >
4   - <el-form ref="elForm" :model="dataForm" size="small" label-width="100px" label-position="right" :disabled="!!isDetail" :rules="rules">
  4 + <el-form ref="elForm" :model="dataForm" size="small" label-width="100px" label-position="right" :disabled="formDisabled" :rules="rules">
5 5 <el-col :span="8">
6 6 <el-form-item label="单据编号" prop="id">
7 7 <el-input v-model="dataForm.id" placeholder="请输入" clearable readonly :style='{"width":"100%"}' >
... ... @@ -23,8 +23,7 @@
23 23 </el-col>
24 24 <el-col :span="8">
25 25 <el-form-item label="经手人" prop="jsr">
26   - <user-select v-model="dataForm.jsr" placeholder="请选择" clearable >
27   - </user-select>
  26 + <el-input :value="jsrDisplayText" readonly placeholder="由明细所选账号带出" />
28 27 </el-form-item>
29 28 </el-col>
30 29 <el-col :span="16">
... ... @@ -33,6 +32,11 @@
33 32 </el-input>
34 33 </el-form-item>
35 34 </el-col>
  35 + <el-col :span="8" v-if="dataForm.id">
  36 + <el-form-item label="审核状态">
  37 + <el-input :value="auditStatusText" readonly />
  38 + </el-form-item>
  39 + </el-col>
36 40 <el-col :span="24" v-if="dataForm.id">
37 41 <el-form-item label="业务摘要">
38 42 <ncc-bill-summary :value="dataForm.billZy" mode="block" />
... ... @@ -44,7 +48,7 @@
44 48 <el-table-column type="index" width="50" label="序号" align="center" />
45 49 <el-table-column prop="zhbh" label="账号">
46 50 <template slot-scope="scope">
47   - <el-select v-model="scope.row.zhbh" placeholder="请选择" clearable filterable>
  51 + <el-select v-model="scope.row.zhbh" placeholder="请选择" clearable filterable @change="syncFsjeFromMx">
48 52 <el-option v-for="(item, index) in zhbhOptions" :key="index" :label="item.fullName" :value="item.id" :disabled="item.disabled"></el-option>
49 53 </el-select>
50 54 </template>
... ... @@ -72,13 +76,13 @@
72 76 <el-input v-model="scope.row.djlx" placeholder="请输入" clearable ></el-input>
73 77 </template>
74 78 </el-table-column>
75   - <el-table-column label="操作" width="50">
  79 + <el-table-column v-if="!formDisabled" label="操作" width="50">
76 80 <template slot-scope="scope">
77 81 <el-button size="mini" type="text" class="NCC-table-delBtn" @click="handleDelWtCwdjmxEntityList(scope.$index)">删除</el-button>
78 82 </template>
79 83 </el-table-column>
80 84 </el-table>
81   - <div class="table-actions" @click="addHandleWtCwdjmxEntityList()">
  85 + <div v-if="!formDisabled" class="table-actions" @click="addHandleWtCwdjmxEntityList()">
82 86 <el-button type="text" icon="el-icon-plus">新增</el-button>
83 87 </div>
84 88 </el-form-item>
... ... @@ -112,7 +116,10 @@
112 116 </el-row>
113 117 <span slot="footer" class="dialog-footer">
114 118 <el-button @click="visible = false">取 消</el-button>
115   - <el-button type="primary" @click="dataFormSubmit()" v-if="!isDetail">确 定</el-button>
  119 + <template v-if="!formDisabled">
  120 + <el-button @click="submitAsDraft">保存草稿</el-button>
  121 + <el-button type="primary" @click="submitForAudit">提 交</el-button>
  122 + </template>
116 123 </span>
117 124 </el-dialog>
118 125 </template>
... ... @@ -129,9 +136,9 @@
129 136 loading: false,
130 137 visible: false,
131 138 isDetail: false,
  139 + pendingDjzt: '待审核',
132 140 dataForm: {
133   - id:'',
134   - id:undefined,
  141 + id: undefined,
135 142 ldrq:undefined,
136 143 wldw:undefined,
137 144 jsr:undefined,
... ... @@ -142,6 +149,7 @@
142 149 skzh:undefined,
143 150 fsje:undefined,
144 151 djlx:undefined,
  152 + djzt: undefined,
145 153 },
146 154 rules: {
147 155 wldw: [{ required: true, message: '请选择往来单位', trigger: 'change' }],
... ... @@ -158,6 +166,39 @@
158 166 const rows = Array.isArray(this.dataForm.wtCwdjmxList) ? this.dataForm.wtCwdjmxList : []
159 167 return rows.reduce((s, r) => s + (Number(r.ybje) || 0), 0)
160 168 },
  169 + auditStatusText() {
  170 + const z = this.dataForm.djzt != null && this.dataForm.djzt !== '' ? String(this.dataForm.djzt).trim() : ''
  171 + if (z) return z
  172 + return '—'
  173 + },
  174 + isBillLockedForEdit() {
  175 + const z = this.auditStatusText
  176 + return z === '待审核' || z === '已审核'
  177 + },
  178 + formDisabled() {
  179 + return !!this.isDetail || this.isBillLockedForEdit
  180 + },
  181 + dialogTitle() {
  182 + if (!this.dataForm.id) return '新建'
  183 + if (this.isDetail) return '详情'
  184 + const z = this.auditStatusText
  185 + if (z === '已审核') return '查看(已审核)'
  186 + if (z === '待审核') return '查看(待审核)'
  187 + if (z === '草稿') return '编辑'
  188 + return '编辑'
  189 + },
  190 + jsrDisplayText() {
  191 + const rows = Array.isArray(this.dataForm.wtCwdjmxList) ? this.dataForm.wtCwdjmxList : []
  192 + for (let i = 0; i < rows.length; i++) {
  193 + const r = rows[i]
  194 + if (r && r.zhbh) {
  195 + const opt = (this.zhbhOptions || []).find(o => o && o.id === r.zhbh)
  196 + if (opt && opt.fullName) return opt.fullName
  197 + }
  198 + }
  199 + const j = this.dataForm.jsr
  200 + return (j != null && j !== '') ? String(j) : '—'
  201 + },
161 202 },
162 203 watch: {},
163 204 created() {
... ... @@ -215,6 +256,7 @@
215 256 this.dataForm.id = id || 0;
216 257 this.visible = true;
217 258 this.isDetail = isDetail || false;
  259 + this.pendingDjzt = '待审核'
218 260 this.$nextTick(() => {
219 261 this.$refs['elForm'].resetFields();
220 262 if (this.dataForm.id) {
... ... @@ -227,18 +269,28 @@
227 269 } else {
228 270 // 新建时设置默认值
229 271 this.dataForm.ldrq = Date.now(); // 录单日期默认为当前日期
230   - var user = (this.$store && this.$store.getters && this.$store.getters.userInfo) ? this.$store.getters.userInfo : {};
231   - this.dataForm.jsr = user.userId || user.id || ''; // 经手人默认为当前用户
232 272 this.dataForm.djlx = '收款单'; // 主表单据类型默认值
  273 + this.dataForm.djzt = '草稿'
233 274 this.dataForm.wtCwdjmxList = []
234 275 this.addHandleWtCwdjmxEntityList()
235 276 }
236 277 })
237 278 },
  279 + submitAsDraft() {
  280 + this.pendingDjzt = '草稿'
  281 + this.dataFormSubmit()
  282 + },
  283 + submitForAudit() {
  284 + this.pendingDjzt = '待审核'
  285 + this.dataFormSubmit()
  286 + },
238 287 dataFormSubmit() {
  288 + if (this.formDisabled) return
239 289 this.syncFsjeFromMx()
240 290 this.$refs['elForm'].validate((valid) => {
241 291 if (valid) {
  292 + this.dataForm.djlx = '收款单';
  293 + this.$set(this.dataForm, 'djzt', this.pendingDjzt || '待审核')
242 294 if (!this.dataForm.id) {
243 295 request({
244 296 url: `/api/Extend/WtCwdj`,
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtCwdj_skd/index.vue
... ... @@ -24,7 +24,7 @@
24 24 <template v-if="showAll">
25 25 <el-col :span="6">
26 26 <el-form-item label="经手人">
27   - <userSelect v-model="query.jsr" placeholder="请选择经手人" />
  27 + <el-input v-model="query.jsr" placeholder="账户名称等" clearable />
28 28 </el-form-item>
29 29 </el-col>
30 30 <el-col :span="6">
... ... @@ -71,6 +71,7 @@
71 71 <el-table-column prop="djzt" label="审核状态" align="left" width="110">
72 72 <template slot-scope="scope">
73 73 <el-tag v-if="scope.row.djzt === '已审核'" type="success">已审核</el-tag>
  74 + <el-tag v-else-if="scope.row.djzt === '草稿'" type="info">草稿</el-tag>
74 75 <el-tag v-else type="warning">{{ scope.row.djzt || '待审核' }}</el-tag>
75 76 </template>
76 77 </el-table-column>
... ... @@ -79,12 +80,13 @@
79 80 <ncc-table-summary-cell :row="scope.row" fields="billZy,BillZy,zy,Zy" />
80 81 </template>
81 82 </el-table-column>
82   - <el-table-column label="操作" fixed="right" width="200">
  83 + <el-table-column label="操作" fixed="right" width="240">
83 84 <template slot-scope="scope">
84 85 <el-button type="text" @click="openDetail(scope.row.id)" >查看</el-button>
85   - <el-button type="text" @click="addOrUpdateHandle(scope.row.id)" >编辑</el-button>
86   - <el-button type="text" v-if="scope.row.djzt !== '已审核'" @click="handleApprove(scope.row.id)" style="color:#409EFF">审核</el-button>
87   - <el-button type="text" @click="handleDel(scope.row.id)" class="NCC-table-delBtn" >删除</el-button>
  86 + <el-button type="text" v-if="isDraftRow(scope.row)" @click="addOrUpdateHandle(scope.row.id)" >编辑</el-button>
  87 + <el-button type="text" v-if="isPendingAudit(scope.row)" @click="handleApprove(scope.row.id)" style="color:#409EFF">审核</el-button>
  88 + <el-button type="text" v-if="scope.row.djzt === '已审核'" @click="handleReverse(scope.row.id)" style="color:#E6A23C">反审</el-button>
  89 + <el-button type="text" v-if="isDraftRow(scope.row)" @click="handleDel(scope.row.id)" class="NCC-table-delBtn" >删除</el-button>
88 90 </template>
89 91 </el-table-column>
90 92 </NCC-table>
... ... @@ -147,6 +149,13 @@
147 149 this.getskzhOptions();
148 150 },
149 151 methods: {
  152 + isDraftRow(row) {
  153 + return row && String(row.djzt || '').trim() === '草稿'
  154 + },
  155 + isPendingAudit(row) {
  156 + const z = row && String(row.djzt || '').trim()
  157 + return z === '待审核' || z === ''
  158 + },
150 159 handleApprove(id) {
151 160 this.$confirm('确认审核该收款单?', '提示', { type: 'warning' }).then(() => {
152 161 request({
... ... @@ -159,6 +168,29 @@
159 168 })
160 169 }).catch(() => {})
161 170 },
  171 + handleReverse(id) {
  172 + if (!id) return
  173 + this.$confirm('反审后单据将恢复为草稿,可再次编辑;摘要汇总表中对应记录将移除。是否继续?', '反审确认', { type: 'warning' })
  174 + .then(() => {
  175 + return request({
  176 + url: `/api/Extend/WtCwdj/Actions/ReverseReceiptAudit/${id}`,
  177 + method: 'POST',
  178 + data: {}
  179 + })
  180 + })
  181 + .then((res) => {
  182 + const d = (res && res.data) || {}
  183 + const ok = d.success === true || String(res && res.code) === '200'
  184 + const msg = d.message || res.msg || (ok ? '反审成功' : '操作失败')
  185 + if (ok) {
  186 + this.$message.success(msg)
  187 + this.initData()
  188 + } else {
  189 + this.$message.error(msg)
  190 + }
  191 + })
  192 + .catch(() => {})
  193 + },
162 194 getwldwOptions(){
163 195 previewDataInterface('716168694526379269').then(res => {
164 196 this.wldwOptions = res.data
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtFpspdzb/Form.vue
... ... @@ -38,8 +38,32 @@
38 38 </el-col>
39 39 <el-col :span="24">
40 40 <el-form-item label="项目名称" prop="xm">
41   - <el-input v-model="dataForm.xm" placeholder="请输入" clearable :style='{"width":"100%"}' >
42   - </el-input>
  41 + <div
  42 + v-for="(item, idx) in xmRows"
  43 + :key="idx"
  44 + style="display:flex;align-items:center;margin-bottom:8px;"
  45 + >
  46 + <el-input
  47 + v-model="xmRows[idx]"
  48 + placeholder="请输入项目名称"
  49 + clearable
  50 + :style='{"width":"100%"}'
  51 + />
  52 + <el-button
  53 + v-if="!isDetail"
  54 + type="text"
  55 + icon="el-icon-plus"
  56 + style="margin-left:8px;"
  57 + @click="addXmRow"
  58 + />
  59 + <el-button
  60 + v-if="!isDetail && xmRows.length > 1"
  61 + type="text"
  62 + icon="el-icon-delete"
  63 + class="NCC-table-delBtn"
  64 + @click="removeXmRow(idx)"
  65 + />
  66 + </div>
43 67 </el-form-item>
44 68 </el-col>
45 69 <el-col :span="24">
... ... @@ -50,7 +74,8 @@
50 74 </el-col>
51 75 <el-col :span="24">
52 76 <el-form-item label="税率" prop="sl">
53   - <el-input v-model="dataForm.sl" placeholder="请输入" clearable :style='{"width":"100%"}' >
  77 + <el-input v-model="dataForm.sl" placeholder="请输入数字,如 13" clearable :style='{"width":"100%"}' >
  78 + <template slot="append">%</template>
54 79 </el-input>
55 80 </el-form-item>
56 81 </el-col>
... ... @@ -84,6 +109,7 @@
84 109 },
85 110 rules: {
86 111 },
  112 + xmRows: [''],
87 113 spLoading: false,
88 114 spOptions: [],
89 115 }
... ... @@ -111,6 +137,9 @@
111 137 method: 'get'
112 138 }).then(res =>{
113 139 this.dataForm = res.data;
  140 + this.$set(this.dataForm, 'sl', this.normalizeTaxRateForDisplay(this.dataForm.sl))
  141 + const parsedXm = this.parseXmList(this.dataForm.xm)
  142 + this.xmRows = parsedXm.length ? parsedXm : ['']
114 143 if (this.dataForm && this.dataForm.sp) {
115 144 this.spOptions = [{
116 145 id: this.dataForm.sp,
... ... @@ -120,8 +149,36 @@
120 149 }
121 150 })
122 151 }
  152 + if (!this.dataForm.id) {
  153 + this.xmRows = ['']
  154 + }
123 155 })
124 156 },
  157 + addXmRow() {
  158 + this.xmRows.push('')
  159 + },
  160 + removeXmRow(index) {
  161 + this.xmRows.splice(index, 1)
  162 + if (!this.xmRows.length) this.xmRows = ['']
  163 + },
  164 + parseXmList(raw) {
  165 + if (raw == null) return []
  166 + const txt = String(raw).trim()
  167 + if (!txt) return []
  168 + return txt.split(/[,,;;\n]+/).map(s => s.trim()).filter(Boolean)
  169 + },
  170 + normalizeTaxRateForDisplay(raw) {
  171 + if (raw == null || raw === '') return ''
  172 + return String(raw).replace(/%/g, '').trim()
  173 + },
  174 + buildSubmitData() {
  175 + const submit = { ...this.dataForm }
  176 + const xm = (this.xmRows || []).map(s => String(s || '').trim()).filter(Boolean)
  177 + submit.xm = xm.join('\n')
  178 + const slRaw = this.normalizeTaxRateForDisplay(submit.sl)
  179 + submit.sl = slRaw === '' ? null : Number(slRaw)
  180 + return submit
  181 + },
125 182 async handleSpSearch(query) {
126 183 const q = query != null ? String(query).trim() : ''
127 184 if (!q) { this.spOptions = []; return }
... ... @@ -153,11 +210,16 @@
153 210 dataFormSubmit() {
154 211 this.$refs['elForm'].validate((valid) => {
155 212 if (valid) {
  213 + const submitData = this.buildSubmitData()
  214 + if (submitData.sl == null || Number.isNaN(submitData.sl)) {
  215 + this.$message.warning('税率请输入数字,如 13')
  216 + return
  217 + }
156 218 if (!this.dataForm.id) {
157 219 request({
158 220 url: `/api/Extend/WtFpspdzb`,
159 221 method: 'post',
160   - data: this.dataForm,
  222 + data: submitData,
161 223 }).then((res) => {
162 224 this.$message({
163 225 message: res.msg,
... ... @@ -173,7 +235,7 @@
173 235 request({
174 236 url: '/api/Extend/WtFpspdzb/' + this.dataForm.id,
175 237 method: 'PUT',
176   - data: this.dataForm
  238 + data: submitData
177 239 }).then((res) => {
178 240 this.$message({
179 241 message: res.msg,
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtFpspdzb/index.vue
... ... @@ -26,7 +26,7 @@
26 26 </el-col>
27 27 <el-col :span="6">
28 28 <el-form-item label="税率">
29   - <el-input v-model="query.sl" placeholder="税率" clearable />
  29 + <el-input v-model="query.sl" placeholder="税率,如 13" clearable />
30 30 </el-form-item>
31 31 </el-col>
32 32 </template>
... ... @@ -57,9 +57,17 @@
57 57 <NCC-table v-loading="listLoading" :data="list" has-c @selection-change="handleSelectionChange">
58 58 <el-table-column prop="sp" label="商品名称" align="left" />
59 59 <el-table-column prop="spbh" label="商品编号" align="left" />
60   - <el-table-column prop="xm" label="项目名称" align="left" />
  60 + <el-table-column prop="xm" label="项目名称" align="left" min-width="180">
  61 + <template slot-scope="scope">
  62 + <div class="fpspdzb-xm-multiline">{{ formatXmCell(scope.row.xm) }}</div>
  63 + </template>
  64 + </el-table-column>
61 65 <el-table-column prop="gg" label="规格型号" align="left" />
62   - <el-table-column prop="sl" label="税率" align="left" />
  66 + <el-table-column prop="sl" label="税率" align="left">
  67 + <template slot-scope="scope">
  68 + {{ formatTaxRate(scope.row.sl) }}
  69 + </template>
  70 + </el-table-column>
63 71 <el-table-column label="操作" fixed="right" width="100">
64 72 <template slot-scope="scope">
65 73 <el-button type="text" @click="addOrUpdateHandle(scope.row.id)" >编辑</el-button>
... ... @@ -117,6 +125,16 @@
117 125 this.initData()
118 126 },
119 127 methods: {
  128 + formatTaxRate(v) {
  129 + if (v === undefined || v === null || v === '') return ''
  130 + const txt = String(v).replace(/%/g, '').trim()
  131 + if (!txt) return ''
  132 + return `${txt}%`
  133 + },
  134 + formatXmCell(v) {
  135 + if (v == null || v === '') return ''
  136 + return String(v).replace(/[,,;;]+/g, '\n')
  137 + },
120 138 initData() {
121 139 this.listLoading = true;
122 140 let _query = {
... ... @@ -131,6 +149,9 @@
131 149 query[key] = _query[key]
132 150 }
133 151 }
  152 + if (query.sl != null && query.sl !== '') {
  153 + query.sl = String(query.sl).replace(/%/g, '').trim()
  154 + }
134 155 request({
135 156 url: `/api/Extend/WtFpspdzb`,
136 157 method: 'GET',
... ... @@ -244,4 +265,10 @@
244 265 }
245 266 }
246 267 }
247   -</script>
248 268 \ No newline at end of file
  269 +</script>
  270 +<style scoped>
  271 +.fpspdzb-xm-multiline {
  272 + white-space: pre-line;
  273 + line-height: 1.5;
  274 +}
  275 +</style>
249 276 \ No newline at end of file
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtFysrd_qtsr/Form.vue
... ... @@ -4,7 +4,7 @@
4 4 <el-form ref="elForm" :model="dataForm" size="small" label-width="100px" label-position="right" :disabled="!!isDetail" :rules="rules">
5 5 <el-col :span="8">
6 6 <el-form-item label="单据编号" prop="id">
7   - <el-input v-model="dataForm.id" placeholder="请输入" clearable :style='{"width":"100%"}' >
  7 + <el-input v-model="dataForm.id" placeholder="系统自动生成" clearable readonly :style='{"width":"100%"}' >
8 8 </el-input>
9 9 </el-form-item>
10 10 </el-col>
... ... @@ -40,7 +40,7 @@
40 40 </el-form-item>
41 41 </el-col>
42 42 <el-col :span="8">
43   - <el-form-item label="结算账户" prop="jszh">
  43 + <el-form-item label="收款账户" prop="jszh">
44 44 <el-select v-model="dataForm.jszh" placeholder="请选择" clearable :style='{"width":"100%"}' >
45 45 <el-option v-for="(item, index) in jszhOptions" :key="index" :label="item.fullName" :value="item.id" ></el-option>
46 46 </el-select>
... ... @@ -132,6 +132,7 @@
132 132 import request from '@/utils/request'
133 133 import { getDictionaryDataSelector } from '@/api/systemData/dictionary'
134 134 import { previewDataInterface } from '@/api/systemData/dataInterface'
  135 + import { getAccountSelector } from '@/api/extend/wtAccount'
135 136 export default {
136 137 components: {},
137 138 props: [],
... ... @@ -141,7 +142,6 @@
141 142 visible: false,
142 143 isDetail: false,
143 144 dataForm: {
144   - id:'',
145 145 id:undefined,
146 146 djrq:undefined,
147 147 fzjg:undefined,
... ... @@ -156,12 +156,12 @@
156 156 gzr:undefined,
157 157 bz:undefined,
158 158 zy:undefined,
159   - djlx:'其它收入',
  159 + djlx:'其他收入单',
160 160 },
161 161 rules: {
162 162 },
163 163 fkdwOptions : [],
164   - jszhOptions:[{"fullName":"选项一","id":"1"},{"fullName":"选项二","id":"2"}],
  164 + jszhOptions:[],
165 165 kmOptions:[{"fullName":"选项一","id":"1"},{"fullName":"选项二","id":"2"}],
166 166 }
167 167 },
... ... @@ -169,6 +169,7 @@
169 169 watch: {},
170 170 created() {
171 171 this.getfkdwOptions();
  172 + this.getjszhOptions();
172 173 },
173 174 mounted() {
174 175 },
... ... @@ -178,15 +179,23 @@
178 179 this.fkdwOptions = res.data
179 180 });
180 181 },
  182 + getjszhOptions(){
  183 + getAccountSelector().then(res => {
  184 + this.jszhOptions = (res.data && res.data.list) ? res.data.list : (res.data || [])
  185 + });
  186 + },
181 187 goBack() {
182 188 this.$emit('refresh')
183 189 },
184 190 init(id, isDetail) {
185   - this.dataForm.id = id || 0;
  191 + this.dataForm.id = id || undefined;
186 192 this.visible = true;
187 193 this.isDetail = isDetail || false;
188 194 this.$nextTick(() => {
189 195 this.$refs['elForm'].resetFields();
  196 + if (!id) {
  197 + this.dataForm.djlx = '其他收入单';
  198 + }
190 199 if (this.dataForm.id) {
191 200 request({
192 201 url: '/api/Extend/WtFysrd/' + this.dataForm.id,
... ... @@ -201,6 +210,7 @@
201 210 this.$refs['elForm'].validate((valid) => {
202 211 if (valid) {
203 212 if (!this.dataForm.id) {
  213 + this.dataForm.djlx = '其他收入单'
204 214 request({
205 215 url: `/api/Extend/WtFysrd`,
206 216 method: 'post',
... ... @@ -217,6 +227,7 @@
217 227 })
218 228 })
219 229 } else {
  230 + this.dataForm.djlx = this.dataForm.djlx || '其他收入单'
220 231 request({
221 232 url: '/api/Extend/WtFysrd/' + this.dataForm.id,
222 233 method: 'PUT',
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtFysrd_qtsr/index.vue
... ... @@ -31,8 +31,8 @@
31 31 </el-form-item>
32 32 </el-col>
33 33 <el-col :span="6">
34   - <el-form-item label="结算账户">
35   - <el-select v-model="query.jszh" placeholder="结算账户" clearable >
  34 + <el-form-item label="收款账户">
  35 + <el-select v-model="query.jszh" placeholder="收款账户" clearable >
36 36 <el-option v-for="(item, index) in jszhOptions" :key="index" :label="item.fullName" :value="item.id" />
37 37 </el-select>
38 38 </el-form-item>
... ... @@ -103,7 +103,7 @@
103 103 <el-table-column prop="fzjg" label="分支机构" align="left" />
104 104 <el-table-column prop="bm" label="部门" align="left" />
105 105 <el-table-column prop="jsr" label="经手人" align="left" />
106   - <el-table-column label="结算账户" prop="jszh" align="left">
  106 + <el-table-column label="收款账户" prop="jszh" align="left">
107 107 <template slot-scope="scope">{{ scope.row.jszh | dynamicText(jszhOptions) }}</template>
108 108 </el-table-column>
109 109 <el-table-column prop="je" label="金额" align="left" />
... ... @@ -133,6 +133,7 @@
133 133 import NCCForm from './Form'
134 134 import ExportBox from './ExportBox'
135 135 import { previewDataInterface } from '@/api/systemData/dataInterface'
  136 + import { getAccountSelector } from '@/api/extend/wtAccount'
136 137 export default {
137 138 components: { NCCForm, ExportBox },
138 139 data() {
... ... @@ -170,7 +171,7 @@
170 171 { prop: 'fzjg', label: '分支机构' },
171 172 { prop: 'bm', label: '部门' },
172 173 { prop: 'jsr', label: '经手人' },
173   - { prop: 'jszh', label: '结算账户' },
  174 + { prop: 'jszh', label: '收款账户' },
174 175 { prop: 'je', label: '金额' },
175 176 { prop: 'zdr', label: '制单人' },
176 177 { prop: 'shr', label: '审核人' },
... ... @@ -180,13 +181,14 @@
180 181 { prop: 'djlx', label: '单据类型' },
181 182 ],
182 183 fkdwOptions : [],
183   - jszhOptions:[{"fullName":"选项一","id":"1"},{"fullName":"选项二","id":"2"}],
  184 + jszhOptions:[],
184 185 }
185 186 },
186 187 computed: {},
187 188 created() {
188 189 this.initData()
189 190 this.getfkdwOptions();
  191 + this.getjszhOptions();
190 192 },
191 193 methods: {
192 194 getfkdwOptions(){
... ... @@ -194,6 +196,11 @@
194 196 this.fkdwOptions = res.data
195 197 });
196 198 },
  199 + getjszhOptions(){
  200 + getAccountSelector().then(res => {
  201 + this.jszhOptions = (res.data && res.data.list) ? res.data.list : (res.data || [])
  202 + });
  203 + },
197 204 initData() {
198 205 this.listLoading = true;
199 206 let _query = {
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtHzd/Form.vue
1 1 <template>
2   - <el-dialog :title="!dataForm.id ? '新建' : isDetail ? '详情':'编辑'" :close-on-click-modal="false" :visible.sync="visible" class="NCC-dialog NCC-dialog_center" lock-scroll width="80%">
  2 + <el-dialog :title="dialogTitle" :close-on-click-modal="false" :visible.sync="visible" class="NCC-dialog NCC-dialog_center" lock-scroll width="80%">
3 3 <el-row :gutter="15" class="" >
4   - <el-form ref="elForm" :model="dataForm" size="small" label-width="100px" label-position="right" :disabled="!!isDetail" :rules="rules">
  4 + <el-form ref="elForm" :model="dataForm" size="small" label-width="100px" label-position="right" :disabled="formDisabled" :rules="rules">
5 5 <el-col :span="12">
6 6 <el-form-item label="单据编号" prop="id">
7 7 <el-input v-model="dataForm.id" placeholder="系统自动生成" clearable readonly :style='{"width":"100%"}' />
... ... @@ -9,7 +9,12 @@
9 9 </el-col>
10 10 <el-col :span="12">
11 11 <el-form-item label="单据日期" prop="djrq">
12   - <el-date-picker v-model="dataForm.djrq" placeholder="请选择" clearable :style='{"width":"100%"}' type='date' format="yyyy-MM-dd" value-format="timestamp" readonly />
  12 + <el-date-picker v-model="dataForm.djrq" placeholder="请选择" clearable :style='{"width":"100%"}' type='date' format="yyyy-MM-dd" value-format="timestamp" :readonly="formDisabled" />
  13 + </el-form-item>
  14 + </el-col>
  15 + <el-col :span="12" v-if="dataForm.id">
  16 + <el-form-item label="审核状态">
  17 + <el-input :value="auditStatusText" readonly />
13 18 </el-form-item>
14 19 </el-col>
15 20 <el-col :span="12" v-if="false">
... ... @@ -213,13 +218,13 @@
213 218 </div>
214 219 </template>
215 220 </el-table-column>
216   - <el-table-column label="操作" width="50">
  221 + <el-table-column v-if="!formDisabled" label="操作" width="50">
217 222 <template slot-scope="scope">
218 223 <el-button size="mini" type="text" class="NCC-table-delBtn" @click="handleDelWtXsckdMxEntityList(scope.$index)">删除</el-button>
219 224 </template>
220 225 </el-table-column>
221 226 </el-table>
222   - <div class="table-actions" @click="addHandleWtXsckdMxEntityList()">
  227 + <div v-if="!formDisabled" class="table-actions" @click="addHandleWtXsckdMxEntityList()">
223 228 <el-button type="text" icon="el-icon-plus">新增</el-button>
224 229 </div>
225 230 </el-form-item>
... ... @@ -276,8 +281,10 @@
276 281 </el-row>
277 282 <span slot="footer" class="dialog-footer">
278 283 <el-button @click="visible = false">取 消</el-button>
279   - <el-button type="default" @click="saveDraft" v-if="!isDetail">保存草稿</el-button>
280   - <el-button type="primary" @click="dataFormSubmit()" v-if="!isDetail">确 定</el-button>
  284 + <template v-if="!formDisabled">
  285 + <el-button @click="saveDraft">保存草稿</el-button>
  286 + <el-button type="primary" @click="submitForAudit">提交审核</el-button>
  287 + </template>
281 288 </span>
282 289 <!-- 商品条码选择弹窗 -->
283 290 <BarcodeSelect ref="barcodeSelect" @select="handleBarcodeSelect" />
... ... @@ -344,7 +351,27 @@
344 351 totalJe() {
345 352 return (this.dataForm.wtXsckdMxList || []).reduce((sum, row) => sum + (parseFloat(row.je) || 0), 0)
346 353 },
347   -
  354 + auditStatusText() {
  355 + const z = this.dataForm.djzt != null && this.dataForm.djzt !== '' ? String(this.dataForm.djzt).trim() : ''
  356 + if (z) return z
  357 + return '—'
  358 + },
  359 + isBillLockedForEdit() {
  360 + const z = this.auditStatusText
  361 + return z === '待审核' || z === '已审核' || z === '一级已审' || z === '待二级' || z === '一级已审/待二级'
  362 + },
  363 + formDisabled() {
  364 + return !!this.isDetail || this.isBillLockedForEdit
  365 + },
  366 + dialogTitle() {
  367 + if (!this.dataForm.id) return '新建'
  368 + if (this.isDetail) return '详情'
  369 + const z = this.auditStatusText
  370 + if (z === '已审核') return '查看(已审核)'
  371 + if (z === '待审核' || z === '一级已审' || z === '待二级' || z === '一级已审/待二级') return '查看(待审核)'
  372 + if (z === '草稿') return '编辑'
  373 + return '编辑'
  374 + },
348 375 },
349 376 watch: {},
350 377 created() {
... ... @@ -662,14 +689,21 @@
662 689 // 恢复序列号信息
663 690 _this.restoreSerialNumbers();
664 691 })
665   - }
666   - else{
667   -
  692 + } else {
  693 + _this.dataForm.djrq = Date.now()
  694 + _this.dataForm.djlx = '获赠单'
  695 + _this.dataForm.djzt = '草稿'
  696 + if (_this.$store && _this.$store.getters && _this.$store.getters.userInfo) {
  697 + _this.dataForm.jsr = _this.$store.getters.userInfo.userId || _this.$store.getters.userInfo.id
  698 + }
  699 + _this.dataForm.wtXsckdMxList = []
  700 + _this.addHandleWtXsckdMxEntityList()
668 701 }
669 702 })
670 703 },
671 704 // 保存草稿:只做基础表单校验,不做序列号强校验,保存时带isDraft:true字段
672 705 async saveDraft() {
  706 + if (this.formDisabled) return
673 707 // 确保单据类型字段赋值
674 708 this.dataForm.djlx = '获赠单';
675 709 this.$refs['elForm'].validate(async (valid) => {
... ... @@ -708,7 +742,8 @@
708 742 }
709 743 });
710 744 },
711   - async dataFormSubmit() {
  745 + async submitForAudit() {
  746 + if (this.formDisabled) return
712 747 const mxCheck = validateMxNoEmptyProductRows(this.dataForm.wtXsckdMxList)
713 748 if (!mxCheck.valid) {
714 749 this.$message.warning(`第 ${mxCheck.emptyLineNos.join('、')} 行未选择商品,请先删除空白行后再提交`)
... ... @@ -720,7 +755,7 @@
720 755 // 确保单据类型字段赋值
721 756 this.dataForm.djlx = '获赠单';
722 757 // 确保单据状态为待审核
723   - this.dataForm.djzt = '待审核';
  758 + this.$set(this.dataForm, 'djzt', '待审核')
724 759  
725 760 // 检查是否有明细数据
726 761 if (!this.dataForm.wtXsckdMxList || this.dataForm.wtXsckdMxList.length === 0) {
... ... @@ -839,7 +874,7 @@
839 874 this.currentBarcodeRow.sptm = barcodeData.barcode
840 875 this.currentBarcodeRow.spmc = barcodeData.productName
841 876 this.currentBarcodeRow.spbh = barcodeData.productCode
842   - // 可根据需要回填其他字段
  877 + this.handleProductChange(this.currentBarcodeRow)
843 878 }
844 879 },
845 880  
... ... @@ -1002,6 +1037,7 @@
1002 1037 // 再次强制更新,确保界面刷新
1003 1038 this.$forceUpdate();
1004 1039 });
  1040 + this.fillAvgCostForRow(row);
1005 1041 } else {
1006 1042 if (response.msg !== '操作成功') {
1007 1043 this.$message.error(response.msg || '获取库存失败');
... ... @@ -1009,6 +1045,7 @@
1009 1045 // 使用$set确保响应式更新
1010 1046 this.$set(row, 'kucun', 0);
1011 1047 console.log('设置库存值为0 (失败)');
  1048 + this.fillAvgCostForRow(row);
1012 1049 }
1013 1050 } catch (error) {
1014 1051 console.error('获取库存失败:', error);
... ... @@ -1017,6 +1054,7 @@
1017 1054 }
1018 1055 row.kucun = 0;
1019 1056 console.log('设置库存值为0 (异常)');
  1057 + this.fillAvgCostForRow(row);
1020 1058 } finally {
1021 1059 row.loadingStock = false;
1022 1060 console.log('=== 库存查询完成 ===');
... ... @@ -1348,24 +1386,23 @@
1348 1386 }
1349 1387 },
1350 1388  
1351   - // 按入库仓库的加权平均成本带出成本;无库存/无成本记录则保留为空,让用户手工填写
  1389 + // 获赠单:仅当所选入库仓(及展开子仓)在 wt_sp_cost 上有在库数量时,按 Σ(sl×cbj)/Σ(sl) 带出成本;无在库则清空单价由用户手填(可改)
1352 1390 async fillAvgCostForRow(row) {
1353 1391 try {
1354 1392 if (!row.spbh || !row.rkck) return;
  1393 + // 注意:全局 request 对 GET 会把 config.params 赋成 config.data,不能只用 params,否则查询串为空
  1394 + const url = `/api/Extend/WtXsckd/GetWarehouseWeightedAvgCost?spbh=${encodeURIComponent(String(row.spbh))}&ckOrMdId=${encodeURIComponent(String(row.rkck))}`
1355 1395 const res = await request({
1356   - url: '/api/Extend/WtXsckd/GetOutboundCostPrice',
1357   - method: 'get',
1358   - params: { spbh: row.spbh, ckOrMdId: row.rkck }
  1396 + url,
  1397 + method: 'get'
1359 1398 });
1360 1399 const body = res && res.data ? res.data : res;
1361 1400 if (body && body.success && body.data != null && Number(body.data) > 0) {
1362 1401 this.$set(row, 'dj', Number(body.data));
1363 1402 this.handleAmountChange(row);
1364 1403 } else {
1365   - // 未查询到成本或成本为 0 → 允许手动编辑
1366   - if (row.dj === undefined || row.dj === null || row.dj === '') {
1367   - this.$set(row, 'dj', undefined);
1368   - }
  1404 + this.$set(row, 'dj', undefined);
  1405 + this.handleAmountChange(row);
1369 1406 }
1370 1407 } catch (e) {
1371 1408 console.warn('[fillAvgCostForRow] 查询成本失败:', e);
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtHzd/index.vue
... ... @@ -109,16 +109,25 @@
109 109 <el-table-column prop="gzr" label="过账人" align="left" />
110 110 <el-table-column prop="bz" label="备注" align="left" />
111 111 <el-table-column prop="djlx" label="单据类型" align="left" />
112   - <el-table-column prop="djzt" label="单据状态" align="left" />
  112 + <el-table-column prop="djzt" label="审核状态" align="left" width="120">
  113 + <template slot-scope="scope">
  114 + <el-tag v-if="scope.row.djzt === '已审核'" type="success">已审核</el-tag>
  115 + <el-tag v-else-if="scope.row.djzt === '草稿'" type="info">草稿</el-tag>
  116 + <el-tag v-else type="warning">{{ scope.row.djzt || '待审核' }}</el-tag>
  117 + </template>
  118 + </el-table-column>
113 119 <el-table-column label="摘要" align="left" min-width="200" show-overflow-tooltip class-name="cell-nowrap">
114 120 <template slot-scope="scope">
115 121 <ncc-table-summary-cell :row="scope.row" fields="zy,Zy" />
116 122 </template>
117 123 </el-table-column>
118   - <el-table-column label="操作" fixed="right" width="100">
  124 + <el-table-column label="操作" fixed="right" width="260">
119 125 <template slot-scope="scope">
120   - <el-button type="text" @click="addOrUpdateHandle(scope.row.id)" >编辑</el-button>
121   - <el-button type="text" @click="handleDel(scope.row.id)" class="NCC-table-delBtn" >删除</el-button>
  126 + <el-button type="text" @click="openDetail(scope.row.id)">查看</el-button>
  127 + <el-button type="text" v-if="isDraftRow(scope.row)" @click="addOrUpdateHandle(scope.row.id)">编辑</el-button>
  128 + <el-button type="text" v-if="isPendingAudit(scope.row)" @click="handleApprove(scope.row.id)" style="color:#409EFF">审核</el-button>
  129 + <el-button type="text" v-if="scope.row.djzt === '已审核'" @click="handleReverse(scope.row.id)" style="color:#E6A23C">反审</el-button>
  130 + <el-button type="text" v-if="isDraftRow(scope.row)" @click="handleDel(scope.row.id)" class="NCC-table-delBtn">删除</el-button>
122 131 </template>
123 132 </el-table-column>
124 133 </NCC-table>
... ... @@ -188,6 +197,54 @@
188 197 this.getkhOptions();
189 198 },
190 199 methods: {
  200 + isDraftRow(row) {
  201 + return row && String(row.djzt || '').trim() === '草稿'
  202 + },
  203 + isPendingAudit(row) {
  204 + const z = row && String(row.djzt || '').trim()
  205 + return z === '待审核' || z === ''
  206 + },
  207 + openDetail(id) {
  208 + this.formVisible = true
  209 + this.$nextTick(() => {
  210 + this.$refs.NCCForm.init(id, true)
  211 + })
  212 + },
  213 + handleApprove(id) {
  214 + this.$confirm('确认审核该获赠单?', '提示', { type: 'warning' }).then(() => {
  215 + request({
  216 + url: `/api/Extend/WtXsckd/ApproveGeneric/${id}`,
  217 + method: 'POST',
  218 + data: {}
  219 + }).then(res => {
  220 + this.$message({ type: 'success', message: res.msg || '审核成功', duration: 1200 })
  221 + this.initData()
  222 + })
  223 + }).catch(() => {})
  224 + },
  225 + handleReverse(id) {
  226 + if (!id) return
  227 + this.$confirm('反审后单据将恢复为可再次处理状态,是否继续?', '反审确认', { type: 'warning' })
  228 + .then(() => {
  229 + return request({
  230 + url: `/api/Extend/WtXsckd/ReverseApproval/${id}`,
  231 + method: 'POST',
  232 + data: {}
  233 + })
  234 + })
  235 + .then((res) => {
  236 + const d = (res && res.data) || {}
  237 + const ok = d.success === true || String(res && res.code) === '200'
  238 + const msg = d.message || res.msg || (ok ? '反审成功' : '操作失败')
  239 + if (ok) {
  240 + this.$message.success(msg)
  241 + this.initData()
  242 + } else {
  243 + this.$message.error(msg)
  244 + }
  245 + })
  246 + .catch(() => {})
  247 + },
191 248 getrkckOptions(){
192 249 previewDataInterface('681758216954053893').then(res => {
193 250 this.rkckOptions = res.data
... ... @@ -321,6 +378,7 @@
321 378 for (let key in this.query) {
322 379 this.query[key] = undefined
323 380 }
  381 + this.query.djlx = '获赠单'
324 382 this.listQuery = {
325 383 currentPage: 1,
326 384 pageSize: 20,
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtPl/Form.vue
... ... @@ -22,6 +22,12 @@
22 22 </el-radio-group>
23 23 </el-form-item>
24 24 </el-col>
  25 + <el-col :span="24">
  26 + <el-form-item label="序号" prop="xh">
  27 + <el-input-number v-model="dataForm.xh" :min="0" :max="999999" :step="1" :controls-position="'right'" placeholder="数值越小越靠前" :style='{"width":"100%"}' />
  28 + <div style="color:#999;font-size:12px;line-height:18px;">数值越小越靠前;留空则排序靠后</div>
  29 + </el-form-item>
  30 + </el-col>
25 31 </el-form>
26 32 </el-row>
27 33 <span slot="footer" class="dialog-footer">
... ... @@ -44,9 +50,9 @@
44 50 isDetail: false,
45 51 dataForm: {
46 52 id:'',
47   - id:undefined,
48 53 plmc:undefined,
49 54 sfmdfl:'0',
  55 + xh:undefined,
50 56 },
51 57 rules: {
52 58 },
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtPl/index.vue
... ... @@ -36,6 +36,11 @@
36 36 </div>
37 37 </div>
38 38 <NCC-table v-loading="listLoading" :data="list" has-c @selection-change="handleSelectionChange">
  39 + <el-table-column prop="xh" label="序号" align="center" width="80">
  40 + <template slot-scope="scope">
  41 + <span>{{ scope.row.xh === null || scope.row.xh === undefined ? '-' : scope.row.xh }}</span>
  42 + </template>
  43 + </el-table-column>
39 44 <el-table-column prop="id" label="分类编号" align="left" />
40 45 <el-table-column prop="plmc" label="品类名称" align="left" />
41 46 <el-table-column prop="sfmdfl" label="是否门店分类" align="center" width="120">
... ... @@ -85,6 +90,7 @@
85 90 formVisible: false,
86 91 exportBoxVisible: false,
87 92 columnList: [
  93 + { prop: 'xh', label: '序号' },
88 94 { prop: 'id', label: '分类编号' },
89 95 { prop: 'plmc', label: '品类名称' },
90 96 { prop: 'sfmdfl', label: '是否门店分类' },
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtPp/Form.vue
... ... @@ -8,6 +8,12 @@
8 8 </el-input>
9 9 </el-form-item>
10 10 </el-col>
  11 + <el-col :span="24">
  12 + <el-form-item label="序号" prop="xh">
  13 + <el-input-number v-model="dataForm.xh" :min="0" :max="999999" :step="1" :controls-position="'right'" placeholder="数值越小越靠前" :style='{"width":"100%"}' />
  14 + <div style="color:#999;font-size:12px;line-height:18px;">数值越小越靠前;留空则排序靠后</div>
  15 + </el-form-item>
  16 + </el-col>
11 17 </el-form>
12 18 </el-row>
13 19 <span slot="footer" class="dialog-footer">
... ... @@ -31,6 +37,7 @@
31 37 dataForm: {
32 38 id:'',
33 39 ppmc:undefined,
  40 + xh:undefined,
34 41 },
35 42 rules: {
36 43 },
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtPp/index.vue
... ... @@ -29,6 +29,11 @@
29 29 </div>
30 30 </div>
31 31 <NCC-table v-loading="listLoading" :data="list">
  32 + <el-table-column prop="xh" label="序号" align="center" width="80">
  33 + <template slot-scope="scope">
  34 + <span>{{ scope.row.xh === null || scope.row.xh === undefined ? '-' : scope.row.xh }}</span>
  35 + </template>
  36 + </el-table-column>
32 37 <el-table-column prop="ppmc" label="品牌名称" align="left" />
33 38 <el-table-column label="操作" fixed="right" width="100">
34 39 <template slot-scope="scope">
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtProductSummary/index.vue
... ... @@ -332,6 +332,12 @@
332 332 <template slot-scope="s">{{ formatNumber(s.row['销售'], 4) }}</template>
333 333 </el-table-column>
334 334 <el-table-column prop="单价" label="单价" min-width="96" align="right" show-overflow-tooltip>
  335 + <template slot="header">
  336 + <span class="sortable-th" @click.stop="toggleSort('brand', '单价')">
  337 + 单价
  338 + <i :class="sortCaretClass('brand', '单价')" class="sort-caret" />
  339 + </span>
  340 + </template>
335 341 <template slot-scope="s">{{ formatNumber(s.row['单价'], 4) }}</template>
336 342 </el-table-column>
337 343 <el-table-column prop="销售金额" label="销售金额" min-width="110" align="right" show-overflow-tooltip>
... ... @@ -344,12 +350,30 @@
344 350 <template slot-scope="s">{{ formatNumber(s.row['销售金额'], 2) }}</template>
345 351 </el-table-column>
346 352 <el-table-column prop="成本单价" label="成本单价" min-width="96" align="right" show-overflow-tooltip>
  353 + <template slot="header">
  354 + <span class="sortable-th" @click.stop="toggleSort('brand', '成本单价')">
  355 + 成本单价
  356 + <i :class="sortCaretClass('brand', '成本单价')" class="sort-caret" />
  357 + </span>
  358 + </template>
347 359 <template slot-scope="s">{{ formatNumber(s.row['成本单价'], 4) }}</template>
348 360 </el-table-column>
349 361 <el-table-column prop="成本金额" label="成本金额" min-width="110" align="right" show-overflow-tooltip>
  362 + <template slot="header">
  363 + <span class="sortable-th" @click.stop="toggleSort('brand', '成本金额')">
  364 + 成本金额
  365 + <i :class="sortCaretClass('brand', '成本金额')" class="sort-caret" />
  366 + </span>
  367 + </template>
350 368 <template slot-scope="s">{{ formatNumber(s.row['成本金额'], 2) }}</template>
351 369 </el-table-column>
352 370 <el-table-column prop="折前销售金额" label="折前销售金额" min-width="120" align="right" show-overflow-tooltip>
  371 + <template slot="header">
  372 + <span class="sortable-th" @click.stop="toggleSort('brand', '折前销售金额')">
  373 + 折前销售金额
  374 + <i :class="sortCaretClass('brand', '折前销售金额')" class="sort-caret" />
  375 + </span>
  376 + </template>
353 377 <template slot-scope="s">{{ formatNumber(s.row['折前销售金额'], 2) }}</template>
354 378 </el-table-column>
355 379 <el-table-column prop="毛利" label="毛利" min-width="96" align="right" show-overflow-tooltip>
... ... @@ -362,9 +386,21 @@
362 386 <template slot-scope="s">{{ formatNumber(s.row['毛利'], 2) }}</template>
363 387 </el-table-column>
364 388 <el-table-column prop="毛利率(%)" label="毛利率(%)" min-width="100" align="right" show-overflow-tooltip>
  389 + <template slot="header">
  390 + <span class="sortable-th" @click.stop="toggleSort('brand', '毛利率(%)')">
  391 + 毛利率(%)
  392 + <i :class="sortCaretClass('brand', '毛利率(%)')" class="sort-caret" />
  393 + </span>
  394 + </template>
365 395 <template slot-scope="s">{{ formatNumber(s.row['毛利率(%)'], 2) }}</template>
366 396 </el-table-column>
367 397 <el-table-column prop="0单价数量" label="0单价数量" min-width="100" align="right" show-overflow-tooltip>
  398 + <template slot="header">
  399 + <span class="sortable-th" @click.stop="toggleSort('brand', '0单价数量')">
  400 + 0单价数量
  401 + <i :class="sortCaretClass('brand', '0单价数量')" class="sort-caret" />
  402 + </span>
  403 + </template>
368 404 <template slot-scope="s">{{ formatNumber(s.row['0单价数量'], 4) }}</template>
369 405 </el-table-column>
370 406 </el-table>
... ... @@ -378,6 +414,11 @@
378 414 </template>
379 415 </el-table-column>
380 416 <el-table-column type="index" label="行号" width="56" align="center" />
  417 + <el-table-column label="线性列表" width="90" align="center">
  418 + <template slot-scope="scope">
  419 + <el-link type="primary" :underline="false" @click="openLinearList(scope.row)">查看</el-link>
  420 + </template>
  421 + </el-table-column>
381 422 <el-table-column label="分类名称" min-width="160" show-overflow-tooltip>
382 423 <template slot="header">
383 424 <span class="sortable-th" @click.stop="toggleSort('category', '分类名称')">
... ... @@ -400,6 +441,12 @@
400 441 <template slot-scope="scope">{{ formatNumber(scope.row['销售'], 4) }}</template>
401 442 </el-table-column>
402 443 <el-table-column prop="单价" label="单价" min-width="96" align="right" show-overflow-tooltip>
  444 + <template slot="header">
  445 + <span class="sortable-th" @click.stop="toggleSort('category', '单价')">
  446 + 单价
  447 + <i :class="sortCaretClass('category', '单价')" class="sort-caret" />
  448 + </span>
  449 + </template>
403 450 <template slot-scope="scope">{{ formatNumber(scope.row['单价'], 4) }}</template>
404 451 </el-table-column>
405 452 <el-table-column prop="销售金额" label="销售金额" min-width="110" align="right" show-overflow-tooltip>
... ... @@ -412,12 +459,30 @@
412 459 <template slot-scope="scope">{{ formatNumber(scope.row['销售金额'], 2) }}</template>
413 460 </el-table-column>
414 461 <el-table-column prop="成本单价" label="成本单价" min-width="96" align="right" show-overflow-tooltip>
  462 + <template slot="header">
  463 + <span class="sortable-th" @click.stop="toggleSort('category', '成本单价')">
  464 + 成本单价
  465 + <i :class="sortCaretClass('category', '成本单价')" class="sort-caret" />
  466 + </span>
  467 + </template>
415 468 <template slot-scope="scope">{{ formatNumber(scope.row['成本单价'], 4) }}</template>
416 469 </el-table-column>
417 470 <el-table-column prop="成本金额" label="成本金额" min-width="110" align="right" show-overflow-tooltip>
  471 + <template slot="header">
  472 + <span class="sortable-th" @click.stop="toggleSort('category', '成本金额')">
  473 + 成本金额
  474 + <i :class="sortCaretClass('category', '成本金额')" class="sort-caret" />
  475 + </span>
  476 + </template>
418 477 <template slot-scope="scope">{{ formatNumber(scope.row['成本金额'], 2) }}</template>
419 478 </el-table-column>
420 479 <el-table-column prop="折前销售金额" label="折前销售金额" min-width="120" align="right" show-overflow-tooltip>
  480 + <template slot="header">
  481 + <span class="sortable-th" @click.stop="toggleSort('category', '折前销售金额')">
  482 + 折前销售金额
  483 + <i :class="sortCaretClass('category', '折前销售金额')" class="sort-caret" />
  484 + </span>
  485 + </template>
421 486 <template slot-scope="scope">{{ formatNumber(scope.row['折前销售金额'], 2) }}</template>
422 487 </el-table-column>
423 488 <el-table-column prop="毛利" label="毛利" min-width="96" align="right" show-overflow-tooltip>
... ... @@ -430,14 +495,50 @@
430 495 <template slot-scope="scope">{{ formatNumber(scope.row['毛利'], 2) }}</template>
431 496 </el-table-column>
432 497 <el-table-column prop="毛利率(%)" label="毛利率(%)" min-width="100" align="right" show-overflow-tooltip>
  498 + <template slot="header">
  499 + <span class="sortable-th" @click.stop="toggleSort('category', '毛利率(%)')">
  500 + 毛利率(%)
  501 + <i :class="sortCaretClass('category', '毛利率(%)')" class="sort-caret" />
  502 + </span>
  503 + </template>
433 504 <template slot-scope="scope">{{ formatNumber(scope.row['毛利率(%)'], 2) }}</template>
434 505 </el-table-column>
435 506 <el-table-column prop="0单价数量" label="0单价数量" min-width="100" align="right" show-overflow-tooltip>
  507 + <template slot="header">
  508 + <span class="sortable-th" @click.stop="toggleSort('category', '0单价数量')">
  509 + 0单价数量
  510 + <i :class="sortCaretClass('category', '0单价数量')" class="sort-caret" />
  511 + </span>
  512 + </template>
436 513 <template slot-scope="scope">{{ formatNumber(scope.row['0单价数量'], 4) }}</template>
437 514 </el-table-column>
438 515 </el-table>
439 516 </div>
440 517 </div>
  518 + <el-dialog
  519 + :title="`线性列表 - ${linearCategoryName || ''}`"
  520 + :visible.sync="linearDialogVisible"
  521 + width="88%"
  522 + :close-on-click-modal="false"
  523 + >
  524 + <el-table v-loading="linearLoading" :data="linearRows" border size="small" class="nested-table">
  525 + <el-table-column prop="商品编号" label="商品编号" min-width="110" show-overflow-tooltip />
  526 + <el-table-column prop="商品名称" label="商品名称" min-width="160" show-overflow-tooltip />
  527 + <el-table-column prop="品牌名称" label="品牌名称" min-width="120" show-overflow-tooltip />
  528 + <el-table-column prop="销售" label="销售" min-width="90" align="right" show-overflow-tooltip>
  529 + <template slot-scope="scope">{{ formatNumber(scope.row['销售'], 4) }}</template>
  530 + </el-table-column>
  531 + <el-table-column prop="销售金额" label="销售金额" min-width="110" align="right" show-overflow-tooltip>
  532 + <template slot-scope="scope">{{ formatNumber(scope.row['销售金额'], 2) }}</template>
  533 + </el-table-column>
  534 + <el-table-column prop="成本金额" label="成本金额" min-width="110" align="right" show-overflow-tooltip>
  535 + <template slot-scope="scope">{{ formatNumber(scope.row['成本金额'], 2) }}</template>
  536 + </el-table-column>
  537 + <el-table-column prop="毛利" label="毛利" min-width="96" align="right" show-overflow-tooltip>
  538 + <template slot-scope="scope">{{ formatNumber(scope.row['毛利'], 2) }}</template>
  539 + </el-table-column>
  540 + </el-table>
  541 + </el-dialog>
441 542 </div>
442 543 </template>
443 544  
... ... @@ -449,8 +550,8 @@ import { getProductSummaryHierarchy } from &#39;@/api/extend/wtXsckdProductSummary&#39;
449 550 const DEFAULT_BILL_TYPES = ['销售出库单', '零售单', '委托代销结算单']
450 551  
451 552 /** 与后端 BuildProductSummaryOrderBy(category) 白名单一致,避免非法排序列 */
452   -const SORT_WHITELIST_CATEGORY = ['分类名称', '销售', '销售金额', '毛利']
453   -const SORT_WHITELIST_BRAND = ['品牌名称', '销售', '销售金额', '毛利']
  553 +const SORT_WHITELIST_CATEGORY = ['分类名称', '销售', '单价', '销售金额', '成本单价', '成本金额', '折前销售金额', '毛利', '毛利率(%)', '0单价数量']
  554 +const SORT_WHITELIST_BRAND = ['品牌名称', '销售', '单价', '销售金额', '成本单价', '成本金额', '折前销售金额', '毛利', '毛利率(%)', '0单价数量']
454 555 /** 与后端 BuildProductSummaryOrderBy(product) 白名单一致 */
455 556 const SORT_WHITELIST_PRODUCT = ['商品编号', '商品名称', '销售', '单价', '销售金额', '成本单价', '成本金额', '毛利', '毛利率(%)']
456 557  
... ... @@ -468,6 +569,10 @@ export default {
468 569 sortCategory: { field: '销售金额', order: 'desc' },
469 570 sortBrand: { field: '销售金额', order: 'desc' },
470 571 sortProduct: { field: '销售金额', order: 'desc' },
  572 + linearDialogVisible: false,
  573 + linearCategoryName: '',
  574 + linearRows: [],
  575 + linearLoading: false,
471 576 filters: {
472 577 dateRange: [],
473 578 productSpId: '',
... ... @@ -646,6 +751,7 @@ export default {
646 751 },
647 752 buildBasePayload() {
648 753 const params = {}
  754 + params.includeAllProducts = true
649 755 if (this.filters.dateRange && this.filters.dateRange.length === 2) {
650 756 params.startDate = this.filters.dateRange[0]
651 757 params.endDate = this.filters.dateRange[1]
... ... @@ -785,6 +891,30 @@ export default {
785 891 this.$set(this.productLoading, k, false)
786 892 })
787 893 },
  894 + openLinearList(catRow) {
  895 + const cid = this.categoryRowKey(catRow)
  896 + if (!cid) return
  897 + this.linearCategoryName = this.displayText(catRow['分类名称'])
  898 + this.linearDialogVisible = true
  899 + this.linearLoading = true
  900 + getProductSummaryHierarchy({
  901 + groupLevel: 'product',
  902 + categoryId: cid,
  903 + ...this.buildBasePayload(),
  904 + sortField: this.sortProduct.field,
  905 + sortOrder: this.sortProduct.order
  906 + })
  907 + .then((res) => {
  908 + this.linearRows = this.normalizeRows(res.data)
  909 + })
  910 + .catch((e) => {
  911 + this.linearRows = []
  912 + this.$message.error((e && e.message) || '加载线性列表失败')
  913 + })
  914 + .finally(() => {
  915 + this.linearLoading = false
  916 + })
  917 + },
788 918 sortState(level) {
789 919 if (level === 'category') return this.sortCategory
790 920 if (level === 'brand') return this.sortBrand
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtSkfkChannelStat/index.vue
1 1 <template>
2   - <div class="NCC-common-layout skfk-channel-stat-page">
  2 + <div class="NCC-common-layout">
3 3 <div class="NCC-common-layout-center">
4 4 <el-row class="NCC-common-search-box" :gutter="16">
5   - <el-form :model="filters" label-width="90px" size="small" @submit.native.prevent>
  5 + <el-form @submit.native.prevent>
  6 + <el-col :span="6">
  7 + <el-form-item label="日期">
  8 + <el-date-picker
  9 + v-model="filters.date"
  10 + type="date"
  11 + value-format="yyyy-MM-dd"
  12 + placeholder="选择日期"
  13 + style="width: 100%"
  14 + />
  15 + </el-form-item>
  16 + </el-col>
  17 + <el-col :span="6">
  18 + <el-form-item label="账户">
  19 + <el-select
  20 + v-model="filters.accountId"
  21 + filterable
  22 + clearable
  23 + placeholder="全部账户"
  24 + style="width: 100%"
  25 + >
  26 + <el-option v-for="item in accountOptions" :key="item.id" :label="item.fullName" :value="item.id" />
  27 + </el-select>
  28 + </el-form-item>
  29 + </el-col>
  30 + <el-col :span="6">
  31 + <el-form-item>
  32 + <el-button type="primary" icon="el-icon-search" @click="handleSearch">查询</el-button>
  33 + <el-button icon="el-icon-refresh-right" @click="handleReset">重置</el-button>
  34 + </el-form-item>
  35 + </el-col>
  36 + </el-form>
  37 + </el-row>
  38 +
  39 + <div class="NCC-common-layout-main NCC-flex-main">
  40 + <div class="title-line">《账户对账表》</div>
  41 + <el-table
  42 + v-loading="summaryLoading"
  43 + :data="tableRows"
  44 + border
  45 + row-key="rowKey"
  46 + class="cell-nowrap reconcile-table"
  47 + :row-class-name="rowClassName"
  48 + >
  49 + <el-table-column prop="date" label="日期" width="110" align="center">
  50 + <template slot-scope="scope">
  51 + <span v-if="!scope.row.isCategory">{{ scope.row.date }}</span>
  52 + </template>
  53 + </el-table-column>
  54 + <el-table-column prop="account" label="账户分类" min-width="220">
  55 + <template slot-scope="scope">
  56 + <span v-if="scope.row.isCategory" class="category-text">{{ scope.row.account }}</span>
  57 + <el-link v-else type="primary" :underline="false" class="account-link" @click="openLedgerDialog(scope.row)">
  58 + -- {{ scope.row.account }}
  59 + </el-link>
  60 + </template>
  61 + </el-table-column>
  62 + <el-table-column prop="openingBalance" label="上期余额" min-width="120" align="right">
  63 + <template slot-scope="scope">{{ formatMoney(scope.row.openingBalance) }}</template>
  64 + </el-table-column>
  65 + <el-table-column prop="todayIncome" label="今日收入" min-width="120" align="right">
  66 + <template slot-scope="scope">{{ formatMoney(scope.row.todayIncome) }}</template>
  67 + </el-table-column>
  68 + <el-table-column prop="todayExpense" label="今日支出" min-width="120" align="right">
  69 + <template slot-scope="scope">{{ formatMoney(scope.row.todayExpense) }}</template>
  70 + </el-table-column>
  71 + <el-table-column prop="periodBalance" label="本期余额" min-width="120" align="right">
  72 + <template slot-scope="scope">{{ formatMoney(scope.row.periodBalance) }}</template>
  73 + </el-table-column>
  74 + <el-table-column prop="realtimeBalance" label="实时余额" min-width="120" align="right">
  75 + <template slot-scope="scope">{{ formatMoney(scope.row.realtimeBalance) }}</template>
  76 + </el-table-column>
  77 + </el-table>
  78 + </div>
  79 + </div>
  80 +
  81 + <el-dialog
  82 + :title="`账户明细 - ${currentLedgerAccountName || ''}`"
  83 + :visible.sync="ledgerDialogVisible"
  84 + width="90%"
  85 + :close-on-click-modal="false"
  86 + >
  87 + <el-row class="NCC-common-search-box" :gutter="16">
  88 + <el-form @submit.native.prevent>
6 89 <el-col :span="8">
7   - <el-form-item label="查询日期">
  90 + <el-form-item label="日期范围">
8 91 <el-date-picker
9   - v-model="filters.dateRange"
  92 + v-model="ledgerFilters.dateRange"
10 93 type="daterange"
  94 + value-format="yyyy-MM-dd"
11 95 range-separator="至"
12 96 start-placeholder="开始日期"
13 97 end-placeholder="结束日期"
14 98 style="width: 100%"
15   - value-format="yyyy-MM-dd"
16 99 />
17 100 </el-form-item>
18 101 </el-col>
19 102 <el-col :span="6">
20   - <el-form-item label="收付方向">
21   - <el-select v-model="filters.fx" clearable placeholder="全部" style="width: 100%">
22   - <el-option label="全部" value="" />
23   - <el-option label="收款" value="sk" />
24   - <el-option label="付款" value="fk" />
  103 + <el-form-item label="账户">
  104 + <el-select v-model="ledgerFilters.accountId" filterable clearable style="width: 100%">
  105 + <el-option v-for="item in accountOptions" :key="item.id" :label="item.fullName" :value="item.id" />
25 106 </el-select>
26 107 </el-form-item>
27 108 </el-col>
28   - <template v-if="showAll">
29   - <el-col :span="6">
30   - <el-form-item label="关键字">
31   - <el-input v-model="filters.qd" clearable placeholder="资金账户或方式" />
32   - </el-form-item>
33   - </el-col>
34   - <el-col :span="12">
35   - <el-form-item label="单据类型">
36   - <el-input v-model="filters.djlx" clearable placeholder="模糊匹配单据类型" />
37   - </el-form-item>
38   - </el-col>
39   - </template>
40   - <el-col :span="showAll ? 24 : 10">
41   - <el-form-item label-width="0">
42   - <span class="action-row">
43   - <el-button type="primary" icon="el-icon-search" @click="handleSearch">查询</el-button>
44   - <el-button icon="el-icon-refresh-right" @click="handleReset">重置</el-button>
45   - <el-button type="text" icon="el-icon-arrow-down" v-if="!showAll" @click="showAll = true">展开</el-button>
46   - <el-button type="text" icon="el-icon-arrow-up" v-else @click="showAll = false">收起</el-button>
47   - </span>
  109 + <el-col :span="6">
  110 + <el-form-item>
  111 + <el-button type="primary" icon="el-icon-search" @click="handleLedgerSearch">查询</el-button>
  112 + <el-button icon="el-icon-refresh-right" @click="handleLedgerReset">重置</el-button>
48 113 </el-form-item>
49 114 </el-col>
50 115 </el-form>
51 116 </el-row>
52 117  
53   - <div class="NCC-common-layout-main NCC-flex-main skfk-channel-stat-page__main">
54   - <div v-loading="summaryLoading" class="summary-wrap">
55   - <el-row :gutter="16" class="summary-cards-row">
56   - <el-col :xs="24" :sm="12">
57   - <div class="stat-card stat-card--sk">
58   - <i class="el-icon-top stat-card__icon stat-card__icon--sk" />
59   - <div class="stat-card__body">
60   - <div class="stat-card__label">收款侧合计</div>
61   - <div class="stat-card__value">{{ formatMoney(skSideTotal) }}</div>
62   - </div>
63   - </div>
64   - </el-col>
65   - <el-col :xs="24" :sm="12">
66   - <div class="stat-card stat-card--fk">
67   - <i class="el-icon-bottom stat-card__icon stat-card__icon--fk" />
68   - <div class="stat-card__body">
69   - <div class="stat-card__label">付款侧合计</div>
70   - <div class="stat-card__value">{{ formatMoney(fkSideTotal) }}</div>
71   - </div>
72   - </div>
73   - </el-col>
74   - </el-row>
75   - </div>
76   -
77   - <div class="subsection-title">收款侧</div>
78   - <el-row :gutter="16" class="channel-tables-row">
79   - <el-col v-for="p in skPanels" :key="p.key" :xs="24" :lg="12">
80   - <div class="table-section channel-block">
81   - <div class="table-title-bar">
82   - <i :class="[p.icon, 'title-icon', 'title-icon--' + p.tone]" />
83   - <span>{{ p.title }}</span>
84   - </div>
85   - <el-table
86   - :data="p.list"
87   - border
88   - stripe
89   - size="small"
90   - :header-cell-style="tableHeaderStyle"
91   - class="channel-table cell-nowrap"
92   - empty-text= ""
93   - >
94   - <el-table-column label="" width="44" align="center">
95   - <template slot-scope="scope">
96   - <i :class="[p.rowIcon, 'row-ico', 'row-ico--' + p.rowTone]" />
97   - </template>
98   - </el-table-column>
99   - <el-table-column prop="qd" :label="p.colLabel" min-width="140" show-overflow-tooltip>
100   - <template slot-scope="scope">
101   - <span>{{ cellText(scope.row.qd) }}</span>
102   - </template>
103   - </el-table-column>
104   - <el-table-column prop="je" label="金额" width="130" align="right">
105   - <template slot-scope="scope">
106   - <span class="amount-cell">{{ formatMoney(scope.row.je) }}</span>
107   - </template>
108   - </el-table-column>
109   - </el-table>
110   - </div>
111   - </el-col>
112   - </el-row>
113   -
114   - <div class="subsection-title">付款侧</div>
115   - <el-row :gutter="16" class="channel-tables-row">
116   - <el-col v-for="p in fkPanels" :key="p.key" :xs="24" :lg="12">
117   - <div class="table-section channel-block">
118   - <div class="table-title-bar">
119   - <i :class="[p.icon, 'title-icon', 'title-icon--' + p.tone]" />
120   - <span>{{ p.title }}</span>
121   - </div>
122   - <el-table
123   - :data="p.list"
124   - border
125   - stripe
126   - size="small"
127   - :header-cell-style="tableHeaderStyle"
128   - class="channel-table cell-nowrap"
129   - empty-text= ""
130   - >
131   - <el-table-column label="" width="44" align="center">
132   - <template slot-scope="scope">
133   - <i :class="[p.rowIcon, 'row-ico', 'row-ico--' + p.rowTone]" />
134   - </template>
135   - </el-table-column>
136   - <el-table-column prop="qd" :label="p.colLabel" min-width="140" show-overflow-tooltip>
137   - <template slot-scope="scope">
138   - <span>{{ cellText(scope.row.qd) }}</span>
139   - </template>
140   - </el-table-column>
141   - <el-table-column prop="je" label="金额" width="130" align="right">
142   - <template slot-scope="scope">
143   - <span class="amount-cell">{{ formatMoney(scope.row.je) }}</span>
144   - </template>
145   - </el-table-column>
146   - </el-table>
147   - </div>
148   - </el-col>
149   - </el-row>
150   -
151   - <div class="table-section ledger-section">
152   - <div class="table-title-bar ledger-title-bar">
153   - <i class="el-icon-tickets title-icon title-icon--ledger" />
154   - <span>流水明细</span>
155   - </div>
156   - <div class="table-scroll">
157   - <el-table
158   - :data="ledgerList"
159   - border
160   - stripe
161   - v-loading="ledgerLoading"
162   - :header-cell-style="tableHeaderStyle"
163   - class="ledger-table cell-nowrap"
164   - >
165   - <el-table-column type="index" label="行号" width="56" :index="indexMethod" fixed />
166   - <el-table-column label="" width="40" align="center" fixed>
167   - <template slot-scope="scope">
168   - <i :class="ledgerRowIcon(scope.row)" />
169   - </template>
170   - </el-table-column>
171   - <el-table-column prop="djrq" label="单据日期" width="108" show-overflow-tooltip />
172   - <el-table-column prop="djbh" label="单据编号" min-width="140" show-overflow-tooltip>
173   - <template slot-scope="scope">
174   - <i class="el-icon-document row-ico row-ico--doc" />
175   - {{ cellText(scope.row.djbh) }}
176   - </template>
177   - </el-table-column>
178   - <el-table-column prop="djlx" label="单据类型" min-width="120" show-overflow-tooltip>
179   - <template slot-scope="scope">
180   - <i class="el-icon-files row-ico row-ico--type" />
181   - {{ cellText(scope.row.djlx) }}
182   - </template>
183   - </el-table-column>
184   - <el-table-column prop="fx" label="方向" width="72" show-overflow-tooltip>
185   - <template slot-scope="scope">
186   - <el-tag :type="scope.row.fx === '收款' ? 'success' : 'danger'" size="mini" effect="plain">{{
187   - cellText(scope.row.fx)
188   - }}</el-tag>
189   - </template>
190   - </el-table-column>
191   - <el-table-column prop="fffs" label="方式/渠道" min-width="120" show-overflow-tooltip>
192   - <template slot-scope="scope">
193   - <i class="el-icon-mobile-phone row-ico row-ico--fk" />
194   - {{ cellText(scope.row.fffs) }}
195   - </template>
196   - </el-table-column>
197   - <el-table-column prop="skzhMc" label="资金账户" min-width="140" show-overflow-tooltip>
198   - <template slot-scope="scope">
199   - <i
200   - :class="
201   - scope.row.fx === '收款'
202   - ? 'el-icon-wallet row-ico row-ico--sk'
203   - : 'el-icon-wallet row-ico row-ico--payacc'
204   - "
205   - />
206   - {{ cellText(scope.row.skzhMc) }}
207   - </template>
208   - </el-table-column>
209   - <el-table-column prop="je" label="金额" width="120" align="right">
210   - <template slot-scope="scope">
211   - <span :class="ledgerAmountClass(scope.row)">{{ formatMoney(scope.row.je) }}</span>
212   - </template>
213   - </el-table-column>
214   - <el-table-column prop="ly" label="来源" width="100" show-overflow-tooltip>
215   - <template slot-scope="scope">
216   - <i class="el-icon-link row-ico row-ico--ly" />
217   - {{ cellText(scope.row.ly) }}
218   - </template>
219   - </el-table-column>
220   - <el-table-column prop="ycddh" label="原单号" min-width="130" show-overflow-tooltip>
221   - <template slot-scope="scope">
222   - <i class="el-icon-back row-ico row-ico--ret" />
223   - {{ cellText(scope.row.ycddh) }}
224   - </template>
225   - </el-table-column>
226   - <el-table-column prop="jsr" label="经手人" width="88" show-overflow-tooltip />
227   - <el-table-column prop="zy" label="备注" min-width="160" show-overflow-tooltip />
228   - </el-table>
229   - <div class="pager-wrap">
230   - <el-pagination
231   - :current-page="pagination.currentPage"
232   - :page-sizes="[50, 100, 200, 500]"
233   - :page-size="pagination.pageSize"
234   - layout="total, sizes, prev, pager, next, jumper"
235   - :total="pagination.total"
236   - @size-change="handleSizeChange"
237   - @current-change="handleCurrentChange"
238   - />
239   - </div>
240   - </div>
241   - </div>
  118 + <el-table v-loading="ledgerLoading" :data="ledgerRows" border stripe class="cell-nowrap">
  119 + <el-table-column type="index" label="行号" width="56" align="center" />
  120 + <el-table-column prop="djrq" label="日期" width="110" align="center" />
  121 + <el-table-column prop="djbh" label="单据编号" min-width="150" show-overflow-tooltip />
  122 + <el-table-column prop="djlx" label="单据类型" min-width="140" show-overflow-tooltip />
  123 + <el-table-column prop="fx" label="方向" width="80" align="center">
  124 + <template slot-scope="scope">
  125 + <el-tag :type="scope.row.fx === '收款' ? 'success' : 'danger'" size="mini" effect="plain">{{ scope.row.fx }}</el-tag>
  126 + </template>
  127 + </el-table-column>
  128 + <el-table-column prop="fffs" label="方式" min-width="120" show-overflow-tooltip />
  129 + <el-table-column prop="skzhMc" label="账户" min-width="140" show-overflow-tooltip />
  130 + <el-table-column prop="je" label="金额" width="120" align="right">
  131 + <template slot-scope="scope">{{ formatMoney(scope.row.je) }}</template>
  132 + </el-table-column>
  133 + <el-table-column prop="jsr" label="经手人" width="90" show-overflow-tooltip />
  134 + <el-table-column prop="zy" label="备注" min-width="180" show-overflow-tooltip />
  135 + </el-table>
  136 + <div class="pager-wrap">
  137 + <el-pagination
  138 + :current-page="ledgerPagination.currentPage"
  139 + :page-size="ledgerPagination.pageSize"
  140 + :page-sizes="[50, 100, 200, 500]"
  141 + layout="total, sizes, prev, pager, next, jumper"
  142 + :total="ledgerPagination.total"
  143 + @size-change="handleLedgerSizeChange"
  144 + @current-change="handleLedgerPageChange"
  145 + />
242 146 </div>
243   - </div>
  147 + </el-dialog>
244 148 </div>
245 149 </template>
246 150  
247 151 <script>
248 152 import request from '@/utils/request'
  153 +import { getAccountSelector } from '@/api/extend/wtAccount'
249 154  
250 155 export default {
251 156 name: 'WtSkfkChannelStat',
252   -
253 157 data() {
254   - const y = new Date().getFullYear()
255   - const m = String(new Date().getMonth() + 1).padStart(2, '0')
256   - const start = `${y}-${m}-01`
257   - const end = `${y}-${m}-${String(new Date(y, new Date().getMonth() + 1, 0).getDate()).padStart(2, '0')}`
258 158 return {
259   - showAll: false,
260 159 summaryLoading: false,
261 160 filters: {
262   - dateRange: [start, end],
263   - fx: '',
264   - qd: '',
265   - djlx: ''
  161 + date: this.today(),
  162 + accountId: ''
266 163 },
267   - skAccountList: [],
268   - skMethodList: [],
269   - fkAccountList: [],
270   - fkMethodList: [],
271   - skSideTotal: 0,
272   - fkSideTotal: 0,
273   - ledgerList: [],
  164 + accountOptions: [],
  165 + tableRows: [],
  166 + ledgerDialogVisible: false,
  167 + currentLedgerAccountName: '',
274 168 ledgerLoading: false,
275   - pagination: {
  169 + ledgerRows: [],
  170 + ledgerFilters: {
  171 + dateRange: this.defaultLedgerRange(),
  172 + accountId: ''
  173 + },
  174 + ledgerPagination: {
276 175 currentPage: 1,
277 176 pageSize: 100,
278 177 total: 0
279   - },
280   - tableHeaderStyle: { background: '#f5f7fa' }
281   - }
282   - },
283   -
284   - computed: {
285   - skPanels() {
286   - return [
287   - {
288   - key: 'skacc',
289   - title: '入账账户汇总',
290   - icon: 'el-icon-coin',
291   - tone: 'sk',
292   - rowIcon: 'el-icon-coin',
293   - rowTone: 'sk',
294   - colLabel: '入账账户',
295   - list: this.skAccountList
296   - },
297   - {
298   - key: 'skmeth',
299   - title: '买方付款方式汇总',
300   - icon: 'el-icon-bank-card',
301   - tone: 'fk',
302   - rowIcon: 'el-icon-bank-card',
303   - rowTone: 'fk',
304   - colLabel: '付款方式',
305   - list: this.skMethodList
306   - }
307   - ]
308   - },
309   - fkPanels() {
310   - return [
311   - {
312   - key: 'fkacc',
313   - title: '付款账户汇总',
314   - icon: 'el-icon-wallet',
315   - tone: 'payacc',
316   - rowIcon: 'el-icon-wallet',
317   - rowTone: 'payacc',
318   - colLabel: '付款账户',
319   - list: this.fkAccountList
320   - },
321   - {
322   - key: 'fkmeth',
323   - title: '方式/渠道汇总',
324   - icon: 'el-icon-sort',
325   - tone: 'fk',
326   - rowIcon: 'el-icon-sort',
327   - rowTone: 'fk',
328   - colLabel: '方式',
329   - list: this.fkMethodList
330   - }
331   - ]
  178 + }
332 179 }
333 180 },
334   -
335 181 methods: {
336   - indexMethod(index) {
337   - return (this.pagination.currentPage - 1) * this.pagination.pageSize + index + 1
  182 + today() {
  183 + const d = new Date()
  184 + const y = d.getFullYear()
  185 + const m = String(d.getMonth() + 1).padStart(2, '0')
  186 + const day = String(d.getDate()).padStart(2, '0')
  187 + return `${y}-${m}-${day}`
338 188 },
339   - cellText(val) {
340   - if (val === null || val === undefined || val === '') return ''
341   - return String(val)
  189 + defaultLedgerRange() {
  190 + const d = new Date()
  191 + const y = d.getFullYear()
  192 + const today = this.today()
  193 + return [`${y}-01-01`, today]
342 194 },
343 195 formatMoney(val) {
344 196 if (val === null || val === undefined || val === '') return ''
... ... @@ -346,323 +198,178 @@ export default {
346 198 if (Number.isNaN(n)) return String(val)
347 199 return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
348 200 },
349   - ledgerRowIcon(row) {
350   - if (row && row.fx === '收款') return 'el-icon-top row-ico row-ico--sk'
351   - return 'el-icon-bottom row-ico row-ico--fk'
  201 + rowClassName({ row }) {
  202 + return row.isCategory ? 'category-row' : ''
352 203 },
353   - ledgerAmountClass(row) {
354   - if (row && row.fx === '收款') return 'amount-cell amount-cell--sk'
355   - return 'amount-cell amount-cell--fk'
  204 + async fetchAccountOptions() {
  205 + const res = await getAccountSelector()
  206 + const list = (res.data && res.data.list) ? res.data.list : []
  207 + this.accountOptions = list
356 208 },
357   - buildDatePayload() {
358   - const p = {}
359   - if (this.filters.dateRange && this.filters.dateRange.length === 2) {
360   - p.startDate = this.filters.dateRange[0]
361   - p.endDate = this.filters.dateRange[1]
362   - }
363   - return p
364   - },
365   - fetchSummary() {
366   - this.summaryLoading = true
367   - const data = this.buildDatePayload()
368   - return request({
369   - url: '/api/Extend/WtSkfkChannelStat/Actions/GetChannelSummary',
370   - method: 'GET',
371   - data
372   - })
373   - .then(res => {
374   - const body = res.data || {}
375   - this.skAccountList = Array.isArray(body.skAccountList) ? body.skAccountList : []
376   - this.skMethodList = Array.isArray(body.skMethodList) ? body.skMethodList : []
377   - this.fkAccountList = Array.isArray(body.fkAccountList) ? body.fkAccountList : []
378   - this.fkMethodList = Array.isArray(body.fkMethodList) ? body.fkMethodList : []
379   - this.skSideTotal = body.skSideTotal != null ? Number(body.skSideTotal) : 0
380   - this.fkSideTotal = body.fkSideTotal != null ? Number(body.fkSideTotal) : 0
  209 + buildTableRows(groups, statDate) {
  210 + const rows = []
  211 + ;(groups || []).forEach(group => {
  212 + rows.push({
  213 + rowKey: `cat-${group.category}`,
  214 + isCategory: true,
  215 + date: '',
  216 + account: group.category,
  217 + openingBalance: group.totalOpeningBalance,
  218 + todayIncome: group.totalTodayIncome,
  219 + todayExpense: group.totalTodayExpense,
  220 + periodBalance: group.totalPeriodBalance,
  221 + realtimeBalance: group.totalRealtimeBalance
381 222 })
382   - .catch(() => {
383   - this.$message.error('加载汇总失败,请稍后重试')
  223 + ;(group.rows || []).forEach(item => {
  224 + rows.push({
  225 + rowKey: `acc-${item.accountId}`,
  226 + isCategory: false,
  227 + accountId: item.accountId,
  228 + date: statDate || item.date,
  229 + account: item.account,
  230 + openingBalance: item.openingBalance,
  231 + todayIncome: item.todayIncome,
  232 + todayExpense: item.todayExpense,
  233 + periodBalance: item.periodBalance,
  234 + realtimeBalance: item.realtimeBalance
  235 + })
384 236 })
385   - .finally(() => {
386   - this.summaryLoading = false
  237 + })
  238 + this.tableRows = rows
  239 + },
  240 + async fetchSummary() {
  241 + this.summaryLoading = true
  242 + try {
  243 + const data = {
  244 + date: this.filters.date,
  245 + accountId: this.filters.accountId || undefined
  246 + }
  247 + const res = await request({
  248 + url: '/api/Extend/WtSkfkChannelStat/Actions/GetAccountReconcileSummary',
  249 + method: 'GET',
  250 + data
387 251 })
  252 + const body = res.data || {}
  253 + const groups = Array.isArray(body.categoryGroups) ? body.categoryGroups : []
  254 + this.buildTableRows(groups, body.date || this.filters.date)
  255 + } catch (e) {
  256 + this.$message.error('加载账户对账表失败')
  257 + this.tableRows = []
  258 + } finally {
  259 + this.summaryLoading = false
  260 + }
388 261 },
389   - fetchLedger() {
390   - this.ledgerLoading = true
  262 + buildLedgerPayload() {
391 263 const data = {
392   - ...this.buildDatePayload(),
393   - currentPage: this.pagination.currentPage,
394   - pageSize: this.pagination.pageSize
  264 + currentPage: this.ledgerPagination.currentPage,
  265 + pageSize: this.ledgerPagination.pageSize,
  266 + accountId: this.ledgerFilters.accountId || undefined
395 267 }
396   - if (this.filters.fx) data.fx = this.filters.fx
397   - if (this.filters.qd && this.filters.qd.trim()) data.qd = this.filters.qd.trim()
398   - if (this.filters.djlx && this.filters.djlx.trim()) data.djlx = this.filters.djlx.trim()
399   -
400   - return request({
401   - url: '/api/Extend/WtSkfkChannelStat/Actions/GetChannelLedger',
402   - method: 'GET',
403   - data
404   - })
405   - .then(res => {
406   - const body = res.data || {}
407   - this.ledgerList = Array.isArray(body.list) ? body.list : []
408   - this.pagination.total = body.total != null ? Number(body.total) : 0
409   - })
410   - .catch(() => {
411   - this.$message.error('加载流水明细失败,请稍后重试')
412   - this.ledgerList = []
413   - this.pagination.total = 0
414   - })
415   - .finally(() => {
416   - this.ledgerLoading = false
  268 + if (this.ledgerFilters.dateRange && this.ledgerFilters.dateRange.length === 2) {
  269 + data.startDate = this.ledgerFilters.dateRange[0]
  270 + data.endDate = this.ledgerFilters.dateRange[1]
  271 + }
  272 + return data
  273 + },
  274 + async fetchLedger() {
  275 + this.ledgerLoading = true
  276 + try {
  277 + const res = await request({
  278 + url: '/api/Extend/WtSkfkChannelStat/Actions/GetAccountReconcileLedger',
  279 + method: 'GET',
  280 + data: this.buildLedgerPayload()
417 281 })
  282 + const body = res.data || {}
  283 + this.ledgerRows = Array.isArray(body.list) ? body.list : []
  284 + this.ledgerPagination.total = body.total ? Number(body.total) : 0
  285 + } catch (e) {
  286 + this.$message.error('加载账户明细失败')
  287 + this.ledgerRows = []
  288 + this.ledgerPagination.total = 0
  289 + } finally {
  290 + this.ledgerLoading = false
  291 + }
418 292 },
419   - refreshAll() {
420   - return Promise.all([this.fetchSummary(), this.fetchLedger()])
  293 + openLedgerDialog(row) {
  294 + if (!row || row.isCategory || !row.accountId) return
  295 + this.currentLedgerAccountName = row.account || ''
  296 + this.ledgerFilters.accountId = row.accountId
  297 + this.ledgerFilters.dateRange = this.defaultLedgerRange()
  298 + this.ledgerPagination.currentPage = 1
  299 + this.ledgerDialogVisible = true
  300 + this.fetchLedger()
421 301 },
422   - handleSearch() {
423   - this.pagination.currentPage = 1
424   - this.refreshAll()
  302 + handleLedgerSearch() {
  303 + this.ledgerPagination.currentPage = 1
  304 + this.fetchLedger()
425 305 },
426   - handleReset() {
427   - const y = new Date().getFullYear()
428   - const m = String(new Date().getMonth() + 1).padStart(2, '0')
429   - const start = `${y}-${m}-01`
430   - const end = `${y}-${m}-${String(new Date(y, new Date().getMonth() + 1, 0).getDate()).padStart(2, '0')}`
431   - this.filters = {
432   - dateRange: [start, end],
433   - fx: '',
434   - qd: '',
435   - djlx: ''
  306 + handleLedgerReset() {
  307 + this.ledgerFilters = {
  308 + dateRange: this.defaultLedgerRange(),
  309 + accountId: ''
436 310 }
437   - this.showAll = false
438   - this.pagination.currentPage = 1
439   - this.refreshAll()
  311 + this.ledgerPagination.currentPage = 1
  312 + this.fetchLedger()
440 313 },
441   - handleSizeChange(size) {
442   - this.pagination.pageSize = size
443   - this.pagination.currentPage = 1
  314 + handleLedgerSizeChange(size) {
  315 + this.ledgerPagination.pageSize = size
  316 + this.ledgerPagination.currentPage = 1
444 317 this.fetchLedger()
445 318 },
446   - handleCurrentChange(page) {
447   - this.pagination.currentPage = page
  319 + handleLedgerPageChange(page) {
  320 + this.ledgerPagination.currentPage = page
448 321 this.fetchLedger()
  322 + },
  323 + handleSearch() {
  324 + this.fetchSummary()
  325 + },
  326 + handleReset() {
  327 + this.filters = {
  328 + date: this.today(),
  329 + accountId: ''
  330 + }
  331 + this.fetchSummary()
449 332 }
450 333 },
451   -
452   - created() {
453   - this.refreshAll()
  334 + async created() {
  335 + await this.fetchAccountOptions()
  336 + await this.fetchSummary()
454 337 }
455 338 }
456 339 </script>
457 340  
458 341 <style scoped lang="scss">
459   -.skfk-channel-stat-page {
460   - &.NCC-common-layout {
461   - background: #ebeef5;
462   - }
463   -
464   - &__main {
465   - padding: 10px 16px 16px;
466   - overflow: auto;
467   - box-sizing: border-box;
468   - }
469   -}
470   -
471   -.summary-wrap {
472   - min-height: 120px;
473   - margin-bottom: 4px;
474   -}
475   -
476   -.summary-cards-row {
477   - margin-bottom: 10px;
478   -}
479   -
480   -.subsection-title {
481   - font-size: 15px;
  342 +.title-line {
  343 + margin: 0 0 8px 2px;
  344 + font-size: 16px;
482 345 font-weight: 600;
483 346 color: #303133;
484   - margin: 14px 0 8px 2px;
485   - padding-left: 8px;
486   - border-left: 3px solid #409eff;
487 347 }
488 348  
489   -.stat-card {
490   - height: 100px;
491   - border-radius: 12px;
492   - padding: 12px;
493   - box-sizing: border-box;
494   - display: flex;
495   - align-items: center;
496   - gap: 12px;
497   - margin-bottom: 10px;
498   - background: #fff;
499   - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
500   -
501   - &--sk {
502   - border-left: 4px solid #67c23a;
503   - }
504   -
505   - &--fk {
506   - border-left: 4px solid #f56c6c;
507   - }
508   -
509   - &__icon {
510   - font-size: 36px;
511   - color: #409eff;
512   -
513   - &--sk {
514   - color: #67c23a;
515   - }
516   -
517   - &--fk {
518   - color: #f56c6c;
519   - }
520   - }
521   -
522   - &__body {
523   - flex: 1;
524   - display: flex;
525   - flex-direction: column;
526   - justify-content: center;
527   - min-width: 0;
528   - }
529   -
530   - &__label {
531   - font-size: 13px;
532   - color: #909399;
533   - margin-bottom: 6px;
534   - line-height: 1.3;
535   - }
536   -
537   - &__value {
538   - font-size: 22px;
539   - font-weight: 600;
540   - color: #303133;
541   - font-variant-numeric: tabular-nums;
542   - }
543   -}
544   -
545   -.action-row {
546   - display: inline-flex;
547   - flex-wrap: wrap;
548   - justify-content: flex-start;
549   - align-items: center;
550   - gap: 8px;
551   -}
552   -
553   -.table-section {
554   - background: #fff;
555   - border-radius: 12px;
556   - padding: 12px 12px 10px 12px;
557   - margin: 0;
558   - width: 100%;
559   - box-sizing: border-box;
560   - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
  349 +.cell-nowrap ::v-deep .cell {
  350 + white-space: nowrap;
561 351 }
562 352  
563   -.channel-block {
564   - margin-bottom: 12px;
  353 +.reconcile-table ::v-deep .el-table__header th {
  354 + background: #f5f7fa;
565 355 }
566 356  
567   -.table-title-bar {
568   - font-size: 15px;
  357 +.category-text {
569 358 font-weight: 600;
570   - margin-bottom: 10px;
571   - display: flex;
572   - align-items: center;
573   - flex-wrap: wrap;
574   - gap: 8px;
575   -}
576   -
577   -.ledger-title-bar {
578   - margin-bottom: 12px;
579   -}
580   -
581   -.title-icon {
582   - font-size: 20px;
583   - margin-right: 2px;
584   -
585   - &--sk {
586   - color: #67c23a;
587   - }
588   -
589   - &--fk {
590   - color: #f56c6c;
591   - }
592   -
593   - &--payacc {
594   - color: #e6a23c;
595   - }
596   -
597   - &--ledger {
598   - color: #409eff;
599   - }
600   -}
601   -
602   -.row-ico {
603   - margin-right: 4px;
604   -
605   - &--sk {
606   - color: #67c23a;
607   - }
608   -
609   - &--fk {
610   - color: #f56c6c;
611   - }
612   -
613   - &--doc {
614   - color: #409eff;
615   - }
616   -
617   - &--type {
618   - color: #e6a23c;
619   - }
620   -
621   - &--ly {
622   - color: #909399;
623   - }
624   -
625   - &--ret {
626   - color: #f56c6c;
627   - }
628   -
629   - &--payacc {
630   - color: #e6a23c;
631   - }
  359 + color: #303133;
632 360 }
633 361  
634   -.amount-cell {
635   - font-variant-numeric: tabular-nums;
636   - font-weight: 500;
637   -
638   - &--sk {
639   - color: #67c23a;
640   - }
641   -
642   - &--fk {
643   - color: #f56c6c;
644   - }
  362 +.account-link {
  363 + color: #303133;
645 364 }
646 365  
647   -.table-scroll {
648   - max-height: none;
  366 +::v-deep .category-row > td {
  367 + background: #f5f7fa !important;
649 368 }
650 369  
651 370 .pager-wrap {
652 371 display: flex;
653 372 justify-content: flex-start;
654   - padding: 12px 0 4px;
655   -}
656   -
657   -.cell-nowrap ::v-deep .cell {
658   - white-space: nowrap;
659   -}
660   -
661   -.channel-tables-row {
662   - margin-bottom: 4px;
663   -}
664   -
665   -.ledger-section {
666   - margin-top: 8px;
  373 + padding-top: 12px;
667 374 }
668 375 </style>
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtSp/Form.vue
... ... @@ -686,8 +686,9 @@
686 686 }
687 687 })
688 688 })
689   - .catch(() => {
690   - this.$message({ message: '提交失败,请检查数据格式', type: 'error', duration: 3000 })
  689 + .catch((err) => {
  690 + const msg = (err && err.message) ? err.message : '提交失败,请检查数据格式'
  691 + this.$message({ message: msg, type: 'error', duration: 3000 })
691 692 })
692 693 } else {
693 694 request({
... ... @@ -706,8 +707,9 @@
706 707 }
707 708 })
708 709 })
709   - .catch(() => {
710   - this.$message({ message: '更新失败,请检查数据格式', type: 'error', duration: 3000 })
  710 + .catch((err) => {
  711 + const msg = (err && err.message) ? err.message : '更新失败,请检查数据格式'
  712 + this.$message({ message: msg, type: 'error', duration: 3000 })
711 713 })
712 714 }
713 715 })
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtSp/index.vue
... ... @@ -5,7 +5,7 @@
5 5 <el-form @submit.native.prevent>
6 6 <el-col :span="6">
7 7 <el-form-item label="商品名称">
8   - <el-input v-model="query.spmc" placeholder="商品名称" clearable />
  8 + <el-input v-model="query.spmc" placeholder="商品名称" clearable @keyup.enter.native="search()" />
9 9 </el-form-item>
10 10 </el-col>
11 11 <el-col :span="6">
... ... @@ -25,7 +25,7 @@
25 25 <template v-if="showAll">
26 26 <el-col :span="6">
27 27 <el-form-item label="商品编码">
28   - <el-input v-model="query.spbm" placeholder="商品编码" clearable />
  28 + <el-input v-model="query.spbm" placeholder="商品编码" clearable @keyup.enter.native="search()" />
29 29 </el-form-item>
30 30 </el-col>
31 31 <el-col :span="6">
... ... @@ -47,7 +47,7 @@
47 47 </el-col>
48 48 <el-col :span="6">
49 49 <el-form-item label="售后规则">
50   - <el-input v-model="query.shgz" placeholder="如:质保 1 年 / 7 天无理由" />
  50 + <el-input v-model="query.shgz" placeholder="如:质保 1 年 / 7 天无理由" clearable @keyup.enter.native="search()" />
51 51 </el-form-item>
52 52 </el-col>
53 53 <el-col :span="6">
... ... @@ -64,6 +64,11 @@
64 64 </el-select>
65 65 </el-form-item>
66 66 </el-col>
  67 + <el-col :span="6">
  68 + <el-form-item label="抖音SKU">
  69 + <el-input v-model="query.dyspid" placeholder="抖音SKU" clearable @keyup.enter.native="search()" />
  70 + </el-form-item>
  71 + </el-col>
67 72 </template>
68 73 <el-col :span="6">
69 74 <el-form-item>
... ... @@ -81,6 +86,8 @@
81 86 <el-button type="primary" icon="el-icon-plus" @click="addOrUpdateHandle()">新增</el-button>
82 87 <el-button type="text" icon="el-icon-download" @click="exportData()">导出</el-button>
83 88 <el-button type="text" icon="el-icon-delete" @click="handleBatchRemoveDel()">批量删除</el-button>
  89 + <el-button type="text" icon="el-icon-postcard" @click="openBatchSetHyxzDialog">批量设置权益卡</el-button>
  90 + <el-button type="text" icon="el-icon-close" @click="handleBatchClearHyxz">批量取消权益卡</el-button>
84 91 </div>
85 92 <div class="NCC-common-head-right">
86 93 <el-tooltip effect="dark" content="刷新" placement="top">
... ... @@ -162,6 +169,34 @@
162 169 </div>
163 170 <NCC-Form v-if="formVisible" ref="NCCForm" @refresh="refresh" />
164 171 <ExportBox v-if="exportBoxVisible" ref="ExportBox" @download="download" />
  172 + <el-dialog
  173 + title="批量设置权益卡"
  174 + :visible.sync="batchSetHyxzVisible"
  175 + width="560px"
  176 + :close-on-click-modal="false"
  177 + >
  178 + <el-form size="small" label-width="100px">
  179 + <el-form-item label="权益卡">
  180 + <el-select
  181 + v-model="batchSetHyxzCardIds"
  182 + multiple
  183 + filterable
  184 + clearable
  185 + placeholder="请选择权益卡"
  186 + style="width: 100%"
  187 + >
  188 + <el-option v-for="card in hyxzCardOptions" :key="card.id" :label="card.kjmc" :value="card.id" />
  189 + </el-select>
  190 + </el-form-item>
  191 + <div class="wt-sp-batch-tip">
  192 + 仅对当前勾选的商品生效。
  193 + </div>
  194 + </el-form>
  195 + <span slot="footer" class="dialog-footer">
  196 + <el-button @click="batchSetHyxzVisible = false">取 消</el-button>
  197 + <el-button type="primary" :loading="batchHyxzLoading" @click="handleBatchSetHyxz">确 定</el-button>
  198 + </span>
  199 + </el-dialog>
165 200 </div>
166 201 </template>
167 202 <script>
... ... @@ -186,6 +221,7 @@
186 221 shgz:undefined,
187 222 xsqd:undefined,
188 223 xsmd:undefined,
  224 + dyspid:undefined,
189 225 },
190 226 list: [],
191 227 listLoading: true,
... ... @@ -198,6 +234,9 @@
198 234 },
199 235 formVisible: false,
200 236 exportBoxVisible: false,
  237 + batchSetHyxzVisible: false,
  238 + batchSetHyxzCardIds: [],
  239 + batchHyxzLoading: false,
201 240 columnList: [
202 241 { prop: 'spmc', label: '商品名称' },
203 242 { prop: 'pl', label: '商品品类' },
... ... @@ -216,6 +255,7 @@
216 255 spxlhTypeOptions:[{"fullName":"入1出1","id":"1"},{"fullName":"入0出1","id":"2"},{"fullName":"入0出0","id":"3"}],
217 256 xsqdOptions:[{"fullName":"门店组","id":"1"},{"fullName":"网店组","id":"2"}],
218 257 mdfzOptions : [],
  258 + hyxzCardOptions: [],
219 259 }
220 260 },
221 261 computed: {},
... ... @@ -224,6 +264,7 @@
224 264 this.getplOptions();
225 265 this.getppOptions();
226 266 this.getMdfzOptions();
  267 + this.getHyxzCardOptions();
227 268 },
228 269 methods: {
229 270 getplOptions(){
... ... @@ -244,6 +285,76 @@
244 285 this.mdfzOptions = res.data || []
245 286 }).catch(() => { this.mdfzOptions = [] })
246 287 },
  288 + getHyxzCardOptions() {
  289 + request({
  290 + url: '/api/Extend/WtHyKjqy',
  291 + method: 'GET',
  292 + data: { currentPage: 1, pageSize: 200 }
  293 + })
  294 + .then(res => {
  295 + this.hyxzCardOptions = res.data && res.data.list ? res.data.list : []
  296 + })
  297 + .catch(() => {
  298 + this.hyxzCardOptions = []
  299 + })
  300 + },
  301 + openBatchSetHyxzDialog() {
  302 + this.batchSetHyxzCardIds = []
  303 + this.batchSetHyxzVisible = true
  304 + },
  305 + handleBatchSetHyxz() {
  306 + if (!this.multipleSelection.length) {
  307 + this.$message.warning('请先勾选需要设置权益卡的商品')
  308 + return
  309 + }
  310 + if (!this.batchSetHyxzCardIds.length) {
  311 + this.$message.warning('请至少选择一张权益卡')
  312 + return
  313 + }
  314 + this.batchHyxzLoading = true
  315 + request({
  316 + url: '/api/Extend/WtSp/BatchSetHyxz',
  317 + method: 'POST',
  318 + data: {
  319 + ids: this.multipleSelection,
  320 + hyxzCardIds: this.batchSetHyxzCardIds
  321 + }
  322 + })
  323 + .then(res => {
  324 + this.$message.success((res.data && res.data.message) || res.msg || '批量设置成功')
  325 + this.batchSetHyxzVisible = false
  326 + this.initData()
  327 + })
  328 + .finally(() => {
  329 + this.batchHyxzLoading = false
  330 + })
  331 + },
  332 + handleBatchClearHyxz() {
  333 + if (!this.multipleSelection.length) {
  334 + this.$message.warning('请先勾选需要取消权益卡的商品')
  335 + return
  336 + }
  337 + const submit = () => {
  338 + this.batchHyxzLoading = true
  339 + request({
  340 + url: '/api/Extend/WtSp/BatchClearHyxz',
  341 + method: 'POST',
  342 + data: {
  343 + ids: this.multipleSelection
  344 + }
  345 + })
  346 + .then(res => {
  347 + this.$message.success((res.data && res.data.message) || res.msg || '批量取消成功')
  348 + this.initData()
  349 + })
  350 + .finally(() => {
  351 + this.batchHyxzLoading = false
  352 + })
  353 + }
  354 + this.$confirm('确认对已勾选商品批量取消权益卡吗?', '提示', { type: 'warning' })
  355 + .then(submit)
  356 + .catch(() => {})
  357 + },
247 358 initData() {
248 359 this.listLoading = true;
249 360 let _query = {
... ... @@ -448,4 +559,9 @@
448 559 .wt-sp-col-icon--info {
449 560 color: #909399;
450 561 }
  562 +.wt-sp-batch-tip {
  563 + color: #909399;
  564 + font-size: 12px;
  565 + line-height: 1.5;
  566 +}
451 567 </style>
452 568 \ No newline at end of file
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtTjdbd/detail-view.vue
... ... @@ -149,13 +149,55 @@ export default {
149 149 },
150 150 resolveBookStock(row) {
151 151 if (!row || typeof row !== 'object') return ''
152   - // 优先展示单据明细上的库存快照字段,避免显示实时库存
153   - const candidates = [row.kucun, row.kc, row.kcsl, row.stockQty, row.stock, row.mdxx]
  152 + // 明细接口无持久化 kucun:由 enrichMxBookStock 拉取后写入;兼容 PascalCase / 旧字段
  153 + const candidates = [
  154 + row.kucun,
  155 + row.Kucun,
  156 + row.kc,
  157 + row.Kc,
  158 + row.kcsl,
  159 + row.Kcsl,
  160 + row.stockQty,
  161 + row.StockQty,
  162 + row.stock,
  163 + row.Stock,
  164 + row.mdxx,
  165 + row.Mdxx
  166 + ]
154 167 for (let i = 0; i < candidates.length; i++) {
155 168 if (!this.isBlankValue(candidates[i])) return String(candidates[i])
156 169 }
157 170 return ''
158 171 },
  172 + /**
  173 + * 与编辑页一致:账面库存来自 wt_sp_cost 汇总接口;GetInfo 仅返回明细表字段,不含 kucun。
  174 + * ckck 已被后端转为门店/仓库名称时,GetStockQuantity 仍支持按名称反查(见服务端注释)。
  175 + */
  176 + async enrichMxBookStock(detail) {
  177 + if (!detail || !Array.isArray(detail.wtXsckdMxList)) return
  178 + const rows = detail.wtXsckdMxList
  179 + await Promise.all(
  180 + rows.map(async row => {
  181 + const spbh = row.spbh != null ? row.spbh : row.Spbh
  182 + const ck = row.ckck != null ? row.ckck : row.Ckck
  183 + if (this.isBlankValue(spbh) || this.isBlankValue(ck)) return
  184 + try {
  185 + const url = `/api/Extend/WtXsckd/GetStockQuantity?productId=${encodeURIComponent(String(spbh).trim())}&warehouseId=${encodeURIComponent(String(ck).trim())}`
  186 + const response = await request({
  187 + url,
  188 + method: 'get'
  189 + })
  190 + const inner = response && response.data
  191 + if (inner && inner.success) {
  192 + const qty = inner.data != null ? inner.data : 0
  193 + this.$set(row, 'kucun', qty)
  194 + }
  195 + } catch (e) {
  196 + // 静默失败,保持空;避免详情弹窗刷屏
  197 + }
  198 + })
  199 + )
  200 + },
159 201 auditStatus(row) {
160 202 if (!row) return ''
161 203 const s = String(row.djzt || row.shzt || '').trim()
... ... @@ -222,6 +264,7 @@ export default {
222 264 detail.wtXsckdMxList = Array.isArray(detail.wtXsckdMxList) ? detail.wtXsckdMxList : []
223 265 this.detail = detail
224 266 this.$emit('loaded', detail)
  267 + this.enrichMxBookStock(detail)
225 268 }).catch(() => {
226 269 this.$message.error('加载详情失败')
227 270 this.visible = false
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtXsckd/SerialNumberSelect.vue
... ... @@ -32,8 +32,17 @@
32 32 <el-form-item label="序列号">
33 33 <el-input v-model="searchForm.serialNumber" placeholder="支持模糊查询"></el-input>
34 34 </el-form-item>
  35 + <el-form-item label="扫码录入">
  36 + <el-input
  37 + v-model="scanSerialNumber"
  38 + placeholder="扫码枪扫入后回车"
  39 + clearable
  40 + @keyup.enter.native="handleScanInput"
  41 + ></el-input>
  42 + </el-form-item>
35 43 <el-form-item>
36 44 <el-button type="primary" @click="searchSerialNumbers">查询</el-button>
  45 + <el-button type="success" @click="handleScanInput">扫码添加</el-button>
37 46 <el-button @click="resetSearch">重置</el-button>
38 47 </el-form-item>
39 48 </el-form>
... ... @@ -117,6 +126,8 @@ export default {
117 126 currentWarehouse: '',
118 127 currentDocumentType: '',
119 128 productCodeDisplay: '' // 商品编码(用于界面显示,如 206466)
  129 + ,
  130 + scanSerialNumber: ''
120 131 }
121 132 },
122 133 computed: {
... ... @@ -139,6 +150,7 @@ export default {
139 150 this.searchForm.productCode = productCode
140 151 this.searchForm.warehouse = warehouse
141 152 this.searchForm.serialNumber = '' // Reset serial number when opening
  153 + this.scanSerialNumber = ''
142 154 this.selectedSerialNumbers = [...selectedSerialNumbers] // 用于回显
143 155 this.searchSerialNumbers().then(() => {
144 156 this.$nextTick(() => {
... ... @@ -153,6 +165,7 @@ export default {
153 165 this.serialNumberList = []
154 166 this.selectedSerialNumbers = []
155 167 this.productCodeDisplay = ''
  168 + this.scanSerialNumber = ''
156 169 },
157 170  
158 171 // 获取仓库选项
... ... @@ -243,6 +256,42 @@ export default {
243 256 this.handleClose()
244 257 },
245 258  
  259 + // 扫码录入:优先从当前列表中精确匹配并勾选;未命中时自动按序列号查询后再匹配
  260 + async handleScanInput() {
  261 + const sn = String(this.scanSerialNumber || '').trim()
  262 + if (!sn) {
  263 + this.$message.warning('请先扫码或输入序列号')
  264 + return
  265 + }
  266 + const trySelectBySerial = (serial) => {
  267 + if (!this.$refs.serialTable || !Array.isArray(this.serialNumberList)) return false
  268 + const row = this.serialNumberList.find(item => String(item.serialNumber || '').trim() === serial)
  269 + if (!row) return false
  270 + this.$refs.serialTable.toggleRowSelection(row, true)
  271 + if (!this.selectedSerialNumbers.includes(serial)) {
  272 + this.selectedSerialNumbers.push(serial)
  273 + }
  274 + return true
  275 + }
  276 +
  277 + // 先从现有结果匹配
  278 + if (trySelectBySerial(sn)) {
  279 + this.scanSerialNumber = ''
  280 + return
  281 + }
  282 +
  283 + // 现有列表无,按该序列号精确查询一次
  284 + this.searchForm.serialNumber = sn
  285 + await this.searchSerialNumbers()
  286 + this.$nextTick(() => {
  287 + const ok = trySelectBySerial(sn)
  288 + if (!ok) {
  289 + this.$message.warning(`未找到序列号:${sn}`)
  290 + }
  291 + this.scanSerialNumber = ''
  292 + })
  293 + },
  294 +
246 295 // 获取仓库名称
247 296 getWarehouseName(warehouseId) {
248 297 const warehouse = this.warehouseOptions.find(item => item.F_Id === warehouseId)
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtXsckd/index.vue
... ... @@ -117,6 +117,9 @@
117 117 <el-table-column prop="jsr" label="经手人" align="left" min-width="88" show-overflow-tooltip>
118 118 <template slot-scope="scope">{{ displayText(scope.row.jsr) }}</template>
119 119 </el-table-column>
  120 + <el-table-column prop="kh" label="往来单位" align="left" min-width="120" show-overflow-tooltip>
  121 + <template slot-scope="scope">{{ displayText(scope.row.kh) }}</template>
  122 + </el-table-column>
120 123 <el-table-column prop="fhr" label="发货人" align="left" min-width="88" show-overflow-tooltip>
121 124 <template slot-scope="scope">{{ displayText(scope.row.fhr) }}</template>
122 125 </el-table-column>
... ... @@ -166,7 +169,7 @@
166 169 <el-tag v-if="scope.row.djzt === '已审核'" type="success">已审核</el-tag>
167 170 <el-tag v-else-if="scope.row.djzt === '一级已审'" type="">一级已审</el-tag>
168 171 <el-tag v-else-if="scope.row.djzt === '草稿'" type="info">草稿</el-tag>
169   - <el-tag v-else-if="isSkipErpAuditSource(scope.row)" type="success" effect="plain">无需审核</el-tag>
  172 + <el-tag v-else-if="isSkipErpAuditSource(scope.row) && (!scope.row.djzt || scope.row.djzt === '已审核')" type="success" effect="plain">无需审核</el-tag>
170 173 <el-tag v-else-if="scope.row.djzt === '待审核'" type="warning">待审核</el-tag>
171 174 <el-tag v-else-if="scope.row.djzt === '审核不通过'" type="danger">审核不通过</el-tag>
172 175 <el-tag v-else type="warning">{{ scope.row.djzt || '待审核' }}</el-tag>
... ... @@ -250,6 +253,7 @@
250 253 { prop: 'djrq', label: '单据日期' },
251 254 { prop: 'cjck', label: '出库仓库' },
252 255 { prop: 'jsr', label: '经手人' },
  256 + { prop: 'kh', label: '往来单位' },
253 257 { prop: 'fhr', label: '发货人' },
254 258 { prop: 'skzh', label: '收款账户' },
255 259 { prop: 'skje', label: '收款金额' },
... ... @@ -309,28 +313,25 @@
309 313 },
310 314 /** 后台来源且草稿可编辑 */
311 315 canEditDraft(row) {
312   - return !this.isSkipErpAuditSource(row) && (row.djzt === '草稿' || row.djzt === '审核不通过')
  316 + return row && (row.djzt === '草稿' || row.djzt === '审核不通过')
313 317 },
314   - /** 抖音来源已审核:允许反审(回退为待审核),用于更正与重提 */
  318 + /** 非后台来源(抖音/门店)已审核:允许反审(回退草稿),用于更正与重提 */
315 319 canReverseApproval(row) {
316 320 if (!row) return false
317   - // 仅对抖音来源(含早期只写备注的单)开放反审入口
318   - const isDy = (row.ly != null && String(row.ly).trim() === '抖音订单') || this.isSkipErpAuditSource(row)
319   - if (!isDy) return false
  321 + // 非后台来源(含早期只写备注的抖音单)开放反审入口
  322 + const isNonBackend = this.isSkipErpAuditSource(row)
  323 + if (!isNonBackend) return false
320 324 return row.djzt === '已审核' || row.djzt === '一级已审' || row.djzt === '一级已审/待二级'
321 325 },
322 326 canFirstApprove(row) {
323   - if (this.isSkipErpAuditSource(row)) return false
324 327 if (row.djzt === '草稿' || row.djzt === '已审核') return false
325 328 return row.djzt === '待审核' || row.djzt === '' || row.djzt == null
326 329 },
327 330 canSecondApprove(row) {
328   - if (this.isSkipErpAuditSource(row)) return false
329 331 const z = row.djzt
330 332 return z === '一级已审' || z === '待二级' || z === '一级已审/待二级'
331 333 },
332 334 canRejectAudit(row) {
333   - if (this.isSkipErpAuditSource(row)) return false
334 335 if (row.djzt === '草稿' || row.djzt === '已审核' || row.djzt === '审核不通过') return false
335 336 return this.canFirstApprove(row) || this.canSecondApprove(row)
336 337 },
... ... @@ -531,7 +532,7 @@
531 532 .catch(() => {})
532 533 },
533 534 handleReverseApproval(id) {
534   - this.$confirm('确认反审该销售出库单吗?反审后将恢复为待审核状态。', '提示', {
  535 + this.$confirm('确认反审该销售出库单吗?反审后将恢复为草稿,可再次编辑。', '提示', {
535 536 type: 'warning'
536 537 }).then(() => {
537 538 return request({
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtXswtdxjsd/Form.vue
... ... @@ -271,6 +271,7 @@
271 271 :single-select="true"
272 272 djlx="委托代销发货单"
273 273 bill-type-label="委托代销发货单"
  274 + :require-approved-source="true"
274 275 :hide-zdr-query="true"
275 276 @confirm="handleShipmentOrderSelect"
276 277 />
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtXswtdxjsd/ShipmentOrderSelect.vue
... ... @@ -82,7 +82,7 @@
82 82 </el-col>
83 83 <el-col :span="6">
84 84 <el-form-item label="审核状态">
85   - <el-select v-model="query.djzt" placeholder="审核状态" clearable style="width: 100%">
  85 + <el-select v-model="query.djzt" placeholder="审核状态" clearable style="width: 100%" :disabled="requireApprovedSource">
86 86 <el-option label="草稿" value="草稿" />
87 87 <el-option label="待审核" value="待审核" />
88 88 <el-option label="一级已审" value="一级已审" />
... ... @@ -247,6 +247,11 @@ export default {
247 247 hideZdrQuery: {
248 248 type: Boolean,
249 249 default: false
  250 + },
  251 + /** 为 true 时仅允许选择已审核(过账)原单 */
  252 + requireApprovedSource: {
  253 + type: Boolean,
  254 + default: false
250 255 }
251 256 },
252 257 data() {
... ... @@ -263,7 +268,7 @@ export default {
263 268 zdr: '',
264 269 cjck: '',
265 270 jsr: '',
266   - djzt: '',
  271 + djzt: this.requireApprovedSource ? '已审核' : '',
267 272 bz: ''
268 273 },
269 274 shipmentList: [],
... ... @@ -297,6 +302,9 @@ export default {
297 302 // 打开弹窗
298 303 open() {
299 304 this.visible = true
  305 + if (this.requireApprovedSource) {
  306 + this.query.djzt = '已审核'
  307 + }
300 308 this.$nextTick(() => {
301 309 this.search()
302 310 })
... ... @@ -420,6 +428,9 @@ export default {
420 428 if (this.query.djzt) {
421 429 query.djzt = this.normalizeScalar(this.query.djzt)
422 430 }
  431 + if (this.requireApprovedSource) {
  432 + query.djzt = '已审核'
  433 + }
423 434 if (this.query.bz) {
424 435 query.bz = this.normalizeScalar(this.query.bz)
425 436 }
... ... @@ -477,7 +488,7 @@ export default {
477 488 zdr: '',
478 489 cjck: '',
479 490 jsr: '',
480   - djzt: '',
  491 + djzt: this.requireApprovedSource ? '已审核' : '',
481 492 bz: ''
482 493 }
483 494 this.selectedShipments = []
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtXswtdxthd/Form.vue
... ... @@ -256,6 +256,7 @@
256 256 :single-select="true"
257 257 djlx="委托代销发货单"
258 258 bill-type-label="委托代销发货单"
  259 + :require-approved-source="true"
259 260 :exclude-with-linked-return="true"
260 261 :hide-zdr-query="true"
261 262 @confirm="handleShipmentOrderSelect"
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtYskzjjs_xj/Form.vue
1 1 <template>
2   - <el-dialog :title="!dataForm.id ? '新建' : isDetail ? '详情':'编辑'" :close-on-click-modal="false" :visible.sync="visible" class="NCC-dialog NCC-dialog_center" lock-scroll width="90%">
  2 + <el-dialog :title="dialogTitle" :close-on-click-modal="false" :visible.sync="visible" class="NCC-dialog NCC-dialog_center" lock-scroll width="90%">
3 3 <el-row :gutter="15" class="" >
4   - <el-form ref="elForm" :model="dataForm" size="small" label-width="100px" label-position="right" :disabled="!!isDetail" :rules="rules">
  4 + <el-form ref="elForm" :model="dataForm" size="small" label-width="100px" label-position="right" :disabled="formDisabled" :rules="rules">
5 5 <el-col :span="8">
6 6 <el-form-item label="单据编号" prop="id">
7 7 <el-input v-model="dataForm.id" placeholder="请输入" clearable readonly :style='{"width":"100%"}' >
... ... @@ -20,6 +20,11 @@
20 20 </user-select>
21 21 </el-form-item>
22 22 </el-col>
  23 + <el-col :span="8" v-if="dataForm.id">
  24 + <el-form-item label="审核状态">
  25 + <el-input :value="auditStatusText" readonly />
  26 + </el-form-item>
  27 + </el-col>
23 28 <el-col :span="24">
24 29 <el-form-item label-width="0">
25 30 <el-table :data="dataForm.wtYskzjjsMxList" size='mini'>
... ... @@ -55,13 +60,13 @@
55 60 <el-input v-model="scope.row.bz" placeholder="请输入" clearable ></el-input>
56 61 </template>
57 62 </el-table-column>
58   - <el-table-column label="操作" width="50">
  63 + <el-table-column v-if="!formDisabled" label="操作" width="50">
59 64 <template slot-scope="scope">
60 65 <el-button size="mini" type="text" class="NCC-table-delBtn" @click="handleDelWtYskzjjsMxEntityList(scope.$index)">删除</el-button>
61 66 </template>
62 67 </el-table-column>
63 68 </el-table>
64   - <div class="table-actions" @click="addHandleWtYskzjjsMxEntityList()">
  69 + <div v-if="!formDisabled" class="table-actions" @click="addHandleWtYskzjjsMxEntityList()">
65 70 <el-button type="text" icon="el-icon-plus">新增</el-button>
66 71 </div>
67 72 </el-form-item>
... ... @@ -83,7 +88,10 @@
83 88 </el-row>
84 89 <span slot="footer" class="dialog-footer">
85 90 <el-button @click="visible = false">取 消</el-button>
86   - <el-button type="primary" @click="dataFormSubmit()" v-if="!isDetail">确 定</el-button>
  91 + <template v-if="!formDisabled">
  92 + <el-button @click="submitAsDraft">保存草稿</el-button>
  93 + <el-button type="primary" @click="submitForAudit">提 交</el-button>
  94 + </template>
87 95 </span>
88 96 </el-dialog>
89 97 </template>
... ... @@ -91,6 +99,7 @@
91 99 import request from '@/utils/request'
92 100 import { getDictionaryDataSelector } from '@/api/systemData/dictionary'
93 101 import { getAccountSelector } from '@/api/extend/wtAccount'
  102 +const FYXM_DICT_ID = '816913803978474757'
94 103 export default {
95 104 components: {},
96 105 props: [],
... ... @@ -99,6 +108,7 @@
99 108 loading: false,
100 109 visible: false,
101 110 isDetail: false,
  111 + pendingDjzt: '待审核',
102 112 dataForm: {
103 113 id: undefined,
104 114 djrq: undefined,
... ... @@ -117,7 +127,26 @@
117 127 skzhOptions: [],
118 128 }
119 129 },
120   - computed: {},
  130 + computed: {
  131 + auditStatusText() {
  132 + const z = this.dataForm.djzt != null && this.dataForm.djzt !== '' ? String(this.dataForm.djzt).trim() : ''
  133 + return z || '—'
  134 + },
  135 + isBillLockedForEdit() {
  136 + const z = this.auditStatusText
  137 + return z === '待审核' || z === '已审核' || z === '一级已审' || z === '待二级' || z === '一级已审/待二级'
  138 + },
  139 + formDisabled() {
  140 + return !!this.isDetail || this.isBillLockedForEdit
  141 + },
  142 + dialogTitle() {
  143 + if (!this.dataForm.id) return '新建'
  144 + if (this.isDetail) return '详情'
  145 + const z = this.auditStatusText
  146 + if (z === '草稿') return '编辑'
  147 + return `查看(${z})`
  148 + }
  149 + },
121 150 watch: {},
122 151 created() {
123 152 this.getfyxmmcOptions();
... ... @@ -135,8 +164,8 @@
135 164 },
136 165 methods: {
137 166 getfyxmmcOptions(){
138   - getDictionaryDataSelector('715562947862070533').then(res => {
139   - this.fyxmmcOptions = res.data.list
  167 + getDictionaryDataSelector(FYXM_DICT_ID).then(res => {
  168 + this.fyxmmcOptions = (res.data && res.data.list) ? res.data.list : []
140 169 });
141 170 },
142 171 getfkzhOptions(){
... ... @@ -156,6 +185,7 @@
156 185 this.dataForm.id = id || 0;
157 186 this.visible = true;
158 187 this.isDetail = isDetail || false;
  188 + this.pendingDjzt = '待审核'
159 189 this.$nextTick(() => {
160 190 this.$refs['elForm'].resetFields();
161 191 if (this.dataForm.id) {
... ... @@ -174,13 +204,25 @@
174 204 this.dataForm.jsr = this.$store.getters.userInfo.userId || this.$store.getters.userInfo.id;
175 205 }
176 206 this.dataForm.djlx = '现金费用单';
  207 + this.dataForm.djzt = '草稿';
177 208 this.dataForm.wtYskzjjsMxList = [];
178 209 }
179 210 })
180 211 },
  212 + submitAsDraft() {
  213 + this.pendingDjzt = '草稿'
  214 + this.dataFormSubmit()
  215 + },
  216 + submitForAudit() {
  217 + this.pendingDjzt = '待审核'
  218 + this.dataFormSubmit()
  219 + },
181 220 dataFormSubmit() {
  221 + if (this.formDisabled) return
182 222 this.$refs['elForm'].validate((valid) => {
183 223 if (valid) {
  224 + this.dataForm.djlx = '现金费用单'
  225 + this.$set(this.dataForm, 'djzt', this.pendingDjzt || '待审核')
184 226 if (!this.dataForm.id) {
185 227 request({
186 228 url: `/api/Extend/WtYskzjjs`,
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtYskzjjs_xj/detail-view.vue
... ... @@ -45,6 +45,7 @@
45 45 import request from '@/utils/request'
46 46 import { getDictionaryDataSelector } from '@/api/systemData/dictionary'
47 47 import { getAccountSelector } from '@/api/extend/wtAccount'
  48 +const FYXM_DICT_ID = '816913803978474757'
48 49  
49 50 export default {
50 51 name: 'WtYskzjjsXjDetailView',
... ... @@ -77,7 +78,7 @@ export default {
77 78 },
78 79 methods: {
79 80 loadFeeItems() {
80   - getDictionaryDataSelector('715562947862070533').then(res => {
  81 + getDictionaryDataSelector(FYXM_DICT_ID).then(res => {
81 82 this.fyxmmcOptions = res.data.list || []
82 83 })
83 84 },
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtYskzjjs_xj/index.vue
... ... @@ -106,9 +106,14 @@
106 106 </el-table-column>
107 107 <el-table-column label="操作" fixed="right" width="140">
108 108 <template slot-scope="scope">
109   - <el-button type="text" @click="openDetail(scope.row.id)" >查看</el-button>
110   - <el-button type="text" @click="addOrUpdateHandle(scope.row.id)" >编辑</el-button>
111   - <el-button type="text" @click="handleDel(scope.row.id)" class="NCC-table-delBtn" >删除</el-button>
  109 + <template v-if="canEditCashExpense(scope.row)">
  110 + <el-button type="text" @click="addOrUpdateHandle(scope.row.id)" >编辑</el-button>
  111 + <el-button type="text" @click="handleDel(scope.row.id)" class="NCC-table-delBtn" >删除</el-button>
  112 + </template>
  113 + <template v-else>
  114 + <el-button type="text" @click="openDetail(scope.row.id)" >查看</el-button>
  115 + <el-button v-if="isCashExpenseApproved(scope.row)" type="text" @click="handleReverseAudit(scope.row.id)">反审</el-button>
  116 + </template>
112 117 </template>
113 118 </el-table-column>
114 119 </NCC-table>
... ... @@ -173,6 +178,17 @@
173 178 this.getskzhOptions();
174 179 },
175 180 methods: {
  181 + cashExpenseDjzt(row) {
  182 + if (!row) return ''
  183 + const z = row.djzt != null && row.djzt !== '' ? String(row.djzt).trim() : ''
  184 + return z || (row.shzt != null ? String(row.shzt).trim() : '')
  185 + },
  186 + canEditCashExpense(row) {
  187 + return this.cashExpenseDjzt(row) === '草稿'
  188 + },
  189 + isCashExpenseApproved(row) {
  190 + return this.cashExpenseDjzt(row) === '已审核'
  191 + },
176 192 getfkzhOptions(){
177 193 getAccountSelector().then(res => {
178 194 this.fkzhOptions = res.data.list
... ... @@ -226,7 +242,33 @@
226 242 this.$refs.NCCDetailView && this.$refs.NCCDetailView.init(id)
227 243 })
228 244 },
  245 + handleReverseAudit(id) {
  246 + if (!id) return
  247 + this.$confirm('反审后单据将恢复为草稿,可再次编辑。是否继续?', '反审确认', { type: 'warning' })
  248 + .then(() => request({
  249 + url: `/api/Extend/WtYskzjjs/Actions/ReverseAudit/${id}`,
  250 + method: 'POST',
  251 + data: {}
  252 + }))
  253 + .then((res) => {
  254 + const d = (res && res.data) || {}
  255 + const ok = d.success === true || String(res && res.code) === '200'
  256 + const msg = d.message || res.msg || (ok ? '反审成功' : '操作失败')
  257 + if (ok) {
  258 + this.$message.success(msg)
  259 + this.initData()
  260 + } else {
  261 + this.$message.error(msg)
  262 + }
  263 + })
  264 + .catch(() => {})
  265 + },
229 266 handleDel(id) {
  267 + const row = this.list.find(r => r.id === id)
  268 + if (row && !this.canEditCashExpense(row)) {
  269 + this.$message.warning('仅草稿状态的现金费用单可删除')
  270 + return
  271 + }
230 272 this.$confirm('此操作将永久删除该数据, 是否继续?', '提示', {
231 273 type: 'warning'
232 274 }).then(() => {
... ... @@ -258,6 +300,11 @@
258 300 })
259 301 return
260 302 }
  303 + const locked = this.list.filter(r => this.multipleSelection.includes(r.id) && !this.canEditCashExpense(r))
  304 + if (locked.length) {
  305 + this.$message.warning(`仅草稿可删除,请去掉已提交/已审核单据(${locked.length} 条)后重试`)
  306 + return
  307 + }
261 308 const ids = this.multipleSelection
262 309 this.$confirm('您确定要删除这些数据吗, 是否继续?', '提示', {
263 310 type: 'warning'
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtZsd/Form.vue
1 1 <template>
2   - <el-dialog :title="!dataForm.id ? '新建' : isDetail ? '详情' : '编辑'" :close-on-click-modal="false" :visible.sync="visible" class="NCC-dialog NCC-dialog_center" lock-scroll width="80%">
  2 + <el-dialog :title="dialogTitle" :close-on-click-modal="false" :visible.sync="visible" class="NCC-dialog NCC-dialog_center" lock-scroll width="80%">
3 3 <el-row :gutter="15">
4   - <el-form ref="elForm" :model="dataForm" size="small" label-width="100px" label-position="right" :disabled="!!isDetail">
  4 + <el-form ref="elForm" :model="dataForm" size="small" label-width="100px" label-position="right" :disabled="formDisabled">
5 5 <el-col :span="8">
6 6 <el-form-item label="单据编号">
7 7 <el-input v-model="dataForm.id" placeholder="系统自动生成" readonly />
... ... @@ -9,7 +9,7 @@
9 9 </el-col>
10 10 <el-col :span="8">
11 11 <el-form-item label="单据日期">
12   - <el-date-picker v-model="dataForm.djrq" type="date" format="yyyy-MM-dd" value-format="timestamp" readonly style="width:100%" />
  12 + <el-date-picker v-model="dataForm.djrq" type="date" format="yyyy-MM-dd" value-format="timestamp" :readonly="formDisabled" style="width:100%" />
13 13 </el-form-item>
14 14 </el-col>
15 15 <el-col :span="8">
... ... @@ -26,7 +26,7 @@
26 26 </el-col>
27 27 <el-col :span="8">
28 28 <el-form-item label="审核状态">
29   - <el-input v-model="dataForm.djzt" readonly placeholder="系统自动带出" />
  29 + <el-input :value="auditStatusText" readonly placeholder="系统自动带出" />
30 30 </el-form-item>
31 31 </el-col>
32 32 <el-col :span="24">
... ... @@ -95,13 +95,13 @@
95 95 <el-input v-model="scope.row.description" />
96 96 </template>
97 97 </el-table-column>
98   - <el-table-column label="操作" width="60">
  98 + <el-table-column v-if="!formDisabled" label="操作" width="60">
99 99 <template slot-scope="scope">
100 100 <el-button size="mini" type="text" class="NCC-table-delBtn" @click="removeRow(scope.$index)">删除</el-button>
101 101 </template>
102 102 </el-table-column>
103 103 </el-table>
104   - <div class="table-actions" @click="addRow">
  104 + <div v-if="!formDisabled" class="table-actions" @click="addRow">
105 105 <el-button type="text" icon="el-icon-plus">新增</el-button>
106 106 </div>
107 107 </el-form-item>
... ... @@ -110,8 +110,10 @@
110 110 </el-row>
111 111 <span slot="footer" class="dialog-footer">
112 112 <el-button @click="visible = false">取 消</el-button>
113   - <el-button type="default" @click="saveDraft" v-if="!isDetail">保存草稿</el-button>
114   - <el-button type="primary" @click="dataFormSubmit" v-if="!isDetail">确 定</el-button>
  113 + <template v-if="!formDisabled">
  114 + <el-button type="default" @click="saveDraft">保存草稿</el-button>
  115 + <el-button type="primary" @click="submitForAudit">提交审核</el-button>
  116 + </template>
115 117 </span>
116 118 </el-dialog>
117 119 </template>
... ... @@ -144,6 +146,25 @@ export default {
144 146 this.getcjckOptions()
145 147 this.getspbhOptions()
146 148 },
  149 + computed: {
  150 + auditStatusText() {
  151 + const z = this.dataForm.djzt != null && this.dataForm.djzt !== '' ? String(this.dataForm.djzt).trim() : ''
  152 + return z || '—'
  153 + },
  154 + isBillLockedForEdit() {
  155 + const z = this.auditStatusText
  156 + return z === '待审核' || z === '已审核' || z === '一级已审' || z === '待二级' || z === '一级已审/待二级'
  157 + },
  158 + formDisabled() {
  159 + return !!this.isDetail || this.isBillLockedForEdit
  160 + },
  161 + dialogTitle() {
  162 + if (!this.dataForm.id) return '新建'
  163 + if (this.isDetail) return '详情'
  164 + if (this.isBillLockedForEdit) return '查看'
  165 + return '编辑'
  166 + }
  167 + },
147 168 methods: {
148 169 getcjckOptions() {
149 170 previewDataInterface('681758216954053893').then(res => {
... ... @@ -257,9 +278,11 @@ export default {
257 278 return sums
258 279 },
259 280 async saveDraft() {
  281 + if (this.formDisabled) return
260 282 await this.submitByStatus('草稿')
261 283 },
262   - async dataFormSubmit() {
  284 + async submitForAudit() {
  285 + if (this.formDisabled) return
263 286 await this.submitByStatus('待审核')
264 287 },
265 288 async submitByStatus(status) {
... ...
Antis.Erp.Plat/antis-ncc-admin/src/views/wtZsd/index.vue
... ... @@ -60,7 +60,13 @@
60 60 <template slot-scope="scope">{{ scope.row.cjck | dynamicText(cjckOptions) }}</template>
61 61 </el-table-column>
62 62 <el-table-column prop="jsr" label="经手人" align="left" min-width="100" />
63   - <el-table-column prop="djzt" label="审核状态" align="left" min-width="100" />
  63 + <el-table-column prop="djzt" label="审核状态" align="left" min-width="120">
  64 + <template slot-scope="scope">
  65 + <el-tag v-if="scope.row.djzt === '已审核'" type="success">已审核</el-tag>
  66 + <el-tag v-else-if="scope.row.djzt === '草稿'" type="info">草稿</el-tag>
  67 + <el-tag v-else type="warning">{{ scope.row.djzt || '待审核' }}</el-tag>
  68 + </template>
  69 + </el-table-column>
64 70 <el-table-column prop="zsl" label="合计数量" align="right" min-width="100">
65 71 <template slot-scope="scope">{{ formatQty(scope.row.zsl) }}</template>
66 72 </el-table-column>
... ... @@ -72,10 +78,13 @@
72 78 <ncc-table-summary-cell :row="scope.row" fields="zy,Zy" />
73 79 </template>
74 80 </el-table-column>
75   - <el-table-column label="操作" fixed="right" width="100">
  81 + <el-table-column label="操作" fixed="right" width="260">
76 82 <template slot-scope="scope">
77   - <el-button type="text" @click="addOrUpdateHandle(scope.row.id)">编辑</el-button>
78   - <el-button type="text" @click="handleDel(scope.row.id)" class="NCC-table-delBtn">删除</el-button>
  83 + <el-button type="text" @click="openDetail(scope.row.id)">查看</el-button>
  84 + <el-button v-if="isDraftRow(scope.row)" type="text" @click="addOrUpdateHandle(scope.row.id)">编辑</el-button>
  85 + <el-button v-if="isPendingAudit(scope.row)" type="text" style="color:#409EFF" @click="handleApprove(scope.row.id)">审核</el-button>
  86 + <el-button v-if="scope.row.djzt === '已审核'" type="text" style="color:#E6A23C" @click="handleReverse(scope.row.id)">反审</el-button>
  87 + <el-button v-if="isDraftRow(scope.row)" type="text" @click="handleDel(scope.row.id)" class="NCC-table-delBtn">删除</el-button>
79 88 </template>
80 89 </el-table-column>
81 90 </NCC-table>
... ... @@ -136,6 +145,48 @@ export default {
136 145 this.getcjckOptions()
137 146 },
138 147 methods: {
  148 + isDraftRow(row) {
  149 + return row && String(row.djzt || '').trim() === '草稿'
  150 + },
  151 + isPendingAudit(row) {
  152 + const z = row && String(row.djzt || '').trim()
  153 + return z === '待审核' || z === ''
  154 + },
  155 + openDetail(id) {
  156 + this.formVisible = true
  157 + this.$nextTick(() => this.$refs.NCCForm.init(id, true))
  158 + },
  159 + handleApprove(id) {
  160 + this.$confirm('确认审核该赠送单?', '提示', { type: 'warning' }).then(() => {
  161 + request({
  162 + url: `/api/Extend/WtXsckd/ApproveGeneric/${id}`,
  163 + method: 'POST',
  164 + data: {}
  165 + }).then(res => {
  166 + this.$message({ type: 'success', message: res.msg || '审核成功', duration: 1200 })
  167 + this.initData()
  168 + })
  169 + }).catch(() => {})
  170 + },
  171 + handleReverse(id) {
  172 + this.$confirm('反审后将恢复草稿并可编辑,是否继续?', '反审确认', { type: 'warning' }).then(() => {
  173 + request({
  174 + url: `/api/Extend/WtXsckd/ReverseApproval/${id}`,
  175 + method: 'POST',
  176 + data: {}
  177 + }).then((res) => {
  178 + const d = (res && res.data) || {}
  179 + const ok = d.success === true || String(res && res.code) === '200'
  180 + const msg = d.message || res.msg || (ok ? '反审成功' : '操作失败')
  181 + if (ok) {
  182 + this.$message.success(msg)
  183 + this.initData()
  184 + } else {
  185 + this.$message.error(msg)
  186 + }
  187 + })
  188 + }).catch(() => {})
  189 + },
139 190 formatQty(v) {
140 191 const n = Number(v || 0)
141 192 return Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/\.?0+$/, '')
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.DouyinLogistics/Controllers/OrdersController.cs
... ... @@ -61,6 +61,7 @@ public class OrdersController : ControllerBase
61 61 [FromQuery] string? receiverPhone = null,
62 62 [FromQuery] string? trackingNumber = null,
63 63 [FromQuery] string? productName = null,
  64 + [FromQuery] string? skuName = null,
64 65 [FromQuery] bool? hasWaybill = null,
65 66 /// <summary>为 true 时:待发货 + 已有运单号 + 尚未成功提交发货单(waybills 无 SalesOrderId)</summary>
66 67 [FromQuery] bool pendingShipmentForm = false,
... ... @@ -74,14 +75,16 @@ public class OrdersController : ControllerBase
74 75 [FromQuery] string? sortOrder = null,
75 76 /// <summary>为 true 时:仅「已发货」且已在系统中提交发货单(waybills 有 SalesOrderId)</summary>
76 77 [FromQuery] bool? shipmentFormSubmitted = null,
  78 + /// <summary>为 true 时:仅异常订单(已取消/已退款/退款中)</summary>
  79 + [FromQuery] bool abnormalOnly = false,
77 80 [FromQuery] long? shopId = null)
78 81 {
79 82 try
80 83 {
81 84 var (orders, total) = await _orderService.GetOrdersWithPagingAsync(
82 85 pageIndex, pageSize, status, orderId, receiverName, receiverPhone,
83   - trackingNumber, productName, hasWaybill, pendingShipmentForm, createTimeStart, createTimeEnd, payTimeStart, payTimeEnd,
84   - douyinOrderTimeStart, douyinOrderTimeEnd, sortBy, sortOrder, shipmentFormSubmitted, shopId);
  86 + trackingNumber, productName, skuName, hasWaybill, pendingShipmentForm, createTimeStart, createTimeEnd, payTimeStart, payTimeEnd,
  87 + douyinOrderTimeStart, douyinOrderTimeEnd, sortBy, sortOrder, shipmentFormSubmitted, abnormalOnly, shopId);
85 88 return Ok(new { data = orders, total, pageIndex, pageSize });
86 89 }
87 90 catch (Exception ex)
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.DouyinLogistics/Services/OrderService.cs
... ... @@ -773,6 +773,7 @@ public class OrderService
773 773 string? receiverPhone = null,
774 774 string? trackingNumber = null,
775 775 string? productName = null,
  776 + string? skuName = null,
776 777 bool? hasWaybill = null,
777 778 bool pendingShipmentForm = false,
778 779 DateTime? createTimeStart = null,
... ... @@ -784,6 +785,7 @@ public class OrderService
784 785 string? sortBy = null,
785 786 string? sortOrder = null,
786 787 bool? shipmentFormSubmitted = null,
  788 + bool abnormalOnly = false,
787 789 long? shopId = null)
788 790 {
789 791 var query = _db.Queryable<Order>();
... ... @@ -793,7 +795,11 @@ public class OrderService
793 795 query = query.Where(o => o.ShopId == shopId.Value);
794 796 }
795 797  
796   - if (status.HasValue)
  798 + if (abnormalOnly)
  799 + {
  800 + query = query.Where(o => o.Status == 2 || o.Status == 3 || o.Status == 4);
  801 + }
  802 + else if (status.HasValue)
797 803 {
798 804 query = query.Where(o => o.Status == status.Value);
799 805 }
... ... @@ -823,6 +829,12 @@ public class OrderService
823 829 query = query.Where(o => o.ProductName != null && o.ProductName.Contains(productName));
824 830 }
825 831  
  832 + if (!string.IsNullOrWhiteSpace(skuName))
  833 + {
  834 + // SKU 名称在商品明细 JSON(ProductItems)中,按关键词模糊筛选。
  835 + query = query.Where(o => o.ProductItems != null && o.ProductItems.Contains(skuName));
  836 + }
  837 +
826 838 // hasWaybill 在「同人同址合并」之后处理:否则只有子单有 waybills/运单号时,SQL 会整组拆散,列表里筛不出已建运单的合并单
827 839  
828 840 if (createTimeStart.HasValue)
... ... @@ -859,8 +871,8 @@ public class OrderService
859 871 // shipmentFormSubmitted 不参与缓存 Key:缓存的是「合并 + 运单 enrich」后的候选集;该条件只在内存中过滤,
860 872 // 避免与其它条件组合时缓存条目不一致。
861 873 var cacheKey = BuildOrderListCacheKey(
862   - shopId, status, orderId, receiverName, receiverPhone, trackingNumber, productName,
863   - hasWaybill, pendingShipmentForm, createTimeStart, createTimeEnd,
  874 + shopId, status, orderId, receiverName, receiverPhone, trackingNumber, productName, skuName,
  875 + hasWaybill, pendingShipmentForm, abnormalOnly, createTimeStart, createTimeEnd,
864 876 payTimeStart, payTimeEnd, douyinOrderTimeStart, douyinOrderTimeEnd);
865 877  
866 878 List<Order> merged;
... ... @@ -910,7 +922,7 @@ public class OrderService
910 922 /// </summary>
911 923 private static string BuildOrderListCacheKey(
912 924 long? shopId, int? status, string? orderId, string? receiverName, string? receiverPhone,
913   - string? trackingNumber, string? productName, bool? hasWaybill, bool pendingShipmentForm,
  925 + string? trackingNumber, string? productName, string? skuName, bool? hasWaybill, bool pendingShipmentForm, bool abnormalOnly,
914 926 DateTime? createTimeStart, DateTime? createTimeEnd,
915 927 DateTime? payTimeStart, DateTime? payTimeEnd,
916 928 DateTime? douyinOrderTimeStart, DateTime? douyinOrderTimeEnd)
... ... @@ -926,8 +938,10 @@ public class OrderService
926 938 receiverPhone?.Trim() ?? "", "|",
927 939 trackingNumber?.Trim() ?? "", "|",
928 940 productName?.Trim() ?? "", "|",
  941 + skuName?.Trim() ?? "", "|",
929 942 hasWaybill?.ToString() ?? "", "|",
930 943 pendingShipmentForm, "|",
  944 + abnormalOnly, "|",
931 945 F(createTimeStart), "|", F(createTimeEnd), "|",
932 946 F(payTimeStart), "|", F(payTimeEnd), "|",
933 947 F(douyinOrderTimeStart), "|", F(douyinOrderTimeEnd), "|");
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtCwdjCrInput.cs
... ... @@ -64,7 +64,7 @@ namespace NCC.Extend.Entitys.Dto.WtCwdj
64 64 public string djlx { get; set; }
65 65  
66 66 /// <summary>
67   - /// 单据状态(费用单:草稿 / 待审核;其它类型可按原逻辑由服务端默认)
  67 + /// 单据状态(费用单 / 收款单 / 付款单:草稿 / 待审核;其它类型可按原逻辑由服务端默认)
68 68 /// </summary>
69 69 public string djzt { get; set; }
70 70  
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtPlCrInput.cs
... ... @@ -22,6 +22,11 @@ namespace NCC.Extend.Entitys.Dto.WtPl
22 22 /// 是否门店分类
23 23 /// </summary>
24 24 public string sfmdfl { get; set; }
  25 +
  26 + /// <summary>
  27 + /// 序号 - 自定义排序(升序),留空按默认排序
  28 + /// </summary>
  29 + public int? xh { get; set; }
25 30  
26 31 }
27 32 }
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtPlInfoOutput.cs
... ... @@ -22,6 +22,11 @@ namespace NCC.Extend.Entitys.Dto.WtPl
22 22 /// 是否门店分类
23 23 /// </summary>
24 24 public string sfmdfl { get; set; }
  25 +
  26 + /// <summary>
  27 + /// 序号 - 自定义排序(升序)
  28 + /// </summary>
  29 + public int? xh { get; set; }
25 30  
26 31 }
27 32 }
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtPlListOutput.cs
... ... @@ -21,6 +21,11 @@ namespace NCC.Extend.Entitys.Dto.WtPl
21 21 /// 是否门店分类
22 22 /// </summary>
23 23 public string sfmdfl { get; set; }
  24 +
  25 + /// <summary>
  26 + /// 序号 - 自定义排序(升序)
  27 + /// </summary>
  28 + public int? xh { get; set; }
24 29  
25 30 }
26 31 }
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtPpCrInput.cs
... ... @@ -12,6 +12,11 @@ namespace NCC.Extend.Entitys.Dto.WtPp
12 12 /// 品牌名称
13 13 /// </summary>
14 14 public string ppmc { get; set; }
  15 +
  16 + /// <summary>
  17 + /// 序号 - 自定义排序(升序),留空按默认排序
  18 + /// </summary>
  19 + public int? xh { get; set; }
15 20  
16 21 }
17 22 }
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtPpInfoOutput.cs
... ... @@ -17,6 +17,11 @@ namespace NCC.Extend.Entitys.Dto.WtPp
17 17 /// 品牌名称
18 18 /// </summary>
19 19 public string ppmc { get; set; }
  20 +
  21 + /// <summary>
  22 + /// 序号 - 自定义排序(升序)
  23 + /// </summary>
  24 + public int? xh { get; set; }
20 25  
21 26 }
22 27 }
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtPpListOutput.cs
... ... @@ -16,6 +16,11 @@ namespace NCC.Extend.Entitys.Dto.WtPp
16 16 /// 品牌名称
17 17 /// </summary>
18 18 public string ppmc { get; set; }
  19 +
  20 + /// <summary>
  21 + /// 序号 - 自定义排序(升序)
  22 + /// </summary>
  23 + public int? xh { get; set; }
19 24  
20 25 }
21 26 }
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtSkfkChannelStat/WtSkfkChannelStatQueryInput.cs
... ... @@ -6,6 +6,11 @@ namespace NCC.Extend.Entitys.Dto.WtSkfkChannelStat
6 6 public class WtSkfkChannelStatQueryInput
7 7 {
8 8 /// <summary>
  9 + /// 对账日期 yyyy-MM-dd(单日口径)。传值时优先于 startDate/endDate。
  10 + /// </summary>
  11 + public string date { get; set; }
  12 +
  13 + /// <summary>
9 14 /// 开始日期 yyyy-MM-dd(含)
10 15 /// </summary>
11 16 public string startDate { get; set; }
... ... @@ -26,6 +31,16 @@ namespace NCC.Extend.Entitys.Dto.WtSkfkChannelStat
26 31 public string qd { get; set; }
27 32  
28 33 /// <summary>
  34 + /// 账户主键(wt_account.F_Id)
  35 + /// </summary>
  36 + public string accountId { get; set; }
  37 +
  38 + /// <summary>
  39 + /// 账户关键字(名称/编码/分类)
  40 + /// </summary>
  41 + public string accountKeyword { get; set; }
  42 +
  43 + /// <summary>
29 44 /// 单据类型模糊匹配(流水)
30 45 /// </summary>
31 46 public string djlx { get; set; }
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtXsckdMxCrInput.cs
... ... @@ -64,7 +64,7 @@ namespace NCC.Extend.Entitys.Dto.WtXsckd
64 64 public decimal je { get; set; }
65 65  
66 66 /// <summary>
67   - /// 变价后成本单价(变价调拨单,服务端会按系数重算覆盖)
  67 + /// 变价后成本单价(变价调拨单):优先取明细录入值,未填写时按表头变价系数(bjsx)折算
68 68 /// </summary>
69 69 public decimal? bjhcb { get; set; }
70 70  
... ... @@ -82,6 +82,13 @@ namespace NCC.Extend.Entitys.Dto.WtXsckd
82 82 /// 已选择的序列号列表
83 83 /// </summary>
84 84 public List<string> selectedSerialNumbers { get; set; }
85   -
  85 +
  86 + /// <summary>
  87 + /// 售后规则快照(质保时间等)。
  88 + /// 前端可不传,服务端会按 <c>spbh</c> 从 <c>wt_sp.F_Shgz</c> 抓取落库作为快照;
  89 + /// 前端传入时以前端值为准(保留下单时点的真实规则),保证商品档案后续变更不影响历史单。
  90 + /// </summary>
  91 + public string shgz { get; set; }
  92 +
86 93 }
87 94 }
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtXsckdMxInfoOutput.cs
... ... @@ -99,7 +99,9 @@ namespace NCC.Extend.Entitys.Dto.WtXsckd
99 99 public List<string> selectedSerialNumbers { get; set; }
100 100  
101 101 /// <summary>
102   - /// 商品档案售后规则(质保时间等):不落库,由服务端按 <c>spbh</c> 从 <c>wt_sp.F_Shgz</c> 关联填入,供前端在收银台 / 出库单据明细中提示。
  102 + /// 售后规则(质保时间等)。
  103 + /// 优先读取 <c>wt_xsckd_mx.shgz</c> 快照(新单据在 Create/Update 时落库),只有旧数据快照为空时才回退到 <c>wt_sp.F_Shgz</c>,
  104 + /// 以确保商品档案后续修改(例如平时 1 年、大促期间 2 年质保)不会影响历史订单的展示。
103 105 /// </summary>
104 106 public string shgz { get; set; }
105 107 }
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/WtPlEntity.cs
... ... @@ -28,6 +28,12 @@ namespace NCC.Extend.Entitys
28 28 /// </summary>
29 29 [SugarColumn(ColumnName = "F_Sfmdfl")]
30 30 public string Sfmdfl { get; set; }
  31 +
  32 + /// <summary>
  33 + /// 序号 - 用于自定义排序,升序显示
  34 + /// </summary>
  35 + [SugarColumn(ColumnName = "F_Xh", IsNullable = true)]
  36 + public int? Xh { get; set; }
31 37  
32 38  
33 39 }
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/WtPpEntity.cs
... ... @@ -22,6 +22,12 @@ namespace NCC.Extend.Entitys
22 22 /// </summary>
23 23 [SugarColumn(ColumnName = "F_Ppmc")]
24 24 public string Ppmc { get; set; }
  25 +
  26 + /// <summary>
  27 + /// 序号 - 用于自定义排序,升序显示
  28 + /// </summary>
  29 + [SugarColumn(ColumnName = "F_Xh", IsNullable = true)]
  30 + public int? Xh { get; set; }
25 31  
26 32 }
27 33 }
28 34 \ No newline at end of file
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/WtXsckdMxEntity.cs
... ... @@ -112,6 +112,13 @@ namespace NCC.Extend.Entitys
112 112 /// </summary>
113 113 [SugarColumn(ColumnName = "bjhcb", IsNullable = true)]
114 114 public decimal? Bjhcb { get; set; }
115   -
  115 +
  116 + /// <summary>
  117 + /// 售后规则快照:出库时从 <c>wt_sp.F_Shgz</c> 按 <c>spbh</c> 抓取落库。
  118 + /// 商品档案后续修改售后规则(例如大促把质保由 1 年改成 2 年),历史单据应继续展示购买当时的质保,故必须作为快照。
  119 + /// </summary>
  120 + [SugarColumn(ColumnName = "shgz", IsNullable = true)]
  121 + public string Shgz { get; set; }
  122 +
116 123 }
117 124 }
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtCwdjService.cs
... ... @@ -93,7 +93,7 @@ namespace NCC.Extend.WtCwdj
93 93 .WhereIF(queryLdrq != null, p => p.Ldrq >= new DateTime(startLdrq.ToDate().Year, startLdrq.ToDate().Month, startLdrq.ToDate().Day, 0, 0, 0))
94 94 .WhereIF(queryLdrq != null, p => p.Ldrq <= new DateTime(endLdrq.ToDate().Year, endLdrq.ToDate().Month, endLdrq.ToDate().Day, 23, 59, 59))
95 95 .WhereIF(!string.IsNullOrEmpty(input.wldw), p => p.Wldw.Equals(input.wldw))
96   - .WhereIF(!string.IsNullOrEmpty(input.jsr), p => p.Jsr.Equals(input.jsr))
  96 + .WhereIF(!string.IsNullOrEmpty(input.jsr), p => (p.Jsr ?? "").Contains(input.jsr))
97 97 .WhereIF(input.txqs.HasValue, p => p.Txqs == input.txqs)
98 98 .WhereIF(input.zjqs.HasValue, p => p.Zjqs == input.zjqs)
99 99 .WhereIF(!string.IsNullOrEmpty(input.zy), p => p.Zy.Contains(input.zy))
... ... @@ -155,8 +155,17 @@ namespace NCC.Extend.WtCwdj
155 155 ? "草稿"
156 156 : "待审核";
157 157 }
  158 + else if (IsReceiptOrPaymentBill(resolvedDjlx))
  159 + {
  160 + entity.Djzt = string.Equals(input?.djzt?.Trim(), "草稿", StringComparison.Ordinal)
  161 + ? "草稿"
  162 + : "待审核";
  163 + }
158 164 else if (string.IsNullOrWhiteSpace(entity.Djzt))
159 165 entity.Djzt = "待审核";
  166 +
  167 + if (IsReceiptOrPaymentBill(resolvedDjlx))
  168 + entity.Jsr = await ResolveJsrAccountNameFromMxInputAsync(input.wtCwdjmxList);
160 169  
161 170 // 生成每日递增单号,前缀根据djlx判断
162 171 var today = DateTime.Now.ToString("yyyyMMdd");
... ... @@ -294,7 +303,7 @@ namespace NCC.Extend.WtCwdj
294 303 .WhereIF(queryLdrq != null, p => p.Ldrq >= new DateTime(startLdrq.ToDate().Year, startLdrq.ToDate().Month, startLdrq.ToDate().Day, 0, 0, 0))
295 304 .WhereIF(queryLdrq != null, p => p.Ldrq <= new DateTime(endLdrq.ToDate().Year, endLdrq.ToDate().Month, endLdrq.ToDate().Day, 23, 59, 59))
296 305 .WhereIF(!string.IsNullOrEmpty(input.wldw), p => p.Wldw.Equals(input.wldw))
297   - .WhereIF(!string.IsNullOrEmpty(input.jsr), p => p.Jsr.Equals(input.jsr))
  306 + .WhereIF(!string.IsNullOrEmpty(input.jsr), p => (p.Jsr ?? "").Contains(input.jsr))
298 307 .WhereIF(input.txqs.HasValue, p => p.Txqs == input.txqs)
299 308 .WhereIF(input.zjqs.HasValue, p => p.Zjqs == input.zjqs)
300 309 .WhereIF(!string.IsNullOrEmpty(input.zy), p => p.Zy.Contains(input.zy))
... ... @@ -414,6 +423,7 @@ namespace NCC.Extend.WtCwdj
414 423 var djlxForMx = (entity.Djlx ?? input.djlx ?? string.Empty).Trim();
415 424 FillFydMxZhbhFromFkzh(input, djlxForMx);
416 425 ApplyExpenseDjztOnUpdate(entity, existingHeader, input, djlxForMx);
  426 + ApplyReceiptPaymentDjztOnUpdate(entity, existingHeader, input, djlxForMx);
417 427 try
418 428 {
419 429 //开启事务
... ... @@ -437,7 +447,9 @@ namespace NCC.Extend.WtCwdj
437 447 throw NCCException.Bah("明细原币金额必填且必须大于0");
438 448 entity.Fsje = input.wtCwdjmxList.Sum(x => x.ybje);
439 449 if (entity.Fsje <= 0) throw NCCException.Bah("发生金额必须大于0");
440   -
  450 + if (IsReceiptOrPaymentBill(djlxForMx))
  451 + entity.Jsr = await ResolveJsrAccountNameFromMxInputAsync(input.wtCwdjmxList);
  452 +
441 453 //更新财务单据记录
442 454 await _db.Updateable(entity).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync();
443 455  
... ... @@ -536,19 +548,19 @@ namespace NCC.Extend.WtCwdj
536 548 }
537 549 }
538 550  
539   - /// <summary>费用单:已提交(待审核)或已审核后不可修改(草稿、反审后可改)。</summary>
  551 + /// <summary>费用单 / 收款单 / 付款单:已提交(待审核)或已审核后不可修改(草稿、反审后可改)。</summary>
540 552 private static void ThrowIfExpenseBillCannotEdit(WtCwdjEntity entity)
541 553 {
542   - if (entity == null || (!IsExpenseBill(entity.Djlx) && !IsDeferredAmortization(entity.Djlx))) return;
  554 + if (entity == null || (!IsExpenseBill(entity.Djlx) && !IsDeferredAmortization(entity.Djlx) && !IsReceiptOrPaymentBill(entity.Djlx))) return;
543 555 var zt = (entity.Djzt ?? string.Empty).Trim();
544 556 if (zt == "待审核" || zt == "已审核")
545 557 throw NCCException.Bah("单据已提交或已审核,仅可查看;反审后可再次编辑");
546 558 }
547 559  
548   - /// <summary>费用单:仅草稿可删;已提交、已审核不可删。</summary>
  560 + /// <summary>费用单 / 收款单 / 付款单:仅草稿可删;已提交、已审核不可删。</summary>
549 561 private static void ThrowIfExpenseBillCannotDelete(WtCwdjEntity entity)
550 562 {
551   - if (entity == null || (!IsExpenseBill(entity.Djlx) && !IsDeferredAmortization(entity.Djlx))) return;
  563 + if (entity == null || (!IsExpenseBill(entity.Djlx) && !IsDeferredAmortization(entity.Djlx) && !IsReceiptOrPaymentBill(entity.Djlx))) return;
552 564 var zt = (entity.Djzt ?? string.Empty).Trim();
553 565 if (zt == "待审核" || zt == "已审核")
554 566 throw NCCException.Bah("已提交或已审核的单据不能删除");
... ... @@ -597,6 +609,51 @@ namespace NCC.Extend.WtCwdj
597 609 entity.Djzt = cur;
598 610 }
599 611  
  612 + /// <summary>收款单 / 付款单编辑时:仅草稿允许在「草稿 / 待审核」间切换。</summary>
  613 + private static void ApplyReceiptPaymentDjztOnUpdate(
  614 + WtCwdjEntity entity,
  615 + WtCwdjEntity existingHeader,
  616 + WtCwdjCrInput input,
  617 + string djlxForMx)
  618 + {
  619 + if (!IsReceiptOrPaymentBill(djlxForMx)) return;
  620 + var cur = (existingHeader.Djzt ?? string.Empty).Trim();
  621 + var inc = (input?.djzt ?? string.Empty).Trim();
  622 + if (cur == "草稿")
  623 + {
  624 + if (inc == "待审核" || inc == "草稿")
  625 + entity.Djzt = inc;
  626 + else
  627 + entity.Djzt = cur;
  628 + }
  629 + else
  630 + entity.Djzt = cur;
  631 + }
  632 +
  633 + private static bool IsReceiptBill(string djlx) =>
  634 + !string.IsNullOrWhiteSpace(djlx) && djlx.Trim().Contains("收款单", StringComparison.Ordinal);
  635 +
  636 + private static bool IsPaymentBill(string djlx) =>
  637 + !string.IsNullOrWhiteSpace(djlx) && djlx.Trim().Contains("付款单", StringComparison.Ordinal);
  638 +
  639 + private static bool IsReceiptOrPaymentBill(string djlx) =>
  640 + IsReceiptBill(djlx) || IsPaymentBill(djlx);
  641 +
  642 + /// <summary>经手人存所选明细首行账号的账户名称(<c>WtAccount.AccountName</c>)。</summary>
  643 + private async Task<string> ResolveJsrAccountNameFromMxInputAsync(List<WtCwdjmxCrInput> mxList)
  644 + {
  645 + if (mxList == null || mxList.Count == 0) return null;
  646 + var firstZhbh = mxList.Select(x => x?.zhbh).FirstOrDefault(z => !string.IsNullOrWhiteSpace(z));
  647 + if (string.IsNullOrWhiteSpace(firstZhbh)) return null;
  648 + var tid = firstZhbh.Trim();
  649 + var accList = await _db.Queryable<WtAccountEntity>().Where(a => a.Id == tid).Take(1).ToListAsync();
  650 + if (accList.Count == 0) throw NCCException.Bah("明细账号不存在,请重新选择");
  651 + var acc = accList[0];
  652 + var name = (acc.AccountName ?? string.Empty).Trim();
  653 + if (!string.IsNullOrEmpty(name)) return name;
  654 + return (acc.AccountCode ?? string.Empty).Trim();
  655 + }
  656 +
600 657 /// <summary>
601 658 /// Create 时解析单据类型,避免费用单流程中 djlx 为空导致列表过滤不到。
602 659 /// </summary>
... ... @@ -635,6 +692,10 @@ namespace NCC.Extend.WtCwdj
635 692 /// <summary>
636 693 /// 收款单审核:更新单据状态、审核人、审核时间
637 694 /// </summary>
  695 + /// <param name="id">单据主键</param>
  696 + /// <returns>审核结果</returns>
  697 + /// <response code="200">成功</response>
  698 + /// <response code="400">参数或业务校验失败</response>
638 699 [HttpPost("Actions/ApproveReceipt/{id}")]
639 700 public async Task<dynamic> ApproveReceipt(string id)
640 701 {
... ... @@ -642,6 +703,19 @@ namespace NCC.Extend.WtCwdj
642 703 }
643 704  
644 705 /// <summary>
  706 + /// 付款单审核:待审核单据一级审核通过
  707 + /// </summary>
  708 + /// <param name="id">单据主键</param>
  709 + /// <returns>审核结果</returns>
  710 + /// <response code="200">成功</response>
  711 + /// <response code="400">参数或业务校验失败</response>
  712 + [HttpPost("Actions/ApprovePayment/{id}")]
  713 + public async Task<dynamic> ApprovePayment(string id)
  714 + {
  715 + return await ApproveCwdjSingleLevelAsync(id, "付款", "付款单", "仅付款单支持审核");
  716 + }
  717 +
  718 + /// <summary>
645 719 /// 费用单审核(wt_cwdj.djlx = 费用单):一级审核,配置项为审核人员设置中的「费用单」
646 720 /// </summary>
647 721 [HttpPost("Actions/ApproveExpense/{id}")]
... ... @@ -662,9 +736,65 @@ namespace NCC.Extend.WtCwdj
662 736 /// <summary>
663 737 /// 费用单反审:已审核 → 草稿,清除审核信息并移除单据摘要行,与应收类反审后「可再次编辑」一致。
664 738 /// </summary>
  739 + /// <param name="id">单据主键</param>
  740 + /// <returns>反审结果</returns>
  741 + /// <response code="200">成功</response>
  742 + /// <response code="400">参数或业务校验失败</response>
665 743 [HttpPost("Actions/ReverseExpenseAudit/{id}")]
666 744 public async Task<dynamic> ReverseExpenseAudit(string id)
667 745 {
  746 + return await ReverseCwdjSingleLevelAuditCoreAsync(
  747 + id,
  748 + dl => dl != null && dl.Contains("费用单", StringComparison.Ordinal),
  749 + "仅费用单支持反审",
  750 + "费用单",
  751 + "仅已审核的费用单可反审");
  752 + }
  753 +
  754 + /// <summary>
  755 + /// 收款单反审:已审核 → 草稿,可再次编辑。
  756 + /// </summary>
  757 + /// <param name="id">单据主键</param>
  758 + /// <returns>反审结果</returns>
  759 + /// <response code="200">成功</response>
  760 + /// <response code="400">参数或业务校验失败</response>
  761 + [HttpPost("Actions/ReverseReceiptAudit/{id}")]
  762 + public async Task<dynamic> ReverseReceiptAudit(string id)
  763 + {
  764 + return await ReverseCwdjSingleLevelAuditCoreAsync(
  765 + id,
  766 + dl => dl != null && dl.Contains("收款单", StringComparison.Ordinal),
  767 + "仅收款单支持反审",
  768 + "收款单",
  769 + "仅已审核的收款单可反审");
  770 + }
  771 +
  772 + /// <summary>
  773 + /// 付款单反审:已审核 → 草稿,可再次编辑。
  774 + /// </summary>
  775 + /// <param name="id">单据主键</param>
  776 + /// <returns>反审结果</returns>
  777 + /// <response code="200">成功</response>
  778 + /// <response code="400">参数或业务校验失败</response>
  779 + [HttpPost("Actions/ReversePaymentAudit/{id}")]
  780 + public async Task<dynamic> ReversePaymentAudit(string id)
  781 + {
  782 + return await ReverseCwdjSingleLevelAuditCoreAsync(
  783 + id,
  784 + dl => dl != null && dl.Contains("付款单", StringComparison.Ordinal),
  785 + "仅付款单支持反审",
  786 + "付款单",
  787 + "仅已审核的付款单可反审");
  788 + }
  789 +
  790 + /// <summary>一级审核反审:已审核 → 草稿,并清理单据摘要。</summary>
  791 + private async Task<dynamic> ReverseCwdjSingleLevelAuditCoreAsync(
  792 + string id,
  793 + Func<string, bool> isAllowedDjlx,
  794 + string wrongDjlxMessage,
  795 + string shryszDjmc,
  796 + string wrongDjztMessage)
  797 + {
668 798 if (string.IsNullOrWhiteSpace(id)) throw NCCException.Bah("id 不能为空");
669 799 var userInfo = await _userManager.GetUserInfo();
670 800 var userId = userInfo?.userId ?? _userManager.UserId;
... ... @@ -673,12 +803,11 @@ namespace NCC.Extend.WtCwdj
673 803 userAccount = _userManager.Account?.Trim();
674 804 var entity = await _db.Queryable<WtCwdjEntity>().FirstAsync(x => x.Id == id);
675 805 if (entity == null) throw NCCException.Bah("单据不存在");
676   - if (entity.Djlx == null || !entity.Djlx.Contains("费用单", StringComparison.Ordinal))
677   - throw NCCException.Bah("仅费用单支持反审");
  806 + if (!isAllowedDjlx(entity.Djlx))
  807 + throw NCCException.Bah(wrongDjlxMessage);
678 808 if (!string.Equals((entity.Djzt ?? string.Empty).Trim(), "已审核", StringComparison.Ordinal))
679   - throw NCCException.Bah("仅已审核的费用单可反审");
  809 + throw NCCException.Bah(wrongDjztMessage);
680 810  
681   - const string shryszDjmc = "费用单";
682 811 var approvalConfig = await _db.Queryable<WtShryszEntity>()
683 812 .Where(c => c.Djmc == shryszDjmc)
684 813 .FirstAsync();
... ... @@ -729,18 +858,21 @@ namespace NCC.Extend.WtCwdj
729 858 if (entity.Djlx == null || !entity.Djlx.Contains(djlxContains, StringComparison.Ordinal))
730 859 throw NCCException.Bah(wrongDjlxMessage);
731 860 var ztHead = (entity.Djzt ?? string.Empty).Trim();
732   - if (djlxContains.Contains("费用单", StringComparison.Ordinal))
  861 + var isExpenseFlow = djlxContains.Contains("费用单", StringComparison.Ordinal);
  862 + var isSkFkFlow = djlxContains.Contains("收款", StringComparison.Ordinal)
  863 + || djlxContains.Contains("付款", StringComparison.Ordinal);
  864 + if (isExpenseFlow || isSkFkFlow)
733 865 {
734 866 if (string.Equals(ztHead, "草稿", StringComparison.Ordinal))
735   - throw NCCException.Bah("费用单为草稿,请先提交后再审核");
  867 + throw NCCException.Bah(isExpenseFlow ? "费用单为草稿,请先提交后再审核" : "单据为草稿,请先提交后再审核");
736 868 if (!string.Equals(ztHead, "待审核", StringComparison.Ordinal) && !string.IsNullOrEmpty(ztHead))
737 869 {
738 870 if (string.Equals(ztHead, "已审核", StringComparison.Ordinal))
739 871 return new { success = false, message = "该单据已经审核,无需重复审核" };
740   - throw NCCException.Bah("当前费用单状态不可审核");
  872 + throw NCCException.Bah(isExpenseFlow ? "当前费用单状态不可审核" : "当前单据状态不可审核");
741 873 }
742 874 }
743   - else if (string.Equals(entity.Djzt, "已审核", StringComparison.Ordinal))
  875 + else if (string.Equals(ztHead, "已审核", StringComparison.Ordinal))
744 876 return new { success = false, message = "该单据已经审核,无需重复审核" };
745 877  
746 878 var approvalConfig = await _db.Queryable<WtShryszEntity>()
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtFysrdService.cs
... ... @@ -205,6 +205,7 @@ namespace NCC.Extend.WtFysrd
205 205 var userInfo = await _userManager.GetUserInfo();
206 206 var entity = input.Adapt<WtFysrdEntity>();
207 207 NormalizeHeaderFromInput(input, entity);
  208 + entity.Djlx = WtFysrdWorkflowHelper.NormalizeOtherIncomeDjlx(entity.Djlx) ?? entity.Djlx;
208 209 entity.Djzt = "草稿";
209 210 // 生成每日递增单号
210 211 var today = DateTime.Now.ToString("yyyyMMdd");
... ... @@ -212,7 +213,8 @@ namespace NCC.Extend.WtFysrd
212 213 // 根据单据类型设置前缀
213 214 if (!string.IsNullOrEmpty(entity.Djlx)) {
214 215 if (entity.Djlx.Contains("现金费用单")) prefix = "XF";
215   - else if (WtFysrdWorkflowHelper.IsOtherIncomeDjlx(entity.Djlx)) prefix = "QT";
  216 + else if (string.Equals(entity.Djlx, WtFysrdWorkflowHelper.BillName, StringComparison.Ordinal)
  217 + || WtFysrdWorkflowHelper.IsOtherIncomeDjlx(entity.Djlx)) prefix = "QT";
216 218 else prefix = "FY"; // 默认费用单前缀
217 219 }
218 220 var maxId = await _db.Queryable<WtFysrdEntity>()
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtPlService.cs
... ... @@ -36,6 +36,8 @@ namespace NCC.Extend.WtPl
36 36 private readonly SqlSugarScope _db;
37 37 private readonly IUserManager _userManager;
38 38  
  39 + private static bool _xhColumnChecked;
  40 +
39 41 /// <summary>
40 42 /// 初始化一个<see cref="WtPlService"/>类型的新实例
41 43 /// </summary>
... ... @@ -49,6 +51,32 @@ namespace NCC.Extend.WtPl
49 51 }
50 52  
51 53 /// <summary>
  54 + /// 确保 wt_pl 表存在「序号」字段 F_Xh(用于商品分类自定义排序)
  55 + /// </summary>
  56 + private void EnsureXhColumn()
  57 + {
  58 + if (_xhColumnChecked) return;
  59 + lock (typeof(WtPlService))
  60 + {
  61 + if (_xhColumnChecked) return;
  62 + try
  63 + {
  64 + if (!_db.DbMaintenance.IsAnyTable("wt_pl")) { _xhColumnChecked = true; return; }
  65 + var columns = _db.DbMaintenance.GetColumnInfosByTableName("wt_pl");
  66 + var columnNames = columns.Select(c => c.DbColumnName.ToLower()).ToHashSet();
  67 + if (!columnNames.Contains("f_xh"))
  68 + {
  69 + _db.Ado.ExecuteCommand(
  70 + "ALTER TABLE `wt_pl` ADD COLUMN `F_Xh` int NULL COMMENT '序号(自定义排序,升序)'");
  71 + Console.WriteLine("✅ wt_pl 已添加字段: F_Xh (序号)");
  72 + }
  73 + }
  74 + catch (Exception ex) { Console.WriteLine($"EnsureXhColumn(wt_pl): {ex.Message}"); }
  75 + _xhColumnChecked = true;
  76 + }
  77 + }
  78 +
  79 + /// <summary>
52 80 /// 获取商品分类
53 81 /// </summary>
54 82 /// <param name="id">参数</param>
... ... @@ -56,6 +84,7 @@ namespace NCC.Extend.WtPl
56 84 [HttpGet("{id}")]
57 85 public async Task<dynamic> GetInfo(string id)
58 86 {
  87 + EnsureXhColumn();
59 88 var entity = await _db.Queryable<WtPlEntity>().FirstAsync(p => p.Id == id);
60 89 var output = entity.Adapt<WtPlInfoOutput>();
61 90 return output;
... ... @@ -69,7 +98,10 @@ namespace NCC.Extend.WtPl
69 98 [HttpGet("")]
70 99 public async Task<dynamic> GetList([FromQuery] WtPlListQueryInput input)
71 100 {
72   - var sidx = input.sidx == null ? "id" : input.sidx;
  101 + EnsureXhColumn();
  102 + // 默认按「序号」升序,空序号的排到最后,同序号按主键 id 倒序
  103 + var defaultOrder = "ISNULL(xh) ASC, xh ASC, id DESC";
  104 + var orderBy = string.IsNullOrEmpty(input.sidx) ? defaultOrder : (input.sidx + " " + input.sort);
73 105 var data = await _db.Queryable<WtPlEntity>()
74 106 .WhereIF(!string.IsNullOrEmpty(input.id), p => p.Id.Contains(input.id))
75 107 .WhereIF(!string.IsNullOrEmpty(input.plmc), p => p.Plmc.Contains(input.plmc))
... ... @@ -79,7 +111,8 @@ namespace NCC.Extend.WtPl
79 111 id = it.Id,
80 112 plmc=it.Plmc,
81 113 sfmdfl=it.Sfmdfl,
82   - }).MergeTable().OrderBy(sidx+" "+input.sort).ToPagedListAsync(input.currentPage, input.pageSize);
  114 + xh=it.Xh,
  115 + }).MergeTable().OrderBy(orderBy).ToPagedListAsync(input.currentPage, input.pageSize);
83 116 return PageResult<WtPlListOutput>.SqlSugarPageResult(data);
84 117 }
85 118  
... ... @@ -91,6 +124,7 @@ namespace NCC.Extend.WtPl
91 124 [HttpPost("")]
92 125 public async Task Create([FromBody] WtPlCrInput input)
93 126 {
  127 + EnsureXhColumn();
94 128 var userInfo = await _userManager.GetUserInfo();
95 129 var entity = input.Adapt<WtPlEntity>();
96 130 entity.Id = YitIdHelper.NextId().ToString();
... ... @@ -106,7 +140,9 @@ namespace NCC.Extend.WtPl
106 140 [NonAction]
107 141 public async Task<dynamic> GetNoPagingList([FromQuery] WtPlListQueryInput input)
108 142 {
109   - var sidx = input.sidx == null ? "id" : input.sidx;
  143 + EnsureXhColumn();
  144 + var defaultOrder = "ISNULL(xh) ASC, xh ASC, id DESC";
  145 + var orderBy = string.IsNullOrEmpty(input.sidx) ? defaultOrder : (input.sidx + " " + input.sort);
110 146 var data = await _db.Queryable<WtPlEntity>()
111 147 .WhereIF(!string.IsNullOrEmpty(input.id), p => p.Id.Contains(input.id))
112 148 .WhereIF(!string.IsNullOrEmpty(input.plmc), p => p.Plmc.Contains(input.plmc))
... ... @@ -115,7 +151,8 @@ namespace NCC.Extend.WtPl
115 151 id = it.Id,
116 152 plmc=it.Plmc,
117 153 sfmdfl=it.Sfmdfl,
118   - }).MergeTable().OrderBy(sidx+" "+input.sort).ToListAsync();
  154 + xh=it.Xh,
  155 + }).MergeTable().OrderBy(orderBy).ToListAsync();
119 156 return data;
120 157 }
121 158  
... ... @@ -138,7 +175,7 @@ namespace NCC.Extend.WtPl
138 175 {
139 176 exportData = await this.GetNoPagingList(input);
140 177 }
141   - List<ParamsModel> paramList = "[{\"value\":\"分类编号\",\"field\":\"id\"},{\"value\":\"品类名称\",\"field\":\"plmc\"},]".ToList<ParamsModel>();
  178 + List<ParamsModel> paramList = "[{\"value\":\"序号\",\"field\":\"xh\"},{\"value\":\"分类编号\",\"field\":\"id\"},{\"value\":\"品类名称\",\"field\":\"plmc\"},]".ToList<ParamsModel>();
142 179 ExcelConfig excelconfig = new ExcelConfig();
143 180 excelconfig.FileName = "商品分类.xls";
144 181 excelconfig.HeadFont = "微软雅黑";
... ... @@ -203,9 +240,15 @@ namespace NCC.Extend.WtPl
203 240 [HttpPut("{id}")]
204 241 public async Task Update(string id, [FromBody] WtPlUpInput input)
205 242 {
  243 + EnsureXhColumn();
206 244 var entity = input.Adapt<WtPlEntity>();
207 245 var isOk = await _db.Updateable(entity).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync();
208 246 if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1001);
  247 + // xh 允许清空为 null:单独显式写入(避开 IgnoreColumns:ignoreAllNullColumns 忽略 null 的行为)
  248 + await _db.Updateable<WtPlEntity>()
  249 + .SetColumns(it => it.Xh == input.xh)
  250 + .Where(it => it.Id == id)
  251 + .ExecuteCommandAsync();
209 252 }
210 253  
211 254 /// <summary>
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtPpService.cs
... ... @@ -32,6 +32,8 @@ namespace NCC.Extend.WtPp
32 32 private readonly SqlSugarScope _db;
33 33 private readonly IUserManager _userManager;
34 34  
  35 + private static bool _xhColumnChecked;
  36 +
35 37 /// <summary>
36 38 /// 初始化一个<see cref="WtPpService"/>类型的新实例
37 39 /// </summary>
... ... @@ -45,6 +47,32 @@ namespace NCC.Extend.WtPp
45 47 }
46 48  
47 49 /// <summary>
  50 + /// 确保 wt_pp 表存在「序号」字段 F_Xh(用于品牌自定义排序)
  51 + /// </summary>
  52 + private void EnsureXhColumn()
  53 + {
  54 + if (_xhColumnChecked) return;
  55 + lock (typeof(WtPpService))
  56 + {
  57 + if (_xhColumnChecked) return;
  58 + try
  59 + {
  60 + if (!_db.DbMaintenance.IsAnyTable("wt_pp")) { _xhColumnChecked = true; return; }
  61 + var columns = _db.DbMaintenance.GetColumnInfosByTableName("wt_pp");
  62 + var columnNames = columns.Select(c => c.DbColumnName.ToLower()).ToHashSet();
  63 + if (!columnNames.Contains("f_xh"))
  64 + {
  65 + _db.Ado.ExecuteCommand(
  66 + "ALTER TABLE `wt_pp` ADD COLUMN `F_Xh` int NULL COMMENT '序号(自定义排序,升序)'");
  67 + Console.WriteLine("✅ wt_pp 已添加字段: F_Xh (序号)");
  68 + }
  69 + }
  70 + catch (Exception ex) { Console.WriteLine($"EnsureXhColumn(wt_pp): {ex.Message}"); }
  71 + _xhColumnChecked = true;
  72 + }
  73 + }
  74 +
  75 + /// <summary>
48 76 /// 获取商品品牌
49 77 /// </summary>
50 78 /// <param name="id">参数</param>
... ... @@ -52,6 +80,7 @@ namespace NCC.Extend.WtPp
52 80 [HttpGet("{id}")]
53 81 public async Task<dynamic> GetInfo(string id)
54 82 {
  83 + EnsureXhColumn();
55 84 var entity = await _db.Queryable<WtPpEntity>().FirstAsync(p => p.Id == id);
56 85 var output = entity.Adapt<WtPpInfoOutput>();
57 86 return output;
... ... @@ -65,14 +94,18 @@ namespace NCC.Extend.WtPp
65 94 [HttpGet("")]
66 95 public async Task<dynamic> GetList([FromQuery] WtPpListQueryInput input)
67 96 {
68   - var sidx = input.sidx == null ? "id" : input.sidx;
  97 + EnsureXhColumn();
  98 + // 默认按「序号」升序,同序号或空序号的按创建时间倒序(以主键 id 近似)
  99 + var defaultOrder = "ISNULL(xh) ASC, xh ASC, id DESC";
  100 + var orderBy = string.IsNullOrEmpty(input.sidx) ? defaultOrder : (input.sidx + " " + input.sort);
69 101 var data = await _db.Queryable<WtPpEntity>()
70 102 .WhereIF(!string.IsNullOrEmpty(input.ppmc), p => p.Ppmc.Contains(input.ppmc))
71 103 .Select(it=> new WtPpListOutput
72 104 {
73 105 id = it.Id,
74 106 ppmc=it.Ppmc,
75   - }).MergeTable().OrderBy(sidx+" "+input.sort).ToPagedListAsync(input.currentPage, input.pageSize);
  107 + xh=it.Xh,
  108 + }).MergeTable().OrderBy(orderBy).ToPagedListAsync(input.currentPage, input.pageSize);
76 109 return PageResult<WtPpListOutput>.SqlSugarPageResult(data);
77 110 }
78 111  
... ... @@ -84,6 +117,7 @@ namespace NCC.Extend.WtPp
84 117 [HttpPost("")]
85 118 public async Task Create([FromBody] WtPpCrInput input)
86 119 {
  120 + EnsureXhColumn();
87 121 var userInfo = await _userManager.GetUserInfo();
88 122 var entity = input.Adapt<WtPpEntity>();
89 123 entity.Id = YitIdHelper.NextId().ToString();
... ... @@ -100,9 +134,16 @@ namespace NCC.Extend.WtPp
100 134 [HttpPut("{id}")]
101 135 public async Task Update(string id, [FromBody] WtPpUpInput input)
102 136 {
  137 + EnsureXhColumn();
103 138 var entity = input.Adapt<WtPpEntity>();
  139 + // xh 允许清空为 null,这里单独显式更新 Xh 字段(IgnoreColumns:ignoreAllNullColumns 会跳过 null,
  140 + // 所以主更新仍保留忽略 null 行为,Xh 通过独立更新语句处理)
104 141 var isOk = await _db.Updateable(entity).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync();
105 142 if (!(isOk > 0)) throw NCCException.Oh(ErrorCode.COM1001);
  143 + await _db.Updateable<WtPpEntity>()
  144 + .SetColumns(it => it.Xh == input.xh)
  145 + .Where(it => it.Id == id)
  146 + .ExecuteCommandAsync();
106 147 }
107 148  
108 149 /// <summary>
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtSkfkChannelStatService.cs
... ... @@ -40,6 +40,201 @@ namespace NCC.Extend.WtSkfkChannelStat
40 40 }
41 41  
42 42 /// <summary>
  43 + /// 账户对账汇总:按账户分类分组,输出「上期余额/今日收入/今日支出/本期余额/实时余额」。
  44 + /// </summary>
  45 + /// <param name="input">支持 date(单日)与 accountId/accountKeyword 筛选</param>
  46 + [HttpGet("Actions/GetAccountReconcileSummary")]
  47 + public async Task<dynamic> GetAccountReconcileSummary([FromQuery] WtSkfkChannelStatQueryInput input)
  48 + {
  49 + input ??= new WtSkfkChannelStatQueryInput();
  50 + if (!TryParseReconcileDate(input, out var statDate, out var err))
  51 + throw NCCException.Oh(err);
  52 +
  53 + var dayStart = statDate.Date;
  54 + var dayEnd = dayStart.AddDays(1);
  55 + var nowEndExclusive = DateTime.Now.Date.AddDays(1);
  56 +
  57 + var accountQuery = _db.Queryable<WtAccountEntity>().Where(a => a.Status == 1);
  58 + if (!string.IsNullOrWhiteSpace(input.accountId))
  59 + {
  60 + var accountId = input.accountId.Trim();
  61 + accountQuery = accountQuery.Where(a => a.Id == accountId);
  62 + }
  63 + if (!string.IsNullOrWhiteSpace(input.accountKeyword))
  64 + {
  65 + var kw = input.accountKeyword.Trim();
  66 + accountQuery = accountQuery.Where(a =>
  67 + a.AccountName.Contains(kw) ||
  68 + a.AccountCode.Contains(kw) ||
  69 + a.Category.Contains(kw));
  70 + }
  71 +
  72 + var accounts = await accountQuery
  73 + .OrderBy(a => a.Category)
  74 + .OrderBy(a => a.SortCode)
  75 + .OrderBy(a => a.AccountCode)
  76 + .Select(a => new
  77 + {
  78 + a.Id,
  79 + a.AccountName,
  80 + a.AccountCode,
  81 + a.Category,
  82 + a.SortCode
  83 + })
  84 + .ToListAsync();
  85 +
  86 + var openingLines = await BuildLedgerLinesAsync(DateTime.MinValue, dayStart);
  87 + var dayLines = await BuildLedgerLinesAsync(dayStart, dayEnd);
  88 + var realtimeLines = await BuildLedgerLinesAsync(DateTime.MinValue, nowEndExclusive);
  89 +
  90 + decimal Signed(InternalLine x) => string.Equals(x.FxCode, "sk", StringComparison.Ordinal) ? x.Je : -x.Je;
  91 +
  92 + var openingMap = openingLines
  93 + .Where(x => !string.IsNullOrWhiteSpace(x.SkzhId))
  94 + .GroupBy(x => x.SkzhId.Trim(), StringComparer.Ordinal)
  95 + .ToDictionary(g => g.Key, g => g.Sum(Signed), StringComparer.Ordinal);
  96 +
  97 + var dayIncomeMap = dayLines
  98 + .Where(x => !string.IsNullOrWhiteSpace(x.SkzhId) && x.FxCode == "sk")
  99 + .GroupBy(x => x.SkzhId.Trim(), StringComparer.Ordinal)
  100 + .ToDictionary(g => g.Key, g => g.Sum(x => x.Je), StringComparer.Ordinal);
  101 +
  102 + var dayExpenseMap = dayLines
  103 + .Where(x => !string.IsNullOrWhiteSpace(x.SkzhId) && x.FxCode == "fk")
  104 + .GroupBy(x => x.SkzhId.Trim(), StringComparer.Ordinal)
  105 + .ToDictionary(g => g.Key, g => g.Sum(x => x.Je), StringComparer.Ordinal);
  106 +
  107 + var realtimeMap = realtimeLines
  108 + .Where(x => !string.IsNullOrWhiteSpace(x.SkzhId))
  109 + .GroupBy(x => x.SkzhId.Trim(), StringComparer.Ordinal)
  110 + .ToDictionary(g => g.Key, g => g.Sum(Signed), StringComparer.Ordinal);
  111 +
  112 + var rows = new List<dynamic>();
  113 + foreach (var a in accounts)
  114 + {
  115 + var aid = a.Id ?? string.Empty;
  116 + openingMap.TryGetValue(aid, out var openingBalance);
  117 + dayIncomeMap.TryGetValue(aid, out var todayIncome);
  118 + dayExpenseMap.TryGetValue(aid, out var todayExpense);
  119 + realtimeMap.TryGetValue(aid, out var realtimeBalance);
  120 + var periodBalance = openingBalance + todayIncome - todayExpense;
  121 +
  122 + rows.Add(new
  123 + {
  124 + date = statDate.ToString("yyyy-MM-dd"),
  125 + accountId = aid,
  126 + account = a.AccountName ?? "",
  127 + accountCode = a.AccountCode ?? "",
  128 + category = string.IsNullOrWhiteSpace(a.Category) ? "未分类" : a.Category.Trim(),
  129 + openingBalance,
  130 + todayIncome,
  131 + todayExpense,
  132 + periodBalance,
  133 + realtimeBalance
  134 + });
  135 + }
  136 +
  137 + var categoryGroups = rows
  138 + .GroupBy(x => (string)x.category, StringComparer.Ordinal)
  139 + .Select(g => new
  140 + {
  141 + category = g.Key,
  142 + totalOpeningBalance = g.Sum(x => (decimal)x.openingBalance),
  143 + totalTodayIncome = g.Sum(x => (decimal)x.todayIncome),
  144 + totalTodayExpense = g.Sum(x => (decimal)x.todayExpense),
  145 + totalPeriodBalance = g.Sum(x => (decimal)x.periodBalance),
  146 + totalRealtimeBalance = g.Sum(x => (decimal)x.realtimeBalance),
  147 + rows = g.OrderBy(x => (string)x.accountCode).ThenBy(x => (string)x.account).ToList()
  148 + })
  149 + .OrderBy(x => x.category)
  150 + .ToList();
  151 +
  152 + return new
  153 + {
  154 + date = statDate.ToString("yyyy-MM-dd"),
  155 + categoryGroups,
  156 + totalOpeningBalance = categoryGroups.Sum(x => x.totalOpeningBalance),
  157 + totalTodayIncome = categoryGroups.Sum(x => x.totalTodayIncome),
  158 + totalTodayExpense = categoryGroups.Sum(x => x.totalTodayExpense),
  159 + totalPeriodBalance = categoryGroups.Sum(x => x.totalPeriodBalance),
  160 + totalRealtimeBalance = categoryGroups.Sum(x => x.totalRealtimeBalance)
  161 + };
  162 + }
  163 +
  164 + /// <summary>
  165 + /// 账户对账明细:按日期、账户筛选收支明细。
  166 + /// </summary>
  167 + [HttpGet("Actions/GetAccountReconcileLedger")]
  168 + public async Task<dynamic> GetAccountReconcileLedger([FromQuery] WtSkfkChannelStatQueryInput input)
  169 + {
  170 + input ??= new WtSkfkChannelStatQueryInput();
  171 + if (!TryParseRange(input, out var start, out var endEx, out var err))
  172 + throw NCCException.Oh(err);
  173 +
  174 + var page = input.currentPage.GetValueOrDefault(1);
  175 + if (page < 1) page = 1;
  176 + var size = input.pageSize.GetValueOrDefault(100);
  177 + if (size < 1) size = 100;
  178 + if (size > 500) size = 500;
  179 +
  180 + var lines = await BuildLedgerLinesAsync(start, endEx);
  181 + IEnumerable<InternalLine> q = lines;
  182 +
  183 + if (!string.IsNullOrWhiteSpace(input.accountId))
  184 + {
  185 + var accountId = input.accountId.Trim();
  186 + var account = await _db.Queryable<WtAccountEntity>()
  187 + .Where(a => a.Id == accountId)
  188 + .Select(a => new { a.Id, a.AccountName, a.AccountCode })
  189 + .FirstAsync();
  190 + var accountName = account?.AccountName?.Trim();
  191 + q = q.Where(x =>
  192 + (!string.IsNullOrWhiteSpace(x.SkzhId) && string.Equals(x.SkzhId.Trim(), accountId, StringComparison.Ordinal))
  193 + || (!string.IsNullOrWhiteSpace(accountName) && string.Equals((x.SkzhMc ?? "").Trim(), accountName, StringComparison.Ordinal))
  194 + || ((x.SkzhMc ?? "").Contains("(" + accountId + ")")));
  195 + }
  196 + if (!string.IsNullOrWhiteSpace(input.accountKeyword))
  197 + {
  198 + var kw = input.accountKeyword.Trim();
  199 + q = q.Where(x => (x.SkzhMc != null && x.SkzhMc.Contains(kw, StringComparison.Ordinal))
  200 + || (x.Fffs != null && x.Fffs.Contains(kw, StringComparison.Ordinal))
  201 + || (x.Djlx != null && x.Djlx.Contains(kw, StringComparison.Ordinal)));
  202 + }
  203 + if (!string.IsNullOrWhiteSpace(input.fx))
  204 + {
  205 + var f = input.fx.Trim().ToLowerInvariant();
  206 + if (f == "sk" || f == "收款")
  207 + q = q.Where(x => x.FxCode == "sk");
  208 + else if (f == "fk" || f == "付款")
  209 + q = q.Where(x => x.FxCode == "fk");
  210 + }
  211 +
  212 + var ordered = q.OrderByDescending(x => x.Dt).ThenByDescending(x => x.Djbh).ToList();
  213 + var total = ordered.Count;
  214 + var slice = ordered.Skip((page - 1) * size).Take(size).ToList();
  215 +
  216 + var userIds = slice.Where(x => !string.IsNullOrEmpty(x.JsrId)).Select(x => x.JsrId).Distinct().ToList();
  217 + var userMap = await BuildUserNameMapAsync(userIds);
  218 +
  219 + var list = slice.Select(x => new WtSkfkChannelStatLedgerRowOutput
  220 + {
  221 + djrq = x.Dt.ToString("yyyy-MM-dd"),
  222 + djbh = x.Djbh ?? "",
  223 + djlx = string.IsNullOrEmpty(x.Djlx) ? "" : x.Djlx,
  224 + fx = x.FxCode == "sk" ? "收款" : "付款",
  225 + fffs = string.IsNullOrEmpty(x.Fffs) ? "" : x.Fffs,
  226 + skzhMc = string.IsNullOrEmpty(x.SkzhMc) ? "" : x.SkzhMc,
  227 + je = x.Je,
  228 + ly = x.Ly ?? "",
  229 + ycddh = string.IsNullOrEmpty(x.Ycddh) ? "" : x.Ycddh,
  230 + zy = string.IsNullOrWhiteSpace(x.Zy) ? "" : x.Zy.Trim(),
  231 + jsr = userMap.TryGetValue(x.JsrId ?? "", out var nm) && !string.IsNullOrEmpty(nm) ? nm : ""
  232 + }).ToList();
  233 +
  234 + return new { list, total };
  235 + }
  236 +
  237 + /// <summary>
43 238 /// 汇总:按收付方向拆分。收款侧仅含 Fx=收款 的流水;付款侧仅含 Fx=付款 的流水,避免采购付款计入「入账」。
44 239 /// </summary>
45 240 /// <param name="input">开始/结束日期</param>
... ... @@ -172,6 +367,8 @@ namespace NCC.Extend.WtSkfkChannelStat
172 367 public string FxCode { get; set; }
173 368 /// <summary>买方付款方式(微信、余额等)</summary>
174 369 public string Fffs { get; set; }
  370 + /// <summary>账户主键(wt_account.F_Id)</summary>
  371 + public string SkzhId { get; set; }
175 372 /// <summary>收款账户入账端(字典名)</summary>
176 373 public string SkzhMc { get; set; }
177 374 public decimal Je { get; set; }
... ... @@ -213,6 +410,20 @@ namespace NCC.Extend.WtSkfkChannelStat
213 410 return true;
214 411 }
215 412  
  413 + private static bool TryParseReconcileDate(WtSkfkChannelStatQueryInput input, out DateTime day, out string err)
  414 + {
  415 + day = default;
  416 + err = null;
  417 + var s = string.IsNullOrWhiteSpace(input.date) ? DateTime.Now.ToString("yyyy-MM-dd") : input.date.Trim();
  418 + if (!DateTime.TryParse(s, out day))
  419 + {
  420 + err = "日期无效";
  421 + return false;
  422 + }
  423 + day = day.Date;
  424 + return true;
  425 + }
  426 +
216 427 private async Task<List<InternalLine>> BuildLedgerLinesAsync(DateTime start, DateTime endExclusive)
217 428 {
218 429 var dictIds = new HashSet<string>(StringComparer.Ordinal);
... ... @@ -272,6 +483,7 @@ namespace NCC.Extend.WtSkfkChannelStat
272 483 Djlx = string.IsNullOrEmpty(s.Djlx) ? "收付款单" : s.Djlx,
273 484 FxCode = fxCode,
274 485 Fffs = fffs,
  486 + SkzhId = string.IsNullOrWhiteSpace(s.Jszh) ? null : s.Jszh.Trim(),
275 487 SkzhMc = zhMc,
276 488 Je = Math.Abs(s.Je),
277 489 Ly = "收付款单",
... ... @@ -301,6 +513,9 @@ namespace NCC.Extend.WtSkfkChannelStat
301 513 Djlx = string.IsNullOrEmpty(c.Djlx) ? "财务单据" : c.Djlx,
302 514 FxCode = fxCode,
303 515 Fffs = fffs,
  516 + SkzhId = isPay
  517 + ? (string.IsNullOrWhiteSpace(c.Fkzh) ? null : c.Fkzh.Trim())
  518 + : (string.IsNullOrWhiteSpace(c.Skzh) ? null : c.Skzh.Trim()),
304 519 SkzhMc = zhMc,
305 520 Je = Math.Abs(c.Fsje),
306 521 Ly = "财务单据",
... ... @@ -423,6 +638,7 @@ namespace NCC.Extend.WtSkfkChannelStat
423 638 Djlx = x.Djlx,
424 639 FxCode = fxCode,
425 640 Fffs = fffs,
  641 + SkzhId = skzhId,
426 642 SkzhMc = skzhMc,
427 643 Je = Math.Abs(je),
428 644 Ly = "进销存单据",
... ... @@ -446,6 +662,7 @@ namespace NCC.Extend.WtSkfkChannelStat
446 662 Djlx = x.Djlx,
447 663 FxCode = fxCode,
448 664 Fffs = "其他",
  665 + SkzhId = mainSkzh,
449 666 SkzhMc = ResolveDictLabel(mainSkzh, dictMap, accountFallback),
450 667 Je = Math.Abs(x.Skje),
451 668 Ly = "进销存单据",
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtSpService.cs
... ... @@ -561,6 +561,72 @@ CREATE TABLE `wt_sp_dysku` (
561 561 }
562 562  
563 563 /// <summary>
  564 + /// 构造商品档案筛选查询(与列表筛选口径一致,不含分页)。
  565 + /// </summary>
  566 + private async Task<ISugarQueryable<WtSpEntity>> BuildSpFilterQueryAsync(WtSpListQueryInput input)
  567 + {
  568 + var safeInput = input ?? new WtSpListQueryInput();
  569 + var kw = safeInput.keyword?.Trim();
  570 + var dyFilter = safeInput.dyspid?.Trim();
  571 + List<object> queryLsj = safeInput.lsj != null ? safeInput.lsj.Split(',').ToObeject<List<object>>() : null;
  572 + var startLsj = safeInput.lsj != null && !string.IsNullOrEmpty(queryLsj.First().ToString()) ? queryLsj.First() : decimal.MinValue;
  573 + var endLsj = safeInput.lsj != null && !string.IsNullOrEmpty(queryLsj.Last().ToString()) ? queryLsj.Last() : decimal.MaxValue;
  574 + List<object> queryZg = safeInput.zg != null ? safeInput.zg.Split(',').ToObeject<List<object>>() : null;
  575 + var startZg = safeInput.zg != null && !string.IsNullOrEmpty(queryZg.First().ToString()) ? queryZg.First() : decimal.MinValue;
  576 + var endZg = safeInput.zg != null && !string.IsNullOrEmpty(queryZg.Last().ToString()) ? queryZg.Last() : decimal.MaxValue;
  577 + List<object> queryKc = safeInput.kc != null ? safeInput.kc.Split(',').ToObeject<List<object>>() : null;
  578 + var startKc = safeInput.kc != null && !string.IsNullOrEmpty(queryKc.First().ToString()) ? queryKc.First() : decimal.MinValue;
  579 + var endKc = safeInput.kc != null && !string.IsNullOrEmpty(queryKc.Last().ToString()) ? queryKc.Last() : decimal.MaxValue;
  580 + var q0 = WherePlCategoryIfAny(_db.Queryable<WtSpEntity>()
  581 + .WhereIF(!string.IsNullOrEmpty(safeInput.spmc), p => p.Spmc.Contains(safeInput.spmc)), safeInput.pl)
  582 + .WhereIF(
  583 + !string.IsNullOrEmpty(kw),
  584 + p => p.Spmc.Contains(kw)
  585 + || p.Spbm.Contains(kw)
  586 + || p.Id == kw
  587 + || (!SqlFunc.IsNullOrEmpty(p.Dyspid) && p.Dyspid.Contains(kw))
  588 + || SqlFunc.Subqueryable<WtSpDyskuEntity>()
  589 + .Where(m => m.SpId == p.Id && m.SkuId.Contains(kw))
  590 + .Any())
  591 + .WhereIF(!string.IsNullOrEmpty(safeInput.pp), p => p.Pp.Equals(safeInput.pp))
  592 + .WhereIF(!string.IsNullOrEmpty(safeInput.spbm), p => p.Spbm.Contains(safeInput.spbm))
  593 + .WhereIF(!string.IsNullOrEmpty(safeInput.spxlhType), p => p.SpxlhType.Equals(safeInput.spxlhType))
  594 + .WhereIF(
  595 + !string.IsNullOrEmpty(dyFilter),
  596 + " (F_Dyspid=@dyf OR FIND_IN_SET(@dyf,F_Dyspid)>0 OR EXISTS (SELECT 1 FROM wt_sp_dysku __d WHERE __d.F_SpId=F_Id AND __d.F_SkuId=@dyf)) ",
  597 + new { dyf = dyFilter })
  598 + .WhereIF(queryLsj != null, p => SqlFunc.Between(p.Lsj, startLsj, endLsj))
  599 + .WhereIF(queryZg != null, p => SqlFunc.Between(p.Zg, startZg, endZg))
  600 + .WhereIF(queryKc != null, p => SqlFunc.Between(p.Kc, startKc, endKc))
  601 + .WhereIF(!string.IsNullOrEmpty(safeInput.shgz), p => p.Shgz.Contains(safeInput.shgz))
  602 + .WhereIF(!string.IsNullOrEmpty(safeInput.yfgz), p => p.Yfgz.Contains(safeInput.yfgz))
  603 + .WhereIF(!string.IsNullOrEmpty(safeInput.xsqd), p => p.Xsqd.Equals(safeInput.xsqd));
  604 + return await ApplyXsmdFilterAsync(q0, safeInput.xsmd);
  605 + }
  606 +
  607 + /// <summary>
  608 + /// 判断是否包含任一筛选条件。
  609 + /// </summary>
  610 + private static bool HasAnySpFilterCondition(WtSpListQueryInput input)
  611 + {
  612 + if (input == null) return false;
  613 + return !string.IsNullOrWhiteSpace(input.spmc)
  614 + || !string.IsNullOrWhiteSpace(input.pl)
  615 + || !string.IsNullOrWhiteSpace(input.pp)
  616 + || !string.IsNullOrWhiteSpace(input.spbm)
  617 + || !string.IsNullOrWhiteSpace(input.spxlhType)
  618 + || !string.IsNullOrWhiteSpace(input.lsj)
  619 + || !string.IsNullOrWhiteSpace(input.zg)
  620 + || !string.IsNullOrWhiteSpace(input.kc)
  621 + || !string.IsNullOrWhiteSpace(input.shgz)
  622 + || !string.IsNullOrWhiteSpace(input.yfgz)
  623 + || !string.IsNullOrWhiteSpace(input.xsqd)
  624 + || !string.IsNullOrWhiteSpace(input.xsmd)
  625 + || !string.IsNullOrWhiteSpace(input.keyword)
  626 + || !string.IsNullOrWhiteSpace(input.dyspid);
  627 + }
  628 +
  629 + /// <summary>
564 630 /// 序列号是否计入门店库存:<c>in_warehouse</c> 与仓库主键比对时 Trim+忽略大小写;兼容历史数据中 <c>in_warehouse</c> 误存为门店 ID 的情况。
565 631 /// </summary>
566 632 private static bool SerialMatchesStoreWarehouses(string inWarehouse, string storeIdTrimmed,
... ... @@ -936,7 +1002,7 @@ CREATE TABLE `wt_sp_dysku` (
936 1002 .WhereIF(
937 1003 !string.IsNullOrEmpty(dyFilter),
938 1004 " (F_Dyspid=@dyf OR FIND_IN_SET(@dyf,F_Dyspid)>0 OR EXISTS (SELECT 1 FROM wt_sp_dysku __d WHERE __d.F_SpId=F_Id AND __d.F_SkuId=@dyf)) ",
939   - new SugarParameter("@dyf", dyFilter))
  1005 + new { dyf = dyFilter })
940 1006 .WhereIF(queryLsj != null, p => SqlFunc.Between(p.Lsj, startLsj, endLsj))
941 1007 .WhereIF(queryZg != null, p => SqlFunc.Between(p.Zg, startZg, endZg))
942 1008 .WhereIF(queryKc != null, p => SqlFunc.Between(p.Kc, startKc, endKc))
... ... @@ -1030,7 +1096,7 @@ CREATE TABLE `wt_sp_dysku` (
1030 1096 .WhereIF(
1031 1097 !string.IsNullOrEmpty(dyFilter),
1032 1098 " (F_Dyspid=@dyf OR FIND_IN_SET(@dyf,F_Dyspid)>0 OR EXISTS (SELECT 1 FROM wt_sp_dysku __d WHERE __d.F_SpId=F_Id AND __d.F_SkuId=@dyf)) ",
1033   - new SugarParameter("@dyf", dyFilter));
  1099 + new { dyf = dyFilter });
1034 1100 var qk1 = await ApplyXsmdFilterAsync(qk0, input.xsmd);
1035 1101 var data = await qk1
1036 1102 // SqlSugar 在包含 Subqueryable/WhereIF 时对 lambda 参数名较敏感
... ... @@ -1398,6 +1464,154 @@ CREATE TABLE `wt_sp_dysku` (
1398 1464 }
1399 1465  
1400 1466 /// <summary>
  1467 + /// 按筛选条件批量设置商品关联权益卡(覆盖写入 <c>F_Hyxz</c>)。
  1468 + /// </summary>
  1469 + [HttpPost("BatchSetHyxzByFilter")]
  1470 + public async Task<dynamic> BatchSetHyxzByFilter([FromBody] WtSpBatchHyxzByFilterInput input)
  1471 + {
  1472 + var req = input ?? new WtSpBatchHyxzByFilterInput();
  1473 + var filter = req.query ?? new WtSpListQueryInput();
  1474 + var hasFilter = HasAnySpFilterCondition(filter);
  1475 + if (!hasFilter && !req.allowAll)
  1476 + throw NCCException.Oh("未设置筛选条件。若需作用于全部商品,请确认后传 allowAll=true");
  1477 +
  1478 + var cardIds = (req.hyxzCardIds ?? new List<string>())
  1479 + .Where(x => !string.IsNullOrWhiteSpace(x))
  1480 + .Select(x => x.Trim())
  1481 + .Distinct(StringComparer.Ordinal)
  1482 + .ToList();
  1483 + if (cardIds.Count == 0)
  1484 + throw NCCException.Oh("请至少选择一张权益卡");
  1485 +
  1486 + var validCards = await _db.Queryable<WtHyKjqyEntity>()
  1487 + .Where(c => cardIds.Contains(c.Id))
  1488 + .Select(c => c.Id)
  1489 + .ToListAsync();
  1490 + var finalCardIds = validCards
  1491 + .Where(x => !string.IsNullOrWhiteSpace(x))
  1492 + .Select(x => x.Trim())
  1493 + .Distinct(StringComparer.Ordinal)
  1494 + .ToList();
  1495 + if (finalCardIds.Count == 0)
  1496 + throw NCCException.Oh("所选权益卡不存在或已失效");
  1497 +
  1498 + var query = await BuildSpFilterQueryAsync(filter);
  1499 + var ids = await query.Select(p => p.Id).ToListAsync();
  1500 + if (ids.Count == 0)
  1501 + return new { success = true, affected = 0, message = "未命中任何商品" };
  1502 +
  1503 + var hyxz = string.Join(",", finalCardIds);
  1504 + var affected = await _db.Updateable<WtSpEntity>()
  1505 + .SetColumns(p => new WtSpEntity { Hyxz = hyxz })
  1506 + .Where(p => ids.Contains(p.Id))
  1507 + .ExecuteCommandAsync();
  1508 + return new { success = true, affected, message = $"批量设置完成,共影响 {affected} 条商品" };
  1509 + }
  1510 +
  1511 + /// <summary>
  1512 + /// 按筛选条件批量取消商品关联权益卡(清空 <c>F_Hyxz</c>)。
  1513 + /// </summary>
  1514 + [HttpPost("BatchClearHyxzByFilter")]
  1515 + public async Task<dynamic> BatchClearHyxzByFilter([FromBody] WtSpBatchHyxzByFilterInput input)
  1516 + {
  1517 + var req = input ?? new WtSpBatchHyxzByFilterInput();
  1518 + var filter = req.query ?? new WtSpListQueryInput();
  1519 + var hasFilter = HasAnySpFilterCondition(filter);
  1520 + if (!hasFilter && !req.allowAll)
  1521 + throw NCCException.Oh("未设置筛选条件。若需作用于全部商品,请确认后传 allowAll=true");
  1522 +
  1523 + var query = await BuildSpFilterQueryAsync(filter);
  1524 + var ids = await query.Select(p => p.Id).ToListAsync();
  1525 + if (ids.Count == 0)
  1526 + return new { success = true, affected = 0, message = "未命中任何商品" };
  1527 +
  1528 + var affected = await _db.Updateable<WtSpEntity>()
  1529 + .SetColumns(p => new WtSpEntity { Hyxz = "" })
  1530 + .Where(p => ids.Contains(p.Id))
  1531 + .ExecuteCommandAsync();
  1532 + return new { success = true, affected, message = $"批量取消完成,共影响 {affected} 条商品" };
  1533 + }
  1534 +
  1535 + /// <summary>
  1536 + /// 按勾选商品批量设置关联权益卡(覆盖写入 <c>F_Hyxz</c>)。
  1537 + /// </summary>
  1538 + [HttpPost("BatchSetHyxz")]
  1539 + public async Task<dynamic> BatchSetHyxz([FromBody] WtSpBatchHyxzInput input)
  1540 + {
  1541 + var req = input ?? new WtSpBatchHyxzInput();
  1542 + var ids = (req.ids ?? new List<string>())
  1543 + .Where(x => !string.IsNullOrWhiteSpace(x))
  1544 + .Select(x => x.Trim())
  1545 + .Distinct(StringComparer.Ordinal)
  1546 + .ToList();
  1547 + if (ids.Count == 0)
  1548 + throw NCCException.Oh("请先勾选商品");
  1549 +
  1550 + var cardIds = (req.hyxzCardIds ?? new List<string>())
  1551 + .Where(x => !string.IsNullOrWhiteSpace(x))
  1552 + .Select(x => x.Trim())
  1553 + .Distinct(StringComparer.Ordinal)
  1554 + .ToList();
  1555 + if (cardIds.Count == 0)
  1556 + throw NCCException.Oh("请至少选择一张权益卡");
  1557 +
  1558 + var validCards = await _db.Queryable<WtHyKjqyEntity>()
  1559 + .Where(c => cardIds.Contains(c.Id))
  1560 + .Select(c => c.Id)
  1561 + .ToListAsync();
  1562 + var finalCardIds = validCards
  1563 + .Where(x => !string.IsNullOrWhiteSpace(x))
  1564 + .Select(x => x.Trim())
  1565 + .Distinct(StringComparer.Ordinal)
  1566 + .ToList();
  1567 + if (finalCardIds.Count == 0)
  1568 + throw NCCException.Oh("所选权益卡不存在或已失效");
  1569 +
  1570 + var hitIds = await _db.Queryable<WtSpEntity>()
  1571 + .Where(p => ids.Contains(p.Id))
  1572 + .Select(p => p.Id)
  1573 + .ToListAsync();
  1574 + if (hitIds.Count == 0)
  1575 + return new { success = true, affected = 0, message = "未命中任何商品" };
  1576 +
  1577 + var hyxz = string.Join(",", finalCardIds);
  1578 + var affected = await _db.Updateable<WtSpEntity>()
  1579 + .SetColumns(p => new WtSpEntity { Hyxz = hyxz })
  1580 + .Where(p => hitIds.Contains(p.Id))
  1581 + .ExecuteCommandAsync();
  1582 + return new { success = true, affected, message = $"批量设置完成,共影响 {affected} 条商品" };
  1583 + }
  1584 +
  1585 + /// <summary>
  1586 + /// 按勾选商品批量取消关联权益卡(清空 <c>F_Hyxz</c>)。
  1587 + /// </summary>
  1588 + [HttpPost("BatchClearHyxz")]
  1589 + public async Task<dynamic> BatchClearHyxz([FromBody] WtSpBatchHyxzInput input)
  1590 + {
  1591 + var req = input ?? new WtSpBatchHyxzInput();
  1592 + var ids = (req.ids ?? new List<string>())
  1593 + .Where(x => !string.IsNullOrWhiteSpace(x))
  1594 + .Select(x => x.Trim())
  1595 + .Distinct(StringComparer.Ordinal)
  1596 + .ToList();
  1597 + if (ids.Count == 0)
  1598 + throw NCCException.Oh("请先勾选商品");
  1599 +
  1600 + var hitIds = await _db.Queryable<WtSpEntity>()
  1601 + .Where(p => ids.Contains(p.Id))
  1602 + .Select(p => p.Id)
  1603 + .ToListAsync();
  1604 + if (hitIds.Count == 0)
  1605 + return new { success = true, affected = 0, message = "未命中任何商品" };
  1606 +
  1607 + var affected = await _db.Updateable<WtSpEntity>()
  1608 + .SetColumns(p => new WtSpEntity { Hyxz = "" })
  1609 + .Where(p => hitIds.Contains(p.Id))
  1610 + .ExecuteCommandAsync();
  1611 + return new { success = true, affected, message = $"批量取消完成,共影响 {affected} 条商品" };
  1612 + }
  1613 +
  1614 + /// <summary>
1401 1615 /// 更新商品档案
1402 1616 /// </summary>
1403 1617 /// <param name="id">主键</param>
... ... @@ -1767,4 +1981,17 @@ CREATE TABLE `wt_sp_dysku` (
1767 1981 public string Spbm { get; set; }
1768 1982 public int Quantity { get; set; }
1769 1983 }
  1984 +
  1985 + public class WtSpBatchHyxzByFilterInput
  1986 + {
  1987 + public WtSpListQueryInput query { get; set; }
  1988 + public List<string> hyxzCardIds { get; set; }
  1989 + public bool allowAll { get; set; }
  1990 + }
  1991 +
  1992 + public class WtSpBatchHyxzInput
  1993 + {
  1994 + public List<string> ids { get; set; }
  1995 + public List<string> hyxzCardIds { get; set; }
  1996 + }
1770 1997 }
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtXsckdService.cs
... ... @@ -60,6 +60,7 @@ namespace NCC.Extend.WtXsckd
60 60 private static bool _ytwtfhdColumnChecked;
61 61 private static bool _bjsxColumnChecked;
62 62 private static bool _bjhcbColumnChecked;
  63 + private static bool _mxShgzColumnChecked;
63 64 private static bool _fkmxColumnChecked;
64 65 private static bool _syPchColumnChecked;
65 66 private static bool _yddhColumnChecked;
... ... @@ -1140,6 +1141,70 @@ WHERE d.djlx IN (&#39;销售退货单&#39;,&#39;预售退货单&#39;,&#39;委托代销退货单&#39;)
1140 1141 }
1141 1142  
1142 1143 /// <summary>
  1144 + /// 确保售后规则快照列存在(销售出库单明细)。
  1145 + /// 业务动机:商品档案的售后规则会变(例如平时 1 年质保、大促 2 年),历史单据需保留下单时点的规则,因此需要在明细落库快照。
  1146 + /// </summary>
  1147 + private void EnsureMxShgzColumn()
  1148 + {
  1149 + if (_mxShgzColumnChecked) return;
  1150 + lock (typeof(WtXsckdService))
  1151 + {
  1152 + if (_mxShgzColumnChecked) return;
  1153 + try
  1154 + {
  1155 + if (!_db.DbMaintenance.IsAnyTable("wt_xsckd_mx")) { _mxShgzColumnChecked = true; return; }
  1156 + var columns = _db.DbMaintenance.GetColumnInfosByTableName("wt_xsckd_mx");
  1157 + var columnNames = columns.Select(c => c.DbColumnName.ToLower()).ToHashSet();
  1158 + if (!columnNames.Contains("shgz"))
  1159 + {
  1160 + _db.Ado.ExecuteCommand("ALTER TABLE `wt_xsckd_mx` ADD COLUMN `shgz` text NULL COMMENT '售后规则快照(出库时从wt_sp.F_Shgz抓取,商品档案后续变更不影响历史)'");
  1161 + Console.WriteLine("✅ 已添加字段: wt_xsckd_mx.shgz (售后规则快照)");
  1162 + }
  1163 + }
  1164 + catch (Exception ex) { Console.WriteLine($"EnsureMxShgzColumn: {ex.Message}"); }
  1165 + _mxShgzColumnChecked = true;
  1166 + }
  1167 + }
  1168 +
  1169 + /// <summary>
  1170 + /// 批量按 <paramref name="mxList"/> 的 <c>Spbh</c> 从 <c>wt_sp.F_Shgz</c> 拉取售后规则,落成明细快照。
  1171 + /// 若明细已经带了非空 <c>Shgz</c>(前端收银台下单时可能直接传当时的规则),则保留前端值不被覆盖。
  1172 + /// </summary>
  1173 + private async Task FillMxShgzSnapshotAsync(List<WtXsckdMxEntity> mxList)
  1174 + {
  1175 + if (mxList == null || mxList.Count == 0) return;
  1176 + var needLookup = mxList
  1177 + .Where(m => m != null && string.IsNullOrWhiteSpace(m.Shgz) && !string.IsNullOrWhiteSpace(m.Spbh))
  1178 + .ToList();
  1179 + if (needLookup.Count == 0) return;
  1180 +
  1181 + var spbhs = needLookup
  1182 + .Select(m => m.Spbh.Trim())
  1183 + .Where(bh => !string.IsNullOrWhiteSpace(bh))
  1184 + .Distinct(StringComparer.Ordinal)
  1185 + .ToList();
  1186 + if (spbhs.Count == 0) return;
  1187 +
  1188 + var rows = await _db.Queryable<WtSpEntity>()
  1189 + .Where(s => spbhs.Contains(s.Id))
  1190 + .Select(s => new { s.Id, s.Shgz })
  1191 + .ToListAsync();
  1192 + var dict = rows
  1193 + .Where(r => !string.IsNullOrWhiteSpace(r.Id))
  1194 + .GroupBy(r => r.Id.Trim(), StringComparer.Ordinal)
  1195 + .ToDictionary(g => g.Key, g => g.First().Shgz, StringComparer.Ordinal);
  1196 +
  1197 + foreach (var mx in needLookup)
  1198 + {
  1199 + var key = mx.Spbh?.Trim();
  1200 + if (!string.IsNullOrEmpty(key) && dict.TryGetValue(key, out var v))
  1201 + {
  1202 + mx.Shgz = v;
  1203 + }
  1204 + }
  1205 + }
  1206 +
  1207 + /// <summary>
1143 1208 /// 从备注中解析原销售出库单号(如:原订单号:CHD202603300001)
1144 1209 /// </summary>
1145 1210 private static string TryParseYcddhFromRemark(string bz)
... ... @@ -1501,6 +1566,31 @@ WHERE d.djlx IN (&#39;销售退货单&#39;,&#39;预售退货单&#39;,&#39;委托代销退货单&#39;)
1501 1566 }
1502 1567  
1503 1568 /// <summary>
  1569 + /// 委托代销退货单、委托代销结算单:若关联原委托代销发货单,则该原单必须已审核(过账)。
  1570 + /// </summary>
  1571 + private async Task EnsureConsignmentSourceBillApprovedAsync(WtXsckdEntity entity)
  1572 + {
  1573 + if (entity == null) return;
  1574 + var lx = entity.Djlx ?? string.Empty;
  1575 + if (!lx.Contains("委托代销退货单") && !lx.Contains("委托代销结算单")) return;
  1576 +
  1577 + var srcId = entity.YtWtfhd?.Trim();
  1578 + if (string.IsNullOrWhiteSpace(srcId)) return;
  1579 +
  1580 + var src = await _db.Queryable<WtXsckdEntity>()
  1581 + .Where(x => x.Id == srcId && x.Djlx == "委托代销发货单")
  1582 + .Select(x => new { x.Id, x.Djzt })
  1583 + .FirstAsync();
  1584 +
  1585 + if (src == null)
  1586 + throw NCCException.Bah($"原委托代销发货单不存在:{srcId}");
  1587 +
  1588 + var zt = (src.Djzt ?? string.Empty).Trim();
  1589 + if (!string.Equals(zt, "已审核", StringComparison.Ordinal) && !string.Equals(zt, "已过账", StringComparison.Ordinal))
  1590 + throw NCCException.Bah($"原委托代销发货单【{srcId}】当前状态为「{zt}」,仅已审核(过账)状态允许调原");
  1591 + }
  1592 +
  1593 + /// <summary>
1504 1594 /// 退货类主表「入库仓库」:前端常用 cjck 存仓库,成本/序列号逻辑读 rkck,创建/更新时对齐。
1505 1595 /// </summary>
1506 1596 private static void NormalizeReturnOrderWarehouseFields(WtXsckdEntity entity)
... ... @@ -2027,12 +2117,14 @@ WHERE d.djlx IN (&#39;销售退货单&#39;,&#39;预售退货单&#39;,&#39;委托代销退货单&#39;)
2027 2117 {
2028 2118 var bh = mxItem.spbh?.Trim();
2029 2119 var hasPositiveQty = decimal.TryParse(mxItem.sl, out var slVal) && slVal > 0;
2030   - if (string.IsNullOrEmpty(bh) || noSerialSpbhSet.Contains(bh) || !hasPositiveQty)
  2120 + // 关键修复:优先返回“单据实际已出库”的序列号,不再被 spxlhType=3 误清空。
  2121 + // 历史数据中存在商品档案序列号类型与实际序列号池不一致的情况(例如档案标记为 3,但单据确有 out_djbh 记录)。
  2122 + if (!string.IsNullOrEmpty(bh) && serialBySpbh.TryGetValue(bh, out var snList))
  2123 + mxItem.selectedSerialNumbers = snList;
  2124 + else if (string.IsNullOrEmpty(bh) || noSerialSpbhSet.Contains(bh) || !hasPositiveQty)
2031 2125 {
2032 2126 mxItem.selectedSerialNumbers = new List<string>();
2033 2127 }
2034   - else if (serialBySpbh.TryGetValue(bh, out var snList))
2035   - mxItem.selectedSerialNumbers = snList;
2036 2128 else
2037 2129 mxItem.selectedSerialNumbers = new List<string>();
2038 2130 }
... ... @@ -2305,13 +2397,20 @@ WHERE d.djlx IN (&#39;销售退货单&#39;,&#39;预售退货单&#39;,&#39;委托代销退货单&#39;)
2305 2397 }
2306 2398  
2307 2399 /// <summary>
2308   - /// 批量按明细 <c>spbh</c> 从 <c>wt_sp.F_Shgz</c> 填入售后规则(质保时间等),供前端在收银台及出库单据明细行提示。
  2400 + /// 补齐明细售后规则:
  2401 + /// 1) 优先保留 <c>wt_xsckd_mx.shgz</c> 快照(下单 / 保存时锁定);
  2402 + /// 2) 仅当明细快照为空(历史旧数据)时,才按 <c>spbh</c> 回退取当前的 <c>wt_sp.F_Shgz</c>。
  2403 + /// 这样商品档案后续调整(例如大促期间把 1 年改为 2 年质保)不会污染已下单据的展示。
2309 2404 /// </summary>
2310 2405 private async Task EnrichMxShgzAsync(List<WtXsckdMxInfoOutput> mxList)
2311 2406 {
2312 2407 if (mxList == null || mxList.Count == 0) return;
2313   - var spbhs = mxList
2314   - .Where(m => !string.IsNullOrWhiteSpace(m.spbh))
  2408 + var needFallback = mxList
  2409 + .Where(m => m != null && string.IsNullOrWhiteSpace(m.shgz) && !string.IsNullOrWhiteSpace(m.spbh))
  2410 + .ToList();
  2411 + if (needFallback.Count == 0) return;
  2412 +
  2413 + var spbhs = needFallback
2315 2414 .Select(m => m.spbh.Trim())
2316 2415 .Distinct(StringComparer.Ordinal)
2317 2416 .ToList();
... ... @@ -2324,7 +2423,7 @@ WHERE d.djlx IN (&#39;销售退货单&#39;,&#39;预售退货单&#39;,&#39;委托代销退货单&#39;)
2324 2423 .Where(r => !string.IsNullOrWhiteSpace(r.Id))
2325 2424 .GroupBy(r => r.Id.Trim(), StringComparer.Ordinal)
2326 2425 .ToDictionary(g => g.Key, g => g.First().Shgz, StringComparer.Ordinal);
2327   - foreach (var mx in mxList)
  2426 + foreach (var mx in needFallback)
2328 2427 {
2329 2428 var bh = mx.spbh?.Trim();
2330 2429 if (!string.IsNullOrEmpty(bh) && dict.TryGetValue(bh, out var v))
... ... @@ -2852,6 +2951,7 @@ WHERE d.djlx IN (&#39;销售退货单&#39;,&#39;预售退货单&#39;,&#39;委托代销退货单&#39;)
2852 2951 entity.Hysjh = input.hysjh ?? "";
2853 2952 ApplyYcddhForReturnOrder(entity, input);
2854 2953 ApplyYtWtfhdForConsignmentDocs(entity, input);
  2954 + await EnsureConsignmentSourceBillApprovedAsync(entity);
2855 2955 NormalizeReturnOrderWarehouseFields(entity);
2856 2956 entity.SyPch = NormalizeSyPch(input.sy_pch);
2857 2957 Console.WriteLine($"[Create] 会员手机号码: input.hysjh={input.hysjh}, entity.Hysjh={entity.Hysjh}");
... ... @@ -2884,7 +2984,7 @@ WHERE d.djlx IN (&#39;销售退货单&#39;,&#39;预售退货单&#39;,&#39;委托代销退货单&#39;)
2884 2984 .Where(x => (x.Id.StartsWith("CHD") || x.Id.StartsWith("YC") || x.Id.StartsWith("CT") ||
2885 2985 x.Id.StartsWith("RK") || x.Id.StartsWith("WF") || x.Id.StartsWith("WT") ||
2886 2986 x.Id.StartsWith("WJ") || x.Id.StartsWith("XF") || x.Id.StartsWith("QT") ||
2887   - x.Id.StartsWith("PDD") || x.Id.StartsWith("BSD") || x.Id.StartsWith("TJD") ||
  2987 + x.Id.StartsWith("PDD") || x.Id.StartsWith("BSD") || x.Id.StartsWith("ZSD") || x.Id.StartsWith("TJD") ||
2888 2988 x.Id.StartsWith("BJD") || x.Id.StartsWith("HZD") || x.Id.StartsWith("BYD")) &&
2889 2989 x.Id.Contains(today)) // 包含今天的日期
2890 2990 .Select(x => x.Id)
... ... @@ -2941,6 +3041,17 @@ WHERE d.djlx IN (&#39;销售退货单&#39;,&#39;预售退货单&#39;,&#39;委托代销退货单&#39;)
2941 3041 {
2942 3042 entity.Djzt = "待审核";
2943 3043 }
  3044 + // ✅ 赠送单/获赠单:草稿保存 / 正式提交(沿用 wt_xsckd.djzt)
  3045 + if (IsGiftAuditDocument(input.djlx))
  3046 + {
  3047 + if (input.isDraft || string.Equals(input.djzt, "草稿", StringComparison.Ordinal))
  3048 + {
  3049 + if (string.IsNullOrEmpty(entity.Djzt))
  3050 + entity.Djzt = "草稿";
  3051 + }
  3052 + else if (string.IsNullOrEmpty(entity.Djzt))
  3053 + entity.Djzt = "待审核";
  3054 + }
2944 3055 await NormalizeSalesOrPresaleReturnJsrToOperatorAsync(entity, userId);
2945 3056 // ✅ 确保会员手机号码被保存(即使为空字符串也要保存)
2946 3057 // 尝试插入主表
... ... @@ -2955,6 +3066,9 @@ WHERE d.djlx IN (&#39;销售退货单&#39;,&#39;预售退货单&#39;,&#39;委托代销退货单&#39;)
2955 3066 item.Djbh = newEntity.Id;
2956 3067 item.Djlx = input.djlx;
2957 3068 }
  3069 + // 售后规则快照:没有前端值时按 spbh 从商品档案抓取,锁定当时的质保规则
  3070 + EnsureMxShgzColumn();
  3071 + await FillMxShgzSnapshotAsync(wtXsckdMxEntityList);
2958 3072 await _db.Insertable(wtXsckdMxEntityList).ExecuteCommandAsync();
2959 3073 }
2960 3074  
... ... @@ -3541,6 +3655,26 @@ WHERE d.djlx IN (&#39;销售退货单&#39;,&#39;预售退货单&#39;,&#39;委托代销退货单&#39;)
3541 3655 var entitys = await _db.Queryable<WtXsckdEntity>().In(it => it.Id, ids).ToListAsync();
3542 3656 foreach (var entity in entitys)
3543 3657 {
  3658 + if (string.Equals(entity.Djlx, "采购入库单", StringComparison.Ordinal))
  3659 + {
  3660 + var z = entity.Djzt?.Trim() ?? "";
  3661 + if (!string.Equals(z, "草稿", StringComparison.Ordinal))
  3662 + throw NCCException.Bah($"采购入库单 {entity.Id} 在「{entity.Djzt}」状态下不可删除,仅草稿状态可删除");
  3663 + }
  3664 + if (string.Equals(entity.Djlx, "采购退货单", StringComparison.Ordinal))
  3665 + {
  3666 + var z = entity.Djzt?.Trim() ?? "";
  3667 + if (!string.Equals(z, "草稿", StringComparison.Ordinal))
  3668 + throw NCCException.Bah($"采购退货单 {entity.Id} 在「{entity.Djzt}」状态下不可删除,仅草稿状态可删除");
  3669 + }
  3670 + if (IsGiftAuditDocument(entity.Djlx))
  3671 + {
  3672 + var z = entity.Djzt?.Trim() ?? "";
  3673 + if (string.Equals(z, "待审核", StringComparison.Ordinal)
  3674 + || string.Equals(z, "已审核", StringComparison.Ordinal)
  3675 + || z == "一级已审" || z == "一级已审/待二级" || z == "待二级")
  3676 + throw NCCException.Bah($"赠送/获赠单 {entity.Id} 在「{entity.Djzt}」状态下不可删除,请先反审回草稿或处理为审核不通过后再删");
  3677 + }
3544 3678 var thCsv = await GetLinkedReturnBillIdsCsvAsync(entity.Id, entity.Djlx);
3545 3679 if (!string.IsNullOrEmpty(thCsv))
3546 3680 throw NCCException.Bah($"单据 {entity.Id} 已存在关联退货单({thCsv}),无法删除");
... ... @@ -3619,7 +3753,12 @@ WHERE d.djlx IN (&#39;销售退货单&#39;,&#39;预售退货单&#39;,&#39;委托代销退货单&#39;)
3619 3753 var oldMxList = await _db.Queryable<WtXsckdMxEntity>().Where(u => u.Djbh == id).ToListAsync();
3620 3754 await EnsureXsckdWarehousesNotLockedForUpdateAsync(oldHeader, input.cjck, input.rkck);
3621 3755 if (IsSalesOutboundSkipErpAudit(oldHeader))
3622   - throw NCCException.Bah("非后台来源的销售出库单不允许在此修改");
  3756 + {
  3757 + var z = oldHeader.Djzt?.Trim() ?? "";
  3758 + if (!string.Equals(z, "草稿", StringComparison.Ordinal)
  3759 + && !string.Equals(z, "审核不通过", StringComparison.Ordinal))
  3760 + throw NCCException.Bah($"来源为「{oldHeader.Ly ?? "非后台"}」的销售出库单当前状态为「{oldHeader.Djzt}」,仅草稿或审核不通过时可编辑");
  3761 + }
3623 3762 if (string.Equals(oldHeader.Djlx, "同价调拨单", StringComparison.Ordinal))
3624 3763 {
3625 3764 var z = oldHeader.Djzt?.Trim() ?? "";
... ... @@ -3630,10 +3769,21 @@ WHERE d.djlx IN (&#39;销售退货单&#39;,&#39;预售退货单&#39;,&#39;委托代销退货单&#39;)
3630 3769 if (string.Equals(oldHeader.Djlx, "采购入库单", StringComparison.Ordinal))
3631 3770 {
3632 3771 var z = oldHeader.Djzt?.Trim() ?? "";
3633   - if (!string.IsNullOrEmpty(z)
3634   - && !string.Equals(z, "草稿", StringComparison.Ordinal)
  3772 + if (!string.Equals(z, "草稿", StringComparison.Ordinal))
  3773 + throw NCCException.Bah($"采购入库单当前状态为「{oldHeader.Djzt}」,仅草稿状态可编辑");
  3774 + }
  3775 + if (string.Equals(oldHeader.Djlx, "采购退货单", StringComparison.Ordinal))
  3776 + {
  3777 + var z = oldHeader.Djzt?.Trim() ?? "";
  3778 + if (!string.Equals(z, "草稿", StringComparison.Ordinal))
  3779 + throw NCCException.Bah($"采购退货单当前状态为「{oldHeader.Djzt}」,仅草稿状态可编辑");
  3780 + }
  3781 + if (IsGiftAuditDocument(oldHeader.Djlx))
  3782 + {
  3783 + var z = oldHeader.Djzt?.Trim() ?? "";
  3784 + if (!string.Equals(z, "草稿", StringComparison.Ordinal)
3635 3785 && !string.Equals(z, "审核不通过", StringComparison.Ordinal))
3636   - throw NCCException.Bah($"采购入库单当前状态为「{oldHeader.Djzt}」,仅草稿或审核不通过时可编辑");
  3786 + throw NCCException.Bah($"赠送/获赠单当前状态为「{oldHeader.Djzt}」,仅草稿或审核不通过时可编辑");
3637 3787 }
3638 3788 var entity = input.Adapt<WtXsckdEntity>();
3639 3789 entity.Bjsx = input.bjsx;
... ... @@ -3645,6 +3795,7 @@ WHERE d.djlx IN (&#39;销售退货单&#39;,&#39;预售退货单&#39;,&#39;委托代销退货单&#39;)
3645 3795 entity.Dyddh = input.dyddh;
3646 3796 ApplyYcddhForReturnOrder(entity, input);
3647 3797 ApplyYtWtfhdForConsignmentDocs(entity, input);
  3798 + await EnsureConsignmentSourceBillApprovedAsync(entity);
3648 3799 NormalizeReturnOrderWarehouseFields(entity);
3649 3800 if (input.sy_pch != null)
3650 3801 entity.SyPch = string.IsNullOrWhiteSpace(input.sy_pch) ? null : NormalizeSyPch(input.sy_pch);
... ... @@ -3748,6 +3899,9 @@ WHERE d.djlx IN (&#39;销售退货单&#39;,&#39;预售退货单&#39;,&#39;委托代销退货单&#39;)
3748 3899 item.Djbh = id;
3749 3900 item.Djlx = entity.Djlx;
3750 3901 }
  3902 + // 售后规则快照:没有前端值时按 spbh 从商品档案抓取,锁定当时的质保规则
  3903 + EnsureMxShgzColumn();
  3904 + await FillMxShgzSnapshotAsync(wtXsckdMxEntityList);
3751 3905 await _db.Insertable(wtXsckdMxEntityList).ExecuteCommandAsync();
3752 3906 }
3753 3907  
... ... @@ -3811,6 +3965,26 @@ WHERE d.djlx IN (&#39;销售退货单&#39;,&#39;预售退货单&#39;,&#39;委托代销退货单&#39;)
3811 3965 {
3812 3966 var entity = await _db.Queryable<WtXsckdEntity>().FirstAsync(p => p.Id == id);
3813 3967 _ = entity ?? throw NCCException.Oh(ErrorCode.COM1005);
  3968 + if (string.Equals(entity.Djlx, "采购入库单", StringComparison.Ordinal))
  3969 + {
  3970 + var z = entity.Djzt?.Trim() ?? "";
  3971 + if (!string.Equals(z, "草稿", StringComparison.Ordinal))
  3972 + throw NCCException.Bah($"采购入库单在「{entity.Djzt}」状态下不可删除,仅草稿状态可删除");
  3973 + }
  3974 + if (string.Equals(entity.Djlx, "采购退货单", StringComparison.Ordinal))
  3975 + {
  3976 + var z = entity.Djzt?.Trim() ?? "";
  3977 + if (!string.Equals(z, "草稿", StringComparison.Ordinal))
  3978 + throw NCCException.Bah($"采购退货单在「{entity.Djzt}」状态下不可删除,仅草稿状态可删除");
  3979 + }
  3980 + if (IsGiftAuditDocument(entity.Djlx))
  3981 + {
  3982 + var z = entity.Djzt?.Trim() ?? "";
  3983 + if (string.Equals(z, "待审核", StringComparison.Ordinal)
  3984 + || string.Equals(z, "已审核", StringComparison.Ordinal)
  3985 + || z == "一级已审" || z == "一级已审/待二级" || z == "待二级")
  3986 + throw NCCException.Bah($"赠送/获赠单在「{entity.Djzt}」状态下不可删除,请先反审回草稿或处理为审核不通过后再删");
  3987 + }
3814 3988 await EnsureXsckdWarehousesNotLockedForHeaderAsync(entity);
3815 3989 var thCsv = await GetLinkedReturnBillIdsCsvAsync(entity.Id, entity.Djlx);
3816 3990 if (!string.IsNullOrEmpty(thCsv))
... ... @@ -4096,8 +4270,20 @@ WHERE d.djlx IN (&#39;销售退货单&#39;,&#39;预售退货单&#39;,&#39;委托代销退货单&#39;)
4096 4270 ["销售"] = "t.`销售`",
4097 4271 ["salesAmount"] = "t.`销售金额`",
4098 4272 ["销售金额"] = "t.`销售金额`",
  4273 + ["unitPrice"] = "t.`单价`",
  4274 + ["单价"] = "t.`单价`",
  4275 + ["costUnitPrice"] = "t.`成本单价`",
  4276 + ["成本单价"] = "t.`成本单价`",
  4277 + ["costAmount"] = "t.`成本金额`",
  4278 + ["成本金额"] = "t.`成本金额`",
  4279 + ["preSalesAmount"] = "t.`折前销售金额`",
  4280 + ["折前销售金额"] = "t.`折前销售金额`",
4099 4281 ["grossProfit"] = "t.`毛利`",
4100   - ["毛利"] = "t.`毛利`"
  4282 + ["毛利"] = "t.`毛利`",
  4283 + ["grossMarginPct"] = "t.`毛利率(%)`",
  4284 + ["毛利率(%)"] = "t.`毛利率(%)`",
  4285 + ["zeroPriceQty"] = "t.`0单价数量`",
  4286 + ["0单价数量"] = "t.`0单价数量`"
4101 4287 };
4102 4288 var orderCol = "t.`销售金额`";
4103 4289 var orderDir = "DESC";
... ... @@ -4121,8 +4307,20 @@ WHERE d.djlx IN (&#39;销售退货单&#39;,&#39;预售退货单&#39;,&#39;委托代销退货单&#39;)
4121 4307 ["销售"] = "t.`销售`",
4122 4308 ["salesAmount"] = "t.`销售金额`",
4123 4309 ["销售金额"] = "t.`销售金额`",
  4310 + ["unitPrice"] = "t.`单价`",
  4311 + ["单价"] = "t.`单价`",
  4312 + ["costUnitPrice"] = "t.`成本单价`",
  4313 + ["成本单价"] = "t.`成本单价`",
  4314 + ["costAmount"] = "t.`成本金额`",
  4315 + ["成本金额"] = "t.`成本金额`",
  4316 + ["preSalesAmount"] = "t.`折前销售金额`",
  4317 + ["折前销售金额"] = "t.`折前销售金额`",
4124 4318 ["grossProfit"] = "t.`毛利`",
4125   - ["毛利"] = "t.`毛利`"
  4319 + ["毛利"] = "t.`毛利`",
  4320 + ["grossMarginPct"] = "t.`毛利率(%)`",
  4321 + ["毛利率(%)"] = "t.`毛利率(%)`",
  4322 + ["zeroPriceQty"] = "t.`0单价数量`",
  4323 + ["0单价数量"] = "t.`0单价数量`"
4126 4324 };
4127 4325 var orderColB = "t.`销售金额`";
4128 4326 var orderDirB = "DESC";
... ... @@ -4308,6 +4506,7 @@ ORDER BY {orderBy}&quot;;
4308 4506 [FromQuery] string groupLevel,
4309 4507 [FromQuery] string categoryId = null,
4310 4508 [FromQuery] string brandId = null,
  4509 + [FromQuery] bool includeAllProducts = false,
4311 4510 string productName = null,
4312 4511 string settleUnit = null,
4313 4512 string contactUnit = null,
... ... @@ -4324,7 +4523,8 @@ ORDER BY {orderBy}&quot;;
4324 4523 if (gl != "category" && gl != "brand" && gl != "product")
4325 4524 throw NCCException.Oh("groupLevel 须为 category、brand 或 product");
4326 4525  
4327   - if ((gl == "brand" || gl == "product") && string.IsNullOrWhiteSpace(categoryId))
  4526 + var hasCategoryIdQuery = App.HttpContext?.Request?.Query.ContainsKey("categoryId") == true;
  4527 + if ((gl == "brand" || gl == "product") && !hasCategoryIdQuery)
4328 4528 throw NCCException.Oh("brand / product 层级须提供 categoryId");
4329 4529  
4330 4530 List<string> extras = null;
... ... @@ -4334,15 +4534,15 @@ ORDER BY {orderBy}&quot;;
4334 4534 extras = new List<string>();
4335 4535 extraParams = new List<SugarParameter>
4336 4536 {
4337   - new SugarParameter("@hierCat", categoryId.Trim())
  4537 + new SugarParameter("@hierCat", (categoryId ?? string.Empty).Trim())
4338 4538 };
4339 4539 if (!string.IsNullOrWhiteSpace(brandId))
4340 4540 {
4341   - extras.Add("mx.spbh IN (SELECT s2.F_Id FROM wt_sp s2 WHERE s2.F_Pl = @hierCat AND s2.F_Pp = @hierBrand)");
  4541 + extras.Add("mx.spbh IN (SELECT s2.F_Id FROM wt_sp s2 WHERE IFNULL(s2.F_Pl,'') = @hierCat AND s2.F_Pp = @hierBrand)");
4342 4542 extraParams.Add(new SugarParameter("@hierBrand", brandId.Trim()));
4343 4543 }
4344 4544 else
4345   - extras.Add("mx.spbh IN (SELECT s2.F_Id FROM wt_sp s2 WHERE s2.F_Pl = @hierCat)");
  4545 + extras.Add("mx.spbh IN (SELECT s2.F_Id FROM wt_sp s2 WHERE IFNULL(s2.F_Pl,'') = @hierCat)");
4346 4546 }
4347 4547  
4348 4548 var (whereSql, paramList, posInSql) = BuildProductSummaryWhere(
... ... @@ -4367,6 +4567,106 @@ ORDER BY {orderBy}&quot;;
4367 4567  
4368 4568 if (gl == "product")
4369 4569 {
  4570 + if (includeAllProducts)
  4571 + {
  4572 + var whereSqlP = whereSql;
  4573 + var paramListP = paramList;
  4574 + var salesAggSql = $@"
  4575 +SELECT
  4576 + mx.spbh AS spbh,
  4577 + MAX(mx.spmc) AS spmc,
  4578 + MAX(mx.dw) AS dw,
  4579 + {sumNetSl} AS salesQty,
  4580 + {sumNetJe} AS salesAmount,
  4581 + {sumNetLineCost} AS costAmount,
  4582 + {sumPreSl} AS preSalesAmount,
  4583 + {sumZeroSl} AS zeroPriceQty
  4584 +FROM wt_xsckd_mx mx
  4585 +INNER JOIN wt_xsckd d ON d.F_Id = mx.djbh
  4586 +LEFT JOIN wt_ck ck ON ck.F_Id = COALESCE(NULLIF(TRIM(mx.ckck), ''), NULLIF(TRIM(d.cjck), ''))
  4587 +LEFT JOIN wt_sp s ON s.F_Id = mx.spbh
  4588 +LEFT JOIN wt_pl pl ON pl.F_Id = s.F_Pl
  4589 +LEFT JOIN wt_pp pp ON pp.F_Id = s.F_Pp
  4590 +{whereSqlP}
  4591 +GROUP BY mx.spbh";
  4592 +
  4593 + var productFilterSql = "";
  4594 + if (!string.IsNullOrWhiteSpace(productName))
  4595 + {
  4596 + productFilterSql = " AND (s.F_Spmc LIKE @hierProductName OR s.F_Spbm LIKE @hierProductName)";
  4597 + paramListP.Add(new SugarParameter("@hierProductName", $"%{productName.Trim()}%"));
  4598 + }
  4599 +
  4600 + var categoryFilterSql = "IFNULL(s.F_Pl,'') = @hierCat";
  4601 + if (!string.IsNullOrWhiteSpace(brandId))
  4602 + categoryFilterSql += " AND s.F_Pp = @hierBrand";
  4603 +
  4604 + var productOuterSql = $@"
  4605 + SELECT
  4606 + s.F_Id AS `商品编号`,
  4607 + s.F_Spmc AS `商品名称`,
  4608 + IFNULL(NULLIF(TRIM(pl.F_Plmc), ''), '无') AS `分类名称`,
  4609 + IFNULL(NULLIF(TRIM(pp.F_Ppmc), ''), '无') AS `品牌名称`,
  4610 + IFNULL(sa.salesQty, 0) AS `销售`,
  4611 + IFNULL(sa.dw, '') AS `单位`,
  4612 + CASE WHEN IFNULL(sa.salesQty, 0) = 0 THEN 0 ELSE IFNULL(sa.salesAmount, 0) / IFNULL(sa.salesQty, 1) END AS `单价`,
  4613 + IFNULL(sa.salesAmount, 0) AS `销售金额`,
  4614 + CASE WHEN IFNULL(sa.salesQty, 0) = 0 THEN 0 ELSE IFNULL(sa.costAmount, 0) / IFNULL(sa.salesQty, 1) END AS `成本单价`,
  4615 + IFNULL(sa.costAmount, 0) AS `成本金额`,
  4616 + IFNULL(sa.preSalesAmount, 0) AS `折前销售金额`,
  4617 + IFNULL(sa.salesAmount, 0) * 1.13 AS `价税合计`,
  4618 + IFNULL(sa.salesAmount, 0) * 0.13 AS `税额`,
  4619 + 0 AS `费用分摊金额`,
  4620 + (IFNULL(sa.salesAmount, 0) - IFNULL(sa.costAmount, 0)) AS `毛利`,
  4621 + CASE WHEN IFNULL(sa.salesAmount, 0) = 0 THEN 0 ELSE (IFNULL(sa.salesAmount, 0) - IFNULL(sa.costAmount, 0)) / IFNULL(sa.salesAmount, 1) * 100 END AS `毛利率(%)`,
  4622 + IFNULL(sa.zeroPriceQty, 0) AS `0单价数量`,
  4623 + (IFNULL(sa.salesAmount, 0) - IFNULL(sa.costAmount, 0)) AS `平均利润`
  4624 + FROM wt_sp s
  4625 + LEFT JOIN wt_pl pl ON pl.F_Id = s.F_Pl
  4626 + LEFT JOIN wt_pp pp ON pp.F_Id = s.F_Pp
  4627 + LEFT JOIN ({salesAggSql}) sa ON sa.spbh = s.F_Id
  4628 + WHERE {categoryFilterSql}{productFilterSql}";
  4629 +
  4630 + if (!paramListP.Any(p => p.ParameterName == "@hierCat"))
  4631 + paramListP.Add(new SugarParameter("@hierCat", (categoryId ?? string.Empty).Trim()));
  4632 + if (!string.IsNullOrWhiteSpace(brandId) && !paramListP.Any(p => p.ParameterName == "@hierBrand"))
  4633 + paramListP.Add(new SugarParameter("@hierBrand", brandId.Trim()));
  4634 +
  4635 + var orderByAll = BuildProductSummaryOrderBy(sortField, sortOrder, "product");
  4636 + var sqlAll = $@"SELECT
  4637 + t.`商品编号`,
  4638 + t.`商品名称`,
  4639 + t.`分类名称`,
  4640 + t.`品牌名称`,
  4641 + t.`销售`,
  4642 + t.`单位`,
  4643 + t.`单价`,
  4644 + t.`销售金额`,
  4645 + t.`成本单价`,
  4646 + t.`成本金额`,
  4647 + t.`折前销售金额`,
  4648 + t.`价税合计`,
  4649 + t.`税额`,
  4650 + t.`费用分摊金额`,
  4651 + t.`毛利`,
  4652 + t.`毛利率(%)`,
  4653 + t.`0单价数量`,
  4654 + t.`平均利润`,
  4655 + CASE WHEN agg.totalSalesQty = 0 THEN 0 ELSE t.`销售` / agg.totalSalesQty * 100 END AS `销售权重(%)`,
  4656 + CASE WHEN agg.totalSalesAmount = 0 THEN 0 ELSE t.`销售金额` / agg.totalSalesAmount * 100 END AS `金额权重(%)`,
  4657 + CASE WHEN agg.totalProfit = 0 THEN 0 ELSE t.`毛利` / agg.totalProfit * 100 END AS `利润权重(%)`
  4658 +FROM ({productOuterSql}) t
  4659 +CROSS JOIN (
  4660 + SELECT
  4661 + SUM(t2.`销售`) AS totalSalesQty,
  4662 + SUM(t2.`销售金额`) AS totalSalesAmount,
  4663 + SUM(t2.`毛利`) AS totalProfit
  4664 + FROM ({productOuterSql}) t2
  4665 +) agg
  4666 +ORDER BY {orderByAll}";
  4667 + return Task.FromResult<dynamic>(_db.Ado.GetDataTable(sqlAll, paramListP));
  4668 + }
  4669 +
4370 4670 var fromSql = $@"
4371 4671 FROM wt_xsckd_mx mx
4372 4672 INNER JOIN wt_xsckd d ON d.F_Id = mx.djbh
... ... @@ -4470,8 +4770,8 @@ LEFT JOIN wt_pp pp ON pp.F_Id = s.F_Pp
4470 4770 }
4471 4771  
4472 4772 // brand
4473   - var brandExtras = new List<string> { "s.F_Pl = @hierBrandCat" };
4474   - var brandParams = new List<SugarParameter> { new SugarParameter("@hierBrandCat", categoryId.Trim()) };
  4773 + var brandExtras = new List<string> { "IFNULL(s.F_Pl,'') = @hierBrandCat" };
  4774 + var brandParams = new List<SugarParameter> { new SugarParameter("@hierBrandCat", (categoryId ?? string.Empty).Trim()) };
4475 4775 var (whereSqlB, paramListB, posInSqlB) = BuildProductSummaryWhere(
4476 4776 productName, settleUnit, contactUnit, agent, warehouse, billType, startDate, endDate,
4477 4777 brandExtras,
... ... @@ -4870,7 +5170,30 @@ ORDER BY SUM(CAST(c.sl AS DECIMAL(18,4)) * c.cbj) DESC&quot;;
4870 5170 var pid = productId.Trim();
4871 5171 var wid = warehouseId.Trim();
4872 5172  
  5173 + // 兼容前端传入「仓库/门店名称」(详情接口会把 ckck 替换为展示名称,编辑页无法反查时会直接带名称过来)
  5174 + // 先尝试按 ID 解析;若按 ID 解析不出仓库集合,再按名称(Mdmc) 反查 ID 再解析一次
4873 5175 var warehouseIds = await ResolveWarehouseIdsForSpCostFromStoreOrCkAsync(wid);
  5176 + if (warehouseIds.Count == 0)
  5177 + {
  5178 + string resolvedId = null;
  5179 + var mdByName = await _db.Queryable<WtMdEntity>()
  5180 + .Where(m => m.Mdmc == wid)
  5181 + .Select(m => m.Id)
  5182 + .FirstAsync();
  5183 + if (!string.IsNullOrWhiteSpace(mdByName)) resolvedId = mdByName;
  5184 + if (string.IsNullOrWhiteSpace(resolvedId))
  5185 + {
  5186 + var ckByName = await _db.Queryable<WtCkEntity>()
  5187 + .Where(c => c.Mdmc == wid)
  5188 + .Select(c => c.Id)
  5189 + .FirstAsync();
  5190 + if (!string.IsNullOrWhiteSpace(ckByName)) resolvedId = ckByName;
  5191 + }
  5192 + if (!string.IsNullOrWhiteSpace(resolvedId))
  5193 + {
  5194 + warehouseIds = await ResolveWarehouseIdsForSpCostFromStoreOrCkAsync(resolvedId);
  5195 + }
  5196 + }
4874 5197 if (warehouseIds.Count == 0) warehouseIds = new List<string> { wid };
4875 5198  
4876 5199 var whSet = new HashSet<string>(
... ... @@ -4962,6 +5285,49 @@ ORDER BY SUM(CAST(c.sl AS DECIMAL(18,4)) * c.cbj) DESC&quot;;
4962 5285 }
4963 5286 }
4964 5287  
  5288 + /// <summary>
  5289 + /// 获赠单成本参考:按 <c>wt_sp_cost</c> 在指定门店/仓库范围内汇总在库数量,仅当在库数量合计 &gt; 0 时返回加权平均成本单价(Σ(sl×cbj)/Σ(sl));无在库则 <c>data</c> 为 null,由用户手工录入。
  5290 + /// </summary>
  5291 + /// <param name="spbh">商品主键 <c>wt_sp.F_Id</c></param>
  5292 + /// <param name="ckOrMdId">入库仓库或门店主键(与 <see cref="GetStockQuantity"/> 展开规则一致)</param>
  5293 + /// <returns>success、data(可空)、qty(在库数量合计)、msg</returns>
  5294 + [HttpGet("GetWarehouseWeightedAvgCost")]
  5295 + public async Task<dynamic> GetWarehouseWeightedAvgCost(string spbh, string ckOrMdId)
  5296 + {
  5297 + if (string.IsNullOrWhiteSpace(spbh) || string.IsNullOrWhiteSpace(ckOrMdId))
  5298 + return new { success = false, msg = "商品编号与仓库不能为空", data = (decimal?)null, qty = 0m };
  5299 +
  5300 + try
  5301 + {
  5302 + var pid = spbh.Trim();
  5303 + var warehouseIds = await ResolveWarehouseIdsForSpCostFromStoreOrCkAsync(ckOrMdId);
  5304 + if (warehouseIds.Count == 0) warehouseIds = new List<string> { ckOrMdId.Trim() };
  5305 + var whSet = new HashSet<string>(
  5306 + warehouseIds.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()),
  5307 + StringComparer.OrdinalIgnoreCase);
  5308 +
  5309 + var rows = await _db.Queryable<WtSpCostEntity>()
  5310 + .Where(c => c.Spbh == pid && whSet.Contains(c.Ck))
  5311 + .Select(c => new { c.Sl, c.Cbj })
  5312 + .ToListAsync();
  5313 +
  5314 + var totalQty = rows.Sum(c => (decimal)c.Sl);
  5315 + if (totalQty <= 0)
  5316 + return new { success = true, data = (decimal?)null, qty = totalQty, msg = "无在库数量,请手动填写成本" };
  5317 +
  5318 + var totalAmt = rows.Sum(c => (decimal)c.Sl * c.Cbj);
  5319 + var avg = Math.Round(totalAmt / totalQty, 4);
  5320 + if (avg <= 0)
  5321 + return new { success = true, data = (decimal?)null, qty = totalQty, msg = "在库但加权成本为0,请手动填写成本" };
  5322 +
  5323 + return new { success = true, data = avg, qty = totalQty, msg = "操作成功" };
  5324 + }
  5325 + catch (Exception ex)
  5326 + {
  5327 + return new { success = false, msg = "查询失败: " + ex.Message, data = (decimal?)null, qty = 0m };
  5328 + }
  5329 + }
  5330 +
4965 5331 private const string PurchaseSummaryJoinFromSql = @"
4966 5332 FROM wt_xsckd_mx mx
4967 5333 INNER JOIN wt_xsckd d ON d.F_Id = mx.djbh
... ... @@ -6075,6 +6441,25 @@ LIMIT {offset}, {pageSize}&quot;;
6075 6441 }
6076 6442  
6077 6443 /// <summary>
  6444 + /// 是否赠送单(仅赠送单)。
  6445 + /// </summary>
  6446 + private static bool IsZsdDocument(string? djlx) =>
  6447 + !string.IsNullOrEmpty(djlx)
  6448 + && djlx.Contains("赠送单", StringComparison.Ordinal);
  6449 +
  6450 + /// <summary>
  6451 + /// 是否获赠单(仅获赠单)。
  6452 + /// </summary>
  6453 + private static bool IsHzdDocument(string? djlx) =>
  6454 + !string.IsNullOrEmpty(djlx)
  6455 + && djlx.Contains("获赠单", StringComparison.Ordinal);
  6456 +
  6457 + /// <summary>
  6458 + /// 是否纳入草稿/审核/反审编辑管控的赠送相关单据(赠送单、获赠单)。
  6459 + /// </summary>
  6460 + private static bool IsGiftAuditDocument(string? djlx) => IsZsdDocument(djlx) || IsHzdDocument(djlx);
  6461 +
  6462 + /// <summary>
6078 6463 /// 将审批备注追加写入 spbz(不写业务备注 bz)
6079 6464 /// </summary>
6080 6465 [NonAction]
... ... @@ -6120,12 +6505,6 @@ LIMIT {offset}, {pageSize}&quot;;
6120 6505 return new { success = false, message = $"该单据不是{expectedDjlx},无法审核" };
6121 6506 }
6122 6507  
6123   - if (entity.Djlx == "销售出库单" && IsSalesOutboundSkipErpAudit(entity))
6124   - {
6125   - _db.RollbackTran();
6126   - return new { success = false, message = "非后台来源的销售出库单无需在此审核" };
6127   - }
6128   -
6129 6508 var actualDjlx = entity.Djlx;
6130 6509  
6131 6510 if (entity.Djzt == "已审核")
... ... @@ -6170,7 +6549,11 @@ LIMIT {offset}, {pageSize}&quot;;
6170 6549 bool hasTwoLevel = !string.IsNullOrWhiteSpace(lvl2.users) || !string.IsNullOrWhiteSpace(lvl2.roles);
6171 6550 bool forceTwoLevelForSamePriceTransfer = entity.Djlx == "同价调拨单";
6172 6551  
6173   - if (entity.Djzt == "待审核" || string.IsNullOrEmpty(entity.Djzt))
  6552 + // 赠送单/获赠单:仅「待审核」可进一级审核(与历史单兼容:其它类型仍允许空状态等同待审)
  6553 + var canEnterLevel1 = string.Equals(entity.Djzt, "待审核", StringComparison.Ordinal)
  6554 + || (string.IsNullOrEmpty(entity.Djzt) && !IsGiftAuditDocument(entity.Djlx));
  6555 +
  6556 + if (canEnterLevel1)
6174 6557 {
6175 6558 // 一级审核
6176 6559 if (entity.Djlx == "同价调拨单")
... ... @@ -6373,9 +6756,6 @@ LIMIT {offset}, {pageSize}&quot;;
6373 6756 [HttpPost("ApproveSalesOutbound/{id}")]
6374 6757 public async Task<dynamic> ApproveSalesOutbound(string id, [FromBody] WtApprovalRemarkInput input)
6375 6758 {
6376   - var header = await _db.Queryable<WtXsckdEntity>().FirstAsync(p => p.Id == id);
6377   - if (header != null && IsSalesOutboundSkipErpAudit(header))
6378   - return new { success = false, message = "非后台来源的销售出库单无需在此审核" };
6379 6759 return await ApproveDocument(id, "销售出库单", input?.remark);
6380 6760 }
6381 6761  
... ... @@ -6437,9 +6817,6 @@ LIMIT {offset}, {pageSize}&quot;;
6437 6817 if (!string.IsNullOrEmpty(expectedDjlx) && entity.Djlx != expectedDjlx)
6438 6818 return new { success = false, message = $"该单据不是{expectedDjlx},无法操作" };
6439 6819  
6440   - if (entity.Djlx == "销售出库单" && IsSalesOutboundSkipErpAudit(entity))
6441   - return new { success = false, message = "非后台来源的销售出库单无需在此审核" };
6442   -
6443 6820 if (entity.Djzt == "已审核")
6444 6821 return new { success = false, message = "该单据已审核通过,如需修改请使用反审" };
6445 6822  
... ... @@ -6577,10 +6954,11 @@ LIMIT {offset}, {pageSize}&quot;;
6577 6954 }
6578 6955 }
6579 6956  
6580   - // ★ 更新状态为审核不通过
  6957 + // ★ 销售出库单:审核不通过直接退回草稿,便于二次编辑并再次提交审核
  6958 + var isSalesOutbound = string.Equals(row.Djlx, "销售出库单", StringComparison.Ordinal);
6581 6959 AppendApprovalSpbzLine(row, "审核不通过", approvalRemark);
6582   - row.Djzt = "审核不通过";
6583   - row.Shr = userId;
  6960 + row.Djzt = isSalesOutbound ? "草稿" : "审核不通过";
  6961 + row.Shr = isSalesOutbound ? null : userId;
6584 6962 row.Shr1 = null;
6585 6963 row.Shr2 = null;
6586 6964 await _db.Updateable(row)
... ... @@ -6588,7 +6966,11 @@ LIMIT {offset}, {pageSize}&quot;;
6588 6966 .ExecuteCommandAsync();
6589 6967  
6590 6968 _db.CommitTran();
6591   - return new { success = true, message = "已标记为审核不通过" };
  6969 + return new
  6970 + {
  6971 + success = true,
  6972 + message = isSalesOutbound ? "审核不通过,已退回草稿,可编辑后再次提交审核" : "已标记为审核不通过"
  6973 + };
6592 6974 }
6593 6975 catch (AppFriendlyException)
6594 6976 {
... ... @@ -6628,8 +7010,15 @@ LIMIT {offset}, {pageSize}&quot;;
6628 7010 if (entity == null)
6629 7011 return new { success = false, message = "单据不存在" };
6630 7012  
6631   - if (entity.Djzt != "已审核" && entity.Djzt != "一级已审" && entity.Djzt != "一级已审/待二级")
  7013 + if (IsGiftAuditDocument(entity.Djlx))
  7014 + {
  7015 + if (!string.Equals(entity.Djzt, "已审核", StringComparison.Ordinal))
  7016 + return new { success = false, message = "赠送/获赠单仅「已审核」状态可反审" };
  7017 + }
  7018 + else if (entity.Djzt != "已审核" && entity.Djzt != "一级已审" && entity.Djzt != "一级已审/待二级")
  7019 + {
6632 7020 return new { success = false, message = "当前单据状态不允许反审" };
  7021 + }
6633 7022  
6634 7023 var userInfo = await _userManager.GetUserInfo();
6635 7024 var userId = userInfo?.userId ?? "";
... ... @@ -6680,11 +7069,21 @@ LIMIT {offset}, {pageSize}&quot;;
6680 7069 if (mxList.Count > 0)
6681 7070 await RollbackSamePriceTransferStock(entity, mxList);
6682 7071 }
  7072 + else if (IsGiftAuditDocument(entity.Djlx) && string.Equals(entity.Djzt, "已审核", StringComparison.Ordinal))
  7073 + {
  7074 + var zsdMx = await _db.Queryable<WtXsckdMxEntity>()
  7075 + .Where(d => d.Djbh == id)
  7076 + .ToListAsync();
  7077 + if (zsdMx.Count > 0)
  7078 + await RollbackSpCostOnDelete(entity);
  7079 + }
6683 7080  
6684   - entity.Djzt = (string.Equals(entity.Djlx, "同价调拨单", StringComparison.Ordinal)
6685   - || string.Equals(entity.Djlx, "采购入库单", StringComparison.Ordinal))
6686   - ? "草稿"
6687   - : "待审核";
  7081 + var backToDraft = string.Equals(entity.Djlx, "同价调拨单", StringComparison.Ordinal)
  7082 + || string.Equals(entity.Djlx, "采购入库单", StringComparison.Ordinal)
  7083 + || string.Equals(entity.Djlx, "采购退货单", StringComparison.Ordinal)
  7084 + || IsGiftAuditDocument(entity.Djlx)
  7085 + || (string.Equals(entity.Djlx, "销售出库单", StringComparison.Ordinal) && IsSalesOutboundSkipErpAudit(entity));
  7086 + entity.Djzt = backToDraft ? "草稿" : "待审核";
6688 7087 entity.Shr = null;
6689 7088 entity.Shr1 = null;
6690 7089 entity.Shr2 = null;
... ... @@ -6693,8 +7092,7 @@ LIMIT {offset}, {pageSize}&quot;;
6693 7092 .ExecuteCommandAsync();
6694 7093  
6695 7094 _db.CommitTran();
6696   - var reverseMsg = (string.Equals(entity.Djlx, "同价调拨单", StringComparison.Ordinal)
6697   - || string.Equals(entity.Djlx, "采购入库单", StringComparison.Ordinal))
  7095 + var reverseMsg = backToDraft
6698 7096 ? "反审成功,单据已恢复为草稿"
6699 7097 : "反审成功,单据已恢复为待审核状态";
6700 7098 return new { success = true, message = reverseMsg };
... ... @@ -7833,6 +8231,16 @@ LIMIT {offset}, {pageSize}&quot;;
7833 8231 "UPDATE wt_xsckd_mx SET cbdj = @cbdj, cbje = @cbje WHERE F_Id = @id",
7834 8232 new { cbdj = snapCbdj, cbje = Math.Round(snapCbdj * qty, 2), id = detail.Id });
7835 8233 }
  8234 + // 报溢单:用户录入的成本单价作为 cbdj 快照,避免「审核后列表金额显示 0」。若用户未填 Dj,则回退到候选仓 wt_sp_cost 历史成本
  8235 + else if (djlx.Contains("报溢单"))
  8236 + {
  8237 + decimal byCbdj = detail.Dj > 0
  8238 + ? detail.Dj
  8239 + : await QueryWtSpCostCbjForWarehouseCandidatesAsync(detail.Spbh, warehouseIds);
  8240 + await _db.Ado.ExecuteCommandAsync(
  8241 + "UPDATE wt_xsckd_mx SET cbdj = @cbdj, cbje = @cbje WHERE F_Id = @id",
  8242 + new { cbdj = byCbdj, cbje = Math.Round(byCbdj * qty, 2), id = detail.Id });
  8243 + }
7836 8244  
7837 8245 var strictSerial = await IsProductStrictSerialOutboundAsync(detail.Spbh);
7838 8246 if (strictSerial)
... ... @@ -8652,15 +9060,15 @@ LIMIT {offset}, {pageSize}&quot;;
8652 9060 }
8653 9061  
8654 9062 /// <summary>
8655   - /// 变价调拨单过账:调出按快照原成本扣减,调入按变价后单价增加,并回写明细 cbdj/bjhcb/cbje
  9063 + /// 变价调拨单过账:调出仓按调出仓加权平均成本扣减对应数量(部分数量变价亦按加权平均,不区分个别品),
  9064 + /// 调入仓按「明细变价后单价(bjhcb)」加权增加。若未提供 bjhcb 则按表头变价系数(bjsx)折算;
  9065 + /// 两者均未提供则抛错。回写明细 cbdj/bjhcb/cbje。
8656 9066 /// </summary>
8657 9067 private async Task ApplyVariablePriceTransferCost(WtXsckdEntity entity, List<WtXsckdMxEntity> mxList)
8658 9068 {
8659 9069 if (string.Equals((entity.Cjck ?? "").Trim(), (entity.Rkck ?? "").Trim(), StringComparison.Ordinal))
8660 9070 throw new Exception("变价调拨单出库仓与入库仓不能相同");
8661 9071 var coef = entity.Bjsx ?? 0;
8662   - if (coef <= 0)
8663   - throw new Exception("变价系数必须大于0");
8664 9072  
8665 9073 foreach (var detail in mxList)
8666 9074 {
... ... @@ -8672,15 +9080,33 @@ LIMIT {offset}, {pageSize}&quot;;
8672 9080 if (string.IsNullOrEmpty(outRef) || string.IsNullOrEmpty(inRef))
8673 9081 throw new Exception($"商品 {detail.Spmc ?? detail.Spbh} 未指定完整的出库/入库仓库");
8674 9082  
  9083 + // 调出仓选取 wt_sp_cost(该仓加权平均成本,部分变价亦按此单价扣减,保证该仓保持平均成本)
8675 9084 var outIds = await ResolveWarehouseIdListAsync(outRef);
8676 9085 var (outCk, originalUnit, _) = await PickOutboundCostRowForTransferAsync(detail.Spbh, outIds, qty);
8677   - var adjustedUnit = ComputeVariableAdjustedUnitCost(originalUnit, coef);
  9086 +
  9087 + // 优先使用明细录入的 bjhcb;否则用表头变价系数换算;两者均无则报错
  9088 + decimal adjustedUnit = 0;
  9089 + if (detail.Bjhcb.HasValue && detail.Bjhcb.Value > 0)
  9090 + {
  9091 + adjustedUnit = Math.Round(detail.Bjhcb.Value, 4);
  9092 + }
  9093 + else if (coef > 0)
  9094 + {
  9095 + adjustedUnit = ComputeVariableAdjustedUnitCost(originalUnit, coef);
  9096 + }
  9097 + else
  9098 + {
  9099 + throw new Exception($"商品 {detail.Spmc ?? detail.Spbh} 未填写变价后成本,且未设置变价系数%");
  9100 + }
  9101 +
8678 9102 var inIds = await ResolveWarehouseIdListAsync(inRef);
8679 9103 if (inIds == null || inIds.Count == 0)
8680 9104 throw new Exception($"商品 {detail.Spbh} 未解析到入库仓库");
8681 9105 var inCk = inIds[0];
8682 9106  
  9107 + // 调出仓按原加权平均成本扣减(保持该仓平均成本不变)
8683 9108 await SpCostWeightedRemoveAsync(detail.Spbh, outCk, qty, originalUnit);
  9109 + // 调入仓按变价后单价加权增加(与该仓其他库存进一步加权平均)
8684 9110 await SpCostWeightedAddAsync(detail.Spbh, inCk, qty, adjustedUnit);
8685 9111  
8686 9112 await _db.Ado.ExecuteCommandAsync(
... ...
Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtYskzjjsService.cs
... ... @@ -482,7 +482,10 @@ namespace NCC.Extend.WtYskzjjs
482 482 if (entitys.Count > 0)
483 483 {
484 484 foreach (var e in entitys)
  485 + {
485 486 ThrowIfTransferBillCannotDelete(e);
  487 + ThrowIfCashExpenseBillCannotDelete(e);
  488 + }
486 489 try
487 490 {
488 491 //开启事务
... ... @@ -518,6 +521,7 @@ namespace NCC.Extend.WtYskzjjs
518 521 var existingHead = await _db.Queryable<WtYskzjjsEntity>().FirstAsync(p => p.Id == id);
519 522 _ = existingHead ?? throw NCCException.Oh(ErrorCode.COM1005);
520 523 ThrowIfTransferBillLockedForEdit(existingHead);
  524 + ThrowIfCashExpenseBillLockedForEdit(existingHead);
521 525  
522 526 NormalizeDjlxForQt(input);
523 527 NormalizeDjlxForTransfer(input);
... ... @@ -589,6 +593,7 @@ namespace NCC.Extend.WtYskzjjs
589 593 var entity = await _db.Queryable<WtYskzjjsEntity>().FirstAsync(p => p.Id == id);
590 594 _ = entity ?? throw NCCException.Oh(ErrorCode.COM1005);
591 595 ThrowIfTransferBillCannotDelete(entity);
  596 + ThrowIfCashExpenseBillCannotDelete(entity);
592 597 try
593 598 {
594 599 //开启事务
... ... @@ -677,6 +682,11 @@ namespace NCC.Extend.WtYskzjjs
677 682 return !string.IsNullOrWhiteSpace(djlx) && djlx.Trim().Contains("转款", StringComparison.Ordinal);
678 683 }
679 684  
  685 + private static bool IsCashExpenseDjlx(string? djlx)
  686 + {
  687 + return !string.IsNullOrWhiteSpace(djlx) && djlx.Trim().Contains("现金费用单", StringComparison.Ordinal);
  688 + }
  689 +
680 690 private static void ThrowIfTransferBillLockedForEdit(WtYskzjjsEntity existing)
681 691 {
682 692 if (existing == null || !IsTransferDjlx(existing.Djlx)) return;
... ... @@ -685,6 +695,14 @@ namespace NCC.Extend.WtYskzjjs
685 695 throw NCCException.Oh($"转款单当前状态「{existing.Djzt}」不可编辑,请查看或反审后再编辑");
686 696 }
687 697  
  698 + private static void ThrowIfCashExpenseBillLockedForEdit(WtYskzjjsEntity existing)
  699 + {
  700 + if (existing == null || !IsCashExpenseDjlx(existing.Djlx)) return;
  701 + var z = (existing.Djzt ?? string.Empty).Trim();
  702 + if (z == "草稿") return;
  703 + throw NCCException.Oh($"现金费用单当前状态「{existing.Djzt}」不可编辑,仅草稿状态可编辑");
  704 + }
  705 +
688 706 private static void ThrowIfTransferBillCannotDelete(WtYskzjjsEntity existing)
689 707 {
690 708 if (existing == null || !IsTransferDjlx(existing.Djlx)) return;
... ... @@ -693,6 +711,14 @@ namespace NCC.Extend.WtYskzjjs
693 711 throw NCCException.Oh($"转款单当前状态「{existing.Djzt}」不可删除");
694 712 }
695 713  
  714 + private static void ThrowIfCashExpenseBillCannotDelete(WtYskzjjsEntity existing)
  715 + {
  716 + if (existing == null || !IsCashExpenseDjlx(existing.Djlx)) return;
  717 + var z = (existing.Djzt ?? string.Empty).Trim();
  718 + if (z == "草稿") return;
  719 + throw NCCException.Oh($"现金费用单当前状态「{existing.Djzt}」不可删除,仅草稿状态可删除");
  720 + }
  721 +
696 722 /// <summary>
697 723 /// 审核状态字符串去空白,便于前端与 <c>待审核</c> 等字面量一致匹配。
698 724 /// </summary>
... ...
Antis.Erp.Plat/sy/css/pos-unified.css
... ... @@ -1255,6 +1255,39 @@ body.pos-page--embed .pos-embed-select {
1255 1255 box-sizing: border-box;
1256 1256 }
1257 1257  
  1258 +/* 购物车行:售后规则提示(质保时间等,来自商品档案;下单时落成明细快照) */
  1259 +.pos-cart-shgz-tip {
  1260 + margin-top: 8px;
  1261 + padding: 6px 10px;
  1262 + display: flex;
  1263 + align-items: flex-start;
  1264 + gap: 8px;
  1265 + background: #fff1f0;
  1266 + border: 1px solid #ffccc7;
  1267 + border-radius: 8px;
  1268 + color: #cf1322;
  1269 + font-size: 12px;
  1270 + line-height: 1.5;
  1271 + word-break: break-all;
  1272 +}
  1273 +
  1274 +.pos-cart-shgz-tip__label {
  1275 + flex: 0 0 auto;
  1276 + padding: 1px 6px;
  1277 + background: #cf1322;
  1278 + color: #fff;
  1279 + border-radius: 4px;
  1280 + font-size: 11px;
  1281 + font-weight: 600;
  1282 + letter-spacing: 0.5px;
  1283 +}
  1284 +
  1285 +.pos-cart-shgz-tip__text {
  1286 + flex: 1 1 auto;
  1287 + min-width: 0;
  1288 + font-weight: 500;
  1289 +}
  1290 +
1258 1291 /* 分类 + 分段切换 */
1259 1292 .pos-page .pos-home-category-row {
1260 1293 display: flex;
... ...
Antis.Erp.Plat/sy/home.html
... ... @@ -589,6 +589,11 @@
589 589 </div>
590 590 </div>
591 591 </div>
  592 + <!-- 售后规则(质保等):来自商品档案,下单时随快照带到出库单 -->
  593 + <div v-if="item.shgz" class="pos-cart-shgz-tip" :title="item.shgz">
  594 + <span class="pos-cart-shgz-tip__label">售后规则</span>
  595 + <span class="pos-cart-shgz-tip__text">{{ item.shgz }}</span>
  596 + </div>
592 597 <!-- 序列号选择区域:预售商品不卡序列号,不显示 -->
593 598 <div v-if="item.spbm && !item.isPresale" class="pos-cart-serial-box">
594 599 <div v-if="item.selectedSerialNumbers && item.selectedSerialNumbers.length > 0" style="margin-bottom: 5px;">
... ...
Antis.Erp.Plat/sy/settlement.html
... ... @@ -3852,6 +3852,11 @@
3852 3852 <div>单品改价后: ¥{{parseFloat(item.modifiedPrice || 0).toFixed(2)}}</div>
3853 3853 </div>
3854 3854 </div>
  3855 + <!-- 售后规则(质保等):来自商品档案,下单时会随快照写入 wt_xsckd_mx.shgz,后续变更不影响历史单 -->
  3856 + <div v-if="getItemShgz(item)" class="pos-cart-shgz-tip" :title="getItemShgz(item)">
  3857 + <span class="pos-cart-shgz-tip__label">售后规则</span>
  3858 + <span class="pos-cart-shgz-tip__text">{{ getItemShgz(item) }}</span>
  3859 + </div>
3855 3860 <!-- ✅ 套餐商品:显示套餐内商品列表 -->
3856 3861 <div v-if="item.isPackageItem && item.packageItems && item.packageItems.length > 0" class="pos-cart-package-inner">
3857 3862 <div class="pos-cart-package-inner-title">套餐内商品</div>
... ... @@ -5969,6 +5974,8 @@
5969 5974 "sl": actualQuantity,
5970 5975 "dj": itemPrice,
5971 5976 "je": itemPrice * actualQuantity,
  5977 + // 售后规则快照:锁定下单当时的规则;缺失时后端会按 spbh 从商品档案再兜底
  5978 + "shgz": (pkgItem && pkgItem.shgz) || '',
5972 5979 });
5973 5980 });
5974 5981 } else {
... ... @@ -5985,6 +5992,8 @@
5985 5992 "sl": packageQuantity,
5986 5993 "dj": packagePrice,
5987 5994 "je": packagePrice * packageQuantity,
  5995 + // 售后规则快照:套餐行本身没有 shgz,交由后端按 spbh 自动兜底
  5996 + "shgz": (item && item.shgz) || '',
5988 5997 });
5989 5998 }
5990 5999 } else {
... ... @@ -6001,6 +6010,8 @@
6001 6010 "sl": item.quantity,
6002 6011 "dj": itemPrice,
6003 6012 "je": itemPrice * item.quantity,
  6013 + // 售后规则快照:锁定下单当时的质保等规则;缺失时后端会按 spbh 从商品档案再兜底
  6014 + "shgz": (item && item.shgz) || '',
6004 6015 });
6005 6016 }
6006 6017 }
... ... @@ -6533,6 +6544,27 @@
6533 6544 const price = this.getSettlementLineUnitPrice(item);
6534 6545 return price * item.quantity;
6535 6546 },
  6547 + /**
  6548 + * 售后规则展示:
  6549 + * 单品直接取 item.shgz;
  6550 + * 套装取套装内各子商品 shgz,非空去重后以「、」拼接(常见场景是套装内各子商品规则一致)。
  6551 + * 为空返回 '',模板上层 v-if 不渲染提示条。
  6552 + */
  6553 + getItemShgz(item) {
  6554 + if (!item) return '';
  6555 + if (item.isPackageItem) {
  6556 + const pkgList = Array.isArray(item.packageItems) ? item.packageItems : [];
  6557 + if (pkgList.length > 0) {
  6558 + const set = new Set();
  6559 + pkgList.forEach(p => {
  6560 + const s = (p && typeof p.shgz === 'string') ? p.shgz.trim() : '';
  6561 + if (s) set.add(s);
  6562 + });
  6563 + if (set.size > 0) return Array.from(set).join('、');
  6564 + }
  6565 + }
  6566 + return (item.shgz || '').toString().trim();
  6567 + },
6536 6568 // ✅ 打开改价模态框
6537 6569 async openPriceEditModal(item, index) {
6538 6570 // ✅ 如果是套餐商品,找到实际的第一个子商品(用于改价)
... ...