refactor(calculators): 重构采购订单计算模块
- 移除旧的 OrderSupplierCalculator 和 SupplierWeightCalculator 实现 - 新增独立的 OrderSupplierCalculator 模块,专注单个供应商计算 - 新增 SupplierWeightCalculator 模块,优化重量计算逻辑 - 引入 WeightCalculationService 统一处理重量计算服务 - 在 PurchaseOrderCalculator 中集成新的计算模块 - 更新导出结构,暴露新的计算器和服务类 - 优化 Decimal 运算工具类使用,提高计算精度和性能 - 完善类型定义和文档说明,增强代码可维护性 - 调整组件中计算器调用方式,适配新架构 - 更新页面路径配置,统一导航地址管理
This commit is contained in:
parent
d4013c986f
commit
94357ac9f3
@ -1,11 +1,7 @@
|
|||||||
import { View } from "@tarojs/components";
|
import { View } from "@tarojs/components";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Table, TableColumnProps } from "@nutui/nutui-react-taro";
|
import { Table, TableColumnProps } from "@nutui/nutui-react-taro";
|
||||||
import {
|
import { formatCurrency, PurchaseOrderCalculator } from "@/utils";
|
||||||
formatCurrency,
|
|
||||||
OrderSupplierCalculator,
|
|
||||||
PurchaseOrderCalculator,
|
|
||||||
} from "@/utils";
|
|
||||||
import { Icon } from "@/components";
|
import { Icon } from "@/components";
|
||||||
import { Decimal } from "decimal.js";
|
import { Decimal } from "decimal.js";
|
||||||
|
|
||||||
@ -144,10 +140,9 @@ export default function PurchasePreview(props: IPurchasePreviewProps) {
|
|||||||
<View className="text-sm font-bold">瓜农装箱明细</View>
|
<View className="text-sm font-bold">瓜农装箱明细</View>
|
||||||
<View className="flex flex-col gap-2.5">
|
<View className="flex flex-col gap-2.5">
|
||||||
{orderSupplierList.map((supplier, index) => {
|
{orderSupplierList.map((supplier, index) => {
|
||||||
const calculator = new OrderSupplierCalculator(
|
const calculator = new PurchaseOrderCalculator(
|
||||||
purchaseOrder as any,
|
purchaseOrder,
|
||||||
supplier as any,
|
).getSupplierCalculator(supplier);
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Image, View } from "@tarojs/components";
|
import { Image, View } from "@tarojs/components";
|
||||||
import { Icon } from "@/components";
|
import { Icon } from "@/components";
|
||||||
import { Button, Toast } from "@nutui/nutui-react-taro";
|
import { Button, Toast } from "@nutui/nutui-react-taro";
|
||||||
import { OrderSupplierCalculator, uploadFile } from "@/utils";
|
import { PurchaseOrderCalculator, uploadFile } from "@/utils";
|
||||||
import Taro from "@tarojs/taro";
|
import Taro from "@tarojs/taro";
|
||||||
import { globalStore } from "@/store/global-store";
|
import { globalStore } from "@/store/global-store";
|
||||||
|
|
||||||
@ -29,9 +29,9 @@ export default function TicketUpload(props: ITicketUploadProps) {
|
|||||||
if (!supplierVO) {
|
if (!supplierVO) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const calculator = new OrderSupplierCalculator(
|
|
||||||
value as any,
|
const calculator = new PurchaseOrderCalculator(value).getSupplierCalculator(
|
||||||
supplierVO as any,
|
supplierVO,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -134,7 +134,7 @@ const quickActionMap = {
|
|||||||
icon: "file-invoice",
|
icon: "file-invoice",
|
||||||
iconColor: "var(--color-orange-600)",
|
iconColor: "var(--color-orange-600)",
|
||||||
bgColorClass: "bg-orange-100",
|
bgColorClass: "bg-orange-100",
|
||||||
path: "/pages/supplier/purchase/invoice",
|
path: "/pages/invoice/upload",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "supplierManage",
|
id: "supplierManage",
|
||||||
@ -142,7 +142,7 @@ const quickActionMap = {
|
|||||||
icon: "clipboard-list",
|
icon: "clipboard-list",
|
||||||
iconColor: "var(--color-green-600)",
|
iconColor: "var(--color-green-600)",
|
||||||
bgColorClass: "bg-green-100",
|
bgColorClass: "bg-green-100",
|
||||||
path: "/pages/supplier/list",
|
path: "/pages/supplier/all",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"market-buyer": [
|
"market-buyer": [
|
||||||
@ -188,34 +188,34 @@ const quickActionMap = {
|
|||||||
bgColorClass: "bg-yellow-100",
|
bgColorClass: "bg-yellow-100",
|
||||||
path: "/pages/delivery/list",
|
path: "/pages/delivery/list",
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
id: "dailyExpense",
|
// id: "dailyExpense",
|
||||||
title: "日常花销",
|
// title: "日常花销",
|
||||||
icon: "receipt",
|
// icon: "receipt",
|
||||||
iconColor: "var(--color-purple-600)",
|
// iconColor: "var(--color-purple-600)",
|
||||||
bgColorClass: "bg-purple-100",
|
// bgColorClass: "bg-purple-100",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: "reschedule",
|
// id: "reschedule",
|
||||||
title: "改签处理",
|
// title: "改签处理",
|
||||||
icon: "right-left",
|
// icon: "right-left",
|
||||||
iconColor: "var(--color-orange-600)",
|
// iconColor: "var(--color-orange-600)",
|
||||||
bgColorClass: "bg-orange-100",
|
// bgColorClass: "bg-orange-100",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: "return",
|
// id: "return",
|
||||||
title: "退货单",
|
// title: "退货单",
|
||||||
icon: "rotate-left",
|
// icon: "rotate-left",
|
||||||
iconColor: "var(--color-rose-600)",
|
// iconColor: "var(--color-rose-600)",
|
||||||
bgColorClass: "bg-rose-100",
|
// bgColorClass: "bg-rose-100",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: "profitBoard",
|
// id: "profitBoard",
|
||||||
title: "利润看板",
|
// title: "利润看板",
|
||||||
icon: "chart-pie",
|
// icon: "chart-pie",
|
||||||
iconColor: "var(--color-blue-600)",
|
// iconColor: "var(--color-blue-600)",
|
||||||
bgColorClass: "bg-blue-100",
|
// bgColorClass: "bg-blue-100",
|
||||||
},
|
// },
|
||||||
],
|
],
|
||||||
boss: [
|
boss: [
|
||||||
{
|
{
|
||||||
@ -224,7 +224,7 @@ const quickActionMap = {
|
|||||||
icon: "file-signature",
|
icon: "file-signature",
|
||||||
iconColor: "var(--color-primary)",
|
iconColor: "var(--color-primary)",
|
||||||
bgColorClass: "bg-primary/10",
|
bgColorClass: "bg-primary/10",
|
||||||
path: "/pages/purchase/approver/audit/list",
|
path: "/pages/purchase/approval/pending",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "history",
|
id: "history",
|
||||||
@ -242,27 +242,27 @@ const quickActionMap = {
|
|||||||
bgColorClass: "bg-yellow-100",
|
bgColorClass: "bg-yellow-100",
|
||||||
path: "/pages/delivery/list",
|
path: "/pages/delivery/list",
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
id: "dailyExpense",
|
// id: "dailyExpense",
|
||||||
title: "录花销",
|
// title: "录花销",
|
||||||
icon: "receipt",
|
// icon: "receipt",
|
||||||
iconColor: "var(--color-purple-600)",
|
// iconColor: "var(--color-purple-600)",
|
||||||
bgColorClass: "bg-purple-100",
|
// bgColorClass: "bg-purple-100",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: "reschedule",
|
// id: "reschedule",
|
||||||
title: "看利润",
|
// title: "看利润",
|
||||||
icon: "chart-line",
|
// icon: "chart-line",
|
||||||
iconColor: "var(--color-orange-600)",
|
// iconColor: "var(--color-orange-600)",
|
||||||
bgColorClass: "bg-orange-100",
|
// bgColorClass: "bg-orange-100",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: "profitBoard",
|
// id: "profitBoard",
|
||||||
title: "查客户",
|
// title: "查客户",
|
||||||
icon: "user",
|
// icon: "user",
|
||||||
iconColor: "var(--color-blue-600)",
|
// iconColor: "var(--color-blue-600)",
|
||||||
bgColorClass: "bg-blue-100",
|
// bgColorClass: "bg-blue-100",
|
||||||
},
|
// },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -43,7 +43,7 @@ export default hocAuth(function Page(props: CommonComponent) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [userRoleVO]);
|
}, [userRoleVO.roleId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export default hocAuth(function Page(props: CommonComponent) {
|
|||||||
data: { data: menuTree },
|
data: { data: menuTree },
|
||||||
} = await auth.user.userMenu({
|
} = await auth.user.userMenu({
|
||||||
roleMenuTreeQry: {
|
roleMenuTreeQry: {
|
||||||
|
//@ts-ignore
|
||||||
roleId: roleIdList,
|
roleId: roleIdList,
|
||||||
platformId: "1991353387274342401",
|
platformId: "1991353387274342401",
|
||||||
},
|
},
|
||||||
@ -25,7 +26,7 @@ export default hocAuth(function Page(props: CommonComponent) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initMenuList([userRoleVO.roleId]).then();
|
initMenuList([userRoleVO.roleId]).then();
|
||||||
}, [userRoleVO]);
|
}, [userRoleVO.roleId]);
|
||||||
|
|
||||||
const handleMenuClick = (menu: BusinessAPI.MenuVO) => {
|
const handleMenuClick = (menu: BusinessAPI.MenuVO) => {
|
||||||
if (menu.path) {
|
if (menu.path) {
|
||||||
|
|||||||
@ -188,7 +188,7 @@ export default hocAuth(function Page(props: CommonComponent) {
|
|||||||
onFinish={() => {
|
onFinish={() => {
|
||||||
// 返回首页
|
// 返回首页
|
||||||
Taro.redirectTo({
|
Taro.redirectTo({
|
||||||
url: "/pages/purchase/approver/audit/list",
|
url: "/pages/purchase/approval/pending",
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -200,7 +200,7 @@ export default hocAuth(function Page(props: CommonComponent) {
|
|||||||
onFinish={() => {
|
onFinish={() => {
|
||||||
// 关闭当前页面并跳转到采购单审核通过页面
|
// 关闭当前页面并跳转到采购单审核通过页面
|
||||||
Taro.redirectTo({
|
Taro.redirectTo({
|
||||||
url: buildUrl(`/pages/purchase/approver/audit/result`, {
|
url: buildUrl(`/pages/purchase/approval/result`, {
|
||||||
orderId: purchaseOrderVO?.orderId,
|
orderId: purchaseOrderVO?.orderId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -691,8 +691,10 @@ export default hocAuth(function Page(props: CommonComponent) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View className={"flex flex-col gap-2.5 p-2.5"}>
|
<View className={"flex flex-col gap-2.5 p-2.5"}>
|
||||||
|
{purchaseOrderVO.state === "REJECTED" &&
|
||||||
|
purchaseOrderVO.auditState === "BOSS_REJECTED" && <View></View>}
|
||||||
{/* 顶部导航 */}
|
{/* 顶部导航 */}
|
||||||
<View className="flex flex-col gap-2.5 rounded-md bg-white p-2.5 shadow-sm">
|
<View className="relative flex flex-col gap-2.5 rounded-md bg-white p-2.5 shadow-sm">
|
||||||
<View className={"flex flex-row justify-between gap-2.5"}>
|
<View className={"flex flex-row justify-between gap-2.5"}>
|
||||||
<View className="text-lg font-bold">
|
<View className="text-lg font-bold">
|
||||||
{purchaseOrderVO?.orderDealer?.shortName || "-"} 第
|
{purchaseOrderVO?.orderDealer?.shortName || "-"} 第
|
||||||
@ -723,7 +725,8 @@ export default hocAuth(function Page(props: CommonComponent) {
|
|||||||
disabled={
|
disabled={
|
||||||
userRoleVO.slug === "boss" ||
|
userRoleVO.slug === "boss" ||
|
||||||
(userRoleVO.slug === "reviewer" &&
|
(userRoleVO.slug === "reviewer" &&
|
||||||
purchaseOrderVO.state !== "WAITING_AUDIT")
|
purchaseOrderVO.state !== "WAITING_AUDIT" &&
|
||||||
|
purchaseOrderVO.state !== "REJECTED")
|
||||||
}
|
}
|
||||||
placeholder="请输入产地负责人姓名"
|
placeholder="请输入产地负责人姓名"
|
||||||
value={originPrincipal || purchaseOrderVO.createdByName}
|
value={originPrincipal || purchaseOrderVO.createdByName}
|
||||||
@ -754,7 +757,10 @@ export default hocAuth(function Page(props: CommonComponent) {
|
|||||||
className={`overflow-x-auto rounded-md rounded-b-lg bg-white p-2.5 shadow-sm`}
|
className={`overflow-x-auto rounded-md rounded-b-lg bg-white p-2.5 shadow-sm`}
|
||||||
>
|
>
|
||||||
<section.component
|
<section.component
|
||||||
readOnly={purchaseOrderVO.state !== "WAITING_AUDIT"}
|
readOnly={
|
||||||
|
purchaseOrderVO.state !== "WAITING_AUDIT" &&
|
||||||
|
purchaseOrderVO.state !== "REJECTED"
|
||||||
|
}
|
||||||
purchaseOrderVO={purchaseOrderVO}
|
purchaseOrderVO={purchaseOrderVO}
|
||||||
onChange={setPurchaseOrderVO}
|
onChange={setPurchaseOrderVO}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -791,33 +797,35 @@ export default hocAuth(function Page(props: CommonComponent) {
|
|||||||
{/* 按钮操作 */}
|
{/* 按钮操作 */}
|
||||||
<View className={"sticky bottom-0 z-10 bg-white"} id={"bottomBar"}>
|
<View className={"sticky bottom-0 z-10 bg-white"} id={"bottomBar"}>
|
||||||
<View className="flex justify-between gap-2 border-t border-gray-200 p-2.5">
|
<View className="flex justify-between gap-2 border-t border-gray-200 p-2.5">
|
||||||
{purchaseOrderVO.state === "WAITING_AUDIT" &&
|
{((purchaseOrderVO.state === "WAITING_AUDIT" &&
|
||||||
purchaseOrderVO.auditState === "PENDING_QUOTE_APPROVAL" && (
|
purchaseOrderVO.auditState === "PENDING_QUOTE_APPROVAL") ||
|
||||||
<>
|
(purchaseOrderVO.state === "REJECTED" &&
|
||||||
<View className={"flex-1"}>
|
purchaseOrderVO.auditState === "BOSS_REJECTED")) && (
|
||||||
<Button
|
<>
|
||||||
block
|
<View className={"flex-1"}>
|
||||||
type={"default"}
|
<Button
|
||||||
size={"xlarge"}
|
block
|
||||||
className="bg-gray-200 text-gray-700"
|
type={"default"}
|
||||||
onClick={() => setMoreActionVisible(true)}
|
size={"xlarge"}
|
||||||
>
|
className="bg-gray-200 text-gray-700"
|
||||||
更多操作
|
onClick={() => setMoreActionVisible(true)}
|
||||||
</Button>
|
>
|
||||||
</View>
|
更多操作
|
||||||
<View className={"flex-1"}>
|
</Button>
|
||||||
<Button
|
</View>
|
||||||
block
|
<View className={"flex-1"}>
|
||||||
type={"primary"}
|
<Button
|
||||||
size={"xlarge"}
|
block
|
||||||
className="bg-primary text-white"
|
type={"primary"}
|
||||||
onClick={handleSubmit}
|
size={"xlarge"}
|
||||||
>
|
className="bg-primary text-white"
|
||||||
提交老板审批
|
onClick={handleSubmit}
|
||||||
</Button>
|
>
|
||||||
</View>
|
提交老板审批
|
||||||
</>
|
</Button>
|
||||||
)}
|
</View>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{purchaseOrderVO.state === "WAITING_AUDIT" &&
|
{purchaseOrderVO.state === "WAITING_AUDIT" &&
|
||||||
purchaseOrderVO.auditState === "PENDING_BOSS_APPROVAL" && (
|
purchaseOrderVO.auditState === "PENDING_BOSS_APPROVAL" && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -23,9 +23,10 @@ import {
|
|||||||
WeighRef,
|
WeighRef,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import { business } from "@/services";
|
import { business } from "@/services";
|
||||||
import { buildUrl, generateShortId, SupplierWeightCalculator } from "@/utils";
|
import { buildUrl, generateShortId } from "@/utils";
|
||||||
import Taro from "@tarojs/taro";
|
import Taro from "@tarojs/taro";
|
||||||
import { Button } from "@nutui/nutui-react-taro";
|
import { Button } from "@nutui/nutui-react-taro";
|
||||||
|
import { WeightCalculationService } from "@/utils/classes/calculators";
|
||||||
|
|
||||||
const defaultSupplierList: Partial<BusinessAPI.OrderSupplier>[] = [
|
const defaultSupplierList: Partial<BusinessAPI.OrderSupplier>[] = [
|
||||||
{
|
{
|
||||||
@ -290,9 +291,10 @@ export default hocAuth(function Page(props: CommonComponent) {
|
|||||||
return {
|
return {
|
||||||
...prev!,
|
...prev!,
|
||||||
...purchaseOrder,
|
...purchaseOrder,
|
||||||
orderSupplierList: new SupplierWeightCalculator(
|
orderSupplierList:
|
||||||
purchaseOrder?.orderSupplierList,
|
WeightCalculationService.calculateWeights(
|
||||||
).calculate(),
|
purchaseOrder?.orderSupplierList,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@ -324,9 +326,10 @@ export default hocAuth(function Page(props: CommonComponent) {
|
|||||||
return {
|
return {
|
||||||
...prev!,
|
...prev!,
|
||||||
...purchaseOrder,
|
...purchaseOrder,
|
||||||
orderSupplierList: new SupplierWeightCalculator(
|
orderSupplierList:
|
||||||
purchaseOrder?.orderSupplierList,
|
WeightCalculationService.calculateWeights(
|
||||||
).calculate(),
|
purchaseOrder?.orderSupplierList,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@ -358,9 +361,10 @@ export default hocAuth(function Page(props: CommonComponent) {
|
|||||||
return {
|
return {
|
||||||
...prev!,
|
...prev!,
|
||||||
...purchaseOrder,
|
...purchaseOrder,
|
||||||
orderSupplierList: new SupplierWeightCalculator(
|
orderSupplierList:
|
||||||
purchaseOrder?.orderSupplierList,
|
WeightCalculationService.calculateWeights(
|
||||||
).calculate(),
|
purchaseOrder.orderSupplierList,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -1,110 +0,0 @@
|
|||||||
import { Decimal } from "decimal.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 采购订单计算器类
|
|
||||||
* 封装所有与采购订单相关的计算逻辑
|
|
||||||
*/
|
|
||||||
export class OrderSupplierCalculator {
|
|
||||||
// @ts-ignore
|
|
||||||
private purchaseOrderVO: BusinessAPI.PurchaseOrderVO;
|
|
||||||
private orderSupplier: BusinessAPI.OrderSupplier;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
purchaseOrderVO: BusinessAPI.PurchaseOrderVO,
|
|
||||||
orderSupplier: BusinessAPI.OrderSupplier,
|
|
||||||
) {
|
|
||||||
this.purchaseOrderVO = purchaseOrderVO;
|
|
||||||
this.orderSupplier = orderSupplier;
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化计算规则
|
|
||||||
*/
|
|
||||||
private init() {
|
|
||||||
Decimal.set({
|
|
||||||
precision: 20,
|
|
||||||
rounding: Decimal.ROUND_HALF_UP, // 使用常量更清晰
|
|
||||||
toExpNeg: -7,
|
|
||||||
toExpPos: 21,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 毛重
|
|
||||||
*/
|
|
||||||
getGrossWeight(): number {
|
|
||||||
return new Decimal(this.orderSupplier.grossWeight || 0).toNumber();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 净重
|
|
||||||
*/
|
|
||||||
getNetWeight(): number {
|
|
||||||
return new Decimal(this.orderSupplier.netWeight || 0).toNumber();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 箱重
|
|
||||||
*/
|
|
||||||
getBoxWeight(): number {
|
|
||||||
return new Decimal(this.getGrossWeight())
|
|
||||||
.sub(this.getNetWeight())
|
|
||||||
.toNumber();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 采购单价
|
|
||||||
*/
|
|
||||||
getPurchasePrice(): number {
|
|
||||||
return new Decimal(this.orderSupplier.purchasePrice || 0).toNumber();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 合计金额
|
|
||||||
*/
|
|
||||||
getTotalAmount(): number {
|
|
||||||
if (
|
|
||||||
this.orderSupplier.orderPackageList?.some((pkg) => pkg.boxType === "USED")
|
|
||||||
) {
|
|
||||||
return new Decimal(this.getNetWeight())
|
|
||||||
.mul(this.getPurchasePrice())
|
|
||||||
.toNumber();
|
|
||||||
} else {
|
|
||||||
return new Decimal(this.getGrossWeight())
|
|
||||||
.mul(this.getPurchasePrice())
|
|
||||||
.toNumber();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 押金支付金额
|
|
||||||
*/
|
|
||||||
getDepositPaidAmount(): number {
|
|
||||||
if (!this.orderSupplier.isDepositPaid) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,6 +3,9 @@ import { CostCalculator } from "./modules/CostCalculator";
|
|||||||
import { SalesCalculator } from "./modules/SalesCalculator";
|
import { SalesCalculator } from "./modules/SalesCalculator";
|
||||||
import { ProfitCalculator } from "./modules/ProfitCalculator";
|
import { ProfitCalculator } from "./modules/ProfitCalculator";
|
||||||
import { PackagingCalculator } from "./modules/PackagingCalculator";
|
import { PackagingCalculator } from "./modules/PackagingCalculator";
|
||||||
|
import { OrderSupplierCalculator } from "./modules/OrderSupplierCalculator";
|
||||||
|
import { SupplierWeightCalculator } from "./modules/SupplierWeightCalculator";
|
||||||
|
import { WeightCalculationService } from "./services/WeightCalculationService";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 采购订单计算器
|
* 采购订单计算器
|
||||||
@ -249,6 +252,55 @@ export class PurchaseOrderCalculator {
|
|||||||
return this.packagingCalculator.calculateBoxSaleAmount();
|
return this.packagingCalculator.calculateBoxSaleAmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 供应商相关计算 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取供应商计算器
|
||||||
|
*/
|
||||||
|
getSupplierCalculator(supplier: BusinessAPI.OrderSupplier): OrderSupplierCalculator {
|
||||||
|
return new OrderSupplierCalculator(supplier, this.rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有供应商的计算器
|
||||||
|
*/
|
||||||
|
getAllSupplierCalculators(): OrderSupplierCalculator[] {
|
||||||
|
if (!this.rules.order.orderSupplierList?.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.rules.order.orderSupplierList.map(supplier =>
|
||||||
|
new OrderSupplierCalculator(supplier, this.rules)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算所有供应商的总采购金额
|
||||||
|
*/
|
||||||
|
getTotalSupplierPurchaseAmount(): number {
|
||||||
|
return this.getAllSupplierCalculators().reduce((total, calc) => {
|
||||||
|
return total + calc.calculatePurchaseAmount();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算所有供应商的总销售金额
|
||||||
|
*/
|
||||||
|
getTotalSupplierSalesAmount(): number {
|
||||||
|
return this.getAllSupplierCalculators().reduce((total, calc) => {
|
||||||
|
return total + calc.calculateSalesAmount();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算所有供应商的总毛利
|
||||||
|
*/
|
||||||
|
getTotalSupplierProfit(): number {
|
||||||
|
return this.getAllSupplierCalculators().reduce((total, calc) => {
|
||||||
|
return total + calc.calculateProfit();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取总纸箱数量
|
* 获取总纸箱数量
|
||||||
*/
|
*/
|
||||||
@ -313,4 +365,64 @@ export class PurchaseOrderCalculator {
|
|||||||
getPackagingCalculator(): PackagingCalculator {
|
getPackagingCalculator(): PackagingCalculator {
|
||||||
return this.packagingCalculator;
|
return this.packagingCalculator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取原始订单数据
|
||||||
|
*/
|
||||||
|
getOrder(): BusinessAPI.PurchaseOrderVO {
|
||||||
|
return this.rules.order;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 重量计算相关 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算供应商重量
|
||||||
|
* @returns 计算后的订单数据
|
||||||
|
*/
|
||||||
|
calculateSupplierWeights(): BusinessAPI.PurchaseOrderVO {
|
||||||
|
if (!this.rules.order.orderSupplierList?.length) {
|
||||||
|
return this.rules.order;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用重量计算服务处理供应商重量
|
||||||
|
this.rules.order.orderSupplierList = WeightCalculationService.calculateWeights(
|
||||||
|
this.rules.order.orderSupplierList
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.rules.order;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取重量计算器
|
||||||
|
*/
|
||||||
|
getWeightCalculator(): SupplierWeightCalculator | null {
|
||||||
|
if (!this.rules.order.orderSupplierList?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return WeightCalculationService.createCalculator(this.rules.order.orderSupplierList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取重量统计信息
|
||||||
|
*/
|
||||||
|
getWeightStatistics() {
|
||||||
|
if (!this.rules.order.orderSupplierList?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return WeightCalculationService.getWeightStatistics(this.rules.order.orderSupplierList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证重量计算结果
|
||||||
|
*/
|
||||||
|
validateWeightCalculation(): { isValid: boolean; errors: string[] } | null {
|
||||||
|
const calculator = this.getWeightCalculator();
|
||||||
|
if (!calculator) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return calculator.validate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,13 @@ calculators/
|
|||||||
│ ├── CostCalculator.ts # 成本计算模块
|
│ ├── CostCalculator.ts # 成本计算模块
|
||||||
│ ├── SalesCalculator.ts # 销售计算模块
|
│ ├── SalesCalculator.ts # 销售计算模块
|
||||||
│ ├── ProfitCalculator.ts # 利润计算模块
|
│ ├── ProfitCalculator.ts # 利润计算模块
|
||||||
│ └── PackagingCalculator.ts # 包装计算模块
|
│ ├── PackagingCalculator.ts # 包装计算模块
|
||||||
|
│ ├── OrderSupplierCalculator.ts # 供应商计算模块
|
||||||
|
│ └── SupplierWeightCalculator.ts # 供应商重量计算模块
|
||||||
|
├── services/ # 服务层
|
||||||
|
│ └── WeightCalculationService.ts # 重量计算服务
|
||||||
|
├── types/ # 类型定义
|
||||||
|
│ └── index.ts
|
||||||
├── PurchaseOrderCalculator.ts # 主计算器类
|
├── PurchaseOrderCalculator.ts # 主计算器类
|
||||||
└── README.md # 文档
|
└── README.md # 文档
|
||||||
```
|
```
|
||||||
@ -37,6 +43,11 @@ calculators/
|
|||||||
- **SalesCalculator**: 销售相关计算
|
- **SalesCalculator**: 销售相关计算
|
||||||
- **ProfitCalculator**: 利润相关计算
|
- **ProfitCalculator**: 利润相关计算
|
||||||
- **PackagingCalculator**: 包装相关计算
|
- **PackagingCalculator**: 包装相关计算
|
||||||
|
- **OrderSupplierCalculator**: 单个供应商相关计算
|
||||||
|
- **SupplierWeightCalculator**: 供应商重量计算
|
||||||
|
|
||||||
|
### 4. 服务层
|
||||||
|
- **WeightCalculationService**: 重量计算服务,提供简化的 API 和批量操作功能
|
||||||
|
|
||||||
## 使用方法
|
## 使用方法
|
||||||
|
|
||||||
@ -57,6 +68,12 @@ const marketPrice = calculator.getMarketPrice();
|
|||||||
|
|
||||||
// 计算单斤成本
|
// 计算单斤成本
|
||||||
const costPerJin = calculator.getSingleCost();
|
const costPerJin = calculator.getSingleCost();
|
||||||
|
|
||||||
|
// 计算供应商重量
|
||||||
|
const updatedOrder = calculator.calculateSupplierWeights();
|
||||||
|
|
||||||
|
// 获取重量统计
|
||||||
|
const weightStats = calculator.getWeightStatistics();
|
||||||
```
|
```
|
||||||
|
|
||||||
## 主要改进
|
## 主要改进
|
||||||
|
|||||||
@ -1,181 +0,0 @@
|
|||||||
import { Decimal } from "decimal.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 农户重量计算器类
|
|
||||||
* 用于计算每个农户的净重和毛重,并赋值到原数据中
|
|
||||||
*/
|
|
||||||
export class SupplierWeightCalculator {
|
|
||||||
private suppliers: BusinessAPI.OrderSupplier[];
|
|
||||||
|
|
||||||
constructor(suppliers: BusinessAPI.OrderSupplier[]) {
|
|
||||||
this.suppliers = suppliers;
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化计算规则
|
|
||||||
*/
|
|
||||||
private init() {
|
|
||||||
Decimal.set({
|
|
||||||
precision: 20,
|
|
||||||
rounding: Decimal.ROUND_HALF_UP, // 使用常量更清晰
|
|
||||||
toExpNeg: -7,
|
|
||||||
toExpPos: 21,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算所有农户的重量信息
|
|
||||||
* @returns {Array} 包含净重、毛重和空磅的农户数据
|
|
||||||
*/
|
|
||||||
calculate(): BusinessAPI.OrderSupplier[] {
|
|
||||||
console.log("开始计算采购订单的农户重量信息...", this.suppliers);
|
|
||||||
if (!this.suppliers || this.suppliers.length === 0) {
|
|
||||||
return this.suppliers;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用第一个瓜农的空磅重量作为初始空磅重量
|
|
||||||
const initialEmptyWeight = this.suppliers[0].emptyWeight || 0;
|
|
||||||
let previousTotalWeight = initialEmptyWeight; // 上一个农户的总磅重量(kg)
|
|
||||||
|
|
||||||
for (let i = 0; i < this.suppliers.length; i++) {
|
|
||||||
const supplier = this.suppliers[i];
|
|
||||||
const isFirstSupplier = i === 0;
|
|
||||||
const isLastSupplier = supplier.isLast;
|
|
||||||
|
|
||||||
// 设置空磅重量(第一个农户使用自己的空磅重量,其他使用前一个农户的总磅重量)
|
|
||||||
if (isFirstSupplier) {
|
|
||||||
// 第一个农户的空磅重量已经是正确的
|
|
||||||
} else {
|
|
||||||
supplier.emptyWeight = this.suppliers[i - 1].totalWeight || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算本次使用纸箱的总重量(斤)
|
|
||||||
const usedBoxesWeight = this.calculateBoxesTotalWeight(supplier.orderPackageList || [], "USED")
|
|
||||||
|
|
||||||
if (!supplier.isPaper) {
|
|
||||||
// 如果不是纸箱包装,直接使用原始重量(kg转斤)
|
|
||||||
supplier.grossWeight = new Decimal(supplier.totalWeight || 0)
|
|
||||||
.sub(supplier.emptyWeight || 0)
|
|
||||||
.mul(2)
|
|
||||||
.toNumber();
|
|
||||||
|
|
||||||
supplier.netWeight = new Decimal(supplier.grossWeight || 0)
|
|
||||||
.sub(usedBoxesWeight)
|
|
||||||
.toNumber();
|
|
||||||
|
|
||||||
previousTotalWeight = supplier.totalWeight;
|
|
||||||
supplier.invoiceAmount = new Decimal(supplier.netWeight || 0)
|
|
||||||
.mul(supplier.purchasePrice || 0)
|
|
||||||
.toDecimalPlaces(0)
|
|
||||||
.toNumber();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算额外配送的纸箱总重量(斤)
|
|
||||||
const extraBoxesWeight = this.calculateBoxesTotalWeight(
|
|
||||||
supplier.orderPackageList || [],
|
|
||||||
"EXTRA",
|
|
||||||
);
|
|
||||||
|
|
||||||
// 计算车上剩余纸箱(斤)
|
|
||||||
const remainingBoxesWeight = this.calculateBoxesTotalWeight(
|
|
||||||
supplier.orderPackageList || [],
|
|
||||||
"REMAIN",
|
|
||||||
);
|
|
||||||
|
|
||||||
// 计算额外配送的已使用纸箱总重量(斤)
|
|
||||||
const extraUsedBoxesWeight = this.calculateBoxesTotalWeight(
|
|
||||||
supplier.orderPackageList || [],
|
|
||||||
"EXTRA_USED",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isFirstSupplier && isLastSupplier) {
|
|
||||||
// 既是第一个也是最后一个瓜农(单个瓜农情况)- 优先使用最后一个瓜农算法
|
|
||||||
// 净重 = (总磅 - 空磅) * 2 + 剩余空箱子重量 - 已使用额外纸箱重量
|
|
||||||
supplier.netWeight = new Decimal(supplier.totalWeight || 0)
|
|
||||||
.sub(initialEmptyWeight)
|
|
||||||
.mul(2)
|
|
||||||
.add(remainingBoxesWeight)
|
|
||||||
.sub(extraUsedBoxesWeight)
|
|
||||||
.toNumber();
|
|
||||||
// 毛重 = 净重 + 本次使用纸箱重量
|
|
||||||
supplier.grossWeight = new Decimal(supplier.netWeight || 0)
|
|
||||||
.add(usedBoxesWeight)
|
|
||||||
.toNumber();
|
|
||||||
} else if (isLastSupplier) {
|
|
||||||
// 最后一个农户(根据isLast标识判断)
|
|
||||||
// 净重 = (总磅 - 前一个总磅) * 2 + 剩余空箱子重量 - 已使用额外纸箱重量
|
|
||||||
supplier.netWeight = new Decimal(supplier.totalWeight || 0)
|
|
||||||
.sub(previousTotalWeight)
|
|
||||||
.mul(2)
|
|
||||||
.add(remainingBoxesWeight)
|
|
||||||
.sub(extraUsedBoxesWeight)
|
|
||||||
.toNumber();
|
|
||||||
// 毛重 = 净重 + 本次使用纸箱重量
|
|
||||||
supplier.grossWeight = new Decimal(supplier.netWeight || 0)
|
|
||||||
.add(usedBoxesWeight)
|
|
||||||
.toNumber();
|
|
||||||
} else if (isFirstSupplier) {
|
|
||||||
// 第一个农户(但不是最后一个)
|
|
||||||
// 净重 = (总磅 - 空磅) * 2 - 额外纸箱重量
|
|
||||||
supplier.netWeight = new Decimal(supplier.totalWeight || 0)
|
|
||||||
.sub(initialEmptyWeight)
|
|
||||||
.mul(2)
|
|
||||||
.sub(extraBoxesWeight)
|
|
||||||
.toNumber();
|
|
||||||
// 毛重 = 净重 + 本次使用纸箱重量
|
|
||||||
supplier.grossWeight = new Decimal(supplier.netWeight || 0)
|
|
||||||
.add(usedBoxesWeight)
|
|
||||||
.toNumber();
|
|
||||||
} else {
|
|
||||||
// 中间农户
|
|
||||||
// 净重 = (总磅 - 前一个总磅) * 2 - 额外纸箱重量
|
|
||||||
supplier.netWeight = new Decimal(supplier.totalWeight || 0)
|
|
||||||
.sub(previousTotalWeight)
|
|
||||||
.mul(2)
|
|
||||||
.sub(extraBoxesWeight)
|
|
||||||
.toNumber();
|
|
||||||
// 毛重 = 净重 + 本次使用纸箱重量
|
|
||||||
supplier.grossWeight = new Decimal(supplier.netWeight || 0)
|
|
||||||
.add(usedBoxesWeight)
|
|
||||||
.toNumber();
|
|
||||||
}
|
|
||||||
|
|
||||||
previousTotalWeight = supplier.totalWeight || 0;
|
|
||||||
supplier.invoiceAmount = new Decimal(supplier.netWeight || 0)
|
|
||||||
.mul(supplier.purchasePrice || 0)
|
|
||||||
.toNumber();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.suppliers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算纸箱的总重量(斤)
|
|
||||||
* @param {Array} orderPackageList - 包装信息列表
|
|
||||||
* @param {string} boxType - 箱子类型 ('USED' 或 'EXTRA',不传则计算所有)
|
|
||||||
* @returns {number} 总重量(斤)
|
|
||||||
*/
|
|
||||||
private calculateBoxesTotalWeight(
|
|
||||||
orderPackageList: BusinessAPI.OrderPackage[],
|
|
||||||
boxType?: any,
|
|
||||||
): number {
|
|
||||||
if (!orderPackageList) return 0;
|
|
||||||
|
|
||||||
let filteredPackages = orderPackageList;
|
|
||||||
if (boxType) {
|
|
||||||
filteredPackages = orderPackageList.filter(
|
|
||||||
(pkg) => pkg.boxType === boxType,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredPackages.reduce((sum, pkg) => {
|
|
||||||
// 纸箱重量单位是斤,直接使用
|
|
||||||
const boxWeight = pkg.boxProductWeight || 0;
|
|
||||||
return new Decimal(sum)
|
|
||||||
.add(new Decimal(pkg.boxCount || 0).mul(boxWeight).toDecimalPlaces(0))
|
|
||||||
.toNumber();
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -4,7 +4,7 @@
|
|||||||
* 将业务判断逻辑集中管理
|
* 将业务判断逻辑集中管理
|
||||||
*/
|
*/
|
||||||
export class PurchaseOrderRules {
|
export class PurchaseOrderRules {
|
||||||
constructor(private order: BusinessAPI.PurchaseOrderVO) {}
|
constructor(public order: BusinessAPI.PurchaseOrderVO) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否包含运费成本
|
* 是否包含运费成本
|
||||||
|
|||||||
@ -6,5 +6,4 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export { PurchaseOrderCalculator } from './PurchaseOrderCalculator'
|
export { PurchaseOrderCalculator } from './PurchaseOrderCalculator'
|
||||||
export { OrderSupplierCalculator } from './OrderSupplierCalculator'
|
export { WeightCalculationService } from './services/WeightCalculationService'
|
||||||
export { SupplierWeightCalculator } from './SupplierWeightCalculator'
|
|
||||||
|
|||||||
@ -0,0 +1,225 @@
|
|||||||
|
import { DecimalUtils } from "../core/DecimalUtils";
|
||||||
|
import { PurchaseOrderRules } from "../core/BusinessRules";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 供应商计算模块
|
||||||
|
* 负责单个供应商相关的计算
|
||||||
|
*/
|
||||||
|
export class OrderSupplierCalculator {
|
||||||
|
constructor(
|
||||||
|
private orderSupplier: BusinessAPI.OrderSupplier,
|
||||||
|
private rules: PurchaseOrderRules,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取毛重
|
||||||
|
*/
|
||||||
|
getGrossWeight(): number {
|
||||||
|
return this.orderSupplier.grossWeight || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取净重
|
||||||
|
*/
|
||||||
|
getNetWeight(): number {
|
||||||
|
return this.orderSupplier.netWeight || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算箱重
|
||||||
|
* = 毛重 - 净重
|
||||||
|
*/
|
||||||
|
getBoxWeight(): number {
|
||||||
|
return DecimalUtils.subtract(this.getGrossWeight(), this.getNetWeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取采购单价
|
||||||
|
*/
|
||||||
|
getPurchasePrice(): number {
|
||||||
|
return this.orderSupplier.purchasePrice || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取销售单价
|
||||||
|
*/
|
||||||
|
getSalesPrice(): number {
|
||||||
|
return this.orderSupplier.salePrice || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据定价方式获取重量
|
||||||
|
*/
|
||||||
|
getWeightByPricingMethod(): number {
|
||||||
|
return this.rules.getWeightByPricingMethod(this.orderSupplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算采购金额
|
||||||
|
* = 重量 × 采购单价
|
||||||
|
*/
|
||||||
|
calculatePurchaseAmount(): number {
|
||||||
|
const weight = this.getWeightByPricingMethod();
|
||||||
|
const price = this.getPurchasePrice();
|
||||||
|
return DecimalUtils.multiply(weight, price);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算销售金额
|
||||||
|
* = 重量 × 销售单价
|
||||||
|
*/
|
||||||
|
calculateSalesAmount(): number {
|
||||||
|
const weight = this.getWeightByPricingMethod();
|
||||||
|
const price = this.getSalesPrice();
|
||||||
|
return DecimalUtils.multiply(weight, price);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算采购合计金额(兼容旧逻辑)
|
||||||
|
* 如果有使用中的纸箱,按净重计算;否则按毛重计算
|
||||||
|
*/
|
||||||
|
getTotalAmount(): number {
|
||||||
|
const hasUsedBoxes = this.hasUsedBoxes();
|
||||||
|
const weight = hasUsedBoxes ? this.getNetWeight() : this.getGrossWeight();
|
||||||
|
const price = this.getPurchasePrice();
|
||||||
|
|
||||||
|
return DecimalUtils.multiply(weight, price);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算毛利
|
||||||
|
* = 销售金额 - 采购金额
|
||||||
|
*/
|
||||||
|
calculateProfit(): number {
|
||||||
|
const salesAmount = this.calculateSalesAmount();
|
||||||
|
const purchaseAmount = this.calculatePurchaseAmount();
|
||||||
|
return DecimalUtils.subtract(salesAmount, purchaseAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已支付的押金金额
|
||||||
|
*/
|
||||||
|
getDepositPaidAmount(): number {
|
||||||
|
if (!this.orderSupplier.isDepositPaid) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return this.orderSupplier.depositAmount || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取定金金额(与押金相同,不同叫法)
|
||||||
|
*/
|
||||||
|
getDepositAmount(): number {
|
||||||
|
return this.getDepositPaidAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否有使用中的纸箱
|
||||||
|
*/
|
||||||
|
hasUsedBoxes(): boolean {
|
||||||
|
return (
|
||||||
|
this.orderSupplier.orderPackageList?.some(
|
||||||
|
(pkg) => pkg.boxType === "USED",
|
||||||
|
) || false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算纸箱总重量
|
||||||
|
* @param boxType 纸箱类型,不传则计算所有类型
|
||||||
|
*/
|
||||||
|
calculateBoxesTotalWeight(boxType?: string): number {
|
||||||
|
if (!this.orderSupplier.orderPackageList?.length) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.orderSupplier.orderPackageList
|
||||||
|
.filter((pkg) => !boxType || pkg.boxType === boxType)
|
||||||
|
.reduce((total, pkg) => {
|
||||||
|
const boxWeight = DecimalUtils.multiply(
|
||||||
|
pkg.boxCount || 0,
|
||||||
|
pkg.boxProductWeight || 0,
|
||||||
|
);
|
||||||
|
return DecimalUtils.add(total, boxWeight);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算使用中纸箱的数量
|
||||||
|
*/
|
||||||
|
getUsedBoxCount(): number {
|
||||||
|
if (!this.orderSupplier.orderPackageList?.length) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.orderSupplier.orderPackageList
|
||||||
|
.filter((pkg) => pkg.boxType === "USED")
|
||||||
|
.reduce((total, pkg) => {
|
||||||
|
return DecimalUtils.add(total, pkg.boxCount || 0);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算使用中纸箱的售卖金额
|
||||||
|
*/
|
||||||
|
getUsedBoxesSaleAmount(): number {
|
||||||
|
if (!this.orderSupplier.orderPackageList?.length) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.orderSupplier.orderPackageList
|
||||||
|
.filter((pkg) => pkg.boxType === "USED")
|
||||||
|
.reduce((total, pkg) => {
|
||||||
|
const saleAmount = DecimalUtils.multiply(
|
||||||
|
pkg.boxCount || 0,
|
||||||
|
pkg.boxSalePrice || 0,
|
||||||
|
);
|
||||||
|
return DecimalUtils.add(total, saleAmount);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算毛利率
|
||||||
|
* = 毛利 ÷ 销售金额 × 100%
|
||||||
|
*/
|
||||||
|
calculateProfitRate(): string {
|
||||||
|
const profit = this.calculateProfit();
|
||||||
|
const salesAmount = this.calculateSalesAmount();
|
||||||
|
|
||||||
|
if (salesAmount === 0) {
|
||||||
|
return "0.00";
|
||||||
|
}
|
||||||
|
|
||||||
|
const rate = DecimalUtils.percentage(profit, salesAmount);
|
||||||
|
return DecimalUtils.toFixed(rate, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算斤价(按计价重量)
|
||||||
|
*/
|
||||||
|
calculatePricePerJin(): string {
|
||||||
|
const purchaseAmount = this.calculatePurchaseAmount();
|
||||||
|
const weight = this.getWeightByPricingMethod();
|
||||||
|
|
||||||
|
if (weight === 0) {
|
||||||
|
return "0.00";
|
||||||
|
}
|
||||||
|
|
||||||
|
const pricePerJin = DecimalUtils.divide(purchaseAmount, weight);
|
||||||
|
return DecimalUtils.toFixed(pricePerJin, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取供应商名称
|
||||||
|
*/
|
||||||
|
getSupplierName(): string {
|
||||||
|
return this.orderSupplier.name || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断供应商是否有效
|
||||||
|
*/
|
||||||
|
isValid(): boolean {
|
||||||
|
return !!this.orderSupplier.supplierId && !!this.orderSupplier.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -88,9 +88,6 @@ export class SalesCalculator {
|
|||||||
const baseCost = costCalculator.calculateBaseCost();
|
const baseCost = costCalculator.calculateBaseCost();
|
||||||
const totalWeight = this.calculateTotalWeight();
|
const totalWeight = this.calculateTotalWeight();
|
||||||
|
|
||||||
console.log("基础成本:", baseCost);
|
|
||||||
console.log("总重量:", totalWeight);
|
|
||||||
|
|
||||||
return DecimalUtils.toFixed(
|
return DecimalUtils.toFixed(
|
||||||
DecimalUtils.divide(baseCost, totalWeight, 0),
|
DecimalUtils.divide(baseCost, totalWeight, 0),
|
||||||
2,
|
2,
|
||||||
|
|||||||
@ -0,0 +1,251 @@
|
|||||||
|
import { DecimalUtils } from "../core/DecimalUtils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 农户重量计算器
|
||||||
|
* 负责计算每个农户的净重和毛重
|
||||||
|
*/
|
||||||
|
export class SupplierWeightCalculator {
|
||||||
|
private suppliers: BusinessAPI.OrderSupplier[];
|
||||||
|
private initialEmptyWeight: number;
|
||||||
|
private previousTotalWeight: number;
|
||||||
|
|
||||||
|
constructor(suppliers: BusinessAPI.OrderSupplier[]) {
|
||||||
|
this.suppliers = suppliers || [];
|
||||||
|
this.initialEmptyWeight = this.suppliers[0]?.emptyWeight || 0;
|
||||||
|
this.previousTotalWeight = this.initialEmptyWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算所有农户的重量信息
|
||||||
|
* @returns 处理后的供应商列表
|
||||||
|
*/
|
||||||
|
calculate(): BusinessAPI.OrderSupplier[] {
|
||||||
|
if (!this.suppliers.length) {
|
||||||
|
return this.suppliers;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < this.suppliers.length; i++) {
|
||||||
|
const supplier = this.suppliers[i];
|
||||||
|
this.calculateSupplierWeight(supplier, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.suppliers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算单个供应商的重量
|
||||||
|
*/
|
||||||
|
private calculateSupplierWeight(supplier: BusinessAPI.OrderSupplier, index: number): void {
|
||||||
|
const isFirstSupplier = index === 0;
|
||||||
|
const isLastSupplier = supplier.isLast;
|
||||||
|
|
||||||
|
// 设置空磅重量
|
||||||
|
this.setEmptyWeight(supplier, index);
|
||||||
|
|
||||||
|
// 计算各类纸箱重量
|
||||||
|
const weights = this.calculateAllBoxWeights(supplier.orderPackageList || []);
|
||||||
|
|
||||||
|
if (!supplier.isPaper) {
|
||||||
|
// 非纸箱包装的简化计算
|
||||||
|
this.calculateNonPaperSupplier(supplier, weights.used);
|
||||||
|
} else {
|
||||||
|
// 纸箱包装的复杂计算
|
||||||
|
this.calculatePaperSupplier(supplier, weights, isFirstSupplier, isLastSupplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算发票金额
|
||||||
|
this.calculateInvoiceAmount(supplier);
|
||||||
|
|
||||||
|
// 更新上一个供应商的总重量
|
||||||
|
this.previousTotalWeight = supplier.totalWeight || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置空磅重量
|
||||||
|
*/
|
||||||
|
private setEmptyWeight(supplier: BusinessAPI.OrderSupplier, index: number): void {
|
||||||
|
if (index === 0) {
|
||||||
|
// 第一个农户保持原有空磅重量
|
||||||
|
} else {
|
||||||
|
// 其他农户使用前一个农户的总磅重量
|
||||||
|
supplier.emptyWeight = this.suppliers[index - 1]?.totalWeight || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算所有类型的纸箱重量
|
||||||
|
*/
|
||||||
|
private calculateAllBoxWeights(packages: BusinessAPI.OrderPackage[]) {
|
||||||
|
return {
|
||||||
|
used: this.calculateBoxWeightByType(packages, "USED"),
|
||||||
|
extra: this.calculateBoxWeightByType(packages, "EXTRA"),
|
||||||
|
remain: this.calculateBoxWeightByType(packages, "REMAIN"),
|
||||||
|
extraUsed: this.calculateBoxWeightByType(packages, "EXTRA_USED"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算非纸箱包装供应商的重量
|
||||||
|
*/
|
||||||
|
private calculateNonPaperSupplier(supplier: BusinessAPI.OrderSupplier, usedBoxesWeight: number): void {
|
||||||
|
// 毛重 = (总磅 - 空磅) × 2
|
||||||
|
supplier.grossWeight = DecimalUtils.multiply(
|
||||||
|
DecimalUtils.subtract(supplier.totalWeight || 0, supplier.emptyWeight || 0),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
|
// 净重 = 毛重 - 使用纸箱重量
|
||||||
|
supplier.netWeight = DecimalUtils.subtract(
|
||||||
|
supplier.grossWeight || 0,
|
||||||
|
usedBoxesWeight
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算纸箱包装供应商的重量
|
||||||
|
*/
|
||||||
|
private calculatePaperSupplier(
|
||||||
|
supplier: BusinessAPI.OrderSupplier,
|
||||||
|
weights: { used: number; extra: number; remain: number; extraUsed: number },
|
||||||
|
isFirstSupplier: boolean,
|
||||||
|
isLastSupplier: boolean
|
||||||
|
): void {
|
||||||
|
const weightDiff = isFirstSupplier
|
||||||
|
? DecimalUtils.subtract(supplier.totalWeight || 0, this.initialEmptyWeight)
|
||||||
|
: DecimalUtils.subtract(supplier.totalWeight || 0, this.previousTotalWeight);
|
||||||
|
|
||||||
|
const weightDiffInJin = DecimalUtils.multiply(weightDiff, 2);
|
||||||
|
|
||||||
|
if (isFirstSupplier && isLastSupplier) {
|
||||||
|
// 单个农户情况
|
||||||
|
supplier.netWeight = DecimalUtils.add(
|
||||||
|
DecimalUtils.subtract(
|
||||||
|
DecimalUtils.add(weightDiffInJin, weights.remain),
|
||||||
|
weights.extraUsed
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (isLastSupplier) {
|
||||||
|
// 最后一个农户
|
||||||
|
supplier.netWeight = DecimalUtils.add(
|
||||||
|
DecimalUtils.subtract(
|
||||||
|
weightDiffInJin,
|
||||||
|
weights.extraUsed
|
||||||
|
),
|
||||||
|
weights.remain
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 中间农户(包括第一个但不是最后一个)
|
||||||
|
supplier.netWeight = DecimalUtils.subtract(weightDiffInJin, weights.extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 毛重 = 净重 + 本次使用纸箱重量
|
||||||
|
supplier.grossWeight = DecimalUtils.add(
|
||||||
|
supplier.netWeight || 0,
|
||||||
|
weights.used
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算发票金额
|
||||||
|
*/
|
||||||
|
private calculateInvoiceAmount(supplier: BusinessAPI.OrderSupplier): void {
|
||||||
|
supplier.invoiceAmount = DecimalUtils.multiply(
|
||||||
|
supplier.netWeight || 0,
|
||||||
|
supplier.purchasePrice || 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据类型计算纸箱总重量(斤)
|
||||||
|
*/
|
||||||
|
private calculateBoxWeightByType(
|
||||||
|
packages: BusinessAPI.OrderPackage[],
|
||||||
|
boxType?: string
|
||||||
|
): number {
|
||||||
|
const filteredPackages = boxType
|
||||||
|
? packages.filter(pkg => pkg.boxType === boxType)
|
||||||
|
: packages;
|
||||||
|
|
||||||
|
return filteredPackages.reduce((total, pkg) => {
|
||||||
|
const weight = DecimalUtils.multiply(
|
||||||
|
pkg.boxCount || 0,
|
||||||
|
pkg.boxProductWeight || 0
|
||||||
|
);
|
||||||
|
return DecimalUtils.add(total, weight);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取计算后的供应商列表
|
||||||
|
*/
|
||||||
|
getSuppliers(): BusinessAPI.OrderSupplier[] {
|
||||||
|
return this.suppliers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定供应商的净重
|
||||||
|
*/
|
||||||
|
getSupplierNetWeight(supplierId: string | number): number {
|
||||||
|
const supplier = this.suppliers.find(s => s.supplierId === supplierId);
|
||||||
|
return supplier?.netWeight || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定供应商的毛重
|
||||||
|
*/
|
||||||
|
getSupplierGrossWeight(supplierId: string | number): number {
|
||||||
|
const supplier = this.suppliers.find(s => s.supplierId === supplierId);
|
||||||
|
return supplier?.grossWeight || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取总净重
|
||||||
|
*/
|
||||||
|
getTotalNetWeight(): number {
|
||||||
|
return this.suppliers.reduce((total, supplier) => {
|
||||||
|
return DecimalUtils.add(total, supplier.netWeight || 0);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取总毛重
|
||||||
|
*/
|
||||||
|
getTotalGrossWeight(): number {
|
||||||
|
return this.suppliers.reduce((total, supplier) => {
|
||||||
|
return DecimalUtils.add(total, supplier.grossWeight || 0);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取总发票金额
|
||||||
|
*/
|
||||||
|
getTotalInvoiceAmount(): number {
|
||||||
|
return this.suppliers.reduce((total, supplier) => {
|
||||||
|
return DecimalUtils.add(total, supplier.invoiceAmount || 0);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证计算结果的合理性
|
||||||
|
*/
|
||||||
|
validate(): { isValid: boolean; errors: string[] } {
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
for (const supplier of this.suppliers) {
|
||||||
|
if ((supplier.netWeight || 0) < 0) {
|
||||||
|
errors.push(`供应商 ${supplier.name} 的净重为负数`);
|
||||||
|
}
|
||||||
|
if ((supplier.grossWeight || 0) < 0) {
|
||||||
|
errors.push(`供应商 ${supplier.name} 的毛重为负数`);
|
||||||
|
}
|
||||||
|
if ((supplier.grossWeight || 0) < (supplier.netWeight || 0)) {
|
||||||
|
errors.push(`供应商 ${supplier.name} 的毛重小于净重`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: errors.length === 0,
|
||||||
|
errors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
import { SupplierWeightCalculator } from "../modules/SupplierWeightCalculator";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重量计算服务
|
||||||
|
* 统一管理供应商重量计算逻辑
|
||||||
|
*/
|
||||||
|
export class WeightCalculationService {
|
||||||
|
/**
|
||||||
|
* 计算供应商重量(静态方法)
|
||||||
|
* @param suppliers 供应商列表
|
||||||
|
* @returns 计算后的供应商列表
|
||||||
|
*/
|
||||||
|
static calculateWeights(suppliers: BusinessAPI.OrderSupplier[]): BusinessAPI.OrderSupplier[] {
|
||||||
|
const calculator = new SupplierWeightCalculator(suppliers);
|
||||||
|
return calculator.calculate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建重量计算器实例
|
||||||
|
* @param suppliers 供应商列表
|
||||||
|
* @returns 计算器实例
|
||||||
|
*/
|
||||||
|
static createCalculator(suppliers: BusinessAPI.OrderSupplier[]): SupplierWeightCalculator {
|
||||||
|
return new SupplierWeightCalculator(suppliers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量计算多个订单的重量
|
||||||
|
* @param orders 订单列表
|
||||||
|
* @returns 处理后的订单列表
|
||||||
|
*/
|
||||||
|
static calculateBatchWeights(
|
||||||
|
orders: Array<{ orderSupplierList?: BusinessAPI.OrderSupplier[] }>
|
||||||
|
): Array<{ orderSupplierList?: BusinessAPI.OrderSupplier[] }> {
|
||||||
|
return orders.map(order => {
|
||||||
|
if (order.orderSupplierList?.length) {
|
||||||
|
order.orderSupplierList = this.calculateWeights(order.orderSupplierList);
|
||||||
|
}
|
||||||
|
return order;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取重量统计信息
|
||||||
|
* @param suppliers 供应商列表
|
||||||
|
* @returns 统计信息
|
||||||
|
*/
|
||||||
|
static getWeightStatistics(suppliers: BusinessAPI.OrderSupplier[]) {
|
||||||
|
const calculator = new SupplierWeightCalculator(suppliers);
|
||||||
|
calculator.calculate();
|
||||||
|
|
||||||
|
return {
|
||||||
|
supplierCount: suppliers.length,
|
||||||
|
totalNetWeight: calculator.getTotalNetWeight(),
|
||||||
|
totalGrossWeight: calculator.getTotalGrossWeight(),
|
||||||
|
totalInvoiceAmount: calculator.getTotalInvoiceAmount(),
|
||||||
|
averageNetWeight: calculator.getTotalNetWeight() / suppliers.length,
|
||||||
|
averageGrossWeight: calculator.getTotalGrossWeight() / suppliers.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// 计算器类
|
// 计算器类
|
||||||
export { PurchaseOrderCalculator, OrderSupplierCalculator, SupplierWeightCalculator } from './calculators'
|
export { PurchaseOrderCalculator, WeightCalculationService } from './calculators'
|
||||||
|
|
||||||
// 模板类
|
// 模板类
|
||||||
export { PdfTemplate } from './templates'
|
export { PdfTemplate } from './templates'
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user