From 94357ac9f34727d448b760f482dfd8a93f566cc0 Mon Sep 17 00:00:00 2001 From: shenyifei Date: Mon, 15 Dec 2025 10:44:09 +0800 Subject: [PATCH] =?UTF-8?q?refactor(calculators):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E9=87=87=E8=B4=AD=E8=AE=A2=E5=8D=95=E8=AE=A1=E7=AE=97=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除旧的 OrderSupplierCalculator 和 SupplierWeightCalculator 实现 - 新增独立的 OrderSupplierCalculator 模块,专注单个供应商计算 - 新增 SupplierWeightCalculator 模块,优化重量计算逻辑 - 引入 WeightCalculationService 统一处理重量计算服务 - 在 PurchaseOrderCalculator 中集成新的计算模块 - 更新导出结构,暴露新的计算器和服务类 - 优化 Decimal 运算工具类使用,提高计算精度和性能 - 完善类型定义和文档说明,增强代码可维护性 - 调整组件中计算器调用方式,适配新架构 - 更新页面路径配置,统一导航地址管理 --- .../purchase/module/PurchasePreview.tsx | 13 +- .../purchase/module/TicketUpload.tsx | 8 +- packages/app-client/src/constant/workbench.ts | 104 ++++---- .../app-client/src/pages/main/index/index.tsx | 2 +- .../app-client/src/pages/main/menu/index.tsx | 3 +- .../src/pages/purchase/approval/audit.tsx | 4 +- .../src/pages/purchase/audit/audit.tsx | 68 ++--- .../src/pages/purchase/enter/create.tsx | 24 +- .../calculators/OrderSupplierCalculator.ts | 110 -------- .../calculators/PurchaseOrderCalculator.ts | 112 ++++++++ .../src/utils/classes/calculators/README.md | 19 +- .../calculators/SupplierWeightCalculator.ts | 181 ------------- .../classes/calculators/core/BusinessRules.ts | 2 +- .../src/utils/classes/calculators/index.ts | 3 +- .../modules/OrderSupplierCalculator.ts | 225 ++++++++++++++++ .../calculators/modules/SalesCalculator.ts | 3 - .../modules/SupplierWeightCalculator.ts | 251 ++++++++++++++++++ .../services/WeightCalculationService.ts | 61 +++++ .../app-client/src/utils/classes/index.ts | 4 +- 19 files changed, 788 insertions(+), 409 deletions(-) delete mode 100644 packages/app-client/src/utils/classes/calculators/OrderSupplierCalculator.ts delete mode 100644 packages/app-client/src/utils/classes/calculators/SupplierWeightCalculator.ts create mode 100644 packages/app-client/src/utils/classes/calculators/modules/OrderSupplierCalculator.ts create mode 100644 packages/app-client/src/utils/classes/calculators/modules/SupplierWeightCalculator.ts create mode 100644 packages/app-client/src/utils/classes/calculators/services/WeightCalculationService.ts diff --git a/packages/app-client/src/components/purchase/module/PurchasePreview.tsx b/packages/app-client/src/components/purchase/module/PurchasePreview.tsx index 18b4efa..bf63d0c 100644 --- a/packages/app-client/src/components/purchase/module/PurchasePreview.tsx +++ b/packages/app-client/src/components/purchase/module/PurchasePreview.tsx @@ -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) { 瓜农装箱明细 {orderSupplierList.map((supplier, index) => { - const calculator = new OrderSupplierCalculator( - purchaseOrder as any, - supplier as any, - ); + const calculator = new PurchaseOrderCalculator( + purchaseOrder, + ).getSupplierCalculator(supplier); return ( diff --git a/packages/app-client/src/pages/main/menu/index.tsx b/packages/app-client/src/pages/main/menu/index.tsx index 9d680bd..67e67db 100644 --- a/packages/app-client/src/pages/main/menu/index.tsx +++ b/packages/app-client/src/pages/main/menu/index.tsx @@ -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) { diff --git a/packages/app-client/src/pages/purchase/approval/audit.tsx b/packages/app-client/src/pages/purchase/approval/audit.tsx index 0643b67..565163a 100644 --- a/packages/app-client/src/pages/purchase/approval/audit.tsx +++ b/packages/app-client/src/pages/purchase/approval/audit.tsx @@ -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, }), }); diff --git a/packages/app-client/src/pages/purchase/audit/audit.tsx b/packages/app-client/src/pages/purchase/audit/audit.tsx index 34fa131..9dd215b 100644 --- a/packages/app-client/src/pages/purchase/audit/audit.tsx +++ b/packages/app-client/src/pages/purchase/audit/audit.tsx @@ -691,8 +691,10 @@ export default hocAuth(function Page(props: CommonComponent) { }} > + {purchaseOrderVO.state === "REJECTED" && + purchaseOrderVO.auditState === "BOSS_REJECTED" && } {/* 顶部导航 */} - + {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`} > - {purchaseOrderVO.state === "WAITING_AUDIT" && - purchaseOrderVO.auditState === "PENDING_QUOTE_APPROVAL" && ( - <> - - - - - - - - )} + {((purchaseOrderVO.state === "WAITING_AUDIT" && + purchaseOrderVO.auditState === "PENDING_QUOTE_APPROVAL") || + (purchaseOrderVO.state === "REJECTED" && + purchaseOrderVO.auditState === "BOSS_REJECTED")) && ( + <> + + + + + + + + )} {purchaseOrderVO.state === "WAITING_AUDIT" && purchaseOrderVO.auditState === "PENDING_BOSS_APPROVAL" && ( <> diff --git a/packages/app-client/src/pages/purchase/enter/create.tsx b/packages/app-client/src/pages/purchase/enter/create.tsx index a2c038e..b2b8f20 100644 --- a/packages/app-client/src/pages/purchase/enter/create.tsx +++ b/packages/app-client/src/pages/purchase/enter/create.tsx @@ -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[] = [ { @@ -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, + ), }; }); }} diff --git a/packages/app-client/src/utils/classes/calculators/OrderSupplierCalculator.ts b/packages/app-client/src/utils/classes/calculators/OrderSupplierCalculator.ts deleted file mode 100644 index 4fca00f..0000000 --- a/packages/app-client/src/utils/classes/calculators/OrderSupplierCalculator.ts +++ /dev/null @@ -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 - ); - } -} diff --git a/packages/app-client/src/utils/classes/calculators/PurchaseOrderCalculator.ts b/packages/app-client/src/utils/classes/calculators/PurchaseOrderCalculator.ts index dcf5134..6de0419 100644 --- a/packages/app-client/src/utils/classes/calculators/PurchaseOrderCalculator.ts +++ b/packages/app-client/src/utils/classes/calculators/PurchaseOrderCalculator.ts @@ -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(); + } } diff --git a/packages/app-client/src/utils/classes/calculators/README.md b/packages/app-client/src/utils/classes/calculators/README.md index c14ba80..ef179de 100644 --- a/packages/app-client/src/utils/classes/calculators/README.md +++ b/packages/app-client/src/utils/classes/calculators/README.md @@ -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(); ``` ## 主要改进 diff --git a/packages/app-client/src/utils/classes/calculators/SupplierWeightCalculator.ts b/packages/app-client/src/utils/classes/calculators/SupplierWeightCalculator.ts deleted file mode 100644 index 650f533..0000000 --- a/packages/app-client/src/utils/classes/calculators/SupplierWeightCalculator.ts +++ /dev/null @@ -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); - } -} diff --git a/packages/app-client/src/utils/classes/calculators/core/BusinessRules.ts b/packages/app-client/src/utils/classes/calculators/core/BusinessRules.ts index 8e4f01c..63a4401 100644 --- a/packages/app-client/src/utils/classes/calculators/core/BusinessRules.ts +++ b/packages/app-client/src/utils/classes/calculators/core/BusinessRules.ts @@ -4,7 +4,7 @@ * 将业务判断逻辑集中管理 */ export class PurchaseOrderRules { - constructor(private order: BusinessAPI.PurchaseOrderVO) {} + constructor(public order: BusinessAPI.PurchaseOrderVO) {} /** * 是否包含运费成本 diff --git a/packages/app-client/src/utils/classes/calculators/index.ts b/packages/app-client/src/utils/classes/calculators/index.ts index 827d0c7..e8fbfc3 100644 --- a/packages/app-client/src/utils/classes/calculators/index.ts +++ b/packages/app-client/src/utils/classes/calculators/index.ts @@ -6,5 +6,4 @@ */ export { PurchaseOrderCalculator } from './PurchaseOrderCalculator' -export { OrderSupplierCalculator } from './OrderSupplierCalculator' -export { SupplierWeightCalculator } from './SupplierWeightCalculator' +export { WeightCalculationService } from './services/WeightCalculationService' diff --git a/packages/app-client/src/utils/classes/calculators/modules/OrderSupplierCalculator.ts b/packages/app-client/src/utils/classes/calculators/modules/OrderSupplierCalculator.ts new file mode 100644 index 0000000..6e60dea --- /dev/null +++ b/packages/app-client/src/utils/classes/calculators/modules/OrderSupplierCalculator.ts @@ -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; + } +} diff --git a/packages/app-client/src/utils/classes/calculators/modules/SalesCalculator.ts b/packages/app-client/src/utils/classes/calculators/modules/SalesCalculator.ts index c371e51..1a4b993 100644 --- a/packages/app-client/src/utils/classes/calculators/modules/SalesCalculator.ts +++ b/packages/app-client/src/utils/classes/calculators/modules/SalesCalculator.ts @@ -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, diff --git a/packages/app-client/src/utils/classes/calculators/modules/SupplierWeightCalculator.ts b/packages/app-client/src/utils/classes/calculators/modules/SupplierWeightCalculator.ts new file mode 100644 index 0000000..1fb9ba3 --- /dev/null +++ b/packages/app-client/src/utils/classes/calculators/modules/SupplierWeightCalculator.ts @@ -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 + }; + } +} diff --git a/packages/app-client/src/utils/classes/calculators/services/WeightCalculationService.ts b/packages/app-client/src/utils/classes/calculators/services/WeightCalculationService.ts new file mode 100644 index 0000000..43ba68b --- /dev/null +++ b/packages/app-client/src/utils/classes/calculators/services/WeightCalculationService.ts @@ -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, + }; + } +} diff --git a/packages/app-client/src/utils/classes/index.ts b/packages/app-client/src/utils/classes/index.ts index 9a5996b..eda3488 100644 --- a/packages/app-client/src/utils/classes/index.ts +++ b/packages/app-client/src/utils/classes/index.ts @@ -6,7 +6,7 @@ */ // 计算器类 -export { PurchaseOrderCalculator, OrderSupplierCalculator, SupplierWeightCalculator } from './calculators' +export { PurchaseOrderCalculator, WeightCalculationService } from './calculators' // 模板类 -export { PdfTemplate } from './templates' \ No newline at end of file +export { PdfTemplate } from './templates'