support split unit for product order
This commit is contained in:
@@ -48,22 +48,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input">
|
<div class="input">
|
||||||
|
<t-button
|
||||||
|
size="extra-small"
|
||||||
|
variant="outline"
|
||||||
|
:content="getFulfillmentUnit(item)?.name || '选择实际单位'"
|
||||||
|
@click.stop="openUnitPicker(item)"
|
||||||
|
/>
|
||||||
<t-input
|
<t-input
|
||||||
v-model.trim="quantityMap[item.id || '']"
|
v-model.trim="quantityMap[item.id || '']"
|
||||||
class="quantity"
|
class="quantity"
|
||||||
type="number"
|
type="number"
|
||||||
borderless
|
borderless
|
||||||
align="right"
|
align="right"
|
||||||
|
:disabled="!getFulfillmentUnit(item)"
|
||||||
placeholder="请输入履约数量"
|
placeholder="请输入履约数量"
|
||||||
>
|
>
|
||||||
<template #suffix-icon>
|
<template #suffix-icon>
|
||||||
<span class="unit" v-text="item.unitSnapshot" />
|
<span class="unit" v-text="getFulfillmentUnit(item)?.name || ''" />
|
||||||
</template>
|
</template>
|
||||||
</t-input>
|
</t-input>
|
||||||
<t-button
|
<t-button
|
||||||
size="extra-small"
|
size="extra-small"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:disabled="getAvailableQuantity(item) === '0'"
|
:disabled="getAvailableBaseQuantity(item) === '0'"
|
||||||
@click.stop="fillAllQuantity(item)"
|
@click.stop="fillAllQuantity(item)"
|
||||||
>全部履约</t-button>
|
>全部履约</t-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,24 +107,29 @@
|
|||||||
<t-popup v-model="pickerVisible" placement="bottom">
|
<t-popup v-model="pickerVisible" placement="bottom">
|
||||||
<t-date-time-picker v-model="draftTime" format="YYYY-MM-DD HH:mm" :mode="['date', 'minute']" title="履约时间" @cancel="pickerVisible = false" @confirm="confirmTime" />
|
<t-date-time-picker v-model="draftTime" format="YYYY-MM-DD HH:mm" :mode="['date', 'minute']" title="履约时间" @cancel="pickerVisible = false" @confirm="confirmTime" />
|
||||||
</t-popup>
|
</t-popup>
|
||||||
|
<t-popup v-model="unitPickerVisible" placement="bottom">
|
||||||
|
<t-picker v-model="unitPickerDraft" :columns="unitPickerOptions" title="选择实际履约单位" @cancel="unitPickerVisible = false" @confirm="confirmUnitPicker" />
|
||||||
|
</t-popup>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SalesOrderAPI from "@/api/SalesOrderAPI";
|
import SalesOrderAPI from "@/api/SalesOrderAPI";
|
||||||
import FulfillmentAPI from "@/api/FulfillmentAPI";
|
import FulfillmentAPI from "@/api/FulfillmentAPI";
|
||||||
|
import ProductAPI from "@/api/ProductAPI";
|
||||||
import PhotoScroller from "@/components/PhotoScroller.vue";
|
import PhotoScroller from "@/components/PhotoScroller.vue";
|
||||||
import ProductSkuSnapshotTags from "@/components/ProductSkuSnapshotTags.vue";
|
import ProductSkuSnapshotTags from "@/components/ProductSkuSnapshotTags.vue";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import { useUserStore } from "timi-web-api";
|
import { useUserStore } from "timi-web-api";
|
||||||
import type { GaoOrderFulfillmentItemRequest } from "@/types/Fulfillment";
|
import type { GaoOrderFulfillmentItemRequest } from "@/types/Fulfillment";
|
||||||
import type { GaoSalesOrderItem, SalesOrder } from "@/types/SalesOrder";
|
import type { GaoSalesOrderItem, SalesOrder } from "@/types/SalesOrder";
|
||||||
|
import type { ProductSku, ProductSkuUnit } from "@/types/Product";
|
||||||
import type { PhotoItem } from "@/types/Photo";
|
import type { PhotoItem } from "@/types/Photo";
|
||||||
import type { Attachment } from "timi-web-api";
|
import type { Attachment } from "timi-web-api";
|
||||||
import { comparePackageQuantity, createPackageRequestNo, getPackageQuantityText, normalizePackageQuantity } from "@/utils/package";
|
import { comparePackageQuantity, convertPackageQuantity, createPackageRequestNo, getPackageQuantityText, normalizePackageQuantity, toPackageBaseQuantity } from "@/utils/package";
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||||
import { uploadPhotoList } from "@/utils/photo";
|
import { uploadPhotoList } from "@/utils/photo";
|
||||||
import { getProductPhotoURL } from "@/utils/product";
|
import { getProductPhotoURL } from "@/utils/product";
|
||||||
|
|
||||||
@@ -128,6 +140,8 @@ const navBarStore = useNavBarStore();
|
|||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const order = ref<SalesOrder>();
|
const order = ref<SalesOrder>();
|
||||||
const quantityMap = reactive<Record<string, string>>({});
|
const quantityMap = reactive<Record<string, string>>({});
|
||||||
|
const fulfillmentSkuMap = reactive<Record<string, ProductSku>>({});
|
||||||
|
const fulfillmentUnitMap = reactive<Record<string, string>>({});
|
||||||
const deliveredAtText = ref("");
|
const deliveredAtText = ref("");
|
||||||
const draftTime = ref("");
|
const draftTime = ref("");
|
||||||
const pickerVisible = ref(false);
|
const pickerVisible = ref(false);
|
||||||
@@ -135,12 +149,19 @@ const remark = ref("");
|
|||||||
const requestNo = ref("");
|
const requestNo = ref("");
|
||||||
const isSaving = ref(false);
|
const isSaving = ref(false);
|
||||||
const photoItems = ref<PhotoItem[]>([]);
|
const photoItems = ref<PhotoItem[]>([]);
|
||||||
|
const unitPickerVisible = ref(false);
|
||||||
|
const unitPickerSourceItemId = ref("");
|
||||||
|
const unitPickerDraft = ref<Array<string | number>>([]);
|
||||||
const photoUrlList = computed(() => photoItems.value.map(item => item.url));
|
const photoUrlList = computed(() => photoItems.value.map(item => item.url));
|
||||||
const isPackage = computed(() => order.value?.orderType === "PACKAGE");
|
const isPackage = computed(() => order.value?.orderType === "PACKAGE");
|
||||||
const expectedItemType = computed(() => isPackage.value ? "PACKAGE_ENTITLEMENT" : "DIRECT_PRODUCT");
|
const expectedItemType = computed(() => isPackage.value ? "PACKAGE_ENTITLEMENT" : "DIRECT_PRODUCT");
|
||||||
const sourceItems = computed<GaoSalesOrderItem[]>(() => (order.value?.itemList || []).filter(item => item.itemType === expectedItemType.value));
|
const sourceItems = computed<GaoSalesOrderItem[]>(() => (order.value?.itemList || []).filter(item => item.itemType === expectedItemType.value
|
||||||
const canOperateStock = computed(() => userStore.hasPermissions(permissionCodes.productStock.create));
|
&& 0 < comparePackageQuantity(item.availableBaseQuantity ?? "0", "0")));
|
||||||
const canFulfillByPayment = computed(() => order.value?.paymentStatus === "PAID" || (order.value?.paymentStatus === "PARTIALLY_PAID" && canOperateStock.value));
|
const unitPickerOptions = computed(() => {
|
||||||
|
const item = sourceItems.value.find(value => value.id === unitPickerSourceItemId.value);
|
||||||
|
return item ? getSellableUnitList(item).map(unit => ({ label: unit.name, value: unit.id || "" })) : [];
|
||||||
|
});
|
||||||
|
const canFulfillByPayment = computed(() => order.value?.paymentStatus === "PAID" || userStore.hasRoles(gaoRoleCodes.storeManager));
|
||||||
|
|
||||||
async function loadData(): Promise<void> {
|
async function loadData(): Promise<void> {
|
||||||
const salesOrderId = String(route.query.salesOrderId || "");
|
const salesOrderId = String(route.query.salesOrderId || "");
|
||||||
@@ -151,23 +172,86 @@ async function loadData(): Promise<void> {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
order.value = await SalesOrderAPI.detail(salesOrderId);
|
order.value = await SalesOrderAPI.detail(salesOrderId);
|
||||||
|
await loadFulfillmentSkuList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Message.error(getErrorMessage(error, "履约来源加载失败"));
|
Message.error(getErrorMessage(error, "履约来源加载失败"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAvailableQuantity(item: GaoSalesOrderItem): string {
|
async function loadFulfillmentSkuList(): Promise<void> {
|
||||||
return getPackageQuantityText(item.availableQuantity);
|
const skuIdList = [...new Set(sourceItems.value.map(item => item.skuId).filter((skuId): skuId is string => !!skuId))];
|
||||||
|
await Promise.all(skuIdList.map(async skuId => {
|
||||||
|
try {
|
||||||
|
fulfillmentSkuMap[skuId] = await ProductAPI.skuDetail(skuId);
|
||||||
|
} catch {
|
||||||
|
// 提交时仍由后端校验实际履约单位,单个 SKU 加载失败不影响其他明细
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
for (const item of sourceItems.value) {
|
||||||
|
if (!item.id || fulfillmentUnitMap[item.id]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const originalUnit = getSellableUnitList(item).find(unit => unit.id === item.skuUnitId);
|
||||||
|
const defaultUnit = originalUnit || getSellableUnitList(item)[0];
|
||||||
|
if (defaultUnit?.id) {
|
||||||
|
fulfillmentUnitMap[item.id] = defaultUnit.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAvailableBaseQuantity(item: GaoSalesOrderItem): string {
|
||||||
|
return getPackageQuantityText(item.availableBaseQuantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getItemDescription(item: GaoSalesOrderItem): string {
|
function getItemDescription(item: GaoSalesOrderItem): string {
|
||||||
return `剩余 ${getAvailableQuantity(item)}`;
|
return `可履约 ${getAvailableBaseQuantity(item)} ${getBaseUnitName(item)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBaseUnitName(item: GaoSalesOrderItem): string {
|
||||||
|
return item.skuId ? fulfillmentSkuMap[item.skuId]?.unitList?.find(unit => unit.isBase)?.name || "基础单位" : "基础单位";
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillAllQuantity(item: GaoSalesOrderItem): void {
|
function fillAllQuantity(item: GaoSalesOrderItem): void {
|
||||||
if (item.id) {
|
const unit = getFulfillmentUnit(item);
|
||||||
quantityMap[item.id] = getAvailableQuantity(item);
|
if (!item.id || !unit) {
|
||||||
|
Message.warning("请先选择实际履约单位");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
const quantity = convertPackageQuantity(getAvailableBaseQuantity(item), "1", unit.baseQuantityPerUnit);
|
||||||
|
if (!quantity) {
|
||||||
|
Message.warning("当前单位不能精确履约全部剩余权益,请填写可精确换算的数量");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
quantityMap[item.id] = quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSellableUnitList(item: GaoSalesOrderItem): ProductSkuUnit[] {
|
||||||
|
return (item.skuId ? fulfillmentSkuMap[item.skuId]?.unitList : [])?.filter(unit => unit.allowSale !== false && !!unit.id) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFulfillmentUnit(item: GaoSalesOrderItem): ProductSkuUnit | undefined {
|
||||||
|
return getSellableUnitList(item).find(unit => unit.id === fulfillmentUnitMap[item.id || ""]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openUnitPicker(item: GaoSalesOrderItem): void {
|
||||||
|
if (!item.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!getSellableUnitList(item).length) {
|
||||||
|
Message.warning("当前 SKU 没有可用于履约的销售单位");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unitPickerSourceItemId.value = item.id;
|
||||||
|
unitPickerDraft.value = [fulfillmentUnitMap[item.id] || getSellableUnitList(item)[0]?.id || ""];
|
||||||
|
unitPickerVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmUnitPicker(value: Array<string | number>): void {
|
||||||
|
const skuUnitId = String(value[0] || "");
|
||||||
|
if (unitPickerSourceItemId.value && skuUnitId) {
|
||||||
|
fulfillmentUnitMap[unitPickerSourceItemId.value] = skuUnitId;
|
||||||
|
}
|
||||||
|
unitPickerVisible.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmTime(value: string | number): void {
|
function confirmTime(value: string | number): void {
|
||||||
@@ -212,11 +296,17 @@ function validate(): boolean {
|
|||||||
}
|
}
|
||||||
for (const item of selected) {
|
for (const item of selected) {
|
||||||
const quantity = normalizePackageQuantity(quantityMap[item.id || ""]);
|
const quantity = normalizePackageQuantity(quantityMap[item.id || ""]);
|
||||||
if (!quantity) {
|
const unit = getFulfillmentUnit(item);
|
||||||
|
if (!quantity || !unit) {
|
||||||
Message.warning("履约数量必须是正数,最多三位小数");
|
Message.warning("履约数量必须是正数,最多三位小数");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (0 < comparePackageQuantity(quantity, item.availableQuantity ?? "0")) {
|
const baseQuantity = toPackageBaseQuantity(quantity, unit.baseQuantityPerUnit);
|
||||||
|
if (!baseQuantity) {
|
||||||
|
Message.warning("履约数量换算基础单位后最多只能保留三位小数");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (0 < comparePackageQuantity(baseQuantity, item.availableBaseQuantity ?? "0")) {
|
||||||
Message.warning(`${item.productNameSnapshot || "产品"} 的可履约数量不足`);
|
Message.warning(`${item.productNameSnapshot || "产品"} 的可履约数量不足`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -231,9 +321,10 @@ async function save(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
const itemList: GaoOrderFulfillmentItemRequest[] = sourceItems.value
|
const itemList: GaoOrderFulfillmentItemRequest[] = sourceItems.value
|
||||||
.filter(item => !!item.id && quantityMap[item.id])
|
.filter(item => !!item.id && quantityMap[item.id])
|
||||||
.map(item => ({
|
.map(item => ({
|
||||||
sourceOrderItemId: item.id!,
|
sourceOrderItemId: item.id!,
|
||||||
quantity: normalizePackageQuantity(quantityMap[item.id || ""])!
|
skuUnitId: fulfillmentUnitMap[item.id!]!,
|
||||||
|
quantity: normalizePackageQuantity(quantityMap[item.id || ""])!
|
||||||
}));
|
}));
|
||||||
const result = await FulfillmentAPI.create({
|
const result = await FulfillmentAPI.create({
|
||||||
salesOrderId: order.value!.id!,
|
salesOrderId: order.value!.id!,
|
||||||
@@ -333,6 +424,7 @@ onUnmounted(() => navBarStore.clearRight());
|
|||||||
.input {
|
.input {
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
margin-top: .5rem;
|
||||||
|
|
||||||
.unit {
|
.unit {
|
||||||
font-size: .875rem;
|
font-size: .875rem;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
v-for="item in fulfillment.itemList || []"
|
v-for="item in fulfillment.itemList || []"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:title="item.productNameSnapshot || '未命名产品'"
|
:title="item.productNameSnapshot || '未命名产品'"
|
||||||
:note="`待履约 ${item.reservedBefore || 0} → ${item.reservedAfter || 0}`"
|
:note="`待履约(基础单位)${item.reservedBaseBefore || 0} → ${item.reservedBaseAfter || 0}`"
|
||||||
>
|
>
|
||||||
<template #description>
|
<template #description>
|
||||||
<div class="item-description">
|
<div class="item-description">
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
<t-form class="items" label-width="2.5rem">
|
<t-form class="items" label-width="2.5rem">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<strong>购买权益明细</strong>
|
<strong>购买权益明细</strong>
|
||||||
<span v-if="!canPriceOverride">当前账号无定制权限,明细不可修改</span>
|
<span v-if="!canPriceOverride">当前账号无定制权限,仅可等值转换单位</span>
|
||||||
</div>
|
</div>
|
||||||
<t-form-item
|
<t-form-item
|
||||||
v-for="(item, index) in packagePurchaseItemList"
|
v-for="(item, index) in packagePurchaseItemList"
|
||||||
@@ -102,7 +102,15 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<span class="name" v-text="getItemLabel(item)" />
|
<div class="item-title">
|
||||||
|
<span class="name" v-text="getItemLabel(item)" />
|
||||||
|
<t-button
|
||||||
|
size="extra-small"
|
||||||
|
variant="outline"
|
||||||
|
:content="getItemUnit(item)?.name || '选择单位'"
|
||||||
|
@click="openItemUnitPicker(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<t-input
|
<t-input
|
||||||
:model-value="item.totalQuantity"
|
:model-value="item.totalQuantity"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -113,9 +121,18 @@
|
|||||||
@update:model-value="updateItemQuantity(index, $event)"
|
@update:model-value="updateItemQuantity(index, $event)"
|
||||||
>
|
>
|
||||||
<template #suffix-icon>
|
<template #suffix-icon>
|
||||||
<span class="unit" v-text="getItemUnit(item)" />
|
<span class="unit" v-text="getItemUnit(item)?.name || ''" />
|
||||||
</template>
|
</template>
|
||||||
</t-input>
|
</t-input>
|
||||||
|
<t-button
|
||||||
|
v-if="canPriceOverride"
|
||||||
|
class="remove"
|
||||||
|
size="extra-small"
|
||||||
|
theme="danger"
|
||||||
|
variant="text"
|
||||||
|
content="移除"
|
||||||
|
@click="removePurchaseItem(index)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<div v-if="!packagePurchaseItemList.length" class="center-tips" v-text="'当前套餐暂无权益明细'" />
|
<div v-if="!packagePurchaseItemList.length" class="center-tips" v-text="'当前套餐暂无权益明细'" />
|
||||||
@@ -185,6 +202,9 @@
|
|||||||
<div v-else class="center-tips" v-text="'暂无可购买套餐'" />
|
<div v-else class="center-tips" v-text="'暂无可购买套餐'" />
|
||||||
</div>
|
</div>
|
||||||
</t-popup>
|
</t-popup>
|
||||||
|
<t-popup v-model="unitPickerVisible" placement="bottom">
|
||||||
|
<t-picker v-model="unitPickerDraft" :columns="unitPickerOptions" title="调整权益单位" @cancel="unitPickerVisible = false" @confirm="confirmItemUnitPicker" />
|
||||||
|
</t-popup>
|
||||||
<t-popup v-model="selectedCustomerListVisible" placement="bottom">
|
<t-popup v-model="selectedCustomerListVisible" placement="bottom">
|
||||||
<div class="selected-customer-popup bg-white">
|
<div class="selected-customer-popup bg-white">
|
||||||
<div class="popup-header">
|
<div class="popup-header">
|
||||||
@@ -221,6 +241,7 @@ import { Time } from "timi-web";
|
|||||||
import { useUserStore } from "timi-web-api";
|
import { useUserStore } from "timi-web-api";
|
||||||
import CustomerAPI from "@/api/CustomerAPI";
|
import CustomerAPI from "@/api/CustomerAPI";
|
||||||
import PackageAPI from "@/api/PackageAPI";
|
import PackageAPI from "@/api/PackageAPI";
|
||||||
|
import ProductAPI from "@/api/ProductAPI";
|
||||||
import CodeScanner from "@/components/CodeScanner.vue";
|
import CodeScanner from "@/components/CodeScanner.vue";
|
||||||
import CustomerList from "@/components/CustomerList.vue";
|
import CustomerList from "@/components/CustomerList.vue";
|
||||||
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
|
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
|
||||||
@@ -232,8 +253,9 @@ import { useSettingStore } from "@/store/settingStore";
|
|||||||
import type { Customer } from "@/types/Customer";
|
import type { Customer } from "@/types/Customer";
|
||||||
import type { PhotoItem } from "@/types/Photo";
|
import type { PhotoItem } from "@/types/Photo";
|
||||||
import type { GaoPackageConfig, PackageAttachment, PackagePurchaseItem } from "@/types/Package";
|
import type { GaoPackageConfig, PackageAttachment, PackagePurchaseItem } from "@/types/Package";
|
||||||
|
import type { ProductSku, ProductSkuUnit } from "@/types/Product";
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
import { formatPackageAmount } from "@/utils/package";
|
import { addPackageQuantity, convertPackageQuantity, formatPackageAmount, normalizePackageQuantity } from "@/utils/package";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { permissionCodes } from "@/utils/permission";
|
||||||
import { uploadPhotoList } from "@/utils/photo";
|
import { uploadPhotoList } from "@/utils/photo";
|
||||||
@@ -256,6 +278,10 @@ const isCustomerPackageCreate = computed(() => route.name === "CustomerPackageCr
|
|||||||
const config = ref<GaoPackageConfig>();
|
const config = ref<GaoPackageConfig>();
|
||||||
const configList = ref<GaoPackageConfig[]>([]);
|
const configList = ref<GaoPackageConfig[]>([]);
|
||||||
const configSelectorVisible = ref(false);
|
const configSelectorVisible = ref(false);
|
||||||
|
const skuMap = reactive<Record<string, ProductSku>>({});
|
||||||
|
const unitPickerVisible = ref(false);
|
||||||
|
const unitPickerIndex = ref(-1);
|
||||||
|
const unitPickerDraft = ref<Array<string | number>>([]);
|
||||||
|
|
||||||
// ---------- 客户 ----------
|
// ---------- 客户 ----------
|
||||||
|
|
||||||
@@ -405,8 +431,13 @@ const {
|
|||||||
buildAttachments
|
buildAttachments
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const unitPickerOptions = computed(() => {
|
||||||
|
const item = packagePurchaseItemList.value[unitPickerIndex.value];
|
||||||
|
return item ? getSelectableUnitList(item).map(unit => ({ label: unit.name, value: unit.id || "" })) : [];
|
||||||
|
});
|
||||||
|
|
||||||
function getItemLabel(item: PackagePurchaseItem): string {
|
function getItemLabel(item: PackagePurchaseItem): string {
|
||||||
const configItem = config.value?.itemList?.find(configItem => configItem.skuId === item.skuId && configItem.skuUnitId === item.skuUnitId);
|
const configItem = config.value?.itemList?.find(configItem => configItem.skuId === item.skuId);
|
||||||
if (!configItem) {
|
if (!configItem) {
|
||||||
return "产品权益";
|
return "产品权益";
|
||||||
}
|
}
|
||||||
@@ -416,16 +447,78 @@ function getItemLabel(item: PackagePurchaseItem): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getItemName(item: PackagePurchaseItem): string {
|
function getItemName(item: PackagePurchaseItem): string {
|
||||||
return config.value?.itemList?.find(configItem => configItem.skuId === item.skuId && configItem.skuUnitId === item.skuUnitId)?.productNameSnapshot || "未命名产品";
|
return config.value?.itemList?.find(configItem => configItem.skuId === item.skuId)?.productNameSnapshot || "未命名产品";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getItemPhotoURL(item: PackagePurchaseItem): string {
|
function getItemPhotoURL(item: PackagePurchaseItem): string {
|
||||||
const configItem = config.value?.itemList?.find(configItem => configItem.skuId === item.skuId && configItem.skuUnitId === item.skuUnitId);
|
return getProductPhotoURL(getItemSku(item) || {});
|
||||||
return getProductPhotoURL(configItem?.sku || {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getItemUnit(item: PackagePurchaseItem): string {
|
function getItemSku(item: PackagePurchaseItem): ProductSku | undefined {
|
||||||
return config.value?.itemList?.find(configItem => configItem.skuId === item.skuId && configItem.skuUnitId === item.skuUnitId)?.unitSnapshot || "";
|
return skuMap[item.skuId] || config.value?.itemList?.find(configItem => configItem.skuId === item.skuId)?.sku;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItemUnit(item: PackagePurchaseItem): ProductSkuUnit | undefined {
|
||||||
|
return getItemSku(item)?.unitList?.find(unit => unit.id === item.skuUnitId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectableUnitList(item: PackagePurchaseItem): ProductSkuUnit[] {
|
||||||
|
const maxBaseQuantityPerUnit = Math.max(...(config.value?.itemList || [])
|
||||||
|
.filter(configItem => configItem.skuId === item.skuId)
|
||||||
|
.map(configItem => Number(configItem.baseQuantityPerUnitSnapshot || 0)));
|
||||||
|
return (getItemSku(item)?.unitList || []).filter(unit => unit.allowSale !== false && !!unit.id
|
||||||
|
&& Number(unit.baseQuantityPerUnit) <= maxBaseQuantityPerUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openItemUnitPicker(index: number): void {
|
||||||
|
const item = packagePurchaseItemList.value[index];
|
||||||
|
if (!item || !getSelectableUnitList(item).length) {
|
||||||
|
Message.warning("当前 SKU 没有可用于套餐权益的销售单位");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unitPickerIndex.value = index;
|
||||||
|
unitPickerDraft.value = [item.skuUnitId];
|
||||||
|
unitPickerVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmItemUnitPicker(value: Array<string | number>): void {
|
||||||
|
const index = unitPickerIndex.value;
|
||||||
|
const item = packagePurchaseItemList.value[index];
|
||||||
|
const skuUnitId = String(value[0] || "");
|
||||||
|
const currentUnit = item && getItemUnit(item);
|
||||||
|
const targetUnit = item && getSelectableUnitList(item).find(unit => unit.id === skuUnitId);
|
||||||
|
if (!item || !currentUnit || !targetUnit || !skuUnitId || skuUnitId === item.skuUnitId) {
|
||||||
|
unitPickerVisible.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const convertedQuantity = convertPackageQuantity(item.totalQuantity, currentUnit.baseQuantityPerUnit, targetUnit.baseQuantityPerUnit);
|
||||||
|
if (!convertedQuantity) {
|
||||||
|
Message.warning("调整单位后数量不能精确保留三位小数");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const duplicateIndex = packagePurchaseItemList.value.findIndex((candidate, candidateIndex) => candidateIndex !== index
|
||||||
|
&& candidate.skuId === item.skuId && candidate.skuUnitId === skuUnitId);
|
||||||
|
if (duplicateIndex < 0) {
|
||||||
|
packagePurchaseItemList.value[index] = { ...item, skuUnitId, totalQuantity: convertedQuantity };
|
||||||
|
} else {
|
||||||
|
const duplicateItem = packagePurchaseItemList.value[duplicateIndex];
|
||||||
|
const totalQuantity = addPackageQuantity(duplicateItem.totalQuantity, convertedQuantity);
|
||||||
|
if (!totalQuantity) {
|
||||||
|
Message.warning("合并同单位权益后数量不能精确保留三位小数");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
packagePurchaseItemList.value = packagePurchaseItemList.value.flatMap((candidate, candidateIndex) => {
|
||||||
|
if (candidateIndex === index) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (candidateIndex === duplicateIndex) {
|
||||||
|
return [{ ...candidate, totalQuantity, sort: Math.min(candidate.sort ?? candidateIndex, item.sort ?? index) }];
|
||||||
|
}
|
||||||
|
return [candidate];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
resetRequestNo();
|
||||||
|
unitPickerVisible.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateItemQuantity(index: number, value: string | number): void {
|
function updateItemQuantity(index: number, value: string | number): void {
|
||||||
@@ -433,13 +526,33 @@ function updateItemQuantity(index: number, value: string | number): void {
|
|||||||
...item,
|
...item,
|
||||||
totalQuantity: String(value)
|
totalQuantity: String(value)
|
||||||
} : item);
|
} : item);
|
||||||
|
resetRequestNo();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePurchaseItem(index: number): void {
|
||||||
|
if (!canPriceOverride.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
packagePurchaseItemList.value = packagePurchaseItemList.value.filter((_, itemIndex) => itemIndex !== index);
|
||||||
|
resetRequestNo();
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateItemDecimalQuantity(): boolean {
|
||||||
|
for (const item of packagePurchaseItemList.value) {
|
||||||
|
const quantity = normalizePackageQuantity(item.totalQuantity);
|
||||||
|
if (quantity && quantity !== "0" && quantity.includes(".") && getItemSku(item)?.allowDecimalQuantity === false) {
|
||||||
|
Message.warning("该 SKU 不允许小数数量");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitRecord(): Promise<void> {
|
async function submitRecord(): Promise<void> {
|
||||||
if (isSaving.value) {
|
if (isSaving.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!validatePurchaseForm()) {
|
if (!validatePurchaseForm() || !validateItemDecimalQuantity()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,6 +586,7 @@ async function loadPageData(): Promise<void> {
|
|||||||
}
|
}
|
||||||
config.value = await PackageAPI.configDetail(configId.value);
|
config.value = await PackageAPI.configDetail(configId.value);
|
||||||
applyConfig(config.value);
|
applyConfig(config.value);
|
||||||
|
await loadConfigSkuList(config.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectConfig(item: GaoPackageConfig): Promise<void> {
|
async function selectConfig(item: GaoPackageConfig): Promise<void> {
|
||||||
@@ -480,9 +594,21 @@ async function selectConfig(item: GaoPackageConfig): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
config.value = await PackageAPI.configDetail(item.id);
|
config.value = await PackageAPI.configDetail(item.id);
|
||||||
|
await loadConfigSkuList(config.value);
|
||||||
configSelectorVisible.value = false;
|
configSelectorVisible.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadConfigSkuList(value?: GaoPackageConfig): Promise<void> {
|
||||||
|
const skuIdList = [...new Set((value?.itemList || []).map(item => item.skuId).filter(Boolean))];
|
||||||
|
await Promise.all(skuIdList.map(async skuId => {
|
||||||
|
try {
|
||||||
|
skuMap[skuId] = await ProductAPI.skuDetail(skuId);
|
||||||
|
} catch {
|
||||||
|
// 后端会校验最终权益单位,单个 SKU 的单位加载失败不影响套餐查看
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
navBarStore.clearRight();
|
navBarStore.clearRight();
|
||||||
if (userStore.hasPermissions(permissionCodes.customerPackage.create)) {
|
if (userStore.hasPermissions(permissionCodes.customerPackage.create)) {
|
||||||
@@ -608,11 +734,27 @@ watch(config, value => {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.item-title {
|
||||||
|
gap: .375rem;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.t-input {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@@ -71,6 +71,16 @@ export function usePackagePurchase(options: UsePackagePurchaseOptions) {
|
|||||||
if (!packageRequestNo.value) {
|
if (!packageRequestNo.value) {
|
||||||
packageRequestNo.value = createPackageRequestNo();
|
packageRequestNo.value = createPackageRequestNo();
|
||||||
}
|
}
|
||||||
|
const itemList = packagePurchaseItemList.value.flatMap((item, index) => {
|
||||||
|
const totalQuantity = normalizePackageQuantity(item.totalQuantity);
|
||||||
|
return totalQuantity && totalQuantity !== "0" ? [{
|
||||||
|
skuId: item.skuId,
|
||||||
|
skuUnitId: item.skuUnitId,
|
||||||
|
totalQuantity,
|
||||||
|
sort: item.sort ?? index,
|
||||||
|
remark: item.remark ?? null
|
||||||
|
}] : [];
|
||||||
|
});
|
||||||
return await PackageAPI.purchase({
|
return await PackageAPI.purchase({
|
||||||
requestNo: packageRequestNo.value,
|
requestNo: packageRequestNo.value,
|
||||||
packageConfigId: config.id,
|
packageConfigId: config.id,
|
||||||
@@ -78,13 +88,7 @@ export function usePackagePurchase(options: UsePackagePurchaseOptions) {
|
|||||||
saleAmount,
|
saleAmount,
|
||||||
purchasedAt: options.buildRegisteredAt(),
|
purchasedAt: options.buildRegisteredAt(),
|
||||||
attachmentList: options.buildAttachments(),
|
attachmentList: options.buildAttachments(),
|
||||||
itemList: packagePurchaseItemList.value.length ? packagePurchaseItemList.value.map((item, index) => ({
|
itemList,
|
||||||
skuId: item.skuId,
|
|
||||||
skuUnitId: item.skuUnitId,
|
|
||||||
totalQuantity: normalizePackageQuantity(item.totalQuantity)!,
|
|
||||||
sort: item.sort ?? index,
|
|
||||||
remark: item.remark ?? null
|
|
||||||
})) : undefined,
|
|
||||||
remark: options.note.value.trim() || null
|
remark: options.note.value.trim() || null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -110,18 +114,28 @@ export function usePackagePurchase(options: UsePackagePurchaseOptions) {
|
|||||||
Message.warning("成交价必须是合法的元金额,最多两位小数");
|
Message.warning("成交价必须是合法的元金额,最多两位小数");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const keySet = new Set<string>();
|
||||||
|
let hasPositiveQuantity = false;
|
||||||
for (const item of packagePurchaseItemList.value) {
|
for (const item of packagePurchaseItemList.value) {
|
||||||
const quantity = normalizePackageQuantity(item.totalQuantity);
|
const quantity = normalizePackageQuantity(item.totalQuantity);
|
||||||
if (!item.skuId || !item.skuUnitId || !quantity || quantity === "0") {
|
if (!item.skuId || !item.skuUnitId || !quantity) {
|
||||||
Message.warning("SKU 单位和正数量不能为空");
|
Message.warning("SKU 单位和数量不能为空,数量最多三位小数");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const configItem = options.config.value.itemList?.find(source => source.skuId === item.skuId && source.skuUnitId === item.skuUnitId);
|
if (quantity !== "0") {
|
||||||
if (configItem?.sku && !configItem.sku.allowDecimalQuantity && quantity.includes(".")) {
|
hasPositiveQuantity = true;
|
||||||
Message.warning("该 SKU 不允许小数数量");
|
const key = `${item.skuId}\u0000${item.skuUnitId}`;
|
||||||
return false;
|
if (keySet.has(key)) {
|
||||||
|
Message.warning("最终套餐权益不能重复相同 SKU 和单位");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
keySet.add(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!hasPositiveQuantity) {
|
||||||
|
Message.warning("套餐至少需要保留一项正数量权益");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,23 +129,23 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<strong v-text="getPackageQuantityText(item.quantity)" />
|
<strong v-text="getPackageQuantityText(item.quantity)" />
|
||||||
<span>总数</span>
|
<span>订单数量</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<strong :class="getAvailableQuantityClass(item)" v-text="getPackageQuantityText(item.availableQuantity)" />
|
<strong :class="getAvailableQuantityClass(item)" v-text="getPackageQuantityText(item.availableBaseQuantity)" />
|
||||||
<span>可履约</span>
|
<span>可履约(基础)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<strong v-text="getPackageQuantityText(item.pendingQuantity)" />
|
<strong v-text="getPackageQuantityText(item.pendingBaseQuantity)" />
|
||||||
<span>待核销</span>
|
<span>待核销(基础)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<strong :class="getFulfilledQuantityClass(item)" v-text="getPackageQuantityText(item.fulfilledQuantity)" />
|
<strong :class="getFulfilledQuantityClass(item)" v-text="getPackageQuantityText(item.fulfilledBaseQuantity)" />
|
||||||
<span>已履约</span>
|
<span>已履约(基础)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric">
|
<div class="metric">
|
||||||
<strong :class="getRemainingQuantityClass(item)" v-text="getPackageQuantityText(item.remainingQuantity)" />
|
<strong :class="getRemainingQuantityClass(item)" v-text="getPackageQuantityText(item.remainingBaseQuantity)" />
|
||||||
<span>剩余</span>
|
<span>剩余(基础)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -242,7 +242,7 @@ import {
|
|||||||
getStatusTheme
|
getStatusTheme
|
||||||
} from "@/utils/salesOrder";
|
} from "@/utils/salesOrder";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||||
import { uploadPhotoList } from "@/utils/photo";
|
import { uploadPhotoList } from "@/utils/photo";
|
||||||
|
|
||||||
defineOptions({ name: "SalesOrderDetail" });
|
defineOptions({ name: "SalesOrderDetail" });
|
||||||
@@ -261,12 +261,11 @@ const canReadFund = computed(() => userStore.hasPermissions(permissionCodes.fund
|
|||||||
const canReadMyReceive = computed(() => userStore.hasPermissions(permissionCodes.fundReceive.create));
|
const canReadMyReceive = computed(() => userStore.hasPermissions(permissionCodes.fundReceive.create));
|
||||||
const canUpdate = computed(() => userStore.hasPermissions(permissionCodes.salesOrder.update));
|
const canUpdate = computed(() => userStore.hasPermissions(permissionCodes.salesOrder.update));
|
||||||
const canCreateFulfillment = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.create));
|
const canCreateFulfillment = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.create));
|
||||||
const canOperateStock = computed(() => userStore.hasPermissions(permissionCodes.productStock.create));
|
|
||||||
const canReceive = computed(() => !!order.value && order.value.status !== "CANCELLED" && order.value.paymentStatus !== "PAID" && userStore.hasPermissions(permissionCodes.fundReceive.create));
|
const canReceive = computed(() => !!order.value && order.value.status !== "CANCELLED" && order.value.paymentStatus !== "PAID" && userStore.hasPermissions(permissionCodes.fundReceive.create));
|
||||||
const canRefund = computed(() => !!order.value && order.value.status === "CANCELLED" && order.value.refundStatus === "PENDING" && userStore.hasPermissions(permissionCodes.fund.refund));
|
const canRefund = computed(() => !!order.value && order.value.status === "CANCELLED" && order.value.refundStatus === "PENDING" && userStore.hasPermissions(permissionCodes.fund.refund));
|
||||||
const canConfirmNoRefund = computed(() => !!order.value && order.value.status === "CANCELLED" && order.value.refundStatus === "PENDING" && userStore.hasPermissions(permissionCodes.fund.refund));
|
const canConfirmNoRefund = computed(() => !!order.value && order.value.status === "CANCELLED" && order.value.refundStatus === "PENDING" && userStore.hasPermissions(permissionCodes.fund.refund));
|
||||||
const canCancel = computed(() => !!order.value && order.value.status === "ACTIVE" && !hasPendingFulfillment.value && userStore.hasPermissions(permissionCodes.salesOrder.cancel));
|
const canCancel = computed(() => !!order.value && order.value.status === "ACTIVE" && !hasPendingFulfillment.value && userStore.hasPermissions(permissionCodes.salesOrder.cancel));
|
||||||
const canFulfillByPayment = computed(() => order.value?.paymentStatus === "PAID" || (order.value?.paymentStatus === "PARTIALLY_PAID" && canOperateStock.value));
|
const canFulfillByPayment = computed(() => order.value?.paymentStatus === "PAID" || userStore.hasRoles(gaoRoleCodes.storeManager));
|
||||||
const photoURLList = computed(() => photoItems.value.map(item => item.url));
|
const photoURLList = computed(() => photoItems.value.map(item => item.url));
|
||||||
|
|
||||||
function toPhotoItems(attachmentList?: readonly Partial<Attachment>[]): PhotoItem[] {
|
function toPhotoItems(attachmentList?: readonly Partial<Attachment>[]): PhotoItem[] {
|
||||||
@@ -312,20 +311,20 @@ function getRemainingAmountClass(amount?: number | null): "green" | "red" | "gra
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAvailableQuantityClass(item: GaoSalesOrderItem): "green" | "orange" {
|
function getAvailableQuantityClass(item: GaoSalesOrderItem): "green" | "orange" {
|
||||||
return Number(item.availableQuantity || 0) === 0 ? "green" : "orange";
|
return Number(item.availableBaseQuantity || 0) === 0 ? "green" : "orange";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFulfilledQuantityClass(item: GaoSalesOrderItem): "green" | "orange" {
|
function getFulfilledQuantityClass(item: GaoSalesOrderItem): "green" | "orange" {
|
||||||
return Number(item.remainingQuantity || 0) === 0 ? "green" : "orange";
|
return Number(item.remainingBaseQuantity || 0) === 0 ? "green" : "orange";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRemainingQuantityClass(item: GaoSalesOrderItem): "green" | "orange" {
|
function getRemainingQuantityClass(item: GaoSalesOrderItem): "green" | "orange" {
|
||||||
return Number(item.remainingQuantity || 0) === 0 ? "green" : "orange";
|
return Number(item.remainingBaseQuantity || 0) === 0 ? "green" : "orange";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getItemFulfillmentStatus(item: GaoSalesOrderItem): { text: string; theme: "success" | "warning" | "danger" } {
|
function getItemFulfillmentStatus(item: GaoSalesOrderItem): { text: string; theme: "success" | "warning" | "danger" } {
|
||||||
const fulfilledQuantity = Number(item.fulfilledQuantity || 0);
|
const fulfilledQuantity = Number(item.fulfilledBaseQuantity || 0);
|
||||||
const remainingQuantity = Number(item.remainingQuantity || 0);
|
const remainingQuantity = Number(item.remainingBaseQuantity || 0);
|
||||||
if (0 < fulfilledQuantity && remainingQuantity === 0) {
|
if (0 < fulfilledQuantity && remainingQuantity === 0) {
|
||||||
return { text: "已完成", theme: "success" };
|
return { text: "已完成", theme: "success" };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
:order="item"
|
:order="item"
|
||||||
:can-receive="canReceive"
|
:can-receive="canReceive"
|
||||||
:can-fulfill="canCreateFulfillment"
|
:can-fulfill="canCreateFulfillment"
|
||||||
:can-operate-stock="canOperateStock"
|
:can-store-manager="canStoreManager"
|
||||||
v-entrance-transition="index * 30"
|
v-entrance-transition="index * 30"
|
||||||
@click="toDetail(item)"
|
@click="toDetail(item)"
|
||||||
@receive="toPayment(item)"
|
@receive="toPayment(item)"
|
||||||
@@ -73,7 +73,7 @@ import { useNavBarStore } from "@/store/navBarStore";
|
|||||||
import type { GaoSalesOrderStatus, GaoSalesOrderType, GaoSalesPaymentStatus, GaoSalesRefundStatus, SalesOrder, SalesOrderPage } from "@/types/SalesOrder";
|
import type { GaoSalesOrderStatus, GaoSalesOrderType, GaoSalesPaymentStatus, GaoSalesRefundStatus, SalesOrder, SalesOrderPage } from "@/types/SalesOrder";
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||||
|
|
||||||
defineOptions({ name: "SalesOrderList" });
|
defineOptions({ name: "SalesOrderList" });
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -84,7 +84,7 @@ const canCreate = computed(() => userStore.hasPermissions(permissionCodes.salesO
|
|||||||
const canCreatePackageOrder = computed(() => userStore.hasPermissions(permissionCodes.customerPackage.create));
|
const canCreatePackageOrder = computed(() => userStore.hasPermissions(permissionCodes.customerPackage.create));
|
||||||
const canReceive = computed(() => userStore.hasPermissions(permissionCodes.fundReceive.create));
|
const canReceive = computed(() => userStore.hasPermissions(permissionCodes.fundReceive.create));
|
||||||
const canCreateFulfillment = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.create));
|
const canCreateFulfillment = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.create));
|
||||||
const canOperateStock = computed(() => userStore.hasPermissions(permissionCodes.productStock.create));
|
const canStoreManager = computed(() => userStore.hasRoles(gaoRoleCodes.storeManager));
|
||||||
const actions = computed(() => [
|
const actions = computed(() => [
|
||||||
...(canCreate.value ? [{
|
...(canCreate.value ? [{
|
||||||
key: "create",
|
key: "create",
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ const props = defineProps<{
|
|||||||
order: SalesOrder;
|
order: SalesOrder;
|
||||||
canReceive: boolean;
|
canReceive: boolean;
|
||||||
canFulfill: boolean;
|
canFulfill: boolean;
|
||||||
canOperateStock: boolean;
|
canStoreManager: boolean;
|
||||||
}>();
|
}>();
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
click: [];
|
click: [];
|
||||||
@@ -113,10 +113,10 @@ const avatarText = computed(() => customerName.value.slice(0, 1) || "客");
|
|||||||
const photoURL = computed(() => getCustomerPhotoURL(props.order.customer, 64));
|
const photoURL = computed(() => getCustomerPhotoURL(props.order.customer, 64));
|
||||||
const orderNoText = computed(() => `订单号:${props.order.orderNo || props.order.id || "未知"}`);
|
const orderNoText = computed(() => `订单号:${props.order.orderNo || props.order.id || "未知"}`);
|
||||||
const totalProductQuantity = computed(() => sumProductQuantity("quantity"));
|
const totalProductQuantity = computed(() => sumProductQuantity("quantity"));
|
||||||
const fulfilledProductQuantity = computed(() => sumProductQuantity("fulfilledQuantity"));
|
const fulfilledProductQuantity = computed(() => sumProductQuantity("fulfilledBaseQuantity"));
|
||||||
const productSummaryText = computed(() => `共 ${itemList.value.length} 项 · 总数量 ${totalProductQuantity.value} · 已履约 ${fulfilledProductQuantity.value}`);
|
const productSummaryText = computed(() => `共 ${itemList.value.length} 项 · 订单数量 ${totalProductQuantity.value} · 已履约(基础)${fulfilledProductQuantity.value}`);
|
||||||
const showReceiveAction = computed(() => props.canReceive && props.order.status !== "CANCELLED" && props.order.paymentStatus !== "PAID");
|
const showReceiveAction = computed(() => props.canReceive && props.order.status !== "CANCELLED" && props.order.paymentStatus !== "PAID");
|
||||||
const canFulfillByPayment = computed(() => props.order.paymentStatus === "PAID" || (props.order.paymentStatus === "PARTIALLY_PAID" && props.canOperateStock));
|
const canFulfillByPayment = computed(() => props.order.paymentStatus === "PAID" || props.canStoreManager);
|
||||||
const showFulfillAction = computed(() => props.canFulfill && canFulfillByPayment.value && props.order.status === "ACTIVE");
|
const showFulfillAction = computed(() => props.canFulfill && canFulfillByPayment.value && props.order.status === "ACTIVE");
|
||||||
|
|
||||||
function getPaymentStatusTheme(status?: GaoSalesPaymentStatus): "success" | "warning" | "danger" | "default" {
|
function getPaymentStatusTheme(status?: GaoSalesPaymentStatus): "success" | "warning" | "danger" | "default" {
|
||||||
@@ -132,7 +132,7 @@ function getPaymentStatusTheme(status?: GaoSalesPaymentStatus): "success" | "war
|
|||||||
return "default";
|
return "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
function sumProductQuantity(field: "quantity" | "fulfilledQuantity"): string {
|
function sumProductQuantity(field: "quantity" | "fulfilledBaseQuantity"): string {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
for (const item of itemList.value) {
|
for (const item of itemList.value) {
|
||||||
const quantity = Number(item[field] || 0);
|
const quantity = Number(item[field] || 0);
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ export type OrderFulfillmentItem = {
|
|||||||
sourceOrderItemId?: string;
|
sourceOrderItemId?: string;
|
||||||
quantity?: number | string;
|
quantity?: number | string;
|
||||||
baseQuantity?: number | string;
|
baseQuantity?: number | string;
|
||||||
reservedBefore?: number | string;
|
reservedBaseBefore?: number | string;
|
||||||
reservedAfter?: number | string;
|
reservedBaseAfter?: number | string;
|
||||||
fulfilledBefore?: number | string;
|
fulfilledBaseBefore?: number | string;
|
||||||
fulfilledAfter?: number | string;
|
fulfilledBaseAfter?: number | string;
|
||||||
productNameSnapshot?: string;
|
productNameSnapshot?: string;
|
||||||
productIdSnapshot?: string;
|
productIdSnapshot?: string;
|
||||||
skuCodeSnapshot?: string;
|
skuCodeSnapshot?: string;
|
||||||
@@ -70,6 +70,7 @@ export type OrderFulfillmentCreateRequest = {
|
|||||||
|
|
||||||
export type GaoOrderFulfillmentItemRequest = {
|
export type GaoOrderFulfillmentItemRequest = {
|
||||||
sourceOrderItemId: string;
|
sourceOrderItemId: string;
|
||||||
|
skuUnitId: string;
|
||||||
quantity: number | string;
|
quantity: number | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -27,13 +27,14 @@ export type GaoSalesOrderItem = {
|
|||||||
unitSnapshot?: string;
|
unitSnapshot?: string;
|
||||||
baseQuantityPerUnitSnapshot?: number | string;
|
baseQuantityPerUnitSnapshot?: number | string;
|
||||||
quantity?: number | string;
|
quantity?: number | string;
|
||||||
reservedQuantity?: number | string;
|
baseQuantity?: number | string;
|
||||||
fulfilledQuantity?: number | string;
|
reservedBaseQuantity?: number | string;
|
||||||
|
fulfilledBaseQuantity?: number | string;
|
||||||
unitPrice?: number | null;
|
unitPrice?: number | null;
|
||||||
lineAmount?: number | null;
|
lineAmount?: number | null;
|
||||||
pendingQuantity?: number | string;
|
pendingBaseQuantity?: number | string;
|
||||||
availableQuantity?: number | string;
|
availableBaseQuantity?: number | string;
|
||||||
remainingQuantity?: number | string;
|
remainingBaseQuantity?: number | string;
|
||||||
sort?: number;
|
sort?: number;
|
||||||
remark?: string | null;
|
remark?: string | null;
|
||||||
} & Model;
|
} & Model;
|
||||||
|
|||||||
+57
-3
@@ -59,6 +59,44 @@ export function comparePackageQuantity(left: number | string, right: number | st
|
|||||||
return leftValue < rightValue ? -1 : rightValue < leftValue ? 1 : 0;
|
return leftValue < rightValue ? -1 : rightValue < leftValue ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 将当前单位数量精确换算为最多三位小数的基础单位数量。 */
|
||||||
|
export function toPackageBaseQuantity(quantity: number | string, baseQuantityPerUnit: number | string): string | undefined {
|
||||||
|
const quantityParts = toDecimalParts(quantity, 3);
|
||||||
|
const unitParts = toDecimalParts(baseQuantityPerUnit, 6);
|
||||||
|
if (!quantityParts || !unitParts) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return formatDecimal(quantityParts.value * unitParts.value, quantityParts.scale + unitParts.scale, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将数量从一个单位精确换算到另一个单位,结果最多保留三位小数。 */
|
||||||
|
export function convertPackageQuantity(quantity: number | string, fromBaseQuantityPerUnit: number | string, toBaseQuantityPerUnit: number | string): string | undefined {
|
||||||
|
const quantityParts = toDecimalParts(quantity, 3);
|
||||||
|
const fromParts = toDecimalParts(fromBaseQuantityPerUnit, 6);
|
||||||
|
const toParts = toDecimalParts(toBaseQuantityPerUnit, 6);
|
||||||
|
if (!quantityParts || !fromParts || !toParts || toParts.value === 0n) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const numerator = quantityParts.value * fromParts.value * 10n ** BigInt(toParts.scale + 3);
|
||||||
|
const denominator = 10n ** BigInt(quantityParts.scale + fromParts.scale) * toParts.value;
|
||||||
|
if (numerator % denominator !== 0n) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return formatDecimal(numerator / denominator, 3, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 精确相加两个最多三位小数的数量。 */
|
||||||
|
export function addPackageQuantity(left: number | string, right: number | string): string | undefined {
|
||||||
|
const leftParts = toQuantityParts(left);
|
||||||
|
const rightParts = toQuantityParts(right);
|
||||||
|
if (!leftParts || !rightParts) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const scale = Math.max(leftParts.scale, rightParts.scale);
|
||||||
|
const value = leftParts.value * 10n ** BigInt(scale - leftParts.scale) + rightParts.value * 10n ** BigInt(scale - rightParts.scale);
|
||||||
|
return formatDecimal(value, scale, 3);
|
||||||
|
}
|
||||||
|
|
||||||
export function getPackageQuantityText(value?: number | string | null): string {
|
export function getPackageQuantityText(value?: number | string | null): string {
|
||||||
if (value === undefined || value === null || value === "") {
|
if (value === undefined || value === null || value === "") {
|
||||||
return "0";
|
return "0";
|
||||||
@@ -79,13 +117,29 @@ export function getFundStatusText(status?: FundRecordStatus): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toQuantityParts(value: number | string): { value: bigint; scale: number } | undefined {
|
function toQuantityParts(value: number | string): { value: bigint; scale: number } | undefined {
|
||||||
const normalized = normalizePackageQuantity(value);
|
return toDecimalParts(value, 3);
|
||||||
if (!normalized) {
|
}
|
||||||
|
|
||||||
|
function toDecimalParts(value: number | string, maxScale: number): { value: bigint; scale: number } | undefined {
|
||||||
|
const text = String(value).trim();
|
||||||
|
if (!new RegExp(`^\\d+(?:\\.\\d{1,${maxScale}})?$`).test(text)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const [integerText, fractionText = ""] = normalized.split(".");
|
const [integerText, fractionText = ""] = text.split(".");
|
||||||
return {
|
return {
|
||||||
value: BigInt(`${integerText}${fractionText}`),
|
value: BigInt(`${integerText}${fractionText}`),
|
||||||
scale: fractionText.length
|
scale: fractionText.length
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDecimal(value: bigint, scale: number, maxScale: number): string | undefined {
|
||||||
|
while (maxScale < scale && value % 10n === 0n) {
|
||||||
|
value /= 10n;
|
||||||
|
scale--;
|
||||||
|
}
|
||||||
|
if (maxScale < scale) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const text = value.toString().padStart(scale + 1, "0");
|
||||||
|
return normalizePackageQuantity(scale ? `${text.slice(0, -scale)}.${text.slice(-scale)}` : text);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user