feat(purchase): 优化采购订单计算逻辑和界面展示

- 引入decimal.js提升金额计算精度
- 重构成本计算方法,明确区分各类费用构成
- 优化采购预览界面,增加计算明细展示
- 改进开票信息展示样式和计算公式说明
- 完善纸箱重量和销售金额的精确计算
- 调整界面布局,提升用户体验和信息可读性
- 修复成本项过滤逻辑,确保数据准确性
- 新增快速导航功能,便于页面内快速定位
- 更新图标资源,支持计算器和指南针图标
- 优化数字格式化处理,统一保留合适的小数位数
This commit is contained in:
shenyifei 2025-12-13 11:08:25 +08:00
parent a79fc0ef9f
commit 3d217b1122
13 changed files with 481 additions and 295 deletions

View File

@ -3,6 +3,8 @@ import classNames from "classnames";
import React from "react";
export type IconNames =
| "calculator"
| "compass"
| "eye"
| "eye-slash"
| "phone-flip"

View File

@ -156,7 +156,6 @@ export default function CostList(props: {
)}
{orderCosts.map((orderCost) => {
if (type === "MATERIAL_TYPE") {
if (
orderCost.name === "空箱费" ||
orderCost.name === "纸箱费" ||
@ -165,7 +164,6 @@ export default function CostList(props: {
) {
return <></>;
}
}
return (
<CostCard
costList={defaultCostList}

View File

@ -6,6 +6,8 @@ import {
OrderSupplierCalculator,
PurchaseOrderCalculator,
} from "@/utils";
import { Icon } from "@/components";
import { Decimal } from "decimal.js";
interface IPurchasePreviewProps {
purchaseOrder: BusinessAPI.PurchaseOrderVO;
@ -61,9 +63,10 @@ export default function PurchasePreview(props: IPurchasePreviewProps) {
title: "重量(斤)",
key: "boxProductWeight",
render: (record: any) => {
return (
<View>{(record.boxProductWeight * record.boxCount).toFixed(2)}</View>
);
return new Decimal(record.boxProductWeight)
.mul(new Decimal(record.boxCount))
.toDecimalPlaces(0, Decimal.ROUND_HALF_UP)
.toNumber();
},
},
]);
@ -148,10 +151,10 @@ export default function PurchasePreview(props: IPurchasePreviewProps) {
return (
<View
className="rounded-lg bg-white p-2.5 shadow-sm"
className="flex flex-col gap-2.5 rounded-lg bg-white p-2.5 shadow-sm"
key={supplier.orderSupplierId}
>
<View className="mb-2 flex items-center justify-between">
<View className="flex items-center justify-between">
<View
className="text-lg font-semibold"
style="font-size: 18px;"
@ -162,52 +165,73 @@ export default function PurchasePreview(props: IPurchasePreviewProps) {
{supplier.isLast ? "最后一个" : "不是最后一个"}
</View>
</View>
<View className="mb-3 flex flex-col gap-2 border-b pb-3">
<View className="flex items-center justify-between">
<View className="text-sm text-gray-600"></View>
<View className="text-sm font-medium">
{formatCurrency(supplier.emptyWeight)} KG
</View>
</View>
<View className="flex items-center justify-between">
<View className="text-sm text-gray-600"></View>
<View className="text-sm font-medium">
{formatCurrency(supplier.totalWeight || 0)} KG
</View>
</View>
<View className="flex items-center justify-between">
<View className="text-sm text-gray-600"></View>
<View className="text-sm font-medium">
{formatCurrency(calculator.getGrossWeight())}
</View>
</View>
<View className="flex items-center justify-between">
<View className="text-sm text-gray-600"></View>
<View className="text-primary text-sm font-medium">
{formatCurrency(calculator.getNetWeight())}
</View>
</View>
<View className="flex items-center justify-between">
<View className="text-sm text-gray-600"></View>
<View className="text-sm font-medium">
{formatCurrency(calculator.getBoxWeight())}
</View>
</View>
<View className="flex items-center justify-between">
<View className="text-sm text-gray-600"></View>
<View className="text-primary text-sm font-medium">
{formatCurrency(calculator.getPurchasePrice())} /
</View>
</View>
<View className="mt-2 flex items-center justify-between">
<View className="text-sm text-gray-600"></View>
<View className="text-primary text-sm font-medium">
{formatCurrency(calculator.getTotalAmount())} {" "}
{supplier.invoiceAmount} {" "}
{supplier.isDepositPaid
? `(含定金:${formatCurrency(calculator.getDepositPaidAmount())}元)`
? `(含定金:${calculator.getDepositPaidAmount()}元)`
: ""}
</View>
</View>
{/* 计算公式区域 */}
<View className="rounded-lg border border-gray-200 bg-gray-50 p-3">
<View className="mb-2 flex items-center">
<Icon
name="calculator"
className="mr-2 leading-4"
size={20}
/>
<View className="text-sm font-medium text-gray-700">
</View>
</View>
<View className="space-y-1">
<View className="flex justify-between text-sm">
<View className="text-gray-600">kg</View>
<View className="font-medium text-gray-800">
{supplier.totalWeight || 0}
</View>
</View>
<View className="flex justify-between text-sm">
<View className="text-gray-600">kg</View>
<View className="font-medium text-gray-800">
{supplier.emptyWeight || 0}
</View>
</View>
<View className="flex justify-between text-sm">
<View className="text-gray-600"></View>
<View className="font-medium text-gray-800">
{calculator.calculateBoxesTotalWeight("USED") || 0}
</View>
</View>
<View className="flex justify-between text-sm">
<View className="text-gray-600"></View>
<View className="font-medium text-gray-800">
{supplier.netWeight || 0}
</View>
</View>
<View className="flex justify-between text-sm">
<View className="text-gray-600">/</View>
<View className="font-medium text-gray-800">
{supplier.purchasePrice || 0}
</View>
</View>
<View className="flex justify-between text-sm">
<View className="text-gray-600"></View>
<View className="font-medium text-gray-800">
{supplier.pricingMethod === "BY_GROSS_WEIGHT"
? "毛重"
: "净重"}
</View>
</View>
<View className="my-2 h-px bg-gray-300"></View>
<View className="text-center text-xs text-gray-500">
×
</View>
</View>
</View>
{/* 根据boxBrandName对packageInfoList进行分组显示 */}
@ -221,7 +245,7 @@ export default function PurchasePreview(props: IPurchasePreviewProps) {
return Object.entries(groupedPackageInfo).map(
([brandName, packageInfos]) => (
<View key={brandName} className="mb-2.5">
<View key={brandName} className={"flex flex-col gap-2.5"}>
<View className="text-primary text-base font-bold">
{brandName}
</View>
@ -238,11 +262,11 @@ export default function PurchasePreview(props: IPurchasePreviewProps) {
<View className="rounded-lg bg-white p-2.5 shadow-md">
<View className="flex items-center justify-between text-sm font-bold">
<View></View>
<View>{formatCurrency(calculator.getBoxCount())} </View>
<View>{calculator.getBoxCount()} </View>
</View>
<View className="mt-2 flex items-center justify-between text-sm font-bold">
<View></View>
<View>{formatCurrency(calculator.getBoxWeight())} </View>
<View>{calculator.getBoxWeight()} </View>
</View>
</View>
</View>

View File

@ -1,7 +1,7 @@
import { Image, View } from "@tarojs/components";
import { Icon } from "@/components";
import { Button, Price, Toast } from "@nutui/nutui-react-taro";
import { uploadFile } from "@/utils";
import { Button, Toast } from "@nutui/nutui-react-taro";
import { OrderSupplierCalculator, uploadFile } from "@/utils";
import Taro from "@tarojs/taro";
import { globalStore } from "@/store/global-store";
@ -29,33 +29,91 @@ export default function TicketUpload(props: ITicketUploadProps) {
if (!supplierVO) {
return;
}
const calculator = new OrderSupplierCalculator(
value as any,
supplierVO as any,
);
return (
<View className="flex flex-1 flex-col bg-[#D1D5DB] px-2.5 pt-2.5">
<View className="flex flex-1 flex-col gap-2.5 bg-[#D1D5DB] p-2.5">
<View className="border-primary rounded-lg border-4 bg-white p-2.5 shadow-sm">
<View className="flex flex-col gap-2.5">
<View className="text-primary text-base font-bold">
{supplierVO.name}
{supplierVO.name}
</View>
<View className="bg-primary/10 flex items-center justify-between rounded-lg p-2.5">
<View
className={"flex flex-1 flex-row items-center justify-between"}
>
<View className="block text-sm font-normal text-[#000000]">
{/* 应开票金额突出显示 */}
<View className="rounded-xl bg-gradient-to-r from-green-500 to-green-600 p-4 text-white shadow-md">
<View className="text-center">
<View className="mb-1 text-sm font-medium opacity-90">
</View>
<View className="text-primary mt-1 text-base font-semibold">
<Price
price={supplierVO.invoiceAmount || 0}
thousands
size={"xlarge"}
/>
<View className="text-4xl font-bold">
{supplierVO.invoiceAmount || 0}
</View>
</View>
</View>
<View className="block text-sm font-normal text-[#000000]">
{/* 计算公式区域 */}
<View className="rounded-lg border border-gray-200 bg-gray-50 p-3">
<View className="mb-2 flex items-center">
<Icon name="calculator" className="mr-2 leading-4" size={20} />
<View className="text-sm font-medium text-gray-700">
</View>
</View>
<View className="space-y-1">
<View className="flex justify-between text-sm">
<View className="text-gray-600">kg</View>
<View className="font-medium text-gray-800">
{supplierVO.totalWeight || 0}
</View>
</View>
<View className="flex justify-between text-sm">
<View className="text-gray-600">kg</View>
<View className="font-medium text-gray-800">
{supplierVO.emptyWeight || 0}
</View>
</View>
<View className="flex justify-between text-sm">
<View className="text-gray-600"></View>
<View className="font-medium text-gray-800">
{calculator.calculateBoxesTotalWeight("USED") || 0}
</View>
</View>
<View className="flex justify-between text-sm">
<View className="text-gray-600"></View>
<View className="font-medium text-gray-800">
{supplierVO.netWeight || 0}
</View>
</View>
<View className="flex justify-between text-sm">
<View className="text-gray-600">/</View>
<View className="font-medium text-gray-800">
{supplierVO.purchasePrice || 0}
</View>
</View>
<View className="flex justify-between text-sm">
<View className="text-gray-600"></View>
<View className="font-medium text-gray-800">
{supplierVO.pricingMethod === "BY_GROSS_WEIGHT"
? "毛重"
: "净重"}
</View>
</View>
<View className="my-2 h-px bg-gray-300"></View>
<View className="text-center text-xs text-gray-500">
×
</View>
</View>
</View>
</View>
</View>
<View className="border-primary rounded-lg border-4 bg-white p-2.5 shadow-sm">
<View className="flex flex-col gap-2.5">
<View className="text-primary text-base font-bold">
{supplierVO.name}
</View>
{supplierVO.invoiceUpload ? (
@ -213,9 +271,13 @@ export default function TicketUpload(props: ITicketUploadProps) {
</View>
</View>
)}
</View>
</View>
<View className="block text-sm font-normal text-[#000000]">
<View className="border-primary rounded-lg border-4 bg-white p-2.5 shadow-sm">
<View className="flex flex-col gap-2.5">
<View className="text-primary text-base font-bold">
{supplierVO.name}
</View>
{supplierVO.contractUpload &&
supplierVO.contractImg &&

View File

@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
import { Button, Input, Popup, SafeArea, Table } from "@nutui/nutui-react-taro";
import { Icon } from "@/components";
import { View } from "@tarojs/components";
import { Decimal } from "decimal.js";
export default function EmptyBoxInfoSection(props: {
purchaseOrderVO: BusinessAPI.PurchaseOrderVO;
@ -336,11 +337,10 @@ export default function EmptyBoxInfoSection(props: {
</span>
);
}
return formatCurrency(
Number(
(rowData?.boxSalePrice || 0) * rowData.boxProductCount,
) as number,
);
return new Decimal(rowData?.boxSalePrice || 0)
.mul(rowData.boxProductCount)
.toDecimalPlaces(0, Decimal.ROUND_HALF_UP)
.toNumber();
},
};
} else if (column.key === "boxProductWeight") {

View File

@ -133,7 +133,7 @@ export default function MarketPriceSection(props: {
// 销售金额
const saleAmount = calculator.getSalesAmount();
const totalAmount = calculator.getTotalAmount();
const totalAmount = calculator.getMarketPrice();
// 计算平均单价(所有供应商单价的平均值)
const averagePurchasePrice = calculator.getAverageSalesPrice();
@ -210,26 +210,25 @@ export default function MarketPriceSection(props: {
<View className="flex items-center justify-between">
<Text className="text-sm text-gray-500"></Text>
<Text className="text-sm font-medium">
{(supplier.grossWeight || 0).toFixed(2)}
{supplier.grossWeight || 0}
</Text>
</View>
<View className="flex items-center justify-between">
<Text className="text-sm text-gray-500"></Text>
<Text className="text-sm font-medium">
{(supplier.netWeight || 0).toFixed(2)}
{supplier.netWeight || 0}
</Text>
</View>
<View className="flex items-center justify-between">
<Text className="text-sm text-gray-500"></Text>
<Text className="text-sm font-medium">
{(supplier.grossWeight - supplier.netWeight).toFixed(2)}{" "}
{supplier.grossWeight - supplier.netWeight}
</Text>
</View>
<View className="flex items-center justify-between">
<Text className="text-sm text-gray-500"></Text>
<Text className="text-sm font-medium">
{supplier.purchasePrice.toFixed(2)} /
{supplier.purchasePrice} /
</Text>
</View>
{supplier.isDepositPaid && (

View File

@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
import { Button, Input, Popup, SafeArea, Table } from "@nutui/nutui-react-taro";
import { Icon } from "@/components";
import { View } from "@tarojs/components";
import { Decimal } from "decimal.js";
export default function PackageInfoSection(props: {
purchaseOrderVO: BusinessAPI.PurchaseOrderVO;
@ -69,15 +70,6 @@ export default function PackageInfoSection(props: {
{
title: "销售金额(元)",
key: "boxSalePayment",
render: (
value: BusinessAPI.OrderPackage & {
boxProductCount: number;
isTotalRow?: boolean;
},
) =>
formatCurrency(
Number((value?.boxSalePrice || 0) * value.boxProductCount) as number,
),
},
{
title: "箱重(斤)",
@ -189,23 +181,29 @@ export default function PackageInfoSection(props: {
}
// 计算各项合计
let totalBoxProductCount = 0;
let totalBoxSalePayment = 0;
let totalBoxProductWeight = 0;
let totalBoxProductCount = new Decimal(0);
let totalBoxSalePayment = new Decimal(0);
let totalBoxProductWeight = new Decimal(0);
packageData.forEach((pkg: any) => {
totalBoxProductCount += pkg.boxProductCount || 0;
totalBoxSalePayment +=
Number((pkg?.boxSalePrice || 0) * pkg.boxProductCount) || 0;
totalBoxProductWeight +=
Number((pkg?.boxProductWeight || 0) * pkg.boxProductCount) || 0;
totalBoxProductCount = totalBoxProductCount.add(pkg.boxProductCount || 0);
totalBoxSalePayment = totalBoxSalePayment.add(
new Decimal(pkg?.boxSalePrice || 0)
.mul(pkg.boxProductCount || 0)
.toDecimalPlaces(0, Decimal.ROUND_HALF_UP),
);
totalBoxProductWeight = totalBoxProductWeight.add(
new Decimal(pkg?.boxProductWeight || 0)
.mul(pkg.boxProductCount || 0)
.toDecimalPlaces(0, Decimal.ROUND_HALF_UP),
);
});
return {
boxProductName: "合计",
boxProductCount: totalBoxProductCount,
boxSalePayment: totalBoxSalePayment,
boxProductWeight: totalBoxProductWeight,
boxProductCount: totalBoxProductCount.toNumber(),
boxSalePayment: totalBoxSalePayment.toNumber(),
boxProductWeight: totalBoxProductWeight.toNumber(),
isTotalRow: true, // 标记这是合计行
};
};
@ -351,11 +349,10 @@ export default function PackageInfoSection(props: {
</span>
);
}
return formatCurrency(
Number(
(rowData?.boxSalePrice || 0) * rowData.boxProductCount,
) as number,
);
return new Decimal(rowData?.boxSalePrice || 0)
.mul(rowData.boxProductCount)
.toDecimalPlaces(0, Decimal.ROUND_HALF_UP)
.toNumber();
},
};
} else if (column.key === "boxProductWeight") {

View File

@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 5042354 */
src: url('//at.alicdn.com/t/c/font_5042354_hkkkrqw0kin.woff2?t=1763456511295') format('woff2'),
url('//at.alicdn.com/t/c/font_5042354_hkkkrqw0kin.woff?t=1763456511295') format('woff'),
url('//at.alicdn.com/t/c/font_5042354_hkkkrqw0kin.ttf?t=1763456511295') format('truetype');
src: url('//at.alicdn.com/t/c/font_5042354_gjumaiad8dh.woff2?t=1765554257380') format('woff2'),
url('//at.alicdn.com/t/c/font_5042354_gjumaiad8dh.woff?t=1765554257380') format('woff'),
url('//at.alicdn.com/t/c/font_5042354_gjumaiad8dh.ttf?t=1765554257380') format('truetype');
}
.iconfont {
@ -13,6 +13,14 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-calculator:before {
content: "\e626";
}
.icon-compass:before {
content: "\e625";
}
.icon-phone-flip:before {
content: "\e624";
}

View File

@ -106,7 +106,7 @@ export default hocAuth(function Page(props: CommonComponent) {
item.price * item.count > 0 && (
<View
className="cost-item flex flex-col px-3 py-2"
key={item.itemId}
key={item.costId}
>
<View className="text-sm text-gray-500">{item.name}</View>
<View className="font-medium">

View File

@ -1,6 +1,6 @@
import hocAuth from "@/hocs/auth";
import { CommonComponent } from "@/types/typings";
import Taro from "@tarojs/taro";
import Taro, { usePageScroll } from "@tarojs/taro";
import { business } from "@/services";
import { useEffect, useState } from "react";
import { View } from "@tarojs/components";
@ -23,6 +23,7 @@ import {
DealerInfoSection,
DeliveryFormSection,
EmptyBoxInfoSection,
Icon,
MarketPriceSection,
MaterialCostSection,
PackageInfoSection,
@ -190,11 +191,11 @@ const fullSections = [
component: CostDifferenceSection,
title: "待分红金额复核",
},
// 成本合计
// 成本合计复核
{
name: "costSummary",
component: CostSummarySection,
title: "成本合计",
title: "成本合计复核",
},
// 个人返点复核
{
@ -309,6 +310,10 @@ export default hocAuth(function Page(props: CommonComponent) {
// 控制更多操作的ActionSheet显示状态
const [moreActionVisible, setMoreActionVisible] = useState(false);
// 控制快速导航的显示状态
const [showQuickNav, setShowQuickNav] = useState(false);
const [quickNavExpanded, setQuickNavExpanded] = useState(false);
const [dealerRebateCustomerVOList, setDealerRebateCustomerVOList] =
useState<BusinessAPI.DealerRebateCustomerVO[]>();
@ -426,6 +431,31 @@ export default hocAuth(function Page(props: CommonComponent) {
setProvisionZeroConfirmVisible(false);
};
// 跳转到指定section
const scrollToSection = (sectionKey: string) => {
if (process.env.TARO_ENV === "h5") {
// H5环境使用DOM API
const element = document.getElementById(`section-${sectionKey}`);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "start" });
}
} else {
// 小程序环境使用Taro的节点查询
Taro.createSelectorQuery()
.select(`#section-${sectionKey}`)
.boundingClientRect((rect) => {
if (rect) {
Taro.pageScrollTo({
//@ts-ignore
scrollTop: rect.top,
duration: 300,
});
}
})
.exec();
}
};
// 表单校验
const validateForm = () => {
// 校验销售方
@ -568,6 +598,12 @@ export default hocAuth(function Page(props: CommonComponent) {
}
}, []);
// 使用Taro的页面滚动监听
usePageScroll((res) => {
const scrollTop = res.scrollTop;
setShowQuickNav(scrollTop > 300); // 滚动超过300px时显示快速导航
});
if (!purchaseOrderVO || !dealerRebateCustomerVOList || !costList) {
return;
}
@ -575,16 +611,84 @@ export default hocAuth(function Page(props: CommonComponent) {
const calculator = new PurchaseOrderCalculator(purchaseOrderVO, true);
const personalProfit = calculator.getPersonalProfit();
const sections =
const sections = (
purchaseOrderVO.orderDealer.shortName === "信誉楼"
? xylSections
: defaultSections;
: defaultSections
)
.map((sectionKey) => {
const section = fullSections.find(
(section) => section.name === sectionKey,
);
if (!section) {
return false;
}
const orderDealer = purchaseOrderVO.orderDealer;
if (!orderDealer?.enableCompanyRebate && sectionKey === "taxSubsidy") {
return false;
}
if (!orderDealer?.enableAccrualTax && sectionKey === "taxProvision") {
return false;
}
if (!orderDealer?.shareAdjusted && sectionKey === "costDifference") {
return false;
}
if (!orderDealer?.enableLoss && sectionKey === "productionLoss") {
return false;
}
// 如果没有返点人这个模块,则不渲染
if (
(!dealerRebateCustomerVOList ||
dealerRebateCustomerVOList.length === 0) &&
sectionKey === "rebateCalc"
) {
return false;
}
if (
(!purchaseOrderVO.orderPackageList ||
purchaseOrderVO?.orderPackageList.length === 0) &&
sectionKey === "emptyBoxInfo"
) {
return false;
}
if (
sectionKey === "purchaseForm" &&
purchaseOrderVO.orderDealer.shortName !== "信誉楼"
) {
return false;
}
if (
sectionKey === "deliveryForm" &&
purchaseOrderVO.orderDealer.shortName === "信誉楼"
) {
return false;
}
return section;
})
.filter(Boolean);
return (
<>
<View
className={"overflow-x-hidden overflow-y-auto bg-[#D1D5DB]"}
id={"purchase-order-audit"}
onClick={() => {
// 点击页面其他区域时收起导航菜单
if (quickNavExpanded) {
setQuickNavExpanded(false);
}
}}
>
<View className={"flex flex-col gap-2.5 p-2.5"}>
{/* 顶部导航 */}
@ -638,75 +742,13 @@ export default hocAuth(function Page(props: CommonComponent) {
</View>
{/* 循环渲染各部分内容 */}
{sections.map((sectionKey) => {
const section = fullSections.find(
(section) => section.name === sectionKey,
);
if (!section) {
return null;
}
const orderDealer = purchaseOrderVO.orderDealer;
if (
!orderDealer?.enableCompanyRebate &&
sectionKey === "taxSubsidy"
) {
return null;
}
if (
!orderDealer?.enableAccrualTax &&
sectionKey === "taxProvision"
) {
return null;
}
if (
!orderDealer?.shareAdjusted &&
sectionKey === "costDifference"
) {
return null;
}
if (!orderDealer?.enableLoss && sectionKey === "productionLoss") {
return null;
}
// 如果没有返点人这个模块,则不渲染
if (
(!dealerRebateCustomerVOList ||
dealerRebateCustomerVOList.length === 0) &&
sectionKey === "rebateCalc"
) {
return null;
}
if (
(!purchaseOrderVO.orderPackageList ||
purchaseOrderVO?.orderPackageList.length === 0) &&
sectionKey === "emptyBoxInfo"
) {
return null;
}
if (
sectionKey === "purchaseForm" &&
purchaseOrderVO.orderDealer.shortName !== "信誉楼"
) {
return null;
}
if (
sectionKey === "deliveryForm" &&
purchaseOrderVO.orderDealer.shortName === "信誉楼"
) {
return null;
}
{sections.map((section: any) => {
return (
<View key={sectionKey} className={"flex flex-col gap-2.5"}>
<View
key={section.name}
id={`section-${section.name}`}
className={"flex flex-col gap-2.5"}
>
<View className="text-sm font-bold">{section.title}</View>
<View
className={`overflow-x-auto rounded-md rounded-b-lg bg-white p-2.5 shadow-sm`}
@ -914,6 +956,66 @@ export default hocAuth(function Page(props: CommonComponent) {
<SafeArea position="bottom" />
</Popup>
{/* 快速导航目录 */}
{showQuickNav && (
<View
className={`fixed left-4 z-40 bg-white shadow-lg transition-all duration-300 ${
quickNavExpanded ? "top-4 w-48" : "top-4 h-10 w-10"
} rounded-lg`}
style={{
opacity: showQuickNav ? 1 : 0,
}}
>
{/* 导航触发按钮 */}
<View
className="relative flex h-10 w-10 cursor-pointer items-center justify-center"
onClick={() => setQuickNavExpanded(!quickNavExpanded)}
>
<Icon
name="compass"
className={`text-blue-600 transition-transform duration-300 ${
quickNavExpanded ? "rotate-180" : ""
}`}
></Icon>
</View>
{/* 展开的导航菜单 */}
{quickNavExpanded && (
<View
className="absolute top-0 left-10 w-48 rounded-lg border border-gray-200 bg-white shadow-lg"
onClick={(e) => e.stopPropagation()} // 防止事件冒泡
>
<View className="border-b border-gray-200 px-3 py-2">
<View className="text-xs font-medium text-gray-700">
</View>
</View>
<View className="max-h-96 overflow-y-auto">
{sections.map((section: any) => (
<View
key={section.name}
className="cursor-pointer border-b border-gray-100 px-3 py-2 text-sm text-gray-600 transition-colors last:border-b-0 hover:bg-blue-50 hover:text-blue-600"
onClick={() => {
scrollToSection(section.name);
setQuickNavExpanded(false); // 跳转后自动收起
}}
>
{section.title}
</View>
))}
</View>
{/* 收起按钮 */}
<View
className="cursor-pointer border-t border-gray-200 px-3 py-2 text-xs text-gray-500 hover:bg-gray-50"
onClick={() => setQuickNavExpanded(false)}
>
</View>
</View>
)}
</View>
)}
</>
);
});

View File

@ -22,10 +22,9 @@ export class OrderSupplierCalculator {
*
*/
private init() {
// this.purchaseOrderVO.orderDealer
Decimal.set({
precision: 20,
rounding: 0, // 0 = ROUND_DOWN
rounding: Decimal.ROUND_HALF_UP, // 使用常量更清晰
toExpNeg: -7,
toExpPos: 21,
});
@ -65,7 +64,9 @@ export class OrderSupplierCalculator {
*
*/
getTotalAmount(): number {
if (this.orderSupplier.orderPackageList?.some((pkg) => pkg.boxType === "USED")) {
if (
this.orderSupplier.orderPackageList?.some((pkg) => pkg.boxType === "USED")
) {
return new Decimal(this.getNetWeight())
.mul(this.getPurchasePrice())
.toNumber();
@ -85,4 +86,25 @@ export class OrderSupplierCalculator {
}
return new Decimal(this.orderSupplier.depositAmount || 0).toNumber();
}
/**
*
* @param {string} boxType - ('USED' 'EXTRA')
* @returns {number}
*/
calculateBoxesTotalWeight(boxType?: any): number {
return (
this.orderSupplier.orderPackageList
?.filter((pkg) => pkg.boxType === boxType)
.reduce((sum, pkg) => {
// 纸箱重量单位是斤,直接使用
const boxWeight = pkg.boxProductWeight || 0;
return new Decimal(sum)
.add(
new Decimal(pkg.boxCount || 0).mul(boxWeight).toDecimalPlaces(0),
)
.toNumber();
}, 0) || 0
);
}
}

View File

@ -18,30 +18,29 @@ export class PurchaseOrderCalculator {
console.table([
{
成本项总和: this.getTotalCostItemAmount(),
辅料费: this.getCostAmount("MATERIAL_TYPE"),
成本项总和: this.getTotalCostAmount(),
人工费: this.getCostAmount("ARTIFICIAL_TYPE"),
辅料费: this.getCostAmount("MATERIAL_TYPE"),
产地垫付: this.getCostAmount("PRODUCTION_TYPE"),
:
this.getCostAmount("MATERIAL_TYPE", "纸箱费") || this.getBoxSale(),
空箱费: this.getCostAmount("MATERIAL_TYPE", "空箱费"),
纸箱费: this.getCostAmount("MATERIAL_TYPE", "纸箱费"),
计提费: this.getCostAmount("OTHER_TYPE", "计提费"),
收代办费: this.getCostAmount("OTHER_TYPE", "收代办费"),
王超费用: this.getCostAmount("OTHER_TYPE", "王超费用"),
其他费用: this.getCostAmount("OTHER_TYPE"),
草帘费: this.getStrawCurtainCost(),
草帘费: this.getCostAmount("OTHER_TYPE", "草帘费"),
},
]);
console.table([
{
西瓜采购成本: this.getSupplierPurchaseCost(),
成本项总和: this.getTotalCostItemAmount(),
西瓜采购成本: this.getMelonPurchaseCost(),
西瓜成本1: this.getMelonCost1(),
西瓜成本2: this.getMelonCost2(),
成本项总和: this.getTotalCostAmount(),
税费补贴: this.getTaxSubsidy(),
计提税金: this.getTaxProvision(),
成本差异: this.getCostDifference(),
"采购成本(不包含运费)": this.getTotalPurchaseCost(),
运费: this.getDeliveryFee(),
"采购成本(含运费)": this.getMelonCost1(),
市场报价: this.getSalesAmount(),
销售金额: this.getSalesAmount(),
平均单价: this.getAverageSalesPrice(),
},
]);
@ -62,7 +61,7 @@ export class PurchaseOrderCalculator {
private init() {
Decimal.set({
precision: 20,
rounding: 0, // 0 = ROUND_DOWN
rounding: Decimal.ROUND_HALF_UP, // 使用常量更清晰
toExpNeg: -7,
toExpPos: 21,
});
@ -79,39 +78,25 @@ export class PurchaseOrderCalculator {
?.filter((item) => item.type === type && (!name || item.name === name))
.reduce((sum, cost) => {
return new Decimal(sum)
.plus(new Decimal(cost.price || 0).mul(cost.count || 0))
.plus(new Decimal(cost.price || 0).mul(cost.count || 0).toDecimalPlaces(0))
.toNumber();
}, 0);
}
/**
* = + + + + + + + +
* = + + + + + + + + +
*/
getTotalCostItemAmount(): number {
const costItemsCost = this.purchaseOrderVO.orderCostList?.reduce(
(sum, cost) => {
// 先过滤一下
if (cost.name === "纸箱费") {
getTotalCostAmount(): number {
return this.purchaseOrderVO.orderCostList?.reduce((sum, cost) => {
if (cost.name === "运费" && !this.purchaseOrderVO.orderDealer?.freightCostFlag) {
return new Decimal(sum).toNumber();
}
return new Decimal(sum)
.plus(new Decimal(cost.price || 0).mul(cost.count || 0))
.toNumber();
},
0,
);
const boxCost = this.getBoxSale();
// 计算草帘费
const strawCurtainCost = this.purchaseOrderVO.orderDealer?.strawMatCostFlag
? this.getStrawCurtainCost()
: 0;
return new Decimal(costItemsCost)
.plus(boxCost)
.plus(strawCurtainCost)
.plus(
new Decimal(cost.price || 0).mul(cost.count || 0).toDecimalPlaces(0),
)
.toNumber();
}, 0);
}
/**
@ -119,17 +104,6 @@ export class PurchaseOrderCalculator {
*/
getSupplierPurchaseCost(): number {
return this.purchaseOrderVO.orderSupplierList.reduce((sum, supplier) => {
const ownBoxWeight =
supplier.orderPackageList
?.filter((pkg) => pkg.boxType === "USED")
?.reduce((sum, pkg) => {
return new Decimal(sum)
.plus(
new Decimal(pkg.boxCount || 0).mul(pkg.boxProductWeight || 0),
)
.toNumber();
}, 0) || 0;
return new Decimal(sum)
.plus(
new Decimal(
@ -137,8 +111,8 @@ export class PurchaseOrderCalculator {
? supplier.grossWeight
: supplier.netWeight,
)
.plus(ownBoxWeight)
.mul(supplier.purchasePrice || 0),
.mul(supplier.purchasePrice || 0)
.toDecimalPlaces(0),
)
.toNumber();
}, 0);
@ -150,7 +124,7 @@ export class PurchaseOrderCalculator {
*/
getTotalPurchaseCost(): number {
return new Decimal(this.getSupplierPurchaseCost())
.plus(this.getTotalCostItemAmount())
.plus(this.getTotalCostAmount())
.plus(this.getTaxSubsidy())
.plus(this.getTaxProvision())
.plus(this.getCostDifference())
@ -158,17 +132,29 @@ export class PurchaseOrderCalculator {
}
/**
* 西1 = +
* 西
*/
getMelonPurchaseCost(): number {
return this.getSupplierPurchaseCost();
}
/**
* 西1 = 西 +
*/
getMelonCost1(): number {
const totalPurchaseCost = this.getTotalPurchaseCost();
const melonPurchaseCost = this.getMelonPurchaseCost();
const totalCostAmount = this.getTotalCostAmount();
// 计算运费
const deliveryFee = this.purchaseOrderVO.orderDealer?.freightCostFlag
? this.getDeliveryFee()
: 0;
return new Decimal(melonPurchaseCost).plus(totalCostAmount).toNumber();
}
return new Decimal(totalPurchaseCost).plus(deliveryFee).toNumber();
/**
* 西2 = 西1 + (
*/
getMelonCost2(): number {
return new Decimal(this.getMelonCost1())
.plus(this.getCostDifference())
.toNumber();
}
/**
@ -230,7 +216,11 @@ export class PurchaseOrderCalculator {
?.filter((pkg) => pkg.boxType === "USED")
.reduce((sum, pkg) => {
return new Decimal(sum)
.plus(new Decimal(pkg.boxCount || 0).mul(pkg.boxSalePrice || 0))
.plus(
new Decimal(pkg.boxCount || 0)
.mul(pkg.boxSalePrice || 0)
.toDecimalPlaces(0),
)
.toNumber();
}, 0) || 0,
)
@ -320,22 +310,12 @@ export class PurchaseOrderCalculator {
* 西 = - 西1
*/
getMelonGrossProfit(): number {
// 计算各种金额
const salesAmount = this.getMarketPrice(); // 市场报价
const melonCost1 = this.getMelonCost1(); // 西瓜成本1
return new Decimal(salesAmount).minus(melonCost1).toNumber(); // 西瓜毛利
}
/**
* 西2 = 西1 + (
*/
getMelonCost2(): number {
return new Decimal(this.getMelonCost1())
.plus(this.getCostDifference())
.toNumber();
}
/**
* (
*/
@ -376,13 +356,6 @@ export class PurchaseOrderCalculator {
return 0;
}
/**
*
*/
getMarketPrice(): number {
return new Decimal(this.getSalesAmount()).toNumber();
}
/**
*
*/
@ -412,14 +385,14 @@ export class PurchaseOrderCalculator {
/**
* +
*/
getTotalAmount(): number {
getMarketPrice(): number {
const decimal = new Decimal(this.getSalesAmount());
const includePackingFlag =
this.purchaseOrderVO.orderDealer?.includePackingFlag;
if (includePackingFlag) {
return decimal.plus(this.getTotalCostItemAmount()).toNumber();
return decimal.plus(this.getTotalCostAmount()).toNumber();
}
return decimal.toNumber();
@ -439,7 +412,10 @@ export class PurchaseOrderCalculator {
? supplier.grossWeight
: supplier.netWeight;
return new Decimal(weight || 0).mul(salePrice || 0).toNumber();
return new Decimal(weight || 0)
.mul(salePrice || 0)
.toDecimalPlaces(0)
.toNumber();
}
/**
@ -447,7 +423,7 @@ export class PurchaseOrderCalculator {
*/
getDefaultTaxProvision(): number {
if (this.purchaseOrderVO.orderDealer?.enableAccrualTax) {
const totalAmount = this.getTotalAmount();
const totalAmount = this.getMarketPrice();
const taxSubsidyValue = this.getTaxSubsidy();
return new Decimal(totalAmount)
@ -465,7 +441,7 @@ export class PurchaseOrderCalculator {
*/
getDefaultTaxSubsidy(): number {
if (this.purchaseOrderVO.orderDealer?.enableCompanyRebate) {
const totalPackagingCost = this.getTotalCostItemAmount();
const totalPackagingCost = this.getTotalCostAmount();
const salesAmount1 = this.getSalesAmount();
return new Decimal(salesAmount1)
@ -487,7 +463,10 @@ export class PurchaseOrderCalculator {
supplier.orderPackageList
?.filter((pkg) => pkg.boxType === "USED")
?.reduce((sum, pkg) => {
return new Decimal(sum).plus(pkg.boxCount || 0).toNumber();
return new Decimal(sum)
.plus(pkg.boxCount || 0)
.toDecimalPlaces(0)
.toNumber();
}, 0) || 0,
)
.toNumber();
@ -506,7 +485,9 @@ export class PurchaseOrderCalculator {
?.reduce((sum, pkg) => {
return new Decimal(sum)
.plus(
new Decimal(pkg.boxProductWeight || 0).mul(pkg.boxCount || 0),
new Decimal(pkg.boxProductWeight || 0)
.mul(pkg.boxCount || 0)
.toDecimalPlaces(0),
)
.toNumber();
}, 0) || 0,

View File

@ -18,7 +18,7 @@ export class SupplierWeightCalculator {
private init() {
Decimal.set({
precision: 20,
rounding: 0, // 向下舍入(更保守的计算方式)
rounding: Decimal.ROUND_HALF_UP, // 使用常量更清晰
toExpNeg: -7,
toExpPos: 21,
});
@ -29,7 +29,7 @@ export class SupplierWeightCalculator {
* @returns {Array}
*/
calculate(): BusinessAPI.OrderSupplier[] {
console.log("开始计算采购订单的农户重量信息...");
console.log("开始计算采购订单的农户重量信息...", this.suppliers);
if (!this.suppliers || this.suppliers.length === 0) {
return this.suppliers;
}
@ -51,16 +51,7 @@ export class SupplierWeightCalculator {
}
// 计算本次使用纸箱的总重量(斤)
const usedBoxesWeight = new Decimal(
this.calculateBoxesTotalWeight(supplier.orderPackageList || [], "USED"),
)
.toNumber();
// 计算额外配送的已使用纸箱总重量(斤)
const extraUsedBoxesWeight = this.calculateBoxesTotalWeight(
supplier.orderPackageList || [],
"EXTRA_USED",
);
const usedBoxesWeight = this.calculateBoxesTotalWeight(supplier.orderPackageList || [], "USED")
if (!supplier.isPaper) {
// 如果不是纸箱包装直接使用原始重量kg转斤
@ -71,12 +62,12 @@ export class SupplierWeightCalculator {
supplier.netWeight = new Decimal(supplier.grossWeight || 0)
.sub(usedBoxesWeight)
.sub(extraUsedBoxesWeight)
.toNumber();
previousTotalWeight = supplier.totalWeight;
supplier.invoiceAmount = new Decimal(supplier.netWeight || 0)
.mul(supplier.purchasePrice || 0)
.toDecimalPlaces(0)
.toNumber();
continue;
}
@ -93,15 +84,15 @@ export class SupplierWeightCalculator {
"REMAIN",
);
// 计算额外配送的已使用纸箱总重量(斤)
const extraUsedBoxesWeight = this.calculateBoxesTotalWeight(
supplier.orderPackageList || [],
"EXTRA_USED",
);
if (isFirstSupplier && isLastSupplier) {
// 既是第一个也是最后一个瓜农(单个瓜农情况)- 优先使用最后一个瓜农算法
// 净重 = (总磅 - 空磅) * 2 + 剩余空箱子重量 - 已使用额外纸箱重量
console.log("总磅", supplier.totalWeight);
console.log("空磅", initialEmptyWeight);
console.log("剩余空箱子重量", remainingBoxesWeight);
console.log("已使用额外纸箱重量", extraUsedBoxesWeight);
supplier.netWeight = new Decimal(supplier.totalWeight || 0)
.sub(initialEmptyWeight)
.mul(2)
@ -183,7 +174,7 @@ export class SupplierWeightCalculator {
// 纸箱重量单位是斤,直接使用
const boxWeight = pkg.boxProductWeight || 0;
return new Decimal(sum)
.add(new Decimal(pkg.boxCount || 0).mul(boxWeight))
.add(new Decimal(pkg.boxCount || 0).mul(boxWeight).toDecimalPlaces(0))
.toNumber();
}, 0);
}