auto complete order
This commit is contained in:
@@ -65,7 +65,7 @@
|
||||
<div class="description" v-text="config.description" />
|
||||
</div>
|
||||
</t-form-item>
|
||||
<t-form-item label="成交价(元)" label-width="6rem" required-mark>
|
||||
<t-form-item label="成交价" label-width="6rem" required-mark>
|
||||
<t-input
|
||||
v-model.trim="saleAmountText"
|
||||
type="number"
|
||||
@@ -170,6 +170,41 @@
|
||||
/>
|
||||
</div>
|
||||
</t-form-item>
|
||||
<t-form-item label="已全额收款和核销" content-align="right">
|
||||
<t-switch
|
||||
v-model="receiveFullPayment"
|
||||
size="small"
|
||||
:disabled="!canReceiveImmediately"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label="已全部产品履约和核销" content-align="right">
|
||||
<t-switch
|
||||
v-model="fulfillAll"
|
||||
size="small"
|
||||
:disabled="!canFulfillImmediately"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label-width="0">
|
||||
<div class="red">全部履约会同步启用全额收款;请先准备全部产品库存,任意一步失败,整张订单都会回滚。</div>
|
||||
</t-form-item>
|
||||
<t-form-item v-if="receiveFullPayment && parsedSaleAmount" label-width="0">
|
||||
<photo-scroller
|
||||
:model-value="paymentPhotoUrlList"
|
||||
v-model:photo-items="paymentPhotoItems"
|
||||
:upload="uploadPhotoList"
|
||||
:edit-mode="true"
|
||||
title="收款凭证照片(必要)"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item v-if="fulfillAll" label-width="0">
|
||||
<photo-scroller
|
||||
:model-value="fulfillmentPhotoUrlList"
|
||||
v-model:photo-items="fulfillmentPhotoItems"
|
||||
:upload="uploadPhotoList"
|
||||
:edit-mode="true"
|
||||
title="履约凭证照片(必要)"
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label="购买备注">
|
||||
<t-textarea
|
||||
v-model.trim="note"
|
||||
@@ -195,26 +230,41 @@
|
||||
:enable-scan="false"
|
||||
@change="handleCustomerChange"
|
||||
/>
|
||||
<t-popup v-model="configSelectorVisible" placement="bottom">
|
||||
<div class="config-selector bg-white">
|
||||
<div class="popup-header">
|
||||
<t-button variant="text" size="small" @click="configSelectorVisible = false">关闭</t-button>
|
||||
<h3>选择套餐</h3>
|
||||
<span />
|
||||
</div>
|
||||
<bottom-popup
|
||||
v-model="configSelectorVisible"
|
||||
title="选择套餐"
|
||||
height="70vh"
|
||||
right-button-text=""
|
||||
@left-button-click="configSelectorVisible = false"
|
||||
>
|
||||
<div class="config-selector">
|
||||
<t-cell-group v-if="configList.length">
|
||||
<t-cell
|
||||
v-for="item in configList"
|
||||
:key="item.id"
|
||||
class="config-option"
|
||||
:title="item.name || '未命名套餐'"
|
||||
:note="`标准价:${formatPackageAmount(item.standardAmount)}`"
|
||||
:note="formatPackageAmount(item.standardAmount)"
|
||||
arrow
|
||||
@click="selectConfig(item)"
|
||||
/>
|
||||
>
|
||||
<template #description>
|
||||
<div class="products">
|
||||
<t-tag
|
||||
v-for="productItem in item.itemList || []"
|
||||
:key="productItem.id || `${productItem.skuId}-${productItem.skuUnitId}`"
|
||||
:content="getConfigProductText(productItem)"
|
||||
theme="primary"
|
||||
variant="light"
|
||||
/>
|
||||
<t-tag v-if="!item.itemList?.length" content="暂无产品权益" />
|
||||
</div>
|
||||
</template>
|
||||
</t-cell>
|
||||
</t-cell-group>
|
||||
<div v-else class="center-tips" v-text="'暂无可购买套餐'" />
|
||||
</div>
|
||||
</t-popup>
|
||||
</bottom-popup>
|
||||
<t-popup v-model="unitPickerVisible" placement="bottom">
|
||||
<t-picker v-model="unitPickerDraft" :columns="unitPickerOptions" title="调整权益单位" @cancel="unitPickerVisible = false" @confirm="confirmItemUnitPicker" />
|
||||
</t-popup>
|
||||
@@ -252,6 +302,8 @@
|
||||
<script setup lang="ts">
|
||||
import { Time } from "timi-web";
|
||||
import { useUserStore } from "timi-web-api";
|
||||
import { BottomPopup } from "timi-tdesign-mobile";
|
||||
import { DialogPlugin } from "tdesign-mobile-vue";
|
||||
import CustomerAPI from "@/api/CustomerAPI";
|
||||
import PackageAPI from "@/api/PackageAPI";
|
||||
import ProductAPI from "@/api/ProductAPI";
|
||||
@@ -265,14 +317,16 @@ import { useNavBarStore } from "@/store/navBarStore";
|
||||
import { useSettingStore } from "@/store/settingStore";
|
||||
import type { Customer } from "@/types/Customer";
|
||||
import type { PhotoItem } from "@/types/Photo";
|
||||
import type { GaoPackageConfig, PackageAttachment, PackagePurchaseItem } from "@/types/Package";
|
||||
import type { GaoPackageConfig, PackageAttachment, PackageConfigItem, PackagePurchaseItem } from "@/types/Package";
|
||||
import type { Product, ProductSku, ProductSkuUnit } from "@/types/Product";
|
||||
import { getErrorMessage } from "@/utils/error";
|
||||
import {
|
||||
addPackageQuantity,
|
||||
convertPackageQuantity,
|
||||
formatPackageAmount,
|
||||
normalizePackageQuantity
|
||||
getPackageQuantityText,
|
||||
normalizePackageQuantity,
|
||||
parsePackageAmount
|
||||
} from "@/utils/package";
|
||||
import { Message } from "@/utils/message";
|
||||
import { permissionCodes } from "@/utils/permission";
|
||||
@@ -423,15 +477,22 @@ function parseDateTime(value: string): Date {
|
||||
const note = ref("");
|
||||
const isSaving = ref(false);
|
||||
const photoItems = ref<PhotoItem[]>([]);
|
||||
const paymentPhotoItems = ref<PhotoItem[]>([]);
|
||||
const fulfillmentPhotoItems = ref<PhotoItem[]>([]);
|
||||
const photoUrlList = computed(() => photoItems.value.map(item => item.url));
|
||||
const paymentPhotoUrlList = computed(() => paymentPhotoItems.value.map(item => item.url));
|
||||
const fulfillmentPhotoUrlList = computed(() => fulfillmentPhotoItems.value.map(item => item.url));
|
||||
|
||||
function buildAttachments(): PackageAttachment[] {
|
||||
return photoItems.value.flatMap(item => item.tempFileId ? [{
|
||||
function buildAttachments(source: PhotoItem[] = photoItems.value): PackageAttachment[] {
|
||||
return source.flatMap(item => item.tempFileId ? [{
|
||||
tempFileId: item.tempFileId,
|
||||
attachType: item.attachType || "PHOTO"
|
||||
}] : []);
|
||||
}
|
||||
|
||||
const receiveFullPayment = ref(false);
|
||||
const fulfillAll = ref(false);
|
||||
|
||||
const {
|
||||
isSaleable,
|
||||
canPriceOverride,
|
||||
@@ -446,9 +507,32 @@ const {
|
||||
config,
|
||||
selectedCustomerList,
|
||||
note,
|
||||
receiveFullPayment,
|
||||
fulfillAll,
|
||||
hasPermission: permissionCode => userStore.hasPermissions(permissionCode),
|
||||
buildRegisteredAt: buildPurchasedAt,
|
||||
buildAttachments
|
||||
buildAttachments,
|
||||
buildPaymentAttachments: () => buildAttachments(paymentPhotoItems.value),
|
||||
buildFulfillmentAttachments: () => buildAttachments(fulfillmentPhotoItems.value)
|
||||
});
|
||||
|
||||
const parsedSaleAmount = computed(() => parsePackageAmount(saleAmountText.value));
|
||||
const canReceiveImmediately = computed(() => userStore.hasPermissions(permissionCodes.fundReceive.create)
|
||||
&& userStore.hasPermissions(permissionCodes.fundReceive.verify));
|
||||
const canFulfillImmediately = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.create)
|
||||
&& userStore.hasPermissions(permissionCodes.orderFulfillment.verify)
|
||||
&& (parsedSaleAmount.value === 0 || canReceiveImmediately.value));
|
||||
|
||||
watch(fulfillAll, value => {
|
||||
if (value && parsedSaleAmount.value !== 0) {
|
||||
receiveFullPayment.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
watch(receiveFullPayment, value => {
|
||||
if (!value && parsedSaleAmount.value !== 0) {
|
||||
fulfillAll.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
const unitPickerOptions = computed(() => {
|
||||
@@ -456,6 +540,12 @@ const unitPickerOptions = computed(() => {
|
||||
return item ? getSelectableUnitList(item).map(unit => ({ label: unit.name, value: unit.id || "" })) : [];
|
||||
});
|
||||
|
||||
function getConfigProductText(item: PackageConfigItem): string {
|
||||
const quantity = getPackageQuantityText(item.defaultQuantity);
|
||||
const skuText = getProductSkuSnapshotText(item.productNameSnapshot, item.skuNameSnapshot, item.specSummarySnapshot);
|
||||
return `${item.productNameSnapshot || "未命名产品"}${skuText ? ` · ${skuText}` : ""} × ${quantity}${item.unitSnapshot ? ` ${item.unitSnapshot}` : ""}`;
|
||||
}
|
||||
|
||||
function getItemLabel(item: PackagePurchaseItem): string {
|
||||
const configItem = config.value?.itemList?.find(configItem => configItem.skuId === item.skuId);
|
||||
const product = item.gifted ? giftProductMap[item.skuId] : undefined;
|
||||
@@ -647,6 +737,28 @@ async function submitRecord(): Promise<void> {
|
||||
if (!validatePurchaseForm() || !validateItemDecimalQuantity()) {
|
||||
return;
|
||||
}
|
||||
if (receiveFullPayment.value || fulfillAll.value) {
|
||||
const dialog = DialogPlugin.confirm({
|
||||
title: "确认直通完成交易",
|
||||
content: fulfillAll.value
|
||||
? "将一次性购买套餐、核销全额收款,并完成全部履约和库存扣减。提交后如需更正,只能走撤销或冲正流程。"
|
||||
: "将一次性购买套餐并核销全额收款。提交后如需更正,只能走资金冲正流程。",
|
||||
cancelBtn: "取消",
|
||||
confirmBtn: "确认并提交",
|
||||
onCancel() {
|
||||
dialog.destroy();
|
||||
},
|
||||
async onConfirm() {
|
||||
dialog.destroy();
|
||||
await purchasePackage();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
await purchasePackage();
|
||||
}
|
||||
|
||||
async function purchasePackage(): Promise<void> {
|
||||
|
||||
isSaving.value = true;
|
||||
try {
|
||||
@@ -656,7 +768,7 @@ async function submitRecord(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
await settingStore.playRegisterSuccessSound();
|
||||
Message.success("套餐购买已提交");
|
||||
Message.success(fulfillAll.value ? "套餐、收款及履约已完成" : receiveFullPayment.value ? "套餐及全额收款已完成" : "套餐购买已提交");
|
||||
await router.replace({
|
||||
name: "SalesOrderDetail",
|
||||
params: { id: result.salesOrderId },
|
||||
@@ -749,6 +861,25 @@ watch(config, value => {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.straight-through {
|
||||
width: 100%;
|
||||
padding: .75rem;
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
border-radius: .875rem;
|
||||
|
||||
.title {
|
||||
font-size: .9375rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tips {
|
||||
color: var(--td-text-color-secondary);
|
||||
margin: .5rem .5rem 0;
|
||||
font-size: .75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.customer-profile {
|
||||
margin: 1rem;
|
||||
}
|
||||
@@ -881,7 +1012,21 @@ watch(config, value => {
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.config-selector,
|
||||
.config-selector {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
.config-option {
|
||||
.products {
|
||||
gap: .35rem;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selected-customer-popup {
|
||||
height: 70vh;
|
||||
overflow: auto;
|
||||
|
||||
@@ -10,9 +10,13 @@ interface UsePackagePurchaseOptions {
|
||||
config: Ref<GaoPackageConfig | undefined>;
|
||||
selectedCustomerList: Ref<Customer[]>;
|
||||
note: Ref<string>;
|
||||
receiveFullPayment: Ref<boolean>;
|
||||
fulfillAll: Ref<boolean>;
|
||||
hasPermission: (permissionCode: string) => boolean;
|
||||
buildRegisteredAt: () => number;
|
||||
buildAttachments: () => PackageAttachment[];
|
||||
buildPaymentAttachments: () => PackageAttachment[];
|
||||
buildFulfillmentAttachments: () => PackageAttachment[];
|
||||
}
|
||||
|
||||
/** 套餐配置直接购买流程,购买请求只依赖套餐配置 ID,不再关联购买事件。 */
|
||||
@@ -91,12 +95,19 @@ export function usePackagePurchase(options: UsePackagePurchaseOptions) {
|
||||
saleAmount,
|
||||
purchasedAt: options.buildRegisteredAt(),
|
||||
attachmentList: options.buildAttachments(),
|
||||
straightThrough: options.receiveFullPayment.value || options.fulfillAll.value ? {
|
||||
receiveFullPayment: options.receiveFullPayment.value,
|
||||
paymentAttachmentList: options.buildPaymentAttachments(),
|
||||
fulfillAll: options.fulfillAll.value,
|
||||
fulfillmentAttachmentList: options.buildFulfillmentAttachments()
|
||||
} : undefined,
|
||||
itemList,
|
||||
remark: options.note.value.trim() || null
|
||||
});
|
||||
}
|
||||
|
||||
function validatePurchaseForm(): boolean {
|
||||
const saleAmount = parsePackageAmount(saleAmountText.value);
|
||||
if (!options.hasPermission(permissionCodes.customerPackage.create)) {
|
||||
Message.warning("当前账号没有套餐购买权限");
|
||||
return false;
|
||||
@@ -113,7 +124,7 @@ export function usePackagePurchase(options: UsePackagePurchaseOptions) {
|
||||
Message.warning("套餐当前不可购买,请检查套餐状态和有效期");
|
||||
return false;
|
||||
}
|
||||
if (parsePackageAmount(saleAmountText.value) === undefined) {
|
||||
if (saleAmount === undefined) {
|
||||
Message.warning("成交价必须是合法的元金额,最多两位小数");
|
||||
return false;
|
||||
}
|
||||
@@ -139,6 +150,31 @@ export function usePackagePurchase(options: UsePackagePurchaseOptions) {
|
||||
Message.warning("套餐至少需要保留一项正数量权益");
|
||||
return false;
|
||||
}
|
||||
const canReceiveImmediately = options.hasPermission(permissionCodes.fundReceive.create)
|
||||
&& options.hasPermission(permissionCodes.fundReceive.verify);
|
||||
const canFulfillImmediately = options.hasPermission(permissionCodes.orderFulfillment.create)
|
||||
&& options.hasPermission(permissionCodes.orderFulfillment.verify)
|
||||
&& (saleAmount === 0 || canReceiveImmediately);
|
||||
if (options.receiveFullPayment.value && !canReceiveImmediately) {
|
||||
Message.warning("当前账号没有收款登记和核销权限");
|
||||
return false;
|
||||
}
|
||||
if (options.fulfillAll.value && !canFulfillImmediately) {
|
||||
Message.warning("当前账号没有交易直通所需权限");
|
||||
return false;
|
||||
}
|
||||
if (options.fulfillAll.value && saleAmount !== 0 && !options.receiveFullPayment.value) {
|
||||
Message.warning("全部履约直通必须同时直接全额收款");
|
||||
return false;
|
||||
}
|
||||
if (options.receiveFullPayment.value && saleAmount !== 0 && !options.buildPaymentAttachments().length) {
|
||||
Message.warning("直接全额收款必须上传收款凭证照片");
|
||||
return false;
|
||||
}
|
||||
if (options.fulfillAll.value && !options.buildFulfillmentAttachments().length) {
|
||||
Message.warning("全部履约直通必须上传履约凭证照片");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user