From d0969cb3222a2c262e6274dcfa92b426eb96d59d Mon Sep 17 00:00:00 2001 From: Timi Date: Wed, 16 Sep 2026 13:07:05 +0800 Subject: [PATCH] auto verify for order fulfillment and payment --- src/api/FulfillmentAPI.ts | 5 +- src/api/SalesOrderAPI.ts | 4 + .../fulfillment/OrderFulfillmentCreate.vue | 73 ++++++++++++++++++- src/pages/salesOrder/SalesOrderCreate.vue | 37 +++++++++- src/pages/salesOrder/SalesOrderPayment.vue | 30 ++++++-- 5 files changed, 135 insertions(+), 14 deletions(-) diff --git a/src/api/FulfillmentAPI.ts b/src/api/FulfillmentAPI.ts index 5f30de2..b7ddded 100644 --- a/src/api/FulfillmentAPI.ts +++ b/src/api/FulfillmentAPI.ts @@ -12,6 +12,9 @@ async function detail(id: string): Promise { async function create(request: OrderFulfillmentCreateRequest): Promise { return await axios.post(`${URI}/create`, request); } +async function createAndVerify(request: OrderFulfillmentCreateRequest): Promise { + return await axios.post(`${URI}/create-and-verify`, request); +} async function prepareStock(request: FulfillmentPrepareStockRequest): Promise { return await axios.post(`${URI}/prepare-stock`, request); } @@ -28,4 +31,4 @@ async function revoke(request: FulfillmentRevokeRequest): Promise { async function receivePayment(request: OrderPaymentReceiveRequest): Promise { return await axios.post(`${ORDER_URI}/payment/receive`, request); } +async function receivePaymentAndVerify(request: OrderPaymentReceiveRequest): Promise { + return await axios.post(`${ORDER_URI}/payment/receive-and-verify`, request); +} async function verifyPayment(id: string): Promise { return await axios.post(`${ORDER_URI}/payment/verify`, undefined, { params: { id } }); } @@ -77,6 +80,7 @@ export default { update, cancel, receivePayment, + receivePaymentAndVerify, verifyPayment, rejectPayment, cancelPayment, diff --git a/src/pages/fulfillment/OrderFulfillmentCreate.vue b/src/pages/fulfillment/OrderFulfillmentCreate.vue index 26d87da..cd8fde7 100644 --- a/src/pages/fulfillment/OrderFulfillmentCreate.vue +++ b/src/pages/fulfillment/OrderFulfillmentCreate.vue @@ -89,7 +89,7 @@
- + + + + + @@ -140,6 +146,7 @@ import { comparePackageQuantity, convertPackageQuantity, createPackageRequestNo, import { getErrorMessage } from "@/utils/error"; import { Message } from "@/utils/message"; import { gaoRoleCodes, permissionCodes } from "@/utils/permission"; +import { getProductStockShortageMessage, mergeProductStockRequirements } from "@/utils/productStock"; import { uploadPhotoList } from "@/utils/photo"; import { getProductPhotoURL } from "@/utils/product"; @@ -162,6 +169,7 @@ const photoItems = ref([]); const unitPickerVisible = ref(false); const unitPickerSourceItemId = ref(""); const unitPickerDraft = ref>([]); +const directVerify = ref(false); const photoUrlList = computed(() => photoItems.value.map(item => item.url)); const isPackage = computed(() => order.value?.orderType === "PACKAGE"); const expectedItemType = computed(() => isPackage.value ? "PACKAGE_ENTITLEMENT" : "DIRECT_PRODUCT"); @@ -172,6 +180,10 @@ const unitPickerOptions = computed(() => { return item ? getFulfillmentUnitList(item).map(unit => ({ label: unit.name, value: unit.id || "" })) : []; }); const canFulfillByPayment = computed(() => order.value?.paymentStatus === "PAID" || userStore.hasRoles(gaoRoleCodes.storeManager)); +const canDirectVerify = computed(() => userStore.hasRoles(gaoRoleCodes.storeManager) + && userStore.hasPermissions(permissionCodes.orderFulfillment.create) + && userStore.hasPermissions(permissionCodes.orderFulfillment.verify) + && userStore.hasPermissions(permissionCodes.productStock.read)); async function loadData(): Promise { const salesOrderId = String(route.query.salesOrderId || ""); @@ -343,6 +355,55 @@ function buildAttachments(): Partial[] { }] : []); } +async function validateDirectStock(): Promise { + const selected = sourceItems.value.filter(item => !!item.id && quantityMap[item.id]); + const skuMap = new Map(); + for (const item of selected) { + if (!item.skuId) { + continue; + } + let sku = fulfillmentSkuMap[item.skuId]; + if (!sku) { + try { + sku = await ProductAPI.skuDetail(item.skuId); + } catch { + // 统一在下方提示,避免部分 SKU 查询失败时静默跳过库存校验 + } + } + if (!sku) { + Message.warning(`${item.productNameSnapshot || "产品"} 的库存信息加载失败,请刷新后重试`); + return false; + } + fulfillmentSkuMap[item.skuId] = sku; + skuMap.set(item.skuId, sku); + } + const requirementList = mergeProductStockRequirements(selected.flatMap(item => { + const sku = item.skuId ? skuMap.get(item.skuId) : undefined; + const unit = getFulfillmentUnit(item); + const quantity = normalizePackageQuantity(quantityMap[item.id || ""] || ""); + const baseQuantity = quantity && unit ? toPackageBaseQuantity(quantity, unit.baseQuantityPerUnit) : undefined; + if (!item.skuId || !sku?.stockEnabled || !baseQuantity) { + return []; + } + return [{ skuId: item.skuId, baseQuantity, productName: item.productNameSnapshot || "未命名产品", unitName: "基础单位" }]; + })); + if (!requirementList.length) { + return true; + } + try { + const stockList = await ProductAPI.stockBatchDetail({ skuIdList: requirementList.map(item => item.skuId) }); + const shortageMessage = getProductStockShortageMessage(stockList, requirementList); + if (shortageMessage) { + Message.warning(`${shortageMessage},不能直接核销履约`); + return false; + } + return true; + } catch (error) { + Message.error(getErrorMessage(error, "库存查询失败,暂不能直接核销履约")); + return false; + } +} + function validate(): boolean { if (!order.value?.id) { Message.warning("请从订单详情进入履约创建"); @@ -397,15 +458,19 @@ async function save(): Promise { skuUnitId: fulfillmentUnitMap[item.id!]!, quantity: normalizePackageQuantity(quantityMap[item.id || ""])! })); - const result = await FulfillmentAPI.create({ + const request = { salesOrderId: order.value!.id!, requestNo: requestNo.value, deliveredAt: resolveTimestamp(deliveredAtText.value), itemList, attachmentList: buildAttachments(), remark: remark.value.trim() || null - }); - Message.success("履约单已提交"); + }; + if (directVerify.value && !await validateDirectStock()) return; + const result = directVerify.value + ? await FulfillmentAPI.createAndVerify(request) + : await FulfillmentAPI.create(request); + Message.success(directVerify.value ? "履约单创建并核销成功" : "履约单已提交"); await router.replace(`/management/order-fulfillments/${result.id}`); } catch (error) { Message.error(getErrorMessage(error, "履约单创建失败,重试会复用同一请求号")); diff --git a/src/pages/salesOrder/SalesOrderCreate.vue b/src/pages/salesOrder/SalesOrderCreate.vue index 613aaf4..2a1e114 100644 --- a/src/pages/salesOrder/SalesOrderCreate.vue +++ b/src/pages/salesOrder/SalesOrderCreate.vue @@ -234,11 +234,12 @@ import type { Customer } from "@/types/Customer"; import type { Product, ProductSku, ProductSkuUnit } from "@/types/Product"; import type { PhotoItem } from "@/types/Photo"; import type { DirectProductOrderItemRequest } from "@/types/SalesOrder"; -import { createPackageRequestNo, normalizePackageQuantity, parsePackageAmount } from "@/utils/package"; +import { createPackageRequestNo, normalizePackageQuantity, parsePackageAmount, toPackageBaseQuantity } from "@/utils/package"; import { formatProductAmount, getProductQuantityStep } from "@/utils/product"; import { getErrorMessage } from "@/utils/error"; import { Message } from "@/utils/message"; import { permissionCodes } from "@/utils/permission"; +import { getProductStockShortageMessage, mergeProductStockRequirements } from "@/utils/productStock"; import { uploadPhotoList } from "@/utils/photo"; import { useNavBarStore } from "@/store/navBarStore"; import { useSettingStore } from "@/store/settingStore"; @@ -289,6 +290,7 @@ const canReceiveImmediately = computed(() => userStore.hasPermissions(permission && userStore.hasPermissions(permissionCodes.fundReceive.verify)); const canFulfillImmediately = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.create) && userStore.hasPermissions(permissionCodes.orderFulfillment.verify) + && userStore.hasPermissions(permissionCodes.productStock.read) && (totalAmount.value === 0 || canReceiveImmediately.value)); watch(fulfillAll, value => { @@ -465,6 +467,34 @@ function resolveTimestamp(value: string): number | undefined { function buildAttachments(source: PhotoItem[] = photoItems.value): Partial[] { return source.flatMap(item => item.tempFileId ? [{ tempFileId: item.tempFileId, attachType: "PHOTO" }] : []); } + +async function validateDirectStock(): Promise { + const requirementList = mergeProductStockRequirements(items.value.flatMap(item => { + const sku = getSku(item); + const unit = getUnit(item); + const quantity = normalizePackageQuantity(item.quantity ?? ""); + const baseQuantity = quantity && unit ? toPackageBaseQuantity(quantity, unit.baseQuantityPerUnit) : undefined; + if (!item.skuId || !sku?.stockEnabled || !baseQuantity) { + return []; + } + return [{ skuId: item.skuId, baseQuantity, productName: getProduct(item)?.name || item.productName || "未命名产品", unitName: "基础单位" }]; + })); + if (!requirementList.length) { + return true; + } + try { + const stockList = await ProductAPI.stockBatchDetail({ skuIdList: requirementList.map(item => item.skuId) }); + const shortageMessage = getProductStockShortageMessage(stockList, requirementList); + if (shortageMessage) { + Message.warning(`${shortageMessage},不能直接履约`); + return false; + } + return true; + } catch (error) { + Message.error(getErrorMessage(error, "库存查询失败,暂不能直接履约")); + return false; + } +} function validate(): boolean { if (!customer.value?.id) { Message.warning("请选择客户"); return false; } if (!items.value.length) { Message.warning("请至少添加一项产品"); return false; } @@ -515,6 +545,7 @@ async function submitOrder(): Promise { if (!requestNo.value) requestNo.value = createPackageRequestNo(); isSaving.value = true; try { + if (fulfillAll.value && !await validateDirectStock()) return; const itemList = items.value.map((item, index) => ({ skuId: item.skuId!, skuUnitId: item.skuUnitId!, @@ -715,8 +746,8 @@ onUnmounted(() => navBarStore.clearRight()); .submit-wrap { gap: 1rem; - left: 2rem; - right: 2rem; + left: 1.5rem; + right: 1.5rem; border: 1px solid rgba(0, 0, 0, .05); bottom: calc(env(safe-area-inset-bottom, 0) + 1rem); z-index: 12; diff --git a/src/pages/salesOrder/SalesOrderPayment.vue b/src/pages/salesOrder/SalesOrderPayment.vue index fe4afc6..ed38778 100644 --- a/src/pages/salesOrder/SalesOrderPayment.vue +++ b/src/pages/salesOrder/SalesOrderPayment.vue @@ -15,7 +15,7 @@ @@ -31,8 +31,9 @@ size="extra-small" theme="primary" variant="outline" + content="全额收款" @click="fillUnpaidAmount" - >全额收款 + /> @@ -55,6 +56,12 @@ />
+ + + + ([]); const photoUrlList = computed(() => photoItems.value.map(item => item.url)); const requestNo = ref(""); +const directVerify = ref(false); +const canDirectVerify = computed(() => userStore.hasRoles(gaoRoleCodes.storeManager) + && userStore.hasPermissions(permissionCodes.fundReceive.create) + && userStore.hasPermissions(permissionCodes.fundReceive.verify)); function openOccurredAtPicker(): void { draftTime.value = occurredAtText.value || Time.formatDateTime(new Date()); @@ -162,6 +173,10 @@ async function loadData(): Promise { async function save(): Promise { if (saving.value || !order.value?.id) return; + if (directVerify.value && !canDirectVerify.value) { + Message.warning("当前账号没有店长直接核销权限"); + return; + } const amount = parsePackageAmount(amountText.value); const attachmentList = buildAttachments(); if (amount === undefined || amount === 0) { @@ -175,15 +190,18 @@ async function save(): Promise { if (!requestNo.value) requestNo.value = createPackageRequestNo(); saving.value = true; try { - const record = await SalesOrderAPI.receivePayment({ + const request = { salesOrderId: order.value.id, requestNo: requestNo.value, amount, occurredAt: resolveTimestamp(occurredAtText.value), attachmentList, remark: remark.value.trim() || null - }); - Message.success("收款登记已提交"); + }; + const record = directVerify.value + ? await SalesOrderAPI.receivePaymentAndVerify(request) + : await SalesOrderAPI.receivePayment(request); + Message.success(directVerify.value ? "收款登记并核销成功" : "收款登记已提交"); await router.replace(record.id && userStore.hasPermissions(permissionCodes.fundReceive.create) ? `/management/fund-records/${record.id}` : `/management/sales-orders/${order.value.id}`);