refactor(calculators): 重构采购订单计算模块

- 移除旧的 OrderSupplierCalculator 和 SupplierWeightCalculator 实现
- 新增独立的 OrderSupplierCalculator 模块,专注单个供应商计算
- 新增 SupplierWeightCalculator 模块,优化重量计算逻辑
- 引入 WeightCalculationService 统一处理重量计算服务
- 在 PurchaseOrderCalculator 中集成新的计算模块
- 更新导出结构,暴露新的计算器和服务类
- 优化 Decimal 运算工具类使用,提高计算精度和性能
- 完善类型定义和文档说明,增强代码可维护性
- 调整组件中计算器调用方式,适配新架构
- 更新页面路径配置,统一导航地址管理
This commit is contained in:
shenyifei 2025-12-15 10:44:09 +08:00
parent d4013c986f
commit 94357ac9f3
19 changed files with 788 additions and 409 deletions

View File

@ -1,11 +1,7 @@
import { View } from "@tarojs/components";
import { useEffect, useState } from "react";
import { Table, TableColumnProps } from "@nutui/nutui-react-taro";
import {
formatCurrency,
OrderSupplierCalculator,
PurchaseOrderCalculator,
} from "@/utils";
import { formatCurrency, PurchaseOrderCalculator } from "@/utils";
import { Icon } from "@/components";
import { Decimal } from "decimal.js";
@ -144,10 +140,9 @@ export default function PurchasePreview(props: IPurchasePreviewProps) {
<View className="text-sm font-bold"></View>
<View className="flex flex-col gap-2.5">
{orderSupplierList.map((supplier, index) => {
const calculator = new OrderSupplierCalculator(
purchaseOrder as any,
supplier as any,
);
const calculator = new PurchaseOrderCalculator(
purchaseOrder,
).getSupplierCalculator(supplier);
return (
<View

View File

@ -1,7 +1,7 @@
import { Image, View } from "@tarojs/components";
import { Icon } from "@/components";
import { Button, Toast } from "@nutui/nutui-react-taro";
import { OrderSupplierCalculator, uploadFile } from "@/utils";
import { PurchaseOrderCalculator, uploadFile } from "@/utils";
import Taro from "@tarojs/taro";
import { globalStore } from "@/store/global-store";
@ -29,9 +29,9 @@ export default function TicketUpload(props: ITicketUploadProps) {
if (!supplierVO) {
return;
}
const calculator = new OrderSupplierCalculator(
value as any,
supplierVO as any,
const calculator = new PurchaseOrderCalculator(value).getSupplierCalculator(
supplierVO,
);
return (

View File

@ -134,7 +134,7 @@ const quickActionMap = {
icon: "file-invoice",
iconColor: "var(--color-orange-600)",
bgColorClass: "bg-orange-100",
path: "/pages/supplier/purchase/invoice",
path: "/pages/invoice/upload",
},
{
id: "supplierManage",
@ -142,7 +142,7 @@ const quickActionMap = {
icon: "clipboard-list",
iconColor: "var(--color-green-600)",
bgColorClass: "bg-green-100",
path: "/pages/supplier/list",
path: "/pages/supplier/all",
},
],
"market-buyer": [
@ -188,34 +188,34 @@ const quickActionMap = {
bgColorClass: "bg-yellow-100",
path: "/pages/delivery/list",
},
{
id: "dailyExpense",
title: "日常花销",
icon: "receipt",
iconColor: "var(--color-purple-600)",
bgColorClass: "bg-purple-100",
},
{
id: "reschedule",
title: "改签处理",
icon: "right-left",
iconColor: "var(--color-orange-600)",
bgColorClass: "bg-orange-100",
},
{
id: "return",
title: "退货单",
icon: "rotate-left",
iconColor: "var(--color-rose-600)",
bgColorClass: "bg-rose-100",
},
{
id: "profitBoard",
title: "利润看板",
icon: "chart-pie",
iconColor: "var(--color-blue-600)",
bgColorClass: "bg-blue-100",
},
// {
// id: "dailyExpense",
// title: "日常花销",
// icon: "receipt",
// iconColor: "var(--color-purple-600)",
// bgColorClass: "bg-purple-100",
// },
// {
// id: "reschedule",
// title: "改签处理",
// icon: "right-left",
// iconColor: "var(--color-orange-600)",
// bgColorClass: "bg-orange-100",
// },
// {
// id: "return",
// title: "退货单",
// icon: "rotate-left",
// iconColor: "var(--color-rose-600)",
// bgColorClass: "bg-rose-100",
// },
// {
// id: "profitBoard",
// title: "利润看板",
// icon: "chart-pie",
// iconColor: "var(--color-blue-600)",
// bgColorClass: "bg-blue-100",
// },
],
boss: [
{
@ -224,7 +224,7 @@ const quickActionMap = {
icon: "file-signature",
iconColor: "var(--color-primary)",
bgColorClass: "bg-primary/10",
path: "/pages/purchase/approver/audit/list",
path: "/pages/purchase/approval/pending",
},
{
id: "history",
@ -242,27 +242,27 @@ const quickActionMap = {
bgColorClass: "bg-yellow-100",
path: "/pages/delivery/list",
},
{
id: "dailyExpense",
title: "录花销",
icon: "receipt",
iconColor: "var(--color-purple-600)",
bgColorClass: "bg-purple-100",
},
{
id: "reschedule",
title: "看利润",
icon: "chart-line",
iconColor: "var(--color-orange-600)",
bgColorClass: "bg-orange-100",
},
{
id: "profitBoard",
title: "查客户",
icon: "user",
iconColor: "var(--color-blue-600)",
bgColorClass: "bg-blue-100",
},
// {
// id: "dailyExpense",
// title: "录花销",
// icon: "receipt",
// iconColor: "var(--color-purple-600)",
// bgColorClass: "bg-purple-100",
// },
// {
// id: "reschedule",
// title: "看利润",
// icon: "chart-line",
// iconColor: "var(--color-orange-600)",
// bgColorClass: "bg-orange-100",
// },
// {
// id: "profitBoard",
// title: "查客户",
// icon: "user",
// iconColor: "var(--color-blue-600)",
// bgColorClass: "bg-blue-100",
// },
],
};

View File

@ -43,7 +43,7 @@ export default hocAuth(function Page(props: CommonComponent) {
}
});
}
}, [userRoleVO]);
}, [userRoleVO.roleId]);
return (
<>

View File

@ -16,6 +16,7 @@ export default hocAuth(function Page(props: CommonComponent) {
data: { data: menuTree },
} = await auth.user.userMenu({
roleMenuTreeQry: {
//@ts-ignore
roleId: roleIdList,
platformId: "1991353387274342401",
},
@ -25,7 +26,7 @@ export default hocAuth(function Page(props: CommonComponent) {
useEffect(() => {
initMenuList([userRoleVO.roleId]).then();
}, [userRoleVO]);
}, [userRoleVO.roleId]);
const handleMenuClick = (menu: BusinessAPI.MenuVO) => {
if (menu.path) {

View File

@ -188,7 +188,7 @@ export default hocAuth(function Page(props: CommonComponent) {
onFinish={() => {
// 返回首页
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={() => {
// 关闭当前页面并跳转到采购单审核通过页面
Taro.redirectTo({
url: buildUrl(`/pages/purchase/approver/audit/result`, {
url: buildUrl(`/pages/purchase/approval/result`, {
orderId: purchaseOrderVO?.orderId,
}),
});

View File

@ -691,8 +691,10 @@ export default hocAuth(function Page(props: CommonComponent) {
}}
>
<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="text-lg font-bold">
{purchaseOrderVO?.orderDealer?.shortName || "-"}
@ -723,7 +725,8 @@ export default hocAuth(function Page(props: CommonComponent) {
disabled={
userRoleVO.slug === "boss" ||
(userRoleVO.slug === "reviewer" &&
purchaseOrderVO.state !== "WAITING_AUDIT")
purchaseOrderVO.state !== "WAITING_AUDIT" &&
purchaseOrderVO.state !== "REJECTED")
}
placeholder="请输入产地负责人姓名"
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`}
>
<section.component
readOnly={purchaseOrderVO.state !== "WAITING_AUDIT"}
readOnly={
purchaseOrderVO.state !== "WAITING_AUDIT" &&
purchaseOrderVO.state !== "REJECTED"
}
purchaseOrderVO={purchaseOrderVO}
onChange={setPurchaseOrderVO}
// @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="flex justify-between gap-2 border-t border-gray-200 p-2.5">
{purchaseOrderVO.state === "WAITING_AUDIT" &&
purchaseOrderVO.auditState === "PENDING_QUOTE_APPROVAL" && (
<>
<View className={"flex-1"}>
<Button
block
type={"default"}
size={"xlarge"}
className="bg-gray-200 text-gray-700"
onClick={() => setMoreActionVisible(true)}
>
</Button>
</View>
<View className={"flex-1"}>
<Button
block
type={"primary"}
size={"xlarge"}
className="bg-primary text-white"
onClick={handleSubmit}
>
</Button>
</View>
</>
)}
{((purchaseOrderVO.state === "WAITING_AUDIT" &&
purchaseOrderVO.auditState === "PENDING_QUOTE_APPROVAL") ||
(purchaseOrderVO.state === "REJECTED" &&
purchaseOrderVO.auditState === "BOSS_REJECTED")) && (
<>
<View className={"flex-1"}>
<Button
block
type={"default"}
size={"xlarge"}
className="bg-gray-200 text-gray-700"
onClick={() => setMoreActionVisible(true)}
>
</Button>
</View>
<View className={"flex-1"}>
<Button
block
type={"primary"}
size={"xlarge"}
className="bg-primary text-white"
onClick={handleSubmit}
>
</Button>
</View>
</>
)}
{purchaseOrderVO.state === "WAITING_AUDIT" &&
purchaseOrderVO.auditState === "PENDING_BOSS_APPROVAL" && (
<>

View File

@ -23,9 +23,10 @@ import {
WeighRef,
} from "@/components";
import { business } from "@/services";
import { buildUrl, generateShortId, SupplierWeightCalculator } from "@/utils";
import { buildUrl, generateShortId } from "@/utils";
import Taro from "@tarojs/taro";
import { Button } from "@nutui/nutui-react-taro";
import { WeightCalculationService } from "@/utils/classes/calculators";
const defaultSupplierList: Partial<BusinessAPI.OrderSupplier>[] = [
{
@ -290,9 +291,10 @@ export default hocAuth(function Page(props: CommonComponent) {
return {
...prev!,
...purchaseOrder,
orderSupplierList: new SupplierWeightCalculator(
purchaseOrder?.orderSupplierList,
).calculate(),
orderSupplierList:
WeightCalculationService.calculateWeights(
purchaseOrder?.orderSupplierList,
),
};
});
}}
@ -324,9 +326,10 @@ export default hocAuth(function Page(props: CommonComponent) {
return {
...prev!,
...purchaseOrder,
orderSupplierList: new SupplierWeightCalculator(
purchaseOrder?.orderSupplierList,
).calculate(),
orderSupplierList:
WeightCalculationService.calculateWeights(
purchaseOrder?.orderSupplierList,
),
};
});
}}
@ -358,9 +361,10 @@ export default hocAuth(function Page(props: CommonComponent) {
return {
...prev!,
...purchaseOrder,
orderSupplierList: new SupplierWeightCalculator(
purchaseOrder?.orderSupplierList,
).calculate(),
orderSupplierList:
WeightCalculationService.calculateWeights(
purchaseOrder.orderSupplierList,
),
};
});
}}

View File

@ -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
);
}
}

View File

@ -3,6 +3,9 @@ import { CostCalculator } from "./modules/CostCalculator";
import { SalesCalculator } from "./modules/SalesCalculator";
import { ProfitCalculator } from "./modules/ProfitCalculator";
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();
}
// ==================== 供应商相关计算 ====================
/**
*
*/
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 {
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();
}
}

View File

@ -15,7 +15,13 @@ calculators/
│ ├── CostCalculator.ts # 成本计算模块
│ ├── SalesCalculator.ts # 销售计算模块
│ ├── ProfitCalculator.ts # 利润计算模块
│ └── PackagingCalculator.ts # 包装计算模块
│ ├── PackagingCalculator.ts # 包装计算模块
│ ├── OrderSupplierCalculator.ts # 供应商计算模块
│ └── SupplierWeightCalculator.ts # 供应商重量计算模块
├── services/ # 服务层
│ └── WeightCalculationService.ts # 重量计算服务
├── types/ # 类型定义
│ └── index.ts
├── PurchaseOrderCalculator.ts # 主计算器类
└── README.md # 文档
```
@ -37,6 +43,11 @@ calculators/
- **SalesCalculator**: 销售相关计算
- **ProfitCalculator**: 利润相关计算
- **PackagingCalculator**: 包装相关计算
- **OrderSupplierCalculator**: 单个供应商相关计算
- **SupplierWeightCalculator**: 供应商重量计算
### 4. 服务层
- **WeightCalculationService**: 重量计算服务,提供简化的 API 和批量操作功能
## 使用方法
@ -57,6 +68,12 @@ const marketPrice = calculator.getMarketPrice();
// 计算单斤成本
const costPerJin = calculator.getSingleCost();
// 计算供应商重量
const updatedOrder = calculator.calculateSupplierWeights();
// 获取重量统计
const weightStats = calculator.getWeightStatistics();
```
## 主要改进

View File

@ -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);
}
}

View File

@ -4,7 +4,7 @@
*
*/
export class PurchaseOrderRules {
constructor(private order: BusinessAPI.PurchaseOrderVO) {}
constructor(public order: BusinessAPI.PurchaseOrderVO) {}
/**
*

View File

@ -6,5 +6,4 @@
*/
export { PurchaseOrderCalculator } from './PurchaseOrderCalculator'
export { OrderSupplierCalculator } from './OrderSupplierCalculator'
export { SupplierWeightCalculator } from './SupplierWeightCalculator'
export { WeightCalculationService } from './services/WeightCalculationService'

View File

@ -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;
}
}

View File

@ -88,9 +88,6 @@ export class SalesCalculator {
const baseCost = costCalculator.calculateBaseCost();
const totalWeight = this.calculateTotalWeight();
console.log("基础成本:", baseCost);
console.log("总重量:", totalWeight);
return DecimalUtils.toFixed(
DecimalUtils.divide(baseCost, totalWeight, 0),
2,

View File

@ -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
};
}
}

View File

@ -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,
};
}
}

View File

@ -6,7 +6,7 @@
*/
// 计算器类
export { PurchaseOrderCalculator, OrderSupplierCalculator, SupplierWeightCalculator } from './calculators'
export { PurchaseOrderCalculator, WeightCalculationService } from './calculators'
// 模板类
export { PdfTemplate } from './templates'
export { PdfTemplate } from './templates'