From 9c3efa00bb0cd87c7fa2c19d3e6ef99028bdc4fa Mon Sep 17 00:00:00 2001 From: xs Date: Fri, 19 Dec 2025 11:20:45 +0800 Subject: [PATCH] =?UTF-8?q?1.0.50=E5=90=8E=E7=AB=AF=EF=BC=9A=20=20=20=20fi?= =?UTF-8?q?x(budget):=E9=A1=B9=E7=9B=AE=E9=A2=84=E7=AE=97=E5=8F=98?= =?UTF-8?q?=E6=9B=B4=E5=8A=9F=E8=83=BD=E7=89=88=E6=9C=AC=E5=A4=84=E7=90=86?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=EF=BC=9B=E5=AE=8C=E5=96=84=E5=B8=82=E5=9C=BA?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E9=A2=84=E7=AE=97=E5=AF=BC=E5=87=BA=20=20=20?= =?UTF-8?q?=20feat(budget):=E5=AE=8C=E6=88=90=E7=A0=94=E5=8F=91=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E9=A2=84=E7=AE=97=E5=AF=BC=E5=87=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflow/enums/FlowConfigEnum.java | 18 +- .../event/AbstractProcessEventHandler.java | 35 +- .../mybatis/utils/UniversalSqlProvider.java | 65 +- .../controller/ErpBudgetInfoController.java | 32 +- .../dromara/oa/erp/enums/LaborTypeEnum.java | 51 + .../oa/erp/enums/LiteratureTypeEnum.java | 56 + .../oa/erp/enums/ProjectCategoryEnum.java | 3 +- .../oa/erp/enums/RdMaterialTypeEnum.java | 51 + .../dromara/oa/erp/enums/TechTypeEnum.java | 56 + .../oa/erp/mapper/OaUniversalMapper.java | 29 + .../oa/erp/service/IErpBudgetInfoService.java | 8 + .../impl/ErpBudgetInfoServiceImpl.java | 33 +- .../dromara/oa/excel/BaseExcelExporter.java | 679 +++++ .../MarketProjectBudgetExcelExporter.java | 1408 ++++++++++ .../excel/RdProjectBudgetExcelExporter.java | 2320 +++++++++++++++++ .../handler/OaProcessEventHandler.java | 37 + .../strategy/BudgetWorkflowStrategy.java | 1 + 17 files changed, 4842 insertions(+), 40 deletions(-) create mode 100644 ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/enums/LaborTypeEnum.java create mode 100644 ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/enums/LiteratureTypeEnum.java create mode 100644 ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/enums/RdMaterialTypeEnum.java create mode 100644 ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/enums/TechTypeEnum.java create mode 100644 ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/excel/BaseExcelExporter.java create mode 100644 ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/excel/MarketProjectBudgetExcelExporter.java create mode 100644 ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/excel/RdProjectBudgetExcelExporter.java diff --git a/ruoyi-common/hwbm-common-workflow/src/main/java/org/dromara/workflow/enums/FlowConfigEnum.java b/ruoyi-common/hwbm-common-workflow/src/main/java/org/dromara/workflow/enums/FlowConfigEnum.java index f4e785e1..1abf0590 100644 --- a/ruoyi-common/hwbm-common-workflow/src/main/java/org/dromara/workflow/enums/FlowConfigEnum.java +++ b/ruoyi-common/hwbm-common-workflow/src/main/java/org/dromara/workflow/enums/FlowConfigEnum.java @@ -15,7 +15,8 @@ public enum FlowConfigEnum { /** * 预算流程 */ - BUDGET("HWOABudget", "erp_budget_info", "budget_status", OAStatusEnum.COMPLETED.getStatus(), OAStatusEnum.DRAFT.getStatus(), "flow_status", "预算审批", "budget_id"); + BUDGET("HWOABudget", "erp_budget_info", "budget_status", OAStatusEnum.COMPLETED.getStatus(), OAStatusEnum.DRAFT.getStatus(), "flow_status", "预算审批", "budget_id", OAStatusEnum.INVALID.getStatus(),"project_id"); + /** @@ -59,9 +60,20 @@ public enum FlowConfigEnum { */ private final String businessPk; + /** + * 流程失效时的业务状态值 + */ + private final String invalidBusinessStatus; + + /** + * 在更新其他生效为失效需要的条件ID字段 + */ + private final String relationIdField; + FlowConfigEnum(String flowCode, String tableName, String businessStatusField, - String completedBusinessStatus, String draftBusinessStatus, String flowStatusField, String businessTitle, String businessPk) { + String completedBusinessStatus, String draftBusinessStatus, String flowStatusField, + String businessTitle, String businessPk,String invalidBusinessStatus,String relationIdField) { this.flowCode = flowCode; this.tableName = tableName; this.businessStatusField = businessStatusField; @@ -70,6 +82,8 @@ public enum FlowConfigEnum { this.flowStatusField = flowStatusField; this.businessTitle = businessTitle; this.businessPk = businessPk; + this.invalidBusinessStatus = invalidBusinessStatus; + this.relationIdField = relationIdField; } /** diff --git a/ruoyi-common/hwbm-common-workflow/src/main/java/org/dromara/workflow/event/AbstractProcessEventHandler.java b/ruoyi-common/hwbm-common-workflow/src/main/java/org/dromara/workflow/event/AbstractProcessEventHandler.java index b3ef75a4..5d6a1f11 100644 --- a/ruoyi-common/hwbm-common-workflow/src/main/java/org/dromara/workflow/event/AbstractProcessEventHandler.java +++ b/ruoyi-common/hwbm-common-workflow/src/main/java/org/dromara/workflow/event/AbstractProcessEventHandler.java @@ -43,14 +43,18 @@ public abstract class AbstractProcessEventHandler implements IProcessEventHandle FlowConfigEnum flowConfig = FlowConfigEnum.getByFlowCode(flowCode); // 步骤2: 构建更新字段 - Map setFields = buildSetFields(flowConfig,processEvent); + Map setFields = buildSetFields(flowConfig, processEvent.getStatus()); // 步骤3: 构建查询条件 - Map conditions = buildConditions(flowConfig,processEvent); + Map conditions = buildConditions(flowConfig, processEvent); // 步骤4: 执行更新 int updateCount = doUpdate(flowConfig, setFields, conditions); + // 如果流程完成,更新业务状态 + if (BusinessStatusEnum.FINISH.getStatus().equals(processEvent.getStatus())) { + makeInvalid(flowConfig, processEvent); + } // 步骤5: 验证更新结果 validateUpdateResult(processEvent, updateCount); @@ -69,7 +73,7 @@ public abstract class AbstractProcessEventHandler implements IProcessEventHandle // 检查是否支持该流程编码 FlowConfigEnum flowConfig = FlowConfigEnum.getByFlowCode(flowCode); if (flowConfig == null) { - throw new IllegalArgumentException("不支持的流程编码: "+flowCode+", 忽略处理"); + throw new IllegalArgumentException("不支持的流程编码: " + flowCode + ", 忽略处理"); } // if (flowConfig == null) { // throw new IllegalArgumentException("流程配置不能为空"); @@ -83,19 +87,28 @@ public abstract class AbstractProcessEventHandler implements IProcessEventHandle } + /** + * 需要将之前可用的变为失效 + * + * @param flowConfig + * @param processEvent + */ + protected abstract void makeInvalid(FlowConfigEnum flowConfig, ProcessEvent processEvent); + + /** * 构建更新字段 */ - protected Map buildSetFields(FlowConfigEnum flowConfig, ProcessEvent processEvent) { + protected Map buildSetFields(FlowConfigEnum flowConfig, String status) { Map setFields = new HashMap<>(); // 设置流程状态 - setFields.put(flowConfig.getFlowStatusField(), processEvent.getStatus()); + setFields.put(flowConfig.getFlowStatusField(), status); // 如果流程完成,更新业务状态 - if (BusinessStatusEnum.FINISH.getStatus().equals(processEvent.getStatus())) { + if (BusinessStatusEnum.FINISH.getStatus().equals(status)) { setFields.put(flowConfig.getBusinessStatusField(), flowConfig.getCompletedBusinessStatus()); - } else if (BusinessStatusEnum.BACK.getStatus().equals(processEvent.getStatus())) { + } else if (BusinessStatusEnum.BACK.getStatus().equals(status)) { setFields.put(flowConfig.getBusinessStatusField(), flowConfig.getDraftBusinessStatus()); } @@ -127,7 +140,13 @@ public abstract class AbstractProcessEventHandler implements IProcessEventHandle * 执行更新操作 - 可被子类重写 */ protected abstract int doUpdate(FlowConfigEnum flowConfig, - Map setFields, Map conditions); + Map setFields, Map conditions); + + /** + * 执行更新操作(带操作符的) - 可被子类重写 + */ + protected abstract int doUpdateWithOperator(FlowConfigEnum flowConfig, + Map setFields, Map conditions,String businessId); /** * 验证更新结果 - 可被子类重写 diff --git a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/utils/UniversalSqlProvider.java b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/utils/UniversalSqlProvider.java index 76bf6a0c..9fe3f30d 100644 --- a/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/utils/UniversalSqlProvider.java +++ b/ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/utils/UniversalSqlProvider.java @@ -17,6 +17,20 @@ import java.util.Map; */ public class UniversalSqlProvider { + /** + * 动态查询字段值 + */ + public String buildDynamicSelect(@Param("tableName") String tableName, + @Param("selectField") String selectField, + @Param("whereField") String whereField, + @Param("whereValue") Object whereValue) { + return new SQL() {{ + SELECT(selectField); + FROM(tableName); + WHERE(whereField + " = #{whereValue}"); + }}.toString(); + } + /** * 构建动态更新SQL */ @@ -47,7 +61,7 @@ public class UniversalSqlProvider { */ public String buildDynamicUpdateWithOperator(@Param("tableName") String tableName, @Param("setFields") Map setFields, - @Param("conditions") Map conditions) { + @Param("conditions") Map conditions) { // 修改为Map return new SQL() {{ UPDATE(tableName); @@ -58,41 +72,44 @@ public class UniversalSqlProvider { Object value = entry.getValue(); if (value instanceof String && ((String) value).startsWith("+")) { - // 增量更新,如: set quantity = quantity + 1 SET(key + " = " + key + " + " + ((String) value).substring(1)); } else if (value instanceof String && ((String) value).startsWith("-")) { - // 减量更新 SET(key + " = " + key + " - " + ((String) value).substring(1)); + } else if (value instanceof String && ((String) value).startsWith("*")) { + SET(key + " = " + key + " * " + ((String) value).substring(1)); + } else if (value instanceof String && ((String) value).startsWith("/")) { + SET(key + " = " + key + " / " + ((String) value).substring(1)); } else { - // 普通赋值更新 SET(key + " = #{setFields." + key + "}"); } } } - // 设置条件 + // 设置条件 - 现在支持完整表达式 if (conditions != null && !conditions.isEmpty()) { - for (Map.Entry entry : conditions.entrySet()) { + for (Map.Entry entry : conditions.entrySet()) { String key = entry.getKey(); - Object value = entry.getValue(); + String value = entry.getValue(); - if (value instanceof String) { - String strValue = (String) value; - if (strValue.startsWith(">=")) { - WHERE(key + " >= " + strValue.substring(2)); - } else if (strValue.startsWith("<=")) { - WHERE(key + " <= " + strValue.substring(2)); - } else if (strValue.startsWith(">")) { - WHERE(key + " > " + strValue.substring(1)); - } else if (strValue.startsWith("<")) { - WHERE(key + " < " + strValue.substring(1)); - } else if (strValue.startsWith("!=")) { - WHERE(key + " != " + strValue.substring(2)); - } else if (strValue.startsWith("LIKE ")) { - WHERE(key + " LIKE '%" + strValue.substring(5) + "%'"); - } else { - WHERE(key + " = #{conditions." + key + "}"); - } + // 如果key包含等号,说明是一个完整表达式 + if (key.contains("=") || key.contains(">") || key.contains("<") || + key.contains("LIKE") || key.contains("IN")) { + WHERE(key); // 直接使用key作为WHERE条件 + } else if (value.startsWith("(")) { + // 子查询 + WHERE(key + " = " + value); + } else if (value.startsWith(">=")) { + WHERE(key + " >= " + value.substring(2)); + } else if (value.startsWith("<=")) { + WHERE(key + " <= " + value.substring(2)); + } else if (value.startsWith(">")) { + WHERE(key + " > " + value.substring(1)); + } else if (value.startsWith("<")) { + WHERE(key + " < " + value.substring(1)); + } else if (value.startsWith("!=")) { + WHERE(key + " != " + value.substring(2)); + } else if (value.startsWith("LIKE ")) { + WHERE(key + " LIKE '%" + value.substring(5) + "%'"); } else { WHERE(key + " = #{conditions." + key + "}"); } diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/controller/ErpBudgetInfoController.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/controller/ErpBudgetInfoController.java index 5b3c352a..6c366385 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/controller/ErpBudgetInfoController.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/controller/ErpBudgetInfoController.java @@ -1,5 +1,6 @@ package org.dromara.oa.erp.controller; +import java.io.IOException; import java.util.List; import lombok.RequiredArgsConstructor; @@ -12,6 +13,7 @@ import org.dromara.oa.base.service.IBaseMaterialInfoService; import org.dromara.oa.erp.domain.bo.ErpProjectInfoBo; import org.dromara.oa.erp.domain.vo.ErpProjectInfoVo; import org.dromara.oa.erp.service.IErpProjectInfoService; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import org.springframework.validation.annotation.Validated; import org.dromara.common.idempotent.annotation.RepeatSubmit; @@ -67,6 +69,34 @@ public class ErpBudgetInfoController extends BaseController { ExcelUtil.exportExcel(list, "项目预算", ErpBudgetInfoVo.class, response); } + /** + * 导出项目预算详细信息 + */ + @SaCheckPermission("oa:erp/budgetInfo:export") + @Log(title = "项目预算", businessType = BusinessType.EXPORT) + @PostMapping("/exportBudgetInfo") + public void exportBudgetInfo(ErpBudgetInfoBo bo, HttpServletResponse response) throws IOException { + + try { + byte[] excelBytes = erpBudgetInfoService.exportBudgetInfo(bo.getBudgetId()); + +// String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString()); + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); +// response.setHeader(HttpHeaders.CONTENT_DISPOSITION, +// "attachment; filename=\"" + encodedFileName + "\""); +// response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(excelBytes.length)); + + // 写入响应流 + response.getOutputStream().write(excelBytes); + response.getOutputStream().flush(); + } catch (Exception e) { + e.printStackTrace(); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + response.setContentType(MediaType.TEXT_PLAIN_VALUE); + response.getWriter().write("导出失败: " + e.getMessage()); + } + } + /** * 获取项目预算详细信息 * @@ -75,7 +105,7 @@ public class ErpBudgetInfoController extends BaseController { @SaCheckPermission("oa:erp/budgetInfo:query") @GetMapping("/{budgetId}") public R getInfo(@NotNull(message = "主键不能为空") - @PathVariable("budgetId") Long budgetId) { + @PathVariable("budgetId") Long budgetId) { return R.ok(erpBudgetInfoService.queryById(budgetId)); } diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/enums/LaborTypeEnum.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/enums/LaborTypeEnum.java new file mode 100644 index 00000000..1ce3022e --- /dev/null +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/enums/LaborTypeEnum.java @@ -0,0 +1,51 @@ +package org.dromara.oa.erp.enums; + +import lombok.Getter; + +/** + * @author xins + * @description erp_rd_budget_labor_cost类型枚举 + * @date 2025/12/11 13:59 + */ +@Getter +public enum LaborTypeEnum { + /** + * 人工费 + */ + LABOR_COST("1", "人工费"), + + /** + * 劳务费 + */ + SERVICE_COST("2", "劳务费"); + + + /** + * 编码 + */ + private final String code; + + /** + * 名称 + */ + private final String name; + + + LaborTypeEnum(String code, String name) { + this.code = code; + this.name = name; + } + + /** + * 根据流程编码获取配置 + */ + public static LaborTypeEnum getByCode(String code) { + for (LaborTypeEnum config : values()) { + if (config.getCode().equals(code)) { + return config; + } + } + return null; + } + +} diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/enums/LiteratureTypeEnum.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/enums/LiteratureTypeEnum.java new file mode 100644 index 00000000..0fcce341 --- /dev/null +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/enums/LiteratureTypeEnum.java @@ -0,0 +1,56 @@ +package org.dromara.oa.erp.enums; + +import lombok.Getter; + +/** + * @author xins + * @description erp_rd_budget_literature_cost类型枚举 + * @date 2025/12/11 16:22 + */ +@Getter +public enum LiteratureTypeEnum { + /** + * 资料费 + */ + MATERIAL_COST("1", "资料费"), + + /** + * 文献检索费 + */ + DOCUMENT_COST("2", "文献检索费"), + + /** + * 专用软件购买费 + */ + SOFTWARE_COST("3", "专用软件购买费"); + + + /** + * 编码 + */ + private final String code; + + /** + * 名称 + */ + private final String name; + + + LiteratureTypeEnum(String code, String name) { + this.code = code; + this.name = name; + } + + /** + * 根据流程编码获取配置 + */ + public static LiteratureTypeEnum getByCode(String code) { + for (LiteratureTypeEnum config : values()) { + if (config.getCode().equals(code)) { + return config; + } + } + return null; + } + +} diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/enums/ProjectCategoryEnum.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/enums/ProjectCategoryEnum.java index 551692dc..e85399ed 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/enums/ProjectCategoryEnum.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/enums/ProjectCategoryEnum.java @@ -5,8 +5,7 @@ import org.dromara.common.core.enums.OAStatusEnum; /** * @author xins - * @description 流程配置枚举 - * 定义流程编码与业务实体的映射关系 + * @description 项目类别枚举 * @date 2025/11/4 9:41 */ @Getter diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/enums/RdMaterialTypeEnum.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/enums/RdMaterialTypeEnum.java new file mode 100644 index 00000000..d80cb724 --- /dev/null +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/enums/RdMaterialTypeEnum.java @@ -0,0 +1,51 @@ +package org.dromara.oa.erp.enums; + +import lombok.Getter; + +/** + * @author xins + * @description erp_rd_budget_material_cost材料类型枚举 + * @date 2025/12/18 16:07 + */ +@Getter +public enum RdMaterialTypeEnum { + /** + * 主要材料费 + */ + MAIN_MATERIAL("1", "主要材料费"), + + /** + * 其他材料费 + */ + OTHER_MATERIAL("2", "其他材料费"); + + + /** + * 编码 + */ + private final String code; + + /** + * 名称 + */ + private final String name; + + + RdMaterialTypeEnum(String code, String name) { + this.code = code; + this.name = name; + } + + /** + * 根据流程编码获取配置 + */ + public static RdMaterialTypeEnum getByCode(String code) { + for (RdMaterialTypeEnum config : values()) { + if (config.getCode().equals(code)) { + return config; + } + } + return null; + } + +} diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/enums/TechTypeEnum.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/enums/TechTypeEnum.java new file mode 100644 index 00000000..0bf8cd7b --- /dev/null +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/enums/TechTypeEnum.java @@ -0,0 +1,56 @@ +package org.dromara.oa.erp.enums; + +import lombok.Getter; + +/** + * @author xins + * @description erp_rd_budget_tech_cost技术类型枚举 + * @date 2025/12/11 13:58 + */ +@Getter +public enum TechTypeEnum { + /** + * 技术咨询开发 + */ + TECH_CONSULT("1", "技术咨询开发"), + + /** + * 专家咨询-会议形式 + */ + EXPERT_MEETING("2", "专家咨询-会议形式"), + + /** + * 专家咨询-通讯形式 + */ + EXPERT_COMMUNICATION("3", "专家咨询-通讯形式"); + + + /** + * 编码 + */ + private final String code; + + /** + * 名称 + */ + private final String name; + + + TechTypeEnum(String code, String name) { + this.code = code; + this.name = name; + } + + /** + * 根据流程编码获取配置 + */ + public static TechTypeEnum getByCode(String code) { + for (TechTypeEnum config : values()) { + if (config.getCode().equals(code)) { + return config; + } + } + return null; + } + +} diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/mapper/OaUniversalMapper.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/mapper/OaUniversalMapper.java index 63ebeb75..f67bdbcc 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/mapper/OaUniversalMapper.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/mapper/OaUniversalMapper.java @@ -2,6 +2,7 @@ package org.dromara.oa.erp.mapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.SelectProvider; import org.apache.ibatis.annotations.UpdateProvider; import org.dromara.common.mybatis.utils.UniversalSqlProvider; @@ -14,6 +15,16 @@ import java.util.Map; @Mapper public interface OaUniversalMapper { + /** + * 动态查询关联ID + */ + @SelectProvider(type = UniversalSqlProvider.class, method = "buildDynamicSelect") + String selectRelationId(@Param("tableName") String tableName, + @Param("selectField") String selectField, + @Param("whereField") String whereField, + @Param("whereValue") Object whereValue); + + /** * 动态更新表数据 * @@ -26,4 +37,22 @@ public interface OaUniversalMapper { int dynamicUpdate(@Param("tableName") String tableName, @Param("setFields") Map setFields, @Param("conditions") Map conditions); + + + /** + * 带操作符的SQL动态更新表数据 + * + * @param tableName 表名 + * @param setFields 更新字段和值 + * @param conditions 条件字段和值 + * @return 更新记录数 + */ + @UpdateProvider(type = UniversalSqlProvider.class, method = "buildDynamicUpdateWithOperator") + int dynamicUpdateWithOperator(@Param("tableName") String tableName, + @Param("setFields") Map setFields, + @Param("conditions") Map conditions, + @Param("businessId") String businessId); + + + } diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/IErpBudgetInfoService.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/IErpBudgetInfoService.java index 30fea8ba..066ce9c1 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/IErpBudgetInfoService.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/IErpBudgetInfoService.java @@ -5,6 +5,7 @@ import org.dromara.oa.erp.domain.vo.ErpBudgetInfoVo; import org.dromara.oa.erp.domain.bo.ErpBudgetInfoBo; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.PageQuery; +import org.springframework.transaction.annotation.Transactional; import java.util.Collection; import java.util.List; @@ -74,4 +75,11 @@ public interface IErpBudgetInfoService { * @return 是否删除成功 */ Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * + * @param budgetId + * @return excel下载 + */ + public byte[] exportBudgetInfo(Long budgetId); } diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpBudgetInfoServiceImpl.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpBudgetInfoServiceImpl.java index 7100a615..a40c4b7c 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpBudgetInfoServiceImpl.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpBudgetInfoServiceImpl.java @@ -23,6 +23,8 @@ import org.dromara.oa.erp.domain.*; import org.dromara.oa.erp.enums.ProjectCategoryEnum; import org.dromara.oa.erp.enums.RdBudgetCostEnums; import org.dromara.oa.erp.mapper.*; +import org.dromara.oa.excel.MarketProjectBudgetExcelExporter; +import org.dromara.oa.excel.RdProjectBudgetExcelExporter; import org.dromara.oa.workflow.strategy.BudgetWorkflowStrategy; import org.dromara.workflow.api.RemoteWorkflowService; import org.dromara.workflow.api.domain.RemoteStartProcess; @@ -35,6 +37,7 @@ import org.dromara.oa.erp.service.IErpBudgetInfoService; import org.dromara.workflow.strategy.WorkflowStrategy; import org.springframework.transaction.annotation.Transactional; +import java.io.IOException; import java.util.*; import java.util.function.Consumer; import java.util.function.Function; @@ -83,6 +86,10 @@ public class ErpBudgetInfoServiceImpl extends AbstractWorkflowService queryWrapper = new LambdaQueryWrapper<>(); @@ -375,8 +382,6 @@ public class ErpBudgetInfoServiceImpl extends AbstractWorkflowService 0; } + + /** + * + * @param budgetId + * @return excel下载 + */ + @Override + @Transactional(readOnly = true) + public byte[] exportBudgetInfo(Long budgetId) { + try { + ErpBudgetInfoVo erpBudgetInfoVo = this.queryById(budgetId); + String projectCategory = erpBudgetInfoVo.getProjectCategory(); + if (projectCategory.equals(ProjectCategoryEnum.MARKET.getCode()) || projectCategory.equals(ProjectCategoryEnum.MARKET_PART.getCode())) { + return marketProjectBudgetExcelExporter.exportToByteArray(erpBudgetInfoVo); + } else { + return rdProjectBudgetExcelExporter.exportToByteArray(erpBudgetInfoVo); + } + } catch (Exception e) { + throw new RuntimeException("导出Excel失败: " + e.getMessage(), e); + } + } + } diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/excel/BaseExcelExporter.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/excel/BaseExcelExporter.java new file mode 100644 index 00000000..c7cd5c2d --- /dev/null +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/excel/BaseExcelExporter.java @@ -0,0 +1,679 @@ +package org.dromara.oa.excel; + +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.dromara.oa.erp.domain.vo.ErpBudgetInfoVo; + +import java.io.ByteArrayOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @Author xins + * @Date 2025/12/9 14:20 + * @Description: Excel导出器抽象基类 + * 提供公共的样式创建、单元格操作等方法 + */ +@Slf4j +public abstract class BaseExcelExporter { + + protected Workbook workbook; + + // 样式定义 + protected CellStyle titleStyle; + protected CellStyle formLeftStyle; + protected CellStyle formRightStyle; + protected CellStyle formFormulaStyle; + protected CellStyle formPercentStyle; + protected CellStyle headerStyle; + protected CellStyle mergedHeaderStyle; + protected CellStyle dataStyle; + protected CellStyle percentStyle; + protected CellStyle formulaStyle; + protected CellStyle footerFormulaStyle; + protected CellStyle moneyStyle; + protected CellStyle remarkStyle; + protected CellStyle leftMergeStyle; + + // 存储各sheet的总计行位置 + protected final Map sheetTotalRowMap = new ConcurrentHashMap<>(); + + /** + * 导出Excel到字节数组 + */ + public abstract byte[] exportToByteArray(ErpBudgetInfoVo budget) throws IOException; + + /** + * 导出Excel到文件 + */ + public abstract void exportToFile(ErpBudgetInfoVo budget, String filePath) throws IOException; + + /** + * 创建工作簿 + */ + protected Workbook createWorkbook() { + return new XSSFWorkbook(); + } + + /** + * 创建所有样式 + */ + protected void createStyles() { + // 创建标题样式 + createTitleStyle(); + + // 创建表单样式 + createFormStyles(); + + // 创建表头样式 + createHeaderStyles(); + + // 创建数据样式 + createDataStyles(); + + // 创建其他样式 + createOtherStyles(); + } + + /** + * 创建标题样式 + */ + protected void createTitleStyle() { + titleStyle = workbook.createCellStyle(); + Font titleFont = workbook.createFont(); + titleFont.setBold(true); + titleFont.setFontName("微软雅黑"); + titleFont.setFontHeightInPoints((short) 16); + titleStyle.setFont(titleFont); + titleStyle.setAlignment(HorizontalAlignment.CENTER); + titleStyle.setVerticalAlignment(VerticalAlignment.CENTER); + setBorder(titleStyle, BorderStyle.MEDIUM); + } + + /** + * 创建表单样式 + */ + protected void createFormStyles() { + Font formFont = workbook.createFont(); + formFont.setColor(IndexedColors.WHITE.getIndex()); + formFont.setFontName("微软雅黑"); + formFont.setFontHeightInPoints((short) 10); + + // 表单居左样式 + formLeftStyle = workbook.createCellStyle(); + formLeftStyle.setFont(formFont); + formLeftStyle.setFillForegroundColor(IndexedColors.GREY_50_PERCENT.getIndex()); + formLeftStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + setBorder(formLeftStyle, BorderStyle.THIN); + formLeftStyle.setVerticalAlignment(VerticalAlignment.CENTER); + formLeftStyle.setAlignment(HorizontalAlignment.LEFT); + + // 表单居右样式 + formRightStyle = workbook.createCellStyle(); + formRightStyle.cloneStyleFrom(formLeftStyle); + formRightStyle.setAlignment(HorizontalAlignment.RIGHT); + + // 表单公式样式 + formFormulaStyle = workbook.createCellStyle(); + formFormulaStyle.cloneStyleFrom(formRightStyle); + formFormulaStyle.setDataFormat(workbook.createDataFormat().getFormat("0.00")); + + // 表单百分比样式 + formPercentStyle = workbook.createCellStyle(); + formPercentStyle.cloneStyleFrom(formRightStyle); + formPercentStyle.setDataFormat(workbook.createDataFormat().getFormat("0.00%")); + } + + /** + * 创建表头样式 + */ + protected void createHeaderStyles() { + // 普通表头样式 + headerStyle = workbook.createCellStyle(); + Font headerFont = workbook.createFont(); + headerFont.setBold(true); + headerFont.setFontName("微软雅黑"); + headerFont.setFontHeightInPoints((short) 10); + headerStyle.setFont(headerFont); + headerStyle.setFillForegroundColor(IndexedColors.LIGHT_ORANGE.getIndex()); + headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + setBorder(headerStyle, BorderStyle.THIN); + headerStyle.setAlignment(HorizontalAlignment.CENTER); + headerStyle.setVerticalAlignment(VerticalAlignment.CENTER); + headerStyle.setWrapText(true); + + // 合并表头样式 + mergedHeaderStyle = workbook.createCellStyle(); + Font mergedHeaderFont = workbook.createFont(); + mergedHeaderFont.setBold(true); + mergedHeaderFont.setFontName("微软雅黑"); + mergedHeaderFont.setColor(IndexedColors.WHITE.getIndex()); + mergedHeaderFont.setFontHeightInPoints((short) 10); + mergedHeaderStyle.setFont(mergedHeaderFont); + mergedHeaderStyle.setFillForegroundColor(IndexedColors.GREY_40_PERCENT.getIndex()); + mergedHeaderStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + setBorder(mergedHeaderStyle, BorderStyle.THIN); + mergedHeaderStyle.setAlignment(HorizontalAlignment.CENTER); + mergedHeaderStyle.setVerticalAlignment(VerticalAlignment.CENTER); + } + + /** + * 创建数据样式 + */ + protected void createDataStyles() { + // 基础数据样式 + dataStyle = workbook.createCellStyle(); + Font dataFont = workbook.createFont(); + dataFont.setFontName("微软雅黑"); + dataFont.setFontHeightInPoints((short) 10); + dataStyle.setFont(dataFont); + setBorder(dataStyle, BorderStyle.THIN); + dataStyle.setVerticalAlignment(VerticalAlignment.CENTER); + dataStyle.setAlignment(HorizontalAlignment.CENTER); + + // 公式样式 + formulaStyle = workbook.createCellStyle(); + formulaStyle.cloneStyleFrom(dataStyle); + formulaStyle.setDataFormat(workbook.createDataFormat().getFormat("0.00")); + + // 金额样式 + moneyStyle = workbook.createCellStyle(); + moneyStyle.cloneStyleFrom(formulaStyle); + + // 百分比样式 + percentStyle = workbook.createCellStyle(); + percentStyle.cloneStyleFrom(dataStyle); + percentStyle.setDataFormat(workbook.createDataFormat().getFormat("0.00%")); + + // 底部公式样式 + footerFormulaStyle = workbook.createCellStyle(); + footerFormulaStyle.cloneStyleFrom(headerStyle); + footerFormulaStyle.setDataFormat(workbook.createDataFormat().getFormat("0.00")); + } + + /** + * 创建其他样式 + */ + protected void createOtherStyles() { + // 备注样式 + remarkStyle = workbook.createCellStyle(); + Font remarkFont = workbook.createFont(); + remarkFont.setItalic(true); + remarkFont.setFontName("微软雅黑"); + remarkFont.setFontHeightInPoints((short) 10); + remarkStyle.setFont(remarkFont); + remarkStyle.setWrapText(true); + + // 左侧合并样式 + leftMergeStyle = workbook.createCellStyle(); + Font leftMergeFont = workbook.createFont(); + leftMergeFont.setBold(true); + leftMergeFont.setFontName("微软雅黑"); + leftMergeFont.setFontHeightInPoints((short) 10); + leftMergeStyle.setFont(leftMergeFont); + leftMergeStyle.setAlignment(HorizontalAlignment.CENTER); + leftMergeStyle.setVerticalAlignment(VerticalAlignment.CENTER); + leftMergeStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); + leftMergeStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + setBorder(leftMergeStyle, BorderStyle.MEDIUM); + } + + /** + * 设置边框 + */ + protected void setBorder(CellStyle style, BorderStyle borderStyle) { + style.setBorderTop(borderStyle); + style.setBorderBottom(borderStyle); + style.setBorderLeft(borderStyle); + style.setBorderRight(borderStyle); + + style.setTopBorderColor(IndexedColors.BLACK.getIndex()); + style.setBottomBorderColor(IndexedColors.BLACK.getIndex()); + style.setLeftBorderColor(IndexedColors.BLACK.getIndex()); + style.setRightBorderColor(IndexedColors.BLACK.getIndex()); + } + + /** + * 创建合并单元格 + */ + protected void createMergedCell(Sheet sheet, Row row, int firstCol, int lastCol, String value, CellStyle style) { + if (firstCol > lastCol) { + throw new IllegalArgumentException("firstCol must be less than or equal to lastCol"); + } + + Cell cell = row.createCell(firstCol); + cell.setCellValue(value); + if (style != null) { + cell.setCellStyle(style); + } + + // 创建合并区域 + if (firstCol < lastCol) { + CellRangeAddress region = new CellRangeAddress( + row.getRowNum(), row.getRowNum(), firstCol, lastCol); + sheet.addMergedRegion(region); + applyStyleToMergedRegion(sheet, region, style); + } + } + + /** + * 创建纵向合并单元格并设置完整边框(简化版) + */ + protected void createVerticalMergedCellWithBorder(Sheet sheet, int startRow, int endRow, + int col, String value, CellStyle style) { + if (startRow < 0 || endRow < startRow || col < 0) { + return; + } + + // 创建主单元格 + Row mainRow = getOrCreateRow(sheet, startRow); + Cell mainCell = mainRow.createCell(col); + mainCell.setCellValue(value); + + // 创建合并区域 + CellRangeAddress region = new CellRangeAddress(startRow, endRow, col, col); + sheet.addMergedRegion(region); + + // 为整个合并区域设置边框 + setBorderForRegion(sheet, region, style); + + // 设置主单元格的样式(值显示) + mainCell.setCellStyle(style); + } + + /** + * 为整个区域设置边框 + */ + private void setBorderForRegion(Sheet sheet, CellRangeAddress region, CellStyle templateStyle) { + int firstRow = region.getFirstRow(); + int lastRow = region.getLastRow(); + int firstCol = region.getFirstColumn(); + int lastCol = region.getLastColumn(); + + // 获取边框样式 + BorderStyle top = templateStyle.getBorderTop(); + BorderStyle bottom = templateStyle.getBorderBottom(); + BorderStyle left = templateStyle.getBorderLeft(); + BorderStyle right = templateStyle.getBorderRight(); + + short borderColor = templateStyle.getTopBorderColor(); + + // 遍历区域的所有单元格并设置边框 + for (int row = firstRow; row <= lastRow; row++) { + Row currentRow = getOrCreateRow(sheet, row); + + for (int col = firstCol; col <= lastCol; col++) { + Cell cell = currentRow.getCell(col); + if (cell == null) { + cell = currentRow.createCell(col); + } + + CellStyle cellStyle = workbook.createCellStyle(); + + // 复制字体、对齐等基本样式 + copyBasicStyle(cellStyle, templateStyle); + + // 设置边框 + setCellBorder(cellStyle, top, bottom, left, right, borderColor); + + // 如果是角上的单元格,设置对应的边框 + if (row == firstRow) { + cellStyle.setBorderTop(top); + } + if (row == lastRow) { + cellStyle.setBorderBottom(bottom); + } + if (col == firstCol) { + cellStyle.setBorderLeft(left); + } + if (col == lastCol) { + cellStyle.setBorderRight(right); + } + + cell.setCellStyle(cellStyle); + } + } + } + + /** + * 复制基本样式(不包括边框) + */ + private void copyBasicStyle(CellStyle target, CellStyle source) { + // 复制字体 + target.setFont(workbook.getFontAt(source.getFontIndex())); + + // 复制对齐方式 + target.setAlignment(source.getAlignment()); + target.setVerticalAlignment(source.getVerticalAlignment()); + + // 复制填充 + target.setFillForegroundColor(source.getFillForegroundColor()); + target.setFillPattern(source.getFillPattern()); + + // 复制数据格式 + target.setDataFormat(source.getDataFormat()); + } + + /** + * 设置单元格边框 + */ + private void setCellBorder(CellStyle style, BorderStyle top, BorderStyle bottom, + BorderStyle left, BorderStyle right, short borderColor) { + style.setBorderTop(top); + style.setBorderBottom(bottom); + style.setBorderLeft(left); + style.setBorderRight(right); + + style.setTopBorderColor(borderColor); + style.setBottomBorderColor(borderColor); + style.setLeftBorderColor(borderColor); + style.setRightBorderColor(borderColor); + } + + /** + * 创建单元格 + */ + protected Cell createCell(Row row, int column, String value, CellStyle style) { + Cell cell = row.createCell(column); + cell.setCellValue(value); + if (style != null) { + cell.setCellStyle(style); + } + return cell; + } + + /** + * 创建数字单元格 + */ + protected Cell createNumericCell(Row row, int column, Double value, CellStyle style) { + Cell cell = row.createCell(column); + if (value != null) { + cell.setCellValue(value); + } + if (style != null) { + cell.setCellStyle(style); + } + return cell; + } + + /** + * 创建数字单元格(BigDecimal) + */ + protected Cell createNumericCell(Row row, int column, BigDecimal value, CellStyle style) { + if (value == null) { + return createCell(row, column, "", style); + } + return createNumericCell(row, column, value.doubleValue(), style); + } + + /** + * 为合并区域应用样式 + */ + protected void applyStyleToMergedRegion(Sheet sheet, CellRangeAddress region, CellStyle style) { + for (int rowNum = region.getFirstRow(); rowNum <= region.getLastRow(); rowNum++) { + Row row = getOrCreateRow(sheet, rowNum); + for (int colNum = region.getFirstColumn(); colNum <= region.getLastColumn(); colNum++) { + Cell cell = row.getCell(colNum); + if (cell == null) { + cell = row.createCell(colNum); + } + // 只设置边框和填充,保留原始字体 + CellStyle newStyle = workbook.createCellStyle(); + newStyle.cloneStyleFrom(style); + cell.setCellStyle(newStyle); + } + } + } + + /** + * 获取或创建行 + */ + protected Row getOrCreateRow(Sheet sheet, int rowNum) { + Row row = sheet.getRow(rowNum); + if (row == null) { + row = sheet.createRow(rowNum); + } + return row; + } + + /** + * 设置列宽 + */ + protected void setColumnWidths(Sheet sheet, int[] widths) { + for (int i = 0; i < widths.length; i++) { + sheet.setColumnWidth(i, widths[i] * 256); + } + } + + /** + * 关键方法:创建数据行,前两列先横向合并,再根据右侧行数动态纵向合并 + */ + protected void createDataRowsWithDynamicMerge(Sheet sheet, int dataStartRow, int rows, int firstCol, int lastCol, String cellValue) { + // 1. 先横向合并前两列(A列和B列) + // 在数据区域的第一行创建"人工费"单元格 + Row firstDataRow = getOrCreateRow(sheet, dataStartRow); + Cell leftMergeCell = firstDataRow.createCell(0); + leftMergeCell.setCellValue(cellValue); + + // 创建左侧合并单元格的样式 + leftMergeCell.setCellStyle(leftMergeStyle); + + // 2. 根据右侧行数动态纵向合并 + if (rows > 1) { + // 纵向合并前两列(覆盖所有数据行) + CellRangeAddress verticalMerge = new CellRangeAddress( + dataStartRow, dataStartRow + rows - 1, firstCol, lastCol); + sheet.addMergedRegion(verticalMerge); + + // 为纵向合并区域的所有单元格设置样式 + applyStyleToMergedRegion(sheet, verticalMerge, leftMergeStyle); + } + } + + /** + * 清空总计行映射 + */ + protected void clearTotalRowMap() { + sheetTotalRowMap.clear(); + } + + /** + * 添加总计行映射 + */ + protected void addTotalRowMapping(String sheetName, Integer rowNum) { + sheetTotalRowMap.put(sheetName, rowNum); + } + + /** + * 获取总计行映射 + */ + protected Integer getTotalRow(String sheetName) { + return sheetTotalRowMap.get(sheetName); + } + + /** + * 写入字节数组 + */ + protected byte[] writeToByteArray() throws IOException { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + workbook.write(outputStream); + return outputStream.toByteArray(); + } + } + + /** + * 写入文件 + */ + protected void writeToFile(String filePath) throws IOException { + try (FileOutputStream outputStream = new FileOutputStream(filePath)) { + workbook.write(outputStream); + } + } + + /** + * 如果不足指定行数,则填充空行 + */ + protected int fillEmptyRow(Sheet sheet, int rowNum, int startCol, int endCol, int fillRowNum) { + return fillEmptyRow(sheet, rowNum, startCol, endCol, fillRowNum, dataStyle); + } + + /** + * 如果不足指定行数,则填充空行(可指定样式) + */ + protected int fillEmptyRow(Sheet sheet, int rowNum, int startCol, int endCol, int fillRowNum, CellStyle style) { + while (rowNum < fillRowNum) { + Row dataRow = sheet.getRow(rowNum); + if (dataRow == null) { + dataRow = sheet.createRow(rowNum); + } + + for (int i = startCol; i <= endCol; i++) { + createCell(dataRow, i, "", style); + } + + rowNum++; + } + return rowNum; + } + + /** + * 如果不足指定行数,则填充空行(可指定样式和公式) + */ + protected int fillEmptyRow(Sheet sheet, int rowNum, int startCol, int endCol, int fillRowNum,String formula, CellStyle style) { + while (rowNum < fillRowNum) { + Row dataRow = sheet.getRow(rowNum); + if (dataRow == null) { + dataRow = sheet.createRow(rowNum); + } + + for (int i = startCol; i <= endCol; i++) { + createCell(dataRow, i, "", style); + } + + rowNum++; + } + return rowNum; + } + + + /** + * 创建纵向合并单元格(修复边框问题) + * 这个方法确保合并后的单元格所有行都有完整的边框 + */ + protected void createVerticalMergedCell(Sheet sheet, int startRow, int endRow, + int col, String value, CellStyle style) { + if (startRow < 0 || endRow < startRow || col < 0) { + throw new IllegalArgumentException("Invalid parameters for vertical merge"); + } + + // 确保所有行都存在 + for (int i = startRow; i <= endRow; i++) { + getOrCreateRow(sheet, i); + } + + // 创建主单元格 + Row mainRow = sheet.getRow(startRow); + Cell mainCell = mainRow.createCell(col); + mainCell.setCellValue(value); + + // 创建合并区域 + CellRangeAddress region = new CellRangeAddress(startRow, endRow, col, col); + sheet.addMergedRegion(region); + + // 关键步骤:为合并区域的所有单元格设置独立样式和边框 + fixMergedCellBorders(sheet, region, style); + + // 设置主单元格的样式(用于显示内容) + mainCell.setCellStyle(style); + } + + /** + * 修复合并单元格的边框问题 + * 为合并区域内的每个单元格单独设置样式和边框 + */ + protected void fixMergedCellBorders(Sheet sheet, CellRangeAddress region, CellStyle templateStyle) { + int firstRow = region.getFirstRow(); + int lastRow = region.getLastRow(); + int firstCol = region.getFirstColumn(); + int lastCol = region.getLastColumn(); + + // 遍历合并区域的所有单元格 + for (int rowNum = firstRow; rowNum <= lastRow; rowNum++) { + Row row = getOrCreateRow(sheet, rowNum); + + for (int colNum = firstCol; colNum <= lastCol; colNum++) { + Cell cell = row.getCell(colNum); + if (cell == null) { + cell = row.createCell(colNum); + } + + // 创建独立的样式对象 + CellStyle cellStyle = workbook.createCellStyle(); + + // 复制模板样式的基本属性 + copyStyleWithoutBorder(cellStyle, templateStyle); + + // 根据单元格位置设置边框 + setBorderBasedOnPosition(cellStyle, templateStyle, + rowNum, colNum, firstRow, lastRow, firstCol, lastCol); + + // 应用样式到单元格 + cell.setCellStyle(cellStyle); + } + } + } + + /** + * 复制样式但不包括边框 + */ + private void copyStyleWithoutBorder(CellStyle target, CellStyle source) { + // 复制字体 + target.setFont(workbook.getFontAt(source.getFontIndex())); + + // 复制对齐方式 + target.setAlignment(source.getAlignment()); + target.setVerticalAlignment(source.getVerticalAlignment()); + + // 复制填充 + target.setFillForegroundColor(source.getFillForegroundColor()); + target.setFillPattern(source.getFillPattern()); + + // 复制数据格式 + target.setDataFormat(source.getDataFormat()); + + // 复制换行设置 + target.setWrapText(source.getWrapText()); + } + + /** + * 根据单元格位置设置边框 + */ + private void setBorderBasedOnPosition(CellStyle style, CellStyle templateStyle, + int row, int col, + int firstRow, int lastRow, + int firstCol, int lastCol) { + // 获取模板的边框样式 + BorderStyle top = templateStyle.getBorderTop(); + BorderStyle bottom = templateStyle.getBorderBottom(); + BorderStyle left = templateStyle.getBorderLeft(); + BorderStyle right = templateStyle.getBorderRight(); + + short borderColor = templateStyle.getTopBorderColor(); + + // 设置所有边框 + style.setBorderTop(top); + style.setBorderBottom(bottom); + style.setBorderLeft(left); + style.setBorderRight(right); + + style.setTopBorderColor(borderColor); + style.setBottomBorderColor(borderColor); + style.setLeftBorderColor(borderColor); + style.setRightBorderColor(borderColor); + } +} diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/excel/MarketProjectBudgetExcelExporter.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/excel/MarketProjectBudgetExcelExporter.java new file mode 100644 index 00000000..40599bfd --- /dev/null +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/excel/MarketProjectBudgetExcelExporter.java @@ -0,0 +1,1408 @@ +package org.dromara.oa.excel; + +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.dromara.oa.erp.domain.*; +import org.dromara.oa.erp.domain.vo.ErpBudgetInfoVo; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author xins + * @Date 2025/12/9 14:43 + * @Description:市场项目预算Excel导出工具类 + */ +@Component +public class MarketProjectBudgetExcelExporter extends BaseExcelExporter { + + // 存储各sheet的总计行位置(行索引,从0开始) + private final Map sheetTotalRowMap = new ConcurrentHashMap<>(); + + // 存储各sheet名称常量 + private static final String SHEET_BUDGET = "预算表"; + private static final String SHEET_MATERIAL = "材料费"; + private static final String SHEET_LABOR = "人工费"; + private static final String SHEET_INSTALLATION = "安装费"; + private static final String SHEET_TRAVEL = "差旅费"; + private static final String SHEET_OTHER = "其他费用"; + private static final String SHEET_TRANSPORTATION = "运输费"; + private static final String SHEET_AFTERCARE = "售后服务费"; + private static final String SHEET_OTHER_COST = "其他成本"; + + @Override + public byte[] exportToByteArray(ErpBudgetInfoVo budget) throws IOException { + try { + // 创建工作簿和样式 + workbook = createWorkbook(); + createStyles(); + clearTotalRowMap(); + + // 先创建预算表的空表在第一个 + Sheet sheet = workbook.createSheet(SHEET_BUDGET); + + // 创建各个工作表 + createDetailSheets(budget); + + // 创建预算总表(引用明细表的合计行) + createMarketBudgetSheet(sheet, budget); + + // 写入字节数组 + return writeToByteArray(); + + } catch (Exception e) { + throw new RuntimeException("导出市场项目预算失败", e); + } + } + + @Override + public void exportToFile(ErpBudgetInfoVo budget, String filePath) throws IOException { + try { + // 创建工作簿 + workbook = new XSSFWorkbook(); + + // 创建样式 + createStyles(); + + // 清空历史数据 + clearTotalRowMap(); + + // 先创建预算表的空表在第一个 + Sheet sheet = workbook.createSheet(SHEET_BUDGET); + + // 创建各个工作表 + createDetailSheets(budget); + + // 创建预算总表(引用明细表的合计行) + createMarketBudgetSheet(sheet, budget); + + // 写入文件 + writeToFile(filePath); + } catch (Exception e) { + throw new RuntimeException("导出市场项目预算到文件失败", e); + } finally { + workbook.close(); + } + } + + /** + * 创建所有明细表 + */ + private void createDetailSheets(ErpBudgetInfoVo budget) { + // 创建材料费明细表 + Integer materialTotalRow = createMarketMaterialSheet(budget.getErpBudgetMaterialCostList() == null ? new ArrayList<>() : budget.getErpBudgetMaterialCostList()); + sheetTotalRowMap.put(SHEET_MATERIAL, materialTotalRow); + + // 创建人工费明细表 + Integer laborTotalRow = createMarketLaborSheet(budget.getErpBudgetLaborCostList() == null ? new ArrayList<>() : budget.getErpBudgetLaborCostList()); + sheetTotalRowMap.put(SHEET_LABOR, laborTotalRow); + + // 创建安装费明细表 + Integer installationTotalRow = createMarketInstallationSheet(budget.getErpBudgetInstallCostList() == null ? new ArrayList<>() : budget.getErpBudgetInstallCostList()); + sheetTotalRowMap.put(SHEET_INSTALLATION, installationTotalRow); + + // 创建差旅费明细表 + Integer travelTotalRow = createMarketTravelSheet(budget.getErpBudgetTravelCostList() == null ? new ArrayList<>() : budget.getErpBudgetTravelCostList()); + sheetTotalRowMap.put(SHEET_TRAVEL, travelTotalRow); + + // 创建其他费用明细表 + Integer otherTotalRow = createMarketOtherSheet(budget.getErpBudgetOtherCostList() == null ? new ArrayList<>() : budget.getErpBudgetOtherCostList()); + sheetTotalRowMap.put(SHEET_OTHER, otherTotalRow); + } + + // =========== 市场项目预算总表相关方法 =========== + + /** + * 创建市场项目预算总表 + */ + protected void createMarketBudgetSheet(Sheet sheet, ErpBudgetInfoVo budget) { + Map budgetDetailMap = createBudgetDetailMap(budget); + int rowNum = 0; + + // 创建表头部分 + rowNum = createMarketBudgetHeader(sheet, budget, rowNum); + + // 创建基本信息部分 + rowNum = createMarketBudgetBaseInfo(sheet, budget, rowNum); + + // 创建预算科目表头 + rowNum = createMarketBudgetSubjectHeader(sheet, rowNum); + + // 创建预算科目数据 + int dataStartRow = rowNum + 1; + rowNum = createMarketBudgetSubjects(sheet, budgetDetailMap, rowNum); + + // 创建合计行 + rowNum = createMarketBudgetTotalRow(sheet, dataStartRow, rowNum); + + // 设置列宽 + setMarketBudgetSheetColumnWidths(sheet); + } + + /** + * 创建预算科目映射 + */ + private Map createBudgetDetailMap(ErpBudgetInfoVo budget) { + List erpBudgetDetailList = budget.getErpBudgetDetailList(); + if (erpBudgetDetailList == null) { + return Map.of(); + } + + return erpBudgetDetailList.stream() + .collect(Collectors.toMap( + ErpBudgetDetail::getBudgetItem, + detail -> detail, + (existing, replacement) -> existing + )); + } + + /** + * 创建预算表头部分 + */ + private int createMarketBudgetHeader(Sheet sheet, ErpBudgetInfoVo budget, int startRowNum) { + int rowNum = startRowNum; + + // 主标题 + Row titleRow = sheet.createRow(rowNum++); + createMergedCell(sheet, titleRow, 0, 3, "项目经费预算表", titleStyle); + titleRow.setHeightInPoints(22); + + // 项目信息行1 + Row infoRow1 = sheet.createRow(rowNum++); + infoRow1.setHeightInPoints(20); + createCell(infoRow1, 0, "项目名称:", formLeftStyle); + createCell(infoRow1, 1, budget.getProjectName(), formRightStyle); + createCell(infoRow1, 2, "项目号:", formLeftStyle); + createCell(infoRow1, 3, budget.getProjectCode(), formRightStyle); + + // 项目信息行2 + Row infoRow2 = sheet.createRow(rowNum++); + infoRow2.setHeightInPoints(20); + createCell(infoRow2, 0, "项目经理:", formLeftStyle); + createCell(infoRow2, 1, budget.getManagerName(), formRightStyle); + createCell(infoRow2, 2, "产品经理:", formLeftStyle); + createCell(infoRow2, 3, budget.getProductManagerName(), formRightStyle); + + // 项目备注(可多行) +// if (budget.getRemark() != null && !budget.getRemark().isEmpty()) { +// Row remarkRow = sheet.createRow(rowNum++); +// remarkRow.setHeightInPoints(60); +// createMergedCell(sheet, remarkRow, 0, 7, budget.getRemark(), remarkStyle); +// } + + return rowNum; + } + + /** + * 创建预算基本信息部分 + */ + private int createMarketBudgetBaseInfo(Sheet sheet, ErpBudgetInfoVo budget, int startRowNum) { + int rowNum = startRowNum; + + // 金额信息行1 + Row amountRow1 = sheet.createRow(rowNum++); + amountRow1.setHeightInPoints(20); + createCell(amountRow1, 0, "合同额:", formLeftStyle); + createNumericCell(amountRow1, 1, + budget.getContractAmount() != null ? budget.getContractAmount().doubleValue() : 0.0, + formRightStyle); + createCell(amountRow1, 2, "合同净额:", formLeftStyle); + Cell netAmountCell = amountRow1.createCell(3); + netAmountCell.setCellFormula("B4/1.13"); // 假设税率为13% + netAmountCell.setCellStyle(formFormulaStyle); + + // 金额信息行2 + Row amountRow2 = sheet.createRow(rowNum++); + amountRow2.setHeightInPoints(20); + createCell(amountRow2, 0, "预算成本:", formLeftStyle); + Cell budgetCostCell = amountRow2.createCell(1); + budgetCostCell.setCellFormula("C17"); + budgetCostCell.setCellStyle(formFormulaStyle); + createCell(amountRow2, 2, "项目毛利率:", formLeftStyle); + Cell profitRateCell = amountRow2.createCell(3); + profitRateCell.setCellFormula("(D4-B5)/D4"); + profitRateCell.setCellStyle(formPercentStyle); + + // 金额信息行3 + Row amountRow3 = sheet.createRow(rowNum++); + amountRow3.setHeightInPoints(20); + createCell(amountRow3, 0, "降成本后预算成本:", formLeftStyle); + Cell reducedCostCell = amountRow3.createCell(1); + reducedCostCell.setCellFormula("D17"); + reducedCostCell.setCellStyle(formFormulaStyle); + createCell(amountRow3, 2, "降成本后预算毛利率:", formLeftStyle); + Cell reducedProfitRateCell = amountRow3.createCell(3); + reducedProfitRateCell.setCellFormula("(D4-B6)/D4"); + reducedProfitRateCell.setCellStyle(formPercentStyle); + + // 基本信息行 + Row baseInfoRow = sheet.createRow(rowNum++); + baseInfoRow.setHeightInPoints(20); + createCell(baseInfoRow, 0, "项目预算期间:", formLeftStyle); + createCell(baseInfoRow, 1, budget.getDuringOperation(), formRightStyle); + createCell(baseInfoRow, 2, "单位:", formLeftStyle); + createCell(baseInfoRow, 3, "万元", formRightStyle); + + return rowNum; + } + + /** + * 创建预算科目表头 + */ + private int createMarketBudgetSubjectHeader(Sheet sheet, int startRowNum) { + int rowNum = startRowNum; + + Row headerRow = sheet.createRow(rowNum++); + headerRow.setHeightInPoints(21); + createCell(headerRow, 0, "序号", headerStyle); + createCell(headerRow, 1, "预算科目名称", headerStyle); + createCell(headerRow, 2, "预算成本", headerStyle); + createCell(headerRow, 3, "降成本后预算成本", headerStyle); + createCell(headerRow, 4, "参考项目", headerStyle); + + return rowNum; + } + + /** + * 创建预算科目数据 + */ + private int createMarketBudgetSubjects(Sheet sheet, Map budgetDetailMap, int startRowNum) { + int rowNum = startRowNum; + String[] budgetItems = { + "材料费", "人工费", "安装费", "差旅费", "运输费", + "售后服务费", "其他费用", "其他成本" + }; + + for (int i = 0; i < budgetItems.length; i++) { + Row dataRow = sheet.createRow(rowNum++); + dataRow.setHeightInPoints(20); + + // 序号 + createCell(dataRow, 0, String.valueOf(i + 1), dataStyle); + // 科目名称 + createCell(dataRow, 1, budgetItems[i], dataStyle); + + // 根据科目设置不同的公式引用 + int excelRowNum = rowNum; // Excel中的行号(从1开始) + setupBudgetSubjectFormula(dataRow, budgetItems[i], budgetDetailMap, excelRowNum); + } + + return rowNum; + } + + /** + * 设置预算科目公式 + */ + private void setupBudgetSubjectFormula(Row row, String subject, Map budgetDetailMap, int excelRowNum) { + ErpBudgetDetail detail = budgetDetailMap.get(subject); + + switch (subject) { + case "材料费": + setupFormulaForMaterial(row, excelRowNum); + createCell(row, 4, detail == null ? "" : detail.getReferenceProjectName(), dataStyle); + break; + case "人工费": + setupFormulaForLabor(row, excelRowNum); + createCell(row, 4, detail == null ? "" : detail.getReferenceProjectName(), dataStyle); + break; + case "安装费": + setupFormulaForInstallation(row, excelRowNum); + createCell(row, 4, detail == null ? "" : detail.getReferenceProjectName(), dataStyle); + break; + case "差旅费": + setupFormulaForTravel(row, excelRowNum); + createCell(row, 4, detail == null ? "" : detail.getReferenceProjectName(), dataStyle); + break; + case "运输费": + createNumericCell(row, 2, detail == null ? BigDecimal.ZERO : detail.getBudgetCost(), moneyStyle); + createNumericCell(row, 3, detail == null ? BigDecimal.ZERO : detail.getReduceBudgetCost(), moneyStyle); + createCell(row, 4, detail == null ? "" : detail.getReferenceProjectName(), dataStyle); + break; + case "售后服务费": + createNumericCell(row, 2, detail == null ? BigDecimal.ZERO : detail.getBudgetCost(), moneyStyle); + createNumericCell(row, 3, detail == null ? BigDecimal.ZERO : detail.getReduceBudgetCost(), moneyStyle); + createCell(row, 4, detail == null ? "" : detail.getReferenceProjectName(), dataStyle); + break; + case "其他费用": + setupFormulaForOther(row, excelRowNum); + createCell(row, 4, detail == null ? "" : detail.getReferenceProjectName(), dataStyle); + break; + case "其他成本": + createNumericCell(row, 2, detail == null ? BigDecimal.ZERO : detail.getBudgetCost(), moneyStyle); + createNumericCell(row, 3, detail == null ? BigDecimal.ZERO : detail.getReduceBudgetCost(), moneyStyle); + createCell(row, 4, detail == null ? "" : detail.getReferenceProjectName(), dataStyle); + break; + } + } + + /** + * 创建合计行 + */ + private int createMarketBudgetTotalRow(Sheet sheet, int dataStartRow, int startRowNum) { + int rowNum = startRowNum; + String[] budgetItems = {"材料费", "人工费", "安装费", "差旅费", "运输费", "售后服务费", "其他费用", "其他成本"}; + + // 合计行 + Row totalRow = sheet.createRow(rowNum++); + totalRow.setHeightInPoints(20); + createMergedCell(sheet, totalRow, 0, 1, "合计", headerStyle); + + // 预算成本合计 + Cell totalBudgetCostCell = totalRow.createCell(2); + totalBudgetCostCell.setCellFormula(String.format("SUM(C%d:C%d)", dataStartRow, dataStartRow + budgetItems.length - 1)); + totalBudgetCostCell.setCellStyle(formulaStyle); + + // 降成本后预算成本合计 + Cell totalReducedCostCell = totalRow.createCell(3); + totalReducedCostCell.setCellFormula(String.format("SUM(D%d:D%d)", dataStartRow, dataStartRow + budgetItems.length - 1)); + totalReducedCostCell.setCellStyle(formulaStyle); + + // 参考项目列 + createCell(totalRow, 4, "", dataStyle); + + // 备注行 + rowNum++; + Row noteRow = sheet.createRow(rowNum); + createCell(noteRow, 0, "注:以上成本均为不含税价格", remarkStyle); + + return rowNum; + } + + /** + * 设置市场预算表列宽 + */ + private void setMarketBudgetSheetColumnWidths(Sheet sheet) { + sheet.setColumnWidth(0, 5000); // 序号 + sheet.setColumnWidth(1, 8000); // 科目名称 + sheet.setColumnWidth(2, 5000); // 预算成本 + sheet.setColumnWidth(3, 6000); // 降成本后预算成本 + sheet.setColumnWidth(4, 4000); // 参考项目 + } + + // =========== 材料费明细表相关方法 =========== + + /** + * 创建市场项目材料费明细表 + */ + protected Integer createMarketMaterialSheet(List details) { + Sheet sheet = workbook.createSheet(SHEET_MATERIAL); + int rowNum = 0; + + // 创建表头 + rowNum = createMaterialSheetHeader(sheet, rowNum); + + // 创建数据行 + int dataStartRow = rowNum + 1; + rowNum = createMaterialDataRows(sheet, details, rowNum); + + // 填充空行 + rowNum = fillMaterialEmptyRows(sheet, details.size(), rowNum, 10); + + // 创建合计行 + rowNum = createMaterialTotalRow(sheet, dataStartRow, rowNum); + + // 设置列宽 + setMaterialSheetColumnWidths(sheet); + + return rowNum; + } + + /** + * 创建材料费表头 + */ + private int createMaterialSheetHeader(Sheet sheet, int startRowNum) { + int rowNum = startRowNum; + + // 表头 + Row row1 = sheet.createRow(rowNum++); + createMergedCell(sheet, row1, 0, 10, "材料费预算明细表", titleStyle); + + Row row2 = sheet.createRow(rowNum++); + createMergedCell(sheet, row2, 0, 3, "基础信息", mergedHeaderStyle); + createMergedCell(sheet, row2, 4, 6, "预算成本", mergedHeaderStyle); + createMergedCell(sheet, row2, 7, 9, "降成本后预算成本", mergedHeaderStyle); + createCell(row2, 10, "降成本方案", mergedHeaderStyle); + + Row headerRow = sheet.createRow(rowNum++); + String[] headers = {"序号", "物料号", "材料名称", "单位", "单价", "购置数量", "金额(元)", + "单价", "购置数量", "金额(元)", ""}; + for (int i = 0; i < headers.length; i++) { + createCell(headerRow, i, headers[i], headerStyle); + } + + return rowNum; + } + + /** + * 创建材料费数据行 + */ + private int createMaterialDataRows(Sheet sheet, List details, int startRowNum) { + int rowNum = startRowNum; + + if (details != null) { + for (int i = 0; i < details.size(); i++) { + ErpBudgetMaterialCost detail = details.get(i); + rowNum = createMaterialDataRow(sheet, detail, rowNum); + } + } + + return rowNum; + } + + /** + * 创建单个材料费数据行 + */ + private int createMaterialDataRow(Sheet sheet, ErpBudgetMaterialCost detail, int rowNum) { + Row dataRow = sheet.createRow(rowNum++); + + // 基础信息 + createCell(dataRow, 0, detail.getSortOrder().toString(), dataStyle); + createCell(dataRow, 1, detail.getMaterialCode(), dataStyle); + createCell(dataRow, 2, detail.getMaterialName(), dataStyle); + createCell(dataRow, 3, detail.getUnitName(), dataStyle); + + // 预算成本 + Double unitPrice = detail.getUnitPrice() != null ? detail.getUnitPrice().doubleValue() : null; + Double amount = detail.getAmount() != null ? detail.getAmount().doubleValue() : null; + createNumericCell(dataRow, 4, unitPrice, dataStyle); + createNumericCell(dataRow, 5, amount, dataStyle); + Cell amountCell1 = dataRow.createCell(6); + amountCell1.setCellFormula("E" + (rowNum) + "*F" + (rowNum)); + amountCell1.setCellStyle(formulaStyle); + + // 降成本后预算成本 + Double reduceUnitPrice = detail.getReduceUnitPrice() != null ? detail.getReduceUnitPrice().doubleValue(): null; + Double reduceAmount = detail.getReduceAmount() != null ? detail.getReduceAmount().doubleValue() : null; + createNumericCell(dataRow, 7, reduceUnitPrice, dataStyle); + createNumericCell(dataRow, 8, reduceAmount, dataStyle); + Cell amountCell2 = dataRow.createCell(9); + amountCell2.setCellFormula("H" + (rowNum) + "*I" + (rowNum)); + amountCell2.setCellStyle(formulaStyle); + + createCell(dataRow, 10, detail.getReduceProposal(), dataStyle); + + return rowNum; + } + + /** + * 填充材料费空行 + */ + private int fillMaterialEmptyRows(Sheet sheet, int dataSize, int startRowNum, int targetSize) { + int rowNum = startRowNum; + int fillRows = targetSize - dataSize; + + while (fillRows > 0) { + Row emptyRow = sheet.createRow(rowNum++); + for (int i = 0; i < 4; i++) { + createCell(emptyRow, i, "", dataStyle); + } + for (int i = 4; i < 6; i++) { + Double value = null; + createNumericCell(emptyRow, i, value, dataStyle); + } + + Cell priceCell = emptyRow.createCell(6); + priceCell.setCellFormula(String.format("E%d*F%d", rowNum, rowNum)); + priceCell.setCellStyle(formulaStyle); + + for (int i = 7; i < 9; i++) { + Double value = null; + createNumericCell(emptyRow, i, value, dataStyle); + } + + Cell reducePriceCell = emptyRow.createCell(9); + reducePriceCell.setCellFormula(String.format("H%d*I%d", rowNum, rowNum)); + reducePriceCell.setCellStyle(formulaStyle); + + createCell(emptyRow, 10, "", dataStyle); + fillRows--; + } + + return rowNum; + } + + + /** + * 创建材料费合计行 + */ + private int createMaterialTotalRow(Sheet sheet, int dataStartRow, int rowNum) { + Row totalRow = sheet.createRow(rowNum); + + createMergedCell(sheet, totalRow, 0, 3, "合计(万元)", headerStyle); + createCell(totalRow, 4, "/", headerStyle); + + Cell totalQuantity1 = totalRow.createCell(5); + totalQuantity1.setCellFormula(String.format("SUM(F%d:F%d)", dataStartRow, rowNum)); + totalQuantity1.setCellStyle(footerFormulaStyle); + + Cell totalAmount1 = totalRow.createCell(6); + totalAmount1.setCellFormula(String.format("SUM(G%d:G%d)/10000", dataStartRow, rowNum)); + totalAmount1.setCellStyle(footerFormulaStyle); + + createCell(totalRow, 7, "/", headerStyle); + + Cell totalQuantity2 = totalRow.createCell(8); + totalQuantity2.setCellFormula(String.format("SUM(I%d:I%d)", dataStartRow, rowNum)); + totalQuantity2.setCellStyle(footerFormulaStyle); + + Cell totalAmount2 = totalRow.createCell(9); + totalAmount2.setCellFormula(String.format("SUM(J%d:J%d)/10000", dataStartRow, rowNum)); + totalAmount2.setCellStyle(footerFormulaStyle); + + createCell(totalRow, 10, "", headerStyle); + + return rowNum; + } + + /** + * 设置材料费表列宽 + */ + private void setMaterialSheetColumnWidths(Sheet sheet) { + for (int i = 0; i < 11; i++) { + sheet.setColumnWidth(i, 4000); + } + } + + // =========== 人工费明细表相关方法 =========== + + /** + * 创建市场项目人工费明细表 + */ + protected Integer createMarketLaborSheet(List budgetDetails) { + Sheet sheet = workbook.createSheet(SHEET_LABOR); + int rowNum = 0; + + // 创建表头 + rowNum = createLaborSheetHeader(sheet, rowNum); + + // 创建数据行 + int dataStartRow = rowNum + 1; + rowNum = createLaborDataRows(sheet, budgetDetails, rowNum); + + // 填充空行 + rowNum = fillLaborEmptyRows(sheet, budgetDetails.size(), rowNum, 10); + + // 创建合计行 + rowNum = createLaborTotalRow(sheet, dataStartRow, rowNum); + + // 创建左侧合并单元格 + createDataRowsWithDynamicMerge(sheet, 1, rowNum, 0, 1, "人工费"); + + return rowNum; + } + + /** + * 创建人工费表头 + */ + private int createLaborSheetHeader(Sheet sheet, int startRowNum) { + int rowNum = startRowNum; + + // 表头 + Row row1 = sheet.createRow(rowNum++); + createMergedCell(sheet, row1, 0, 14, "人工费预算明细表", titleStyle); + + Row row2 = sheet.createRow(rowNum++); + createMergedCell(sheet, row2, 2, 7, "预算成本", mergedHeaderStyle); + createMergedCell(sheet, row2, 8, 13, "降成本后预算成本", mergedHeaderStyle); + createCell(row2, 14, "降成本方案", mergedHeaderStyle); + + Row headerRow = sheet.createRow(rowNum++); + String[] leftHeaders = {"人员类别", "人数", "累计时间(月)", + "月平均投入比例(%)", "人工标准(元/人月)", "金额(万元)", "人员类别", "人数", "累计时间(月)", + "月平均投入比例(%)", "人工标准(元/人月)", "金额(万元)", ""}; + + for (int i = 0; i < leftHeaders.length; i++) { + Cell cell = headerRow.createCell(i + 2); + cell.setCellValue(leftHeaders[i]); + cell.setCellStyle(headerStyle); + } + + // 设置列宽 + int[] widths = {8, 8, 12, 12, 18, 22, 22, 15, 12, 12, 18, 22, 22, 15, 25}; + for (int i = 0; i < widths.length; i++) { + sheet.setColumnWidth(i, widths[i] * 256); + } + + return rowNum; + } + + /** + * 创建人工费数据行 + */ + private int createLaborDataRows(Sheet sheet, List budgetDetails, int startRowNum) { + int rowNum = startRowNum; + + if (budgetDetails != null) { + for (int i = 0; i < budgetDetails.size(); i++) { + ErpBudgetLaborCost detail = budgetDetails.get(i); + rowNum = createLaborDataRow(sheet, detail, rowNum); + } + } + + return rowNum; + } + + /** + * 创建单个人工费数据行 + */ + private int createLaborDataRow(Sheet sheet, ErpBudgetLaborCost detail, int rowNum) { + Row dataRow = sheet.createRow(rowNum++); + + // 预算成本 + Double peopleNumber = detail.getPeopleNumber() != null ? detail.getPeopleNumber().doubleValue() : null; + Double cumulativeTime = detail.getCumulativeTime() != null ? detail.getCumulativeTime().doubleValue() : null; + Double artificialStandard = detail.getArtificialStandard() != null ? detail.getArtificialStandard().doubleValue() : null; + createCell(dataRow, 2, detail.getPersonnelCategory(), dataStyle); + createNumericCell(dataRow, 3, peopleNumber, dataStyle); + createNumericCell(dataRow, 4, cumulativeTime, dataStyle); + createNumericCell(dataRow, 5, detail.getMonthRate() == null ? BigDecimal.ZERO : + detail.getMonthRate().divide(new BigDecimal(100), 2, RoundingMode.HALF_UP), percentStyle); + createNumericCell(dataRow, 6, artificialStandard, dataStyle); + + Cell amountCell = dataRow.createCell(7); + amountCell.setCellFormula("D" + (rowNum) + "*E" + (rowNum) + "*F" + (rowNum) + "*G" + (rowNum) + "/10000"); + amountCell.setCellStyle(formulaStyle); + + // 降成本后预算成本 + Double reducePeopleNumber = detail.getReducePeopleNumber() != null ? detail.getReducePeopleNumber().doubleValue() : null; + Double reduceCumulativeTime = detail.getReduceCumulativeTime() != null ? detail.getReduceCumulativeTime().doubleValue() : null; + Double reduceArtificialStandard = detail.getReduceArtificialStandard() != null ? detail.getReduceArtificialStandard().doubleValue() : null; + createCell(dataRow, 8, detail.getPersonnelCategory(), dataStyle); + createNumericCell(dataRow, 9, reducePeopleNumber, dataStyle); + createNumericCell(dataRow, 10, reduceCumulativeTime, dataStyle); + createNumericCell(dataRow, 11, detail.getReduceMonthRate() == null ? BigDecimal.ZERO : + detail.getReduceMonthRate().divide(new BigDecimal(100), 2, RoundingMode.HALF_UP), percentStyle); + createNumericCell(dataRow, 12, reduceArtificialStandard, dataStyle); + + Cell reduceAmountCell = dataRow.createCell(13); + reduceAmountCell.setCellFormula("J" + (rowNum) + "*K" + (rowNum) + "*L" + (rowNum) + "*M" + (rowNum) + "/10000"); + reduceAmountCell.setCellStyle(formulaStyle); + + // 降成本方案 + createCell(dataRow, 14, detail.getReduceProposal(), dataStyle); + + return rowNum; + } + + /** + * 填充人工费空行 + */ + private int fillLaborEmptyRows(Sheet sheet, int dataSize, int startRowNum, int targetSize) { + int rowNum = startRowNum; + int fillRows = targetSize - dataSize; + + while (fillRows > 0) { + Row emptyRow = sheet.createRow(rowNum++); + + createCell(emptyRow, 2, "", dataStyle); + for (int i = 3; i < 7; i++) { + Double value = null; + createNumericCell(emptyRow, i, value, dataStyle); + } + Cell priceCell = emptyRow.createCell(7); + priceCell.setCellFormula(String.format("D%d*E%d*F%d*G%d/10000", rowNum, rowNum, rowNum, rowNum)); + priceCell.setCellStyle(formulaStyle); + + createCell(emptyRow, 8, "", dataStyle); + for (int i = 9; i < 13; i++) { + Double value = null; + createNumericCell(emptyRow, i, value, dataStyle); + } + + Cell reducePriceCell = emptyRow.createCell(13); + reducePriceCell.setCellFormula(String.format("J%d*K%d*L%d*M%d/10000", rowNum, rowNum, rowNum, rowNum)); + reducePriceCell.setCellStyle(formulaStyle); + + createCell(emptyRow, 14, "", dataStyle); + fillRows--; + } + + return rowNum; + } + + + /** + * 创建人工费合计行 + */ + private int createLaborTotalRow(Sheet sheet, int dataStartRow, int rowNum) { + Row totalRow = sheet.createRow(rowNum); + + createMergedCell(sheet, totalRow, 2, 6, "合计", headerStyle); + + Cell totalCell1 = totalRow.createCell(7); + totalCell1.setCellFormula(String.format("SUM(H%d:H%d)", dataStartRow, rowNum)); + totalCell1.setCellStyle(footerFormulaStyle); + + createCell(totalRow, 8, "", headerStyle); + createCell(totalRow, 9, "", headerStyle); + createCell(totalRow, 10, "", headerStyle); + createCell(totalRow, 11, "", headerStyle); + createCell(totalRow, 12, "", headerStyle); + + Cell totalCell2 = totalRow.createCell(13); + totalCell2.setCellFormula(String.format("SUM(N%d:N%d)", dataStartRow, rowNum)); + totalCell2.setCellStyle(footerFormulaStyle); + + createCell(totalRow, 14, "", dataStyle); + + return rowNum; + } + + // =========== 安装费明细表相关方法 =========== + + /** + * 创建市场项目安装费明细表 + */ + protected Integer createMarketInstallationSheet(List budgetDetails) { + Sheet sheet = workbook.createSheet(SHEET_INSTALLATION); + int rowNum = 0; + + // 创建表头 + rowNum = createInstallationSheetHeader(sheet, rowNum); + + // 创建数据行 + int dataStartRow = rowNum + 1; + rowNum = createInstallationDataRows(sheet, budgetDetails, rowNum); + + // 填充空行 + rowNum = fillInstallationEmptyRows(sheet, budgetDetails.size(), rowNum, 10); + + // 创建合计行 + rowNum = createInstallationTotalRow(sheet, dataStartRow, rowNum); + + // 创建左侧合并单元格 + createDataRowsWithDynamicMerge(sheet, 1, rowNum, 0, 1, "安装费"); + + return rowNum; + } + + /** + * 创建安装费表头(与人工费结构类似,可以复用) + */ + private int createInstallationSheetHeader(Sheet sheet, int startRowNum) { + int rowNum = startRowNum; + + Row row1 = sheet.createRow(rowNum++); + createMergedCell(sheet, row1, 0, 14, "安装费预算明细表", titleStyle); + + Row row2 = sheet.createRow(rowNum++); + createMergedCell(sheet, row2, 2, 7, "预算成本", mergedHeaderStyle); + createMergedCell(sheet, row2, 8, 13, "降成本后预算成本", mergedHeaderStyle); + createCell(row2, 14, "降成本方案", mergedHeaderStyle); + + Row headerRow = sheet.createRow(rowNum++); + String[] headers = {"人员类别", "人数", "累计时间(月)", + "月平均投入比例(%)", "人工标准(元/人月)", "金额(万元)", "人员类别", "人数", "累计时间(月)", + "月平均投入比例(%)", "人工标准(元/人月)", "金额(万元)", ""}; + + for (int i = 0; i < headers.length; i++) { + Cell cell = headerRow.createCell(i + 2); + cell.setCellValue(headers[i]); + cell.setCellStyle(headerStyle); + } + + // 设置列宽 + int[] widths = {8, 8, 12, 12, 18, 22, 22, 15, 12, 12, 18, 22, 22, 15, 25}; + for (int i = 0; i < widths.length; i++) { + sheet.setColumnWidth(i, widths[i] * 256); + } + + return rowNum; + } + + /** + * 创建安装费数据行 + */ + private int createInstallationDataRows(Sheet sheet, List budgetDetails, int startRowNum) { + int rowNum = startRowNum; + + if (budgetDetails != null) { + for (int i = 0; i < budgetDetails.size(); i++) { + ErpBudgetInstallCost detail = budgetDetails.get(i); + rowNum = createInstallationDataRow(sheet, detail, rowNum); + } + } + + return rowNum; + } + + /** + * 创建单个安装费数据行 + */ + private int createInstallationDataRow(Sheet sheet, ErpBudgetInstallCost detail, int rowNum) { + Row dataRow = sheet.createRow(rowNum++); + + // 预算成本 + Double peopleNumber = detail.getPeopleNumber() != null ? detail.getPeopleNumber().doubleValue() : null; + Double cumulativeTime = detail.getCumulativeTime() != null ? detail.getCumulativeTime().doubleValue() : null; + Double artificialStandard = detail.getArtificialStandard() != null ? detail.getArtificialStandard().doubleValue() : null; + createCell(dataRow, 2, detail.getPersonnelCategory(), dataStyle); + createNumericCell(dataRow, 3, peopleNumber, dataStyle); + createNumericCell(dataRow, 4, cumulativeTime, dataStyle); + createNumericCell(dataRow, 5, detail.getMonthRate() == null ? BigDecimal.ZERO : + detail.getMonthRate().divide(new BigDecimal(100), 2, RoundingMode.HALF_UP), percentStyle); + createNumericCell(dataRow, 6, artificialStandard, dataStyle); + + Cell amountCell = dataRow.createCell(7); + amountCell.setCellFormula("D" + (rowNum) + "*E" + (rowNum) + "*F" + (rowNum) + "*G" + (rowNum) + "/10000"); + amountCell.setCellStyle(formulaStyle); + + // 降成本后预算成本 + Double reducePeopleNumber = detail.getReducePeopleNumber() != null ? detail.getReducePeopleNumber().doubleValue() : null; + Double reduceCumulativeTime = detail.getReduceCumulativeTime() != null ? detail.getReduceCumulativeTime().doubleValue() : null; + Double reduceArtificialStandard = detail.getReduceArtificialStandard() != null ? detail.getReduceArtificialStandard().doubleValue() : null; + createCell(dataRow, 8, detail.getPersonnelCategory(), dataStyle); + createNumericCell(dataRow, 9, reducePeopleNumber, dataStyle); + createNumericCell(dataRow, 10, reduceCumulativeTime, dataStyle); + createNumericCell(dataRow, 11, detail.getReduceMonthRate() == null ? BigDecimal.ZERO : + detail.getReduceMonthRate().divide(new BigDecimal(100), 2, RoundingMode.HALF_UP), percentStyle); + createNumericCell(dataRow, 12, reduceArtificialStandard, dataStyle); + + Cell reduceAmountCell = dataRow.createCell(13); + reduceAmountCell.setCellFormula("J" + (rowNum) + "*K" + (rowNum) + "*L" + (rowNum) + "*M" + (rowNum) + "/10000"); + reduceAmountCell.setCellStyle(formulaStyle); + + // 降成本方案 + createCell(dataRow, 14, detail.getReduceProposal(), dataStyle); + + return rowNum; + } + + /** + * 填充安装费空行 + */ + private int fillInstallationEmptyRows(Sheet sheet, int dataSize, int startRowNum, int targetSize) { + int rowNum = startRowNum; + int fillRows = targetSize - dataSize; + + while (fillRows > 0) { + Row emptyRow = sheet.createRow(rowNum++); + + createCell(emptyRow, 2, "", dataStyle); + for (int i = 3; i < 7; i++) { + Double value = null; + createNumericCell(emptyRow, i, value, dataStyle); + } + Cell priceCell = emptyRow.createCell(7); + priceCell.setCellFormula(String.format("D%d*E%d*F%d*G%d/10000", rowNum, rowNum, rowNum, rowNum)); + priceCell.setCellStyle(formulaStyle); + + createCell(emptyRow, 8, "", dataStyle); + for (int i = 9; i < 13; i++) { + Double value = null; + createNumericCell(emptyRow, i, value, dataStyle); + } + + Cell reducePriceCell = emptyRow.createCell(13); + reducePriceCell.setCellFormula(String.format("J%d*K%d*L%d*M%d/10000", rowNum, rowNum, rowNum, rowNum)); + reducePriceCell.setCellStyle(formulaStyle); + + createCell(emptyRow, 14, "", dataStyle); + fillRows--; + } + + return rowNum; + } + + /** + * 创建安装费合计行 + */ + private int createInstallationTotalRow(Sheet sheet, int dataStartRow, int rowNum) { + Row totalRow = sheet.createRow(rowNum); + + createMergedCell(sheet, totalRow, 2, 6, "合计", headerStyle); + + Cell totalCell1 = totalRow.createCell(7); + totalCell1.setCellFormula("SUM(H4:H11)"); + totalCell1.setCellStyle(footerFormulaStyle); + + createCell(totalRow, 8, "", headerStyle); + createCell(totalRow, 9, "", headerStyle); + createCell(totalRow, 10, "", headerStyle); + createCell(totalRow, 11, "", headerStyle); + createCell(totalRow, 12, "", headerStyle); + + Cell totalCell2 = totalRow.createCell(13); + totalCell2.setCellFormula(String.format("SUM(N%d:N%d)", dataStartRow, rowNum)); + totalCell2.setCellStyle(footerFormulaStyle); + + createCell(totalRow, 14, "", dataStyle); + + return rowNum; + } + + // =========== 差旅费明细表相关方法 =========== + + /** + * 创建市场项目差旅费明细表 + */ + protected Integer createMarketTravelSheet(List budgetDetails) { + Sheet sheet = workbook.createSheet(SHEET_TRAVEL); + int rowNum = 0; + + // 创建表头 + rowNum = createTravelSheetHeader(sheet, rowNum); + + // 创建数据行 + int dataStartRow = rowNum + 1; + rowNum = createTravelDataRows(sheet, budgetDetails, rowNum); + + // 填充空行 + rowNum = fillTravelEmptyRows(sheet, budgetDetails.size(), rowNum, 10); + + // 创建合计行 + rowNum = createTravelTotalRow(sheet, dataStartRow, rowNum); + + // 创建左侧合并单元格 + createDataRowsWithDynamicMerge(sheet, 2, rowNum - 1, 0, 0, "差旅费"); + + return rowNum; + } + + /** + * 创建差旅费表头 + */ + private int createTravelSheetHeader(Sheet sheet, int startRowNum) { + int rowNum = startRowNum; + + Row row1 = sheet.createRow(rowNum++); + createMergedCell(sheet, row1, 0, 23, "差旅费预算明细表", titleStyle); + + Row row2 = sheet.createRow(rowNum++); + createMergedCell(sheet, row2, 0, 1, "", mergedHeaderStyle); + createMergedCell(sheet, row2, 2, 11, "预算成本", mergedHeaderStyle); + createMergedCell(sheet, row2, 12, 22, "降成本后预算成本", mergedHeaderStyle); + createCell(row2, 23, "降成本方案", mergedHeaderStyle); + + Row headerRow = sheet.createRow(rowNum++); + String[] headers = {"序号", "出差地点", "事由", "次数", "人数", "天数", "住宿标准(元)", "往返路费(元)", "住宿费", "补贴(元)", "小计(元)", + "序号", "出差地点", "事由", "次数", "人数", "天数", "住宿标准(元)", "往返路费(元)", "住宿费", "补贴(元)", "小计(元)", ""}; + + for (int i = 0; i < headers.length; i++) { + Cell cell = headerRow.createCell(i + 1); + cell.setCellValue(headers[i]); + cell.setCellStyle(headerStyle); + } + + // 设置列宽 + int[] widths = {8, 8, 15, 15, 10, 12, 12, 18, 18, 18, 18, 20, 8, 15, 15, 10, 12, 12, 18, 18, 18, 18, 20, 23}; + for (int i = 0; i < widths.length; i++) { + sheet.setColumnWidth(i, widths[i] * 256); + } + + return rowNum; + } + + /** + * 创建差旅费数据行 + */ + private int createTravelDataRows(Sheet sheet, List budgetDetails, int startRowNum) { + int rowNum = startRowNum; + + if (budgetDetails != null) { + for (int i = 0; i < budgetDetails.size(); i++) { + ErpBudgetTravelCost detail = budgetDetails.get(i); + rowNum = createTravelDataRow(sheet, detail, rowNum); + } + } + + return rowNum; + } + + /** + * 创建单个差旅费数据行 + */ + private int createTravelDataRow(Sheet sheet, ErpBudgetTravelCost detail, int rowNum) { + Row dataRow = sheet.createRow(rowNum++); + + // 预算成本 + Double frequency = detail.getFrequency() != null ? detail.getFrequency().doubleValue() : null; + Double peopleNumber = detail.getPeopleNumber() != null ? detail.getPeopleNumber().doubleValue() : null; + Double days = detail.getDays() != null ? detail.getDays().doubleValue() : null; + Double stayStandard = detail.getStayStandard() != null ? detail.getStayStandard().doubleValue() : null; + Double travelExpenses = detail.getTravelExpenses() != null ? detail.getTravelExpenses().doubleValue() : null; + createCell(dataRow, 1, detail.getSortOrder().toString(), dataStyle); + createCell(dataRow, 2, detail.getTripLocation(), dataStyle); + createCell(dataRow, 3, detail.getReason(), dataStyle); + createNumericCell(dataRow, 4, frequency, dataStyle); + createNumericCell(dataRow, 5, peopleNumber, dataStyle); + createNumericCell(dataRow, 6, days, dataStyle); + createNumericCell(dataRow, 7, stayStandard, dataStyle); + createNumericCell(dataRow, 8, travelExpenses, dataStyle); + + // 公式计算 + Cell accommodationCell = dataRow.createCell(9); + accommodationCell.setCellFormula("H" + (rowNum) + "*G" + (rowNum) + "*F" + (rowNum)); + accommodationCell.setCellStyle(formulaStyle); + + Cell subsidyCell = dataRow.createCell(10); + subsidyCell.setCellFormula("50*G" + (rowNum) + "*F" + (rowNum)); + subsidyCell.setCellStyle(formulaStyle); + + Cell subtotalCell = dataRow.createCell(11); + subtotalCell.setCellFormula("(I" + (rowNum) + "+J" + (rowNum) + "+K" + (rowNum) + ")"); + subtotalCell.setCellStyle(formulaStyle); + + // 降成本后预算成本 + Double reduceFrequency = detail.getReduceFrequency() != null ? detail.getReduceFrequency().doubleValue() : null; + Double reducePeopleNumber = detail.getReducePeopleNumber() != null ? detail.getReducePeopleNumber().doubleValue() : null; + Double reduceDays = detail.getReduceDayNumber() != null ? detail.getReduceDayNumber().doubleValue() : null; + Double reduceStayStandard = detail.getReduceStayStandard() != null ? detail.getReduceStayStandard().doubleValue() : null; + Double reduceTravelExpenses = detail.getReduceTravelExpenses() != null ? detail.getReduceTravelExpenses().doubleValue() : null; + createCell(dataRow, 12, detail.getReduceSortOrder().toString(), dataStyle); + createCell(dataRow, 13, detail.getReduceTripLocation(), dataStyle); + createCell(dataRow, 14, detail.getReduceReason(), dataStyle); + createNumericCell(dataRow, 15, reduceFrequency, dataStyle); + createNumericCell(dataRow, 16, reducePeopleNumber, dataStyle); + createNumericCell(dataRow, 17, reduceDays, dataStyle); + createNumericCell(dataRow, 18, reduceStayStandard, dataStyle); + createNumericCell(dataRow, 19, reduceTravelExpenses, dataStyle); + + // 公式计算 + Cell reduceAccommodationCell = dataRow.createCell(20); + reduceAccommodationCell.setCellFormula("S" + (rowNum) + "*R" + (rowNum) + "*Q" + (rowNum)); + reduceAccommodationCell.setCellStyle(formulaStyle); + + Cell reduceSubsidyCell = dataRow.createCell(21); + reduceSubsidyCell.setCellFormula("50*R" + (rowNum) + "*Q" + (rowNum)); + reduceSubsidyCell.setCellStyle(formulaStyle); + + Cell reduceSubtotalCell = dataRow.createCell(22); + reduceSubtotalCell.setCellFormula("(T" + (rowNum) + "+U" + (rowNum) + "+V" + (rowNum) + ")"); + reduceSubtotalCell.setCellStyle(formulaStyle); + + createCell(dataRow, 23, detail.getReduceProposal(), dataStyle); + + return rowNum; + } + + + /** + * 填充差旅费空行 + */ + private int fillTravelEmptyRows(Sheet sheet, int dataSize, int startRowNum, int targetSize) { + int rowNum = startRowNum; + int fillRows = targetSize - dataSize; + + while (fillRows > 0) { + Row emptyRow = sheet.createRow(rowNum++); + for (int i = 1; i < 4; i++) { + createCell(emptyRow, i, "", dataStyle); + } + + for (int i = 4; i < 9; i++) { + Double value = null; + createNumericCell(emptyRow, i, value, dataStyle); + } + Cell accommodationFeeCell = emptyRow.createCell(9); + accommodationFeeCell.setCellFormula(String.format("F%d*G%d*H%d", rowNum, rowNum, rowNum)); + accommodationFeeCell.setCellStyle(formulaStyle); + Cell subsidyFeeCell = emptyRow.createCell(10); + subsidyFeeCell.setCellFormula(String.format("90*F%d*G%d", rowNum, rowNum)); + subsidyFeeCell.setCellStyle(formulaStyle); + Cell subtotalCell = emptyRow.createCell(11); + subtotalCell.setCellFormula(String.format("I%d+J%d+K%d", rowNum, rowNum, rowNum)); + subtotalCell.setCellStyle(formulaStyle); + + + for (int i = 12; i < 15; i++) { + createCell(emptyRow, i, "", dataStyle); + } + + for (int i = 15; i < 20; i++) { + Double value = null; + createNumericCell(emptyRow, i, value, dataStyle); + } + Cell reduceAccommodationFeeCell = emptyRow.createCell(20); + reduceAccommodationFeeCell.setCellFormula(String.format("Q%d*R%d*S%d", rowNum, rowNum, rowNum)); + reduceAccommodationFeeCell.setCellStyle(formulaStyle); + Cell reduceSubsidyFeeCell = emptyRow.createCell(21); + reduceSubsidyFeeCell.setCellFormula(String.format("90*Q%d*R%d", rowNum, rowNum)); + reduceSubsidyFeeCell.setCellStyle(formulaStyle); + Cell reduceSubtotalCell = emptyRow.createCell(22); + reduceSubtotalCell.setCellFormula(String.format("T%d+U%d+V%d", rowNum, rowNum, rowNum)); + reduceSubtotalCell.setCellStyle(formulaStyle); + + + createCell(emptyRow, 23, "", dataStyle); + fillRows--; + } + + return rowNum; + } + + + /** + * 创建差旅费合计行 + */ + private int createTravelTotalRow(Sheet sheet, int dataStartRow, int rowNum) { + Row totalRow = sheet.createRow(rowNum); + + // 预算成本合计 + createMergedCell(sheet, totalRow, 1, 7, "合计", headerStyle); + + Cell totalRoundTrip1 = totalRow.createCell(8); + totalRoundTrip1.setCellFormula(String.format("SUM(I%d:I%d)", dataStartRow, rowNum)); + totalRoundTrip1.setCellStyle(footerFormulaStyle); + + Cell totalAccommodation1 = totalRow.createCell(9); + totalAccommodation1.setCellFormula(String.format("SUM(J%d:J%d)", dataStartRow, rowNum)); + totalAccommodation1.setCellStyle(footerFormulaStyle); + + Cell totalSubsidy1 = totalRow.createCell(10); + totalSubsidy1.setCellFormula(String.format("SUM(K%d:K%d)", dataStartRow, rowNum)); + totalSubsidy1.setCellStyle(footerFormulaStyle); + + Cell totalSubtotal1 = totalRow.createCell(11); + totalSubtotal1.setCellFormula(String.format("SUM(L%d:L%d)", dataStartRow, rowNum)); + totalSubtotal1.setCellStyle(footerFormulaStyle); + + // 降成本后预算成本合计 + createMergedCell(sheet, totalRow, 12, 18, "合计", headerStyle); + + Cell totalRoundTrip2 = totalRow.createCell(19); + totalRoundTrip2.setCellFormula(String.format("SUM(T%d:T%d)", dataStartRow, rowNum)); + totalRoundTrip2.setCellStyle(footerFormulaStyle); + + Cell totalAccommodation2 = totalRow.createCell(20); + totalAccommodation2.setCellFormula(String.format("SUM(U%d:U%d)", dataStartRow, rowNum)); + totalAccommodation2.setCellStyle(footerFormulaStyle); + + Cell totalSubsidy2 = totalRow.createCell(21); + totalSubsidy2.setCellFormula(String.format("SUM(V%d:V%d)", dataStartRow, rowNum)); + totalSubsidy2.setCellStyle(footerFormulaStyle); + + Cell totalSubtotal2 = totalRow.createCell(22); + totalSubtotal2.setCellFormula(String.format("SUM(W%d:W%d)", dataStartRow, rowNum)); + totalSubtotal2.setCellStyle(footerFormulaStyle); + + createCell(totalRow, 23, "", dataStyle); + + return rowNum; + } + + // =========== 其他费用明细表相关方法 =========== + + /** + * 创建市场项目其他费用明细表 + */ + protected Integer createMarketOtherSheet(List budgetDetails) { + Sheet sheet = workbook.createSheet(SHEET_OTHER); + int rowNum = 0; + + // 创建表头 + rowNum = createOtherSheetHeader(sheet, rowNum); + + // 创建数据行 + int dataStartRow = rowNum + 1; + rowNum = createOtherDataRows(sheet, budgetDetails, rowNum); + + // 填充空行 + rowNum = fillEmptyRow(sheet, rowNum, 0, 5, 11); + + // 创建合计行 + rowNum = createOtherTotalRow(sheet, dataStartRow, rowNum); + + return rowNum; + } + + /** + * 创建其他费用表头 + */ + private int createOtherSheetHeader(Sheet sheet, int startRowNum) { + int rowNum = startRowNum; + + Row row1 = sheet.createRow(rowNum++); + createMergedCell(sheet, row1, 0, 5, "其他费用预算表", titleStyle); + + Row row2 = sheet.createRow(rowNum++); + createMergedCell(sheet, row2, 0, 2, "预算成本", mergedHeaderStyle); + createMergedCell(sheet, row2, 3, 4, "降成本后预算成本", mergedHeaderStyle); + createCell(row2, 5, "降成本方案", mergedHeaderStyle); + + Row headerRow = sheet.createRow(rowNum++); + String[] headers = {"序号", "项目", "金额(元)", "项目", "金额(元)", ""}; + + for (int i = 0; i < headers.length; i++) { + Cell cell = headerRow.createCell(i); + cell.setCellValue(headers[i]); + cell.setCellStyle(headerStyle); + } + + // 设置列宽 + int[] widths = {8, 30, 20, 30, 20, 25}; + for (int i = 0; i < widths.length; i++) { + sheet.setColumnWidth(i, widths[i] * 256); + } + + return rowNum; + } + + /** + * 创建其他费用数据行 + */ + private int createOtherDataRows(Sheet sheet, List budgetDetails, int startRowNum) { + int rowNum = startRowNum; + + if (budgetDetails != null) { + for (int i = 0; i < budgetDetails.size(); i++) { + ErpBudgetOtherCost budgetDetail = budgetDetails.get(i); + rowNum = createOtherDataRow(sheet, budgetDetail, rowNum); + } + } + + return rowNum; + } + + /** + * 创建单个其他费用数据行 + */ + private int createOtherDataRow(Sheet sheet, ErpBudgetOtherCost budgetDetail, int rowNum) { + Row dataRow = sheet.createRow(rowNum++); + + // 预算成本数据 + Double price = budgetDetail.getPrice() != null ? budgetDetail.getPrice().doubleValue() : null; + Double reducePrice = budgetDetail.getReducePrice() != null ? budgetDetail.getReducePrice().doubleValue() : null; + createCell(dataRow, 0, budgetDetail.getSortOrder().toString(), dataStyle); + createCell(dataRow, 1, budgetDetail.getItemDesc(), dataStyle); + createNumericCell(dataRow, 2, price, dataStyle); + createCell(dataRow, 3, budgetDetail.getReduceItemDesc(), dataStyle); + createNumericCell(dataRow, 4, reducePrice, dataStyle); + createCell(dataRow, 5, budgetDetail.getReduceProposal(), dataStyle); + + return rowNum; + } + + /** + * 创建其他费用合计行 + */ + private int createOtherTotalRow(Sheet sheet, int dataStartRow, int rowNum) { + Row totalRow = sheet.createRow(rowNum); + + createMergedCell(sheet, totalRow, 0, 1, "合计", headerStyle); + + Cell totalAmount1 = totalRow.createCell(2); + totalAmount1.setCellFormula(String.format("SUM(C%d:C%d)", dataStartRow, rowNum)); + totalAmount1.setCellStyle(footerFormulaStyle); + + createCell(totalRow, 3, "", headerStyle); + + Cell totalAmount2 = totalRow.createCell(4); + totalAmount2.setCellFormula(String.format("SUM(E%d:E%d)", dataStartRow, rowNum)); + totalAmount2.setCellStyle(footerFormulaStyle); + + createCell(totalRow, 5, "", headerStyle); + + return rowNum; + } + + // =========== 公式引用设置方法 =========== + + /** + * 设置材料费公式引用 + */ + private void setupFormulaForMaterial(Row row, int excelRowNum) { + Integer totalRow = sheetTotalRowMap.get(SHEET_MATERIAL); + if (totalRow != null) { + // 预算成本引用材料费表的G列合计行 + Cell budgetCostCell = row.createCell(2); + budgetCostCell.setCellFormula(String.format("%s!G%d", SHEET_MATERIAL, totalRow + 1)); + budgetCostCell.setCellStyle(formulaStyle); + + // 降成本后预算成本引用材料费表的J列合计行 + Cell reducedCostCell = row.createCell(3); + reducedCostCell.setCellFormula(String.format("%s!J%d", SHEET_MATERIAL, totalRow + 1)); + reducedCostCell.setCellStyle(formulaStyle); + } else { + createCell(row, 2, "", dataStyle); + createCell(row, 3, "", dataStyle); + } + } + + /** + * 设置人工费公式引用 + */ + private void setupFormulaForLabor(Row row, int excelRowNum) { + Integer totalRow = sheetTotalRowMap.get(SHEET_LABOR); + if (totalRow != null) { + Cell budgetCostCell = row.createCell(2); + budgetCostCell.setCellFormula(String.format("%s!H%d", SHEET_LABOR, totalRow + 1)); + budgetCostCell.setCellStyle(formulaStyle); + + Cell reducedCostCell = row.createCell(3); + reducedCostCell.setCellFormula(String.format("%s!N%d", SHEET_LABOR, totalRow + 1)); + reducedCostCell.setCellStyle(formulaStyle); + } else { + createCell(row, 2, "", dataStyle); + createCell(row, 3, "", dataStyle); + } + } + + /** + * 设置安装费公式引用 + */ + private void setupFormulaForInstallation(Row row, int excelRowNum) { + Integer totalRow = sheetTotalRowMap.get(SHEET_INSTALLATION); + if (totalRow != null) { + Cell budgetCostCell = row.createCell(2); + budgetCostCell.setCellFormula(String.format("%s!H%d", SHEET_INSTALLATION, totalRow + 1)); + budgetCostCell.setCellStyle(formulaStyle); + + Cell reducedCostCell = row.createCell(3); + reducedCostCell.setCellFormula(String.format("%s!N%d", SHEET_INSTALLATION, totalRow + 1)); + reducedCostCell.setCellStyle(formulaStyle); + } else { + createCell(row, 2, "", dataStyle); + createCell(row, 3, "", dataStyle); + } + } + + /** + * 设置差旅费公式引用 + */ + private void setupFormulaForTravel(Row row, int excelRowNum) { + Integer totalRow = sheetTotalRowMap.get(SHEET_TRAVEL); + if (totalRow != null) { + // 预算成本(万元)= 差旅费!L列合计/10000 + Cell budgetCostCell = row.createCell(2); + budgetCostCell.setCellFormula(String.format("%s!L%d/10000", SHEET_TRAVEL, totalRow + 1)); + budgetCostCell.setCellStyle(formulaStyle); + + // 降成本后预算成本(万元)= 差旅费!W列合计/10000 + Cell reducedCostCell = row.createCell(3); + reducedCostCell.setCellFormula(String.format("%s!W%d/10000", SHEET_TRAVEL, totalRow + 1)); + reducedCostCell.setCellStyle(formulaStyle); + } else { + createCell(row, 2, "", dataStyle); + createCell(row, 3, "", dataStyle); + } + } + + /** + * 设置其他费用公式引用 + */ + private void setupFormulaForOther(Row row, int excelRowNum) { + Integer totalRow = sheetTotalRowMap.get(SHEET_OTHER); + if (totalRow != null) { + Cell budgetCostCell = row.createCell(2); + budgetCostCell.setCellFormula(String.format("%s!C%d/10000", SHEET_OTHER, totalRow + 1)); + budgetCostCell.setCellStyle(formulaStyle); + + Cell reducedCostCell = row.createCell(3); + reducedCostCell.setCellFormula(String.format("%s!E%d/10000", SHEET_OTHER, totalRow + 1)); + reducedCostCell.setCellStyle(formulaStyle); + } else { + createCell(row, 2, "", dataStyle); + createCell(row, 3, "", dataStyle); + } + } +} diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/excel/RdProjectBudgetExcelExporter.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/excel/RdProjectBudgetExcelExporter.java new file mode 100644 index 00000000..a423a508 --- /dev/null +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/excel/RdProjectBudgetExcelExporter.java @@ -0,0 +1,2320 @@ +package org.dromara.oa.excel; + +import org.apache.poi.ss.usermodel.*; +import org.dromara.oa.erp.domain.*; +import org.dromara.oa.erp.domain.vo.ErpBudgetInfoVo; +import org.dromara.oa.erp.enums.LaborTypeEnum; +import org.dromara.oa.erp.enums.LiteratureTypeEnum; +import org.dromara.oa.erp.enums.RdMaterialTypeEnum; +import org.dromara.oa.erp.enums.TechTypeEnum; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * @Author xins + * @Date 2025/12/9 14:24 + * @Description:研发项目预算Excel导出器 + */ +@Component +public class RdProjectBudgetExcelExporter extends BaseExcelExporter { + + private static final String SHEET_BUDGET = "预算表"; + private static final String SHEET_EQUIPMENT = "设备费"; + private static final String SHEET_MATERIAL = "材料费"; + private static final String SHEET_TRAVEL_MEETING = "会议差旅交流"; + private static final String SHEET_LABOR_SERVICE = "人工劳务咨询"; + private static final String SHEET_DOCUMENT = "资料文献费"; + private static final String SHEET_TESTING = "测试化验费"; + private static final String SHEET_OTHER = "其他"; + + private static final String BUDGET_TRAVEL = "差旅费"; + private static final String BUDGET_MEETING = "会议费"; + private static final String BUDGET_INTERNATIONAL = "国际合作与交流费"; + private static final String BUDGET_TECH = "咨询开发费"; + private static final String BUDGET_LABOR = "人工费"; + private static final String BUDGET_SERVICE = "劳务费"; + + @Override + public byte[] exportToByteArray(ErpBudgetInfoVo budget) throws IOException { + try { + // 创建工作簿和样式 + workbook = createWorkbook(); + createStyles(); + clearTotalRowMap(); + + // 先创建预算表的空表在第一个 + Sheet sheet = workbook.createSheet(SHEET_BUDGET); + + // 创建明细表并记录总计行位置 + createDetailSheets(budget); + + // 创建预算总表(引用明细表的合计行) + createBudgetSheet(sheet, budget); + + // 写入字节数组 + return writeToByteArray(); + + } catch (Exception e) { + throw new RuntimeException("导出研发项目预算失败", e); + } + } + + @Override + public void exportToFile(ErpBudgetInfoVo budget, String filePath) throws IOException { + try { + // 创建工作簿和样式 + workbook = createWorkbook(); + createStyles(); + clearTotalRowMap(); + + // 先创建预算表的空表在第一个 + Sheet sheet = workbook.createSheet(SHEET_BUDGET); + + // 创建明细表并记录总计行位置 + createDetailSheets(budget); + + // 创建预算总表(引用明细表的合计行) + createBudgetSheet(sheet, budget); + + // 写入文件 + writeToFile(filePath); + + } catch (Exception e) { + throw new RuntimeException("导出研发项目预算到文件失败", e); + } + } + + /** + * 创建所有明细表 + */ + private void createDetailSheets(ErpBudgetInfoVo budget) { + // 创建设备费明细表 + Integer equipmentTotalRow = createEquipmentSheet(budget); + addTotalRowMapping(SHEET_EQUIPMENT, equipmentTotalRow); + + // 创建材料费明细表 + Integer materialTotalRow = createMaterialSheet(budget); + addTotalRowMapping(SHEET_MATERIAL, materialTotalRow); + + // 创建差旅费、会议费、国际交流费明细表 + Integer travelMeetingTotalRow = createTravelMeetingSheet(budget); + addTotalRowMapping(SHEET_TRAVEL_MEETING, travelMeetingTotalRow); + + // 创建人工费、劳务费、咨询开发费明细表 + Integer laborServiceTotalRow = createLaborServiceSheet(budget); + addTotalRowMapping(SHEET_LABOR_SERVICE, laborServiceTotalRow); + + // 创建资料/文献费明细表 + Integer documentTotalRow = createDocumentSheet(budget); + addTotalRowMapping(SHEET_DOCUMENT, documentTotalRow); + + // 创建测试化验费明细表 + Integer testingTotalRow = createTestingSheet(budget); + addTotalRowMapping(SHEET_TESTING, testingTotalRow); + + // 创建其他费用明细表 + Integer otherTotalRow = createOtherSheet(budget); + addTotalRowMapping(SHEET_OTHER, otherTotalRow); + } + + // =========== 预算总表相关方法 =========== + + /** + * 创建预算总表 + */ + private void createBudgetSheet(Sheet sheet, ErpBudgetInfoVo budget) { + int rowNum = 0; + + // 创建表头 + rowNum = createBudgetSheetHeader(sheet, budget, rowNum); + + // 创建预算科目数据 + rowNum = createBudgetSubjects(sheet, rowNum); + + // 创建合计行 + rowNum = createBudgetTotalRow(sheet, rowNum); + + // 创建编制审核信息 + rowNum = createBudgetSignInfo(sheet, rowNum, budget); + + // 设置列宽 + setBudgetSheetColumnWidths(sheet); + } + + /** + * 创建预算表头 + */ + private int createBudgetSheetHeader(Sheet sheet, ErpBudgetInfoVo budget, int startRowNum) { + int rowNum = startRowNum; + + // 表头 + Row titleRow1 = sheet.createRow(rowNum++); + createMergedCell(sheet, titleRow1, 0, 2, "表一", titleStyle); + + Row titleRow2 = sheet.createRow(rowNum++); + createMergedCell(sheet, titleRow2, 0, 2, "项目经费预算表", titleStyle); + + // 项目信息 + Row projectRow = sheet.createRow(rowNum++); + createMergedCell(sheet, projectRow, 0, 1, "项目名称:" + (budget.getProjectName() == null ? "" : budget.getProjectName()), formLeftStyle); + createCell(projectRow, 2, "项目号:" + (budget.getProjectCode() == null ? "" : budget.getProjectCode()), formLeftStyle); + + // 预算期间 + Row periodRow = sheet.createRow(rowNum++); + createCell(periodRow, 0, "项目预算期间:", formLeftStyle); + String period = budget.getDuringOperation() == null ? "" : budget.getDuringOperation(); + createCell(periodRow, 1, period, formLeftStyle); + createCell(periodRow, 2, "金额单位:万元", formLeftStyle); + + return rowNum; + } + + /** + * 创建预算科目 + */ + private int createBudgetSubjects(Sheet sheet, int startRowNum) { + int rowNum = startRowNum; + + // 预算科目表头 + Row headerRow = sheet.createRow(rowNum++); + createCell(headerRow, 0, "序号", headerStyle); + createCell(headerRow, 1, "预算科目名称", headerStyle); + createCell(headerRow, 2, "项目经费", headerStyle); + + // 预算科目数据 + String[] budgetItems = { + "设备费", "材料费", "差旅费", "会议费", "国际合作与交流费", + "咨询开发费", "人工费", "劳务费", "资料/文献费", "测试化验费", "其他费用" + }; + + for (int i = 0; i < budgetItems.length; i++) { + Row dataRow = sheet.createRow(rowNum++); + createCell(dataRow, 0, String.valueOf(i + 1), dataStyle); + createCell(dataRow, 1, budgetItems[i], dataStyle); + + setupBudgetSubjectFormula(dataRow, budgetItems[i]); + } + + return rowNum; + } + + /** + * 设置预算科目公式 + */ + private void setupBudgetSubjectFormula(Row row, String subject) { + switch (subject) { + case "设备费": + setupEquipmentFormula(row); + break; + case "材料费": + setupMaterialFormula(row); + break; + case "差旅费": + setupTravelFormula(row); + break; + case "会议费": + setupMeetingFormula(row); + break; + case "国际合作与交流费": + setupInternationalFormula(row); + break; + case "咨询开发费": + setupTechFormula(row); + break; + case "人工费": + setupLaborFormula(row); + break; + case "劳务费": + setupServiceFormula(row); + break; + case "资料/文献费": + setupDocumentFormula(row); + break; + case "测试化验费": + setupTestingFormula(row); + break; + case "其他费用": + setupOtherFormula(row); + break; + default: + createCell(row, 2, "", dataStyle); + } + } + + /** + * 创建预算合计行 + */ + private int createBudgetTotalRow(Sheet sheet, int startRowNum) { + int rowNum = startRowNum; + + Row totalRow = sheet.createRow(rowNum++); + createMergedCell(sheet, totalRow, 0, 1, "合计", headerStyle); + + Cell totalCell = totalRow.createCell(2); + totalCell.setCellFormula("SUM(C6:C16)"); + totalCell.setCellStyle(formulaStyle); + + return rowNum; + } + + /** + * 创建预算编制审核信息 + */ + private int createBudgetSignInfo(Sheet sheet, int startRowNum, ErpBudgetInfoVo budget) { + int rowNum = startRowNum; + + rowNum += 1; + Row signRow1 = sheet.createRow(rowNum++); + createCell(signRow1, 0, "编制(项目经理):", formLeftStyle); + createCell(signRow1, 1, budget.getManagerName() == null ? "" : budget.getManagerName(), formLeftStyle); + + rowNum += 4; + Row signRow2 = sheet.createRow(rowNum); + createCell(signRow2, 0, "审核(评审组长):", formLeftStyle); + createCell(signRow2, 1, budget.getApproveUserName() == null ? "" : budget.getApproveUserName(), formLeftStyle); + + return rowNum; + } + + /** + * 设置预算表列宽 + */ + private void setBudgetSheetColumnWidths(Sheet sheet) { + setColumnWidths(sheet, new int[]{20, 40, 30}); + } + + // =========== 公式设置方法 =========== + + /** + * 设置设备费公式 + */ + private void setupEquipmentFormula(Row row) { + Integer totalRow = getTotalRow(SHEET_EQUIPMENT); + if (totalRow != null) { + Cell cell = row.createCell(2); + cell.setCellFormula(String.format("%s!F%d", SHEET_EQUIPMENT, totalRow + 1)); + cell.setCellStyle(formulaStyle); + } else { + createCell(row, 2, "", dataStyle); + } + } + + /** + * 设置材料费公式 + */ + private void setupMaterialFormula(Row row) { + Integer totalRow = getTotalRow(SHEET_MATERIAL); + if (totalRow != null) { + Cell cell = row.createCell(2); + cell.setCellFormula(String.format("%s!F%d", SHEET_MATERIAL, totalRow + 1)); + cell.setCellStyle(formulaStyle); + } else { + createCell(row, 2, "", dataStyle); + } + } + + /** + * 设置差旅费公式 + */ + private void setupTravelFormula(Row row) { + Integer travelTotalRow = getTotalRow(BUDGET_TRAVEL); + if (travelTotalRow != null) { + Cell cell = row.createCell(2); + cell.setCellFormula(String.format("%s!L%d", SHEET_TRAVEL_MEETING, travelTotalRow)); + cell.setCellStyle(formulaStyle); + } else { + createCell(row, 2, "", dataStyle); + } + } + + /** + * 设置会议费公式 + */ + private void setupMeetingFormula(Row row) { + Integer meetingTotalRow = getTotalRow(BUDGET_MEETING); + if (meetingTotalRow != null) { + Cell cell = row.createCell(2); + cell.setCellFormula(String.format("%s!L%d", SHEET_TRAVEL_MEETING, meetingTotalRow)); + cell.setCellStyle(formulaStyle); + } else { + createCell(row, 2, "", dataStyle); + } + } + + /** + * 设置国际交流费公式 + */ + private void setupInternationalFormula(Row row) { + Integer internationalTotalRow = getTotalRow(BUDGET_INTERNATIONAL); + if (internationalTotalRow != null) { + Cell cell = row.createCell(2); + cell.setCellFormula(String.format("%s!L%d", SHEET_TRAVEL_MEETING, internationalTotalRow)); + cell.setCellStyle(formulaStyle); + } else { + createCell(row, 2, "", dataStyle); + } + } + + /** + * 设置技术咨询费公式 + */ + private void setupTechFormula(Row row) { + Integer techTotalRow = getTotalRow(BUDGET_TECH); + if (techTotalRow != null) { + Cell cell = row.createCell(2); + cell.setCellFormula(String.format("%s!I%d", SHEET_LABOR_SERVICE, techTotalRow)); + cell.setCellStyle(formulaStyle); + } else { + createCell(row, 2, "", dataStyle); + } + } + + /** + * 设置人工费公式 + */ + private void setupLaborFormula(Row row) { + Integer laborTotalRow = getTotalRow(BUDGET_LABOR); + if (laborTotalRow != null) { + Cell cell = row.createCell(2); + cell.setCellFormula(String.format("%s!I%d", SHEET_LABOR_SERVICE, laborTotalRow)); + cell.setCellStyle(formulaStyle); + } else { + createCell(row, 2, "", dataStyle); + } + } + + /** + * 设置劳务费公式 + */ + private void setupServiceFormula(Row row) { + Integer serviceTotalRow = getTotalRow(BUDGET_SERVICE); + if (serviceTotalRow != null) { + Cell cell = row.createCell(2); + cell.setCellFormula(String.format("%s!I%d", SHEET_LABOR_SERVICE, serviceTotalRow)); + cell.setCellStyle(formulaStyle); + } else { + createCell(row, 2, "", dataStyle); + } + } + + /** + * 设置资料文献费公式 + */ + private void setupDocumentFormula(Row row) { + Integer totalRow = getTotalRow(SHEET_DOCUMENT); + if (totalRow != null) { + Cell cell = row.createCell(2); + cell.setCellFormula(String.format("%s!B%d", SHEET_DOCUMENT, totalRow + 1)); + cell.setCellStyle(formulaStyle); + } else { + createCell(row, 2, "", dataStyle); + } + } + + /** + * 设置测试化验费公式 + */ + private void setupTestingFormula(Row row) { + Integer totalRow = getTotalRow(SHEET_TESTING); + if (totalRow != null) { + Cell cell = row.createCell(2); + cell.setCellFormula(String.format("%s!G%d", SHEET_TESTING, totalRow + 1)); + cell.setCellStyle(formulaStyle); + } else { + createCell(row, 2, "", dataStyle); + } + } + + /** + * 设置其他费用公式 + */ + private void setupOtherFormula(Row row) { + Integer totalRow = getTotalRow(SHEET_OTHER); + if (totalRow != null) { + Cell cell = row.createCell(2); + cell.setCellFormula(String.format("%s!C%d", SHEET_OTHER, totalRow + 1)); + cell.setCellStyle(formulaStyle); + } else { + createCell(row, 2, "", dataStyle); + } + } + + // =========== 设备费明细表相关方法 =========== + + /** + * 创建设备费明细表 + */ + private Integer createEquipmentSheet(ErpBudgetInfoVo budget) { + Sheet sheet = workbook.createSheet(SHEET_EQUIPMENT); + int rowNum = 0; + + // 创建表头 + rowNum = createEquipmentSheetHeader(sheet, rowNum); + + // 创建数据行 + int dataStartRow = rowNum + 1; + rowNum = createEquipmentDataRows(sheet, budget, rowNum); + + // 填充空行 + rowNum = fillEquipmentEmptyRow(sheet, rowNum, 0, 5, 11, dataStyle); + + // 创建合计行 + rowNum = createEquipmentTotalRow(sheet, dataStartRow, rowNum); + + // 设置列宽 + setEquipmentSheetColumnWidths(sheet); + + return rowNum; + } + + private int fillEquipmentEmptyRow(Sheet sheet, int rowNum, int startCol, int endCol, int fillRowNum, CellStyle style) { + while (rowNum < fillRowNum) { + Row dataRow = sheet.getRow(rowNum); + if (dataRow == null) { + dataRow = sheet.createRow(rowNum); + } + + for (int i = startCol; i <= endCol - 1; i++) { + Double value = null; + createNumericCell(dataRow, i, value, style); + } + + Cell amountCell = dataRow.createCell(endCol); + amountCell.setCellFormula(String.format("D%d*E%d/10000", rowNum + 1, rowNum + 1)); + amountCell.setCellStyle(formulaStyle); + + rowNum++; + } + return rowNum; + } + + /** + * 创建设备费表头 + */ + private int createEquipmentSheetHeader(Sheet sheet, int startRowNum) { + int rowNum = startRowNum; + + // 表头 + Row titleRow1 = sheet.createRow(rowNum++); + createMergedCell(sheet, titleRow1, 0, 5, "表二", titleStyle); + + Row titleRow2 = sheet.createRow(rowNum++); + createMergedCell(sheet, titleRow2, 0, 5, "购置设备预算明细表", titleStyle); + + // 表头 + Row headerRow = sheet.createRow(rowNum++); + createCell(headerRow, 2, "设备", headerStyle); + createCell(headerRow, 3, "单价", headerStyle); + createCell(headerRow, 4, "数量", headerStyle); + createCell(headerRow, 5, "金额", headerStyle); + + Row headerRow1 = sheet.createRow(rowNum++); + createCell(headerRow1, 2, "型号", headerStyle); + createCell(headerRow1, 3, "(元/台件)", headerStyle); + createCell(headerRow1, 4, "(台件)", headerStyle); + createCell(headerRow1, 5, "(万元)", headerStyle); + + createVerticalMergedCell(sheet, 2, 3, 0, "序号", headerStyle); + createVerticalMergedCell(sheet, 2, 3, 1, "设备名称", headerStyle); + + return rowNum; + } + + /** + * 创建设备费数据行 + */ + private int createEquipmentDataRows(Sheet sheet, ErpBudgetInfoVo budget, int startRowNum) { + int rowNum = startRowNum; + + if (budget.getErpRdBudgetEquipmentCostList() != null) { + for (int i = 0; i < budget.getErpRdBudgetEquipmentCostList().size(); i++) { + var detail = budget.getErpRdBudgetEquipmentCostList().get(i); + rowNum = createEquipmentDataRow(sheet, detail, rowNum); + } + } + + return rowNum; + } + + /** + * 创建单个设备费数据行 + */ + private int createEquipmentDataRow(Sheet sheet, ErpRdBudgetEquipmentCost detail, int rowNum) { + Row dataRow = sheet.createRow(rowNum++); + + createCell(dataRow, 0, detail.getSortOrder().toString(), dataStyle); + createCell(dataRow, 1, detail.getEquipmentName(), dataStyle); + createCell(dataRow, 2, detail.getEquipmentSpec(), dataStyle); + Double unitPrice = detail.getUnitPrice() != null ? detail.getUnitPrice().doubleValue() : null; + createNumericCell(dataRow, 3, unitPrice, dataStyle); + Double amount = detail.getAmount() != null ? detail.getAmount().doubleValue() : null; + createNumericCell(dataRow, 4, amount, dataStyle); + + // 设置金额公式 + Cell amountCell = dataRow.createCell(5); + amountCell.setCellFormula(String.format("D%d*E%d/10000", rowNum, rowNum)); + amountCell.setCellStyle(formulaStyle); + + + return rowNum; + } + + /** + * 创建设备费合计行 + */ + private int createEquipmentTotalRow(Sheet sheet, int dataStartRow, int rowNum) { + Row totalRow = sheet.createRow(rowNum); + + createMergedCell(sheet, totalRow, 0, 1, "合计", headerStyle); + createCell(totalRow, 2, "/", headerStyle); + createCell(totalRow, 3, "/", headerStyle); + createCell(totalRow, 4, "", headerStyle); + + Cell totalCell = totalRow.createCell(5); + totalCell.setCellFormula(String.format("SUM(F%d:F%d)", dataStartRow, rowNum)); + totalCell.setCellStyle(formulaStyle); + + return rowNum; + } + + /** + * 设置设备费表列宽 + */ + private void setEquipmentSheetColumnWidths(Sheet sheet) { + setColumnWidths(sheet, new int[]{15, 30, 20, 20, 20, 20}); + } + + // =========== 材料费明细表相关方法 =========== + + /** + * 创建材料费明细表 + */ + private Integer createMaterialSheet(ErpBudgetInfoVo budget) { + Sheet sheet = workbook.createSheet(SHEET_MATERIAL); + int rowNum = 0; + + List rdBudgetMaterialCostList = prepareMaterialCostData(budget); + List mainMaterialCostList = filterMaterialCostByType(rdBudgetMaterialCostList, RdMaterialTypeEnum.MAIN_MATERIAL.getCode()); + List otherMaterialCostList = filterMaterialCostByType(rdBudgetMaterialCostList, RdMaterialTypeEnum.OTHER_MATERIAL.getCode()); + + + // 创建表头 + rowNum = createMaterialSheetHeader(sheet, rowNum); + + // 创建数据行 + int dataStartRow = rowNum + 1; + rowNum = createMaterialDataRows(sheet, mainMaterialCostList, rowNum); + + // 填充空行 + rowNum = fillMaterialEmptyRow(sheet, rowNum, 0, 5, 11, dataStyle); + + // 创建小计行 + rowNum = createMaterialSubtotalRows(sheet, dataStartRow, rowNum, otherMaterialCostList); + + // 设置列宽 + setMaterialSheetColumnWidths(sheet); + + return rowNum; + } + + /** + * 准备技术服务费数据 + */ + private List prepareMaterialCostData(ErpBudgetInfoVo budget) { + List budgetMaterialCostList = budget.getErpRdBudgetMaterialCostList() == null ? + new ArrayList<>() : budget.getErpRdBudgetMaterialCostList(); + return budgetMaterialCostList; + } + + /** + * 按类型筛选技术服务费数据 + */ + private List filterMaterialCostByType(List list, String type) { + return list.stream() + .filter(item -> item.getMaterialType().equals(type)) + .toList(); + } + + + private int fillMaterialEmptyRow(Sheet sheet, int rowNum, int startCol, int endCol, int fillRowNum, CellStyle style) { + while (rowNum < fillRowNum) { + Row dataRow = sheet.getRow(rowNum); + if (dataRow == null) { + dataRow = sheet.createRow(rowNum); + } + + for (int i = startCol; i <= endCol - 1; i++) { + Double value = null; + createNumericCell(dataRow, i, value, style); + } + + Cell amountCell = dataRow.createCell(endCol); + amountCell.setCellFormula(String.format("D%d*E%d/10000", rowNum + 1, rowNum + 1)); + amountCell.setCellStyle(formulaStyle); + + rowNum++; + } + return rowNum; + } + + /** + * 创建材料费表头 + */ + private int createMaterialSheetHeader(Sheet sheet, int startRowNum) { + int rowNum = startRowNum; + + // 表头 + Row row1 = sheet.createRow(rowNum++); + createMergedCell(sheet, row1, 0, 5, "表三", titleStyle); + + Row row2 = sheet.createRow(rowNum++); + createMergedCell(sheet, row2, 0, 5, "材料费预算明细表", titleStyle); + + // 表头 + Row headerRow = sheet.createRow(rowNum++); + createCell(headerRow, 3, "单价", headerStyle); + + Row headerRow1 = sheet.createRow(rowNum++); + createCell(headerRow1, 3, "(元/单位数量)", headerStyle); + + createVerticalMergedCell(sheet, 2, 3, 0, "序号", headerStyle); + createVerticalMergedCell(sheet, 2, 3, 1, "材料名称", headerStyle); + createVerticalMergedCell(sheet, 2, 3, 2, "单位", headerStyle); + createVerticalMergedCell(sheet, 2, 3, 4, "购置数量", headerStyle); + createVerticalMergedCell(sheet, 2, 3, 5, "金额(万元)", headerStyle); + + return rowNum; + } + + /** + * 创建材料费数据行 + */ + private int createMaterialDataRows(Sheet sheet, List erpRdBudgetMaterialCostList, int startRowNum) { + int rowNum = startRowNum; + + for (int i = 0; i < erpRdBudgetMaterialCostList.size(); i++) { + ErpRdBudgetMaterialCost detail = erpRdBudgetMaterialCostList.get(i); + rowNum = createMaterialDataRow(sheet, detail, rowNum); + } + + return rowNum; + } + + /** + * 创建单个材料费数据行 + */ + private int createMaterialDataRow(Sheet sheet, ErpRdBudgetMaterialCost detail, int rowNum) { + Row dataRow = sheet.createRow(rowNum++); + + createCell(dataRow, 0, detail.getSortOrder().toString(), dataStyle); + createCell(dataRow, 1, detail.getMaterialName(), dataStyle); + createCell(dataRow, 2, detail.getUnitName(), dataStyle); + Double unitPrice = detail.getUnitPrice() != null ? detail.getUnitPrice().doubleValue() : null; + createNumericCell(dataRow, 3, unitPrice, dataStyle); + Double amount = detail.getAmount() != null ? detail.getAmount().doubleValue() : null; + createNumericCell(dataRow, 4, amount, dataStyle); + + // 设置金额公式 + Cell amountCell = dataRow.createCell(5); + amountCell.setCellFormula("D" + (rowNum) + "*E" + (rowNum) + "/10000"); + amountCell.setCellStyle(formulaStyle); + + return rowNum; + } + + /** + * 创建材料费小计行 + */ + private int createMaterialSubtotalRows(Sheet sheet, int dataStartRow, int startRowNum, List otherMaterialCostList) { + int rowNum = startRowNum; + + ErpRdBudgetMaterialCost otherMaterialCost = !otherMaterialCostList.isEmpty() ? otherMaterialCostList.get(0) : new ErpRdBudgetMaterialCost(); + + // 主要材料费小计 + Row subtotalRow1 = sheet.createRow(rowNum++); + createMergedCell(sheet, subtotalRow1, 0, 1, "主要材料费小计", headerStyle); + createCell(subtotalRow1, 2, "/", headerStyle); + createCell(subtotalRow1, 3, "/", headerStyle); + createCell(subtotalRow1, 4, "/", headerStyle); + + Cell subtotalCell1 = subtotalRow1.createCell(5); + subtotalCell1.setCellFormula(String.format("SUM(F%d:F%d)", dataStartRow, rowNum - 1)); + subtotalCell1.setCellStyle(formulaStyle); + + // 其他材料费 + Row subtotalRow2 = sheet.createRow(rowNum++); + createMergedCell(sheet, subtotalRow2, 0, 1, "其他材料费", headerStyle); + createCell(subtotalRow2, 2, "/", headerStyle); + createCell(subtotalRow2, 3, "/", headerStyle); + createCell(subtotalRow2, 4, "/", headerStyle); + createNumericCell(subtotalRow2, 5, otherMaterialCost.getPrice() == null ? null : otherMaterialCost.getPrice().divide(new BigDecimal(10000)), moneyStyle); + + // 合计 + Row totalRow = sheet.createRow(rowNum); + createMergedCell(sheet, totalRow, 0, 1, "合计", headerStyle); + createCell(totalRow, 2, "/", headerStyle); + createCell(totalRow, 3, "/", headerStyle); + createCell(totalRow, 4, "/", headerStyle); + + Cell totalCell = totalRow.createCell(5); + totalCell.setCellFormula(String.format("SUM(F%d:F%d)", rowNum - 1, rowNum)); + totalCell.setCellStyle(formulaStyle); + + return rowNum; + } + + /** + * 设置材料费表列宽 + */ + private void setMaterialSheetColumnWidths(Sheet sheet) { + setColumnWidths(sheet, new int[]{20, 30, 30, 30, 30, 30}); + } + + // =========== 差旅费、会议费、国际交流费明细表相关方法 =========== + + /** + * 创建差旅费、会议费、国际交流费明细表 + */ + private Integer createTravelMeetingSheet(ErpBudgetInfoVo budget) { + Sheet sheet = workbook.createSheet(SHEET_TRAVEL_MEETING); + int rowNum = 0; + + // 创建差旅费部分 + rowNum = createTravelSection(sheet, budget, rowNum); + addTotalRowMapping(BUDGET_TRAVEL, rowNum); + + // 创建会议费部分 + rowNum = createMeetingSection(sheet, budget, rowNum); + addTotalRowMapping(BUDGET_MEETING, rowNum); + + // 创建国际交流费部分 + rowNum = createInternationalSection(sheet, budget, rowNum); + addTotalRowMapping(BUDGET_INTERNATIONAL, rowNum + 1); + + // 设置列宽 + setTravelMeetingSheetColumnWidths(sheet); + + return rowNum; + } + + /** + * 创建差旅费部分 + */ + private int createTravelSection(Sheet sheet, ErpBudgetInfoVo budget, int startRowNum) { + int rowNum = startRowNum; + + // 表头 + Row row1 = sheet.createRow(rowNum++); + createMergedCell(sheet, row1, 0, 11, "表四", titleStyle); + + Row row2 = sheet.createRow(rowNum++); + createMergedCell(sheet, row2, 0, 11, "差旅费预算明细表", titleStyle); + + Row travelHeaderRow = sheet.createRow(rowNum++); + createCell(travelHeaderRow, 0, "差旅费", headerStyle); + String[] travelHeaders = {"序号", "出差地点", "事由", "次数", "人数", "天数", "住宿标准(元)", "往返路费(元)", "住宿费(元)", "补贴(元)", "小计(万元)"}; + for (int i = 0; i < travelHeaders.length; i++) { + createCell(travelHeaderRow, i + 1, travelHeaders[i], headerStyle); + } + + // 数据行 + int travelDataStartRow = rowNum + 1; + rowNum = createTravelDataRows(sheet, budget, rowNum); + + // 填充空行 + rowNum = createEmptyTravelRow(sheet, rowNum, budget); + + // 差旅费合计 + rowNum = createTravelTotalRow(sheet, travelDataStartRow, rowNum); + + // 创建左侧合并单元格 + createDataRowsWithDynamicMerge(sheet, 2, rowNum - 2, 0, 0, "差旅费"); + + return rowNum; + } + + /** + * 创建差旅费数据行 + */ + private int createTravelDataRows(Sheet sheet, ErpBudgetInfoVo budget, int startRowNum) { + int rowNum = startRowNum; + + if (budget.getErpRdBudgetTravelCostList() != null) { + for (int i = 0; i < budget.getErpRdBudgetTravelCostList().size(); i++) { + ErpRdBudgetTravelCost detail = budget.getErpRdBudgetTravelCostList().get(i); + rowNum = createTravelDataRow(sheet, detail, rowNum); + } + } + + return rowNum; + } + + /** + * 创建单个差旅费数据行 + */ + private int createTravelDataRow(Sheet sheet, ErpRdBudgetTravelCost detail, int rowNum) { + Row dataRow = sheet.createRow(rowNum++); + + createCell(dataRow, 0, "", dataStyle); + createCell(dataRow, 1, detail.getSortOrder().toString(), dataStyle); + createCell(dataRow, 2, detail.getTripLocation(), dataStyle); + createCell(dataRow, 3, detail.getReason(), dataStyle); + createNumericCell(dataRow, 4, detail.getFrequency() == null ? null : detail.getFrequency().doubleValue(), dataStyle); + createNumericCell(dataRow, 5, detail.getPeopleNumber() == null ? null : detail.getPeopleNumber().doubleValue(), dataStyle); + createNumericCell(dataRow, 6, detail.getDays(), dataStyle); + createNumericCell(dataRow, 7, detail.getStayStandard(), dataStyle); + createNumericCell(dataRow, 8, detail.getTravelExpenses(), dataStyle); + + // 公式计算 + createTravelFormulas(dataRow, rowNum); + + return rowNum; + } + + /** + * 创建差旅费公式 + */ + private void createTravelFormulas(Row row, int rowNum) { + Cell accommodationCell = row.createCell(9); + accommodationCell.setCellFormula(String.format("H%d*G%d*F%d", rowNum, rowNum, rowNum)); + accommodationCell.setCellStyle(formulaStyle); + + Cell subsidyCell = row.createCell(10); + subsidyCell.setCellFormula(String.format("F%d*G%d*90", rowNum, rowNum)); + subsidyCell.setCellStyle(formulaStyle); + + Cell subtotalCell = row.createCell(11); + subtotalCell.setCellFormula("SUM(I" + rowNum + ":K" + rowNum + ")/10000"); + subtotalCell.setCellStyle(formulaStyle); + } + + /** + * 创建空差旅费行 + */ + private int createEmptyTravelRow(Sheet sheet, int rowNum, ErpBudgetInfoVo budget) { + List erpRdBudgetTravelCostList = budget.getErpRdBudgetTravelCostList() == null ? new ArrayList<>() : + budget.getErpRdBudgetTravelCostList(); + + int rdBudgetTravelFillRowNum = rowNum + (7 - erpRdBudgetTravelCostList.size()); + while (rowNum <= rdBudgetTravelFillRowNum) { + Row emptyRow = sheet.createRow(rowNum++); + for (int i = 1; i < 4; i++) { + createCell(emptyRow, i, "", dataStyle); + } + for (int i = 4; i < 12; i++) { + Double value = null; + createNumericCell(emptyRow, i, value, dataStyle); + } + createTravelFormulas(emptyRow, rowNum); + } + + return rowNum; + } + + /** + * 创建差旅费合计行 + */ + private int createTravelTotalRow(Sheet sheet, int dataStartRow, int rowNum) { + Row travelTotalRow = sheet.createRow(rowNum++); + + createMergedCell(sheet, travelTotalRow, 1, 7, "合计", headerStyle); + createCell(travelTotalRow, 8, "", headerStyle); + createCell(travelTotalRow, 9, "", headerStyle); + createCell(travelTotalRow, 10, "", headerStyle); + + Cell travelTotalCell = travelTotalRow.createCell(11); + travelTotalCell.setCellFormula(String.format("SUM(L%d:L%d)", dataStartRow, rowNum - 1)); + travelTotalCell.setCellStyle(formulaStyle); + + return rowNum; + } + + /** + * 创建会议费部分 + */ + private int createMeetingSection(Sheet sheet, ErpBudgetInfoVo budget, int startRowNum) { + int rowNum = startRowNum; + + // 会议费部分 + Row meetingTitleRow = sheet.createRow(rowNum++); + createMergedCell(sheet, meetingTitleRow, 0, 11, "会议费预算明细表", titleStyle); + + int meetingStartRow = rowNum; + + Row meetingHeaderRow1 = sheet.createRow(rowNum++); + createMergedCell(sheet, meetingHeaderRow1, 1, 8, "公司组织会议", headerStyle); + createMergedCell(sheet, meetingHeaderRow1, 9, 10, "外出参加会议", headerStyle); + + Row meetingHeaderRow2 = sheet.createRow(rowNum++); + createCell(meetingHeaderRow2, 0, "会议费", headerStyle); + String[] meetingHeaders = {"序号", "会议内容", "", "", "场地日租金(元)", "日均杂费(元)", "天数", "专家交通住宿费", "人数", "人均交费", ""}; + for (int i = 0; i < meetingHeaders.length; i++) { + createCell(meetingHeaderRow2, i + 1, meetingHeaders[i], headerStyle); + } + + // 合并单元格 + createMergedCell(sheet, meetingHeaderRow2, 2, 4, "会议内容", headerStyle); + createVerticalMergedCell(sheet, meetingStartRow, meetingStartRow + 1, 11, "会议费(万元)", headerStyle); + + int meetingDataStartRow = rowNum + 1; + // 数据行 + rowNum = createMeetingDataRows(sheet, budget, rowNum); + + // 填充空行 + rowNum = createEmptyMeetingRow(sheet, rowNum, budget); + + // 会议费合计 + rowNum = createMeetingTotalRow(sheet, meetingDataStartRow, rowNum); + + // 创建左侧合并单元格 + createDataRowsWithDynamicMerge(sheet, meetingStartRow, rowNum - meetingStartRow, 0, 0, "会议费"); + + + return rowNum; + } + + /** + * 创建会议费数据行 + */ + private int createMeetingDataRows(Sheet sheet, ErpBudgetInfoVo budget, int startRowNum) { + int rowNum = startRowNum; + + if (budget.getErpRdBudgetMeetingCostList() != null) { + for (int i = 0; i < budget.getErpRdBudgetMeetingCostList().size(); i++) { + ErpRdBudgetMeetingCost detail = budget.getErpRdBudgetMeetingCostList().get(i); + rowNum = createMeetingDataRow(sheet, detail, rowNum); + } + } + + return rowNum; + } + + /** + * 创建单个会议费数据行 + */ + private int createMeetingDataRow(Sheet sheet, ErpRdBudgetMeetingCost detail, int rowNum) { + Row dataRow = sheet.createRow(rowNum++); + + createCell(dataRow, 0, "", dataStyle); + createCell(dataRow, 1, detail.getSortOrder().toString(), dataStyle); + createMergedCell(sheet, dataRow, 2, 4, detail.getMeetingContent(), dataStyle); + Double rentalFee = detail.getRentalFee() != null ? detail.getRentalFee().doubleValue() : null; + createNumericCell(dataRow, 5, rentalFee, dataStyle); + Double dailyExpense = detail.getDailyExpense() != null ? detail.getDailyExpense().doubleValue() : null; + createNumericCell(dataRow, 6, dailyExpense, dataStyle); + Double days = detail.getDays() != null ? detail.getDays().doubleValue() : null; + createNumericCell(dataRow, 7, days, dataStyle); + Double expertExpense = detail.getExpertExpense() != null ? detail.getExpertExpense().doubleValue() : null; + createNumericCell(dataRow, 8, expertExpense, dataStyle); + Double peopleNumber = detail.getPeopleNumber() == null ? null : detail.getPeopleNumber().doubleValue(); + createNumericCell(dataRow, 9, peopleNumber, dataStyle); + Double perPersonExpense = detail.getPerPersonExpense() != null ? detail.getPerPersonExpense().doubleValue() : null; + createNumericCell(dataRow, 10, perPersonExpense, dataStyle); + + // 会议费公式 + Cell meetingCostCell = dataRow.createCell(11); + meetingCostCell.setCellFormula("ROUND(((N(F" + rowNum + ")+N(G" + rowNum + "))*N(H" + rowNum + + ")+N(I" + rowNum + + ")+N(J" + rowNum + ")*N(K" + rowNum + ")" + + ")/10000,2)"); + meetingCostCell.setCellStyle(formulaStyle); + + return rowNum; + } + + /** + * 创建空会议费行 + */ + private int createEmptyMeetingRow(Sheet sheet, int rowNum, ErpBudgetInfoVo budget) { + List erpRdBudgetMeetingCostList = budget.getErpRdBudgetMeetingCostList() == null ? new ArrayList<>() : + budget.getErpRdBudgetMeetingCostList(); + + int rdBudgetMeetingFillRowNum = rowNum + (4 - erpRdBudgetMeetingCostList.size()); + while (rowNum <= rdBudgetMeetingFillRowNum) { + Row emptyRow = sheet.createRow(rowNum++); + createCell(emptyRow, 1, "", dataStyle); + createMergedCell(sheet, emptyRow, 2, 4, "", dataStyle); + for (int i = 5; i < 11; i++) { + Double value = null; + createNumericCell(emptyRow, i, value, dataStyle); + } + Cell meetingCostCell = emptyRow.createCell(11); + meetingCostCell.setCellFormula("ROUND(((F" + (rowNum) + "+G" + (rowNum) + ")*H" + (rowNum) + "+I" + (rowNum) + "+J" + (rowNum) + "*K" + (rowNum) + ")/10000,2)"); + meetingCostCell.setCellStyle(formulaStyle); + } + + return rowNum; + } + + /** + * 创建会议费合计行 + */ + private int createMeetingTotalRow(Sheet sheet, int dataStartRow, int rowNum) { + Row meetingTotalRow = sheet.createRow(rowNum++); + + createMergedCell(sheet, meetingTotalRow, 1, 10, "合计", headerStyle); + + Cell meetingTotalCell = meetingTotalRow.createCell(11); + meetingTotalCell.setCellFormula(String.format("SUM(L%d:L%d)", dataStartRow, rowNum - 1)); + meetingTotalCell.setCellStyle(formulaStyle); + + return rowNum; + } + + /** + * 创建国际交流费部分 + */ + private int createInternationalSection(Sheet sheet, ErpBudgetInfoVo budget, int startRowNum) { + int rowNum = startRowNum; + + // 国际交流费部分 + Row internationalTitleRow = sheet.createRow(rowNum++); + createMergedCell(sheet, internationalTitleRow, 0, 11, "国际交流费预算明细表", titleStyle); + + int internationalStartRow = rowNum; + + Row internationalHeaderRow = sheet.createRow(rowNum++); + createCell(internationalHeaderRow, 0, "国际合作与交流费", headerStyle); + String[] internationalHeaders = {"序号", "合作交流类型", "国家和地区", "", "机构", "", "人数(人)", "时间(天)", "往返路费及住宿费", "补贴(元)", "小计(万元)"}; + for (int i = 0; i < internationalHeaders.length; i++) { + createCell(internationalHeaderRow, i + 1, internationalHeaders[i], headerStyle); + } + + // 合并单元格 + createMergedCell(sheet, internationalHeaderRow, 3, 4, "国家和地区", headerStyle); + createMergedCell(sheet, internationalHeaderRow, 5, 6, "机构", headerStyle); + + int internationalDataStartRow = rowNum + 1; + // 数据行 + rowNum = createInternationalDataRows(sheet, budget, rowNum); + + // 填充空行 + rowNum = createEmptyInternationalRow(sheet, rowNum, budget); + + // 国际交流费合计 + rowNum = createInternationalTotalRow(sheet, internationalDataStartRow, rowNum); + + // 创建左侧合并单元格 + createDataRowsWithDynamicMerge(sheet, internationalStartRow, rowNum - internationalStartRow + 1, 0, 0, "国际合作与交流费"); + + return rowNum; + } + + /** + * 创建国际交流费数据行 + */ + private int createInternationalDataRows(Sheet sheet, ErpBudgetInfoVo budget, int startRowNum) { + int rowNum = startRowNum; + + if (budget.getErpRdBudgetExchangeCostList() != null) { + for (int i = 0; i < budget.getErpRdBudgetExchangeCostList().size(); i++) { + ErpRdBudgetExchangeCost detail = budget.getErpRdBudgetExchangeCostList().get(i); + rowNum = createInternationalDataRow(sheet, detail, rowNum); + } + } + + return rowNum; + } + + /** + * 创建单个国际交流费数据行 + */ + private int createInternationalDataRow(Sheet sheet, ErpRdBudgetExchangeCost detail, int rowNum) { + Row dataRow = sheet.createRow(rowNum++); + + createCell(dataRow, 0, "", dataStyle); + createCell(dataRow, 1, detail.getSortOrder().toString(), dataStyle); + createCell(dataRow, 2, detail.getCommunicationType(), dataStyle); + createMergedCell(sheet, dataRow, 3, 4, detail.getCountryRegion(), dataStyle); + createMergedCell(sheet, dataRow, 5, 6, detail.getInstitution(), dataStyle); + createNumericCell(dataRow, 7, detail.getPeopleNumber() == null ? null : detail.getPeopleNumber().doubleValue(), dataStyle); + createNumericCell(dataRow, 8, detail.getDays(), dataStyle); + createNumericCell(dataRow, 9, detail.getTravelAccommodationExpense(), dataStyle); + + // 补贴公式 + Cell subsidyCell = dataRow.createCell(10); + subsidyCell.setCellFormula("IF(C" + (rowNum) + "=\"出国考察\",40*I" + (rowNum) + "*H" + (rowNum) + ",0)"); + subsidyCell.setCellStyle(formulaStyle); + + // 小计公式 + Cell subtotalCell = dataRow.createCell(11); + subtotalCell.setCellFormula("ROUND((K" + (rowNum) + "+J" + (rowNum) + ")/10000,2)"); + subtotalCell.setCellStyle(formulaStyle); + + return rowNum; + } + + /** + * 创建空国际交流费行 + */ + private int createEmptyInternationalRow(Sheet sheet, int rowNum, ErpBudgetInfoVo budget) { + List erpRdBudgetExchangeCostList = budget.getErpRdBudgetExchangeCostList() == null ? new ArrayList<>() : + budget.getErpRdBudgetExchangeCostList(); + + int rdBudgetInternationalFillRowNum = rowNum + (4 - erpRdBudgetExchangeCostList.size()); + while (rowNum <= rdBudgetInternationalFillRowNum) { + Row emptyRow = sheet.createRow(rowNum++); + createCell(emptyRow, 1, "", dataStyle); + createCell(emptyRow, 2, "", dataStyle); + createMergedCell(sheet, emptyRow, 3, 4, "", dataStyle); + createMergedCell(sheet, emptyRow, 5, 6, "", dataStyle); + for (int i = 7; i < 10; i++) { + Double value = null; + createNumericCell(emptyRow, i, value, dataStyle); + } + Cell subsidyCell = emptyRow.createCell(10); + subsidyCell.setCellFormula("IF(C" + (rowNum) + "=\"出国考察\",40*I" + (rowNum) + "*H" + (rowNum) + ",0)"); + subsidyCell.setCellStyle(formulaStyle); + + Cell subtotalCell = emptyRow.createCell(11); + subtotalCell.setCellFormula("ROUND((K" + (rowNum) + "+J" + (rowNum) + ")/10000,2)"); + subtotalCell.setCellStyle(formulaStyle); + } + + return rowNum; + } + + /** + * 创建国际交流费合计行 + */ + private int createInternationalTotalRow(Sheet sheet, int internationalDataStartRow, int rowNum) { + Row internationalTotalRow = sheet.createRow(rowNum); + + createMergedCell(sheet, internationalTotalRow, 1, 10, "合计", headerStyle); + + Cell internationalTotalCell = internationalTotalRow.createCell(11); + internationalTotalCell.setCellFormula(String.format("SUM(L%d:L%d)", internationalDataStartRow, rowNum)); + internationalTotalCell.setCellStyle(formulaStyle); + + return rowNum; + } + + /** + * 设置差旅会议交流表列宽 + */ + private void setTravelMeetingSheetColumnWidths(Sheet sheet) { + setColumnWidths(sheet, new int[]{20, 15, 20, 20, 15, 20, 15, 20, 20, 20, 20, 20, 20}); + } + + // =========== 人工费、劳务费、咨询开发费明细表相关方法 =========== + + /** + * 创建人工费、劳务费、咨询开发费明细表 + */ + private Integer createLaborServiceSheet(ErpBudgetInfoVo budget) { + Sheet sheet = workbook.createSheet(SHEET_LABOR_SERVICE); + + // 准备数据 + List budgetTechCostList = prepareTechCostData(budget); + List budgetLaborCostList = prepareLaborCostData(budget); + + int rowNum = 0; + + // 创建技术服务费部分 + rowNum = createTechConsultSection(sheet, budgetTechCostList, rowNum); + addTotalRowMapping(BUDGET_TECH, rowNum); + + // 创建人工费部分 + rowNum = createLaborCostSection(sheet, budgetLaborCostList, rowNum); + addTotalRowMapping(BUDGET_LABOR, rowNum); + + // 创建劳务费部分 + rowNum = createServiceCostSection(sheet, budgetLaborCostList, rowNum); + addTotalRowMapping(BUDGET_SERVICE, rowNum + 1); + + // 设置列宽 + setLaborServiceSheetColumnWidths(sheet); + + return rowNum; + } + + /** + * 准备技术服务费数据 + */ + private List prepareTechCostData(ErpBudgetInfoVo budget) { + List budgetTechCostList = budget.getErpRdBudgetTechCostList() == null ? + new ArrayList<>() : budget.getErpRdBudgetTechCostList(); + return budgetTechCostList; + } + + /** + * 准备人工费劳务费数据 + */ + private List prepareLaborCostData(ErpBudgetInfoVo budget) { + List budgetLaborCostList = budget.getErpRdBudgetLaborCostList() == null ? + new ArrayList<>() : budget.getErpRdBudgetLaborCostList(); + return budgetLaborCostList; + } + + /** + * 创建技术服务费部分 + */ + private int createTechConsultSection(Sheet sheet, List budgetTechCostList, int startRowNum) { + int rowNum = startRowNum; + + // 表头 + Row row1 = sheet.createRow(rowNum++); + createMergedCell(sheet, row1, 0, 8, "表五", titleStyle); + + Row row2 = sheet.createRow(rowNum++); + createMergedCell(sheet, row2, 0, 8, "技术服务费预算明细表", titleStyle); + + // 分组数据 + List techConsultList = filterTechCostByType(budgetTechCostList, TechTypeEnum.TECH_CONSULT.getCode()); + List expertMeetingList = filterTechCostByType(budgetTechCostList, TechTypeEnum.EXPERT_MEETING.getCode()); + List expertCommunicationList = filterTechCostByType(budgetTechCostList, TechTypeEnum.EXPERT_COMMUNICATION.getCode()); + + // 技术咨询开发部分 + int techConsultStartRow = rowNum; + rowNum = createTechConsultSubsection(sheet, techConsultList, rowNum, "技术咨询开发", 1); + int techConsultTotalRowNum = rowNum; + + // 专家咨询-会议形式部分 + int expertMeetingStartRow = rowNum; + rowNum = createExpertMeetingSubsection(sheet, expertMeetingList, rowNum); + int expertMeetingTotalRowNum = rowNum; + + // 专家咨询-通讯形式部分 + int expertCommunicationStartRow = rowNum; + rowNum = createExpertCommunicationSubsection(sheet, expertCommunicationList, rowNum); + int expertCommunicationTotalRowNum = rowNum; + + // 合并左侧单元格 + createVerticalMergedCell(sheet, expertMeetingStartRow, rowNum - 1, 1, "专家咨询", leftMergeStyle); + createVerticalMergedCell(sheet, techConsultStartRow, rowNum, 0, "咨询、开发费", leftMergeStyle); + + // 技术咨询-合计 + rowNum = createTechTotalRow(sheet, techConsultTotalRowNum, expertMeetingTotalRowNum, expertCommunicationTotalRowNum, rowNum); + + return rowNum; + } + + /** + * 按类型筛选技术服务费数据 + */ + private List filterTechCostByType(List list, String type) { + return list.stream() + .filter(item -> item.getTechType().equals(type)) + .toList(); + } + + /** + * 创建技术咨询开发子部分 + */ + private int createTechConsultSubsection(Sheet sheet, List techConsultList, int startRowNum, String label, int labelCol) { + int rowNum = startRowNum; + + Row serviceHeaderRow1 = sheet.createRow(rowNum++); + createMergedCell(sheet, serviceHeaderRow1, 2, 5, "内容", headerStyle); + createCell(serviceHeaderRow1, 6, "单位", headerStyle); + createCell(serviceHeaderRow1, 7, "备注", headerStyle); + createCell(serviceHeaderRow1, 8, "金额(万元)", headerStyle); + + // 技术咨询开发数据 + int dataStartRow = rowNum + 1; + for (ErpRdBudgetTechCost detail : techConsultList) { + rowNum = createTechConsultDataRow(sheet, detail, rowNum); + } + + // 填充空行 + rowNum = fillTechConsultEmptyRows(sheet, techConsultList.size(), rowNum, 3); + + // 小计行 + rowNum = createTechConsultSubtotalRow(sheet, dataStartRow, rowNum); + + // 创建左侧合并单元格 + createVerticalMergedCell(sheet, startRowNum, rowNum - 1, labelCol, label, leftMergeStyle); + + return rowNum; + } + + /** + * 创建技术咨询开发数据行 + */ + private int createTechConsultDataRow(Sheet sheet, ErpRdBudgetTechCost detail, int rowNum) { + Row dataRow = sheet.createRow(rowNum++); + + createMergedCell(sheet, dataRow, 2, 5, detail.getTechContent(), dataStyle); + createCell(dataRow, 6, detail.getUnitName(), dataStyle); + createCell(dataRow, 7, detail.getRemark(), dataStyle); + Double price = detail.getPrice() != null ? detail.getPrice().doubleValue() : null; + createNumericCell(dataRow, 8, price, dataStyle); + + return rowNum; + } + + /** + * 填充技术咨询开发空行 + */ + private int fillTechConsultEmptyRows(Sheet sheet, int dataSize, int startRowNum, int targetSize) { + int rowNum = startRowNum; + int fillRows = targetSize - dataSize; + + while (fillRows > 0) { + Row emptyRow = sheet.createRow(rowNum++); + createMergedCell(sheet, emptyRow, 2, 5, "", dataStyle); + createCell(emptyRow, 6, "", dataStyle); + createCell(emptyRow, 7, "", dataStyle); + createCell(emptyRow, 8, "", dataStyle); + fillRows--; + } + + return rowNum; + } + + /** + * 创建技术咨询开发小计行 + */ + private int createTechConsultSubtotalRow(Sheet sheet, int dataStartRow, int rowNum) { + Row totalRow = sheet.createRow(rowNum++); + + createMergedCell(sheet, totalRow, 2, 7, "小计", headerStyle); + + Cell totalCell = totalRow.createCell(8); + totalCell.setCellFormula(String.format("SUM(I%d:I%d)", dataStartRow, rowNum - 1)); + totalCell.setCellStyle(formulaStyle); + + return rowNum; + } + + /** + * 创建专家咨询-会议形式子部分 + */ + private int createExpertMeetingSubsection(Sheet sheet, List expertMeetingList, int startRowNum) { + int rowNum = startRowNum; + + Row expertHeaderRow = sheet.createRow(rowNum++); + createMergedCell(sheet, expertHeaderRow, 3, 4, "内容", headerStyle); + createCell(expertHeaderRow, 5, "专家人数", headerStyle); + createCell(expertHeaderRow, 6, "", headerStyle); + createCell(expertHeaderRow, 7, "天数", headerStyle); + createCell(expertHeaderRow, 8, "金额(万元)", headerStyle); + + // 专家咨询-会议形式数据 + int dataStartRow = rowNum + 1; + for (ErpRdBudgetTechCost detail : expertMeetingList) { + rowNum = createExpertMeetingDataRow(sheet, detail, rowNum); + } + + // 填充空行 + rowNum = fillExpertMeetingEmptyRows(sheet, expertMeetingList.size(), rowNum, 3); + + // 小计行 + rowNum = createExpertMeetingSubtotalRow(sheet, dataStartRow, rowNum); + + // 创建左侧合并单元格 + createVerticalMergedCell(sheet, startRowNum, rowNum - 1, 2, "会议形式", leftMergeStyle); + + return rowNum; + } + + /** + * 创建专家咨询-会议形式数据行 + */ + private int createExpertMeetingDataRow(Sheet sheet, ErpRdBudgetTechCost detail, int rowNum) { + Row dataRow = sheet.createRow(rowNum++); + + createMergedCell(sheet, dataRow, 3, 4, detail.getTechContent(), dataStyle); + Double peopleNumber = detail.getPeopleNumber() != null ? detail.getPeopleNumber().doubleValue() : null; + createNumericCell(dataRow, 5, peopleNumber, dataStyle); + createCell(dataRow, 6, "", dataStyle); + Double days = detail.getDays() != null ? detail.getDays().doubleValue() : null; + createNumericCell(dataRow, 7, days, dataStyle); + + Cell priceCell = dataRow.createCell(8); + priceCell.setCellFormula(String.format("ROUND(IF(H%d<2,H%d*F%d*600,(F%d*2*600+F%d*(H%d-2)*300))/10000,2)", rowNum, rowNum, rowNum, rowNum, rowNum, rowNum)); + priceCell.setCellStyle(formulaStyle); + + return rowNum; + } + + /** + * 填充专家咨询-会议形式空行 + */ + private int fillExpertMeetingEmptyRows(Sheet sheet, int dataSize, int startRowNum, int targetSize) { + int rowNum = startRowNum; + int fillRows = targetSize - dataSize; + + while (fillRows > 0) { + Row emptyRow = sheet.createRow(rowNum++); + createMergedCell(sheet, emptyRow, 3, 4, "", dataStyle); + createCell(emptyRow, 5, "", dataStyle); + createCell(emptyRow, 6, "", dataStyle); + createCell(emptyRow, 7, "", dataStyle); + createCell(emptyRow, 8, "", dataStyle); + fillRows--; + } + + return rowNum; + } + + /** + * 创建专家咨询-会议形式小计行 + */ + private int createExpertMeetingSubtotalRow(Sheet sheet, int dataStartRow, int rowNum) { + Row totalRow = sheet.createRow(rowNum++); + + createMergedCell(sheet, totalRow, 3, 7, "小计", headerStyle); + + Cell totalCell = totalRow.createCell(8); + totalCell.setCellFormula(String.format("SUM(I%d:I%d)", dataStartRow, rowNum - 1)); + totalCell.setCellStyle(formulaStyle); + + return rowNum; + } + + /** + * 创建专家咨询-通讯形式子部分 + */ + private int createExpertCommunicationSubsection(Sheet sheet, List expertCommunicationList, int startRowNum) { + int rowNum = startRowNum; + + Row expertCommunicationHeaderRow = sheet.createRow(rowNum++); + createMergedCell(sheet, expertCommunicationHeaderRow, 3, 4, "内容", headerStyle); + createCell(expertCommunicationHeaderRow, 5, "人数", headerStyle); + createCell(expertCommunicationHeaderRow, 6, "", headerStyle); + createCell(expertCommunicationHeaderRow, 7, "次数", headerStyle); + createCell(expertCommunicationHeaderRow, 8, "金额(万元)", headerStyle); + + // 专家咨询-通讯形式数据 + int dataStartRow = rowNum + 1; + for (ErpRdBudgetTechCost detail : expertCommunicationList) { + rowNum = createExpertCommunicationDataRow(sheet, detail, rowNum); + } + + // 填充空行 + rowNum = fillExpertCommunicationEmptyRows(sheet, expertCommunicationList.size(), rowNum, 3); + + // 小计行 + rowNum = createExpertCommunicationSubtotalRow(sheet, dataStartRow, rowNum); + + // 创建左侧合并单元格 + createVerticalMergedCell(sheet, startRowNum, rowNum - 1, 2, "通讯形式", leftMergeStyle); + + return rowNum; + } + + /** + * 创建专家咨询-通讯形式数据行 + */ + private int createExpertCommunicationDataRow(Sheet sheet, ErpRdBudgetTechCost detail, int rowNum) { + Row dataRow = sheet.createRow(rowNum++); + + createMergedCell(sheet, dataRow, 3, 4, detail.getTechContent(), dataStyle); + createNumericCell(dataRow, 5, detail.getPeopleNumber() == null ? null : detail.getPeopleNumber().doubleValue(), dataStyle); + createCell(dataRow, 6, "", dataStyle); + createNumericCell(dataRow, 7, detail.getFrequency() == null ? null : detail.getFrequency().doubleValue(), dataStyle); + + Cell priceCell = dataRow.createCell(8); + priceCell.setCellFormula(String.format("ROUND(F%d*H%d*70/10000,2)", rowNum, rowNum)); + priceCell.setCellStyle(formulaStyle); + + return rowNum; + } + + /** + * 填充专家咨询-通讯形式空行 + */ + private int fillExpertCommunicationEmptyRows(Sheet sheet, int dataSize, int startRowNum, int targetSize) { + int rowNum = startRowNum; + int fillRows = targetSize - dataSize; + + while (fillRows > 0) { + Row emptyRow = sheet.createRow(rowNum++); + createMergedCell(sheet, emptyRow, 3, 4, "", dataStyle); + createCell(emptyRow, 5, "", dataStyle); + createCell(emptyRow, 6, "", dataStyle); + createCell(emptyRow, 7, "", dataStyle); + createCell(emptyRow, 8, "", dataStyle); + fillRows--; + } + + return rowNum; + } + + /** + * 创建专家咨询-通讯形式小计行 + */ + private int createExpertCommunicationSubtotalRow(Sheet sheet, int dataStartRow, int rowNum) { + Row totalRow = sheet.createRow(rowNum++); + + createMergedCell(sheet, totalRow, 3, 7, "小计", headerStyle); + + Cell totalCell = totalRow.createCell(8); + totalCell.setCellFormula(String.format("SUM(I%d:I%d)", dataStartRow, rowNum - 1)); + totalCell.setCellStyle(formulaStyle); + + return rowNum; + } + + /** + * 创建技术服务费合计行 + */ + private int createTechTotalRow(Sheet sheet, int techConsultTotalRowNum, int expertMeetingTotalRowNum, + int expertCommunicationTotalRowNum, int rowNum) { + Row techTotalRow = sheet.createRow(rowNum++); + + createMergedCell(sheet, techTotalRow, 1, 7, "合计", headerStyle); + + Cell techTotalCell = techTotalRow.createCell(8); + techTotalCell.setCellFormula(String.format("I%d+I%d+I%d", techConsultTotalRowNum, expertMeetingTotalRowNum, expertCommunicationTotalRowNum)); + techTotalCell.setCellStyle(formulaStyle); + + return rowNum; + } + + /** + * 创建人工费部分 + */ + private int createLaborCostSection(Sheet sheet, List budgetLaborCostList, int startRowNum) { + int rowNum = startRowNum; + + // 人工费部分标题 + Row laborTitleRow = sheet.createRow(rowNum++); + createMergedCell(sheet, laborTitleRow, 0, 9, "人工费预算明细表", titleStyle); + + int laborStartRowNum = rowNum; + + // 人工费表头 + Row laborHeaderRow = sheet.createRow(rowNum++); + createMergedCell(sheet, laborHeaderRow, 2, 3, "人员类别", headerStyle); + createCell(laborHeaderRow, 4, "人数", headerStyle); + createCell(laborHeaderRow, 5, "累计时间(月)", headerStyle); + createCell(laborHeaderRow, 6, "月平均投入比例(%)", headerStyle); + createCell(laborHeaderRow, 7, "人工标准(元/人月)", headerStyle); + createCell(laborHeaderRow, 8, "金额(万元)", headerStyle); + createCell(laborHeaderRow, 9, "投入人员", headerStyle); + + // 过滤人工费数据 + List laborCostList = filterLaborCostByType(budgetLaborCostList, LaborTypeEnum.LABOR_COST.getCode()); + + // 创建人工费数据行 + int dataStartRow = rowNum + 1; + for (ErpRdBudgetLaborCost detail : laborCostList) { + rowNum = createLaborCostDataRow(sheet, detail, rowNum); + } + + // 填充空行 + rowNum = fillLaborCostEmptyRows(sheet, laborCostList.size(), rowNum, 9); + + // 人工费合计 + rowNum = createLaborCostTotalRow(sheet, dataStartRow, rowNum); + + // 创建左侧合并单元格 + createDataRowsWithDynamicMerge(sheet, laborStartRowNum, rowNum - laborStartRowNum, 0, 1, "人工费"); + + return rowNum; + } + + /** + * 按类型筛选人工费劳务费数据 + */ + private List filterLaborCostByType(List list, String type) { + return list.stream() + .filter(item -> item.getLaborType().equals(type)) + .toList(); + } + + /** + * 创建人工费数据行 + */ + private int createLaborCostDataRow(Sheet sheet, ErpRdBudgetLaborCost detail, int rowNum) { + Row dataRow = sheet.createRow(rowNum++); + + createMergedCell(sheet, dataRow, 2, 3, detail.getPersonnelCategory(), dataStyle); + createNumericCell(dataRow, 4, detail.getPeopleNumber() == null ? null : detail.getPeopleNumber().doubleValue(), dataStyle); + Double camulativeTime = detail.getCumulativeTime() != null ? detail.getCumulativeTime().doubleValue() : null; + createNumericCell(dataRow, 5, camulativeTime, dataStyle); + Double monthRate = detail.getMonthRate() != null ? detail.getMonthRate().divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).doubleValue() : null; + + createNumericCell(dataRow, 6, monthRate, percentStyle); + Double artificialStandard = detail.getArtificialStandard() != null ? detail.getArtificialStandard().doubleValue() : null; + createNumericCell(dataRow, 7, artificialStandard, dataStyle); + + // 金额公式 + Cell amountCell = dataRow.createCell(8); + amountCell.setCellFormula("E" + (rowNum) + "*F" + (rowNum) + "*G" + (rowNum) + "*H" + (rowNum) + "/10000"); + amountCell.setCellStyle(formulaStyle); + + createCell(dataRow, 9, detail.getProjectPersonnel(), dataStyle); + + return rowNum; + } + + /** + * 填充人工费空行 + */ + private int fillLaborCostEmptyRows(Sheet sheet, int dataSize, int startRowNum, int targetSize) { + int rowNum = startRowNum; + int fillRows = targetSize - dataSize; + + while (fillRows > 0) { + Row emptyRow = sheet.createRow(rowNum++); + createMergedCell(sheet, emptyRow, 2, 3, "", dataStyle); + for (int i = 4; i < 8; i++) { + Double value = null; + createNumericCell(emptyRow, i, value, dataStyle); + } + + Cell amountCell = emptyRow.createCell(8); + amountCell.setCellFormula("E" + (rowNum) + "*F" + (rowNum) + "*G" + (rowNum) + "*H" + (rowNum) + "/10000"); + amountCell.setCellStyle(formulaStyle); + + createCell(emptyRow, 9, "", dataStyle); + fillRows--; + } + + return rowNum; + } + + /** + * 创建人工费合计行 + */ + private int createLaborCostTotalRow(Sheet sheet, int dataStartRow, int rowNum) { + Row laborTotalRow = sheet.createRow(rowNum++); + + createMergedCell(sheet, laborTotalRow, 2, 7, "合计", headerStyle); + + Cell laborTotalCell = laborTotalRow.createCell(8); + laborTotalCell.setCellFormula(String.format("SUM(I%d:I%d)", dataStartRow, rowNum - 1)); + laborTotalCell.setCellStyle(formulaStyle); + + createCell(laborTotalRow, 9, "", dataStyle); + + return rowNum; + } + + /** + * 创建劳务费部分 + */ + private int createServiceCostSection(Sheet sheet, List budgetLaborCostList, int startRowNum) { + int rowNum = startRowNum; + + // 劳务费部分标题 + Row serviceFeeTitleRow = sheet.createRow(rowNum++); + createMergedCell(sheet, serviceFeeTitleRow, 0, 8, "劳务费预算明细表", titleStyle); + + int serviceFeeStartRowNum = rowNum; + + // 劳务费表头 + Row serviceFeeHeaderRow = sheet.createRow(rowNum++); + createMergedCell(sheet, serviceFeeHeaderRow, 2, 3, "人员类别", headerStyle); + createCell(serviceFeeHeaderRow, 4, "人数", headerStyle); + createCell(serviceFeeHeaderRow, 5, "累计时间(月)", headerStyle); + createCell(serviceFeeHeaderRow, 6, "月平均投入比例(%)", headerStyle); + createCell(serviceFeeHeaderRow, 7, "人工标准(元/人月)", headerStyle); + createCell(serviceFeeHeaderRow, 8, "金额(万元)", headerStyle); + + // 过滤劳务费数据 + List serviceCostList = filterLaborCostByType(budgetLaborCostList, LaborTypeEnum.SERVICE_COST.getCode()); + + // 创建劳务费数据行 + int dataStartRow = rowNum + 1; + for (ErpRdBudgetLaborCost detail : serviceCostList) { + rowNum = createServiceCostDataRow(sheet, detail, rowNum); + } + + // 填充空行 + rowNum = fillServiceCostEmptyRows(sheet, serviceCostList.size(), rowNum, 3); + + // 劳务费合计 + rowNum = createServiceCostTotalRow(sheet, dataStartRow, rowNum); + + // 创建左侧合并单元格 + createDataRowsWithDynamicMerge(sheet, serviceFeeStartRowNum, rowNum - serviceFeeStartRowNum + 1, 0, 1, "劳务费"); + + return rowNum; + } + + /** + * 创建劳务费数据行 + */ + private int createServiceCostDataRow(Sheet sheet, ErpRdBudgetLaborCost detail, int rowNum) { + Row dataRow = sheet.createRow(rowNum++); + + createMergedCell(sheet, dataRow, 2, 3, detail.getPersonnelCategory(), dataStyle); + createNumericCell(dataRow, 4, detail.getPeopleNumber() == null ? null : detail.getPeopleNumber().doubleValue(), dataStyle); + Double camulativeTime = detail.getCumulativeTime() != null ? detail.getCumulativeTime().doubleValue() : null; + createNumericCell(dataRow, 5, camulativeTime, dataStyle); + Double monthRate = detail.getMonthRate() != null ? detail.getMonthRate().divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).doubleValue() : null; + createNumericCell(dataRow, 6, monthRate, percentStyle); + Double artificialStandard = detail.getArtificialStandard() != null ? detail.getArtificialStandard().doubleValue() : null; + createNumericCell(dataRow, 7, artificialStandard, dataStyle); + + // 金额公式 + Cell amountCell = dataRow.createCell(8); + amountCell.setCellFormula("ROUND(E" + (rowNum) + "*F" + (rowNum) + "*G" + (rowNum) + "*H" + (rowNum) + "/10000,2)"); + amountCell.setCellStyle(formulaStyle); + + return rowNum; + } + + /** + * 填充劳务费空行 + */ + private int fillServiceCostEmptyRows(Sheet sheet, int dataSize, int startRowNum, int targetSize) { + int rowNum = startRowNum; + int fillRows = targetSize - dataSize; + + while (fillRows > 0) { + Row emptyRow = sheet.createRow(rowNum++); + createMergedCell(sheet, emptyRow, 2, 3, "", dataStyle); + for (int i = 4; i < 8; i++) { + Double value = null; + createNumericCell(emptyRow, i, value, dataStyle); + } + + Cell amountCell = emptyRow.createCell(8); + amountCell.setCellFormula("E" + (rowNum) + "*F" + (rowNum) + "*G" + (rowNum) + "*H" + (rowNum) + "/10000"); + amountCell.setCellStyle(formulaStyle); + + fillRows--; + } + + return rowNum; + } + + /** + * 创建劳务费合计行 + */ + private int createServiceCostTotalRow(Sheet sheet, int dataStartRow, int rowNum) { + Row serviceFeeTotalRow = sheet.createRow(rowNum); + + createMergedCell(sheet, serviceFeeTotalRow, 2, 7, "合计", headerStyle); + + Cell serviceFeeTotalCell = serviceFeeTotalRow.createCell(8); + serviceFeeTotalCell.setCellFormula(String.format("SUM(I%d:I%d)", dataStartRow, rowNum)); + serviceFeeTotalCell.setCellStyle(formulaStyle); + + return rowNum; + } + + /** + * 设置人工劳务咨询表列宽 + */ + private void setLaborServiceSheetColumnWidths(Sheet sheet) { + setColumnWidths(sheet, new int[]{12, 12, 15, 20, 15, 15, 20, 20, 20, 30}); + } + + // =========== 资料/文献费明细表相关方法 =========== + + /** + * 创建资料/文献费明细表 + */ + private Integer createDocumentSheet(ErpBudgetInfoVo budget) { + Sheet sheet = workbook.createSheet(SHEET_DOCUMENT); + + // 准备数据 + List literatureCostList = prepareLiteratureCostData(budget); + + int rowNum = 0; + + // 创建表头 + rowNum = createDocumentSheetHeader(sheet, rowNum); + + int materialTotalDataRowNum = rowNum + 1; + + // 创建资料费部分 + rowNum = createMaterialCostSection(sheet, literatureCostList, rowNum); + + // 创建文献检索费部分 + rowNum = createDocumentRetrievalSection(sheet, literatureCostList, rowNum); + + int documentRetrievalDataRowNum = rowNum; + + int softwarePurchaseDataRowNum = documentRetrievalDataRowNum + 1; + + // 创建专用软件购买费部分 + rowNum = createSoftwarePurchaseSection(sheet, literatureCostList, rowNum); + + // 创建合计行 + rowNum = createDocumentTotalRow(sheet, rowNum, materialTotalDataRowNum, documentRetrievalDataRowNum, softwarePurchaseDataRowNum); + + // 设置列宽 + setDocumentSheetColumnWidths(sheet); + + return rowNum; + } + + /** + * 准备资料文献费数据 + */ + private List prepareLiteratureCostData(ErpBudgetInfoVo budget) { + List literatureCostList = budget.getErpRdBudgetLiteratureCostList() == null ? + new ArrayList<>() : budget.getErpRdBudgetLiteratureCostList(); + return literatureCostList; + } + + /** + * 创建资料/文献费表头 + */ + private int createDocumentSheetHeader(Sheet sheet, int startRowNum) { + int rowNum = startRowNum; + + // 表头 + Row row1 = sheet.createRow(rowNum++); + createMergedCell(sheet, row1, 0, 1, "表六", titleStyle); + + Row row2 = sheet.createRow(rowNum++); + createMergedCell(sheet, row2, 0, 1, "资料/文献费预算表", titleStyle); + + Row headerRow = sheet.createRow(rowNum++); + createCell(headerRow, 0, "项目", dataStyle); + createCell(headerRow, 1, "金额(万元)", dataStyle); + + return rowNum; + } + + /** + * 创建资料费部分 + */ + private int createMaterialCostSection(Sheet sheet, List literatureCostList, int startRowNum) { + int rowNum = startRowNum; + + // 资料费标题 + Row materialTitleRow = sheet.createRow(rowNum++); + createCell(materialTitleRow, 0, "一、资料费", headerStyle); + + Cell materialCostCell = materialTitleRow.createCell(1); + materialCostCell.setCellStyle(formulaStyle); + + // 过滤资料费数据 + List materialCostList = filterLiteratureCostByType(literatureCostList, LiteratureTypeEnum.MATERIAL_COST.getCode()); + + // 创建资料费数据行 + int dataStartRow = rowNum + 1; + for (ErpRdBudgetLiteratureCost detail : materialCostList) { + rowNum = createMaterialCostDataRow(sheet, detail, rowNum); + } + + // 填充空行 + rowNum = fillDocumentEmptyRows(sheet, materialCostList.size(), rowNum, 5); + + // 设置合计公式 + materialCostCell.setCellFormula(String.format("SUM(B%d:B%d)", dataStartRow, rowNum)); + + return rowNum; + } + + /** + * 按类型筛选资料文献费数据 + */ + private List filterLiteratureCostByType(List list, String type) { + return list.stream() + .filter(item -> item.getLiteratureType().equals(type)) + .toList(); + } + + /** + * 创建资料费数据行 + */ + private int createMaterialCostDataRow(Sheet sheet, ErpRdBudgetLiteratureCost detail, int rowNum) { + Row dataRow = sheet.createRow(rowNum++); + + createCell(dataRow, 0, detail.getItemDesc(), headerStyle); + Double price = detail.getPrice() != null ? detail.getPrice().doubleValue() : null; + createNumericCell(dataRow, 1, price, dataStyle); + + return rowNum; + } + + /** + * 填充资料文献费空行 + */ + private int fillDocumentEmptyRows(Sheet sheet, int dataSize, int startRowNum, int targetSize) { + int rowNum = startRowNum; + int fillRows = targetSize - dataSize; + + while (fillRows > 0) { + Row emptyRow = sheet.createRow(rowNum++); + for (int i = 0; i < 2; i++) { + createCell(emptyRow, i, "", dataStyle); + } + fillRows--; + } + + return rowNum; + } + + /** + * 创建文献检索费部分 + */ + private int createDocumentRetrievalSection(Sheet sheet, List literatureCostList, int startRowNum) { + int rowNum = startRowNum; + + // 文献检索费 + Optional documentCostOptional = literatureCostList.stream() + .filter(item -> item.getLiteratureType().equals(LiteratureTypeEnum.SOFTWARE_COST.getCode())) + .findFirst(); + + ErpRdBudgetLiteratureCost documentCost = new ErpRdBudgetLiteratureCost(); + if (documentCostOptional.isPresent()) { + documentCost = documentCostOptional.get(); + } + + Row documentRow = sheet.createRow(rowNum++); + createCell(documentRow, 0, "二、文献检索费", headerStyle); + Double price = documentCost.getPrice() != null ? documentCost.getPrice().doubleValue() : null; + createNumericCell(documentRow, 1, price, dataStyle); + + return rowNum; + } + + /** + * 创建专用软件购买费部分 + */ + private int createSoftwarePurchaseSection(Sheet sheet, List literatureCostList, int startRowNum) { + int rowNum = startRowNum; + + // 专用软件购买费标题 + Row softwareTitleRow = sheet.createRow(rowNum++); + createCell(softwareTitleRow, 0, "三、专用软件购买费", headerStyle); + + Cell softwareCostCell = softwareTitleRow.createCell(1); + softwareCostCell.setCellStyle(formulaStyle); + + // 过滤专用软件购买费数据 + List softwareCostList = filterLiteratureCostByType(literatureCostList, LiteratureTypeEnum.SOFTWARE_COST.getCode()); + + // 创建专用软件购买费数据行 + int dataStartRow = rowNum + 1; + for (ErpRdBudgetLiteratureCost detail : softwareCostList) { + rowNum = createSoftwarePurchaseDataRow(sheet, detail, rowNum); + } + + // 填充空行 + rowNum = fillDocumentEmptyRows(sheet, softwareCostList.size(), rowNum, 6); + + // 设置合计公式 + softwareCostCell.setCellFormula(String.format("SUM(B%d:B%d)", dataStartRow, rowNum)); + + return rowNum; + } + + /** + * 创建专用软件购买费数据行 + */ + private int createSoftwarePurchaseDataRow(Sheet sheet, ErpRdBudgetLiteratureCost detail, int rowNum) { + Row dataRow = sheet.createRow(rowNum++); + + createCell(dataRow, 0, detail.getItemDesc(), headerStyle); + Double price = detail.getPrice() != null ? detail.getPrice().doubleValue() : null; + createNumericCell(dataRow, 1, price, dataStyle); + + return rowNum; + } + + /** + * 创建资料/文献费合计行 + */ + private int createDocumentTotalRow(Sheet sheet, int startRowNum, int materialTotalDataRowNum, int documentRetrievalDataRowNum, int softwarePurchaseDataRowNum) { + int rowNum = startRowNum; + + Row totalRow = sheet.createRow(rowNum); + createCell(totalRow, 0, "合计", headerStyle); + + Cell totalCell = totalRow.createCell(1); + totalCell.setCellFormula(String.format("SUM(B%d,B%d,B%d)", materialTotalDataRowNum, documentRetrievalDataRowNum, softwarePurchaseDataRowNum)); + totalCell.setCellStyle(formulaStyle); + + return rowNum; + } + + /** + * 设置资料/文献费表列宽 + */ + private void setDocumentSheetColumnWidths(Sheet sheet) { + setColumnWidths(sheet, new int[]{30, 50}); + } + + // =========== 测试化验费明细表相关方法 =========== + + /** + * 创建测试化验费明细表 + */ + private Integer createTestingSheet(ErpBudgetInfoVo budget) { + Sheet sheet = workbook.createSheet(SHEET_TESTING); + + // 准备数据 + List testingCostList = prepareTestingCostData(budget); + + int rowNum = 0; + + // 创建表头 + rowNum = createTestingSheetHeader(sheet, rowNum); + + // 创建数据行 + int dataStartRow = rowNum + 1; + rowNum = createTestingDataRows(sheet, testingCostList, rowNum); + + // 填充空行 + rowNum = fillTestingEmptyRows(sheet, testingCostList.size(), rowNum, 12); + + // 创建合计行 + rowNum = createTestingTotalRow(sheet, dataStartRow, rowNum); + + // 设置列宽 + setTestingSheetColumnWidths(sheet); + + return rowNum; + } + + /** + * 准备测试化验费数据 + */ + private List prepareTestingCostData(ErpBudgetInfoVo budget) { + List testingCostList = budget.getErpRdBudgetTestingCostList() == null ? + new ArrayList<>() : budget.getErpRdBudgetTestingCostList(); + return testingCostList; + } + + /** + * 创建测试化验费表头 + */ + private int createTestingSheetHeader(Sheet sheet, int startRowNum) { + int rowNum = startRowNum; + + // 表头 + Row row1 = sheet.createRow(rowNum++); + createMergedCell(sheet, row1, 0, 6, "表七", titleStyle); + + Row row2 = sheet.createRow(rowNum++); + createMergedCell(sheet, row2, 0, 6, "测试化验费预算明细表", titleStyle); + + // 表头 + Row headerRow = sheet.createRow(rowNum++); + headerRow.setHeightInPoints(40); + String[] headers = {"序号", "测试化验的内容", "测试化验单位", "单位", "单价\n(元/单位数量)", "数量", "金额(万元)"}; + for (int i = 0; i < headers.length; i++) { + createCell(headerRow, i, headers[i], headerStyle); + } + + return rowNum; + } + + /** + * 创建测试化验费数据行 + */ + private int createTestingDataRows(Sheet sheet, List testingCostList, int startRowNum) { + int rowNum = startRowNum; + + for (ErpRdBudgetTestingCost detail : testingCostList) { + rowNum = createTestingDataRow(sheet, detail, rowNum); + } + + return rowNum; + } + + /** + * 创建单个测试化验费数据行 + */ + private int createTestingDataRow(Sheet sheet, ErpRdBudgetTestingCost detail, int rowNum) { + Row dataRow = sheet.createRow(rowNum++); + + createCell(dataRow, 0, detail.getSortOrder().toString(), dataStyle); + createCell(dataRow, 1, detail.getTestingContent(), dataStyle); + createCell(dataRow, 2, detail.getTestingUnitName(), dataStyle); + createCell(dataRow, 3, detail.getUnitName(), dataStyle); + Double unitPrice = detail.getUnitPrice() != null ? detail.getUnitPrice().doubleValue() : null; + createNumericCell(dataRow, 4, unitPrice, dataStyle); + Double amount = detail.getAmount() != null ? detail.getAmount().doubleValue() : null; + createNumericCell(dataRow, 5, amount, dataStyle); + + // 金额公式 + Cell amountCell = dataRow.createCell(6); + amountCell.setCellFormula("ROUND(E" + (rowNum) + "*F" + (rowNum) + "/10000,2)"); + amountCell.setCellStyle(formulaStyle); + + return rowNum; + } + + /** + * 填充测试化验费空行 + */ + private int fillTestingEmptyRows(Sheet sheet, int dataSize, int startRowNum, int targetSize) { + int rowNum = startRowNum; + int fillRows = targetSize - dataSize; + + while (fillRows > 0) { + Row emptyRow = sheet.createRow(rowNum++); + for (int i = 0; i < 4; i++) { + createCell(emptyRow, i, "", dataStyle); + } + for (int i = 4; i < 6; i++) { + Double value = null; + createNumericCell(emptyRow, i, value, dataStyle); + } + + Cell amountCell = emptyRow.createCell(6); + amountCell.setCellFormula("ROUND(E" + (rowNum) + "*F" + (rowNum) + "/10000,2)"); + amountCell.setCellStyle(formulaStyle); + + fillRows--; + } + + return rowNum; + } + + /** + * 创建测试化验费合计行 + */ + private int createTestingTotalRow(Sheet sheet, int dataStartRow, int rowNum) { + Row totalRow = sheet.createRow(rowNum); + + createMergedCell(sheet, totalRow, 0, 1, "合计", headerStyle); + createCell(totalRow, 2, "/", headerStyle); + createCell(totalRow, 3, "/", headerStyle); + createCell(totalRow, 4, "/", headerStyle); + createCell(totalRow, 5, "/", headerStyle); + + Cell totalCell = totalRow.createCell(6); + totalCell.setCellFormula(String.format("SUM(G%d:G%d)", dataStartRow, rowNum)); + totalCell.setCellStyle(formulaStyle); + + return rowNum; + } + + /** + * 设置测试化验费表列宽 + */ + private void setTestingSheetColumnWidths(Sheet sheet) { + setColumnWidths(sheet, new int[]{15, 30, 20, 15, 20, 20, 20}); + } + + // =========== 其他费用明细表相关方法 =========== + + /** + * 创建其他费用明细表 + */ + private Integer createOtherSheet(ErpBudgetInfoVo budget) { + Sheet sheet = workbook.createSheet(SHEET_OTHER); + + // 准备数据 + List otherCostList = prepareOtherCostData(budget); + + int rowNum = 0; + + // 创建表头 + rowNum = createOtherSheetHeader(sheet, rowNum); + + // 创建数据行 + int dataStartRow = rowNum + 1; + rowNum = createOtherDataRows(sheet, otherCostList, rowNum); + + // 填充空行 + rowNum = fillOtherEmptyRows(sheet, otherCostList.size(), rowNum, 10); + + // 创建合计行 + rowNum = createOtherTotalRow(sheet, dataStartRow, rowNum); + + // 设置列宽 + setOtherSheetColumnWidths(sheet); + + return rowNum; + } + + /** + * 准备其他费用数据 + */ + private List prepareOtherCostData(ErpBudgetInfoVo budget) { + List otherCostList = budget.getErpRdBudgetOtherCostList() == null ? + new ArrayList<>() : budget.getErpRdBudgetOtherCostList(); + return otherCostList; + } + + /** + * 创建其他费用表头 + */ + private int createOtherSheetHeader(Sheet sheet, int startRowNum) { + int rowNum = startRowNum; + + // 表头 + Row row1 = sheet.createRow(rowNum++); + createMergedCell(sheet, row1, 0, 2, "表八", titleStyle); + + Row row2 = sheet.createRow(rowNum++); + createMergedCell(sheet, row2, 0, 2, "其他费用预算表", titleStyle); + + // 表头 + Row headerRow = sheet.createRow(rowNum++); + String[] headers = {"序号", "项目", "金额(万元)"}; + for (int i = 0; i < headers.length; i++) { + createCell(headerRow, i, headers[i], headerStyle); + } + + return rowNum; + } + + /** + * 创建其他费用数据行 + */ + private int createOtherDataRows(Sheet sheet, List otherCostList, int startRowNum) { + int rowNum = startRowNum; + + for (ErpRdBudgetOtherCost detail : otherCostList) { + rowNum = createOtherDataRow(sheet, detail, rowNum); + } + + return rowNum; + } + + /** + * 创建单个其他费用数据行 + */ + private int createOtherDataRow(Sheet sheet, ErpRdBudgetOtherCost detail, int rowNum) { + Row dataRow = sheet.createRow(rowNum++); + + createCell(dataRow, 0, detail.getSortOrder().toString(), dataStyle); + createCell(dataRow, 1, detail.getItemDesc(), dataStyle); + Double price = detail.getPrice() != null ? detail.getPrice().doubleValue() : null; + createNumericCell(dataRow, 2, price, moneyStyle); + + return rowNum; + } + + /** + * 填充其他费用空行 + */ + private int fillOtherEmptyRows(Sheet sheet, int dataSize, int startRowNum, int targetSize) { + int rowNum = startRowNum; + int fillRows = targetSize - dataSize; + + while (fillRows > 0) { + Row emptyRow = sheet.createRow(rowNum++); + for (int i = 0; i < 3; i++) { + createCell(emptyRow, i, "", dataStyle); + } + fillRows--; + } + + return rowNum; + } + + /** + * 创建其他费用合计行 + */ + private int createOtherTotalRow(Sheet sheet, int dataStartRow, int rowNum) { + Row totalRow = sheet.createRow(rowNum); + + createMergedCell(sheet, totalRow, 0, 1, "合计", headerStyle); + + Cell totalCell = totalRow.createCell(2); + totalCell.setCellFormula(String.format("SUM(C%d:C%d)", dataStartRow, rowNum)); + totalCell.setCellStyle(formulaStyle); + + return rowNum; + } + + /** + * 设置其他费用表列宽 + */ + private void setOtherSheetColumnWidths(Sheet sheet) { + setColumnWidths(sheet, new int[]{15, 25, 30}); + } +} diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/workflow/handler/OaProcessEventHandler.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/workflow/handler/OaProcessEventHandler.java index e67cc809..71eaea6b 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/workflow/handler/OaProcessEventHandler.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/workflow/handler/OaProcessEventHandler.java @@ -2,6 +2,7 @@ package org.dromara.oa.workflow.handler; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.utils.StringUtils; import org.dromara.oa.erp.mapper.OaUniversalMapper; import org.dromara.workflow.api.event.ProcessEvent; import org.dromara.workflow.enums.FlowConfigEnum; @@ -10,6 +11,7 @@ import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import java.util.HashMap; import java.util.Map; /** @@ -51,4 +53,39 @@ public class OaProcessEventHandler extends AbstractProcessEventHandler { return oaUniversalMapper.dynamicUpdate(tableName, setFields, conditions); } + @Override + protected int doUpdateWithOperator(FlowConfigEnum flowConfig, Map setFields, Map conditions,String businessId) { + String tableName = flowConfig.getTableName(); + log.debug("执行数据库更新with operator: table={}, conditions={}", tableName, conditions); + + return oaUniversalMapper.dynamicUpdateWithOperator(tableName, setFields, conditions,businessId); + } + + @Override + protected void makeInvalid(FlowConfigEnum flowConfig, ProcessEvent processEvent) { + String invalidStatus = flowConfig.getInvalidBusinessStatus(); + if (StringUtils.isNotBlank(invalidStatus)) { + Map params = processEvent.getParams(); + // 步骤1: 构建更新字段 + Map setFields = new HashMap<>(); + setFields.put(flowConfig.getBusinessStatusField(),invalidStatus); + + String relationId = oaUniversalMapper.selectRelationId( + flowConfig.getTableName(), + flowConfig.getRelationIdField(), + flowConfig.getBusinessPk(), + processEvent.getBusinessId() + ); + + // 步骤2: 构建查询条件 + Map conditions = new HashMap<>(); + conditions.put(flowConfig.getBusinessPk(), "!=" + processEvent.getBusinessId()); + conditions.put(flowConfig.getRelationIdField(),relationId); + // 步骤3: 执行更新 + int updateCount = doUpdateWithOperator(flowConfig, setFields, conditions,processEvent.getBusinessId()); + } + + } + + } diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/workflow/strategy/BudgetWorkflowStrategy.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/workflow/strategy/BudgetWorkflowStrategy.java index 9920eca9..6730752e 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/workflow/strategy/BudgetWorkflowStrategy.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/workflow/strategy/BudgetWorkflowStrategy.java @@ -28,6 +28,7 @@ public class BudgetWorkflowStrategy implements WorkflowStrategy @Override public Map getVariables(ErpBudgetInfoBo bo) { Map variables = new HashMap<>(); + variables.put("projectId", bo.getProjectId()); variables.put("projectName", bo.getProjectName()); return variables; }