From 1d35ca444e5ec3eaca61cdbd48bb58504800c9a8 Mon Sep 17 00:00:00 2001 From: yangk Date: Fri, 22 May 2026 08:42:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(oa/erp):=20=E5=AE=8C=E5=96=84=E9=A2=84?= =?UTF-8?q?=E6=8A=95=E5=B7=A5=E6=97=B6=E5=88=86=E9=85=8D=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加员工级分配明细列表功能 - 新增查询员工级分配详情接口getStaffAllocDetails - 将新增和编辑方法统一为saveAlloc方法 - 禁止手动维护分配明细,改为由主单自动生成 - 增强数据校验和权限控制逻辑 - 实现工时分配的事务性处理和数据一致性保障 - 将工时字段从Long类型改为BigDecimal类型以支持小数精度 - 修改项目ID字段描述为来源预投项目ID - 更新涉及人数字段从Long改为Integer类型 - 移除分配单编号的非空验证要求 --- .../ErpTimesheetPreAllocController.java | 19 +- .../ErpTimesheetPreAllocDetailController.java | 6 +- .../oa/erp/domain/ErpTimesheetPreAlloc.java | 10 +- .../domain/ErpTimesheetPreAllocDetail.java | 5 +- .../erp/domain/bo/ErpTimesheetPreAllocBo.java | 21 +- .../bo/ErpTimesheetPreAllocDetailBo.java | 5 +- .../erp/domain/bo/PreAllocStaffAllocBo.java | 37 + .../oa/erp/domain/bo/PreAllocTargetBo.java | 26 + .../vo/ErpTimesheetPreAllocDetailVo.java | 6 +- .../erp/domain/vo/ErpTimesheetPreAllocVo.java | 13 +- .../oa/erp/domain/vo/PreAllocDetailVo.java | 64 ++ .../erp/domain/vo/PreAllocStaffAllocVo.java | 53 ++ .../oa/erp/domain/vo/PreAllocTargetVo.java | 41 + .../service/IErpTimesheetPreAllocService.java | 27 +- .../impl/ErpTimesheetPreAllocServiceImpl.java | 744 ++++++++++++++++-- 15 files changed, 984 insertions(+), 93 deletions(-) create mode 100644 ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/bo/PreAllocStaffAllocBo.java create mode 100644 ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/bo/PreAllocTargetBo.java create mode 100644 ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/PreAllocDetailVo.java create mode 100644 ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/PreAllocStaffAllocVo.java create mode 100644 ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/PreAllocTargetVo.java diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/controller/ErpTimesheetPreAllocController.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/controller/ErpTimesheetPreAllocController.java index 467dc8cf..0e951eed 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/controller/ErpTimesheetPreAllocController.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/controller/ErpTimesheetPreAllocController.java @@ -19,6 +19,7 @@ import org.dromara.common.log.enums.BusinessType; import org.dromara.common.excel.utils.ExcelUtil; import org.dromara.oa.erp.domain.vo.ErpTimesheetPreAllocVo; import org.dromara.oa.erp.domain.bo.ErpTimesheetPreAllocBo; +import org.dromara.oa.erp.domain.vo.PreAllocDetailVo; import org.dromara.oa.erp.service.IErpTimesheetPreAllocService; import org.dromara.common.mybatis.core.page.TableDataInfo; @@ -64,9 +65,19 @@ public class ErpTimesheetPreAllocController extends BaseController { */ @SaCheckPermission("oa/erp:timesheetPreAlloc:query") @GetMapping("/{allocId}") - public R getInfo(@NotNull(message = "主键不能为空") + public R getInfo(@NotNull(message = "主键不能为空") @PathVariable("allocId") Long allocId) { - return R.ok(erpTimesheetPreAllocService.queryById(allocId)); + return R.ok(erpTimesheetPreAllocService.queryDetailById(allocId)); + } + + /** + * 查询当前部门指定月份、来源预投项目的员工级可分配明细 + */ + @SaCheckPermission("oa/erp:timesheetPreAlloc:query") + @GetMapping("/getStaffAllocDetails") + public R getStaffAllocDetails(@NotBlank(message = "月份编码不能为空") String monthCode, + @NotNull(message = "来源预投项目不能为空") Long projectId) { + return R.ok(erpTimesheetPreAllocService.getStaffAllocDetails(monthCode, projectId)); } /** @@ -77,7 +88,7 @@ public class ErpTimesheetPreAllocController extends BaseController { @RepeatSubmit() @PostMapping() public R add(@Validated(AddGroup.class) @RequestBody ErpTimesheetPreAllocBo bo) { - return toAjax(erpTimesheetPreAllocService.insertByBo(bo)); + return toAjax(erpTimesheetPreAllocService.saveAlloc(bo)); } /** @@ -88,7 +99,7 @@ public class ErpTimesheetPreAllocController extends BaseController { @RepeatSubmit() @PutMapping() public R edit(@Validated(EditGroup.class) @RequestBody ErpTimesheetPreAllocBo bo) { - return toAjax(erpTimesheetPreAllocService.updateByBo(bo)); + return toAjax(erpTimesheetPreAllocService.saveAlloc(bo)); } /** diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/controller/ErpTimesheetPreAllocDetailController.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/controller/ErpTimesheetPreAllocDetailController.java index 9839a450..3d47f36d 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/controller/ErpTimesheetPreAllocDetailController.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/controller/ErpTimesheetPreAllocDetailController.java @@ -77,7 +77,7 @@ public class ErpTimesheetPreAllocDetailController extends BaseController { @RepeatSubmit() @PostMapping() public R add(@Validated(AddGroup.class) @RequestBody ErpTimesheetPreAllocDetailBo bo) { - return toAjax(erpTimesheetPreAllocDetailService.insertByBo(bo)); + return R.fail("分配明细由主单自动生成,不允许手工维护"); } /** @@ -88,7 +88,7 @@ public class ErpTimesheetPreAllocDetailController extends BaseController { @RepeatSubmit() @PutMapping() public R edit(@Validated(EditGroup.class) @RequestBody ErpTimesheetPreAllocDetailBo bo) { - return toAjax(erpTimesheetPreAllocDetailService.updateByBo(bo)); + return R.fail("分配明细由主单自动生成,不允许手工维护"); } /** @@ -101,7 +101,7 @@ public class ErpTimesheetPreAllocDetailController extends BaseController { @DeleteMapping("/{allocDetailIds}") public R remove(@NotEmpty(message = "主键不能为空") @PathVariable("allocDetailIds") Long[] allocDetailIds) { - return toAjax(erpTimesheetPreAllocDetailService.deleteWithValidByIds(List.of(allocDetailIds), true)); + return R.fail("分配明细由主单自动生成,不允许手工维护"); } /** diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/ErpTimesheetPreAlloc.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/ErpTimesheetPreAlloc.java index 483bfd9f..8c4aa349 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/ErpTimesheetPreAlloc.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/ErpTimesheetPreAlloc.java @@ -4,8 +4,8 @@ import org.dromara.common.tenant.core.TenantEntity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import lombok.EqualsAndHashCode; +import java.math.BigDecimal; import java.util.Date; -import com.fasterxml.jackson.annotation.JsonFormat; import java.io.Serial; @@ -45,7 +45,7 @@ public class ErpTimesheetPreAlloc extends TenantEntity { private Long standardMonthId; /** - * 项目ID(备件、物流) + * 来源预投项目ID */ private Long projectId; @@ -62,17 +62,17 @@ public class ErpTimesheetPreAlloc extends TenantEntity { /** * 来源预投工时合计(天,带出汇总) */ - private Long sourceTotalHours; + private BigDecimal sourceTotalHours; /** * 已分配合计(天) */ - private Long allocatedTotalHours; + private BigDecimal allocatedTotalHours; /** * 涉及人数 */ - private Long staffCount; + private Integer staffCount; /** * 单据状态(0未分配 1部分分配 2已分配) diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/ErpTimesheetPreAllocDetail.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/ErpTimesheetPreAllocDetail.java index 9541f05c..b52d229e 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/ErpTimesheetPreAllocDetail.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/ErpTimesheetPreAllocDetail.java @@ -6,6 +6,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import java.io.Serial; +import java.math.BigDecimal; /** * 预投工时分配明细对象 erp_timesheet_pre_alloc_detail @@ -35,7 +36,7 @@ public class ErpTimesheetPreAllocDetail extends TenantEntity { /** * 排序号 */ - private Long sortOrder; + private Integer sortOrder; /** * 原项目ID @@ -70,7 +71,7 @@ public class ErpTimesheetPreAllocDetail extends TenantEntity { /** * 分配工时(天) */ - private Long allocHours; + private BigDecimal allocHours; /** * 备注 diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/bo/ErpTimesheetPreAllocBo.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/bo/ErpTimesheetPreAllocBo.java index 7c216fe0..64f6efbe 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/bo/ErpTimesheetPreAllocBo.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/bo/ErpTimesheetPreAllocBo.java @@ -8,8 +8,9 @@ import io.github.linpeilie.annotations.AutoMapper; import lombok.Data; import lombok.EqualsAndHashCode; import jakarta.validation.constraints.*; +import java.math.BigDecimal; import java.util.Date; -import com.fasterxml.jackson.annotation.JsonFormat; +import java.util.List; /** * 预投工时分配业务对象 erp_timesheet_pre_alloc @@ -31,7 +32,6 @@ public class ErpTimesheetPreAllocBo extends BaseEntity { /** * 分配单编号 */ - @NotBlank(message = "分配单编号不能为空", groups = { AddGroup.class, EditGroup.class }) private String allocCode; /** @@ -46,9 +46,9 @@ public class ErpTimesheetPreAllocBo extends BaseEntity { private Long standardMonthId; /** - * 项目ID(备件、物流) + * 来源预投项目ID */ - @NotNull(message = "项目ID(备件、物流)不能为空", groups = { AddGroup.class, EditGroup.class }) + @NotNull(message = "来源预投项目ID不能为空", groups = { AddGroup.class, EditGroup.class }) private Long projectId; /** @@ -64,24 +64,21 @@ public class ErpTimesheetPreAllocBo extends BaseEntity { /** * 来源预投工时合计(天,带出汇总) */ - @NotNull(message = "来源预投工时合计(天,带出汇总)不能为空", groups = { AddGroup.class, EditGroup.class }) - private Long sourceTotalHours; + private BigDecimal sourceTotalHours; /** * 已分配合计(天) */ - @NotNull(message = "已分配合计(天)不能为空", groups = { AddGroup.class, EditGroup.class }) - private Long allocatedTotalHours; + private BigDecimal allocatedTotalHours; /** * 涉及人数 */ - private Long staffCount; + private Integer staffCount; /** * 单据状态(0未分配 1部分分配 2已分配) */ - @NotBlank(message = "单据状态(0未分配 1部分分配 2已分配)不能为空", groups = { AddGroup.class, EditGroup.class }) private String allocStatus; /** @@ -104,5 +101,9 @@ public class ErpTimesheetPreAllocBo extends BaseEntity { */ private String remark; + /** + * 员工级分配明细 + */ + private List staffAllocList; } diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/bo/ErpTimesheetPreAllocDetailBo.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/bo/ErpTimesheetPreAllocDetailBo.java index 58225dd4..bc8e4ea1 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/bo/ErpTimesheetPreAllocDetailBo.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/bo/ErpTimesheetPreAllocDetailBo.java @@ -8,6 +8,7 @@ import io.github.linpeilie.annotations.AutoMapper; import lombok.Data; import lombok.EqualsAndHashCode; import jakarta.validation.constraints.*; +import java.math.BigDecimal; /** * 预投工时分配明细业务对象 erp_timesheet_pre_alloc_detail @@ -35,7 +36,7 @@ public class ErpTimesheetPreAllocDetailBo extends BaseEntity { /** * 排序号 */ - private Long sortOrder; + private Integer sortOrder; /** * 原项目ID @@ -73,7 +74,7 @@ public class ErpTimesheetPreAllocDetailBo extends BaseEntity { * 分配工时(天) */ @NotNull(message = "分配工时(天)不能为空", groups = { AddGroup.class, EditGroup.class }) - private Long allocHours; + private BigDecimal allocHours; /** * 备注 diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/bo/PreAllocStaffAllocBo.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/bo/PreAllocStaffAllocBo.java new file mode 100644 index 00000000..30562a89 --- /dev/null +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/bo/PreAllocStaffAllocBo.java @@ -0,0 +1,37 @@ +package org.dromara.oa.erp.domain.bo; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 预投工时员工级分配入参 + * + * @author Yangk + * @date 2026-05-21 + */ +@Data +public class PreAllocStaffAllocBo { + + /** + * 员工用户ID + */ + private Long staffUserId; + + /** + * 员工姓名 + */ + private String staffName; + + /** + * 来源预投工时(天) + */ + private BigDecimal sourceHours; + + /** + * 目标项目分配明细 + */ + private List allocItems; + +} diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/bo/PreAllocTargetBo.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/bo/PreAllocTargetBo.java new file mode 100644 index 00000000..63bdbd86 --- /dev/null +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/bo/PreAllocTargetBo.java @@ -0,0 +1,26 @@ +package org.dromara.oa.erp.domain.bo; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 预投工时目标项目分配入参 + * + * @author Yangk + * @date 2026-05-21 + */ +@Data +public class PreAllocTargetBo { + + /** + * 目标实际项目ID + */ + private Long targetProjectId; + + /** + * 分配工时(天) + */ + private BigDecimal allocHours; + +} diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/ErpTimesheetPreAllocDetailVo.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/ErpTimesheetPreAllocDetailVo.java index 1d95396e..183c9a80 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/ErpTimesheetPreAllocDetailVo.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/ErpTimesheetPreAllocDetailVo.java @@ -10,7 +10,7 @@ import lombok.Data; import java.io.Serial; import java.io.Serializable; -import java.util.Date; +import java.math.BigDecimal; @@ -44,7 +44,7 @@ public class ErpTimesheetPreAllocDetailVo implements Serializable { * 排序号 */ @ExcelProperty(value = "排序号") - private Long sortOrder; + private Integer sortOrder; /** * 原项目ID @@ -86,7 +86,7 @@ public class ErpTimesheetPreAllocDetailVo implements Serializable { * 分配工时(天) */ @ExcelProperty(value = "分配工时(天)") - private Long allocHours; + private BigDecimal allocHours; /** * 备注 diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/ErpTimesheetPreAllocVo.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/ErpTimesheetPreAllocVo.java index 1a8e1c67..79267b65 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/ErpTimesheetPreAllocVo.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/ErpTimesheetPreAllocVo.java @@ -1,7 +1,6 @@ package org.dromara.oa.erp.domain.vo; import java.util.Date; -import com.fasterxml.jackson.annotation.JsonFormat; import org.dromara.oa.erp.domain.ErpTimesheetPreAlloc; import cn.idev.excel.annotation.ExcelIgnoreUnannotated; import cn.idev.excel.annotation.ExcelProperty; @@ -12,7 +11,7 @@ import lombok.Data; import java.io.Serial; import java.io.Serializable; -import java.util.Date; +import java.math.BigDecimal; @@ -55,9 +54,9 @@ public class ErpTimesheetPreAllocVo implements Serializable { private Long standardMonthId; /** - * 项目ID(备件、物流) + * 来源预投项目ID */ - @ExcelProperty(value = "项目ID(备件、物流)") + @ExcelProperty(value = "来源预投项目ID") private Long projectId; /** @@ -76,19 +75,19 @@ public class ErpTimesheetPreAllocVo implements Serializable { * 来源预投工时合计(天,带出汇总) */ @ExcelProperty(value = "来源预投工时合计(天,带出汇总)") - private Long sourceTotalHours; + private BigDecimal sourceTotalHours; /** * 已分配合计(天) */ @ExcelProperty(value = "已分配合计(天)") - private Long allocatedTotalHours; + private BigDecimal allocatedTotalHours; /** * 涉及人数 */ @ExcelProperty(value = "涉及人数") - private Long staffCount; + private Integer staffCount; /** * 单据状态(0未分配 1部分分配 2已分配) diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/PreAllocDetailVo.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/PreAllocDetailVo.java new file mode 100644 index 00000000..1bb503a5 --- /dev/null +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/PreAllocDetailVo.java @@ -0,0 +1,64 @@ +package org.dromara.oa.erp.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * 预投工时分配员工级详情 + * + * @author Yangk + * @date 2026-05-21 + */ +@Data +public class PreAllocDetailVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long allocId; + + private String allocCode; + + private String monthCode; + + private Long standardMonthId; + + /** + * 来源预投项目ID + */ + private Long projectId; + + private String projectCode; + + private String projectName; + + private BigDecimal sourceTotalHours = BigDecimal.ZERO; + + private BigDecimal allocatedTotalHours = BigDecimal.ZERO; + + private BigDecimal remainingTotalHours = BigDecimal.ZERO; + + private Integer staffCount = 0; + + private String allocStatus; + + private String appliedFlag; + + private Long summaryId; + + private Date applyTime; + + private String remark; + + /** + * 员工级分配明细 + */ + private List staffAllocList = new ArrayList<>(); + +} diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/PreAllocStaffAllocVo.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/PreAllocStaffAllocVo.java new file mode 100644 index 00000000..82c84739 --- /dev/null +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/PreAllocStaffAllocVo.java @@ -0,0 +1,53 @@ +package org.dromara.oa.erp.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/** + * 预投工时员工级分配视图 + * + * @author Yangk + * @date 2026-05-21 + */ +@Data +public class PreAllocStaffAllocVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 员工用户ID + */ + private Long staffUserId; + + /** + * 员工姓名 + */ + private String staffName; + + /** + * 来源预投工时(天) + */ + private BigDecimal sourceHours = BigDecimal.ZERO; + + /** + * 已分配工时(天) + */ + private BigDecimal allocatedHours = BigDecimal.ZERO; + + /** + * 未分配工时(天) + */ + private BigDecimal remainingHours = BigDecimal.ZERO; + + /** + * 目标项目分配明细 + */ + private List allocItems = new ArrayList<>(); + +} diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/PreAllocTargetVo.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/PreAllocTargetVo.java new file mode 100644 index 00000000..675b2d9c --- /dev/null +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/vo/PreAllocTargetVo.java @@ -0,0 +1,41 @@ +package org.dromara.oa.erp.domain.vo; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * 预投工时目标项目分配视图 + * + * @author Yangk + * @date 2026-05-21 + */ +@Data +public class PreAllocTargetVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 目标实际项目ID + */ + private Long targetProjectId; + + /** + * 目标项目编码 + */ + private String targetProjectCode; + + /** + * 目标项目名称 + */ + private String targetProjectName; + + /** + * 分配工时(天) + */ + private BigDecimal allocHours; + +} diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/IErpTimesheetPreAllocService.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/IErpTimesheetPreAllocService.java index d2fc4c32..2d7265ec 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/IErpTimesheetPreAllocService.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/IErpTimesheetPreAllocService.java @@ -1,10 +1,10 @@ package org.dromara.oa.erp.service; -import org.dromara.oa.erp.domain.ErpTimesheetPreAlloc; import org.dromara.oa.erp.domain.vo.ErpTimesheetPreAllocVo; import org.dromara.oa.erp.domain.bo.ErpTimesheetPreAllocBo; import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.oa.erp.domain.vo.PreAllocDetailVo; import java.util.Collection; import java.util.List; @@ -25,6 +25,14 @@ public interface IErpTimesheetPreAllocService { */ ErpTimesheetPreAllocVo queryById(Long allocId); + /** + * 查询预投工时分配员工级详情 + * + * @param allocId 主键 + * @return 预投工时分配员工级详情 + */ + PreAllocDetailVo queryDetailById(Long allocId); + /** * 分页查询预投工时分配列表 * @@ -42,6 +50,15 @@ public interface IErpTimesheetPreAllocService { */ List queryList(ErpTimesheetPreAllocBo bo); + /** + * 查询当前部门指定月份、来源预投项目的员工级可分配明细 + * + * @param monthCode 月份编码(YYYYMM) + * @param projectId 来源预投项目ID + * @return 员工级可分配明细 + */ + PreAllocDetailVo getStaffAllocDetails(String monthCode, Long projectId); + /** * 新增预投工时分配 * @@ -58,6 +75,14 @@ public interface IErpTimesheetPreAllocService { */ Boolean updateByBo(ErpTimesheetPreAllocBo bo); + /** + * 保存预投工时分配并回写月汇总明细 + * + * @param bo 预投工时分配 + * @return 是否保存成功 + */ + Boolean saveAlloc(ErpTimesheetPreAllocBo bo); + /** * 校验并批量删除预投工时分配信息 * diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpTimesheetPreAllocServiceImpl.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpTimesheetPreAllocServiceImpl.java index 674a404a..61a545b0 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpTimesheetPreAllocServiceImpl.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpTimesheetPreAllocServiceImpl.java @@ -1,24 +1,52 @@ package org.dromara.oa.erp.service.impl; -import org.dromara.common.core.utils.MapstructUtils; -import org.dromara.common.core.utils.StringUtils; - import org.dromara.common.mybatis.core.page.TableDataInfo; - import org.dromara.common.mybatis.core.page.PageQuery; - import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.github.yulichang.toolkit.JoinWrappers; import com.github.yulichang.wrapper.MPJLambdaWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.dromara.oa.erp.domain.bo.ErpTimesheetPreAllocBo; -import org.dromara.oa.erp.domain.vo.ErpTimesheetPreAllocVo; +import org.apache.dubbo.config.annotation.DubboReference; +import org.dromara.common.core.exception.ServiceException; +import org.dromara.common.core.utils.StringUtils; +import org.dromara.common.mybatis.core.page.PageQuery; +import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.satoken.utils.LoginHelper; +import org.dromara.oa.erp.domain.ErpProjectInfo; import org.dromara.oa.erp.domain.ErpTimesheetPreAlloc; +import org.dromara.oa.erp.domain.ErpTimesheetPreAllocDetail; +import org.dromara.oa.erp.domain.ErpTimesheetSummary; +import org.dromara.oa.erp.domain.ErpTimesheetSummaryDetail; +import org.dromara.oa.erp.domain.bo.ErpTimesheetPreAllocBo; +import org.dromara.oa.erp.domain.bo.PreAllocStaffAllocBo; +import org.dromara.oa.erp.domain.bo.PreAllocTargetBo; +import org.dromara.oa.erp.domain.vo.ErpTimesheetPreAllocVo; +import org.dromara.oa.erp.domain.vo.PreAllocDetailVo; +import org.dromara.oa.erp.domain.vo.PreAllocStaffAllocVo; +import org.dromara.oa.erp.domain.vo.PreAllocTargetVo; +import org.dromara.oa.erp.mapper.ErpProjectInfoMapper; +import org.dromara.oa.erp.mapper.ErpTimesheetPreAllocDetailMapper; import org.dromara.oa.erp.mapper.ErpTimesheetPreAllocMapper; +import org.dromara.oa.erp.mapper.ErpTimesheetSummaryDetailMapper; +import org.dromara.oa.erp.mapper.ErpTimesheetSummaryMapper; import org.dromara.oa.erp.service.IErpTimesheetPreAllocService; +import org.dromara.system.api.RemoteCodeRuleService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Collection; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * 预投工时分配Service业务层处理 @@ -30,7 +58,25 @@ import java.util.Collection; @Service public class ErpTimesheetPreAllocServiceImpl implements IErpTimesheetPreAllocService { + private static final String DEL_FLAG_NORMAL = "0"; + private static final String PROJECT_WORK = "1"; + private static final String MANUAL_ROW = "0"; + private static final String PRE_PROJECT_CATEGORY = "4"; + private static final Set TARGET_PROJECT_CATEGORIES = Set.of("1", "2"); + private static final String STATUS_NOT_ALLOCATED = "0"; + private static final String STATUS_PART_ALLOCATED = "1"; + private static final String STATUS_ALLOCATED = "2"; + private static final String APPLIED = "1"; + private static final Pattern MONTH_CODE_PATTERN = Pattern.compile("^\\d{6}$"); + private final ErpTimesheetPreAllocMapper baseMapper; + private final ErpTimesheetPreAllocDetailMapper preAllocDetailMapper; + private final ErpTimesheetSummaryMapper summaryMapper; + private final ErpTimesheetSummaryDetailMapper summaryDetailMapper; + private final ErpProjectInfoMapper projectInfoMapper; + + @DubboReference + private RemoteCodeRuleService remoteCodeRuleService; /** * 查询预投工时分配 @@ -39,23 +85,46 @@ public class ErpTimesheetPreAllocServiceImpl implements IErpTimesheetPreAllocSer * @return 预投工时分配 */ @Override - public ErpTimesheetPreAllocVo queryById(Long allocId){ + public ErpTimesheetPreAllocVo queryById(Long allocId) { return baseMapper.selectVoById(allocId); } - /** - * 分页查询预投工时分配列表 - * - * @param bo 查询条件 - * @param pageQuery 分页参数 - * @return 预投工时分配分页列表 - */ - @Override - public TableDataInfo queryPageList(ErpTimesheetPreAllocBo bo, PageQuery pageQuery) { - MPJLambdaWrapper lqw = buildQueryWrapper(bo); - Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); - return TableDataInfo.build(result); + /** + * 查询预投工时分配员工级详情 + * + * @param allocId 主键 + * @return 预投工时分配员工级详情 + */ + @Override + public PreAllocDetailVo queryDetailById(Long allocId) { + ErpTimesheetPreAlloc alloc = baseMapper.selectById(allocId); + if (alloc == null) { + throw new ServiceException("预投工时分配单不存在"); } + Long deptId = LoginHelper.getDeptId(); + if (alloc.getCreateDept() != null && !Objects.equals(alloc.getCreateDept(), deptId)) { + throw new ServiceException("只能查看当前登录部门的预投工时分配单"); + } + ErpTimesheetSummary summary = summaryMapper.selectById(alloc.getSummaryId()); + if (summary != null && !Objects.equals(summary.getDeptId(), deptId)) { + throw new ServiceException("只能查看当前登录部门的预投工时分配单"); + } + return getStaffAllocDetails(alloc.getMonthCode(), alloc.getProjectId()); + } + + /** + * 分页查询预投工时分配列表 + * + * @param bo 查询条件 + * @param pageQuery 分页参数 + * @return 预投工时分配分页列表 + */ + @Override + public TableDataInfo queryPageList(ErpTimesheetPreAllocBo bo, PageQuery pageQuery) { + MPJLambdaWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } /** * 查询符合条件的预投工时分配列表 @@ -70,25 +139,79 @@ public class ErpTimesheetPreAllocServiceImpl implements IErpTimesheetPreAllocSer } private MPJLambdaWrapper buildQueryWrapper(ErpTimesheetPreAllocBo bo) { - Map params = bo.getParams(); - MPJLambdaWrapper lqw = JoinWrappers.lambda(ErpTimesheetPreAlloc.class) - .selectAll(ErpTimesheetPreAlloc.class) - .eq(ErpTimesheetPreAlloc::getDelFlag, "0") - .eq(StringUtils.isNotBlank(bo.getAllocCode()), ErpTimesheetPreAlloc::getAllocCode, bo.getAllocCode()) - .eq(StringUtils.isNotBlank(bo.getMonthCode()), ErpTimesheetPreAlloc::getMonthCode, bo.getMonthCode()) - .eq(bo.getStandardMonthId() != null, ErpTimesheetPreAlloc::getStandardMonthId, bo.getStandardMonthId()) - .eq(bo.getProjectId() != null, ErpTimesheetPreAlloc::getProjectId, bo.getProjectId()) - .eq(StringUtils.isNotBlank(bo.getProjectCode()), ErpTimesheetPreAlloc::getProjectCode, bo.getProjectCode()) - .like(StringUtils.isNotBlank(bo.getProjectName()), ErpTimesheetPreAlloc::getProjectName, bo.getProjectName()) - .eq(bo.getSourceTotalHours() != null, ErpTimesheetPreAlloc::getSourceTotalHours, bo.getSourceTotalHours()) - .eq(bo.getAllocatedTotalHours() != null, ErpTimesheetPreAlloc::getAllocatedTotalHours, bo.getAllocatedTotalHours()) - .eq(bo.getStaffCount() != null, ErpTimesheetPreAlloc::getStaffCount, bo.getStaffCount()) - .eq(StringUtils.isNotBlank(bo.getAllocStatus()), ErpTimesheetPreAlloc::getAllocStatus, bo.getAllocStatus()) - .eq(StringUtils.isNotBlank(bo.getAppliedFlag()), ErpTimesheetPreAlloc::getAppliedFlag, bo.getAppliedFlag()) - .eq(bo.getSummaryId() != null, ErpTimesheetPreAlloc::getSummaryId, bo.getSummaryId()) - .eq(bo.getApplyTime() != null, ErpTimesheetPreAlloc::getApplyTime, bo.getApplyTime()) -; - return lqw; + Long deptId = LoginHelper.getDeptId(); + return JoinWrappers.lambda(ErpTimesheetPreAlloc.class) + .selectAll(ErpTimesheetPreAlloc.class) + .eq(ErpTimesheetPreAlloc::getDelFlag, DEL_FLAG_NORMAL) + .eq(deptId != null, ErpTimesheetPreAlloc::getCreateDept, deptId) + .eq(StringUtils.isNotBlank(bo.getAllocCode()), ErpTimesheetPreAlloc::getAllocCode, bo.getAllocCode()) + .eq(StringUtils.isNotBlank(bo.getMonthCode()), ErpTimesheetPreAlloc::getMonthCode, bo.getMonthCode()) + .eq(bo.getStandardMonthId() != null, ErpTimesheetPreAlloc::getStandardMonthId, bo.getStandardMonthId()) + .eq(bo.getProjectId() != null, ErpTimesheetPreAlloc::getProjectId, bo.getProjectId()) + .eq(StringUtils.isNotBlank(bo.getProjectCode()), ErpTimesheetPreAlloc::getProjectCode, bo.getProjectCode()) + .like(StringUtils.isNotBlank(bo.getProjectName()), ErpTimesheetPreAlloc::getProjectName, bo.getProjectName()) + .eq(StringUtils.isNotBlank(bo.getAllocStatus()), ErpTimesheetPreAlloc::getAllocStatus, bo.getAllocStatus()) + .eq(StringUtils.isNotBlank(bo.getAppliedFlag()), ErpTimesheetPreAlloc::getAppliedFlag, bo.getAppliedFlag()) + .eq(bo.getSummaryId() != null, ErpTimesheetPreAlloc::getSummaryId, bo.getSummaryId()) + .eq(bo.getApplyTime() != null, ErpTimesheetPreAlloc::getApplyTime, bo.getApplyTime()) + .orderByDesc(ErpTimesheetPreAlloc::getApplyTime) + .orderByDesc(ErpTimesheetPreAlloc::getAllocId); + } + + /** + * 查询当前部门指定月份、来源预投项目的员工级可分配明细 + * + * @param monthCode 月份编码(YYYYMM) + * @param projectId 来源预投项目ID + * @return 员工级可分配明细 + */ + @Override + public PreAllocDetailVo getStaffAllocDetails(String monthCode, Long projectId) { + validateMonthCode(monthCode); + if (projectId == null) { + throw new ServiceException("来源预投项目不能为空"); + } + Long deptId = LoginHelper.getDeptId(); + ErpTimesheetSummary summary = getSingleSummary(monthCode, deptId); + ErpProjectInfo sourceProject = getProject(projectId, "来源预投项目不存在"); + validateProjectCategory(sourceProject, PRE_PROJECT_CATEGORY, "来源项目必须是预投项目"); + + ErpTimesheetPreAlloc alloc = queryActiveAlloc(summary.getSummaryId(), projectId); + List staffSources = loadStaffSources(summary.getSummaryId(), sourceProject); + List staffVos = buildStaffVos(staffSources); + + PreAllocDetailVo detailVo = new PreAllocDetailVo(); + if (alloc != null) { + detailVo.setAllocId(alloc.getAllocId()); + detailVo.setAllocCode(alloc.getAllocCode()); + detailVo.setAppliedFlag(alloc.getAppliedFlag()); + detailVo.setApplyTime(alloc.getApplyTime()); + detailVo.setRemark(alloc.getRemark()); + } else { + detailVo.setAppliedFlag("0"); + } + detailVo.setMonthCode(monthCode); + detailVo.setStandardMonthId(summary.getStandardMonthId()); + detailVo.setSummaryId(summary.getSummaryId()); + detailVo.setProjectId(sourceProject.getProjectId()); + detailVo.setProjectCode(sourceProject.getProjectCode()); + detailVo.setProjectName(sourceProject.getProjectName()); + detailVo.setStaffAllocList(staffVos); + + BigDecimal sourceTotal = staffVos.stream() + .map(PreAllocStaffAllocVo::getSourceHours) + .reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal allocatedTotal = staffVos.stream() + .map(PreAllocStaffAllocVo::getAllocatedHours) + .reduce(BigDecimal.ZERO, BigDecimal::add); + detailVo.setSourceTotalHours(sourceTotal); + detailVo.setAllocatedTotalHours(allocatedTotal); + detailVo.setRemainingTotalHours(sourceTotal.subtract(allocatedTotal)); + detailVo.setStaffCount((int) staffVos.stream() + .filter(item -> item.getSourceHours().compareTo(BigDecimal.ZERO) > 0) + .count()); + detailVo.setAllocStatus(resolveAllocStatus(sourceTotal, allocatedTotal)); + return detailVo; } /** @@ -99,13 +222,7 @@ public class ErpTimesheetPreAllocServiceImpl implements IErpTimesheetPreAllocSer */ @Override public Boolean insertByBo(ErpTimesheetPreAllocBo bo) { - ErpTimesheetPreAlloc add = MapstructUtils.convert(bo, ErpTimesheetPreAlloc.class); - validEntityBeforeSave(add); - boolean flag = baseMapper.insert(add) > 0; - if (flag) { - bo.setAllocId(add.getAllocId()); - } - return flag; + return saveAlloc(bo); } /** @@ -116,16 +233,64 @@ public class ErpTimesheetPreAllocServiceImpl implements IErpTimesheetPreAllocSer */ @Override public Boolean updateByBo(ErpTimesheetPreAllocBo bo) { - ErpTimesheetPreAlloc update = MapstructUtils.convert(bo, ErpTimesheetPreAlloc.class); - validEntityBeforeSave(update); - return baseMapper.updateById(update) > 0; + return saveAlloc(bo); } /** - * 保存前的数据校验 + * 保存预投工时分配并回写月汇总明细 + * + * @param bo 预投工时分配 + * @return 是否保存成功 */ - private void validEntityBeforeSave(ErpTimesheetPreAlloc entity){ - //TODO 做一些数据校验,如唯一约束 + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean saveAlloc(ErpTimesheetPreAllocBo bo) { + validateMonthCode(bo.getMonthCode()); + if (bo.getProjectId() == null) { + throw new ServiceException("来源预投项目不能为空"); + } + + Long deptId = LoginHelper.getDeptId(); + ErpTimesheetSummary summary = getSingleSummary(bo.getMonthCode(), deptId); + ErpTimesheetSummary lockedSummary = lockSummary(summary.getSummaryId()); + if (!Objects.equals(lockedSummary.getDeptId(), deptId)) { + throw new ServiceException("只能维护当前登录部门的预投工时分配"); + } + + ErpProjectInfo sourceProject = getProject(bo.getProjectId(), "来源预投项目不存在"); + validateProjectCategory(sourceProject, PRE_PROJECT_CATEGORY, "来源项目必须是预投项目"); + List staffSources = loadStaffSources(lockedSummary.getSummaryId(), sourceProject); + BigDecimal sourceTotal = staffSources.stream() + .map(StaffSource::getSourceHours) + .reduce(BigDecimal.ZERO, BigDecimal::add); + if (sourceTotal.compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException("本月该预投项目没有可分配工时"); + } + + AllocationResult allocationResult = buildAllocationResult( + bo.getStaffAllocList(), staffSources, lockedSummary.getSummaryId(), sourceProject); + ErpTimesheetPreAlloc alloc = resolveAlloc(bo.getAllocId(), lockedSummary.getSummaryId(), sourceProject.getProjectId(), deptId); + boolean isNew = alloc == null; + if (isNew) { + alloc = new ErpTimesheetPreAlloc(); + alloc.setAllocCode(generateAllocCode()); + alloc.setCreateDept(deptId); + } else if (StringUtils.isBlank(alloc.getAllocCode())) { + alloc.setAllocCode(generateAllocCode()); + } + + fillAlloc(alloc, bo, lockedSummary, sourceProject, allocationResult); + if (isNew) { + baseMapper.insert(alloc); + } else { + baseMapper.updateById(alloc); + } + + rewriteSummaryDetails(lockedSummary.getSummaryId(), sourceProject.getProjectId(), allocationResult.getSummaryDetails()); + rewritePreAllocDetails(alloc, sourceProject, allocationResult.getTargetAggregations()); + recalculateSummary(lockedSummary.getSummaryId()); + bo.setAllocId(alloc.getAllocId()); + return true; } /** @@ -136,10 +301,477 @@ public class ErpTimesheetPreAllocServiceImpl implements IErpTimesheetPreAllocSer * @return 是否删除成功 */ @Override + @Transactional(rollbackFor = Exception.class) public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { - if(isValid){ - //TODO 做一些业务上的校验,判断是否需要校验 + if (ids == null || ids.isEmpty()) { + return false; } + List allocList = baseMapper.selectList(Wrappers.lambdaQuery() + .in(ErpTimesheetPreAlloc::getAllocId, ids) + .eq(ErpTimesheetPreAlloc::getDelFlag, DEL_FLAG_NORMAL)); + if (allocList.size() != ids.size()) { + throw new ServiceException("部分预投工时分配单不存在或已删除"); + } + Long deptId = LoginHelper.getDeptId(); + for (ErpTimesheetPreAlloc alloc : allocList) { + if (APPLIED.equals(alloc.getAppliedFlag())) { + throw new ServiceException("已回写月汇总的预投工时分配单不允许删除"); + } + if (alloc.getCreateDept() != null && !Objects.equals(alloc.getCreateDept(), deptId)) { + throw new ServiceException("只能删除当前登录部门的预投工时分配单"); + } + } + preAllocDetailMapper.delete(Wrappers.lambdaQuery() + .in(ErpTimesheetPreAllocDetail::getAllocId, ids)); return baseMapper.deleteByIds(ids) > 0; } + + private void validateMonthCode(String monthCode) { + if (StringUtils.isBlank(monthCode) || !MONTH_CODE_PATTERN.matcher(monthCode).matches()) { + throw new ServiceException("月份编码必须是YYYYMM格式"); + } + } + + private ErpTimesheetSummary getSingleSummary(String monthCode, Long deptId) { + if (deptId == null) { + throw new ServiceException("当前登录用户未绑定部门,无法进行预投工时分配"); + } + List summaries = summaryMapper.selectList(Wrappers.lambdaQuery() + .eq(ErpTimesheetSummary::getMonthCode, monthCode) + .eq(ErpTimesheetSummary::getDeptId, deptId) + .eq(ErpTimesheetSummary::getDelFlag, DEL_FLAG_NORMAL)); + if (summaries.isEmpty()) { + throw new ServiceException("当前部门该月份的月汇总工时不存在"); + } + if (summaries.size() > 1) { + throw new ServiceException("当前部门该月份存在多个月汇总工时,请先修正月汇总数据"); + } + return summaries.get(0); + } + + private ErpTimesheetSummary lockSummary(Long summaryId) { + ErpTimesheetSummary summary = summaryMapper.selectOne(Wrappers.lambdaQuery() + .eq(ErpTimesheetSummary::getSummaryId, summaryId) + .eq(ErpTimesheetSummary::getDelFlag, DEL_FLAG_NORMAL) + .last("FOR UPDATE")); + if (summary == null) { + throw new ServiceException("月汇总工时不存在或已删除"); + } + return summary; + } + + private ErpProjectInfo getProject(Long projectId, String message) { + ErpProjectInfo project = projectInfoMapper.selectById(projectId); + if (project == null || !DEL_FLAG_NORMAL.equals(project.getDelFlag())) { + throw new ServiceException(message); + } + return project; + } + + private void validateProjectCategory(ErpProjectInfo project, String expectedCategory, String message) { + if (!expectedCategory.equals(project.getProjectCategory())) { + throw new ServiceException(message); + } + } + + private void validateTargetProject(ErpProjectInfo project) { + if (!TARGET_PROJECT_CATEGORIES.contains(project.getProjectCategory())) { + throw new ServiceException("目标项目只能选择物流或备件项目"); + } + } + + private ErpTimesheetPreAlloc queryActiveAlloc(Long summaryId, Long projectId) { + List allocList = baseMapper.selectList(Wrappers.lambdaQuery() + .eq(ErpTimesheetPreAlloc::getSummaryId, summaryId) + .eq(ErpTimesheetPreAlloc::getProjectId, projectId) + .eq(ErpTimesheetPreAlloc::getDelFlag, DEL_FLAG_NORMAL)); + if (allocList.size() > 1) { + throw new ServiceException("当前部门该月份该预投项目存在多张分配单,请先修正分配单数据"); + } + return allocList.isEmpty() ? null : allocList.get(0); + } + + private List loadStaffSources(Long summaryId, ErpProjectInfo sourceProject) { + List detailList = summaryDetailMapper.selectList(Wrappers.lambdaQuery() + .eq(ErpTimesheetSummaryDetail::getSummaryId, summaryId) + .eq(ErpTimesheetSummaryDetail::getOriginalProjectId, sourceProject.getProjectId()) + .eq(ErpTimesheetSummaryDetail::getIsProject, PROJECT_WORK) + .eq(ErpTimesheetSummaryDetail::getDelFlag, DEL_FLAG_NORMAL) + .orderByAsc(ErpTimesheetSummaryDetail::getSortOrder) + .orderByAsc(ErpTimesheetSummaryDetail::getSummaryDetailId)); + + Map> staffMap = detailList.stream() + .filter(detail -> detail.getStaffUserId() != null) + .collect(Collectors.groupingBy( + ErpTimesheetSummaryDetail::getStaffUserId, + LinkedHashMap::new, + Collectors.toList())); + + List result = new ArrayList<>(); + for (Map.Entry> entry : staffMap.entrySet()) { + List rows = entry.getValue(); + List positiveOriginalHours = rows.stream() + .map(item -> nvl(item.getOriginalHours())) + .filter(value -> value.compareTo(BigDecimal.ZERO) > 0) + .collect(Collectors.toList()); + long distinctPositiveCount = positiveOriginalHours.stream() + .map(this::decimalKey) + .distinct() + .count(); + if (distinctPositiveCount > 1) { + String staffName = rows.get(0).getStaffName(); + throw new ServiceException("员工[" + staffName + "]同一预投项目存在多个原始工时,请先修正月汇总数据"); + } + + BigDecimal sourceHours = positiveOriginalHours.stream() + .max(Comparator.naturalOrder()) + .orElse(BigDecimal.ZERO); + StaffSource staffSource = new StaffSource(); + staffSource.setStaffUserId(entry.getKey()); + staffSource.setStaffName(rows.get(0).getStaffName()); + staffSource.setSourceHours(sourceHours); + staffSource.setExistingRows(rows); + result.add(staffSource); + } + return result; + } + + private List buildStaffVos(List staffSources) { + List staffVos = new ArrayList<>(); + for (StaffSource source : staffSources) { + PreAllocStaffAllocVo staffVo = new PreAllocStaffAllocVo(); + staffVo.setStaffUserId(source.getStaffUserId()); + staffVo.setStaffName(source.getStaffName()); + staffVo.setSourceHours(source.getSourceHours()); + + Map targetMap = new LinkedHashMap<>(); + for (ErpTimesheetSummaryDetail row : source.getExistingRows()) { + BigDecimal adjustedHours = nvl(row.getAdjustedHours()); + if (adjustedHours.compareTo(BigDecimal.ZERO) <= 0) { + continue; + } + if (Objects.equals(row.getAdjustedProjectId(), row.getOriginalProjectId())) { + continue; + } + if (row.getAdjustedProjectId() == null) { + continue; + } + PreAllocTargetVo targetVo = targetMap.computeIfAbsent(row.getAdjustedProjectId(), targetProjectId -> { + PreAllocTargetVo vo = new PreAllocTargetVo(); + vo.setTargetProjectId(targetProjectId); + vo.setTargetProjectCode(row.getAdjustedProjectCode()); + vo.setTargetProjectName(row.getAdjustedProjectName()); + vo.setAllocHours(BigDecimal.ZERO); + return vo; + }); + targetVo.setAllocHours(targetVo.getAllocHours().add(adjustedHours)); + } + staffVo.setAllocItems(new ArrayList<>(targetMap.values())); + BigDecimal allocatedHours = staffVo.getAllocItems().stream() + .map(PreAllocTargetVo::getAllocHours) + .reduce(BigDecimal.ZERO, BigDecimal::add); + staffVo.setAllocatedHours(allocatedHours); + staffVo.setRemainingHours(staffVo.getSourceHours().subtract(allocatedHours)); + staffVos.add(staffVo); + } + return staffVos; + } + + private AllocationResult buildAllocationResult(List inputList, + List staffSources, + Long summaryId, + ErpProjectInfo sourceProject) { + Map sourceMap = staffSources.stream() + .filter(item -> item.getSourceHours().compareTo(BigDecimal.ZERO) > 0) + .collect(Collectors.toMap(StaffSource::getStaffUserId, item -> item, (a, b) -> a, LinkedHashMap::new)); + Map> inputMap = buildInputMap(inputList, sourceMap); + Map projectCache = new HashMap<>(); + List newSummaryDetails = new ArrayList<>(); + Map targetAggregations = new LinkedHashMap<>(); + + int sortOrder = 1; + BigDecimal allocatedTotal = BigDecimal.ZERO; + for (StaffSource source : sourceMap.values()) { + List targetInputs = inputMap.getOrDefault(source.getStaffUserId(), Collections.emptyList()); + BigDecimal staffAllocated = BigDecimal.ZERO; + List targetAllocations = new ArrayList<>(); + Set usedTargetIds = new java.util.HashSet<>(); + + for (PreAllocTargetBo targetInput : targetInputs) { + if (targetInput == null) { + throw new ServiceException("员工分配明细不能为空"); + } + Long targetProjectId = targetInput.getTargetProjectId(); + if (targetProjectId == null) { + throw new ServiceException("目标项目不能为空"); + } + if (!usedTargetIds.add(targetProjectId)) { + throw new ServiceException("员工[" + source.getStaffName() + "]不能重复选择同一目标项目"); + } + BigDecimal allocHours = nvl(targetInput.getAllocHours()); + if (allocHours.compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException("员工[" + source.getStaffName() + "]分配工时必须大于0"); + } + ErpProjectInfo targetProject = projectCache.computeIfAbsent(targetProjectId, + id -> getProject(id, "目标项目不存在")); + validateTargetProject(targetProject); + + staffAllocated = staffAllocated.add(allocHours); + targetAllocations.add(new TargetAllocation(targetProject, allocHours)); + } + + if (staffAllocated.compareTo(source.getSourceHours()) > 0) { + throw new ServiceException("员工[" + source.getStaffName() + "]分配工时不能超过原预投工时"); + } + allocatedTotal = allocatedTotal.add(staffAllocated); + + boolean originalHoursWritten = false; + for (TargetAllocation allocation : targetAllocations) { + ErpTimesheetSummaryDetail detail = buildSummaryDetail( + summaryId, sortOrder++, sourceProject, source, allocation.getTargetProject(), allocation.getAllocHours(), + originalHoursWritten ? BigDecimal.ZERO : source.getSourceHours()); + originalHoursWritten = true; + newSummaryDetails.add(detail); + targetAggregations.computeIfAbsent(allocation.getTargetProject().getProjectId(), id -> { + TargetAggregation aggregation = new TargetAggregation(); + aggregation.setTargetProject(allocation.getTargetProject()); + aggregation.setAllocHours(BigDecimal.ZERO); + return aggregation; + }).addAllocHours(allocation.getAllocHours()); + } + + BigDecimal remainingHours = source.getSourceHours().subtract(staffAllocated); + if (remainingHours.compareTo(BigDecimal.ZERO) > 0) { + ErpTimesheetSummaryDetail detail = buildSummaryDetail( + summaryId, sortOrder++, sourceProject, source, sourceProject, remainingHours, + originalHoursWritten ? BigDecimal.ZERO : source.getSourceHours()); + newSummaryDetails.add(detail); + } + } + + AllocationResult result = new AllocationResult(); + result.setSourceTotal(sourceMap.values().stream() + .map(StaffSource::getSourceHours) + .reduce(BigDecimal.ZERO, BigDecimal::add)); + result.setAllocatedTotal(allocatedTotal); + result.setStaffCount(sourceMap.size()); + result.setSummaryDetails(newSummaryDetails); + result.setTargetAggregations(new ArrayList<>(targetAggregations.values())); + return result; + } + + private Map> buildInputMap(List inputList, + Map sourceMap) { + Map> inputMap = new HashMap<>(); + if (inputList == null) { + return inputMap; + } + Set seenStaffIds = new java.util.HashSet<>(); + for (PreAllocStaffAllocBo staffInput : inputList) { + if (staffInput == null || staffInput.getStaffUserId() == null) { + throw new ServiceException("员工分配信息不能为空"); + } + if (!seenStaffIds.add(staffInput.getStaffUserId())) { + throw new ServiceException("员工分配信息存在重复员工"); + } + StaffSource source = sourceMap.get(staffInput.getStaffUserId()); + if (source == null) { + throw new ServiceException("员工[" + staffInput.getStaffName() + "]不属于当前预投项目可分配范围"); + } + if (staffInput.getSourceHours() != null + && staffInput.getSourceHours().compareTo(source.getSourceHours()) != 0) { + throw new ServiceException("员工[" + source.getStaffName() + "]预投工时已变化,请重新查询后分配"); + } + inputMap.put(staffInput.getStaffUserId(), + staffInput.getAllocItems() == null ? Collections.emptyList() : staffInput.getAllocItems()); + } + return inputMap; + } + + private ErpTimesheetSummaryDetail buildSummaryDetail(Long summaryId, + int sortOrder, + ErpProjectInfo sourceProject, + StaffSource source, + ErpProjectInfo adjustedProject, + BigDecimal adjustedHours, + BigDecimal originalHours) { + ErpTimesheetSummaryDetail detail = new ErpTimesheetSummaryDetail(); + detail.setSummaryId(summaryId); + detail.setSortOrder(sortOrder); + detail.setStaffUserId(source.getStaffUserId()); + detail.setStaffName(source.getStaffName()); + detail.setIsProject(PROJECT_WORK); + detail.setOriginalProjectId(sourceProject.getProjectId()); + detail.setOriginalProjectCode(sourceProject.getProjectCode()); + detail.setOriginalProjectName(sourceProject.getProjectName()); + detail.setOriginalHours(originalHours); + detail.setAdjustedProjectId(adjustedProject.getProjectId()); + detail.setAdjustedProjectCode(adjustedProject.getProjectCode()); + detail.setAdjustedProjectName(adjustedProject.getProjectName()); + detail.setAdjustedHours(adjustedHours); + detail.setIsGenerated(MANUAL_ROW); + return detail; + } + + private ErpTimesheetPreAlloc resolveAlloc(Long allocId, Long summaryId, Long projectId, Long deptId) { + ErpTimesheetPreAlloc keyAlloc = queryActiveAlloc(summaryId, projectId); + if (allocId == null) { + return keyAlloc; + } + ErpTimesheetPreAlloc idAlloc = baseMapper.selectById(allocId); + if (idAlloc == null || !DEL_FLAG_NORMAL.equals(idAlloc.getDelFlag())) { + throw new ServiceException("预投工时分配单不存在"); + } + if (keyAlloc != null && !Objects.equals(keyAlloc.getAllocId(), allocId)) { + throw new ServiceException("当前部门该月份该预投项目已存在分配单"); + } + if (!Objects.equals(idAlloc.getSummaryId(), summaryId) || !Objects.equals(idAlloc.getProjectId(), projectId)) { + throw new ServiceException("不允许修改分配单的月份或来源预投项目"); + } + if (idAlloc.getCreateDept() != null && !Objects.equals(idAlloc.getCreateDept(), deptId)) { + throw new ServiceException("只能维护当前登录部门的预投工时分配单"); + } + return idAlloc; + } + + private void fillAlloc(ErpTimesheetPreAlloc alloc, + ErpTimesheetPreAllocBo bo, + ErpTimesheetSummary summary, + ErpProjectInfo sourceProject, + AllocationResult allocationResult) { + alloc.setMonthCode(summary.getMonthCode()); + alloc.setStandardMonthId(summary.getStandardMonthId()); + alloc.setProjectId(sourceProject.getProjectId()); + alloc.setProjectCode(sourceProject.getProjectCode()); + alloc.setProjectName(sourceProject.getProjectName()); + alloc.setSourceTotalHours(allocationResult.getSourceTotal()); + alloc.setAllocatedTotalHours(allocationResult.getAllocatedTotal()); + alloc.setStaffCount(allocationResult.getStaffCount()); + alloc.setAllocStatus(resolveAllocStatus(allocationResult.getSourceTotal(), allocationResult.getAllocatedTotal())); + alloc.setAppliedFlag(APPLIED); + alloc.setSummaryId(summary.getSummaryId()); + alloc.setApplyTime(new Date()); + alloc.setRemark(bo.getRemark()); + alloc.setCreateDept(summary.getDeptId()); + } + + private void rewriteSummaryDetails(Long summaryId, Long sourceProjectId, List details) { + summaryDetailMapper.delete(Wrappers.lambdaQuery() + .eq(ErpTimesheetSummaryDetail::getSummaryId, summaryId) + .eq(ErpTimesheetSummaryDetail::getOriginalProjectId, sourceProjectId) + .eq(ErpTimesheetSummaryDetail::getIsProject, PROJECT_WORK)); + if (!details.isEmpty()) { + summaryDetailMapper.insertBatch(details); + } + } + + private void rewritePreAllocDetails(ErpTimesheetPreAlloc alloc, + ErpProjectInfo sourceProject, + List aggregations) { + preAllocDetailMapper.delete(Wrappers.lambdaQuery() + .eq(ErpTimesheetPreAllocDetail::getAllocId, alloc.getAllocId())); + if (aggregations.isEmpty()) { + return; + } + List details = new ArrayList<>(); + int sortOrder = 1; + for (TargetAggregation aggregation : aggregations) { + ErpTimesheetPreAllocDetail detail = new ErpTimesheetPreAllocDetail(); + detail.setAllocId(alloc.getAllocId()); + detail.setSortOrder(sortOrder++); + detail.setOriginalProjectId(sourceProject.getProjectId()); + detail.setOriginalProjectCode(sourceProject.getProjectCode()); + detail.setOriginalProjectName(sourceProject.getProjectName()); + detail.setTargetProjectId(aggregation.getTargetProject().getProjectId()); + detail.setTargetProjectCode(aggregation.getTargetProject().getProjectCode()); + detail.setTargetProjectName(aggregation.getTargetProject().getProjectName()); + detail.setAllocHours(aggregation.getAllocHours()); + details.add(detail); + } + preAllocDetailMapper.insertBatch(details); + } + + private void recalculateSummary(Long summaryId) { + List details = summaryDetailMapper.selectList(Wrappers.lambdaQuery() + .eq(ErpTimesheetSummaryDetail::getSummaryId, summaryId) + .eq(ErpTimesheetSummaryDetail::getDelFlag, DEL_FLAG_NORMAL)); + BigDecimal totalProjectHours = details.stream() + .filter(item -> PROJECT_WORK.equals(item.getIsProject())) + .map(item -> nvl(item.getAdjustedHours())) + .reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal totalDeptHours = details.stream() + .filter(item -> !PROJECT_WORK.equals(item.getIsProject())) + .map(item -> nvl(item.getAdjustedHours())) + .reduce(BigDecimal.ZERO, BigDecimal::add); + long staffCount = details.stream() + .map(ErpTimesheetSummaryDetail::getStaffUserId) + .filter(Objects::nonNull) + .distinct() + .count(); + + ErpTimesheetSummary update = new ErpTimesheetSummary(); + update.setSummaryId(summaryId); + update.setTotalProjectHours(totalProjectHours); + update.setTotalDeptHours(totalDeptHours); + update.setTotalHours(totalProjectHours.add(totalDeptHours)); + update.setStaffCount((int) staffCount); + summaryMapper.updateById(update); + } + + private String resolveAllocStatus(BigDecimal sourceTotal, BigDecimal allocatedTotal) { + BigDecimal safeSource = nvl(sourceTotal); + BigDecimal safeAllocated = nvl(allocatedTotal); + if (safeAllocated.compareTo(BigDecimal.ZERO) <= 0) { + return STATUS_NOT_ALLOCATED; + } + if (safeAllocated.compareTo(safeSource) < 0) { + return STATUS_PART_ALLOCATED; + } + return STATUS_ALLOCATED; + } + + private String generateAllocCode() { + return remoteCodeRuleService.selectCodeRuleCode("1034"); + } + + private BigDecimal nvl(BigDecimal value) { + return value == null ? BigDecimal.ZERO : value; + } + + private String decimalKey(BigDecimal value) { + return nvl(value).stripTrailingZeros().toPlainString(); + } + + @lombok.Data + private static class StaffSource { + private Long staffUserId; + private String staffName; + private BigDecimal sourceHours = BigDecimal.ZERO; + private List existingRows = new ArrayList<>(); + } + + @lombok.Data + private static class TargetAllocation { + private final ErpProjectInfo targetProject; + private final BigDecimal allocHours; + } + + @lombok.Data + private static class TargetAggregation { + private ErpProjectInfo targetProject; + private BigDecimal allocHours = BigDecimal.ZERO; + + private void addAllocHours(BigDecimal hours) { + this.allocHours = this.allocHours.add(hours); + } + } + + @lombok.Data + private static class AllocationResult { + private BigDecimal sourceTotal = BigDecimal.ZERO; + private BigDecimal allocatedTotal = BigDecimal.ZERO; + private Integer staffCount = 0; + private List summaryDetails = new ArrayList<>(); + private List targetAggregations = new ArrayList<>(); + } }