Compare commits

...

3 Commits

Author SHA1 Message Date
shenyifei
9be4076df8 feat(order): 优化订单处理流程和数据计算逻辑
- 在Step3Success组件中按车次正序排列订单
- 为MadeOption和MarketOption组件添加滚动到顶部功能
- 更新供应商数据结构,添加收款人姓名、银行名称等字段
- 修改TicketUpload组件中的毛重量显示和发票ID初始化
- 优化审批页面中的条件渲染逻辑,避免显示零值
- 更新销售价格计算逻辑,使用总重量替代供应商数量
- 移除废弃的StallWeightCalculator类,整合到SupplierWeightCalculator中
- 修复小数计算精度问题,统一使用两位小数精度
- 添加草帘费成本的开关控制逻辑
2026-01-09 11:18:18 +08:00
shenyifei
e6855dba63 fix(delivery): 修复Excel下载逻辑并优化UI组件样式
- 移除Taro.downloadFile调用,直接使用URL设置临时文件路径
- 调整PDF查看按钮的图标颜色和类型样式
- 调整Excel查看按钮的图标颜色和类型样式
- 修复采购订单暂存跳转路径使用动态配置
- 添加空值检查避免访问undefined属性导致错误
- 移除档口信息中的冗余提示文本
- 更新Excel文件名格式化方式,移除分隔符
- 添加文件路径日志输出便于调试
2026-01-06 13:21:29 +08:00
shenyifei
b46137d0c3 feat(delivery): 添加利润表功能和发货单预览
- 集成 Tabs 组件实现发货单和利润表的标签页切换
- 实现利润表数据计算和展示功能
- 添加 Excel 导出和预览功能
- 增加利润表图片生成和保存功能
- 更新订单接口类型定义支持利润表相关字段
- 添加 ProfitTableTemplate 模板类用于生成利润表 HTML
- 实现按月查询订单数据并计算单车利润逻辑
- 优化发货单预览界面样式和交互体验
- 增加 file-excel 图标支持 Excel 文件操作
- 更新应用版本号至 v0.0.66
2026-01-04 18:26:52 +08:00
29 changed files with 884 additions and 505 deletions

View File

@ -13,7 +13,7 @@ export default {
"process.env.TARO_API_DOMAIN": "process.env.TARO_API_DOMAIN":
process.env.TARO_ENV === "h5" process.env.TARO_ENV === "h5"
? '"/api"' ? '"/api"'
: '"https://api.erp.qilincloud168.com"', : '"http://api.erp.xunhong168.test"',
"process.env.TARO_POSTER_DOMAIN": "process.env.TARO_POSTER_DOMAIN":
process.env.TARO_ENV === "h5" process.env.TARO_ENV === "h5"
? '""' ? '""'

View File

@ -1,17 +1,84 @@
import { useState } from "react"; import { useEffect, useState } from "react";
import { Image, Text, View } from "@tarojs/components"; import { Image, Text, View } from "@tarojs/components";
import { Button } from "@nutui/nutui-react-taro"; import { Button, TabPane, Tabs } from "@nutui/nutui-react-taro";
import Taro from "@tarojs/taro"; import Taro from "@tarojs/taro";
import { Icon } from "@/components"; import { Icon } from "@/components";
import { business, poster } from "@/services";
import {
exportProfitTableExcel,
OrderCalculator,
ProfitTableRow,
ProfitTableTemplate,
} from "@/utils";
import dayjs from "dayjs";
interface Step3SuccessProps { interface Step3SuccessProps {
pdfUrl: string; pdfUrl: string;
picUrl?: string; picUrl?: string;
orderVO: BusinessAPI.OrderVO;
} }
export default function Step3Success(props: Step3SuccessProps) { export default function Step3Success(props: Step3SuccessProps) {
const { pdfUrl, picUrl } = props; const { pdfUrl, picUrl, orderVO } = props!;
const [tempFilePath, setTempFilePath] = useState<string>(); const [tempFilePath, setTempFilePath] = useState<string>();
const [tabValue, setTabValue] = useState<string>("0"); // 默认选中发货单
const [excelTempFilePath, setExcelTempFilePath] = useState<string>();
// 利润表相关状态
const [profitPicUrl, setProfitPicUrl] = useState<string>();
// 获取当前月份
const currentMonth = dayjs(orderVO.orderVehicle.deliveryTime).format(
"YYYY-MM-01",
);
const [profitData, setProfitData] = useState<ProfitTableRow[]>([]);
const init = async (currentMonth: string) => {
// 查询本月的所有订单数据
const {
data: { data: orderVOList = [] },
} = await business.order.listOrder({
orderListQry: {
month: currentMonth,
state: "COMPLETED", // 只查询已完成的订单
},
});
// 获取当前车次
const currentVehicleNo = orderVO.orderVehicle.vehicleNo;
// 筛选截止到当前车次之前的数据
const profitData: ProfitTableRow[] = [];
let includeNext = true;
// orderVOList 根据车次正序
orderVOList.sort((a, b) => {
return (
Number(a.orderVehicle.vehicleNo) - Number(b.orderVehicle?.vehicleNo)
);
});
for (const order of orderVOList) {
// 如果还没有到达当前车次,继续
if (includeNext) {
const profitRow = calculateOrderProfit(order);
profitData.push(profitRow);
// 如果是当前车次,停止添加
if (order.orderVehicle.vehicleNo === currentVehicleNo) {
includeNext = false;
}
}
}
setProfitData(profitData);
};
useEffect(() => {
if (currentMonth) {
init(currentMonth).then();
}
}, [currentMonth]);
// 保存图片 // 保存图片
const handleSaveImage = async () => { const handleSaveImage = async () => {
@ -95,10 +162,180 @@ export default function Step3Success(props: Step3SuccessProps) {
} }
}; };
// 下载Excel
const handleDownloadExcel = async () => {
Taro.showToast({
title: "正在下载Excel文件",
icon: "loading",
duration: 2000,
});
const profitExcelUrl = await exportProfitTableExcel(
profitData,
currentMonth,
);
// 根据环境选择不同的导出方式
const processEnv = process.env.TARO_ENV;
if (processEnv === "h5") {
const link = document.createElement("a");
link.href = profitExcelUrl;
link.download = `诚信志远利润明细_${currentMonth}.xlsx`;
link.style.display = "none";
// 触发下载
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// 清理URL对象
window.URL.revokeObjectURL(profitExcelUrl);
await Taro.showToast({
title: "导出成功",
icon: "success",
});
} else {
setExcelTempFilePath(profitExcelUrl);
Taro.showToast({
title: "Excel下载成功",
icon: "success",
duration: 2000,
});
}
};
// 查看Excel文档
const handleViewExcel = async () => {
if (!excelTempFilePath) {
await handleDownloadExcel();
}
if (excelTempFilePath) {
Taro.openDocument({
filePath: excelTempFilePath,
showMenu: true,
});
}
};
// 保存利润表图片
const handleSaveProfitImage = async () => {
if (!profitPicUrl) {
Taro.showToast({
title: "没有可保存的利润表图片",
icon: "none",
duration: 2000,
});
return;
}
try {
// 下载文件
const downloadRes = await Taro.downloadFile({
url: profitPicUrl!,
});
// 保存图片到相册
await Taro.saveImageToPhotosAlbum({
filePath: downloadRes.tempFilePath,
});
Taro.showToast({
title: "图片已保存到相册",
icon: "success",
duration: 2000,
});
} catch (error) {
console.error("保存图片失败:", error);
Taro.showToast({
title: "保存图片失败,请检查相册权限",
icon: "none",
duration: 2000,
});
}
};
// 计算单车的利润数据
const calculateOrderProfit = (order: BusinessAPI.OrderVO): ProfitTableRow => {
// 车次
const vehicleNo = `${order.orderVehicle.vehicleNo || ""}`;
// 发货日期 - 从 orderShipList 获取
const shippingDate = dayjs(order.orderVehicle.deliveryTime).format(
"YYYY/MM/DD",
);
const orderCalculator = new OrderCalculator(order);
// 计算产地成本
const originCost = orderCalculator
.getCostCalculator()
.calculateMelonPurchaseCost();
// 报价金额
const quoteAmount = orderCalculator.getSalesAmount();
// 计算利润
const profit = orderCalculator.getPersonalProfit();
return {
vehicleNo,
shippingDate,
originCost,
quoteAmount,
profit,
};
};
// 生成利润表
const generateProfitTable = async () => {
try {
if (!profitData || profitData.length === 0) {
Taro.showToast({
title: "本月暂无订单数据",
icon: "none",
});
return;
}
// 生成预览图片
const template = new ProfitTableTemplate(profitData, currentMonth);
const {
data: { data: picData },
} = await poster.poster.postApiV1Poster({
html: template.generateHtmlString(),
//@ts-ignore
format: "a4",
});
setProfitPicUrl(picData?.path);
} catch (error) {
console.error("生成利润表失败:", error);
Taro.showToast({
title: "生成利润表失败",
icon: "none",
});
}
};
return ( return (
<View className="flex flex-1 flex-col overflow-hidden rounded-md bg-gray-100 shadow-md"> <View className="flex flex-1 flex-col overflow-hidden rounded-md bg-gray-100 shadow-md">
{/* Tab 切换 */}
<View className="bg-white px-2.5 pt-2.5">
<Tabs
style={{
// @ts-ignore
"--nutui-tabs-tabpane-padding": 0,
}}
value={tabValue}
onChange={async (value) => {
if (value === 1 && !profitPicUrl) {
await generateProfitTable();
}
setTabValue(value.toString());
}}
>
<TabPane title="发货单" value={0}>
{/* 预览区域 */} {/* 预览区域 */}
<View className="flex-1 p-2.5"> <View className="p-2.5">
<View className="rounded-lg bg-white shadow-sm"> <View className="rounded-lg bg-white shadow-sm">
{picUrl ? ( {picUrl ? (
<View <View
@ -159,14 +396,15 @@ export default function Step3Success(props: Step3SuccessProps) {
{tempFilePath && ( {tempFilePath && (
<View className="flex-1"> <View className="flex-1">
<Button <Button
icon={<Icon name="eye" size={16} />} icon={<Icon name="eye" size={16} color={"orange"} />}
type="primary" type="default"
color="orange"
fill={"outline"} fill={"outline"}
size={"large"} size={"large"}
block block
onClick={handleViewPDF} onClick={handleViewPDF}
> >
<Text className="font-medium">PDF</Text> <Text className="text-orange font-medium">PDF</Text>
</Button> </Button>
</View> </View>
)} )}
@ -180,6 +418,100 @@ export default function Step3Success(props: Step3SuccessProps) {
</View> </View>
</View> </View>
</View> </View>
</TabPane>
{orderVO.orderDealer.shareAdjusted && (
<TabPane title="利润表" value={1}>
{/* 预览区域 */}
<View className="p-2.5">
<View className="rounded-lg bg-white shadow-sm">
{profitPicUrl ? (
<View
className="overflow-hidden rounded-lg border border-gray-200"
style={{ maxHeight: Taro.pxTransform(400) }}
>
<Image
src={profitPicUrl}
mode="widthFix"
className="w-full"
onClick={() => {
Taro.previewImage({
urls: [profitPicUrl],
current: profitPicUrl,
});
}}
/>
</View>
) : (
<View className="flex h-48 items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50">
<Text className="text-gray-500"></Text>
</View>
)}
</View>
</View>
{/* 底部按钮区域 */}
<View className="border-t border-gray-100 bg-white p-2.5">
<View className="flex flex-col gap-2.5">
{/* 保存利润表图片按钮 */}
{profitPicUrl && (
<Button
icon={<Icon name="download" size={16} color={"white"} />}
type="primary"
size={"large"}
block
onClick={handleSaveProfitImage}
className="flex items-center justify-center"
>
<Text className="font-medium"></Text>
</Button>
)}
{/* Excel操作按钮 */}
<View className="flex gap-3">
<View className="flex-1">
<Button
icon={
<Icon name="file-excel" size={16} color={"white"} />
}
type="primary"
size={"large"}
color="orange"
block
onClick={handleDownloadExcel}
>
<Text className="font-medium">Excel</Text>
</Button>
</View>
{excelTempFilePath && (
<View className="flex-1">
<Button
icon={<Icon name="eye" size={16} color={"orange"} />}
type="default"
color="orange"
fill={"outline"}
size={"large"}
block
onClick={handleViewExcel}
>
<Text className="font-medium">Excel</Text>
</Button>
</View>
)}
</View>
{/* 提示信息 */}
<View className="text-center">
<Text className="text-xs text-gray-500">
</Text>
</View>
</View>
</View>
</TabPane>
)}
</Tabs>
</View>
</View> </View>
); );
} }

View File

@ -3,6 +3,7 @@ import classNames from "classnames";
import React from "react"; import React from "react";
export type IconNames = export type IconNames =
| "file-excel"
| "copy" | "copy"
| "wallet" | "wallet"
| "download" | "download"

View File

@ -10,6 +10,7 @@ import { business } from "@/services";
// 定义ref暴露的方法接口 // 定义ref暴露的方法接口
export interface MadeOptionRef { export interface MadeOptionRef {
onAdd: () => void; onAdd: () => void;
scrollToTop: () => void;
} }
interface IMadeOptionProps { interface IMadeOptionProps {
@ -45,7 +46,16 @@ export default forwardRef<MadeOptionRef, IMadeOptionProps>(function MadeOption(
const active = Number(value.active); const active = Number(value.active);
// 滚动到页面顶部
const scrollToTop = () => {
Taro.pageScrollTo({
scrollTop: 0,
duration: 100,
});
};
const setActive = (active: number) => { const setActive = (active: number) => {
scrollToTop();
onChange({ onChange({
...value, ...value,
active, active,
@ -251,6 +261,7 @@ export default forwardRef<MadeOptionRef, IMadeOptionProps>(function MadeOption(
return; return;
} }
scrollToTop();
onChange({ onChange({
...value, ...value,
active: 2, active: 2,
@ -263,10 +274,14 @@ export default forwardRef<MadeOptionRef, IMadeOptionProps>(function MadeOption(
orderSupplierId: generateShortId(), orderSupplierId: generateShortId(),
supplierId: "", supplierId: "",
name: "瓜农" + (orderSupplierList.length + 1), name: "瓜农" + (orderSupplierList.length + 1),
payeeName: "",
idCard: "", idCard: "",
bankName: "",
bankCard: "", bankCard: "",
phone: "", phone: "",
selected: true, selected: true,
loadingMode: orderSupplierList[selectedIndex].loadingMode,
pricingMethod: orderSupplierList[selectedIndex].pricingMethod,
isPaper: orderSupplierList[selectedIndex].isPaper, isPaper: orderSupplierList[selectedIndex].isPaper,
orderPackageList: [], orderPackageList: [],
productId: orderSupplierList[selectedIndex]?.productId, productId: orderSupplierList[selectedIndex]?.productId,
@ -280,6 +295,7 @@ export default forwardRef<MadeOptionRef, IMadeOptionProps>(function MadeOption(
// 将校验方法暴露给父组件 // 将校验方法暴露给父组件
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
onAdd, onAdd,
scrollToTop,
})); }));
async function saveDraft() { async function saveDraft() {
@ -316,7 +332,7 @@ export default forwardRef<MadeOptionRef, IMadeOptionProps>(function MadeOption(
content: "当前采购订单已暂存成功", content: "当前采购订单已暂存成功",
}); });
Taro.redirectTo({ Taro.redirectTo({
url: "/pages/purchase/made/drafts", url: purchase.path["PRODUCTION_PURCHASE"].drafts,
}); });
} }
}, },

View File

@ -10,6 +10,7 @@ import purchase from "@/constant/purchase";
// 定义ref暴露的方法接口 // 定义ref暴露的方法接口
export interface MarketOptionRef { export interface MarketOptionRef {
onAdd: () => void; onAdd: () => void;
scrollToTop: () => void;
} }
interface IMarketOptionProps { interface IMarketOptionProps {
@ -39,7 +40,15 @@ export default forwardRef<MarketOptionRef, IMarketOptionProps>(
const active = Number(value.active); const active = Number(value.active);
// 滚动到页面顶部
const scrollToTop = () => {
Taro.pageScrollTo({
scrollTop: 0,
duration: 100,
});
};
const setActive = (active: number) => { const setActive = (active: number) => {
scrollToTop();
onChange({ onChange({
...value, ...value,
active, active,
@ -192,6 +201,7 @@ export default forwardRef<MarketOptionRef, IMarketOptionProps>(
return; return;
} }
scrollToTop();
onChange({ onChange({
...value, ...value,
active: 2, active: 2,
@ -204,11 +214,14 @@ export default forwardRef<MarketOptionRef, IMarketOptionProps>(
orderSupplierId: generateShortId(), orderSupplierId: generateShortId(),
supplierId: "", supplierId: "",
name: "档口" + (orderSupplierList.length + 1), name: "档口" + (orderSupplierList.length + 1),
payeeName: "",
idCard: "", idCard: "",
bankName: "",
bankCard: "", bankCard: "",
phone: "", phone: "",
selected: true, selected: true,
isPaper: orderSupplierList[selectedIndex].isPaper, loadingMode: orderSupplierList[selectedIndex].loadingMode,
pricingMethod: orderSupplierList[selectedIndex].pricingMethod,
orderPackageList: [], orderPackageList: [],
productId: orderSupplierList[selectedIndex]?.productId, productId: orderSupplierList[selectedIndex]?.productId,
productName: orderSupplierList[selectedIndex]?.productName, productName: orderSupplierList[selectedIndex]?.productName,
@ -221,6 +234,7 @@ export default forwardRef<MarketOptionRef, IMarketOptionProps>(
// 将校验方法暴露给父组件 // 将校验方法暴露给父组件
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
onAdd, onAdd,
scrollToTop,
})); }));
async function saveDraft() { async function saveDraft() {
@ -252,7 +266,7 @@ export default forwardRef<MarketOptionRef, IMarketOptionProps>(
content: "当前采购订单已暂存成功", content: "当前采购订单已暂存成功",
}); });
Taro.redirectTo({ Taro.redirectTo({
url: "/pages/purchase/made/drafts", url: purchase.path["MARKET_PURCHASE"].drafts,
}); });
} }
}, },

View File

@ -485,10 +485,8 @@ export default forwardRef<OrderVehicleRef, IOrderVehicleProps>(
plate: newVehicle.plate?.split("-")[0]!, plate: newVehicle.plate?.split("-")[0]!,
dealerId: newVehicle.dealerId || orderVehicle?.dealerId, dealerId: newVehicle.dealerId || orderVehicle?.dealerId,
dealerName: newVehicle.dealerName || orderVehicle?.dealerName, dealerName: newVehicle.dealerName || orderVehicle?.dealerName,
openStrawCurtain: false,
//@ts-ignore
strawCurtainPrice: "",
deliveryTime: dayjs().format("YYYY-MM-DD"), deliveryTime: dayjs().format("YYYY-MM-DD"),
//@ts-ignore
originalData: originalData || "", originalData: originalData || "",
}, },
orderDealer: { orderDealer: {

View File

@ -190,7 +190,7 @@ export default forwardRef<StallInfoRef, IStallInfoProps>(
return false; return false;
} }
// 银行名称至少2个字符 // 银行名称至少2个字符
return bankName.length >= 2; return bankName?.length >= 2;
}; };
// 校验手机号函数 // 校验手机号函数
@ -955,7 +955,7 @@ export default forwardRef<StallInfoRef, IStallInfoProps>(
clearable clearable
type="text" type="text"
placeholder="请输入档口名称" placeholder="请输入档口名称"
value={supplierVO.name} value={supplierVO?.name || ""}
onChange={(value) => handleNameChange(value, supplierVO)} onChange={(value) => handleNameChange(value, supplierVO)}
onBlur={() => onBlur={() =>
handleNameBlur( handleNameBlur(
@ -982,7 +982,7 @@ export default forwardRef<StallInfoRef, IStallInfoProps>(
clearable clearable
type="text" type="text"
placeholder="请输入收款人姓名" placeholder="请输入收款人姓名"
value={supplierVO.payeeName} value={supplierVO?.payeeName || ""}
onChange={(value) => onChange={(value) =>
handlePayeeNameChange(value, supplierVO) handlePayeeNameChange(value, supplierVO)
} }
@ -1016,7 +1016,7 @@ export default forwardRef<StallInfoRef, IStallInfoProps>(
clearable clearable
type="text" type="text"
placeholder="请输入银行名称" placeholder="请输入银行名称"
value={supplierVO.bankName} value={supplierVO?.bankName || ""}
onChange={(value) => onChange={(value) =>
handleBankNameChange(value, supplierVO) handleBankNameChange(value, supplierVO)
} }
@ -1050,7 +1050,7 @@ export default forwardRef<StallInfoRef, IStallInfoProps>(
clearable clearable
type="digit" type="digit"
placeholder="请输入银行卡号" placeholder="请输入银行卡号"
value={supplierVO.bankCard} value={supplierVO?.bankCard || ""}
onChange={(value) => onChange={(value) =>
handleBankCardChange(value, supplierVO) handleBankCardChange(value, supplierVO)
} }
@ -1079,7 +1079,7 @@ export default forwardRef<StallInfoRef, IStallInfoProps>(
clearable clearable
type="tel" type="tel"
placeholder="请输入手机号码" placeholder="请输入手机号码"
value={supplierVO.phone} value={supplierVO?.phone || ""}
onChange={(value) => handlePhoneChange(value, supplierVO)} onChange={(value) => handlePhoneChange(value, supplierVO)}
onBlur={() => onBlur={() =>
handlePhoneBlur( handlePhoneBlur(
@ -1121,9 +1121,6 @@ export default forwardRef<StallInfoRef, IStallInfoProps>(
</View> </View>
<View className="text-sm text-green-600"></View> <View className="text-sm text-green-600"></View>
<View className="text-neutral-darker mt-1 text-xs">
</View>
</View> </View>
</View> </View>
<View className="flex flex-row gap-2.5"> <View className="flex flex-row gap-2.5">
@ -1176,9 +1173,6 @@ export default forwardRef<StallInfoRef, IStallInfoProps>(
<View className="text-neutral-darker mt-1 text-xs"> <View className="text-neutral-darker mt-1 text-xs">
</View> </View>
<View className="mt-2 text-center text-xs text-gray-400">
</View>
</View> </View>
)} )}
</View> </View>

View File

@ -869,7 +869,7 @@ export default forwardRef<SupplierInfoRef, ISupplierInfoProps>(
clearable clearable
type="text" type="text"
placeholder="请输入姓名" placeholder="请输入姓名"
value={supplierVO.name} value={supplierVO?.name || ""}
onChange={(value) => handleNameChange(value, supplierVO)} onChange={(value) => handleNameChange(value, supplierVO)}
onBlur={() => onBlur={() =>
handleNameBlur( handleNameBlur(
@ -896,7 +896,7 @@ export default forwardRef<SupplierInfoRef, ISupplierInfoProps>(
clearable clearable
type="idcard" type="idcard"
placeholder="请输入身份证号" placeholder="请输入身份证号"
value={supplierVO.idCard || ""} value={supplierVO?.idCard || ""}
onChange={(value) => handleIdCardChange(value, supplierVO)} onChange={(value) => handleIdCardChange(value, supplierVO)}
onBlur={() => onBlur={() =>
handleIdCardBlur( handleIdCardBlur(
@ -921,7 +921,7 @@ export default forwardRef<SupplierInfoRef, ISupplierInfoProps>(
clearable clearable
type="text" type="text"
placeholder="请输入银行名称" placeholder="请输入银行名称"
value={supplierVO.bankName || ""} value={supplierVO?.bankName || ""}
onChange={(value) => handleBankNameChange(value, supplierVO)} onChange={(value) => handleBankNameChange(value, supplierVO)}
onBlur={() => onBlur={() =>
handleBankNameBlur( handleBankNameBlur(
@ -953,7 +953,7 @@ export default forwardRef<SupplierInfoRef, ISupplierInfoProps>(
clearable clearable
type="digit" type="digit"
placeholder="请输入银行卡号" placeholder="请输入银行卡号"
value={supplierVO.bankCard || ""} value={supplierVO?.bankCard || ""}
onChange={(value) => handleBankCardChange(value, supplierVO)} onChange={(value) => handleBankCardChange(value, supplierVO)}
onBlur={() => onBlur={() =>
handleBankCardBlur( handleBankCardBlur(
@ -978,7 +978,7 @@ export default forwardRef<SupplierInfoRef, ISupplierInfoProps>(
clearable clearable
type="tel" type="tel"
placeholder="请输入手机号码" placeholder="请输入手机号码"
value={supplierVO.phone} value={supplierVO?.phone || ""}
onChange={(value) => handlePhoneChange(value, supplierVO)} onChange={(value) => handlePhoneChange(value, supplierVO)}
onBlur={() => onBlur={() =>
handlePhoneBlur( handlePhoneBlur(

View File

@ -95,6 +95,14 @@ export default function TicketUpload(props: ITicketUploadProps) {
{supplierVO.netWeight || 0} {supplierVO.netWeight || 0}
</View> </View>
</View> </View>
<View className="flex justify-between text-sm">
<View className="text-neutral-darker text-sm">
</View>
<View className="font-medium text-gray-800">
{supplierVO.grossWeight || 0}
</View>
</View>
<View className="flex justify-between text-sm"> <View className="flex justify-between text-sm">
<View className="text-neutral-darker text-sm"> <View className="text-neutral-darker text-sm">
/ /
@ -167,7 +175,7 @@ export default function TicketUpload(props: ITicketUploadProps) {
...supplierVO!, ...supplierVO!,
invoiceImg: [], invoiceImg: [],
invoiceUpload: false, invoiceUpload: false,
invoiceId: 0, invoiceId: undefined,
}); });
}} }}
> >
@ -185,7 +193,7 @@ export default function TicketUpload(props: ITicketUploadProps) {
...supplierVO!, ...supplierVO!,
invoiceImg: [], invoiceImg: [],
invoiceUpload: false, invoiceUpload: false,
invoiceId: 0, invoiceId: undefined,
}); });
Toast.show("toast", { Toast.show("toast", {
title: "删除成功", title: "删除成功",

View File

@ -27,8 +27,8 @@ export default function OrderSupplierPicker(props: IOrderSupplierPickerProps) {
const [orderSupplierList, setOrderSupplierList] = useState< const [orderSupplierList, setOrderSupplierList] = useState<
BusinessAPI.OrderSupplierVO[] BusinessAPI.OrderSupplierVO[]
>([]); >([]);
// 发票开具人(有开发票资质的瓜农 // 瓜农
const [invoiceIssuer, setInvoiceIssuer] = useState<BusinessAPI.SupplierVO>(); const [supplierVO, setSupplierVO] = useState<BusinessAPI.SupplierVO>();
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set()); const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
useEffect(() => { useEffect(() => {
@ -112,7 +112,7 @@ export default function OrderSupplierPicker(props: IOrderSupplierPickerProps) {
<SupplierPicker <SupplierPicker
type={"FARMER"} type={"FARMER"}
onFinish={(supplierVO) => { onFinish={(supplierVO) => {
setInvoiceIssuer(supplierVO); setSupplierVO(supplierVO);
actionRef.current?.reload(); actionRef.current?.reload();
}} }}
trigger={ trigger={
@ -120,14 +120,14 @@ export default function OrderSupplierPicker(props: IOrderSupplierPickerProps) {
className={`border-primary flex h-6 items-center rounded-md border-2 px-2.5`} className={`border-primary flex h-6 items-center rounded-md border-2 px-2.5`}
> >
<View className={"text-primary text-xs"}> <View className={"text-primary text-xs"}>
{invoiceIssuer?.name || "发票开具人"} {supplierVO?.name || "瓜农"}
</View> </View>
{invoiceIssuer?.name ? ( {supplierVO?.name ? (
<Icon <Icon
name={"circle-xmark"} name={"circle-xmark"}
size={16} size={16}
onClick={(event) => { onClick={(event) => {
setInvoiceIssuer(undefined); setSupplierVO(undefined);
actionRef.current?.reload(); actionRef.current?.reload();
event.stopPropagation(); event.stopPropagation();
}} }}
@ -390,6 +390,7 @@ export default function OrderSupplierPicker(props: IOrderSupplierPickerProps) {
invoiceUpload: false, invoiceUpload: false,
poStates: ["AUDITING", "COMPLETED"], poStates: ["AUDITING", "COMPLETED"],
poType: "PRODUCTION_PURCHASE", poType: "PRODUCTION_PURCHASE",
supplierId: supplierVO?.supplierId,
}, },
}); });

View File

@ -1,2 +1,2 @@
// App 相关常量 // App 相关常量
export const APP_VERSION = "v0.0.65"; export const APP_VERSION = "v0.0.69";

View File

@ -57,11 +57,13 @@ const path = {
create: "/pages/purchase/made/create", create: "/pages/purchase/made/create",
preview: "/pages/purchase/made/preview", preview: "/pages/purchase/made/preview",
result: "/pages/purchase/made/result", result: "/pages/purchase/made/result",
drafts: "/pages/purchase/made/drafts",
}, },
MARKET_PURCHASE: { MARKET_PURCHASE: {
create: "/pages/purchase/market/create", create: "/pages/purchase/market/create",
preview: "/pages/purchase/market/preview", preview: "/pages/purchase/market/preview",
result: "/pages/purchase/market/result", result: "/pages/purchase/market/result",
drafts: "/pages/purchase/market/drafts",
}, },
}; };

View File

@ -1,11 +1,11 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 5042354 */ font-family: "iconfont"; /* Project id 5042354 */
src: src:
url("//at.alicdn.com/t/c/font_5042354_c95qf1bojkl.woff2?t=1766645005700") url("//at.alicdn.com/t/c/font_5042354_y6uqn91vagr.woff2?t=1767522385418")
format("woff2"), format("woff2"),
url("//at.alicdn.com/t/c/font_5042354_c95qf1bojkl.woff?t=1766645005700") url("//at.alicdn.com/t/c/font_5042354_y6uqn91vagr.woff?t=1767522385418")
format("woff"), format("woff"),
url("//at.alicdn.com/t/c/font_5042354_c95qf1bojkl.ttf?t=1766645005700") url("//at.alicdn.com/t/c/font_5042354_y6uqn91vagr.ttf?t=1767522385418")
format("truetype"); format("truetype");
} }
@ -17,6 +17,10 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-file-excel:before {
content: "\e639";
}
.icon-wallet:before { .icon-wallet:before {
content: "\e638"; content: "\e638";
} }

View File

@ -251,7 +251,8 @@ export default hocAuth(function Page(props: CommonComponent) {
); );
})} })}
{orderVO.orderDealer?.enableLoss && ( {orderVO.orderDealer?.enableLoss &&
Number(orderVO.orderDealer?.lossAmount) > 0 && (
<View className="cost-item flex flex-col px-3 py-2"> <View className="cost-item flex flex-col px-3 py-2">
<View className="text-sm text-gray-500"></View> <View className="text-sm text-gray-500"></View>
<View className="font-medium"> <View className="font-medium">
@ -260,8 +261,7 @@ export default hocAuth(function Page(props: CommonComponent) {
</View> </View>
)} )}
{orderVO.orderDealer?.taxProvision && {Number(orderVO.orderDealer?.taxProvision || 0) > 0 && (
orderVO.orderDealer?.taxProvision > 0 && (
<View className="cost-item flex flex-col px-3 py-2"> <View className="cost-item flex flex-col px-3 py-2">
<View className="text-sm text-gray-500"></View> <View className="text-sm text-gray-500"></View>
<View className="font-medium"> <View className="font-medium">
@ -270,8 +270,7 @@ export default hocAuth(function Page(props: CommonComponent) {
</View> </View>
)} )}
{orderVO.orderDealer?.taxSubsidy && {Number(orderVO.orderDealer?.taxSubsidy || 0) > 0 && (
orderVO.orderDealer?.taxSubsidy > 0 && (
<View className="cost-item flex flex-col px-3 py-2"> <View className="cost-item flex flex-col px-3 py-2">
<View className="text-sm text-gray-500"></View> <View className="text-sm text-gray-500"></View>
<View className="font-medium"> <View className="font-medium">
@ -280,7 +279,8 @@ export default hocAuth(function Page(props: CommonComponent) {
</View> </View>
)} )}
{orderVO.orderRebate?.amount && orderVO.orderRebate?.amount > 0 && ( {orderVO.orderRebate &&
Number(orderVO.orderRebate.amount || 0) > 0 && (
<View className="cost-item flex flex-col px-3 py-2"> <View className="cost-item flex flex-col px-3 py-2">
<View className="text-sm text-gray-500"></View> <View className="text-sm text-gray-500"></View>
<View className="font-medium"> <View className="font-medium">
@ -289,8 +289,7 @@ export default hocAuth(function Page(props: CommonComponent) {
</View> </View>
)} )}
{orderVO.orderDealer?.costDifference && {Number(orderVO.orderDealer?.costDifference || 0) > 0 && (
orderVO.orderDealer?.costDifference > 0 && (
<View className="cost-item flex flex-col px-3 py-2"> <View className="cost-item flex flex-col px-3 py-2">
<View className="text-sm text-gray-500"></View> <View className="text-sm text-gray-500"></View>
<View className="font-medium"> <View className="font-medium">

View File

@ -719,6 +719,7 @@ export default hocAuth(function Page(props: CommonComponent) {
}) })
.filter(Boolean); .filter(Boolean);
console.log("orderVO", orderVO);
return ( return (
<> <>
<View <View
@ -861,14 +862,14 @@ export default hocAuth(function Page(props: CommonComponent) {
"text-red-500": personalProfit < 0, "text-red-500": personalProfit < 0,
})} })}
> >
{personalProfit || "-"} {personalProfit || "0"}
</View> </View>
</View> </View>
</View> </View>
<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">
{auditVO?.type === "REVIEWER_AUDIT" && {orderVO.state === "AUDITING" &&
(auditVO.state === "WAITING_AUDIT" || (orderVO.auditState === "PENDING_QUOTE_APPROVAL" ||
auditVO.state === "AUDIT_REJECTED") ? ( orderVO.auditState === "BOSS_REJECTED") ? (
<> <>
<View className={"flex-1"}> <View className={"flex-1"}>
<Button <Button
@ -881,6 +882,10 @@ export default hocAuth(function Page(props: CommonComponent) {
</Button> </Button>
</View> </View>
{auditVO &&
auditVO?.type === "REVIEWER_AUDIT" &&
(auditVO.state === "WAITING_AUDIT" ||
auditVO.state === "AUDIT_REJECTED") && (
<View className={"flex-1"}> <View className={"flex-1"}>
<OrderRejectApprove <OrderRejectApprove
size={"large"} size={"large"}
@ -892,6 +897,7 @@ export default hocAuth(function Page(props: CommonComponent) {
}} }}
/> />
</View> </View>
)}
<View className={"flex-1"}> <View className={"flex-1"}>
<Button <Button
block block

View File

@ -364,7 +364,11 @@ export default hocAuth(function Page(props: CommonComponent) {
{step === 2 && <DeliveryStep2Preview moduleList={moduleList} />} {step === 2 && <DeliveryStep2Preview moduleList={moduleList} />}
{step === 3 && pdfUrl && ( {step === 3 && pdfUrl && (
<DeliveryStep3Success pdfUrl={pdfUrl} picUrl={picUrl} /> <DeliveryStep3Success
pdfUrl={pdfUrl}
picUrl={picUrl}
orderVO={orderVO}
/>
)} )}
</View> </View>
</View> </View>

View File

@ -275,6 +275,7 @@ export default hocAuth(function Page(props: CommonComponent) {
value={order as any} value={order as any}
onChange={(orderVO: BusinessAPI.OrderVO) => { onChange={(orderVO: BusinessAPI.OrderVO) => {
setOrder(orderVO); setOrder(orderVO);
orderOptionRef.current?.scrollToTop();
}} }}
melonFarmerRefs={supplierInfoRefs} melonFarmerRefs={supplierInfoRefs}
weighRefs={supplierWeighRefs} weighRefs={supplierWeighRefs}

View File

@ -605,9 +605,6 @@ export default hocAuth(function Page(props: CommonComponent) {
</View> </View>
<View className="text-sm text-green-600"></View> <View className="text-sm text-green-600"></View>
<View className="text-neutral-darker mt-1 text-xs">
</View>
</View> </View>
</View> </View>
<View className="flex flex-row gap-2.5"> <View className="flex flex-row gap-2.5">
@ -660,9 +657,6 @@ export default hocAuth(function Page(props: CommonComponent) {
<View className="text-neutral-darker mt-1 text-xs"> <View className="text-neutral-darker mt-1 text-xs">
</View> </View>
<View className="mt-2 text-center text-xs text-gray-400">
</View>
</View> </View>
)} )}
</View> </View>

View File

@ -3219,6 +3219,8 @@ declare namespace BusinessAPI {
| "BOSS_REJECTED"; | "BOSS_REJECTED";
/** 采购类型1_产地采购2_市场采购 */ /** 采购类型1_产地采购2_市场采购 */
type?: "PRODUCTION_PURCHASE" | "MARKET_PURCHASE"; type?: "PRODUCTION_PURCHASE" | "MARKET_PURCHASE";
/** 月份 */
month?: string;
}; };
type OrderPackage = { type OrderPackage = {
@ -3316,7 +3318,7 @@ declare namespace BusinessAPI {
/** 返点单价 */ /** 返点单价 */
unitPrice?: number; unitPrice?: number;
/** 返点金额 */ /** 返点金额 */
amount?: number; amount: number;
/** 是否已付款 */ /** 是否已付款 */
isPaid?: boolean; isPaid?: boolean;
}; };
@ -3447,6 +3449,10 @@ declare namespace BusinessAPI {
pdfUrl?: string; pdfUrl?: string;
/** 图片文件地址 */ /** 图片文件地址 */
picUrl?: string; picUrl?: string;
/** 利润表图片文件地址 */
profitPicUrl?: string;
/** 利润表文件地址 */
profitExcelUrl?: string;
}; };
type OrderShipCreateCmd = { type OrderShipCreateCmd = {
@ -3645,6 +3651,10 @@ declare namespace BusinessAPI {
pdfUrl?: string; pdfUrl?: string;
/** 图片文件地址 */ /** 图片文件地址 */
picUrl?: string; picUrl?: string;
/** 利润表图片文件地址 */
profitPicUrl?: string;
/** 利润表文件地址 */
profitExcelUrl?: string;
/** 发货单状态0_草稿1_待发货2_待回款3_待改签4_部分回款5_已回款6_拒收完结7_已完结 */ /** 发货单状态0_草稿1_待发货2_待回款3_待改签4_部分回款5_已回款6_拒收完结7_已完结 */
state: state:
| "DRAFT" | "DRAFT"

View File

@ -16,6 +16,10 @@ export class OrderRules {
* *
*/ */
shouldIncludeStrawCurtainCost(): boolean { shouldIncludeStrawCurtainCost(): boolean {
if (!this.order.orderDealer?.strawMatCostFlag) {
return false;
}
return !!( return !!(
this.order.orderVehicle?.openStrawCurtain && this.order.orderVehicle?.openStrawCurtain &&
this.order.orderVehicle?.strawCurtainPrice this.order.orderVehicle?.strawCurtainPrice

View File

@ -20,7 +20,7 @@ export class DecimalUtils {
* Decimal * Decimal
*/ */
static create(value: number | string | Decimal = 0): Decimal { static create(value: number | string | Decimal = 0): Decimal {
return new Decimal(value); return new Decimal(value || 0);
} }
/** /**
@ -69,7 +69,7 @@ export class DecimalUtils {
if (!b || this.create(b).isZero()) { if (!b || this.create(b).isZero()) {
return defaultValue; return defaultValue;
} }
return this.create(a).div(b).toNumber(); return this.create(a).div(b).toDecimalPlaces(2).toNumber();
} }
/** /**

View File

@ -74,20 +74,8 @@ export class SalesCalculator {
* *
*/ */
calculateAverageSalesPrice(): number { calculateAverageSalesPrice(): number {
if (!this.order.orderSupplierList.length) { const totalSalesPrice = this.calculateSalesAmount();
return 0; return DecimalUtils.divide(totalSalesPrice, this.calculateTotalWeight(), 0);
}
const totalSalesPrice = this.order.orderSupplierList.reduce(
(total, supplier) => DecimalUtils.add(total, supplier.salePrice || 0),
0,
);
return DecimalUtils.divide(
totalSalesPrice,
this.order.orderSupplierList.length,
0,
);
} }
/** /**

View File

@ -1,285 +0,0 @@
import { DecimalUtils } from "../core/DecimalUtils";
/**
*
*
*/
export class StallWeightCalculator {
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 calculateQuoteWeight(supplier: BusinessAPI.OrderSupplier): number {
return supplier.pricingMethod === "BY_GROSS_WEIGHT"
? supplier.grossWeight
: supplier.netWeight;
}
/**
*
*/
private calculateInvoiceAmount(supplier: BusinessAPI.OrderSupplier): void {
supplier.invoiceAmount = DecimalUtils.multiply(
this.calculateQuoteWeight(supplier),
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

@ -35,7 +35,10 @@ export class SupplierWeightCalculator {
/** /**
* *
*/ */
private calculateSupplierWeight(supplier: BusinessAPI.OrderSupplier, index: number): void { private calculateSupplierWeight(
supplier: BusinessAPI.OrderSupplier,
index: number,
): void {
const isFirstSupplier = index === 0; const isFirstSupplier = index === 0;
const isLastSupplier = supplier.isLast; const isLastSupplier = supplier.isLast;
@ -43,14 +46,21 @@ export class SupplierWeightCalculator {
this.setEmptyWeight(supplier, index); this.setEmptyWeight(supplier, index);
// 计算各类纸箱重量 // 计算各类纸箱重量
const weights = this.calculateAllBoxWeights(supplier.orderPackageList || []); const weights = this.calculateAllBoxWeights(
supplier.orderPackageList || [],
);
if (!supplier.isPaper) { if (!supplier.isPaper) {
// 不带纸箱包装的简化计算 // 不带纸箱包装的简化计算
this.calculateNonPaperSupplier(supplier, weights.used); this.calculateNonPaperSupplier(supplier, weights.used);
} else { } else {
// 纸箱包装的复杂计算 // 纸箱包装的复杂计算
this.calculatePaperSupplier(supplier, weights, isFirstSupplier, isLastSupplier); this.calculatePaperSupplier(
supplier,
weights,
isFirstSupplier,
isLastSupplier,
);
} }
// 计算发票金额 // 计算发票金额
@ -63,7 +73,10 @@ export class SupplierWeightCalculator {
/** /**
* *
*/ */
private setEmptyWeight(supplier: BusinessAPI.OrderSupplier, index: number): void { private setEmptyWeight(
supplier: BusinessAPI.OrderSupplier,
index: number,
): void {
if (index === 0) { if (index === 0) {
// 第一个农户保持原有空磅重量 // 第一个农户保持原有空磅重量
} else { } else {
@ -87,19 +100,25 @@ export class SupplierWeightCalculator {
/** /**
* *
*/ */
private calculateNonPaperSupplier(supplier: BusinessAPI.OrderSupplier, usedBoxesWeight: number): void { private calculateNonPaperSupplier(
if (supplier.type === 'FARMER') { supplier: BusinessAPI.OrderSupplier,
usedBoxesWeight: number,
): void {
if (supplier.type === "FARMER") {
// 毛重 = (总磅 - 空磅) × 2 // 毛重 = (总磅 - 空磅) × 2
supplier.grossWeight = DecimalUtils.multiply( supplier.grossWeight = DecimalUtils.multiply(
DecimalUtils.subtract(supplier.totalWeight || 0, supplier.emptyWeight || 0), DecimalUtils.subtract(
2 supplier.totalWeight || 0,
supplier.emptyWeight || 0,
),
2,
); );
} }
// 净重 = 毛重 - 使用纸箱重量 // 净重 = 毛重 - 使用纸箱重量
supplier.netWeight = DecimalUtils.subtract( supplier.netWeight = DecimalUtils.subtract(
supplier.grossWeight || 0, supplier.grossWeight || 0,
usedBoxesWeight usedBoxesWeight,
); );
} }
@ -110,11 +129,17 @@ export class SupplierWeightCalculator {
supplier: BusinessAPI.OrderSupplier, supplier: BusinessAPI.OrderSupplier,
weights: { used: number; extra: number; remain: number; extraUsed: number }, weights: { used: number; extra: number; remain: number; extraUsed: number },
isFirstSupplier: boolean, isFirstSupplier: boolean,
isLastSupplier: boolean isLastSupplier: boolean,
): void { ): void {
const weightDiff = isFirstSupplier const weightDiff = isFirstSupplier
? DecimalUtils.subtract(supplier.totalWeight || 0, this.initialEmptyWeight) ? DecimalUtils.subtract(
: DecimalUtils.subtract(supplier.totalWeight || 0, this.previousTotalWeight); supplier.totalWeight || 0,
this.initialEmptyWeight,
)
: DecimalUtils.subtract(
supplier.totalWeight || 0,
this.previousTotalWeight,
);
const weightDiffInJin = DecimalUtils.multiply(weightDiff, 2); const weightDiffInJin = DecimalUtils.multiply(weightDiff, 2);
@ -123,37 +148,46 @@ export class SupplierWeightCalculator {
supplier.netWeight = DecimalUtils.add( supplier.netWeight = DecimalUtils.add(
DecimalUtils.subtract( DecimalUtils.subtract(
DecimalUtils.add(weightDiffInJin, weights.remain), DecimalUtils.add(weightDiffInJin, weights.remain),
weights.extraUsed weights.extraUsed,
) ),
); );
} else if (isLastSupplier) { } else if (isLastSupplier) {
// 最后一个农户 // 最后一个农户
supplier.netWeight = DecimalUtils.add( supplier.netWeight = DecimalUtils.add(
DecimalUtils.subtract( DecimalUtils.subtract(weightDiffInJin, weights.extraUsed),
weightDiffInJin, weights.remain,
weights.extraUsed
),
weights.remain
); );
} else { } else {
// 中间农户(包括第一个但不是最后一个) // 中间农户(包括第一个但不是最后一个)
supplier.netWeight = DecimalUtils.subtract(weightDiffInJin, weights.extra); supplier.netWeight = DecimalUtils.subtract(
weightDiffInJin,
weights.extra,
);
} }
// 毛重 = 净重 + 本次使用纸箱重量 // 毛重 = 净重 + 本次使用纸箱重量
supplier.grossWeight = DecimalUtils.add( supplier.grossWeight = DecimalUtils.add(
supplier.netWeight || 0, supplier.netWeight || 0,
weights.used weights.used,
); );
} }
/**
*
*/
private calculateQuoteWeight(supplier: BusinessAPI.OrderSupplier): number {
console.log("quote weight:", supplier.pricingMethod);
return supplier.pricingMethod === "BY_GROSS_WEIGHT"
? supplier.grossWeight
: supplier.netWeight;
}
/** /**
* *
*/ */
private calculateInvoiceAmount(supplier: BusinessAPI.OrderSupplier): void { private calculateInvoiceAmount(supplier: BusinessAPI.OrderSupplier): void {
supplier.invoiceAmount = DecimalUtils.multiply( supplier.invoiceAmount = DecimalUtils.multiply(
supplier.netWeight || 0, this.calculateQuoteWeight(supplier),
supplier.purchasePrice || 0 supplier.purchasePrice || 0,
); );
} }
@ -162,16 +196,16 @@ export class SupplierWeightCalculator {
*/ */
private calculateBoxWeightByType( private calculateBoxWeightByType(
packages: BusinessAPI.OrderPackage[], packages: BusinessAPI.OrderPackage[],
boxType?: string boxType?: string,
): number { ): number {
const filteredPackages = boxType const filteredPackages = boxType
? packages.filter(pkg => pkg.boxType === boxType) ? packages.filter((pkg) => pkg.boxType === boxType)
: packages; : packages;
return filteredPackages.reduce((total, pkg) => { return filteredPackages.reduce((total, pkg) => {
const weight = DecimalUtils.multiply( const weight = DecimalUtils.multiply(
pkg.boxCount || 0, pkg.boxCount || 0,
pkg.boxProductWeight || 0 pkg.boxProductWeight || 0,
); );
return DecimalUtils.add(total, weight); return DecimalUtils.add(total, weight);
}, 0); }, 0);
@ -188,7 +222,7 @@ export class SupplierWeightCalculator {
* *
*/ */
getSupplierNetWeight(supplierId: string | number): number { getSupplierNetWeight(supplierId: string | number): number {
const supplier = this.suppliers.find(s => s.supplierId === supplierId); const supplier = this.suppliers.find((s) => s.supplierId === supplierId);
return supplier?.netWeight || 0; return supplier?.netWeight || 0;
} }
@ -196,7 +230,7 @@ export class SupplierWeightCalculator {
* *
*/ */
getSupplierGrossWeight(supplierId: string | number): number { getSupplierGrossWeight(supplierId: string | number): number {
const supplier = this.suppliers.find(s => s.supplierId === supplierId); const supplier = this.suppliers.find((s) => s.supplierId === supplierId);
return supplier?.grossWeight || 0; return supplier?.grossWeight || 0;
} }
@ -247,7 +281,7 @@ export class SupplierWeightCalculator {
return { return {
isValid: errors.length === 0, isValid: errors.length === 0,
errors errors,
}; };
} }
} }

View File

@ -9,4 +9,4 @@
export { OrderCalculator, WeightCalculationService } from "./calculators"; export { OrderCalculator, WeightCalculationService } from "./calculators";
// 模板类 // 模板类
export { PdfTemplate } from "./templates"; export { PdfTemplate, ProfitTableTemplate } from "./templates";

View File

@ -8,7 +8,48 @@ export class PdfTemplate {
// 将预览内容转换为HTML字符串的函数 // 将预览内容转换为HTML字符串的函数
generateHtmlString = () => { generateHtmlString = () => {
let htmlString = ` let htmlString = `
<style> @page {size: 210mm 297mm;margin: 0;padding: 0;}* {outline: none;box-sizing: border-box;margin: 0;padding: 0;border: 0 solid;}body {background-color: #fff;color: #4d4d4d;font-size: 14px;font-style: normal;box-sizing: border-box;}.page-wrap {width: 210mm;min-height: 297mm;margin: 0 auto;}.page-content {position: relative;box-sizing: border-box;width: 100%;height: 100%;padding: 20mm 10mm 0;display: flex;flex-direction: column;gap: 2mm;}@media print {.print-controls {display: none !important;}body {padding: 0;margin: 0;}}.print-module {margin-bottom: 15px;text-align: center;}.print-controls {position: fixed;top: 10px;right: 10px;z-index: 9999;}.print-button,.close-button {padding: 8px 16px;margin-left: 10px;cursor: pointer;border: 1px solid #d9d9d9;border-radius: 4px;}.print-button {background-color: #1890ff;color: white;}.close-button {background-color: #fff;color: #000;}.preview {width: 19cm;div {height: 0.7cm;}}.table-border {border: 2px solid #000;}.table-border>div {border-bottom: 1px solid #000;}.table-border>div>div {border-right: 1px solid #000;}.table-border>div>div:last-child {border-right: none;}.table-border>div:last-child {border-bottom: none;}.col-span-1 {grid-column: span 1 / span 1;}.col-span-2 {grid-column: span 2 / span 2;}.col-span-3 {grid-column: span 3 / span 3;}.col-span-6 {grid-column: span 6 / span 6;}.col-span-8 {grid-column: span 8 / span 8;}.flex {display: flex;}.items-center {align-items: center;}.grid {display: grid;}.w-full {width: 100%;}.grid-cols-1 {grid-template-columns: repeat(1, minmax(0, 1fr));}.grid-cols-2 {grid-template-columns: repeat(2, minmax(0, 1fr));}.grid-cols-3 {grid-template-columns: repeat(3, minmax(0, 1fr));}.grid-cols-4 {grid-template-columns: repeat(4, minmax(0, 1fr));}.grid-cols-5 {grid-template-columns: repeat(5, minmax(0, 1fr));}.grid-cols-6 {grid-template-columns: repeat(6, minmax(0, 1fr));}.grid-cols-7 {grid-template-columns: repeat(7, minmax(0, 1fr));}.grid-cols-8 {grid-template-columns: repeat(8, minmax(0, 1fr));}.items-end {align-items: flex-end;}.justify-center {justify-content: center;}.border-t-0 {border-top-width: 0px;}.border-b {border-bottom-width: 1px;}.border-black {border-color: #000000;}.bg-white {background-color: #ffffff;}.text-2xl {font-size: 24px;line-height: 1;}.text-base {font-size: 16px;line-height: 1;}.text-lg {font-size: 18px;line-height: 1;}.font-bold {font-weight: bold;}.preview {width: 19cm;div {height: 0.69cm;}}.table-border {border: 2px solid #000;}.table-border>div {border-bottom: 1px solid #000;}.table-border>div>div {border-right: 1px solid #000;}.table-border>div>div:last-child {border-right: none;}.table-border>div:last-child {border-bottom: none;}.p-2 {padding:8px}</style> <style>
@page {size: 210mm 297mm;margin: 0;padding: 0;}
* {outline: none;box-sizing: border-box;margin: 0;padding: 0;border: 0 solid;}
body {background-color: #fff;color: #4d4d4d;font-size: 14px;font-style: normal;box-sizing: border-box;}
.page-wrap {width: 210mm;min-height: 297mm;margin: 0 auto;}
.page-content {position: relative;box-sizing: border-box;width: 100%;height: 100%;padding: 20mm 10mm 0;display: flex;flex-direction: column;gap: 2mm;}
@media print {.print-controls {display: none !important;}body {padding: 0;margin: 0;}}
.col-span-1 {grid-column: span 1 / span 1;}
.col-span-2 {grid-column: span 2 / span 2;}
.col-span-3 {grid-column: span 3 / span 3;}
.col-span-6 {grid-column: span 6 / span 6;}
.col-span-8 {grid-column: span 8 / span 8;}
.flex {display: flex;}
.items-center {align-items: center;}
.grid {display: grid;}
.w-full {width: 100%;}
.grid-cols-1 {grid-template-columns: repeat(1, minmax(0, 1fr));}
.grid-cols-2 {grid-template-columns: repeat(2, minmax(0, 1fr));}
.grid-cols-3 {grid-template-columns: repeat(3, minmax(0, 1fr));}
.grid-cols-4 {grid-template-columns: repeat(4, minmax(0, 1fr));}
.grid-cols-5 {grid-template-columns: repeat(5, minmax(0, 1fr));}
.grid-cols-6 {grid-template-columns: repeat(6, minmax(0, 1fr));}
.grid-cols-7 {grid-template-columns: repeat(7, minmax(0, 1fr));}
.grid-cols-8 {grid-template-columns: repeat(8, minmax(0, 1fr));}
.items-end {align-items: flex-end;}
.justify-center {justify-content: center;}
.border-t-0 {border-top-width: 0px;}
.border-b {border-bottom-width: 1px;}
.border-black {border-color: #000000;}
.bg-white {background-color: #ffffff;}
.text-2xl {font-size: 24px;line-height: 1;}
.text-base {font-size: 16px;line-height: 1;}
.text-lg {font-size: 18px;line-height: 1;}
.font-bold {font-weight: bold;}
.preview {width: 19cm;div {height: 0.69cm;}}
.table-border {border: 2px solid #000;}
.table-border>div {border-bottom: 1px solid #000;}
.table-border>div>div {border-right: 1px solid #000;}
.table-border>div>div:last-child {border-right: none;}
.table-border>div:last-child {border-bottom: none;}
.p-2 {padding:8px}
</style>
<div class="page-wrap"> <div class="page-wrap">
<div class="page-content"> <div class="page-content">
`; `;

View File

@ -0,0 +1,107 @@
// 利润表数据接口
import dayjs from "dayjs";
import "dayjs/locale/zh-cn";
dayjs.locale("zh-cn");
export interface ProfitTableRow {
vehicleNo: string; // 车次
shippingDate: string; // 发货日期
originCost: number; // 产地成本
quoteAmount: number; // 报价金额
profit: number; // 利润
}
export class ProfitTableTemplate {
private data: ProfitTableRow[];
private month: string;
constructor(data: ProfitTableRow[], month: string) {
this.data = data;
this.month = month;
}
// 将利润表内容转换为HTML字符串的函数
generateHtmlString = () => {
// 计算合计
const totalOriginCost = this.data.reduce(
(sum, row) => sum + row.originCost,
0,
);
const totalQuoteAmount = this.data.reduce(
(sum, row) => sum + row.quoteAmount,
0,
);
const totalProfit = this.data.reduce((sum, row) => sum + row.profit, 0);
return `
<style>
@page {size: 210mm 297mm;margin: 0;padding: 0;}
* {outline: none;box-sizing: border-box;margin: 0;padding: 0;border: 0 solid;}
body {background-color: #fff;color: #4d4d4d;font-size: 14px;font-style: normal;box-sizing: border-box;}
.page-wrap {width: 210mm;min-height: 297mm;margin: 0 auto;}
.page-content {position: relative;box-sizing: border-box;width: 100%;height: 100%;padding: 20mm 10mm 0;display: flex;flex-direction: column;gap: 2mm;}
@media print {.print-controls {display: none !important;}body {padding: 0;margin: 0;}}
.preview {width: 19cm;}
.table-border {border: 2px solid #000;border-collapse: collapse;width: 100%;}
.table-border th,
.table-border td {border: 1px solid #000;padding: 8px;text-align: center;}
.table-border th {font-weight: bold;background-color: #f5f5f5;}
.table-title {font-size: 20px;font-weight: bold;text-align: center;padding: 16px;border: 2px solid #000;border-bottom: none;}
.text-center {text-align: center;}
.text-right {text-align: right;}
.font-bold {font-weight: bold;}
.text-xl {font-size: 20px;}
.text-lg {font-size: 18px;}
.text-2xl {font-size: 24px;}
.mb-4 {margin-bottom: 16px;}
.p-4 {padding: 16px;}
.gap-4 {gap: 16px;}
.negative {color: #ff0000;}
.positive {color: #00ff00;}
.footer {background-color: yellow}
</style>
<div class="page-wrap">
<div class="page-content">
<div class="preview">
<div class="table-title"></div>
<table class="table-border">
<thead>
<tr>
<th style="width: 20%;"></th>
<th style="width: 20%;"></th>
<th style="width: 20%;"></th>
<th style="width: 20%;"></th>
<th style="width: 20%;"></th>
</tr>
</thead>
<tbody>
${this.data
.map(
(row) => `
<tr>
<td>${row.vehicleNo}</td>
<td>${row.shippingDate}</td>
<td>${row.originCost}</td>
<td>${row.quoteAmount}</td>
<td>${row.profit.toFixed(2)}</td>
</tr>
`,
)
.join("")}
</tbody>
<tfoot class="footer">
<tr class="font-bold">
<td colspan="2">${dayjs(this.month).format("MMMM")}</td>
<td>${totalOriginCost.toFixed(2)}</td>
<td>${totalQuoteAmount.toFixed(2)}</td>
<td>${totalProfit.toFixed(2)}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
`;
};
}

View File

@ -5,4 +5,5 @@
* import { PdfTemplate } from '@/utils/classes/templates' * import { PdfTemplate } from '@/utils/classes/templates'
*/ */
export { PdfTemplate } from './PdfTemplate' export { PdfTemplate } from "./PdfTemplate";
export { ProfitTableTemplate } from "./ProfitTableTemplate";

View File

@ -2,6 +2,15 @@ import Taro from "@tarojs/taro";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { utils, write } from "xlsx"; import { utils, write } from "xlsx";
// 利润表行数据接口
export interface ProfitTableRow {
vehicleNo: string; // 车次
shippingDate: string; // 发货日期
originCost: number; // 产地成本
quoteAmount: number; // 报价金额
profit: number; // 利润
}
// 费用统计数据接口 // 费用统计数据接口
interface ExpenseStatistics { interface ExpenseStatistics {
totalVehicles: number; totalVehicles: number;
@ -336,3 +345,99 @@ export const exportExcel = async (
}); });
} }
}; };
/**
* Excel文件
* @param data
* @param month YYYY-MM
* @returns Promise<string>
*/
export const exportProfitTableExcel = async (
data: ProfitTableRow[],
month: string,
): Promise<string> => {
// 创建工作簿
const wb = utils.book_new();
// 创建工作表数据
const wsData: (string | number)[][] = [];
// 添加标题
wsData.push([`诚信志远利润明细`]);
wsData.push([]); // 空行
// 添加表头
wsData.push(["车次", "发货日期", "产地成本", "报价金额", "利润"]);
// 添加数据行
data.forEach((row) => {
wsData.push([
row.vehicleNo,
row.shippingDate,
row.originCost,
row.quoteAmount,
row.profit,
]);
});
// 添加合计行
const totalOriginCost = data.reduce((sum, row) => sum + row.originCost, 0);
const totalQuoteAmount = data.reduce((sum, row) => sum + row.quoteAmount, 0);
const totalProfit = data.reduce((sum, row) => sum + row.profit, 0);
wsData.push([]);
wsData.push([
dayjs(month).format("MMMM") + "合计",
"-",
totalOriginCost.toFixed(2),
totalQuoteAmount.toFixed(2),
totalProfit.toFixed(2),
]);
// 创建工作表
const ws = utils.aoa_to_sheet(wsData);
// 设置列宽
const wscols = [
{ wch: 15 }, // 车次
{ wch: 15 }, // 发货日期
{ wch: 15 }, // 产地成本
{ wch: 15 }, // 报价金额
{ wch: 15 }, // 利润
];
ws["!cols"] = wscols;
// 将工作表添加到工作簿
utils.book_append_sheet(wb, ws, "诚信志远利润明细");
// 生成文件名
const fileName = `诚信志远利润明细_${month}_${dayjs().format("YYYYMMDDHHmmss")}.xlsx`;
// 根据环境选择不同的导出方式
const processEnv = process.env.TARO_ENV;
if (processEnv === "h5") {
// H5环境创建下载链接
const excelBuffer = write(wb, { type: "array", bookType: "xlsx" });
const blob = new Blob([excelBuffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
// 创建下载链接
return window.URL.createObjectURL(blob);
} else {
// 小程序环境使用文件系统API
const excelBuffer = write(wb, { type: "array", bookType: "xlsx" });
// 将文件保存到本地
const filePath = `${Taro.env.USER_DATA_PATH}/${fileName}`;
console.log("filePath", filePath);
Taro.getFileSystemManager().writeFile({
filePath,
data: excelBuffer,
encoding: "binary",
});
return filePath;
}
};